[FEATURE] Implementation of $ref JSON Schema Keyword #20
@@ -9,6 +9,7 @@ from .boolean_type_parser import BooleanTypeParser
|
||||
from .float_type_parser import FloatTypeParser
|
||||
from .int_type_parser import IntTypeParser
|
||||
from .object_type_parser import ObjectTypeParser
|
||||
from .ref_type_parser import RefTypeParser
|
||||
from .string_type_parser import StringTypeParser
|
||||
|
||||
|
||||
@@ -22,4 +23,5 @@ __all__ = [
|
||||
"IntTypeParser",
|
||||
"ObjectTypeParser",
|
||||
"StringTypeParser",
|
||||
"RefTypeParser",
|
||||
]
|
||||
|
||||
72
jambo/parser/ref_type_parser.py
Normal file
72
jambo/parser/ref_type_parser.py
Normal file
@@ -0,0 +1,72 @@
|
||||
from jambo.parser import GenericTypeParser
|
||||
from jambo.types.type_parser_options import TypeParserOptions
|
||||
|
||||
from typing_extensions import Any, ForwardRef, TypeVar, Union, Unpack
|
||||
|
||||
|
||||
RefType = TypeVar("RefType", bound=Union[int, str])
|
||||
|
||||
|
||||
class RefTypeParser(GenericTypeParser):
|
||||
json_schema_type = "$ref"
|
||||
|
||||
def from_properties_impl(
|
||||
self, name: str, properties: dict[str, Any], **kwargs: Unpack[TypeParserOptions]
|
||||
) -> tuple[RefType, dict]:
|
||||
if "$ref" not in properties:
|
||||
raise ValueError(f"RefTypeParser: Missing $ref in properties for {name}")
|
||||
|
||||
context = kwargs["context"]
|
||||
required = kwargs.get("required", False)
|
||||
|
||||
if context is None:
|
||||
raise RuntimeError(
|
||||
f"RefTypeParser: Missing $content in properties for {name}"
|
||||
)
|
||||
|
||||
if not properties["$ref"].startswith("#"):
|
||||
raise ValueError(
|
||||
"At the moment, only local references are supported. "
|
||||
"Look into $defs and # for recursive references."
|
||||
)
|
||||
|
||||
ref_type = None
|
||||
mapped_properties = {}
|
||||
|
||||
if properties["$ref"] == "#":
|
||||
if "title" not in context:
|
||||
raise ValueError(
|
||||
"RefTypeParser: Missing title in properties for $ref #"
|
||||
)
|
||||
|
||||
ref_type = ForwardRef(context["title"])
|
||||
|
||||
elif properties["$ref"].startswith("#/$defs/"):
|
||||
target_name = None
|
||||
target_property = context
|
||||
for prop_name in properties["$ref"].split("/")[1:]:
|
||||
if prop_name not in target_property:
|
||||
raise ValueError(
|
||||
f"RefTypeParser: Missing {prop_name} in"
|
||||
" properties for $ref {properties['$ref']}"
|
||||
)
|
||||
target_name = prop_name
|
||||
target_property = target_property[prop_name]
|
||||
|
||||
if target_name is None or target_property is None:
|
||||
raise ValueError(f"RefTypeParser: Invalid $ref {properties['$ref']}")
|
||||
|
||||
ref_type, mapped_properties = GenericTypeParser.type_from_properties(
|
||||
target_name, target_property, **kwargs
|
||||
)
|
||||
|
||||
else:
|
||||
raise ValueError(
|
||||
"RefTypeParser: Invalid $ref format. "
|
||||
"Only local references are supported."
|
||||
)
|
||||
|
||||
if not required:
|
||||
mapped_properties["default"] = None
|
||||
|
||||
return ref_type, mapped_properties
|
||||
@@ -1,5 +1,7 @@
|
||||
from typing_extensions import TypedDict
|
||||
from typing_extensions import Any, NotRequired, TypedDict
|
||||
|
||||
|
||||
class TypeParserOptions(TypedDict):
|
||||
required: bool
|
||||
context: dict[str, Any]
|
||||
ref_cache: NotRequired[dict[str, type]]
|
||||
|
||||
34
tests/parser/test_ref_type_parser.py
Normal file
34
tests/parser/test_ref_type_parser.py
Normal file
@@ -0,0 +1,34 @@
|
||||
from jambo.parser import RefTypeParser
|
||||
|
||||
from unittest import TestCase
|
||||
|
||||
|
||||
class TestRefTypeParser(TestCase):
|
||||
def test_ref_type_parser_local_ref(self):
|
||||
properties = {
|
||||
"title": "person",
|
||||
"$ref": "#/$defs/person",
|
||||
"$defs": {
|
||||
"person": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {"type": "string"},
|
||||
"age": {"type": "integer"},
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
type_parsing, type_validator = RefTypeParser().from_properties(
|
||||
properties=properties,
|
||||
name="placeholder",
|
||||
context=properties,
|
||||
required=True,
|
||||
)
|
||||
|
||||
self.assertIsInstance(type_parsing, type)
|
||||
|
||||
obj = type_parsing(name="John", age=30)
|
||||
|
||||
self.assertEqual(obj.name, "John")
|
||||
self.assertEqual(obj.age, 30)
|
||||
Reference in New Issue
Block a user