json: test/fix additional props corner cases
This commit is contained in:
parent
bbd70800c8
commit
dd922a4da3
7 changed files with 671 additions and 612 deletions
|
@ -325,7 +325,9 @@ class SchemaConverter:
|
||||||
rule = ' | '.join((self._generate_constant_rule(v) for v in schema['enum']))
|
rule = ' | '.join((self._generate_constant_rule(v) for v in schema['enum']))
|
||||||
return self._add_rule(rule_name, rule)
|
return self._add_rule(rule_name, rule)
|
||||||
|
|
||||||
elif schema_type in (None, 'object') and ('properties' in schema or 'additionalProperties' in schema):
|
elif schema_type in (None, 'object') and \
|
||||||
|
('properties' in schema or \
|
||||||
|
('additionalProperties' in schema and schema['additionalProperties'] is not True)):
|
||||||
required = set(schema.get('required', []))
|
required = set(schema.get('required', []))
|
||||||
properties = list(schema.get('properties', {}).items())
|
properties = list(schema.get('properties', {}).items())
|
||||||
return self._add_rule(rule_name, self._build_object_rule(properties, required, name, schema.get('additionalProperties')))
|
return self._add_rule(rule_name, self._build_object_rule(properties, required, name, schema.get('additionalProperties')))
|
||||||
|
@ -385,11 +387,6 @@ class SchemaConverter:
|
||||||
elif schema_type in (None, 'string') and 'pattern' in schema:
|
elif schema_type in (None, 'string') and 'pattern' in schema:
|
||||||
return self._visit_pattern(schema['pattern'], rule_name)
|
return self._visit_pattern(schema['pattern'], rule_name)
|
||||||
|
|
||||||
elif (schema_type == 'object' and len(schema) == 1) or (len(schema) == 0):
|
|
||||||
for n in OBJECT_RULE_NAMES:
|
|
||||||
self._add_rule(n, PRIMITIVE_RULES[n])
|
|
||||||
return self._add_rule(rule_name, 'object')
|
|
||||||
|
|
||||||
elif schema_type in (None, 'string') and re.match(r'^uuid[1-5]?$', schema_format or ''):
|
elif schema_type in (None, 'string') and re.match(r'^uuid[1-5]?$', schema_format or ''):
|
||||||
return self._add_rule(
|
return self._add_rule(
|
||||||
'root' if rule_name == 'root' else schema_format,
|
'root' if rule_name == 'root' else schema_format,
|
||||||
|
@ -401,6 +398,11 @@ class SchemaConverter:
|
||||||
self._add_rule(t, r)
|
self._add_rule(t, r)
|
||||||
return schema_format + '-string'
|
return schema_format + '-string'
|
||||||
|
|
||||||
|
elif (schema_type == 'object') or (len(schema) == 0):
|
||||||
|
for n in OBJECT_RULE_NAMES:
|
||||||
|
self._add_rule(n, PRIMITIVE_RULES[n])
|
||||||
|
return self._add_rule(rule_name, 'object')
|
||||||
|
|
||||||
else:
|
else:
|
||||||
assert schema_type in PRIMITIVE_RULES, f'Unrecognized schema: {schema}'
|
assert schema_type in PRIMITIVE_RULES, f'Unrecognized schema: {schema}'
|
||||||
# TODO: support minimum, maximum, exclusiveMinimum, exclusiveMaximum at least for zero
|
# TODO: support minimum, maximum, exclusiveMinimum, exclusiveMaximum at least for zero
|
||||||
|
@ -424,9 +426,9 @@ class SchemaConverter:
|
||||||
required_props = [k for k in sorted_props if k in required]
|
required_props = [k for k in sorted_props if k in required]
|
||||||
optional_props = [k for k in sorted_props if k not in required]
|
optional_props = [k for k in sorted_props if k not in required]
|
||||||
|
|
||||||
if additional_properties:
|
if additional_properties == True or isinstance(additional_properties, dict):
|
||||||
sub_name = f'{name}{"-" if name else ""}additional'
|
sub_name = f'{name}{"-" if name else ""}additional'
|
||||||
value_rule = self.visit(additional_properties, f'{sub_name}-value')
|
value_rule = self.visit({} if additional_properties == True else additional_properties, f'{sub_name}-value')
|
||||||
prop_kv_rule_names["*"] = self._add_rule(
|
prop_kv_rule_names["*"] = self._add_rule(
|
||||||
f'{sub_name}-kv',
|
f'{sub_name}-kv',
|
||||||
self._add_rule('string', PRIMITIVE_RULES['string']) + f' ":" space {value_rule}'
|
self._add_rule('string', PRIMITIVE_RULES['string']) + f' ":" space {value_rule}'
|
||||||
|
|
|
@ -413,9 +413,9 @@ private:
|
||||||
optional_props.push_back(prop_name);
|
optional_props.push_back(prop_name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (additional_properties.is_object()) {
|
if (additional_properties.is_object() || (additional_properties.is_boolean() && additional_properties.get<bool>())) {
|
||||||
string sub_name = name + (name.empty() ? "" : "-") + "additional";
|
string sub_name = name + (name.empty() ? "" : "-") + "additional";
|
||||||
string value_rule = visit(additional_properties, sub_name + "-value");
|
string value_rule = visit(additional_properties.is_object() ? additional_properties : json::object(), sub_name + "-value");
|
||||||
string kv_rule = _add_rule(sub_name + "-kv", _add_rule("string", PRIMITIVE_RULES.at("string")) + " \":\" space " + value_rule);
|
string kv_rule = _add_rule(sub_name + "-kv", _add_rule("string", PRIMITIVE_RULES.at("string")) + " \":\" space " + value_rule);
|
||||||
prop_kv_rule_names["*"] = kv_rule;
|
prop_kv_rule_names["*"] = kv_rule;
|
||||||
optional_props.push_back("*");
|
optional_props.push_back("*");
|
||||||
|
@ -581,7 +581,8 @@ public:
|
||||||
}
|
}
|
||||||
return _add_rule(rule_name, join(enum_values.begin(), enum_values.end(), " | "));
|
return _add_rule(rule_name, join(enum_values.begin(), enum_values.end(), " | "));
|
||||||
} else if ((schema_type.is_null() || schema_type == "object")
|
} else if ((schema_type.is_null() || schema_type == "object")
|
||||||
&& (schema.contains("properties") || schema.contains("additionalProperties"))) {
|
&& (schema.contains("properties") ||
|
||||||
|
(schema.contains("additionalProperties") && schema["additionalProperties"] != true))) {
|
||||||
unordered_set<string> required;
|
unordered_set<string> required;
|
||||||
if (schema.contains("required") && schema["required"].is_array()) {
|
if (schema.contains("required") && schema["required"].is_array()) {
|
||||||
for (const auto& item : schema["required"]) {
|
for (const auto& item : schema["required"]) {
|
||||||
|
@ -666,11 +667,6 @@ public:
|
||||||
}
|
}
|
||||||
} else if ((schema_type.is_null() || schema_type == "string") && schema.contains("pattern")) {
|
} else if ((schema_type.is_null() || schema_type == "string") && schema.contains("pattern")) {
|
||||||
return _visit_pattern(schema["pattern"], rule_name);
|
return _visit_pattern(schema["pattern"], rule_name);
|
||||||
} else if (schema.empty() || (schema.size() == 1 && schema_type == "object")) {
|
|
||||||
for (const auto& n : OBJECT_RULE_NAMES) {
|
|
||||||
_add_rule(n, PRIMITIVE_RULES.at(n));
|
|
||||||
}
|
|
||||||
return _add_rule(rule_name, "object");
|
|
||||||
} else if ((schema_type.is_null() || schema_type == "string") && regex_match(schema_format, regex("^uuid[1-5]?$"))) {
|
} else if ((schema_type.is_null() || schema_type == "string") && regex_match(schema_format, regex("^uuid[1-5]?$"))) {
|
||||||
return _add_rule(rule_name == "root" ? "root" : schema_format, PRIMITIVE_RULES.at("uuid"));
|
return _add_rule(rule_name == "root" ? "root" : schema_format, PRIMITIVE_RULES.at("uuid"));
|
||||||
} else if ((schema_type.is_null() || schema_type == "string") && DATE_RULES.find(schema_format) != DATE_RULES.end()) {
|
} else if ((schema_type.is_null() || schema_type == "string") && DATE_RULES.find(schema_format) != DATE_RULES.end()) {
|
||||||
|
@ -678,6 +674,11 @@ public:
|
||||||
_add_rule(kv.first, kv.second);
|
_add_rule(kv.first, kv.second);
|
||||||
}
|
}
|
||||||
return schema_format + "-string";
|
return schema_format + "-string";
|
||||||
|
} else if (schema.empty() || schema_type == "object") {
|
||||||
|
for (const auto& n : OBJECT_RULE_NAMES) {
|
||||||
|
_add_rule(n, PRIMITIVE_RULES.at(n));
|
||||||
|
}
|
||||||
|
return _add_rule(rule_name, "object");
|
||||||
} else {
|
} else {
|
||||||
if (!schema_type.is_string() || PRIMITIVE_RULES.find(schema_type.get<string>()) == PRIMITIVE_RULES.end()) {
|
if (!schema_type.is_string() || PRIMITIVE_RULES.find(schema_type.get<string>()) == PRIMITIVE_RULES.end()) {
|
||||||
_errors.push_back("Unrecognized schema: " + schema.dump());
|
_errors.push_back("Unrecognized schema: " + schema.dump());
|
||||||
|
|
File diff suppressed because it is too large
Load diff
File diff suppressed because one or more lines are too long
|
@ -353,7 +353,9 @@ export class SchemaConverter {
|
||||||
} else if ('enum' in schema) {
|
} else if ('enum' in schema) {
|
||||||
const rule = schema.enum.map(v => this._generateConstantRule(v)).join(' | ');
|
const rule = schema.enum.map(v => this._generateConstantRule(v)).join(' | ');
|
||||||
return this._addRule(ruleName, rule);
|
return this._addRule(ruleName, rule);
|
||||||
} else if ((schemaType === undefined || schemaType === 'object') && ('properties' in schema || 'additionalProperties' in schema)) {
|
} else if ((schemaType === undefined || schemaType === 'object') &&
|
||||||
|
('properties' in schema ||
|
||||||
|
('additionalProperties' in schema && schema.additionalProperties !== true))) {
|
||||||
const required = new Set(schema.required || []);
|
const required = new Set(schema.required || []);
|
||||||
const properties = Object.entries(schema.properties ?? {});
|
const properties = Object.entries(schema.properties ?? {});
|
||||||
return this._addRule(ruleName, this._buildObjectRule(properties, required, name, schema.additionalProperties));
|
return this._addRule(ruleName, this._buildObjectRule(properties, required, name, schema.additionalProperties));
|
||||||
|
@ -427,7 +429,7 @@ export class SchemaConverter {
|
||||||
this._addRule(t, r);
|
this._addRule(t, r);
|
||||||
}
|
}
|
||||||
return schemaFormat + '-string';
|
return schemaFormat + '-string';
|
||||||
} else if ((schemaType === 'object' && Object.keys(schema).length === 1) || (Object.keys(schema).length === 0)) {
|
} else if ((schemaType === 'object') || (Object.keys(schema).length === 0)) {
|
||||||
for (const n of OBJECT_RULE_NAMES) {
|
for (const n of OBJECT_RULE_NAMES) {
|
||||||
this._addRule(n, PRIMITIVE_RULES[n]);
|
this._addRule(n, PRIMITIVE_RULES[n]);
|
||||||
}
|
}
|
||||||
|
@ -449,9 +451,6 @@ export class SchemaConverter {
|
||||||
const orderB = propOrder[b] || Infinity;
|
const orderB = propOrder[b] || Infinity;
|
||||||
return orderA - orderB || properties.findIndex(([k]) => k === a) - properties.findIndex(([k]) => k === b);
|
return orderA - orderB || properties.findIndex(([k]) => k === a) - properties.findIndex(([k]) => k === b);
|
||||||
});
|
});
|
||||||
// const sortedProps = properties.map(([name]) => name).sort(
|
|
||||||
// (a, b) => (propOrder[a] ?? Infinity) - (propOrder[b] ?? Infinity)
|
|
||||||
// );
|
|
||||||
|
|
||||||
const propKvRuleNames = {};
|
const propKvRuleNames = {};
|
||||||
for (const [propName, propSchema] of properties) {
|
for (const [propName, propSchema] of properties) {
|
||||||
|
@ -464,9 +463,9 @@ export class SchemaConverter {
|
||||||
const requiredProps = sortedProps.filter(k => required.has(k));
|
const requiredProps = sortedProps.filter(k => required.has(k));
|
||||||
const optionalProps = sortedProps.filter(k => !required.has(k));
|
const optionalProps = sortedProps.filter(k => !required.has(k));
|
||||||
|
|
||||||
if (typeof additionalProperties === 'object') {
|
if (typeof additionalProperties === 'object' || additionalProperties === true) {
|
||||||
const subName = `${name ?? ''}${name ? '-' : ''}additional`;
|
const subName = `${name ?? ''}${name ? '-' : ''}additional`;
|
||||||
const valueRule = this.visit(additionalProperties, `${subName}-value`);
|
const valueRule = this.visit(additionalProperties === true ? {} : additionalProperties, `${subName}-value`);
|
||||||
propKvRuleNames['*'] = this._addRule(
|
propKvRuleNames['*'] = this._addRule(
|
||||||
`${subName}-kv`,
|
`${subName}-kv`,
|
||||||
`${this._addRule('string', PRIMITIVE_RULES['string'])} ":" space ${valueRule}`);
|
`${this._addRule('string', PRIMITIVE_RULES['string'])} ":" space ${valueRule}`);
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
#
|
#
|
||||||
# ./examples/ts-type-to-grammar.sh "{a:string,b:string,c?:string}"
|
# ./examples/ts-type-to-grammar.sh "{a:string,b:string,c?:string}"
|
||||||
# python examples/json-schema-to-grammar.py https://raw.githubusercontent.com/SchemaStore/schemastore/master/src/schemas/json/tsconfig.json
|
# python examples/json-schema-to-grammar.py https://json.schemastore.org/tsconfig.json
|
||||||
#
|
#
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
|
|
|
@ -475,6 +475,64 @@ static void test_all(const string& lang, std::function<void(const TestCase&)> ru
|
||||||
)"""
|
)"""
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test({
|
||||||
|
SUCCESS,
|
||||||
|
"additional props (true)",
|
||||||
|
R"""({
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": true
|
||||||
|
})""",
|
||||||
|
R"""(
|
||||||
|
array ::= "[" space ( value ("," space value)* )? "]" space
|
||||||
|
boolean ::= ("true" | "false") space
|
||||||
|
null ::= "null" space
|
||||||
|
number ::= ("-"? ([0-9] | [1-9] [0-9]*)) ("." [0-9]+)? ([eE] [-+]? [0-9]+)? space
|
||||||
|
object ::= "{" space ( string ":" space value ("," space string ":" space value)* )? "}" space
|
||||||
|
root ::= object
|
||||||
|
space ::= " "?
|
||||||
|
string ::= "\"" (
|
||||||
|
[^"\\] |
|
||||||
|
"\\" (["\\/bfnrt] | "u" [0-9a-fA-F] [0-9a-fA-F] [0-9a-fA-F] [0-9a-fA-F])
|
||||||
|
)* "\"" space
|
||||||
|
value ::= object | array | string | number | boolean
|
||||||
|
)"""
|
||||||
|
});
|
||||||
|
|
||||||
|
test({
|
||||||
|
SUCCESS,
|
||||||
|
"additional props (implicit)",
|
||||||
|
R"""({
|
||||||
|
"type": "object"
|
||||||
|
})""",
|
||||||
|
R"""(
|
||||||
|
array ::= "[" space ( value ("," space value)* )? "]" space
|
||||||
|
boolean ::= ("true" | "false") space
|
||||||
|
null ::= "null" space
|
||||||
|
number ::= ("-"? ([0-9] | [1-9] [0-9]*)) ("." [0-9]+)? ([eE] [-+]? [0-9]+)? space
|
||||||
|
object ::= "{" space ( string ":" space value ("," space string ":" space value)* )? "}" space
|
||||||
|
root ::= object
|
||||||
|
space ::= " "?
|
||||||
|
string ::= "\"" (
|
||||||
|
[^"\\] |
|
||||||
|
"\\" (["\\/bfnrt] | "u" [0-9a-fA-F] [0-9a-fA-F] [0-9a-fA-F] [0-9a-fA-F])
|
||||||
|
)* "\"" space
|
||||||
|
value ::= object | array | string | number | boolean
|
||||||
|
)"""
|
||||||
|
});
|
||||||
|
|
||||||
|
test({
|
||||||
|
SUCCESS,
|
||||||
|
"empty w/o additional props",
|
||||||
|
R"""({
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false
|
||||||
|
})""",
|
||||||
|
R"""(
|
||||||
|
root ::= "{" space "}" space
|
||||||
|
space ::= " "?
|
||||||
|
)"""
|
||||||
|
});
|
||||||
|
|
||||||
test({
|
test({
|
||||||
SUCCESS,
|
SUCCESS,
|
||||||
"required + additional props",
|
"required + additional props",
|
||||||
|
@ -716,17 +774,20 @@ int main() {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
test_all("Python", [](const TestCase& tc) {
|
test_all("Python", [](const TestCase& tc) {
|
||||||
|
|
||||||
write("test-json-schema-input.tmp", tc.schema);
|
write("test-json-schema-input.tmp", tc.schema);
|
||||||
tc.verify_status(std::system(
|
tc.verify_status(std::system(
|
||||||
"python ./examples/json-schema-to-grammar.py test-json-schema-input.tmp > test-grammar-output.tmp") == 0 ? SUCCESS : FAILURE);
|
"python ./examples/json-schema-to-grammar.py test-json-schema-input.tmp > test-grammar-output.tmp") == 0 ? SUCCESS : FAILURE);
|
||||||
tc.verify(read("test-grammar-output.tmp"));
|
tc.verify(read("test-grammar-output.tmp"));
|
||||||
});
|
});
|
||||||
test_all("JavaScript", [](const TestCase& tc) {
|
test_all("JavaScript", [](const TestCase& tc) {
|
||||||
|
|
||||||
write("test-json-schema-input.tmp", tc.schema);
|
write("test-json-schema-input.tmp", tc.schema);
|
||||||
tc.verify_status(std::system(
|
tc.verify_status(std::system(
|
||||||
"node ./tests/run-json-schema-to-grammar.mjs test-json-schema-input.tmp > test-grammar-output.tmp") == 0 ? SUCCESS : FAILURE);
|
"node ./tests/run-json-schema-to-grammar.mjs test-json-schema-input.tmp > test-grammar-output.tmp") == 0 ? SUCCESS : FAILURE);
|
||||||
tc.verify(read("test-grammar-output.tmp"));
|
tc.verify(read("test-grammar-output.tmp"));
|
||||||
});
|
});
|
||||||
|
|
||||||
test_all("Check Expectations Validity", [](const TestCase& tc) {
|
test_all("Check Expectations Validity", [](const TestCase& tc) {
|
||||||
if (tc.expected_status == SUCCESS) {
|
if (tc.expected_status == SUCCESS) {
|
||||||
tc.verify_expectation_parseable();
|
tc.verify_expectation_parseable();
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue