-
Notifications
You must be signed in to change notification settings - Fork 6
Expand file tree
/
Copy pathInputSchemaService.java
More file actions
306 lines (255 loc) · 12.1 KB
/
InputSchemaService.java
File metadata and controls
306 lines (255 loc) · 12.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
package org.acme.service;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import jakarta.enterprise.context.ApplicationScoped;
import org.acme.model.domain.Benefit;
import org.acme.model.domain.CheckConfig;
import org.acme.model.domain.FormPath;
import java.util.*;
/**
* Service for transforming and extracting paths from JSON Schema input definitions.
*/
@ApplicationScoped
public class InputSchemaService {
private final ObjectMapper objectMapper = new ObjectMapper();
/**
* Extracts all unique input paths from all benefits in a screener.
*
* @param benefits List of benefits containing checks with inputDefinitions
* @return Set of unique dot-separated paths (e.g., "people.applicant.dateOfBirth")
*/
public Set<FormPath> extractAllInputPaths(List<Benefit> benefits) {
Set<FormPath> pathSet = new HashSet<>();
for (Benefit benefit : benefits) {
List<CheckConfig> checks = benefit.getChecks();
if (checks == null) continue;
for (CheckConfig check : checks) {
JsonNode transformedSchema = transformInputDefinitionSchema(check);
List<FormPath> paths = extractJsonSchemaPaths(transformedSchema);
pathSet.addAll(paths);
}
}
return pathSet;
}
/**
* Transforms a CheckConfig's inputDefinition JSON Schema by applying all schema transformations.
* Currently applies:
* 1. People transformation: converts people array to object keyed by personId
* 2. Enrollments transformation: moves enrollments under people.{personId}.enrollments
*
* @param checkConfig The CheckConfig containing inputDefinition and parameters
* @return A new JsonNode with all transformations applied
*/
public JsonNode transformInputDefinitionSchema(CheckConfig checkConfig) {
JsonNode inputDefinition = checkConfig.getInputDefinition();
if (inputDefinition == null || !inputDefinition.has("properties")) {
return inputDefinition != null ? inputDefinition.deepCopy() : objectMapper.createObjectNode();
}
// Extract personId from parameters
Map<String, Object> parameters = checkConfig.getParameters();
String personId = parameters != null ? (String) parameters.get("personId") : null;
// Apply each transformation in sequence
JsonNode schema = inputDefinition.deepCopy();
schema = transformPeopleSchema(schema, personId);
schema = transformEnrollmentsSchema(schema, personId);
return schema;
}
/**
* Transforms the `people` array property into an object with personId-keyed properties.
*
* Example:
* Input: { people: { type: "array", items: { properties: { dateOfBirth: ... } } } }
* Output: { people: { type: "object", properties: { [personId]: { properties: { dateOfBirth: ... } } } } }
*
* @param schema The JSON Schema to transform
* @param personId The personId to use as the key under people
* @return A new JsonNode with `people` transformed, or the original if no transformation needed
*/
public JsonNode transformPeopleSchema(JsonNode schema, String personId) {
if (schema == null || !schema.has("properties")) {
return schema != null ? schema.deepCopy() : objectMapper.createObjectNode();
}
JsonNode properties = schema.get("properties");
JsonNode peopleProperty = properties.get("people");
// If no people property, return a copy of the original schema
if (peopleProperty == null) {
return schema.deepCopy();
}
// If people property exists but no personId, return original (can't transform)
if (personId == null || personId.isEmpty()) {
return schema.deepCopy();
}
// Deep clone the schema to avoid mutations
ObjectNode transformedSchema = schema.deepCopy();
ObjectNode transformedProperties = (ObjectNode) transformedSchema.get("properties");
// Get the items schema from the people array
JsonNode itemsSchema = peopleProperty.get("items");
// Transform people from array to object with personId as a nested property
ObjectNode newPeopleSchema = objectMapper.createObjectNode();
newPeopleSchema.put("type", "object");
ObjectNode newPeopleProperties = objectMapper.createObjectNode();
if (itemsSchema != null) {
newPeopleProperties.set(personId, itemsSchema.deepCopy());
}
newPeopleSchema.set("properties", newPeopleProperties);
transformedProperties.set("people", newPeopleSchema);
return transformedSchema;
}
/**
* Transforms the `enrollments` array property by moving it under people.{personId}.enrollments
* as an array of strings (benefit names).
*
* Example:
* Input: { enrollments: { type: "array", items: { properties: { personId: ..., benefit: ... } } } }
* Output: { people: { type: "object", properties: { [personId]: { properties: { enrollments: { type: "array", items: { type: "string" } } } } } } }
*
* @param schema The JSON Schema to transform
* @param personId The personId to use as the key under people
* @return A new JsonNode with `enrollments` transformed, or the original if no transformation needed
*/
public JsonNode transformEnrollmentsSchema(JsonNode schema, String personId) {
if (schema == null || !schema.has("properties")) {
return schema != null ? schema.deepCopy() : objectMapper.createObjectNode();
}
JsonNode properties = schema.get("properties");
JsonNode enrollmentsProperty = properties.get("enrollments");
// If no enrollments property, return a copy of the original schema
if (enrollmentsProperty == null) {
return schema.deepCopy();
}
// If enrollments property exists but no personId, return original (can't transform)
if (personId == null || personId.isEmpty()) {
return schema.deepCopy();
}
// Deep clone the schema to avoid mutations
ObjectNode transformedSchema = schema.deepCopy();
ObjectNode transformedProperties = (ObjectNode) transformedSchema.get("properties");
// Remove the top-level enrollments property
transformedProperties.remove("enrollments");
// Create enrollments schema as array of strings
ObjectNode enrollmentsSchema = objectMapper.createObjectNode();
enrollmentsSchema.put("type", "array");
ObjectNode itemsSchema = objectMapper.createObjectNode();
itemsSchema.put("type", "string");
enrollmentsSchema.set("items", itemsSchema);
// Get or create the people property
JsonNode existingPeople = transformedProperties.get("people");
ObjectNode peopleSchema;
ObjectNode peopleProps;
if (existingPeople != null && existingPeople.has("properties")) {
// People already exists (from transformPeopleSchema)
peopleSchema = (ObjectNode) existingPeople;
peopleProps = (ObjectNode) peopleSchema.get("properties");
} else {
// Create new people structure
peopleSchema = objectMapper.createObjectNode();
peopleSchema.put("type", "object");
peopleProps = objectMapper.createObjectNode();
peopleSchema.set("properties", peopleProps);
transformedProperties.set("people", peopleSchema);
}
// Get or create the personId property under people
JsonNode existingPersonId = peopleProps.get(personId);
ObjectNode personIdSchema;
ObjectNode personIdProps;
if (existingPersonId != null && existingPersonId.has("properties")) {
// PersonId already exists
personIdSchema = (ObjectNode) existingPersonId;
personIdProps = (ObjectNode) personIdSchema.get("properties");
} else {
// Create new personId structure
personIdSchema = objectMapper.createObjectNode();
personIdSchema.put("type", "object");
personIdProps = objectMapper.createObjectNode();
personIdSchema.set("properties", personIdProps);
peopleProps.set(personId, personIdSchema);
}
// Add enrollments under the personId
personIdProps.set("enrollments", enrollmentsSchema);
return transformedSchema;
}
/**
* Extracts all property paths from a JSON Schema inputDefinition.
* Recursively traverses nested objects to build dot-separated paths.
* Excludes the top-level "parameters" property and "id" properties.
*
* @param jsonSchema The JSON Schema to parse
* @return List of dot-separated paths (e.g., ["people.applicant.dateOfBirth", "income"])
*/
public List<FormPath> extractJsonSchemaPaths(JsonNode jsonSchema) {
if (jsonSchema == null || !jsonSchema.has("properties")) {
return new ArrayList<>();
}
return traverseSchema(jsonSchema, "");
}
private List<FormPath> traverseSchema(JsonNode schema, String parentPath) {
List<FormPath> formPaths = new ArrayList<>();
if (schema == null || !schema.has("properties")) {
return formPaths;
}
JsonNode propertiesJsonNode = schema.get("properties");
Iterator<Map.Entry<String, JsonNode>> nestedProperties = propertiesJsonNode.properties().iterator();
while (nestedProperties.hasNext()) {
Map.Entry<String, JsonNode> currentProperty = nestedProperties.next();
String propKey = currentProperty.getKey();
JsonNode propValue = currentProperty.getValue();
// Skip top-level "parameters" property
if (parentPath.isEmpty() && "parameters".equals(propKey)) {
continue;
}
// Skip "id" properties
if ("id".equals(propKey)) {
continue;
}
String currentPath = parentPath.isEmpty() ? propKey : parentPath + "." + propKey;
String currentType = getEffectiveType(propValue);
// If this property has nested properties, recurse into it
if (propValue.has("properties")) {
formPaths.addAll(traverseSchema(propValue, currentPath));
} else if ("array".equals(currentType) && propValue.has("items")) {
// Handle arrays - recurse into items schema with the current path
JsonNode itemsSchema = propValue.get("items");
if (itemsSchema.has("properties")) {
formPaths.addAll(traverseSchema(itemsSchema, currentPath));
} else {
// Array of primitives - add the path
String itemType = getType(itemsSchema);
formPaths.add(new FormPath(currentPath, "array:" + (itemType != null ? itemType : "any")));
}
} else {
// Leaf property - add the path
formPaths.add(new FormPath(currentPath, currentType));
}
}
return formPaths;
}
/**
* Determines the effective type of a JSON Schema property, considering format hints.
* For example, a string with format "date" returns "date" instead of "string".
*/
private String getEffectiveType(JsonNode schema) {
if (schema == null) {
return "any";
}
String type = getType(schema);
if (type == null) {
return "any";
}
// Check for format hints that provide more specific type info
if (type.equals("string") && schema.has("format")) {
String format = schema.get("format").asText();
// Common date/time formats
if ("date".equals(format) || "date-time".equals(format) || "time".equals(format)) {
return format;
}
}
return type;
}
private String getType(JsonNode schema) {
if (schema == null || !schema.has("type")) {
return null;
}
return schema.get("type").asText();
}
}