Skip to content

Commit e2d90b7

Browse files
committed
chore: error handling
1 parent be9e919 commit e2d90b7

File tree

3 files changed

+275
-1
lines changed

3 files changed

+275
-1
lines changed

src/jsonLogic.js

Lines changed: 91 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export function createValidationChecker(schema) {
1212
const scopes = new Map();
1313

1414
function createScopes(jsonSchema, key = 'root') {
15+
const sampleEmptyObject = buildSampleEmptyObject(schema);
1516
scopes.set(key, createValidationsScope(jsonSchema));
1617
Object.entries(jsonSchema?.properties ?? {})
1718
.filter(([, property]) => property.type === 'object' || property.type === 'array')
@@ -21,6 +22,8 @@ export function createValidationChecker(schema) {
2122
}
2223
createScopes(property, key);
2324
});
25+
26+
validateInlineRules(jsonSchema, sampleEmptyObject);
2427
}
2528

2629
createScopes(schema);
@@ -44,12 +47,25 @@ function createValidationsScope(schema) {
4447

4548
const validations = Object.entries(logic.validations ?? {});
4649
const computedValues = Object.entries(logic.computedValues ?? {});
50+
const sampleEmptyObject = buildSampleEmptyObject(schema);
4751

4852
validations.forEach(([id, validation]) => {
53+
if (!validation.rule) {
54+
throw Error(`Missing rule for validation with id of: "${id}".`);
55+
}
56+
57+
checkRuleIntegrity(validation.rule, id, sampleEmptyObject);
58+
4959
validationMap.set(id, validation);
5060
});
5161

5262
computedValues.forEach(([id, computedValue]) => {
63+
if (!computedValue.rule) {
64+
throw Error(`Missing rule for computedValue with id of: "${id}".`);
65+
}
66+
67+
checkRuleIntegrity(computedValue.rule, id, sampleEmptyObject);
68+
5369
computedValuesMap.set(id, computedValue);
5470
});
5571

@@ -65,8 +81,11 @@ function createValidationsScope(schema) {
6581
const validation = validationMap.get(id);
6682
return evaluateValidation(validation.rule, values);
6783
},
68-
evaluateComputedValueRuleForField(id, values) {
84+
evaluateComputedValueRuleForField(id, values, fieldName) {
6985
const validation = computedValuesMap.get(id);
86+
if (validation === undefined)
87+
throw Error(`"${id}" computedValue in field "${fieldName}" doesn't exist.`);
88+
7089
return evaluateValidation(validation.rule, values);
7190
},
7291
evaluateComputedValueRuleInCondition(id, values) {
@@ -125,6 +144,9 @@ function replaceHandlebarsTemplates({
125144
} else if (typeof toReplace === 'object') {
126145
const { value, ...rules } = toReplace;
127146

147+
if (Object.keys(rules).length > 1 && !value)
148+
throw Error('Cannot define multiple rules without a template string with key `value`.');
149+
128150
const computedTemplateValue = Object.entries(rules).reduce((prev, [key, rule]) => {
129151
const computedValue = validations.getScope(parentID).evaluateValidation(rule, formValues);
130152
return prev.replaceAll(`{{${key}}}`, computedValue);
@@ -212,3 +234,71 @@ function handleNestedObjectForComputedValues(values, formValues, parentID, valid
212234
})
213235
);
214236
}
237+
238+
function buildSampleEmptyObject(schema = {}) {
239+
const sample = {};
240+
if (typeof schema !== 'object' || !schema.properties) {
241+
return schema;
242+
}
243+
244+
for (const key in schema.properties) {
245+
if (schema.properties[key].type === 'object') {
246+
sample[key] = buildSampleEmptyObject(schema.properties[key]);
247+
} else if (schema.properties[key].type === 'array') {
248+
const itemSchema = schema.properties[key].items;
249+
sample[key] = buildSampleEmptyObject(itemSchema);
250+
} else {
251+
sample[key] = true;
252+
}
253+
}
254+
255+
return sample;
256+
}
257+
258+
function validateInlineRules(jsonSchema, sampleEmptyObject) {
259+
const properties = (jsonSchema?.properties || jsonSchema?.items?.properties) ?? {};
260+
Object.entries(properties)
261+
.filter(([, property]) => property['x-jsf-logic-computedAttrs'] !== undefined)
262+
.forEach(([fieldName, property]) => {
263+
Object.entries(property['x-jsf-logic-computedAttrs'])
264+
.filter(([, value]) => typeof value === 'object')
265+
.forEach(([key, item]) => {
266+
Object.values(item).forEach((rule) => {
267+
checkRuleIntegrity(
268+
rule,
269+
fieldName,
270+
sampleEmptyObject,
271+
(item) =>
272+
`"${item.var}" in inline rule in property "${fieldName}.x-jsf-logic-computedAttrs.${key}" does not exist as a JSON schema property.`
273+
);
274+
});
275+
});
276+
});
277+
}
278+
279+
function checkRuleIntegrity(
280+
rule,
281+
id,
282+
data,
283+
errorMessage = (item) => `"${item.var}" in rule "${id}" does not exist as a JSON schema property.`
284+
) {
285+
Object.values(rule ?? {}).map((subRule) => {
286+
if (!Array.isArray(subRule) && subRule !== null && subRule !== undefined) return;
287+
subRule.map((item) => {
288+
const isVar = item !== null && typeof item === 'object' && Object.hasOwn(item, 'var');
289+
if (isVar) {
290+
const exists = jsonLogic.apply({ var: removeIndicesFromPath(item.var) }, data);
291+
if (exists === null) {
292+
throw Error(errorMessage(item));
293+
}
294+
} else {
295+
checkRuleIntegrity(item, id, data);
296+
}
297+
});
298+
});
299+
}
300+
301+
function removeIndicesFromPath(path) {
302+
const intermediatePath = path.replace(/\.\d+\./g, '.');
303+
return intermediatePath.replace(/\.\d+$/, '');
304+
}

src/tests/jsonLogic.test.js

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,15 @@ import {
44
createSchemaWithRulesOnFieldA,
55
createSchemaWithThreePropertiesWithRuleOnFieldA,
66
multiRuleSchema,
7+
schemaWithComputedAttributeThatDoesntExist,
8+
schemaWithComputedAttributeThatDoesntExistDescription,
9+
schemaWithComputedAttributeThatDoesntExistTitle,
710
schemaWithComputedAttributes,
811
schemaWithComputedAttributesAndErrorMessages,
12+
schemaWithInlinedRuleOnComputedAttributeThatReferencesUnknownVar,
13+
schemaWithMissingComputedValue,
14+
schemaWithMissingRule,
15+
schemaWithMissingValueInlineRule,
916
schemaWithNativeAndJSONLogicChecks,
1017
schemaWithNonRequiredField,
1118
schemaWithTwoRules,
@@ -82,6 +89,82 @@ describe('cross-value validations', () => {
8289
});
8390
});
8491

92+
describe('Incorrectly written schemas', () => {
93+
beforeEach(() => {
94+
jest.spyOn(console, 'error').mockImplementation(() => {});
95+
});
96+
97+
afterEach(() => {
98+
console.error.mockRestore();
99+
});
100+
101+
it('Should throw when theres a missing rule', () => {
102+
createHeadlessForm(schemaWithMissingRule, { strictInputType: false });
103+
expect(console.error).toHaveBeenCalledWith(
104+
'JSON Schema invalid!',
105+
Error('Missing rule for validation with id of: "a_greater_than_ten".')
106+
);
107+
});
108+
109+
it('Should throw when theres a missing computed value', () => {
110+
createHeadlessForm(schemaWithMissingComputedValue, { strictInputType: false });
111+
expect(console.error).toHaveBeenCalledWith(
112+
'JSON Schema invalid!',
113+
Error('Missing rule for computedValue with id of: "a_plus_ten".')
114+
);
115+
});
116+
117+
it('Should throw when theres an inline computed ruleset with no value.', () => {
118+
createHeadlessForm(schemaWithMissingValueInlineRule, { strictInputType: false });
119+
expect(console.error).toHaveBeenCalledWith(
120+
'JSON Schema invalid!',
121+
Error('Cannot define multiple rules without a template string with key `value`.')
122+
);
123+
});
124+
125+
it('On x-jsf-logic-computedAttrs, error if theres a value that does not exist.', () => {
126+
createHeadlessForm(schemaWithComputedAttributeThatDoesntExist, {
127+
strictInputType: false,
128+
});
129+
expect(console.error).toHaveBeenCalledWith(
130+
'JSON Schema invalid!',
131+
Error(`"iDontExist" computedValue in field "field_a" doesn't exist.`)
132+
);
133+
});
134+
135+
it('On x-jsf-logic-computedAttrs, error if theres a value that does not exist on a title.', () => {
136+
createHeadlessForm(schemaWithComputedAttributeThatDoesntExistTitle, {
137+
strictInputType: false,
138+
});
139+
expect(console.error).toHaveBeenCalledWith(
140+
'JSON Schema invalid!',
141+
Error(`"iDontExist" computedValue in field "field_a" doesn't exist.`)
142+
);
143+
});
144+
145+
it('On x-jsf-logic-computedAttrs, error if theres a value that does not exist on a description.', () => {
146+
createHeadlessForm(schemaWithComputedAttributeThatDoesntExistDescription, {
147+
strictInputType: false,
148+
});
149+
expect(console.error).toHaveBeenCalledWith(
150+
'JSON Schema invalid!',
151+
Error(`"iDontExist" computedValue in field "field_a" doesn't exist.`)
152+
);
153+
});
154+
155+
it('On an inline rule for a computedAttribute, error if theres a value referenced that does not exist', () => {
156+
createHeadlessForm(schemaWithInlinedRuleOnComputedAttributeThatReferencesUnknownVar, {
157+
strictInputType: false,
158+
});
159+
expect(console.error).toHaveBeenCalledWith(
160+
'JSON Schema invalid!',
161+
Error(
162+
'"IdontExist" in inline rule in property "field_a.x-jsf-logic-computedAttrs.title" does not exist as a JSON schema property.'
163+
)
164+
);
165+
});
166+
});
167+
85168
describe('Arithmetic: +, -, *, /', () => {
86169
it('multiple: field_a > field_b * 2', () => {
87170
const schema = createSchemaWithRulesOnFieldA({

src/tests/jsonLogicFixtures.js

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,40 @@ export const schemaWithNativeAndJSONLogicChecks = {
7474
required: ['field_a'],
7575
};
7676

77+
export const schemaWithMissingRule = {
78+
properties: {
79+
field_a: {
80+
type: 'number',
81+
'x-jsf-logic-validations': ['a_greater_than_ten'],
82+
},
83+
},
84+
'x-jsf-logic': {
85+
validations: {
86+
a_greater_than_ten: {
87+
errorMessage: 'Must be greater than 10',
88+
},
89+
},
90+
},
91+
required: [],
92+
};
93+
94+
export const schemaWithMissingComputedValue = {
95+
properties: {
96+
field_a: {
97+
type: 'number',
98+
'x-jsf-logic-computedAttrs': {
99+
title: '{{a_plus_ten}}',
100+
},
101+
},
102+
},
103+
'x-jsf-logic': {
104+
computedValues: {
105+
a_plus_ten: {},
106+
},
107+
},
108+
required: [],
109+
};
110+
77111
export const multiRuleSchema = {
78112
properties: {
79113
field_a: {
@@ -103,6 +137,25 @@ export const multiRuleSchema = {
103137
},
104138
};
105139

140+
export const schemaWithMissingValueInlineRule = {
141+
properties: {
142+
field_a: {
143+
type: 'number',
144+
'x-jsf-logic-computedAttrs': {
145+
title: {
146+
ruleOne: {
147+
'+': [1, 2],
148+
},
149+
ruleTwo: {
150+
'+': [3, 4],
151+
},
152+
},
153+
},
154+
},
155+
},
156+
required: [],
157+
};
158+
106159
export const schemaWithTwoRules = {
107160
properties: {
108161
field_a: {
@@ -178,6 +231,54 @@ export const schemaWithInlineRuleForComputedAttributeWithoutCopy = {
178231
},
179232
};
180233

234+
export const schemaWithComputedAttributeThatDoesntExist = {
235+
properties: {
236+
field_a: {
237+
type: 'number',
238+
'x-jsf-logic-computedAttrs': {
239+
default: 'iDontExist',
240+
},
241+
},
242+
},
243+
};
244+
245+
export const schemaWithInlinedRuleOnComputedAttributeThatReferencesUnknownVar = {
246+
properties: {
247+
field_a: {
248+
type: 'number',
249+
'x-jsf-logic-computedAttrs': {
250+
title: {
251+
rule: {
252+
'+': [{ var: 'IdontExist' }],
253+
},
254+
},
255+
},
256+
},
257+
},
258+
};
259+
260+
export const schemaWithComputedAttributeThatDoesntExistTitle = {
261+
properties: {
262+
field_a: {
263+
type: 'number',
264+
'x-jsf-logic-computedAttrs': {
265+
title: `this doesn't exist {{iDontExist}}`,
266+
},
267+
},
268+
},
269+
};
270+
271+
export const schemaWithComputedAttributeThatDoesntExistDescription = {
272+
properties: {
273+
field_a: {
274+
type: 'number',
275+
'x-jsf-logic-computedAttrs': {
276+
description: `this doesn't exist {{iDontExist}}`,
277+
},
278+
},
279+
},
280+
};
281+
181282
export const schemaWithInlineRuleForComputedAttributeWithOnlyTheRule = {
182283
properties: {
183284
field_a: {

0 commit comments

Comments
 (0)