schemagen: drop support for additionalProperties: true
CI / build (-DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++, clang-arm64, ubuntu-latest-arm64, true) (pull_request) Successful in 52s
CI / pre-commit (pull_request) Successful in 51s
CI / build (-DCMAKE_C_COMPILER=gcc -DCMAKE_CXX_COMPILER=g++, gcc-arm64, ubuntu-latest-arm64, false) (pull_request) Successful in 48s
CI / build (-DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++, clang-amd64, ubuntu-latest-amd64, true) (pull_request) Successful in 1m29s
CI / build (-DCMAKE_C_COMPILER=gcc -DCMAKE_CXX_COMPILER=g++, gcc-amd64, ubuntu-latest-amd64, false) (pull_request) Successful in 1m24s
CI / build (-DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++, clang-arm64, ubuntu-latest-arm64, true) (pull_request) Successful in 52s
CI / pre-commit (pull_request) Successful in 51s
CI / build (-DCMAKE_C_COMPILER=gcc -DCMAKE_CXX_COMPILER=g++, gcc-arm64, ubuntu-latest-arm64, false) (pull_request) Successful in 48s
CI / build (-DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++, clang-amd64, ubuntu-latest-amd64, true) (pull_request) Successful in 1m29s
CI / build (-DCMAKE_C_COMPILER=gcc -DCMAKE_CXX_COMPILER=g++, gcc-amd64, ubuntu-latest-amd64, false) (pull_request) Successful in 1m24s
Following review feedback, the generator no longer supports permissive objects. Changes: - Reject `additionalProperties: true` at generation time. - Treat an absent `additionalProperties` as `false`, so every object is strict by default and unknown keys are rejected during parsing. - Remove the now-dead permissive-object infrastructure: `Kind::Skip`, `Cat::Skip`, `kSkip`, the per-frame `unknown` key set, and `isStrict()`. - Update the README feature/rejection tables accordingly. - Remove the permissive "loose" object from example.schema.json and the associated tests from test_gen.cpp. - Add Python unit tests verifying the new `additionalProperties` behavior. All tests pass (`ctest --output-on-failure`).
This commit is contained in:
@@ -60,7 +60,6 @@ class ObjectType:
|
||||
def __init__(self, name):
|
||||
self.name = name
|
||||
self.fields = [] # list[Field]
|
||||
self.strict = False # additionalProperties: false
|
||||
|
||||
|
||||
class EnumType:
|
||||
@@ -234,7 +233,7 @@ class Builder:
|
||||
@staticmethod
|
||||
def _is_reserved(name):
|
||||
"""Names generated internally that must not collide with user types."""
|
||||
return name in ("Root", "Skip", "RootScalar")
|
||||
return name in ("Root", "RootScalar")
|
||||
|
||||
def ref_name(self, ref):
|
||||
if not ref.startswith("#/"):
|
||||
@@ -339,12 +338,13 @@ class Builder:
|
||||
# register for $ref cycles before building fields
|
||||
if defname is not None:
|
||||
self._building[defname] = TObj(name)
|
||||
ap = node.get("additionalProperties", True)
|
||||
ap = node.get("additionalProperties", False)
|
||||
if ap is True:
|
||||
raise GenError("additionalProperties: true is not supported")
|
||||
if isinstance(ap, dict):
|
||||
raise GenError(
|
||||
"additionalProperties with a schema (typed map) is not " "supported yet"
|
||||
)
|
||||
obj.strict = ap is False
|
||||
required = set(node.get("required", []))
|
||||
props = node.get("properties", {})
|
||||
seen_cpp = set()
|
||||
@@ -566,7 +566,6 @@ class Emitter:
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
|
||||
#include "weaseljson.h"
|
||||
@@ -621,7 +620,6 @@ namespace {ns} {{"""
|
||||
root_is_container = isinstance(self.root_ty, (TObj, TArr))
|
||||
if not root_is_container:
|
||||
kinds.append("RootScalar")
|
||||
kinds.append("Skip")
|
||||
self.kind_order = kinds
|
||||
return " enum class Kind : uint8_t { " + ", ".join(kinds) + " };"
|
||||
|
||||
@@ -810,17 +808,6 @@ namespace {ns} {{"""
|
||||
lines.append(" }")
|
||||
return "\n".join(lines)
|
||||
|
||||
def _is_strict(self):
|
||||
strict = [n for n, o in self.b.objects.items() if o.strict]
|
||||
if not strict:
|
||||
return " bool isStrict(Kind) const { return false; }"
|
||||
cases = " ".join(f"case Kind::{n}:" for n in strict)
|
||||
return (
|
||||
" bool isStrict(Kind k) const {\n"
|
||||
f" switch (k) {{ {cases} return true; default: return false; }}\n"
|
||||
" }"
|
||||
)
|
||||
|
||||
def _enum_name_arrays(self):
|
||||
out = []
|
||||
for e in self.b.enums.values():
|
||||
@@ -862,13 +849,7 @@ namespace {ns} {{"""
|
||||
else:
|
||||
lines.append(" if (stack_.empty()) { reject(); return; }")
|
||||
lines.append(" Frame &f = stack_.back();")
|
||||
lines.append(
|
||||
" if (f.kind == Kind::Skip) { stack_.push_back(Frame{Kind::Skip, nullptr}); return; }"
|
||||
)
|
||||
lines.append(" SlotInfo si = slotInfoG(f);")
|
||||
lines.append(
|
||||
" if (si.cat == Cat::Skip) { stack_.push_back(Frame{Kind::Skip, nullptr}); return; }"
|
||||
)
|
||||
lines.append(f" if (si.cat != Cat::{event_cat}) {{ reject(); return; }}")
|
||||
lines.append(" void *p = engage(f, true);")
|
||||
lines.append(" stack_.push_back(Frame{si.child, p});")
|
||||
@@ -919,7 +900,7 @@ public:
|
||||
|
||||
private:
|
||||
{kind_enum}
|
||||
enum class Cat {{ Reject, Str, Int, Dbl, Bool, Enum, Obj, Arr, Skip }};
|
||||
enum class Cat {{ Reject, Str, Int, Dbl, Bool, Enum, Obj, Arr }};
|
||||
struct SlotInfo {{
|
||||
Cat cat;
|
||||
Kind child{{}};
|
||||
@@ -930,12 +911,10 @@ private:
|
||||
struct Frame {{
|
||||
Kind kind;
|
||||
void *dest;
|
||||
int field = -1; // object: selected field (-1 want key, -2 skip)
|
||||
int field = -1; // object: selected field (-1 means waiting for key)
|
||||
std::vector<uint64_t> seen; // populated field bitset (object frames)
|
||||
std::unordered_set<std::string> unknown; // unknown keys seen in non-strict objects
|
||||
}};
|
||||
static constexpr int kWantKey = -1;
|
||||
static constexpr int kSkip = -2;
|
||||
|
||||
{self._enum_name_arrays()}
|
||||
|
||||
@@ -950,10 +929,9 @@ private:
|
||||
|
||||
void reject() {{ error_ = true; }}
|
||||
|
||||
// Wrap the generated slotInfo() with the generic key/skip states.
|
||||
// Wrap the generated slotInfo() with the generic key state.
|
||||
SlotInfo slotInfoG(const Frame &f) {{
|
||||
if (isObjectKind(f.kind)) {{
|
||||
if (f.field == kSkip) return SlotInfo{{Cat::Skip}};
|
||||
if (f.field < 0) return SlotInfo{{Cat::Reject}};
|
||||
}}
|
||||
return slotInfo(f);
|
||||
@@ -978,11 +956,6 @@ private:
|
||||
void cbEndObject() {{
|
||||
if (error_) return;
|
||||
Frame &f = stack_.back();
|
||||
if (f.kind == Kind::Skip) {{
|
||||
stack_.pop_back();
|
||||
if (stack_.empty() || stack_.back().kind != Kind::Skip) valueComplete();
|
||||
return;
|
||||
}}
|
||||
if (isObjectKind(f.kind)) {{
|
||||
const auto &req = requiredMask(f.kind);
|
||||
bool missing = false;
|
||||
@@ -1000,11 +973,6 @@ private:
|
||||
void cbEndArray() {{
|
||||
if (error_) return;
|
||||
Frame f = stack_.back();
|
||||
if (f.kind == Kind::Skip) {{
|
||||
stack_.pop_back();
|
||||
if (stack_.empty() || stack_.back().kind != Kind::Skip) valueComplete();
|
||||
return;
|
||||
}}
|
||||
stack_.pop_back();
|
||||
valueComplete();
|
||||
}}
|
||||
@@ -1017,13 +985,7 @@ private:
|
||||
if (!done) return;
|
||||
int idx = matchKey(f.kind, scratch_);
|
||||
if (idx < 0) {{
|
||||
if (isStrict(f.kind)) {{ reject(); return; }}
|
||||
if (!f.unknown.insert(scratch_).second) {{
|
||||
reject(); return; // duplicate unknown key
|
||||
}}
|
||||
scratch_.clear();
|
||||
f.field = kSkip;
|
||||
return;
|
||||
reject(); return; // unknown key
|
||||
}}
|
||||
scratch_.clear();
|
||||
if (f.seen[idx >> 6] & (1ull << (idx & 63))) {{
|
||||
@@ -1035,9 +997,7 @@ private:
|
||||
void cbStringData(const char *buf, int len, int done) {{
|
||||
if (error_) return;
|
||||
Frame &f = stack_.back();
|
||||
if (f.kind == Kind::Skip) return;
|
||||
SlotInfo si = slotInfoG(f);
|
||||
if (si.cat == Cat::Skip) {{ if (done) valueComplete(); return; }}
|
||||
if (si.cat == Cat::Str) {{
|
||||
auto *s = (std::string *)engage(f, !started_);
|
||||
s->append(buf, len);
|
||||
@@ -1063,9 +1023,7 @@ private:
|
||||
void cbNumberData(const char *buf, int len, int done) {{
|
||||
if (error_) return;
|
||||
Frame &f = stack_.back();
|
||||
if (f.kind == Kind::Skip) return;
|
||||
SlotInfo si = slotInfoG(f);
|
||||
if (si.cat == Cat::Skip) {{ if (done) valueComplete(); return; }}
|
||||
if (si.cat != Cat::Int && si.cat != Cat::Dbl) {{ reject(); return; }}
|
||||
scratch_.append(buf, len);
|
||||
started_ = true;
|
||||
@@ -1103,9 +1061,7 @@ private:
|
||||
void cbBool(bool value) {{
|
||||
if (error_) return;
|
||||
Frame &f = stack_.back();
|
||||
if (f.kind == Kind::Skip) return;
|
||||
SlotInfo si = slotInfoG(f);
|
||||
if (si.cat == Cat::Skip) {{ valueComplete(); return; }}
|
||||
if (si.cat != Cat::Bool) {{ reject(); return; }}
|
||||
*(bool *)engage(f, true) = value;
|
||||
valueComplete();
|
||||
@@ -1121,9 +1077,7 @@ private:
|
||||
{null_at_root}
|
||||
}}
|
||||
Frame &f = stack_.back();
|
||||
if (f.kind == Kind::Skip) return;
|
||||
SlotInfo si = slotInfoG(f);
|
||||
if (si.cat == Cat::Skip) {{ valueComplete(); return; }}
|
||||
if (!si.nullable) {{ reject(); return; }}
|
||||
if (isArrayKind(f.kind)) appendNull(f); // keep null array elements
|
||||
valueComplete(); // leave optional empty / pointer null
|
||||
@@ -1161,8 +1115,6 @@ private:
|
||||
{self._reqmask()}
|
||||
|
||||
{self._is_object_kind()}
|
||||
|
||||
{self._is_strict()}
|
||||
}};
|
||||
"""
|
||||
|
||||
|
||||
Reference in New Issue
Block a user