Skip to content

Commit 183f413

Browse files
committed
feat(util): add Sbx.create() factory for entity instantiation
Eliminates need for convenience constructors in records. Sbx.create() auto-sets key and meta to null via reflection.
1 parent 3cba7fe commit 183f413

File tree

4 files changed

+105
-48
lines changed

4 files changed

+105
-48
lines changed

README.md

Lines changed: 18 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ Java 21+ client for SBX Cloud APIs. Spring Boot 3.x compatible.
1717
<dependency>
1818
<groupId>com.github.socobox</groupId>
1919
<artifactId>sbxcloud-lib-java</artifactId>
20-
<version>v0.0.11</version>
20+
<version>v0.0.13</version>
2121
</dependency>
2222
```
2323

@@ -29,7 +29,7 @@ repositories {
2929
}
3030
3131
dependencies {
32-
implementation 'com.github.socobox:sbxcloud-lib-java:v0.0.11'
32+
implementation 'com.github.socobox:sbxcloud-lib-java:v0.0.13'
3333
}
3434
```
3535

@@ -51,13 +51,7 @@ public record Contact(
5151
String name,
5252
String email,
5353
String status
54-
) implements SbxEntity {
55-
56-
// Constructor for creating new records (without key/meta)
57-
public Contact(String name, String email, String status) {
58-
this(null, null, name, email, status);
59-
}
60-
}
54+
) implements SbxEntity {}
6155
```
6256

6357
### 2. Initialize the Service
@@ -81,8 +75,8 @@ var sbx = SBXServiceFactory.builder()
8175
// Get typed repository
8276
SbxRepository<Contact> contacts = sbx.repository(Contact.class);
8377

84-
// Create (meta is ignored, null values are ignored)
85-
var contact = new Contact("John Doe", "john@example.com", "ACTIVE");
78+
// Create using Sbx.create() - automatically sets key and meta to null
79+
var contact = Sbx.create(Contact.class, "John Doe", "john@example.com", "ACTIVE");
8680
String key = contacts.save(contact);
8781

8882
// Read
@@ -211,22 +205,19 @@ The `@SbxModel` annotation automatically:
211205
- Ignores unknown JSON properties
212206
- Strips null values (enables partial updates)
213207

214-
### With Convenience Constructor
208+
### Creating Entities with Sbx.create()
209+
210+
Instead of manual convenience constructors, use `Sbx.create()`:
215211

216212
```java
217-
@SbxModel("contact")
218-
public record Contact(
219-
String key,
220-
SBXMeta meta,
221-
String name,
222-
String email
223-
) implements SbxEntity {
213+
// No need to add a convenience constructor to your record!
214+
// Sbx.create() automatically sets key and meta to null
224215

225-
// For creating new records
226-
public Contact(String name, String email) {
227-
this(null, null, name, email);
228-
}
229-
}
216+
var contact = Sbx.create(Contact.class, "John", "john@example.com");
217+
// Equivalent to: new Contact(null, null, "John", "john@example.com")
218+
219+
// Pass field values in order (excluding key and meta)
220+
String key = contacts.save(contact);
230221
```
231222

232223
### Without Repository (Manual Jackson)
@@ -269,6 +260,9 @@ sbx.delete("contact", List.of("k1", "k2", "k3"));
269260
```java
270261
import static com.sbxcloud.sbx.util.Sbx.*;
271262

263+
// Create entities (automatically sets key and meta to null)
264+
var contact = create(Contact.class, "John", "john@example.com", "ACTIVE");
265+
272266
// JSON conversion
273267
String json = toJson(entity);
274268
String pretty = toPrettyJson(entity);

src/main/java/com/sbxcloud/sbx/util/Sbx.java

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,12 @@
66
import com.fasterxml.jackson.databind.ObjectMapper;
77
import com.fasterxml.jackson.databind.SerializationFeature;
88
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
9+
import com.sbxcloud.sbx.annotation.SbxEntity;
910
import com.sbxcloud.sbx.jackson.SbxModule;
11+
import com.sbxcloud.sbx.model.SBXMeta;
1012

13+
import java.lang.reflect.Constructor;
14+
import java.lang.reflect.RecordComponent;
1115
import java.util.Map;
1216

1317
/**
@@ -106,6 +110,66 @@ public static <T> T fromMap(Map<String, Object> map, Class<T> type) {
106110
return MAPPER.convertValue(map, type);
107111
}
108112

113+
/**
114+
* Creates a new entity instance, automatically setting key and meta to null.
115+
* <p>
116+
* Pass only the data fields (excluding key and meta) in the order they appear
117+
* in the record definition.
118+
*
119+
* <pre>{@code
120+
* // Instead of:
121+
* new Contact(null, null, "John", "john@example.com", "ACTIVE")
122+
*
123+
* // Use:
124+
* Sbx.create(Contact.class, "John", "john@example.com", "ACTIVE")
125+
* }</pre>
126+
*
127+
* @param type the entity class (must be a record implementing SbxEntity)
128+
* @param values the field values (excluding key and meta)
129+
* @return new entity instance with key=null, meta=null
130+
*/
131+
@SuppressWarnings("unchecked")
132+
public static <T extends SbxEntity> T create(Class<T> type, Object... values) {
133+
if (!type.isRecord()) {
134+
throw new SbxUtilException("Class must be a record: " + type.getName(), null);
135+
}
136+
137+
RecordComponent[] components = type.getRecordComponents();
138+
Class<?>[] paramTypes = new Class<?>[components.length];
139+
Object[] args = new Object[components.length];
140+
141+
int valueIndex = 0;
142+
for (int i = 0; i < components.length; i++) {
143+
RecordComponent comp = components[i];
144+
paramTypes[i] = comp.getType();
145+
146+
// Skip key and meta - set to null
147+
if ("key".equals(comp.getName()) || "meta".equals(comp.getName())) {
148+
args[i] = null;
149+
} else {
150+
if (valueIndex >= values.length) {
151+
throw new SbxUtilException(
152+
"Not enough values provided. Expected " + (components.length - 2) +
153+
" but got " + values.length, null);
154+
}
155+
args[i] = values[valueIndex++];
156+
}
157+
}
158+
159+
if (valueIndex < values.length) {
160+
throw new SbxUtilException(
161+
"Too many values provided. Expected " + (components.length - 2) +
162+
" but got " + values.length, null);
163+
}
164+
165+
try {
166+
Constructor<T> constructor = type.getDeclaredConstructor(paramTypes);
167+
return constructor.newInstance(args);
168+
} catch (Exception e) {
169+
throw new SbxUtilException("Failed to create entity: " + type.getName(), e);
170+
}
171+
}
172+
109173
/**
110174
* Runtime exception for utility operations.
111175
*/

src/test/java/com/sbxcloud/sbx/SBXIntegrationTest.java

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import com.sbxcloud.sbx.model.InventoryHistory;
66
import com.sbxcloud.sbx.query.FindQuery;
77
import com.sbxcloud.sbx.repository.SbxRepository;
8+
import com.sbxcloud.sbx.util.Sbx;
89
import org.junit.jupiter.api.BeforeAll;
910
import org.junit.jupiter.api.Disabled;
1011
import org.junit.jupiter.api.MethodOrderer;
@@ -209,8 +210,9 @@ void testSimpleFindWithClass() {
209210
@Test
210211
@Order(12)
211212
void testCreateWithEntity() {
212-
// Create entity using constructor
213-
var entity = new InventoryHistory("test-entity-" + System.currentTimeMillis(), 20250126, 3.99, 100);
213+
// Create entity using Sbx.create() - no manual constructor needed
214+
var entity = Sbx.create(InventoryHistory.class,
215+
"test-entity-" + System.currentTimeMillis(), 20250126, 3.99, 100);
214216

215217
var response = sbx.create(entity);
216218

@@ -239,14 +241,14 @@ void testUpdateWithEntity() {
239241

240242
var existing = findResponse.results().get(0);
241243

242-
// Create updated entity with new price
244+
// Partial update - only key and changed field (price)
243245
var updated = new InventoryHistory(
244246
existing.key(),
245-
existing.meta(),
246-
existing.masterlist(),
247-
existing.week(),
248-
9.99, // new price
249-
existing.quantity()
247+
null, // meta is ignored
248+
null, // masterlist unchanged
249+
null, // week unchanged
250+
9.99, // new price
251+
null // quantity unchanged
250252
);
251253

252254
var response = sbx.update(updated);
@@ -283,8 +285,9 @@ void testDeleteWithEntity() {
283285
@Test
284286
@Order(15)
285287
void testDeleteWithClassAndKey() {
286-
// Create a new record to delete
287-
var entity = new InventoryHistory("delete-test-" + System.currentTimeMillis(), 20250126, 0.99, 1);
288+
// Create a new record to delete using Sbx.create()
289+
var entity = Sbx.create(InventoryHistory.class,
290+
"delete-test-" + System.currentTimeMillis(), 20250126, 0.99, 1);
288291
var createResponse = sbx.create(entity);
289292
assertTrue(createResponse.success());
290293

@@ -315,8 +318,9 @@ void testRepositoryFindAll() {
315318
void testRepositorySaveAndFindById() {
316319
SbxRepository<InventoryHistory> repo = sbx.repository(InventoryHistory.class);
317320

318-
// Create
319-
var entity = new InventoryHistory("repo-test-" + System.currentTimeMillis(), 20250126, 5.99, 25);
321+
// Create using Sbx.create()
322+
var entity = Sbx.create(InventoryHistory.class,
323+
"repo-test-" + System.currentTimeMillis(), 20250126, 5.99, 25);
320324
String key = repo.save(entity);
321325

322326
assertNotNull(key);

src/test/java/com/sbxcloud/sbx/model/InventoryHistory.java

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,22 +6,17 @@
66
/**
77
* Test model for inventory_history records in SBX domain 96.
88
* <p>
9-
* Single annotation does everything - no @JsonProperty, @JsonNaming, or @JsonIgnoreProperties needed!
9+
* No convenience constructor needed - use Sbx.create() instead:
10+
* <pre>{@code
11+
* var entity = Sbx.create(InventoryHistory.class, "masterlist", 20250126, 1.99, 100);
12+
* }</pre>
1013
*/
1114
@SbxModel("inventory_history")
1215
public record InventoryHistory(
13-
String key, // Auto-mapped to _KEY
14-
SBXMeta meta, // Auto-mapped to _META
16+
String key,
17+
SBXMeta meta,
1518
String masterlist,
1619
Integer week,
1720
Double price,
1821
Integer quantity
19-
) implements SbxEntity {
20-
21-
/**
22-
* Convenience constructor for creating new entities (without key/meta).
23-
*/
24-
public InventoryHistory(String masterlist, Integer week, Double price, Integer quantity) {
25-
this(null, null, masterlist, week, price, quantity);
26-
}
27-
}
22+
) implements SbxEntity {}

0 commit comments

Comments
 (0)