Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -10,22 +10,50 @@
*
* The Form-JS editor uses a "people" object with personId keys (e.g., {applicant: {...}, spouse: {...}}),
* while DMN models expect a "people" array with id fields (e.g., [{id: "applicant", ...}, {id: "spouse", ...}]).
*
* Enrollments are stored per-person as arrays of benefit strings (e.g., {applicant: {enrollments: ["SNAP", "Medicaid"]}}),
* while DMN models expect a flat enrollments array (e.g., [{personId: "applicant", benefit: "SNAP"}, ...]).
*/
public class FormDataTransformer {

/**
* Transforms form data by converting a "people" object (with personId keys) into a "people" array
* (with id fields). This is the reverse of the frontend's transformInputDefinitionSchema function.
* Transforms form data by applying all data transformations.
* Currently applies:
* 1. People transformation: converts people object to array with id fields
* 2. Enrollments transformation: extracts enrollments from people objects into flat array
*
* @param formData The form data from the user, potentially containing a "people" object
* @return A new Map with the "people" object converted to an array, or the original data if no transformation needed
* @param formData The form data from the user
* @return A new Map with all transformations applied
*/
@SuppressWarnings("unchecked")
public static Map<String, Object> transformFormData(Map<String, Object> formData) {
if (formData == null) {
return new HashMap<>();
}

// Apply each transformation in sequence
Map<String, Object> result = new HashMap<>(formData);
result = transformPeopleData(result);
result = transformEnrollmentsData(result);

return result;
}

/**
* Transforms a "people" object (with personId keys) into a "people" array (with id fields).
*
* Example:
* Input: { people: { applicant: { dateOfBirth: "1960-01-01" } } }
* Output: { people: [{ id: "applicant", dateOfBirth: "1960-01-01" }] }
*
* @param formData The form data potentially containing a "people" object
* @return A new Map with the "people" object converted to an array
*/
@SuppressWarnings("unchecked")
public static Map<String, Object> transformPeopleData(Map<String, Object> formData) {
if (formData == null) {
return new HashMap<>();
}

Object peopleValue = formData.get("people");

// If no people key, or it's already a List (array), return a copy of the original
Expand Down Expand Up @@ -64,4 +92,70 @@ public static Map<String, Object> transformFormData(Map<String, Object> formData

return result;
}

/**
* Extracts enrollments from people objects and creates a flat enrollments array.
* Must be called after transformPeopleData (expects people to be an array).
*
* Example:
* Input: { people: [{ id: "applicant", enrollments: ["SNAP", "Medicaid"] }] }
* Output: { people: [{ id: "applicant" }], enrollments: [{ personId: "applicant", benefit: "SNAP" }, { personId: "applicant", benefit: "Medicaid" }] }
*
* @param formData The form data with people as an array
* @return A new Map with enrollments extracted into a flat array
*/
@SuppressWarnings("unchecked")
public static Map<String, Object> transformEnrollmentsData(Map<String, Object> formData) {
if (formData == null) {
return new HashMap<>();
}

Object peopleValue = formData.get("people");

// If no people key or it's not a List, return a copy of the original
if (!(peopleValue instanceof List)) {
return new HashMap<>(formData);
}

List<Map<String, Object>> peopleArray = (List<Map<String, Object>>) peopleValue;
List<Map<String, Object>> enrollmentsList = new ArrayList<>();
List<Map<String, Object>> transformedPeopleArray = new ArrayList<>();

// Extract enrollments from each person
for (Map<String, Object> person : peopleArray) {
String personId = (String) person.get("id");
Object personEnrollments = person.get("enrollments");

// Create a copy of the person data without enrollments
Map<String, Object> personCopy = new HashMap<>(person);

if (personEnrollments instanceof List) {
// Remove enrollments from person object (DMN doesn't expect it there)
personCopy.remove("enrollments");

// Convert each enrollment string to an enrollment object
for (Object enrollment : (List<?>) personEnrollments) {
if (enrollment instanceof String) {
Map<String, Object> enrollmentEntry = new HashMap<>();
enrollmentEntry.put("personId", personId);
enrollmentEntry.put("benefit", enrollment);
enrollmentsList.add(enrollmentEntry);
}
}
}

transformedPeopleArray.add(personCopy);
}

// Create the result
Map<String, Object> result = new HashMap<>(formData);
result.put("people", transformedPeopleArray);

// Only add enrollments if we extracted any
if (!enrollmentsList.isEmpty()) {
result.put("enrollments", enrollmentsList);
}

return result;
}
}
141 changes: 124 additions & 17 deletions builder-api/src/main/java/org/acme/service/InputSchemaService.java
Original file line number Diff line number Diff line change
Expand Up @@ -42,15 +42,13 @@ public Set<String> extractAllInputPaths(List<Benefit> benefits) {
}

/**
* Transforms a CheckConfig's inputDefinition JSON Schema by converting the `people`
* array property into an object with personId-keyed properties nested under it.
*
* Example:
* Input: { people: { type: "array", items: { properties: { dateOfBirth: ... } } } }
* Output: { people: { type: "object", properties: { [personId]: { properties: { dateOfBirth: ... } } } } }
* 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 `people` transformed to an object with personId-keyed properties
* @return A new JsonNode with all transformations applied
*/
public JsonNode transformInputDefinitionSchema(CheckConfig checkConfig) {
JsonNode inputDefinition = checkConfig.getInputDefinition();
Expand All @@ -59,26 +57,49 @@ public JsonNode transformInputDefinitionSchema(CheckConfig checkConfig) {
return inputDefinition != null ? inputDefinition.deepCopy() : objectMapper.createObjectNode();
}

JsonNode properties = inputDefinition.get("properties");
JsonNode peopleProperty = properties.get("people");
boolean hasPeopleProperty = peopleProperty != null;

// Extract personId from parameters
Map<String, Object> parameters = checkConfig.getParameters();
String personId = parameters != null ? (String) parameters.get("personId") : null;

// If people property exists but no personId, return original (can't transform)
if (hasPeopleProperty && (personId == null || personId.isEmpty())) {
return inputDefinition.deepCopy();
// 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 (!hasPeopleProperty) {
return inputDefinition.deepCopy();
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 = inputDefinition.deepCopy();
ObjectNode transformedSchema = schema.deepCopy();
ObjectNode transformedProperties = (ObjectNode) transformedSchema.get("properties");

// Get the items schema from the people array
Expand All @@ -97,6 +118,92 @@ public JsonNode transformInputDefinitionSchema(CheckConfig checkConfig) {
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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -148,13 +148,15 @@ public EvaluationResult evaluateCheck(CheckConfig checkConfig, Map<String, Objec
);

// TODO: Need a safer way to validate the returned data is in the right format
String result = responseBody.get("checkResult").toString();
return EvaluationResult.fromStringIgnoreCase(result);
Object result = responseBody.get("checkResult");
if (result == null) {
return EvaluationResult.UNABLE_TO_DETERMINE;
}
return EvaluationResult.fromStringIgnoreCase(result.toString());
}
catch (Exception e){
Log.error(e);
return EvaluationResult.UNABLE_TO_DETERMINE;
}
}
}

Loading
Loading