Skip to content

Commit 2c3e57f

Browse files
committed
feat(input_schema): Input sub-schema
1 parent 713018b commit 2c3e57f

File tree

4 files changed

+544
-19
lines changed

4 files changed

+544
-19
lines changed

packages/input_schema/src/input_schema.ts

Lines changed: 47 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,11 @@ export function parseAjvError(
3838
let fieldKey: string;
3939
let message: string;
4040

41+
const cleanPropertyName = (name: string) => {
42+
// remove leading and trailing slashes and replace remaining slashes with dots
43+
return name.replace(/^\/|\/$/g, '').replace(/\//g, '.');
44+
};
45+
4146
// If error is with keyword type, it means that type of input is incorrect
4247
// this can mean that provided value is null
4348
if (error.keyword === 'type') {
@@ -48,20 +53,20 @@ export function parseAjvError(
4853
}
4954
message = m('inputSchema.validation.generic', { rootName, fieldKey, message: error.message });
5055
} else if (error.keyword === 'required') {
51-
fieldKey = error.params.missingProperty;
56+
fieldKey = cleanPropertyName(`${error.instancePath}/${error.params.missingProperty}`);
5257
message = m('inputSchema.validation.required', { rootName, fieldKey });
5358
} else if (error.keyword === 'additionalProperties') {
54-
fieldKey = error.params.additionalProperty;
59+
fieldKey = cleanPropertyName(`${error.instancePath}/${error.params.additionalProperty}`);
5560
message = m('inputSchema.validation.additionalProperty', { rootName, fieldKey });
5661
} else if (error.keyword === 'enum') {
57-
fieldKey = error.instancePath.split('/').pop()!;
62+
fieldKey = cleanPropertyName(error.instancePath);
5863
const errorMessage = `${error.message}: "${error.params.allowedValues.join('", "')}"`;
5964
message = m('inputSchema.validation.generic', { rootName, fieldKey, message: errorMessage });
6065
} else if (error.keyword === 'const') {
61-
fieldKey = error.instancePath.split('/').pop()!;
66+
fieldKey = cleanPropertyName(error.instancePath);
6267
message = m('inputSchema.validation.generic', { rootName, fieldKey, message: error.message });
6368
} else {
64-
fieldKey = error.instancePath.split('/').pop()!;
69+
fieldKey = cleanPropertyName(error.instancePath);
6570
message = m('inputSchema.validation.generic', { rootName, fieldKey, message: error.message });
6671
}
6772

@@ -93,10 +98,19 @@ function validateBasicStructure(validator: Ajv, obj: Record<string, unknown>): a
9398
/**
9499
* Validates particular field against it's schema.
95100
*/
96-
function validateField(validator: Ajv, fieldSchema: Record<string, unknown>, fieldKey: string): asserts fieldSchema is FieldDefinition {
101+
function validateField(validator: Ajv, fieldSchema: Record<string, unknown>, fieldKey: string, subField = false): asserts fieldSchema is FieldDefinition {
97102
const matchingDefinitions = Object
98103
.values<any>(definitions) // cast as any, as the code in first branch seems to be invalid
99104
.filter((definition) => {
105+
if (!subField && definition.title.startsWith('Sub-schema')) {
106+
// This is a sub-schema definition, so we skip it.
107+
return false;
108+
}
109+
if (subField && !definition.title.startsWith('Sub-schema')) {
110+
// This is a normal definition, so we skip it.
111+
return false;
112+
}
113+
100114
return definition.properties.type.enum
101115
// This is a normal case where fieldSchema.type can be only one possible value matching definition.properties.type.enum.0
102116
? definition.properties.type.enum[0] === fieldSchema.type
@@ -110,9 +124,18 @@ function validateField(validator: Ajv, fieldSchema: Record<string, unknown>, fie
110124
throw new Error(`Input schema is not valid (${errorMessage})`);
111125
}
112126

127+
// When validating against schema of one definition, the definition can reference other definitions.
128+
// So we need to add all of them to the schema.
129+
function enhanceDefinition(definition: any) {
130+
return {
131+
...definition,
132+
definitions,
133+
};
134+
}
135+
113136
// If there is only one matching then we are done and simply compare it.
114137
if (matchingDefinitions.length === 1) {
115-
validateAgainstSchemaOrThrow(validator, fieldSchema, matchingDefinitions[0], `schema.properties.${fieldKey}`);
138+
validateAgainstSchemaOrThrow(validator, fieldSchema, enhanceDefinition(matchingDefinitions[0]), `schema.properties.${fieldKey}`);
116139
return;
117140
}
118141

@@ -121,30 +144,41 @@ function validateField(validator: Ajv, fieldSchema: Record<string, unknown>, fie
121144
if ((fieldSchema as StringFieldDefinition).enum) {
122145
const definition = matchingDefinitions.filter((item) => !!item.properties.enum).pop();
123146
if (!definition) throw new Error('Input schema validation failed to find "enum property" definition');
124-
validateAgainstSchemaOrThrow(validator, fieldSchema, definition, `schema.properties.${fieldKey}.enum`);
147+
validateAgainstSchemaOrThrow(validator, fieldSchema, enhanceDefinition(definition), `schema.properties.${fieldKey}.enum`);
125148
return;
126149
}
127150
// If the definition contains "resourceType" property then it's resource type.
128151
if ((fieldSchema as CommonResourceFieldDefinition<unknown>).resourceType) {
129152
const definition = matchingDefinitions.filter((item) => !!item.properties.resourceType).pop();
130153
if (!definition) throw new Error('Input schema validation failed to find "resource property" definition');
131-
validateAgainstSchemaOrThrow(validator, fieldSchema, definition, `schema.properties.${fieldKey}`);
154+
validateAgainstSchemaOrThrow(validator, fieldSchema, enhanceDefinition(definition), `schema.properties.${fieldKey}`);
132155
return;
133156
}
134157
// Otherwise we use the other definition.
135158
const definition = matchingDefinitions.filter((item) => !item.properties.enum && !item.properties.resourceType).pop();
136159
if (!definition) throw new Error('Input schema validation failed to find other than "enum property" definition');
137160

138-
validateAgainstSchemaOrThrow(validator, fieldSchema, definition, `schema.properties.${fieldKey}`);
161+
validateAgainstSchemaOrThrow(validator, fieldSchema, enhanceDefinition(definition), `schema.properties.${fieldKey}`);
162+
}
163+
164+
function validateSubFields(validator: Ajv, fieldSchema: InputSchemaBaseChecked, fieldKey: string) {
165+
Object.entries(fieldSchema.properties).forEach(([subFieldKey, subFieldSchema]) => (
166+
validateField(validator, subFieldSchema, `${fieldKey}.${subFieldKey}`, true)),
167+
);
139168
}
140169

141170
/**
142171
* Validates all properties in the input schema
143172
*/
144173
function validateProperties(inputSchema: InputSchemaBaseChecked, validator: Ajv): asserts inputSchema is InputSchema {
145-
Object.entries(inputSchema.properties).forEach(([fieldKey, fieldSchema]) => (
146-
validateField(validator, fieldSchema, fieldKey)),
147-
);
174+
Object.entries(inputSchema.properties).forEach(([fieldKey, fieldSchema]) => {
175+
// The sub-properties has to be validated first, so we got more relevant error messages.
176+
if ((fieldSchema as any).properties) {
177+
// If the field has sub-fields, we need to validate them as well.
178+
validateSubFields(validator, fieldSchema as any as InputSchemaBaseChecked, fieldKey);
179+
}
180+
validateField(validator, fieldSchema, fieldKey);
181+
});
148182
}
149183

150184
/**

0 commit comments

Comments
 (0)