[FEATURE] Implementation of $ref JSON Schema Keyword #20
@@ -4,7 +4,7 @@ from jambo.types.type_parser_options import TypeParserOptions
|
|||||||
from typing_extensions import Any, ForwardRef, TypeVar, Union, Unpack
|
from typing_extensions import Any, ForwardRef, TypeVar, Union, Unpack
|
||||||
|
|
||||||
|
|
||||||
RefType = TypeVar("RefType", bound=Union[int, str])
|
RefType = TypeVar("RefType", bound=Union[type, ForwardRef])
|
||||||
|
|
||||||
|
|
||||||
class RefTypeParser(GenericTypeParser):
|
class RefTypeParser(GenericTypeParser):
|
||||||
@@ -30,16 +30,13 @@ class RefTypeParser(GenericTypeParser):
|
|||||||
"Look into $defs and # for recursive references."
|
"Look into $defs and # for recursive references."
|
||||||
)
|
)
|
||||||
|
|
||||||
ref_type = None
|
|
||||||
mapped_properties = {}
|
|
||||||
|
|
||||||
if properties["$ref"] == "#":
|
if properties["$ref"] == "#":
|
||||||
if "title" not in context:
|
if "title" not in context:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
"RefTypeParser: Missing title in properties for $ref #"
|
"RefTypeParser: Missing title in properties for $ref #"
|
||||||
)
|
)
|
||||||
|
|
||||||
ref_type = ForwardRef(context["title"])
|
return ForwardRef(context["title"]), {}
|
||||||
|
|
||||||
elif properties["$ref"].startswith("#/$defs/"):
|
elif properties["$ref"].startswith("#/$defs/"):
|
||||||
target_name = None
|
target_name = None
|
||||||
@@ -56,17 +53,8 @@ class RefTypeParser(GenericTypeParser):
|
|||||||
if target_name is None or target_property is None:
|
if target_name is None or target_property is None:
|
||||||
raise ValueError(f"RefTypeParser: Invalid $ref {properties['$ref']}")
|
raise ValueError(f"RefTypeParser: Invalid $ref {properties['$ref']}")
|
||||||
|
|
||||||
ref_type, mapped_properties = GenericTypeParser.type_from_properties(
|
return GenericTypeParser.type_from_properties(
|
||||||
target_name, target_property, **kwargs
|
target_name, target_property, **kwargs
|
||||||
)
|
)
|
||||||
|
|
||||||
else:
|
raise ValueError(f"RefTypeParser: Unsupported $ref {properties['$ref']}")
|
||||||
raise ValueError(
|
|
||||||
"RefTypeParser: Invalid $ref format. "
|
|
||||||
"Only local references are supported."
|
|
||||||
)
|
|
||||||
|
|
||||||
if not required:
|
|
||||||
mapped_properties["default"] = None
|
|
||||||
|
|
||||||
return ref_type, mapped_properties
|
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
from typing_extensions import Any, NotRequired, TypedDict
|
from jambo.types.json_schema_type import JSONSchema
|
||||||
|
|
||||||
|
from typing_extensions import NotRequired, TypedDict
|
||||||
|
|
||||||
|
|
||||||
class TypeParserOptions(TypedDict):
|
class TypeParserOptions(TypedDict):
|
||||||
required: bool
|
required: bool
|
||||||
context: dict[str, Any]
|
context: JSONSchema
|
||||||
ref_cache: NotRequired[dict[str, type]]
|
ref_cache: NotRequired[dict[str, type]]
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
from jambo.parser import RefTypeParser
|
from jambo.parser import ObjectTypeParser, RefTypeParser
|
||||||
|
|
||||||
|
from typing_extensions import ForwardRef, get_type_hints
|
||||||
|
|
||||||
from unittest import TestCase
|
from unittest import TestCase
|
||||||
|
|
||||||
|
|
||||||
class TestRefTypeParser(TestCase):
|
class TestRefTypeParser(TestCase):
|
||||||
def test_ref_type_parser_local_ref(self):
|
def test_ref_type_parser_with_def(self):
|
||||||
properties = {
|
properties = {
|
||||||
"title": "person",
|
"title": "person",
|
||||||
"$ref": "#/$defs/person",
|
"$ref": "#/$defs/person",
|
||||||
@@ -20,8 +22,8 @@ class TestRefTypeParser(TestCase):
|
|||||||
}
|
}
|
||||||
|
|
||||||
type_parsing, type_validator = RefTypeParser().from_properties(
|
type_parsing, type_validator = RefTypeParser().from_properties(
|
||||||
properties=properties,
|
"person",
|
||||||
name="placeholder",
|
properties,
|
||||||
context=properties,
|
context=properties,
|
||||||
required=True,
|
required=True,
|
||||||
)
|
)
|
||||||
@@ -32,3 +34,50 @@ class TestRefTypeParser(TestCase):
|
|||||||
|
|
||||||
self.assertEqual(obj.name, "John")
|
self.assertEqual(obj.name, "John")
|
||||||
self.assertEqual(obj.age, 30)
|
self.assertEqual(obj.age, 30)
|
||||||
|
|
||||||
|
def test_ref_type_parser_with_forward_ref(self):
|
||||||
|
properties = {
|
||||||
|
"title": "person",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"name": {"type": "string"},
|
||||||
|
"age": {"type": "integer"},
|
||||||
|
"emergency_contact": {
|
||||||
|
"$ref": "#",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
type_parsing, type_validator = ObjectTypeParser().from_properties(
|
||||||
|
"person",
|
||||||
|
properties,
|
||||||
|
context=properties,
|
||||||
|
required=True,
|
||||||
|
)
|
||||||
|
type_parsing.update_forward_refs(person=type_parsing)
|
||||||
|
|
||||||
|
self.assertIsInstance(type_parsing, type)
|
||||||
|
|
||||||
|
type_hints = get_type_hints(type_parsing, globals(), locals())
|
||||||
|
|
||||||
|
self.assertIsInstance(type_hints["emergency_contact"], ForwardRef)
|
||||||
|
|
||||||
|
"""
|
||||||
|
This is a example of how to resolve ForwardRef in a dynamic model:
|
||||||
|
```python
|
||||||
|
from typing import get_type_hints
|
||||||
|
|
||||||
|
# Make sure your dynamic model has a name
|
||||||
|
model = type_parsing
|
||||||
|
model.update_forward_refs(person=model) # 👈 resolve the ForwardRef("person")
|
||||||
|
|
||||||
|
# Inject into globals manually
|
||||||
|
globalns = globals().copy()
|
||||||
|
globalns['person'] = model
|
||||||
|
|
||||||
|
# Now you can get the resolved hints
|
||||||
|
type_hints = get_type_hints(model, globalns=globalns)
|
||||||
|
```
|
||||||
|
Use `TypeParserOptions.ref_cache` option to cache and resolve ForwardRefs
|
||||||
|
inside the ObjectTypeParser.to_model method.
|
||||||
|
"""
|
||||||
|
|||||||
Reference in New Issue
Block a user