json: unified properties order across optional & required

This commit is contained in:
ochafik 2024-06-26 09:39:15 +01:00
parent 139cc621e9
commit 757c4df0e0
5 changed files with 147 additions and 103 deletions

View File

@ -700,10 +700,9 @@ private:
const std::string & name,
const json & additional_properties)
{
std::vector<std::string> required_props;
std::vector<std::string> optional_props;
std::unordered_map<std::string, std::string> prop_kv_rule_names;
std::vector<std::string> prop_names;
prop_names.reserve(properties.size() + 1);
std::unordered_map<std::string, std::string> prop_kv_rule_names;
for (const auto & kv : properties) {
const auto &prop_name = kv.first;
const auto &prop_schema = kv.second;
@ -713,11 +712,6 @@ private:
name + (name.empty() ? "" : "-") + prop_name + "-kv",
format_literal(json(prop_name).dump()) + " space \":\" space " + prop_rule_name
);
if (required.find(prop_name) != required.end()) {
required_props.push_back(prop_name);
} else {
optional_props.push_back(prop_name);
}
prop_names.push_back(prop_name);
}
if ((additional_properties.is_boolean() && additional_properties.get<bool>()) || additional_properties.is_object()) {
@ -731,23 +725,11 @@ private:
: _add_rule(sub_name + "-k", _not_strings(prop_names));
std::string kv_rule = _add_rule(sub_name + "-kv", key_rule + " \":\" space " + value_rule);
prop_kv_rule_names["*"] = kv_rule;
optional_props.push_back("*");
prop_names.push_back("*");
}
std::string rule = "\"{\" space ";
for (size_t i = 0; i < required_props.size(); i++) {
if (i > 0) {
rule += " \",\" space ";
}
rule += prop_kv_rule_names[required_props[i]];
}
if (!optional_props.empty()) {
rule += " (";
if (!required_props.empty()) {
rule += " \",\" space ( ";
}
if (!prop_kv_rule_names.empty()) {
std::function<std::string(const std::vector<std::string> &, bool)> get_recursive_refs = [&](const std::vector<std::string> & ks, bool first_is_optional) {
std::string res;
if (ks.empty()) {
@ -755,11 +737,15 @@ private:
}
std::string k = ks[0];
std::string kv_rule_name = prop_kv_rule_names[k];
std::string comma_ref = "( \",\" space " + kv_rule_name + " )";
std::string comma_ref = "\",\" space " + kv_rule_name;
if (first_is_optional) {
res = comma_ref + (k == "*" ? "*" : "?");
if (required.find(k) == required.end()) {
res = "( " + comma_ref + (k == "*" ? " )*" : " )?");
} else {
res = comma_ref;
}
} else {
res = kv_rule_name + (k == "*" ? " " + comma_ref + "*" : "");
res = kv_rule_name + (k == "*" ? " ( " + comma_ref + " )*" : "");
}
if (ks.size() > 1) {
res += " " + _add_rule(
@ -770,16 +756,21 @@ private:
return res;
};
for (size_t i = 0; i < optional_props.size(); i++) {
if (i > 0) {
rule += " | ";
std::vector<std::string> alternatives;
auto has_required = false;
for (size_t i = 0; i < prop_names.size(); i++) {
alternatives.push_back(get_recursive_refs(std::vector<std::string>(prop_names.begin() + i, prop_names.end()), false));
if (required.find(prop_names[i]) != required.end()) {
has_required = true;
break;
}
rule += get_recursive_refs(std::vector<std::string>(optional_props.begin() + i, optional_props.end()), false);
}
if (!required_props.empty()) {
rule += " )";
auto alts = join(alternatives.begin(), alternatives.end(), " | ");
if (alternatives.size() > 1 || !has_required) {
rule += "( " + alts + (has_required ? " )" : " )?");
} else {
rule += alts;
}
rule += " )?";
}
rule += " \"}\" space";

View File

@ -6,6 +6,7 @@ import re
import sys
from typing import Any, List, Optional, Set, Tuple, Union
def _build_repetition(item_rule, min_items, max_items, separator_rule=None):
if min_items == 0 and max_items == 1:
@ -677,49 +678,48 @@ class SchemaConverter:
return n
def _build_object_rule(self, properties: List[Tuple[str, Any]], required: Set[str], name: str, additional_properties: Optional[Union[bool, Any]]):
prop_order = self._prop_order
# sort by position in prop_order (if specified) then by original order
sorted_props = [kv[0] for _, kv in sorted(enumerate(properties), key=lambda ikv: (prop_order.get(ikv[1][0], len(prop_order)), ikv[0]))]
prop_kv_rule_names = {}
prop_names = []
for prop_name, prop_schema in properties:
prop_rule_name = self.visit(prop_schema, f'{name}{"-" if name else ""}{prop_name}')
prop_kv_rule_names[prop_name] = self._add_rule(
f'{name}{"-" if name else ""}{prop_name}-kv',
fr'{self._format_literal(json.dumps(prop_name))} space ":" space {prop_rule_name}'
)
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]
prop_names.append(prop_name)
prop_order = self._prop_order
if prop_order:
# sort by position in prop_order (if specified) then by original order
prop_names.sort(key=lambda k: (prop_order.get(k, float('inf')), prop_names.index(k)))
if additional_properties is not None and additional_properties != False:
sub_name = f'{name}{"-" if name else ""}additional'
value_rule = self.visit(additional_properties, f'{sub_name}-value') if isinstance(additional_properties, dict) else \
self._add_primitive('value', PRIMITIVE_RULES['value'])
key_rule = self._add_primitive('string', PRIMITIVE_RULES['string']) if not sorted_props \
else self._add_rule(f'{sub_name}-k', self._not_strings(sorted_props))
key_rule = self._add_primitive('string', PRIMITIVE_RULES['string']) if not prop_names \
else self._add_rule(f'{sub_name}-k', self._not_strings(prop_names))
prop_kv_rule_names["*"] = self._add_rule(
f'{sub_name}-kv',
f'{key_rule} ":" space {value_rule}'
)
optional_props.append("*")
prop_names.append("*")
rule = '"{" space '
rule += ' "," space '.join(prop_kv_rule_names[k] for k in required_props)
if optional_props:
rule += ' ('
if required_props:
rule += ' "," space ( '
if prop_kv_rule_names:
def get_recursive_refs(ks, first_is_optional):
[k, *rest] = ks
kv_rule_name = prop_kv_rule_names[k]
comma_ref = f'( "," space {kv_rule_name} )'
comma_ref = f'"," space {kv_rule_name}'
if first_is_optional:
res = comma_ref + ('*' if k == '*' else '?')
if k not in required:
res = '( ' + comma_ref + (' )*' if k == '*' else ' )?')
else:
res = comma_ref
else:
res = kv_rule_name + (' ' + comma_ref + "*" if k == '*' else '')
res = kv_rule_name + (' ( ' + comma_ref + " )*" if k == '*' else '')
if len(rest) > 0:
res += ' ' + self._add_rule(
f'{name}{"-" if name else ""}{k}-rest',
@ -727,13 +727,19 @@ class SchemaConverter:
)
return res
rule += ' | '.join(
get_recursive_refs(optional_props[i:], first_is_optional=False)
for i in range(len(optional_props))
)
if required_props:
rule += ' )'
rule += ' )?'
alternatives = []
has_required = False
for i, k in enumerate(prop_names):
alternatives.append(get_recursive_refs(prop_names[i:], first_is_optional=False))
if k in required:
has_required = True
break
alts = ' | '.join(alternatives)
if len(alternatives) > 1 or not has_required:
rule += '( ' + alts + (' )' if has_required else ' )?')
else:
rule += alts
rule += ' "}" space'

View File

@ -732,24 +732,26 @@ export class SchemaConverter {
}
_buildObjectRule(properties, required, name, additionalProperties) {
const propOrder = this._propOrder;
// sort by position in prop_order (if specified) then by original order
const sortedProps = properties.map(([k]) => k).sort((a, b) => {
const orderA = propOrder[a] || Infinity;
const orderB = propOrder[b] || Infinity;
return orderA - orderB || properties.findIndex(([k]) => k === a) - properties.findIndex(([k]) => k === b);
});
const propKvRuleNames = {};
const propNames = []
for (const [propName, propSchema] of properties) {
const propRuleName = this.visit(propSchema, `${name ?? ''}${name ? '-' : ''}${propName}`);
propKvRuleNames[propName] = this._addRule(
`${name ?? ''}${name ? '-' : ''}${propName}-kv`,
`${this._formatLiteral(JSON.stringify(propName))} space ":" space ${propRuleName}`
);
propNames.push(propName);
}
const propOrder = this._propOrder;
if (Object.keys(propOrder).length > 0) {
// sort by position in prop_order (if specified) then by original order
propNames.sort((a, b) => {
const orderA = propOrder[a] || Infinity;
const orderB = propOrder[b] || Infinity;
return orderA - orderB || properties.findIndex(([k]) => k === a) - properties.findIndex(([k]) => k === b);
});
}
const requiredProps = sortedProps.filter(k => required.has(k));
const optionalProps = sortedProps.filter(k => !required.has(k));
if (additionalProperties) {
const subName = `${name ?? ''}${name ? '-' : ''}additional`;
@ -758,33 +760,32 @@ export class SchemaConverter {
: this._addPrimitive('value', PRIMITIVE_RULES['value']);
const key_rule =
sortedProps.length === 0 ? this._addPrimitive('string', PRIMITIVE_RULES['string'])
: this._addRule(`${subName}-k`, this._notStrings(sortedProps));
propNames.length === 0 ? this._addPrimitive('string', PRIMITIVE_RULES['string'])
: this._addRule(`${subName}-k`, this._notStrings(propNames));
propKvRuleNames['*'] = this._addRule(
`${subName}-kv`,
`${key_rule} ":" space ${valueRule}`);
optionalProps.push('*');
propNames.push('*');
}
let rule = '"{" space ';
rule += requiredProps.map(k => propKvRuleNames[k]).join(' "," space ');
if (optionalProps.length > 0) {
rule += ' (';
if (requiredProps.length > 0) {
rule += ' "," space ( ';
}
if (propNames.length > 0) {
const getRecursiveRefs = (ks, firstIsOptional) => {
const [k, ...rest] = ks;
const kvRuleName = propKvRuleNames[k];
let res;
const commaRef = `( "," space ${kvRuleName} )`;
const commaRef = `"," space ${kvRuleName}`;
if (firstIsOptional) {
res = commaRef + (k === '*' ? '*' : '?');
// res = commaRef + (k === '*' ? '*' : '?');
if (!required.has(k)) {
res = `( ${commaRef} )${k === '*' ? '*' : '?'}`;
} else {
res = commaRef;
}
} else {
res = kvRuleName + (k === '*' ? ' ' + commaRef + '*' : '');
res = kvRuleName + (k === '*' ? ' ( ' + commaRef + ' )*' : '');
}
if (rest.length > 0) {
res += ' ' + this._addRule(
@ -795,11 +796,22 @@ export class SchemaConverter {
return res;
};
rule += optionalProps.map((_, i) => getRecursiveRefs(optionalProps.slice(i), false)).join(' | ');
if (requiredProps.length > 0) {
rule += ' )';
let hasRequired = false;
const alternatives = [];
for (let i = 0; i < propNames.length; i++) {
alternatives.push(getRecursiveRefs(propNames.slice(i), false));
if (required.has(propNames[i])) {
hasRequired = true;
break;
}
}
const alts = alternatives.join(' | ');
if (alternatives.length > 1 || !hasRequired) {
rule += `( ${alts} )${hasRequired ? '' : '?'}`;
} else {
rule += alts;
}
rule += ' )?';
}
rule += ' "}" space';

View File

@ -1112,6 +1112,33 @@ static void test_json_schema() {
}
);
test_schema(
"object property order",
// Schema
R"""({
"type": "object",
"properties": {
"a": { "type": "integer" },
"b": { "type": "integer" },
"c": { "type": "integer" },
"d": { "type": "integer" }
},
"required": ["b", "d"]
})""",
// Passing strings
{
R"""({ "b": 0, "d": 0 })""",
R"""({ "b": 0, "d": 0, "E": -1 })""",
R"""({ "a": 0, "b": 0, "c": 0, "d": 0 })""",
R"""({ "a": 0, "b": 0, "c": 0, "d": 0, "E": -1 })""",
},
// Failing strings
{
R"""({ "E": -1, "b": 0, "d": 0 })""",
R"""({ "b": 0, "d": 0, "a": 0 })""",
}
);
test_schema(
"additional properties can't override other properties",
R"""({

View File

@ -761,9 +761,11 @@ static void test_all(const std::string & lang, std::function<void(const TestCase
R"""(
a-kv ::= "\"a\"" space ":" space string
b-kv ::= "\"b\"" space ":" space string
b-rest ::= "," space c-kv c-rest
c-kv ::= "\"c\"" space ":" space string
c-rest ::= "," space a-kv
char ::= [^"\\\x7F\x00-\x1F] | [\\] (["\\bfnrt] | "u" [0-9a-fA-F]{4})
root ::= "{" space b-kv "," space c-kv "," space a-kv "}" space
root ::= "{" space b-kv b-rest "}" space
space ::= | " " | "\n" [ \t]{0,20}
string ::= "\"" char* "\"" space
)"""
@ -783,7 +785,7 @@ static void test_all(const std::string & lang, std::function<void(const TestCase
R"""(
a-kv ::= "\"a\"" space ":" space string
char ::= [^"\\\x7F\x00-\x1F] | [\\] (["\\bfnrt] | "u" [0-9a-fA-F]{4})
root ::= "{" space (a-kv )? "}" space
root ::= "{" space ( a-kv )? "}" space
space ::= | " " | "\n" [ \t]{0,20}
string ::= "\"" char* "\"" space
)"""
@ -807,7 +809,7 @@ static void test_all(const std::string & lang, std::function<void(const TestCase
b-rest ::= ( "," space c-kv )?
c-kv ::= "\"c\"" space ":" space string
char ::= [^"\\\x7F\x00-\x1F] | [\\] (["\\bfnrt] | "u" [0-9a-fA-F]{4})
root ::= "{" space (a-kv a-rest | b-kv b-rest | c-kv )? "}" space
root ::= "{" space ( a-kv a-rest | b-kv b-rest | c-kv )? "}" space
space ::= | " " | "\n" [ \t]{0,20}
string ::= "\"" char* "\"" space
)"""
@ -815,13 +817,13 @@ static void test_all(const std::string & lang, std::function<void(const TestCase
test({
SUCCESS,
"required + optional props each in original order",
"required + optional props each in unified order",
R"""({
"properties": {
"b": {"type": "string"},
"a": {"type": "string"},
"d": {"type": "string"},
"c": {"type": "string"}
"c": {"type": "string"},
"b": {"type": "string"},
"a": {"type": "string"}
},
"required": ["a", "b"],
"additionalProperties": false
@ -829,11 +831,13 @@ static void test_all(const std::string & lang, std::function<void(const TestCase
R"""(
a-kv ::= "\"a\"" space ":" space string
b-kv ::= "\"b\"" space ":" space string
b-rest ::= "," space a-kv
c-kv ::= "\"c\"" space ":" space string
c-rest ::= "," space b-kv b-rest
char ::= [^"\\\x7F\x00-\x1F] | [\\] (["\\bfnrt] | "u" [0-9a-fA-F]{4})
d-kv ::= "\"d\"" space ":" space string
d-rest ::= ( "," space c-kv )?
root ::= "{" space b-kv "," space a-kv ( "," space ( d-kv d-rest | c-kv ) )? "}" space
d-rest ::= ( "," space c-kv )? c-rest
root ::= "{" space ( d-kv d-rest | c-kv c-rest | b-kv b-rest ) "}" space
space ::= | " " | "\n" [ \t]{0,20}
string ::= "\"" char* "\"" space
)"""
@ -853,7 +857,7 @@ static void test_all(const std::string & lang, std::function<void(const TestCase
decimal-part ::= [0-9]{1,16}
integral-part ::= [0] | [1-9] [0-9]{0,15}
number ::= ("-"? integral-part) ("." decimal-part)? ([eE] [-+]? integral-part)? space
root ::= "{" space (additional-kv ( "," space additional-kv )* )? "}" space
root ::= "{" space ( additional-kv ( "," space additional-kv )* )? "}" space
space ::= | " " | "\n" [ \t]{0,20}
string ::= "\"" char* "\"" space
)"""
@ -930,13 +934,14 @@ static void test_all(const std::string & lang, std::function<void(const TestCase
})""",
R"""(
a-kv ::= "\"a\"" space ":" space number
a-rest ::= ( "," space additional-kv )*
additional-k ::= ["] ( [a] char+ | [^"a] char* )? ["] space
additional-kv ::= additional-k ":" space string
char ::= [^"\\\x7F\x00-\x1F] | [\\] (["\\bfnrt] | "u" [0-9a-fA-F]{4})
decimal-part ::= [0-9]{1,16}
integral-part ::= [0] | [1-9] [0-9]{0,15}
number ::= ("-"? integral-part) ("." decimal-part)? ([eE] [-+]? integral-part)? space
root ::= "{" space a-kv ( "," space ( additional-kv ( "," space additional-kv )* ) )? "}" space
root ::= "{" space a-kv a-rest "}" space
space ::= | " " | "\n" [ \t]{0,20}
string ::= "\"" char* "\"" space
)"""
@ -961,7 +966,7 @@ static void test_all(const std::string & lang, std::function<void(const TestCase
decimal-part ::= [0-9]{1,16}
integral-part ::= [0] | [1-9] [0-9]{0,15}
number ::= ("-"? integral-part) ("." decimal-part)? ([eE] [-+]? integral-part)? space
root ::= "{" space (a-kv a-rest | additional-kv ( "," space additional-kv )* )? "}" space
root ::= "{" space ( a-kv a-rest | additional-kv ( "," space additional-kv )* )? "}" space
space ::= | " " | "\n" [ \t]{0,20}
)"""
});
@ -984,11 +989,12 @@ static void test_all(const std::string & lang, std::function<void(const TestCase
also-kv ::= "\"also\"" space ":" space number
also-rest ::= ( "," space additional-kv )*
and-kv ::= "\"and\"" space ":" space number
and-rest ::= ( "," space also-kv )? also-rest
char ::= [^"\\\x7F\x00-\x1F] | [\\] (["\\bfnrt] | "u" [0-9a-fA-F]{4})
decimal-part ::= [0-9]{1,16}
integral-part ::= [0] | [1-9] [0-9]{0,15}
number ::= ("-"? integral-part) ("." decimal-part)? ([eE] [-+]? integral-part)? space
root ::= "{" space and-kv ( "," space ( also-kv also-rest | additional-kv ( "," space additional-kv )* ) )? "}" space
root ::= "{" space and-kv and-rest "}" space
space ::= | " " | "\n" [ \t]{0,20}
)"""
});
@ -1014,7 +1020,7 @@ static void test_all(const std::string & lang, std::function<void(const TestCase
integer ::= ("-"? integral-part) space
integral-part ::= [0] | [1-9] [0-9]{0,15}
root ::= ("-"? integral-part) space
root0 ::= "{" space (-kv -rest | a-kv a-rest | additional-kv ( "," space additional-kv )* )? "}" space
root0 ::= "{" space ( -kv -rest | a-kv a-rest | additional-kv ( "," space additional-kv )* )? "}" space
space ::= | " " | "\n" [ \t]{0,20}
)"""
});
@ -1039,7 +1045,7 @@ static void test_all(const std::string & lang, std::function<void(const TestCase
char ::= [^"\\\x7F\x00-\x1F] | [\\] (["\\bfnrt] | "u" [0-9a-fA-F]{4})
integer ::= ("-"? integral-part) space
integral-part ::= [0] | [1-9] [0-9]{0,15}
root ::= "{" space (a-kv a-rest | aa-kv aa-rest | additional-kv ( "," space additional-kv )* )? "}" space
root ::= "{" space ( a-kv a-rest | aa-kv aa-rest | additional-kv ( "," space additional-kv )* )? "}" space
space ::= | " " | "\n" [ \t]{0,20}
)"""
});
@ -1064,7 +1070,7 @@ static void test_all(const std::string & lang, std::function<void(const TestCase
char ::= [^"\\\x7F\x00-\x1F] | [\\] (["\\bfnrt] | "u" [0-9a-fA-F]{4})
integer ::= ("-"? integral-part) space
integral-part ::= [0] | [1-9] [0-9]{0,15}
root ::= "{" space (ab-kv ab-rest | ac-kv ac-rest | additional-kv ( "," space additional-kv )* )? "}" space
root ::= "{" space ( ab-kv ab-rest | ac-kv ac-rest | additional-kv ( "," space additional-kv )* )? "}" space
space ::= | " " | "\n" [ \t]{0,20}
)"""
});
@ -1120,10 +1126,10 @@ static void test_all(const std::string & lang, std::function<void(const TestCase
R"""(
alternative-0 ::= foo
alternative-1 ::= bar
bar ::= "{" space (bar-b-kv )? "}" space
bar ::= "{" space ( bar-b-kv )? "}" space
bar-b-kv ::= "\"b\"" space ":" space number
decimal-part ::= [0-9]{1,16}
foo ::= "{" space (foo-a-kv )? "}" space
foo ::= "{" space ( foo-a-kv )? "}" space
foo-a-kv ::= "\"a\"" space ":" space number
integral-part ::= [0] | [1-9] [0-9]{0,15}
number ::= ("-"? integral-part) ("." decimal-part)? ([eE] [-+]? integral-part)? space
@ -1164,14 +1170,16 @@ static void test_all(const std::string & lang, std::function<void(const TestCase
})""",
R"""(
a-kv ::= "\"a\"" space ":" space number
a-rest ::= "," space b-kv b-rest
b-kv ::= "\"b\"" space ":" space number
b-rest ::= ( "," space d-kv )? d-rest
c-kv ::= "\"c\"" space ":" space number
d-kv ::= "\"d\"" space ":" space number
d-rest ::= ( "," space c-kv )?
decimal-part ::= [0-9]{1,16}
integral-part ::= [0] | [1-9] [0-9]{0,15}
number ::= ("-"? integral-part) ("." decimal-part)? ([eE] [-+]? integral-part)? space
root ::= "{" space a-kv "," space b-kv ( "," space ( d-kv d-rest | c-kv ) )? "}" space
root ::= "{" space a-kv a-rest "}" space
space ::= | " " | "\n" [ \t]{0,20}
)"""
});