Skip to content

Commit ee37da6

Browse files
committed
coverage for choice
Signed-off-by: Clemens Vasters <clemens@vasters.com>
1 parent 884fc76 commit ee37da6

File tree

3 files changed

+182
-2
lines changed

3 files changed

+182
-2
lines changed

samples/py/json_structure_instance_validator.py

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,7 @@ def validate_instance(self, instance, schema=None, path="#", meta=None):
167167
return self.errors
168168

169169
# Process $extends. [Metaschema: $extends in ObjectType/TupleType]
170-
if "$extends" in schema:
170+
if schema_type != "choice" and "$extends" in schema:
171171
base = self._resolve_ref(schema["$extends"])
172172
if base is None:
173173
self.errors.append(f"Cannot resolve $extends {schema['$extends']} at {path}")
@@ -181,6 +181,7 @@ def validate_instance(self, instance, schema=None, path="#", meta=None):
181181
merged = dict(base)
182182
merged.update(schema)
183183
merged.pop("$extends", None)
184+
merged.pop("abstract", None)
184185
schema = merged
185186

186187
# Reject abstract schemas. [Metaschema: abstract property]
@@ -359,6 +360,39 @@ def validate_instance(self, instance, schema=None, path="#", meta=None):
359360
else:
360361
for (prop, prop_schema), item in zip(props.items(), instance):
361362
self.validate_instance(item, prop_schema, f"{path}/{prop}")
363+
elif schema_type == "choice":
364+
if not isinstance(instance, dict):
365+
self.errors.append(f"Expected choice object at {path}, got {type(instance).__name__}")
366+
else:
367+
choices = schema.get("choices", {})
368+
extends = schema.get("$extends")
369+
selector = schema.get("selector")
370+
if extends is None:
371+
# Tagged union: exactly one property matching a choice key
372+
if len(instance) != 1:
373+
self.errors.append(f"Tagged union at {path} must have a single property")
374+
else:
375+
key, value = next(iter(instance.items()))
376+
if key not in choices:
377+
self.errors.append(f"Property '{key}' at {path} not one of choices {list(choices.keys())}")
378+
else:
379+
self.validate_instance(value, choices[key], f"{path}/{key}")
380+
else:
381+
# Inline union: must have selector property
382+
if selector is None:
383+
self.errors.append(f"Inline union at {path} missing 'selector' in schema")
384+
else:
385+
sel_val = instance.get(selector)
386+
if not isinstance(sel_val, str):
387+
self.errors.append(f"Selector '{selector}' at {path} must be a string")
388+
elif sel_val not in choices:
389+
self.errors.append(f"Selector '{sel_val}' at {path} not one of choices {list(choices.keys())}")
390+
else:
391+
# validate remaining properties against chosen variant
392+
variant = choices[sel_val]
393+
inst_copy = dict(instance)
394+
inst_copy.pop(selector, None)
395+
self.validate_instance(inst_copy, variant, path)
362396
else:
363397
self.errors.append(f"Unsupported type '{schema_type}' at {path}")
364398

samples/py/json_structure_schema_validator.py

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ class JSONStructureSchemaCoreValidator:
4444
"datetime", "time", "duration", "uuid", "uri", "binary", "jsonpointer",
4545
"any"
4646
}
47-
COMPOUND_TYPES = {"object", "array", "set", "map", "tuple"}
47+
COMPOUND_TYPES = {"object", "array", "set", "map", "tuple", "choice"}
4848

4949
def __init__(self, allow_dollar=False, allow_import=False, import_map=None):
5050
"""
@@ -318,6 +318,8 @@ def _validate_schema(self, schema_obj, is_root=False, path="", name_in_namespace
318318
self._check_map_schema(schema_obj, path)
319319
elif tval == "tuple":
320320
self._check_tuple_schema(schema_obj, path)
321+
elif tval == "choice":
322+
self._check_choice_schema(schema_obj, path)
321323
else:
322324
self._check_primitive_schema(schema_obj, path)
323325
if "required" in schema_obj:
@@ -435,6 +437,27 @@ def _check_tuple_schema(self, obj, path):
435437
else:
436438
self._err(f"Tuple property '{prop_name}' must be an object (a schema).", path + f"/properties/{prop_name}")
437439

440+
def _check_choice_schema(self, obj, path):
441+
"""
442+
Checks constraints for a 'choice' type (tagged or inline union).
443+
"""
444+
if "choices" not in obj:
445+
self._err("Choice type must have 'choices'.", path + "/choices")
446+
else:
447+
choices = obj["choices"]
448+
if not isinstance(choices, dict):
449+
self._err("'choices' must be an object (map).", path + "/choices")
450+
else:
451+
for name, choice_schema in choices.items():
452+
if not isinstance(name, str):
453+
self._err(f"Choice key '{name}' must be a string.", path + f"/choices/{name}")
454+
if isinstance(choice_schema, dict):
455+
self._validate_schema(choice_schema, is_root=False, path=f"{path}/choices/{name}")
456+
else:
457+
self._err(f"Choice value for '{name}' must be an object (schema).", path + f"/choices/{name}")
458+
if "selector" in obj and not isinstance(obj.get("selector"), str):
459+
self._err("'selector' must be a string.", path + "/selector")
460+
438461
def _check_primitive_schema(self, obj, path):
439462
"""
440463
Checks constraints for a recognized primitive type.

samples/py/test_json_structure_instance_validator.py

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1136,6 +1136,129 @@ def test_not_fail():
11361136
errors = validator.validate_instance(instance)
11371137
assert any("should not validate against 'not' schema" in err for err in errors)
11381138

1139+
# -------------------------------------------------------------------
1140+
# Choice Type Tests
1141+
# -------------------------------------------------------------------
1142+
1143+
def test_choice_tagged_valid():
1144+
schema = {
1145+
"$schema": "https://json-structure.org/meta/core/v0/#",
1146+
"$id": "dummy",
1147+
"name": "MyChoice",
1148+
"type": "choice",
1149+
"choices": {
1150+
"a": {"type": "string"},
1151+
"b": {"type": "number"}
1152+
}
1153+
}
1154+
instance = {"a": "hello"}
1155+
validator = JSONStructureInstanceValidator(schema)
1156+
errors = validator.validate_instance(instance)
1157+
assert errors == []
1158+
1159+
1160+
def test_choice_tagged_invalid_multiple_properties():
1161+
schema = {
1162+
"$schema": "https://json-structure.org/meta/core/v0/#",
1163+
"$id": "dummy",
1164+
"name": "MyChoice",
1165+
"type": "choice",
1166+
"choices": {
1167+
"a": {"type": "string"},
1168+
"b": {"type": "number"}
1169+
}
1170+
}
1171+
instance = {"a": "x", "b": 1}
1172+
validator = JSONStructureInstanceValidator(schema)
1173+
errors = validator.validate_instance(instance)
1174+
assert any("must have a single property" in err for err in errors)
1175+
1176+
1177+
def test_choice_tagged_invalid_key():
1178+
schema = {
1179+
"$schema": "https://json-structure.org/meta/core/v0/#",
1180+
"$id": "dummy",
1181+
"name": "MyChoice",
1182+
"type": "choice",
1183+
"choices": {
1184+
"a": {"type": "string"},
1185+
"b": {"type": "number"}
1186+
}
1187+
}
1188+
instance = {"c": "oops"}
1189+
validator = JSONStructureInstanceValidator(schema)
1190+
errors = validator.validate_instance(instance)
1191+
assert any("not one of choices" in err for err in errors)
1192+
1193+
1194+
def test_choice_inline_valid():
1195+
schema = {
1196+
"$schema": "https://json-structure.org/meta/core/v0/#",
1197+
"$id": "dummy",
1198+
"name": "InlineChoice",
1199+
"type": "choice",
1200+
"$extends": "#/definitions/Base",
1201+
"selector": "type",
1202+
"choices": {
1203+
"X": {"$ref": "#/definitions/X"},
1204+
"Y": {"$ref": "#/definitions/Y"}
1205+
},
1206+
"definitions": {
1207+
"Base": {
1208+
"name": "Base",
1209+
"abstract": True,
1210+
"type": "object",
1211+
"properties": {"common": {"type": "string"}}
1212+
},
1213+
"X": {
1214+
"type": "object",
1215+
"$extends": "#/definitions/Base",
1216+
"properties": {"x": {"type": "number"}}
1217+
},
1218+
"Y": {
1219+
"type": "object",
1220+
"$extends": "#/definitions/Base",
1221+
"properties": {"y": {"type": "boolean"}}
1222+
}
1223+
}
1224+
}
1225+
instance = {"type": "X", "x": 123}
1226+
validator = JSONStructureInstanceValidator(schema)
1227+
errors = validator.validate_instance(instance)
1228+
assert errors == []
1229+
1230+
1231+
def test_choice_inline_missing_selector():
1232+
schema = {
1233+
"$schema": "https://json-structure.org/meta/core/v0/#",
1234+
"$id": "dummy",
1235+
"name": "InlineChoice",
1236+
"type": "choice",
1237+
"$extends": "#/definitions/Base",
1238+
"choices": {"A": {"type": "string"}},
1239+
"definitions": {"Base": {"name": "Base", "abstract": True, "type": "object", "properties": {}}}
1240+
}
1241+
instance = {"A": "value"}
1242+
validator = JSONStructureInstanceValidator(schema)
1243+
errors = validator.validate_instance(instance)
1244+
assert any("missing 'selector'" in err for err in errors)
1245+
1246+
1247+
def test_choice_inline_invalid_selector_value():
1248+
schema = {
1249+
"$schema": "https://json-structure.org/meta/core/v0/#",
1250+
"$id": "dummy",
1251+
"name": "InlineChoice",
1252+
"type": "choice",
1253+
"$extends": "#/definitions/Base",
1254+
"selector": "kind",
1255+
"choices": {"A": {"type": "string"}},
1256+
"definitions": {"Base": {"name": "Base", "abstract": True, "type": "object", "properties": {}}}
1257+
}
1258+
instance = {"kind": "B", "B": "oops"}
1259+
validator = JSONStructureInstanceValidator(schema)
1260+
errors = validator.validate_instance(instance)
1261+
assert any("not one of choices" in err for err in errors)
11391262

11401263
# -------------------------------------------------------------------
11411264
# End of tests.

0 commit comments

Comments
 (0)