Merge pull request 'schemagen: reject absent additionalProperties' (#58) from weaselbot/weaseljson:weaselbot/issue-55 into main
CI / pre-commit (push) Successful in 53s
CI / build (-DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++, clang-arm64, ubuntu-latest-arm64, true) (push) Successful in 53s
CI / build (-DCMAKE_C_COMPILER=gcc -DCMAKE_CXX_COMPILER=g++, gcc-arm64, ubuntu-latest-arm64, false) (push) Successful in 50s
CI / build (-DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++, clang-amd64, ubuntu-latest-amd64, true) (push) Successful in 1m36s
CI / build (-DCMAKE_C_COMPILER=gcc -DCMAKE_CXX_COMPILER=g++, gcc-amd64, ubuntu-latest-amd64, false) (push) Successful in 1m34s
CI / pre-commit (push) Successful in 53s
CI / build (-DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++, clang-arm64, ubuntu-latest-arm64, true) (push) Successful in 53s
CI / build (-DCMAKE_C_COMPILER=gcc -DCMAKE_CXX_COMPILER=g++, gcc-arm64, ubuntu-latest-arm64, false) (push) Successful in 50s
CI / build (-DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++, clang-amd64, ubuntu-latest-amd64, true) (push) Successful in 1m36s
CI / build (-DCMAKE_C_COMPILER=gcc -DCMAKE_CXX_COMPILER=g++, gcc-amd64, ubuntu-latest-amd64, false) (push) Successful in 1m34s
Reviewed-on: #58
This commit was merged in pull request #58.
This commit is contained in:
@@ -60,8 +60,9 @@ into the result, so it is non-movable.
|
|||||||
## Not supported (rejected at generation time, no fallback)
|
## Not supported (rejected at generation time, no fallback)
|
||||||
|
|
||||||
`oneOf` / `anyOf` / `allOf` / `not` / `if`-`then`-`else`,
|
`oneOf` / `anyOf` / `allOf` / `not` / `if`-`then`-`else`,
|
||||||
`patternProperties`, `additionalProperties` with a schema (typed map),
|
`patternProperties`, `additionalProperties` absent or set to `true`,
|
||||||
`additionalProperties: true`, `prefixItems` (tuples), `const`,
|
`additionalProperties` with a schema (typed map), `prefixItems` (tuples),
|
||||||
|
`const`,
|
||||||
`dependentSchemas`/`dependentRequired`, union `type` lists other than
|
`dependentSchemas`/`dependentRequired`, union `type` lists other than
|
||||||
`["T", "null"]`, non-string enums, and remote (`$ref` to other documents).
|
`["T", "null"]`, non-string enums, and remote (`$ref` to other documents).
|
||||||
|
|
||||||
|
|||||||
@@ -56,6 +56,7 @@
|
|||||||
"type": "array",
|
"type": "array",
|
||||||
"items": {
|
"items": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
"required": [
|
"required": [
|
||||||
"name"
|
"name"
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ class SchemagenEnumTest(unittest.TestCase):
|
|||||||
def test_colliding_enum_values_deduplicate(self):
|
def test_colliding_enum_values_deduplicate(self):
|
||||||
schema = {
|
schema = {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
|
"additionalProperties": False,
|
||||||
"properties": {"role": {"enum": ["foo-bar", "foo_bar"]}},
|
"properties": {"role": {"enum": ["foo-bar", "foo_bar"]}},
|
||||||
}
|
}
|
||||||
rc, stdout, stderr = self.run_schemagen(schema)
|
rc, stdout, stderr = self.run_schemagen(schema)
|
||||||
@@ -45,6 +46,7 @@ class SchemagenEnumTest(unittest.TestCase):
|
|||||||
def test_distinct_enum_values_generate(self):
|
def test_distinct_enum_values_generate(self):
|
||||||
schema = {
|
schema = {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
|
"additionalProperties": False,
|
||||||
"properties": {"role": {"enum": ["admin", "user", "guest"]}},
|
"properties": {"role": {"enum": ["admin", "user", "guest"]}},
|
||||||
}
|
}
|
||||||
rc, stdout, stderr = self.run_schemagen(schema)
|
rc, stdout, stderr = self.run_schemagen(schema)
|
||||||
@@ -80,7 +82,7 @@ class SchemagenKeywordTest(unittest.TestCase):
|
|||||||
"module",
|
"module",
|
||||||
"import",
|
"import",
|
||||||
]
|
]
|
||||||
schema = {"type": "object", "properties": {}}
|
schema = {"type": "object", "additionalProperties": False, "properties": {}}
|
||||||
for kw in keywords:
|
for kw in keywords:
|
||||||
schema["properties"][kw] = {"type": "string"}
|
schema["properties"][kw] = {"type": "string"}
|
||||||
rc, stdout, stderr = self.run_schemagen(schema)
|
rc, stdout, stderr = self.run_schemagen(schema)
|
||||||
@@ -164,11 +166,18 @@ class SchemagenCollisionTest(unittest.TestCase):
|
|||||||
def test_kind_enum_does_not_duplicate_arr0(self):
|
def test_kind_enum_does_not_duplicate_arr0(self):
|
||||||
schema = {
|
schema = {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
|
"additionalProperties": False,
|
||||||
"properties": {
|
"properties": {
|
||||||
"arr": {"type": "array", "items": {"type": "string"}},
|
"arr": {"type": "array", "items": {"type": "string"}},
|
||||||
"obj": {"$ref": "#/$defs/Arr0"},
|
"obj": {"$ref": "#/$defs/Arr0"},
|
||||||
},
|
},
|
||||||
"$defs": {"Arr0": {"type": "object", "properties": {}}},
|
"$defs": {
|
||||||
|
"Arr0": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": False,
|
||||||
|
"properties": {},
|
||||||
|
}
|
||||||
|
},
|
||||||
}
|
}
|
||||||
out = self.generate_and_compile(schema)
|
out = self.generate_and_compile(schema)
|
||||||
self.assertIn("struct Arr0", out)
|
self.assertIn("struct Arr0", out)
|
||||||
@@ -183,7 +192,13 @@ class SchemagenCollisionTest(unittest.TestCase):
|
|||||||
schema = {
|
schema = {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"items": {"$ref": "#/$defs/Skip"},
|
"items": {"$ref": "#/$defs/Skip"},
|
||||||
"$defs": {"Skip": {"type": "object", "properties": {}}},
|
"$defs": {
|
||||||
|
"Skip": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": False,
|
||||||
|
"properties": {},
|
||||||
|
}
|
||||||
|
},
|
||||||
}
|
}
|
||||||
out = self.generate_and_compile(schema)
|
out = self.generate_and_compile(schema)
|
||||||
self.assertIn("struct Skip", out)
|
self.assertIn("struct Skip", out)
|
||||||
@@ -199,6 +214,7 @@ class SchemagenCollisionTest(unittest.TestCase):
|
|||||||
"""Regression test for issue #17: enum and array $defs must be reused."""
|
"""Regression test for issue #17: enum and array $defs must be reused."""
|
||||||
schema = {
|
schema = {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
|
"additionalProperties": False,
|
||||||
"properties": {
|
"properties": {
|
||||||
"role1": {"$ref": "#/$defs/Role"},
|
"role1": {"$ref": "#/$defs/Role"},
|
||||||
"role2": {"$ref": "#/$defs/Role"},
|
"role2": {"$ref": "#/$defs/Role"},
|
||||||
@@ -252,17 +268,14 @@ class SchemagenAdditionalPropertiesTest(unittest.TestCase):
|
|||||||
self.assertNotEqual(rc, 0)
|
self.assertNotEqual(rc, 0)
|
||||||
self.assertIn("additionalProperties: true is not supported", stderr)
|
self.assertIn("additionalProperties: true is not supported", stderr)
|
||||||
|
|
||||||
def test_additional_properties_absent_defaults_to_strict(self):
|
def test_additional_properties_absent_rejected(self):
|
||||||
schema = {
|
schema = {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {"name": {"type": "string"}},
|
"properties": {"name": {"type": "string"}},
|
||||||
}
|
}
|
||||||
rc, stdout, stderr = self.run_schemagen(schema)
|
rc, stdout, stderr = self.run_schemagen(schema)
|
||||||
self.assertEqual(rc, 0, msg=stderr)
|
self.assertNotEqual(rc, 0)
|
||||||
# The generated parser should reject unknown keys. Verify the key-matching
|
self.assertIn("additionalProperties is required for object schemas", stderr)
|
||||||
# helper returns -1 for an unknown key and cbKeyData rejects it.
|
|
||||||
self.assertIn("int matchKey(Kind k, std::string_view key) const {", stdout)
|
|
||||||
self.assertNotIn("bool isStrict(Kind k) const", stdout)
|
|
||||||
|
|
||||||
def test_additional_properties_false_accepted(self):
|
def test_additional_properties_false_accepted(self):
|
||||||
schema = {
|
schema = {
|
||||||
@@ -310,6 +323,7 @@ class SchemagenCyclicArrayTest(unittest.TestCase):
|
|||||||
def test_object_field_to_self_referential_array_rejected(self):
|
def test_object_field_to_self_referential_array_rejected(self):
|
||||||
schema = {
|
schema = {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
|
"additionalProperties": False,
|
||||||
"properties": {"items": {"$ref": "#/$defs/Items"}},
|
"properties": {"items": {"$ref": "#/$defs/Items"}},
|
||||||
"$defs": {
|
"$defs": {
|
||||||
"Items": {
|
"Items": {
|
||||||
@@ -326,6 +340,7 @@ class SchemagenCyclicArrayTest(unittest.TestCase):
|
|||||||
def test_chain_of_array_refs_rejected(self):
|
def test_chain_of_array_refs_rejected(self):
|
||||||
schema = {
|
schema = {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
|
"additionalProperties": False,
|
||||||
"properties": {"x": {"$ref": "#/$defs/A"}},
|
"properties": {"x": {"$ref": "#/$defs/A"}},
|
||||||
"$defs": {
|
"$defs": {
|
||||||
"A": {"type": "array", "items": {"$ref": "#/$defs/B"}},
|
"A": {"type": "array", "items": {"$ref": "#/$defs/B"}},
|
||||||
@@ -339,6 +354,7 @@ class SchemagenCyclicArrayTest(unittest.TestCase):
|
|||||||
def test_non_recursive_array_refs_still_allowed(self):
|
def test_non_recursive_array_refs_still_allowed(self):
|
||||||
schema = {
|
schema = {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
|
"additionalProperties": False,
|
||||||
"properties": {"roles": {"$ref": "#/$defs/Roles"}},
|
"properties": {"roles": {"$ref": "#/$defs/Roles"}},
|
||||||
"$defs": {
|
"$defs": {
|
||||||
"Role": {"enum": ["admin", "user"]},
|
"Role": {"enum": ["admin", "user"]},
|
||||||
@@ -439,10 +455,12 @@ class SchemagenCyclicNullableObjectTest(unittest.TestCase):
|
|||||||
self.skipTest("C++ compiler not available")
|
self.skipTest("C++ compiler not available")
|
||||||
schema = {
|
schema = {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
|
"additionalProperties": False,
|
||||||
"properties": {"self": {"$ref": "#/$defs/Self"}},
|
"properties": {"self": {"$ref": "#/$defs/Self"}},
|
||||||
"$defs": {
|
"$defs": {
|
||||||
"Self": {
|
"Self": {
|
||||||
"type": ["object", "null"],
|
"type": ["object", "null"],
|
||||||
|
"additionalProperties": False,
|
||||||
"properties": {"self": {"$ref": "#/$defs/Self"}},
|
"properties": {"self": {"$ref": "#/$defs/Self"}},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -680,6 +698,7 @@ class SchemagenIntegerBoundaryTest(unittest.TestCase):
|
|||||||
)
|
)
|
||||||
schema = {
|
schema = {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
|
"additionalProperties": False,
|
||||||
"properties": {"age": {"type": "integer"}},
|
"properties": {"age": {"type": "integer"}},
|
||||||
"required": ["age"],
|
"required": ["age"],
|
||||||
}
|
}
|
||||||
@@ -795,14 +814,22 @@ class SchemagenStringEscapeTest(unittest.TestCase):
|
|||||||
|
|
||||||
def test_newline_in_property_key(self):
|
def test_newline_in_property_key(self):
|
||||||
"""A JSON key containing a newline must become a valid C++ literal."""
|
"""A JSON key containing a newline must become a valid C++ literal."""
|
||||||
schema = {"type": "object", "properties": {"a\nb": {"type": "string"}}}
|
schema = {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": False,
|
||||||
|
"properties": {"a\nb": {"type": "string"}},
|
||||||
|
}
|
||||||
out = self.generate_and_compile(schema)
|
out = self.generate_and_compile(schema)
|
||||||
self.assertIn('// "a\\nb"', out)
|
self.assertIn('// "a\\nb"', out)
|
||||||
self.assertIn('if (key == "a\\nb") return 0;', out)
|
self.assertIn('if (key == "a\\nb") return 0;', out)
|
||||||
|
|
||||||
def test_newline_in_enum_value(self):
|
def test_newline_in_enum_value(self):
|
||||||
"""An enum value containing a newline must become a valid C++ literal."""
|
"""An enum value containing a newline must become a valid C++ literal."""
|
||||||
schema = {"type": "object", "properties": {"x": {"enum": ["a\nb"]}}}
|
schema = {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": False,
|
||||||
|
"properties": {"x": {"enum": ["a\nb"]}},
|
||||||
|
}
|
||||||
out = self.generate_and_compile(schema)
|
out = self.generate_and_compile(schema)
|
||||||
self.assertIn('static constexpr const char *X_names[] = { "a\\nb" };', out)
|
self.assertIn('static constexpr const char *X_names[] = { "a\\nb" };', out)
|
||||||
|
|
||||||
@@ -810,6 +837,7 @@ class SchemagenStringEscapeTest(unittest.TestCase):
|
|||||||
"""Mixed control characters in an enum value must be escaped."""
|
"""Mixed control characters in an enum value must be escaped."""
|
||||||
schema = {
|
schema = {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
|
"additionalProperties": False,
|
||||||
"properties": {
|
"properties": {
|
||||||
"x": {"enum": ["x\ny\rz\tw\vq\x00\x01"]},
|
"x": {"enum": ["x\ny\rz\tw\vq\x00\x01"]},
|
||||||
},
|
},
|
||||||
@@ -822,7 +850,11 @@ class SchemagenStringEscapeTest(unittest.TestCase):
|
|||||||
|
|
||||||
def test_backslash_and_quote_still_escaped(self):
|
def test_backslash_and_quote_still_escaped(self):
|
||||||
"""Existing escaping for backslash and double quote must remain correct."""
|
"""Existing escaping for backslash and double quote must remain correct."""
|
||||||
schema = {"type": "object", "properties": {'a"b\\c': {"type": "string"}}}
|
schema = {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": False,
|
||||||
|
"properties": {'a"b\\c': {"type": "string"}},
|
||||||
|
}
|
||||||
out = self.generate_and_compile(schema)
|
out = self.generate_and_compile(schema)
|
||||||
self.assertIn('// "a\\"b\\\\c"', out)
|
self.assertIn('// "a\\"b\\\\c"', out)
|
||||||
self.assertIn('if (key == "a\\"b\\\\c") return 0;', out)
|
self.assertIn('if (key == "a\\"b\\\\c") return 0;', out)
|
||||||
|
|||||||
@@ -417,7 +417,13 @@ class Builder:
|
|||||||
tobj = TObj(name)
|
tobj = TObj(name)
|
||||||
if defname is not None:
|
if defname is not None:
|
||||||
self._building[defname] = (tobj, nullable)
|
self._building[defname] = (tobj, nullable)
|
||||||
ap = node.get("additionalProperties", False)
|
ap = node.get("additionalProperties", None)
|
||||||
|
if ap is None:
|
||||||
|
raise GenError(
|
||||||
|
"additionalProperties is required for object schemas; set it "
|
||||||
|
"explicitly to false to reject unknown keys (absent "
|
||||||
|
"additionalProperties is not supported)"
|
||||||
|
)
|
||||||
if ap is True:
|
if ap is True:
|
||||||
raise GenError("additionalProperties: true is not supported")
|
raise GenError("additionalProperties: true is not supported")
|
||||||
if isinstance(ap, dict):
|
if isinstance(ap, dict):
|
||||||
|
|||||||
Reference in New Issue
Block a user