Implements: allOf, anyOf #11

Merged
HideyoshiNakazone merged 15 commits from any-all-ref-implementation into main 2025-04-19 20:32:58 +00:00
4 changed files with 120 additions and 1 deletions
Showing only changes of commit 6d1febbcc1 - Show all commits

View File

@@ -8,3 +8,4 @@ from .string_type_parser import StringTypeParser as StringTypeParser
from .array_type_parser import ArrayTypeParser as ArrayTypeParser
from .boolean_type_parser import BooleanTypeParser as BooleanTypeParser
from .float_type_parser import FloatTypeParser as FloatTypeParser
from .allof_type_parser import AllOfTypeParser as AllOfTypeParser

View File

@@ -0,0 +1,76 @@
from jambo.parser._type_parser import GenericTypeParser
class AllOfTypeParser(GenericTypeParser):
mapped_type = any
json_schema_type = "allOf"
@staticmethod
def from_properties(name, properties):
subProperties = properties.get("allOf")
if not subProperties:
raise ValueError("Invalid JSON Schema: 'allOf' is not specified.")
_mapped_type = properties.get("type")
if _mapped_type is None:
_mapped_type = subProperties[0].get("type")
if _mapped_type is None:
raise ValueError("Invalid JSON Schema: 'type' is not specified.")
if not all(prop.get("type") == _mapped_type for prop in subProperties):
raise ValueError("Invalid JSON Schema: allOf types do not match.")
combined_properties = AllOfTypeParser._rebuild_properties_from_subproperties(
subProperties
)
return GenericTypeParser.get_impl(_mapped_type).from_properties(
name, combined_properties
)
@staticmethod
def _rebuild_properties_from_subproperties(subProperties):
properties = {}
for subProperty in subProperties:
for name, prop in subProperty.items():
if name not in properties:
properties[name] = prop
else:
# Merge properties if they exist in both sub-properties
properties[name] = AllOfTypeParser._validate_prop(
name, properties[name], prop
)
return properties
@staticmethod
def _validate_prop(prop_name, old_value, new_value):
if prop_name == "type":
if old_value != new_value:
raise ValueError(
f"Invalid JSON Schema: conflicting types for '{prop_name}'"
)
return old_value
if prop_name == "description":
return f"{old_value} | {new_value}"
if prop_name == "default":
if old_value != new_value:
raise ValueError(
f"Invalid JSON Schema: conflicting defaults for '{prop_name}'"
)
return old_value
if prop_name == "required":
return old_value + new_value
if prop_name in ("maxLength", "maximum", "exclusiveMaximum"):
return old_value if old_value > new_value else new_value
if prop_name in ("minLength", "minimum", "exclusiveMinimum"):
return old_value if old_value < new_value else new_value
# Handle other properties by just returning the first valued
return old_value

View File

@@ -79,8 +79,20 @@ class SchemaConverter:
def _build_field(
name, properties: dict, required_keys: list[str]
) -> tuple[type, dict]:
match properties:
case {"anyOf": _}:
_field_type = "anyOf"
case {"allOf": _}:
_field_type = "allOf"
case {"oneOf": _}:
_field_type = "oneOf"
case {"type": _}:
_field_type = properties["type"]
case _:
raise ValueError(f"Invalid JSON Schema: {properties}")
_field_type, _field_args = GenericTypeParser.get_impl(
properties["type"]
_field_type
).from_properties(name, properties)
_field_args = _field_args or {}

View File

@@ -281,3 +281,33 @@ class TestSchemaConverter(TestCase):
self.assertEqual(obj.address.street, "123 Main St")
self.assertEqual(obj.address.city, "Springfield")
def test_all_of(self):
schema = {
"title": "Person",
"description": "A person",
"type": "object",
"properties": {
"name": {
"allOf": [
{"type": "string", "maxLength": 4},
{"type": "string", "minLength": 1},
{"type": "string", "minLength": 2},
]
},
},
}
Model = SchemaConverter.build(schema)
obj = Model(
name="J",
)
self.assertEqual(obj.name, "J")
with self.assertRaises(ValueError):
Model(name="John Invalid")
with self.assertRaises(ValueError):
Model(name="")