Merge pull request #11 from HideyoshiNakazone/any-all-ref-implementation
Implements: allOf, anyOf Finalizes the implementation of allOf and anyOf, but the implementation of oneOf was cancelled for the time being
This commit was merged in pull request #11.
This commit is contained in:
@@ -1,10 +1,25 @@
|
|||||||
# Exports generic type parser
|
# Exports generic type parser
|
||||||
from ._type_parser import GenericTypeParser as GenericTypeParser
|
from ._type_parser import GenericTypeParser
|
||||||
|
|
||||||
# Exports Implementations
|
# Exports Implementations
|
||||||
from .int_type_parser import IntTypeParser as IntTypeParser
|
from .allof_type_parser import AllOfTypeParser
|
||||||
from .object_type_parser import ObjectTypeParser as ObjectTypeParser
|
from .anyof_type_parser import AnyOfTypeParser
|
||||||
from .string_type_parser import StringTypeParser as StringTypeParser
|
from .array_type_parser import ArrayTypeParser
|
||||||
from .array_type_parser import ArrayTypeParser as ArrayTypeParser
|
from .boolean_type_parser import BooleanTypeParser
|
||||||
from .boolean_type_parser import BooleanTypeParser as BooleanTypeParser
|
from .float_type_parser import FloatTypeParser
|
||||||
from .float_type_parser import FloatTypeParser as FloatTypeParser
|
from .int_type_parser import IntTypeParser
|
||||||
|
from .object_type_parser import ObjectTypeParser
|
||||||
|
from .string_type_parser import StringTypeParser
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"GenericTypeParser",
|
||||||
|
"AllOfTypeParser",
|
||||||
|
"AnyOfTypeParser",
|
||||||
|
"ArrayTypeParser",
|
||||||
|
"BooleanTypeParser",
|
||||||
|
"FloatTypeParser",
|
||||||
|
"IntTypeParser",
|
||||||
|
"ObjectTypeParser",
|
||||||
|
"StringTypeParser",
|
||||||
|
]
|
||||||
|
|||||||
@@ -1,31 +1,54 @@
|
|||||||
from abc import ABC, abstractmethod
|
from pydantic import Field, TypeAdapter
|
||||||
from typing import Generic, TypeVar
|
from typing_extensions import Annotated, Self
|
||||||
from typing_extensions import Self
|
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
|
from typing import Generic, Type, TypeVar
|
||||||
|
|
||||||
from pydantic import Field
|
|
||||||
|
|
||||||
T = TypeVar("T")
|
T = TypeVar("T")
|
||||||
|
|
||||||
|
|
||||||
class GenericTypeParser(ABC, Generic[T]):
|
class GenericTypeParser(ABC, Generic[T]):
|
||||||
@property
|
mapped_type: Type[T] = None
|
||||||
@abstractmethod
|
|
||||||
def mapped_type(self) -> type[T]: ...
|
|
||||||
|
|
||||||
@property
|
json_schema_type: str = None
|
||||||
@abstractmethod
|
|
||||||
def json_schema_type(self) -> str: ...
|
|
||||||
|
|
||||||
@staticmethod
|
default_mappings = {
|
||||||
@abstractmethod
|
"default": "default",
|
||||||
def from_properties(
|
"description": "description",
|
||||||
name: str, properties: dict[str, any]
|
}
|
||||||
) -> tuple[type[T], Field]: ...
|
|
||||||
|
type_mappings: dict[str, str] = None
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_impl(cls, type_name: str) -> Self:
|
def get_impl(cls, type_name: str) -> Self:
|
||||||
for subcls in cls.__subclasses__():
|
for subcls in cls.__subclasses__():
|
||||||
|
if subcls.json_schema_type is None:
|
||||||
|
raise RuntimeError(f"Unknown type: {type_name}")
|
||||||
|
|
||||||
if subcls.json_schema_type == type_name:
|
if subcls.json_schema_type == type_name:
|
||||||
return subcls
|
return subcls()
|
||||||
|
|
||||||
raise ValueError(f"Unknown type: {type_name}")
|
raise ValueError(f"Unknown type: {type_name}")
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def from_properties(
|
||||||
|
self, name: str, properties: dict[str, any], required: bool = False
|
||||||
|
) -> tuple[T, dict]: ...
|
||||||
|
|
||||||
|
def mappings_properties_builder(self, properties, required=False) -> dict[str, any]:
|
||||||
|
if self.type_mappings is None:
|
||||||
|
raise NotImplementedError("Type mappings not defined")
|
||||||
|
|
||||||
|
if not required:
|
||||||
|
properties["default"] = properties.get("default", None)
|
||||||
|
|
||||||
|
mappings = self.default_mappings | self.type_mappings
|
||||||
|
|
||||||
|
return {
|
||||||
|
mappings[key]: value for key, value in properties.items() if key in mappings
|
||||||
|
}
|
||||||
|
|
||||||
|
def validate_default(self, field_type: type, field_prop: dict, value) -> None:
|
||||||
|
field = Annotated[field_type, Field(**field_prop)]
|
||||||
|
TypeAdapter(field).validate_python(value)
|
||||||
|
|||||||
86
jambo/parser/allof_type_parser.py
Normal file
86
jambo/parser/allof_type_parser.py
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
from jambo.parser._type_parser import GenericTypeParser
|
||||||
|
|
||||||
|
|
||||||
|
class AllOfTypeParser(GenericTypeParser):
|
||||||
|
mapped_type = any
|
||||||
|
|
||||||
|
json_schema_type = "allOf"
|
||||||
|
|
||||||
|
def from_properties(self, name, properties, required=False):
|
||||||
|
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 any(
|
||||||
|
[prop.get("type", _mapped_type) != _mapped_type for prop in subProperties]
|
||||||
|
):
|
||||||
|
raise ValueError("Invalid JSON Schema: allOf types do not match.")
|
||||||
|
|
||||||
|
for subProperty in subProperties:
|
||||||
|
# If a sub-property has not defined a type, we need to set it to the top-level type
|
||||||
|
subProperty["type"] = _mapped_type
|
||||||
|
|
||||||
|
combined_properties = self._rebuild_properties_from_subproperties(subProperties)
|
||||||
|
|
||||||
|
return GenericTypeParser.get_impl(_mapped_type).from_properties(
|
||||||
|
name, combined_properties
|
||||||
|
)
|
||||||
|
|
||||||
|
def _rebuild_properties_from_subproperties(self, 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 == "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
|
||||||
|
|
||||||
|
if prop_name == "properties":
|
||||||
|
for key, value in new_value.items():
|
||||||
|
if key not in old_value:
|
||||||
|
old_value[key] = value
|
||||||
|
continue
|
||||||
|
|
||||||
|
for sub_key, sub_value in value.items():
|
||||||
|
if sub_key not in old_value[key]:
|
||||||
|
old_value[key][sub_key] = sub_value
|
||||||
|
else:
|
||||||
|
# Merge properties if they exist in both sub-properties
|
||||||
|
old_value[key][sub_key] = AllOfTypeParser._validate_prop(
|
||||||
|
sub_key, old_value[key][sub_key], sub_value
|
||||||
|
)
|
||||||
|
|
||||||
|
# Handle other properties by just returning the first valued
|
||||||
|
return old_value
|
||||||
55
jambo/parser/anyof_type_parser.py
Normal file
55
jambo/parser/anyof_type_parser.py
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
from jambo.parser._type_parser import GenericTypeParser
|
||||||
|
|
||||||
|
from pydantic import Field
|
||||||
|
from typing_extensions import Annotated
|
||||||
|
|
||||||
|
from typing import Union
|
||||||
|
|
||||||
|
|
||||||
|
class AnyOfTypeParser(GenericTypeParser):
|
||||||
|
mapped_type = Union
|
||||||
|
|
||||||
|
json_schema_type = "anyOf"
|
||||||
|
|
||||||
|
def from_properties(self, name, properties, required=False):
|
||||||
|
if "anyOf" not in properties:
|
||||||
|
raise ValueError(f"Invalid JSON Schema: {properties}")
|
||||||
|
|
||||||
|
if not isinstance(properties["anyOf"], list):
|
||||||
|
raise ValueError(f"Invalid JSON Schema: {properties['anyOf']}")
|
||||||
|
|
||||||
|
mapped_properties = dict()
|
||||||
|
|
||||||
|
subProperties = properties["anyOf"]
|
||||||
|
|
||||||
|
sub_types = [
|
||||||
|
GenericTypeParser.get_impl(subProperty["type"]).from_properties(
|
||||||
|
name, subProperty
|
||||||
|
)
|
||||||
|
for subProperty in subProperties
|
||||||
|
]
|
||||||
|
|
||||||
|
default_value = properties.get("default")
|
||||||
|
if default_value is not None:
|
||||||
|
for sub_type, sub_property in sub_types:
|
||||||
|
try:
|
||||||
|
self.validate_default(sub_type, sub_property, default_value)
|
||||||
|
break
|
||||||
|
except ValueError:
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
raise ValueError(
|
||||||
|
f"Invalid default value {default_value} for anyOf types: {sub_types}"
|
||||||
|
)
|
||||||
|
|
||||||
|
mapped_properties["default"] = default_value
|
||||||
|
|
||||||
|
if not required:
|
||||||
|
mapped_properties["default"] = mapped_properties.get("default")
|
||||||
|
|
||||||
|
# By defining the type as Union of Annotated type we can use the Field validator
|
||||||
|
# to enforce the constraints of each union type when needed.
|
||||||
|
# We use Annotated to attach the Field validators to the type.
|
||||||
|
field_types = [Annotated[t, Field(**v)] if v else t for t, v in sub_types]
|
||||||
|
|
||||||
|
return Union[(*field_types,)], mapped_properties
|
||||||
@@ -1,12 +1,8 @@
|
|||||||
import copy
|
|
||||||
|
|
||||||
from jambo.parser._type_parser import GenericTypeParser
|
from jambo.parser._type_parser import GenericTypeParser
|
||||||
|
|
||||||
|
import copy
|
||||||
from typing import TypeVar
|
from typing import TypeVar
|
||||||
|
|
||||||
from jambo.utils.properties_builder.mappings_properties_builder import (
|
|
||||||
mappings_properties_builder,
|
|
||||||
)
|
|
||||||
|
|
||||||
V = TypeVar("V")
|
V = TypeVar("V")
|
||||||
|
|
||||||
@@ -16,44 +12,33 @@ class ArrayTypeParser(GenericTypeParser):
|
|||||||
|
|
||||||
json_schema_type = "array"
|
json_schema_type = "array"
|
||||||
|
|
||||||
@classmethod
|
default_mappings = {"description": "description"}
|
||||||
def from_properties(cls, name, properties):
|
|
||||||
|
type_mappings = {
|
||||||
|
"maxItems": "max_length",
|
||||||
|
"minItems": "min_length",
|
||||||
|
}
|
||||||
|
|
||||||
|
def from_properties(self, name, properties, required=False):
|
||||||
_item_type, _item_args = GenericTypeParser.get_impl(
|
_item_type, _item_args = GenericTypeParser.get_impl(
|
||||||
properties["items"]["type"]
|
properties["items"]["type"]
|
||||||
).from_properties(name, properties["items"])
|
).from_properties(name, properties["items"], required=True)
|
||||||
|
|
||||||
_mappings = {
|
|
||||||
"maxItems": "max_length",
|
|
||||||
"minItems": "min_length",
|
|
||||||
}
|
|
||||||
|
|
||||||
wrapper_type = set if properties.get("uniqueItems", False) else list
|
wrapper_type = set if properties.get("uniqueItems", False) else list
|
||||||
|
field_type = wrapper_type[_item_type]
|
||||||
|
|
||||||
mapped_properties = mappings_properties_builder(
|
mapped_properties = self.mappings_properties_builder(
|
||||||
properties, _mappings, {"description": "description"}
|
properties,
|
||||||
|
required=required,
|
||||||
)
|
)
|
||||||
|
|
||||||
if "default" in properties:
|
default_list = properties.pop("default", None)
|
||||||
default_list = properties["default"]
|
if default_list is not None:
|
||||||
if not isinstance(default_list, list):
|
self.validate_default(
|
||||||
raise ValueError(
|
field_type,
|
||||||
f"Default value must be a list, got {type(default_list).__name__}"
|
mapped_properties,
|
||||||
)
|
default_list,
|
||||||
|
)
|
||||||
if len(default_list) > properties.get("maxItems", float("inf")):
|
|
||||||
raise ValueError(
|
|
||||||
f"Default list exceeds maxItems limit of {properties.get('maxItems')}"
|
|
||||||
)
|
|
||||||
|
|
||||||
if len(default_list) < properties.get("minItems", 0):
|
|
||||||
raise ValueError(
|
|
||||||
f"Default list is below minItems limit of {properties.get('minItems')}"
|
|
||||||
)
|
|
||||||
|
|
||||||
if not all(isinstance(item, _item_type) for item in default_list):
|
|
||||||
raise ValueError(
|
|
||||||
f"All items in the default list must be of type {_item_type.__name__}"
|
|
||||||
)
|
|
||||||
|
|
||||||
if wrapper_type is list:
|
if wrapper_type is list:
|
||||||
mapped_properties["default_factory"] = lambda: copy.deepcopy(
|
mapped_properties["default_factory"] = lambda: copy.deepcopy(
|
||||||
@@ -64,4 +49,4 @@ class ArrayTypeParser(GenericTypeParser):
|
|||||||
default_list
|
default_list
|
||||||
)
|
)
|
||||||
|
|
||||||
return wrapper_type[_item_type], mapped_properties
|
return field_type, mapped_properties
|
||||||
|
|||||||
@@ -1,7 +1,4 @@
|
|||||||
from jambo.parser._type_parser import GenericTypeParser
|
from jambo.parser._type_parser import GenericTypeParser
|
||||||
from jambo.utils.properties_builder.mappings_properties_builder import (
|
|
||||||
mappings_properties_builder,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class BooleanTypeParser(GenericTypeParser):
|
class BooleanTypeParser(GenericTypeParser):
|
||||||
@@ -9,9 +6,15 @@ class BooleanTypeParser(GenericTypeParser):
|
|||||||
|
|
||||||
json_schema_type = "boolean"
|
json_schema_type = "boolean"
|
||||||
|
|
||||||
@staticmethod
|
type_mappings = {
|
||||||
def from_properties(name, properties):
|
"default": "default",
|
||||||
_mappings = {
|
}
|
||||||
"default": "default",
|
|
||||||
}
|
def from_properties(self, name, properties, required=False):
|
||||||
return bool, mappings_properties_builder(properties, _mappings)
|
mapped_properties = self.mappings_properties_builder(properties, required)
|
||||||
|
|
||||||
|
default_value = properties.get("default")
|
||||||
|
if default_value is not None and not isinstance(default_value, bool):
|
||||||
|
raise ValueError(f"Default value for {name} must be a boolean.")
|
||||||
|
|
||||||
|
return bool, mapped_properties
|
||||||
|
|||||||
@@ -1,7 +1,4 @@
|
|||||||
from jambo.parser._type_parser import GenericTypeParser
|
from jambo.parser._type_parser import GenericTypeParser
|
||||||
from jambo.utils.properties_builder.numeric_properties_builder import (
|
|
||||||
numeric_properties_builder,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class FloatTypeParser(GenericTypeParser):
|
class FloatTypeParser(GenericTypeParser):
|
||||||
@@ -9,6 +6,20 @@ class FloatTypeParser(GenericTypeParser):
|
|||||||
|
|
||||||
json_schema_type = "number"
|
json_schema_type = "number"
|
||||||
|
|
||||||
@staticmethod
|
type_mappings = {
|
||||||
def from_properties(name, properties):
|
"minimum": "ge",
|
||||||
return float, numeric_properties_builder(properties)
|
"exclusiveMinimum": "gt",
|
||||||
|
"maximum": "le",
|
||||||
|
"exclusiveMaximum": "lt",
|
||||||
|
"multipleOf": "multiple_of",
|
||||||
|
"default": "default",
|
||||||
|
}
|
||||||
|
|
||||||
|
def from_properties(self, name, properties, required=False):
|
||||||
|
mapped_properties = self.mappings_properties_builder(properties, required)
|
||||||
|
|
||||||
|
default_value = mapped_properties.get("default")
|
||||||
|
if default_value is not None:
|
||||||
|
self.validate_default(float, mapped_properties, default_value)
|
||||||
|
|
||||||
|
return float, mapped_properties
|
||||||
|
|||||||
@@ -1,7 +1,4 @@
|
|||||||
from jambo.parser._type_parser import GenericTypeParser
|
from jambo.parser._type_parser import GenericTypeParser
|
||||||
from jambo.utils.properties_builder.numeric_properties_builder import (
|
|
||||||
numeric_properties_builder,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class IntTypeParser(GenericTypeParser):
|
class IntTypeParser(GenericTypeParser):
|
||||||
@@ -9,6 +6,20 @@ class IntTypeParser(GenericTypeParser):
|
|||||||
|
|
||||||
json_schema_type = "integer"
|
json_schema_type = "integer"
|
||||||
|
|
||||||
@staticmethod
|
type_mappings = {
|
||||||
def from_properties(name, properties):
|
"minimum": "ge",
|
||||||
return int, numeric_properties_builder(properties)
|
"exclusiveMinimum": "gt",
|
||||||
|
"maximum": "le",
|
||||||
|
"exclusiveMaximum": "lt",
|
||||||
|
"multipleOf": "multiple_of",
|
||||||
|
"default": "default",
|
||||||
|
}
|
||||||
|
|
||||||
|
def from_properties(self, name, properties, required=False):
|
||||||
|
mapped_properties = self.mappings_properties_builder(properties, required)
|
||||||
|
|
||||||
|
default_value = mapped_properties.get("default")
|
||||||
|
if default_value is not None:
|
||||||
|
self.validate_default(int, mapped_properties, default_value)
|
||||||
|
|
||||||
|
return int, mapped_properties
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ class ObjectTypeParser(GenericTypeParser):
|
|||||||
json_schema_type = "object"
|
json_schema_type = "object"
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def from_properties(name, properties):
|
def from_properties(name, properties, required=False):
|
||||||
from jambo.schema_converter import SchemaConverter
|
from jambo.schema_converter import SchemaConverter
|
||||||
|
|
||||||
type_parsing = SchemaConverter.build_object(name, properties)
|
type_parsing = SchemaConverter.build_object(name, properties)
|
||||||
|
|||||||
@@ -1,7 +1,4 @@
|
|||||||
from jambo.parser._type_parser import GenericTypeParser
|
from jambo.parser._type_parser import GenericTypeParser
|
||||||
from jambo.utils.properties_builder.mappings_properties_builder import (
|
|
||||||
mappings_properties_builder,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class StringTypeParser(GenericTypeParser):
|
class StringTypeParser(GenericTypeParser):
|
||||||
@@ -9,32 +6,17 @@ class StringTypeParser(GenericTypeParser):
|
|||||||
|
|
||||||
json_schema_type = "string"
|
json_schema_type = "string"
|
||||||
|
|
||||||
@staticmethod
|
type_mappings = {
|
||||||
def from_properties(name, properties):
|
"maxLength": "max_length",
|
||||||
_mappings = {
|
"minLength": "min_length",
|
||||||
"maxLength": "max_length",
|
"pattern": "pattern",
|
||||||
"minLength": "min_length",
|
}
|
||||||
"pattern": "pattern",
|
|
||||||
}
|
|
||||||
|
|
||||||
mapped_properties = mappings_properties_builder(properties, _mappings)
|
def from_properties(self, name, properties, required=False):
|
||||||
|
mapped_properties = self.mappings_properties_builder(properties, required)
|
||||||
|
|
||||||
if "default" in properties:
|
default_value = properties.get("default")
|
||||||
default_value = properties["default"]
|
if default_value is not None:
|
||||||
if not isinstance(default_value, str):
|
self.validate_default(str, mapped_properties, default_value)
|
||||||
raise ValueError(
|
|
||||||
f"Default value for {name} must be a string, "
|
|
||||||
f"but got <{type(properties['default']).__name__}>."
|
|
||||||
)
|
|
||||||
|
|
||||||
if len(default_value) > properties.get("maxLength", float("inf")):
|
|
||||||
raise ValueError(
|
|
||||||
f"Default value for {name} exceeds maxLength limit of {properties.get('maxLength')}"
|
|
||||||
)
|
|
||||||
|
|
||||||
if len(default_value) < properties.get("minLength", 0):
|
|
||||||
raise ValueError(
|
|
||||||
f"Default value for {name} is below minLength limit of {properties.get('minLength')}"
|
|
||||||
)
|
|
||||||
|
|
||||||
return str, mapped_properties
|
return str, mapped_properties
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ from jambo.parser import GenericTypeParser
|
|||||||
from jambo.types.json_schema_type import JSONSchema
|
from jambo.types.json_schema_type import JSONSchema
|
||||||
|
|
||||||
from jsonschema.exceptions import SchemaError
|
from jsonschema.exceptions import SchemaError
|
||||||
from jsonschema.protocols import Validator
|
from jsonschema.validators import validator_for
|
||||||
from pydantic import create_model
|
from pydantic import create_model
|
||||||
from pydantic.fields import Field
|
from pydantic.fields import Field
|
||||||
from pydantic.main import ModelT
|
from pydantic.main import ModelT
|
||||||
@@ -42,7 +42,8 @@ class SchemaConverter:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
try:
|
try:
|
||||||
Validator.check_schema(schema)
|
validator = validator_for(schema)
|
||||||
|
validator.check_schema(schema)
|
||||||
except SchemaError as e:
|
except SchemaError as e:
|
||||||
raise ValueError(f"Invalid JSON Schema: {e}")
|
raise ValueError(f"Invalid JSON Schema: {e}")
|
||||||
|
|
||||||
@@ -71,27 +72,25 @@ class SchemaConverter:
|
|||||||
|
|
||||||
fields = {}
|
fields = {}
|
||||||
for name, prop in properties.items():
|
for name, prop in properties.items():
|
||||||
fields[name] = SchemaConverter._build_field(name, prop, required_keys)
|
is_required = name in required_keys
|
||||||
|
fields[name] = SchemaConverter._build_field(name, prop, is_required)
|
||||||
|
|
||||||
return fields
|
return fields
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _build_field(
|
def _build_field(name, properties: dict, required=False) -> tuple[type, Field]:
|
||||||
name, properties: dict, required_keys: list[str]
|
match properties:
|
||||||
) -> tuple[type, dict]:
|
case {"anyOf": _}:
|
||||||
|
_field_type = "anyOf"
|
||||||
|
case {"allOf": _}:
|
||||||
|
_field_type = "allOf"
|
||||||
|
case {"type": _}:
|
||||||
|
_field_type = properties["type"]
|
||||||
|
case _:
|
||||||
|
raise ValueError(f"Invalid JSON Schema: {properties}")
|
||||||
|
|
||||||
_field_type, _field_args = GenericTypeParser.get_impl(
|
_field_type, _field_args = GenericTypeParser.get_impl(
|
||||||
properties["type"]
|
_field_type
|
||||||
).from_properties(name, properties)
|
).from_properties(name, properties, required)
|
||||||
|
|
||||||
_field_args = _field_args or {}
|
|
||||||
|
|
||||||
if description := properties.get("description"):
|
|
||||||
_field_args["description"] = description
|
|
||||||
|
|
||||||
if name not in required_keys:
|
|
||||||
_field_args["default"] = properties.get("default", None)
|
|
||||||
|
|
||||||
if "default_factory" in _field_args and "default" in _field_args:
|
|
||||||
del _field_args["default"]
|
|
||||||
|
|
||||||
return _field_type, Field(**_field_args)
|
return _field_type, Field(**_field_args)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
from typing import List, Dict, Union, TypedDict, Literal
|
from typing import Dict, List, Literal, TypedDict, Union
|
||||||
|
|
||||||
|
|
||||||
JSONSchemaType = Literal[
|
JSONSchemaType = Literal[
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
def mappings_properties_builder(properties, mappings, default_mappings=None):
|
|
||||||
default_mappings = default_mappings or {
|
|
||||||
"default": "default",
|
|
||||||
"description": "description",
|
|
||||||
}
|
|
||||||
|
|
||||||
mappings = default_mappings | mappings
|
|
||||||
|
|
||||||
return {
|
|
||||||
mappings[key]: value for key, value in properties.items() if key in mappings
|
|
||||||
}
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
from jambo.utils.properties_builder.mappings_properties_builder import (
|
|
||||||
mappings_properties_builder,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def numeric_properties_builder(properties):
|
|
||||||
_mappings = {
|
|
||||||
"minimum": "ge",
|
|
||||||
"exclusiveMinimum": "gt",
|
|
||||||
"maximum": "le",
|
|
||||||
"exclusiveMaximum": "lt",
|
|
||||||
"multipleOf": "multiple_of",
|
|
||||||
"default": "default",
|
|
||||||
}
|
|
||||||
|
|
||||||
mapped_properties = mappings_properties_builder(properties, _mappings)
|
|
||||||
|
|
||||||
if "default" in properties:
|
|
||||||
default_value = properties["default"]
|
|
||||||
if not isinstance(default_value, (int, float)):
|
|
||||||
raise ValueError(
|
|
||||||
f"Default value must be a number, got {type(default_value).__name__}"
|
|
||||||
)
|
|
||||||
|
|
||||||
if default_value > properties.get("maximum", float("inf")):
|
|
||||||
raise ValueError(
|
|
||||||
f"Default value exceeds maximum limit of {properties.get('maximum')}"
|
|
||||||
)
|
|
||||||
|
|
||||||
if default_value < properties.get("minimum", float("-inf")):
|
|
||||||
raise ValueError(
|
|
||||||
f"Default value is below minimum limit of {properties.get('minimum')}"
|
|
||||||
)
|
|
||||||
|
|
||||||
if default_value >= properties.get("exclusiveMaximum", float("inf")):
|
|
||||||
raise ValueError(
|
|
||||||
f"Default value exceeds exclusive maximum limit of {properties.get('exclusiveMaximum')}"
|
|
||||||
)
|
|
||||||
|
|
||||||
if default_value <= properties.get("exclusiveMinimum", float("-inf")):
|
|
||||||
raise ValueError(
|
|
||||||
f"Default value is below exclusive minimum limit of {properties.get('exclusiveMinimum')}"
|
|
||||||
)
|
|
||||||
|
|
||||||
if "multipleOf" in properties:
|
|
||||||
if default_value % properties["multipleOf"] != 0:
|
|
||||||
raise ValueError(
|
|
||||||
f"Default value {default_value} is not a multiple of {properties['multipleOf']}"
|
|
||||||
)
|
|
||||||
|
|
||||||
return mapped_properties
|
|
||||||
@@ -57,8 +57,20 @@ requires = ["hatchling", "hatch-vcs"]
|
|||||||
build-backend = "hatchling.build"
|
build-backend = "hatchling.build"
|
||||||
|
|
||||||
|
|
||||||
|
# Tests
|
||||||
|
[tool.coverage.run]
|
||||||
|
omit = [
|
||||||
|
"tests/*",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# Linters
|
# Linters
|
||||||
|
[tool.ruff.lint]
|
||||||
|
extend-select = ["I"]
|
||||||
|
|
||||||
[tool.ruff.lint.isort]
|
[tool.ruff.lint.isort]
|
||||||
|
known-first-party = ["jambo"]
|
||||||
section-order=[
|
section-order=[
|
||||||
"future",
|
"future",
|
||||||
"first-party",
|
"first-party",
|
||||||
|
|||||||
292
tests/parser/test_allof_type_parser.py
Normal file
292
tests/parser/test_allof_type_parser.py
Normal file
@@ -0,0 +1,292 @@
|
|||||||
|
from jambo.parser.allof_type_parser import AllOfTypeParser
|
||||||
|
|
||||||
|
from unittest import TestCase
|
||||||
|
|
||||||
|
|
||||||
|
class TestAllOfTypeParser(TestCase):
|
||||||
|
def test_all_of_type_parser_object_type(self):
|
||||||
|
"""
|
||||||
|
Test the AllOfTypeParser with an object type and validate the properties.
|
||||||
|
When using allOf with object it should be able to validate the properties
|
||||||
|
and join them correctly.
|
||||||
|
"""
|
||||||
|
properties = {
|
||||||
|
"type": "object",
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"properties": {
|
||||||
|
"name": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"name": {
|
||||||
|
"type": "string",
|
||||||
|
"maxLength": 4,
|
||||||
|
},
|
||||||
|
"age": {
|
||||||
|
"type": "integer",
|
||||||
|
"maximum": 100,
|
||||||
|
"minimum": 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
type_parsing, type_validator = AllOfTypeParser().from_properties(
|
||||||
|
"placeholder", properties
|
||||||
|
)
|
||||||
|
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
type_parsing(name="John", age=101)
|
||||||
|
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
type_parsing(name="", age=30)
|
||||||
|
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
type_parsing(name="John Invalid", age=30)
|
||||||
|
|
||||||
|
obj = type_parsing(name="John", age=30)
|
||||||
|
self.assertEqual(obj.name, "John")
|
||||||
|
self.assertEqual(obj.age, 30)
|
||||||
|
|
||||||
|
def test_all_of_type_parser_object_type_required(self):
|
||||||
|
"""
|
||||||
|
Tests the required properties of the AllOfTypeParser with an object type.
|
||||||
|
"""
|
||||||
|
|
||||||
|
properties = {
|
||||||
|
"type": "object",
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"properties": {
|
||||||
|
"name": {
|
||||||
|
"type": "string",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["name"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"age": {
|
||||||
|
"type": "integer",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["age"],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
type_parsing, type_validator = AllOfTypeParser().from_properties(
|
||||||
|
"placeholder", properties
|
||||||
|
)
|
||||||
|
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
type_parsing(name="John")
|
||||||
|
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
type_parsing(age=30)
|
||||||
|
|
||||||
|
obj = type_parsing(name="John", age=30)
|
||||||
|
self.assertEqual(obj.name, "John")
|
||||||
|
self.assertEqual(obj.age, 30)
|
||||||
|
|
||||||
|
def test_all_of_type_top_level_type(self):
|
||||||
|
"""
|
||||||
|
Tests the AllOfTypeParser with a top-level type and validate the properties.
|
||||||
|
"""
|
||||||
|
|
||||||
|
properties = {
|
||||||
|
"type": "string",
|
||||||
|
"allOf": [
|
||||||
|
{"maxLength": 11},
|
||||||
|
{"maxLength": 4},
|
||||||
|
{"minLength": 1},
|
||||||
|
{"minLength": 2},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
type_parsing, type_validator = AllOfTypeParser().from_properties(
|
||||||
|
"placeholder", properties
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(type_parsing, str)
|
||||||
|
self.assertEqual(type_validator["max_length"], 11)
|
||||||
|
self.assertEqual(type_validator["min_length"], 1)
|
||||||
|
|
||||||
|
def test_all_of_type_parser_in_fields(self):
|
||||||
|
"""
|
||||||
|
Tests the AllOfTypeParser when set in the fields of a model.
|
||||||
|
"""
|
||||||
|
properties = {
|
||||||
|
"allOf": [
|
||||||
|
{"type": "string", "maxLength": 11},
|
||||||
|
{"type": "string", "maxLength": 4},
|
||||||
|
{"type": "string", "minLength": 1},
|
||||||
|
{"type": "string", "minLength": 2},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
type_parsing, type_validator = AllOfTypeParser().from_properties(
|
||||||
|
"placeholder", properties
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(type_parsing, str)
|
||||||
|
self.assertEqual(type_validator["max_length"], 11)
|
||||||
|
self.assertEqual(type_validator["min_length"], 1)
|
||||||
|
|
||||||
|
def test_invalid_all_of(self):
|
||||||
|
"""
|
||||||
|
Tests that an error is raised when the allOf type is not present.
|
||||||
|
"""
|
||||||
|
properties = {
|
||||||
|
"wrongKey": [
|
||||||
|
{"type": "string", "maxLength": 11},
|
||||||
|
{"type": "string", "maxLength": 4},
|
||||||
|
{"type": "string", "minLength": 1},
|
||||||
|
{"type": "string", "minLength": 2},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
AllOfTypeParser().from_properties("placeholder", properties)
|
||||||
|
|
||||||
|
def test_all_of_invalid_type_not_present(self):
|
||||||
|
properties = {
|
||||||
|
"allOf": [
|
||||||
|
{"maxLength": 11},
|
||||||
|
{"maxLength": 4},
|
||||||
|
{"minLength": 1},
|
||||||
|
{"minLength": 2},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
AllOfTypeParser().from_properties("placeholder", properties)
|
||||||
|
|
||||||
|
def test_all_of_invalid_type_in_fields(self):
|
||||||
|
properties = {
|
||||||
|
"allOf": [
|
||||||
|
{"type": "string", "maxLength": 11},
|
||||||
|
{"type": "integer", "maxLength": 4},
|
||||||
|
{"type": "string", "minLength": 1},
|
||||||
|
{"minLength": 2},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
AllOfTypeParser().from_properties("placeholder", properties)
|
||||||
|
|
||||||
|
def test_all_of_description_field(self):
|
||||||
|
"""
|
||||||
|
Tests the AllOfTypeParser with a description field.
|
||||||
|
"""
|
||||||
|
|
||||||
|
properties = {
|
||||||
|
"type": "object",
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"properties": {
|
||||||
|
"name": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "One",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"properties": {
|
||||||
|
"name": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Of",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"properties": {
|
||||||
|
"name": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Us",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
type_parsing, _ = AllOfTypeParser().from_properties("placeholder", properties)
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
type_parsing.schema()["properties"]["name"]["description"],
|
||||||
|
"One | Of | Us",
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_all_of_with_defaults(self):
|
||||||
|
"""
|
||||||
|
Tests the AllOfTypeParser with a default value.
|
||||||
|
"""
|
||||||
|
|
||||||
|
properties = {
|
||||||
|
"type": "object",
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"properties": {
|
||||||
|
"name": {
|
||||||
|
"type": "string",
|
||||||
|
"default": "John",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"properties": {
|
||||||
|
"name": {
|
||||||
|
"type": "string",
|
||||||
|
"default": "John",
|
||||||
|
},
|
||||||
|
"age": {
|
||||||
|
"type": "integer",
|
||||||
|
"default": 30,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
type_parsing, _ = AllOfTypeParser().from_properties("placeholder", properties)
|
||||||
|
obj = type_parsing()
|
||||||
|
self.assertEqual(obj.name, "John")
|
||||||
|
self.assertEqual(obj.age, 30)
|
||||||
|
|
||||||
|
def test_all_of_with_conflicting_defaults(self):
|
||||||
|
"""
|
||||||
|
Tests the AllOfTypeParser with conflicting default values.
|
||||||
|
"""
|
||||||
|
|
||||||
|
properties = {
|
||||||
|
"type": "object",
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"properties": {
|
||||||
|
"name": {
|
||||||
|
"type": "string",
|
||||||
|
"default": "John",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"properties": {
|
||||||
|
"name": {
|
||||||
|
"type": "string",
|
||||||
|
"default": "Doe",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
AllOfTypeParser().from_properties("placeholder", properties)
|
||||||
100
tests/parser/test_anyof_type_parser.py
Normal file
100
tests/parser/test_anyof_type_parser.py
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
from jambo.parser.anyof_type_parser import AnyOfTypeParser
|
||||||
|
|
||||||
|
from typing_extensions import Annotated
|
||||||
|
|
||||||
|
from typing import Union, get_args, get_origin
|
||||||
|
from unittest import TestCase
|
||||||
|
|
||||||
|
|
||||||
|
class TestAnyOfTypeParser(TestCase):
|
||||||
|
def test_any_with_missing_properties(self):
|
||||||
|
properties = {
|
||||||
|
"notAnyOf": [
|
||||||
|
{"type": "string"},
|
||||||
|
{"type": "integer"},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
AnyOfTypeParser().from_properties("placeholder", properties)
|
||||||
|
|
||||||
|
def test_any_of_with_invalid_properties(self):
|
||||||
|
properties = {
|
||||||
|
"anyOf": None,
|
||||||
|
}
|
||||||
|
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
AnyOfTypeParser().from_properties("placeholder", properties)
|
||||||
|
|
||||||
|
def test_any_of_string_or_int(self):
|
||||||
|
"""
|
||||||
|
Tests the AnyOfTypeParser with a string or int type.
|
||||||
|
"""
|
||||||
|
|
||||||
|
properties = {
|
||||||
|
"anyOf": [
|
||||||
|
{"type": "string"},
|
||||||
|
{"type": "integer"},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
type_parsing, _ = AnyOfTypeParser().from_properties(
|
||||||
|
"placeholder", properties, required=True
|
||||||
|
)
|
||||||
|
|
||||||
|
# check union type has string and int
|
||||||
|
self.assertEqual(get_origin(type_parsing), Union)
|
||||||
|
|
||||||
|
type_1, type_2 = get_args(type_parsing)
|
||||||
|
|
||||||
|
self.assertEqual(get_origin(type_1), Annotated)
|
||||||
|
self.assertIn(str, get_args(type_1))
|
||||||
|
|
||||||
|
self.assertEqual(get_origin(type_2), Annotated)
|
||||||
|
self.assertIn(int, get_args(type_2))
|
||||||
|
|
||||||
|
def test_any_of_string_or_int_with_default(self):
|
||||||
|
"""
|
||||||
|
Tests the AnyOfTypeParser with a string or int type and a default value.
|
||||||
|
"""
|
||||||
|
|
||||||
|
properties = {
|
||||||
|
"anyOf": [
|
||||||
|
{"type": "string"},
|
||||||
|
{"type": "integer"},
|
||||||
|
],
|
||||||
|
"default": 42,
|
||||||
|
}
|
||||||
|
|
||||||
|
type_parsing, type_validator = AnyOfTypeParser().from_properties(
|
||||||
|
"placeholder", properties
|
||||||
|
)
|
||||||
|
|
||||||
|
# check union type has string and int
|
||||||
|
self.assertEqual(get_origin(type_parsing), Union)
|
||||||
|
|
||||||
|
type_1, type_2 = get_args(type_parsing)
|
||||||
|
|
||||||
|
self.assertEqual(get_origin(type_1), Annotated)
|
||||||
|
self.assertIn(str, get_args(type_1))
|
||||||
|
|
||||||
|
self.assertEqual(get_origin(type_2), Annotated)
|
||||||
|
self.assertIn(int, get_args(type_2))
|
||||||
|
|
||||||
|
self.assertEqual(type_validator["default"], 42)
|
||||||
|
|
||||||
|
def test_any_string_or_int_with_invalid_defaults(self):
|
||||||
|
"""
|
||||||
|
Tests the AnyOfTypeParser with a string or int type and an invalid default value.
|
||||||
|
"""
|
||||||
|
|
||||||
|
properties = {
|
||||||
|
"anyOf": [
|
||||||
|
{"type": "string"},
|
||||||
|
{"type": "integer"},
|
||||||
|
],
|
||||||
|
"default": 3.14,
|
||||||
|
}
|
||||||
|
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
AnyOfTypeParser().from_properties("placeholder", properties)
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
|
from jambo.parser import ArrayTypeParser
|
||||||
|
|
||||||
from typing import get_args
|
from typing import get_args
|
||||||
from unittest import TestCase
|
from unittest import TestCase
|
||||||
|
|
||||||
from jambo.parser import ArrayTypeParser
|
|
||||||
|
|
||||||
|
|
||||||
class TestArrayTypeParser(TestCase):
|
class TestArrayTypeParser(TestCase):
|
||||||
def test_array_parser_no_options(self):
|
def test_array_parser_no_options(self):
|
||||||
@@ -66,38 +66,25 @@ class TestArrayTypeParser(TestCase):
|
|||||||
|
|
||||||
properties = {"items": {"type": "string"}, "default": ["a", 1, "c"]}
|
properties = {"items": {"type": "string"}, "default": ["a", 1, "c"]}
|
||||||
|
|
||||||
with self.assertRaises(ValueError) as context:
|
with self.assertRaises(ValueError):
|
||||||
parser.from_properties("placeholder", properties)
|
parser.from_properties("placeholder", properties)
|
||||||
|
|
||||||
self.assertEqual(
|
|
||||||
str(context.exception),
|
|
||||||
"All items in the default list must be of type str",
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_array_parser_with_invalid_default_type(self):
|
def test_array_parser_with_invalid_default_type(self):
|
||||||
parser = ArrayTypeParser()
|
parser = ArrayTypeParser()
|
||||||
|
|
||||||
properties = {"items": {"type": "string"}, "default": "not_a_list"}
|
properties = {"items": {"type": "string"}, "default": "not_a_list"}
|
||||||
|
|
||||||
with self.assertRaises(ValueError) as context:
|
with self.assertRaises(ValueError):
|
||||||
parser.from_properties("placeholder", properties)
|
parser.from_properties("placeholder", properties)
|
||||||
|
|
||||||
self.assertEqual(
|
|
||||||
str(context.exception), "Default value must be a list, got str"
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_array_parser_with_invalid_default_min(self):
|
def test_array_parser_with_invalid_default_min(self):
|
||||||
parser = ArrayTypeParser()
|
parser = ArrayTypeParser()
|
||||||
|
|
||||||
properties = {"items": {"type": "string"}, "default": ["a"], "minItems": 2}
|
properties = {"items": {"type": "string"}, "default": ["a"], "minItems": 2}
|
||||||
|
|
||||||
with self.assertRaises(ValueError) as context:
|
with self.assertRaises(ValueError):
|
||||||
parser.from_properties("placeholder", properties)
|
parser.from_properties("placeholder", properties)
|
||||||
|
|
||||||
self.assertEqual(
|
|
||||||
str(context.exception), "Default list is below minItems limit of 2"
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_array_parser_with_invalid_default_max(self):
|
def test_array_parser_with_invalid_default_max(self):
|
||||||
parser = ArrayTypeParser()
|
parser = ArrayTypeParser()
|
||||||
|
|
||||||
@@ -107,9 +94,5 @@ class TestArrayTypeParser(TestCase):
|
|||||||
"maxItems": 3,
|
"maxItems": 3,
|
||||||
}
|
}
|
||||||
|
|
||||||
with self.assertRaises(ValueError) as context:
|
with self.assertRaises(ValueError):
|
||||||
parser.from_properties("placeholder", properties)
|
parser.from_properties("placeholder", properties)
|
||||||
|
|
||||||
self.assertEqual(
|
|
||||||
str(context.exception), "Default list exceeds maxItems limit of 3"
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from unittest import TestCase
|
|
||||||
|
|
||||||
from jambo.parser import BooleanTypeParser
|
from jambo.parser import BooleanTypeParser
|
||||||
|
|
||||||
|
from unittest import TestCase
|
||||||
|
|
||||||
|
|
||||||
class TestBoolTypeParser(TestCase):
|
class TestBoolTypeParser(TestCase):
|
||||||
def test_bool_parser_no_options(self):
|
def test_bool_parser_no_options(self):
|
||||||
@@ -12,7 +12,7 @@ class TestBoolTypeParser(TestCase):
|
|||||||
type_parsing, type_validator = parser.from_properties("placeholder", properties)
|
type_parsing, type_validator = parser.from_properties("placeholder", properties)
|
||||||
|
|
||||||
self.assertEqual(type_parsing, bool)
|
self.assertEqual(type_parsing, bool)
|
||||||
self.assertEqual(type_validator, {})
|
self.assertEqual(type_validator, {"default": None})
|
||||||
|
|
||||||
def test_bool_parser_with_default(self):
|
def test_bool_parser_with_default(self):
|
||||||
parser = BooleanTypeParser()
|
parser = BooleanTypeParser()
|
||||||
@@ -26,3 +26,14 @@ class TestBoolTypeParser(TestCase):
|
|||||||
|
|
||||||
self.assertEqual(type_parsing, bool)
|
self.assertEqual(type_parsing, bool)
|
||||||
self.assertEqual(type_validator["default"], True)
|
self.assertEqual(type_validator["default"], True)
|
||||||
|
|
||||||
|
def test_bool_parser_with_invalid_default(self):
|
||||||
|
parser = BooleanTypeParser()
|
||||||
|
|
||||||
|
properties = {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": "invalid",
|
||||||
|
}
|
||||||
|
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
parser.from_properties("placeholder", properties)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from unittest import TestCase
|
|
||||||
|
|
||||||
from jambo.parser import FloatTypeParser
|
from jambo.parser import FloatTypeParser
|
||||||
|
|
||||||
|
from unittest import TestCase
|
||||||
|
|
||||||
|
|
||||||
class TestFloatTypeParser(TestCase):
|
class TestFloatTypeParser(TestCase):
|
||||||
def test_float_parser_no_options(self):
|
def test_float_parser_no_options(self):
|
||||||
@@ -12,7 +12,7 @@ class TestFloatTypeParser(TestCase):
|
|||||||
type_parsing, type_validator = parser.from_properties("placeholder", properties)
|
type_parsing, type_validator = parser.from_properties("placeholder", properties)
|
||||||
|
|
||||||
self.assertEqual(type_parsing, float)
|
self.assertEqual(type_parsing, float)
|
||||||
self.assertEqual(type_validator, {})
|
self.assertEqual(type_validator, {"default": None})
|
||||||
|
|
||||||
def test_float_parser_with_options(self):
|
def test_float_parser_with_options(self):
|
||||||
parser = FloatTypeParser()
|
parser = FloatTypeParser()
|
||||||
@@ -61,14 +61,9 @@ class TestFloatTypeParser(TestCase):
|
|||||||
"multipleOf": 0.5,
|
"multipleOf": 0.5,
|
||||||
}
|
}
|
||||||
|
|
||||||
with self.assertRaises(ValueError) as context:
|
with self.assertRaises(ValueError):
|
||||||
parser.from_properties("placeholder", properties)
|
parser.from_properties("placeholder", properties)
|
||||||
|
|
||||||
self.assertEqual(
|
|
||||||
str(context.exception),
|
|
||||||
"Default value must be a number, got str",
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_float_parser_with_default_invalid_maximum(self):
|
def test_float_parser_with_default_invalid_maximum(self):
|
||||||
parser = FloatTypeParser()
|
parser = FloatTypeParser()
|
||||||
|
|
||||||
@@ -80,14 +75,9 @@ class TestFloatTypeParser(TestCase):
|
|||||||
"multipleOf": 0.5,
|
"multipleOf": 0.5,
|
||||||
}
|
}
|
||||||
|
|
||||||
with self.assertRaises(ValueError) as context:
|
with self.assertRaises(ValueError):
|
||||||
parser.from_properties("placeholder", properties)
|
parser.from_properties("placeholder", properties)
|
||||||
|
|
||||||
self.assertEqual(
|
|
||||||
str(context.exception),
|
|
||||||
"Default value exceeds maximum limit of 10.5",
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_float_parser_with_default_invalid_minimum(self):
|
def test_float_parser_with_default_invalid_minimum(self):
|
||||||
parser = FloatTypeParser()
|
parser = FloatTypeParser()
|
||||||
|
|
||||||
@@ -99,14 +89,9 @@ class TestFloatTypeParser(TestCase):
|
|||||||
"multipleOf": 0.5,
|
"multipleOf": 0.5,
|
||||||
}
|
}
|
||||||
|
|
||||||
with self.assertRaises(ValueError) as context:
|
with self.assertRaises(ValueError):
|
||||||
parser.from_properties("placeholder", properties)
|
parser.from_properties("placeholder", properties)
|
||||||
|
|
||||||
self.assertEqual(
|
|
||||||
str(context.exception),
|
|
||||||
"Default value is below minimum limit of 1.0",
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_float_parser_with_default_invalid_exclusive_maximum(self):
|
def test_float_parser_with_default_invalid_exclusive_maximum(self):
|
||||||
parser = FloatTypeParser()
|
parser = FloatTypeParser()
|
||||||
|
|
||||||
@@ -118,14 +103,9 @@ class TestFloatTypeParser(TestCase):
|
|||||||
"multipleOf": 0.5,
|
"multipleOf": 0.5,
|
||||||
}
|
}
|
||||||
|
|
||||||
with self.assertRaises(ValueError) as context:
|
with self.assertRaises(ValueError):
|
||||||
parser.from_properties("placeholder", properties)
|
parser.from_properties("placeholder", properties)
|
||||||
|
|
||||||
self.assertEqual(
|
|
||||||
str(context.exception),
|
|
||||||
"Default value exceeds exclusive maximum limit of 10.5",
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_float_parser_with_default_invalid_exclusive_minimum(self):
|
def test_float_parser_with_default_invalid_exclusive_minimum(self):
|
||||||
parser = FloatTypeParser()
|
parser = FloatTypeParser()
|
||||||
|
|
||||||
@@ -137,14 +117,9 @@ class TestFloatTypeParser(TestCase):
|
|||||||
"multipleOf": 0.5,
|
"multipleOf": 0.5,
|
||||||
}
|
}
|
||||||
|
|
||||||
with self.assertRaises(ValueError) as context:
|
with self.assertRaises(ValueError):
|
||||||
parser.from_properties("placeholder", properties)
|
parser.from_properties("placeholder", properties)
|
||||||
|
|
||||||
self.assertEqual(
|
|
||||||
str(context.exception),
|
|
||||||
"Default value is below exclusive minimum limit of 1.0",
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_float_parser_with_default_invalid_multiple(self):
|
def test_float_parser_with_default_invalid_multiple(self):
|
||||||
parser = FloatTypeParser()
|
parser = FloatTypeParser()
|
||||||
|
|
||||||
@@ -156,10 +131,5 @@ class TestFloatTypeParser(TestCase):
|
|||||||
"multipleOf": 2.0,
|
"multipleOf": 2.0,
|
||||||
}
|
}
|
||||||
|
|
||||||
with self.assertRaises(ValueError) as context:
|
with self.assertRaises(ValueError):
|
||||||
parser.from_properties("placeholder", properties)
|
parser.from_properties("placeholder", properties)
|
||||||
|
|
||||||
self.assertEqual(
|
|
||||||
str(context.exception),
|
|
||||||
"Default value 5.0 is not a multiple of 2.0",
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from unittest import TestCase
|
|
||||||
|
|
||||||
from jambo.parser import IntTypeParser
|
from jambo.parser import IntTypeParser
|
||||||
|
|
||||||
|
from unittest import TestCase
|
||||||
|
|
||||||
|
|
||||||
class TestIntTypeParser(TestCase):
|
class TestIntTypeParser(TestCase):
|
||||||
def test_int_parser_no_options(self):
|
def test_int_parser_no_options(self):
|
||||||
@@ -12,7 +12,7 @@ class TestIntTypeParser(TestCase):
|
|||||||
type_parsing, type_validator = parser.from_properties("placeholder", properties)
|
type_parsing, type_validator = parser.from_properties("placeholder", properties)
|
||||||
|
|
||||||
self.assertEqual(type_parsing, int)
|
self.assertEqual(type_parsing, int)
|
||||||
self.assertEqual(type_validator, {})
|
self.assertEqual(type_validator, {"default": None})
|
||||||
|
|
||||||
def test_int_parser_with_options(self):
|
def test_int_parser_with_options(self):
|
||||||
parser = IntTypeParser()
|
parser = IntTypeParser()
|
||||||
@@ -61,14 +61,9 @@ class TestIntTypeParser(TestCase):
|
|||||||
"multipleOf": 2,
|
"multipleOf": 2,
|
||||||
}
|
}
|
||||||
|
|
||||||
with self.assertRaises(ValueError) as context:
|
with self.assertRaises(ValueError):
|
||||||
parser.from_properties("placeholder", properties)
|
parser.from_properties("placeholder", properties)
|
||||||
|
|
||||||
self.assertEqual(
|
|
||||||
str(context.exception),
|
|
||||||
"Default value must be a number, got str",
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_int_parser_with_default_invalid_maximum(self):
|
def test_int_parser_with_default_invalid_maximum(self):
|
||||||
parser = IntTypeParser()
|
parser = IntTypeParser()
|
||||||
|
|
||||||
@@ -80,14 +75,9 @@ class TestIntTypeParser(TestCase):
|
|||||||
"multipleOf": 2,
|
"multipleOf": 2,
|
||||||
}
|
}
|
||||||
|
|
||||||
with self.assertRaises(ValueError) as context:
|
with self.assertRaises(ValueError):
|
||||||
parser.from_properties("placeholder", properties)
|
parser.from_properties("placeholder", properties)
|
||||||
|
|
||||||
self.assertEqual(
|
|
||||||
str(context.exception),
|
|
||||||
"Default value exceeds maximum limit of 10",
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_int_parser_with_default_invalid_minimum(self):
|
def test_int_parser_with_default_invalid_minimum(self):
|
||||||
parser = IntTypeParser()
|
parser = IntTypeParser()
|
||||||
|
|
||||||
@@ -99,14 +89,9 @@ class TestIntTypeParser(TestCase):
|
|||||||
"multipleOf": 2,
|
"multipleOf": 2,
|
||||||
}
|
}
|
||||||
|
|
||||||
with self.assertRaises(ValueError) as context:
|
with self.assertRaises(ValueError):
|
||||||
parser.from_properties("placeholder", properties)
|
parser.from_properties("placeholder", properties)
|
||||||
|
|
||||||
self.assertEqual(
|
|
||||||
str(context.exception),
|
|
||||||
"Default value is below minimum limit of 1",
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_int_parser_with_default_invalid_exclusive_maximum(self):
|
def test_int_parser_with_default_invalid_exclusive_maximum(self):
|
||||||
parser = IntTypeParser()
|
parser = IntTypeParser()
|
||||||
|
|
||||||
@@ -118,14 +103,9 @@ class TestIntTypeParser(TestCase):
|
|||||||
"multipleOf": 2,
|
"multipleOf": 2,
|
||||||
}
|
}
|
||||||
|
|
||||||
with self.assertRaises(ValueError) as context:
|
with self.assertRaises(ValueError):
|
||||||
parser.from_properties("placeholder", properties)
|
parser.from_properties("placeholder", properties)
|
||||||
|
|
||||||
self.assertEqual(
|
|
||||||
str(context.exception),
|
|
||||||
"Default value exceeds exclusive maximum limit of 10",
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_int_parser_with_default_invalid_exclusive_minimum(self):
|
def test_int_parser_with_default_invalid_exclusive_minimum(self):
|
||||||
parser = IntTypeParser()
|
parser = IntTypeParser()
|
||||||
|
|
||||||
@@ -137,14 +117,9 @@ class TestIntTypeParser(TestCase):
|
|||||||
"multipleOf": 2,
|
"multipleOf": 2,
|
||||||
}
|
}
|
||||||
|
|
||||||
with self.assertRaises(ValueError) as context:
|
with self.assertRaises(ValueError):
|
||||||
parser.from_properties("placeholder", properties)
|
parser.from_properties("placeholder", properties)
|
||||||
|
|
||||||
self.assertEqual(
|
|
||||||
str(context.exception),
|
|
||||||
"Default value is below exclusive minimum limit of 1",
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_int_parser_with_default_invalid_multipleOf(self):
|
def test_int_parser_with_default_invalid_multipleOf(self):
|
||||||
parser = IntTypeParser()
|
parser = IntTypeParser()
|
||||||
|
|
||||||
@@ -156,10 +131,5 @@ class TestIntTypeParser(TestCase):
|
|||||||
"multipleOf": 2,
|
"multipleOf": 2,
|
||||||
}
|
}
|
||||||
|
|
||||||
with self.assertRaises(ValueError) as context:
|
with self.assertRaises(ValueError):
|
||||||
parser.from_properties("placeholder", properties)
|
parser.from_properties("placeholder", properties)
|
||||||
|
|
||||||
self.assertEqual(
|
|
||||||
str(context.exception),
|
|
||||||
"Default value 5 is not a multiple of 2",
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from unittest import TestCase
|
|
||||||
|
|
||||||
from jambo.parser import ObjectTypeParser
|
from jambo.parser import ObjectTypeParser
|
||||||
|
|
||||||
|
from unittest import TestCase
|
||||||
|
|
||||||
|
|
||||||
class TestObjectTypeParser(TestCase):
|
class TestObjectTypeParser(TestCase):
|
||||||
def test_object_type_parser(self):
|
def test_object_type_parser(self):
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from unittest import TestCase
|
|
||||||
|
|
||||||
from jambo.parser import StringTypeParser
|
from jambo.parser import StringTypeParser
|
||||||
|
|
||||||
|
from unittest import TestCase
|
||||||
|
|
||||||
|
|
||||||
class TestStringTypeParser(TestCase):
|
class TestStringTypeParser(TestCase):
|
||||||
def test_string_parser_no_options(self):
|
def test_string_parser_no_options(self):
|
||||||
@@ -57,14 +57,9 @@ class TestStringTypeParser(TestCase):
|
|||||||
"minLength": 5,
|
"minLength": 5,
|
||||||
}
|
}
|
||||||
|
|
||||||
with self.assertRaises(ValueError) as context:
|
with self.assertRaises(ValueError):
|
||||||
parser.from_properties("placeholder", properties)
|
parser.from_properties("placeholder", properties)
|
||||||
|
|
||||||
self.assertEqual(
|
|
||||||
str(context.exception),
|
|
||||||
"Default value for placeholder must be a string, but got <int>.",
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_string_parser_with_default_invalid_maxlength(self):
|
def test_string_parser_with_default_invalid_maxlength(self):
|
||||||
parser = StringTypeParser()
|
parser = StringTypeParser()
|
||||||
|
|
||||||
@@ -75,14 +70,9 @@ class TestStringTypeParser(TestCase):
|
|||||||
"minLength": 1,
|
"minLength": 1,
|
||||||
}
|
}
|
||||||
|
|
||||||
with self.assertRaises(ValueError) as context:
|
with self.assertRaises(ValueError):
|
||||||
parser.from_properties("placeholder", properties)
|
parser.from_properties("placeholder", properties)
|
||||||
|
|
||||||
self.assertEqual(
|
|
||||||
str(context.exception),
|
|
||||||
"Default value for placeholder exceeds maxLength limit of 2",
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_string_parser_with_default_invalid_minlength(self):
|
def test_string_parser_with_default_invalid_minlength(self):
|
||||||
parser = StringTypeParser()
|
parser = StringTypeParser()
|
||||||
|
|
||||||
@@ -93,10 +83,5 @@ class TestStringTypeParser(TestCase):
|
|||||||
"minLength": 2,
|
"minLength": 2,
|
||||||
}
|
}
|
||||||
|
|
||||||
with self.assertRaises(ValueError) as context:
|
with self.assertRaises(ValueError):
|
||||||
parser.from_properties("placeholder", properties)
|
parser.from_properties("placeholder", properties)
|
||||||
|
|
||||||
self.assertEqual(
|
|
||||||
str(context.exception),
|
|
||||||
"Default value for placeholder is below minLength limit of 2",
|
|
||||||
)
|
|
||||||
|
|||||||
31
tests/parser/test_type_parser.py
Normal file
31
tests/parser/test_type_parser.py
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
from jambo.parser._type_parser import GenericTypeParser
|
||||||
|
|
||||||
|
from unittest import TestCase
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidGenericTypeParser(GenericTypeParser):
|
||||||
|
mapped_type = str
|
||||||
|
json_schema_type = "invalid"
|
||||||
|
|
||||||
|
def from_properties(
|
||||||
|
self, name: str, properties: dict[str, any], required: bool = False
|
||||||
|
): ...
|
||||||
|
|
||||||
|
|
||||||
|
class TestGenericTypeParser(TestCase):
|
||||||
|
def test_invalid_get_impl(self):
|
||||||
|
# Assuming GenericTypeParser is imported from the module
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
GenericTypeParser.get_impl("another_invalid_type")
|
||||||
|
|
||||||
|
def test_invalid_json_schema_type(self):
|
||||||
|
InvalidGenericTypeParser.json_schema_type = None
|
||||||
|
|
||||||
|
# This is more for the developer's sanity check
|
||||||
|
with self.assertRaises(RuntimeError):
|
||||||
|
GenericTypeParser.get_impl("another_invalid_type")
|
||||||
|
|
||||||
|
def test_invalid_mappings_properties_builder(self):
|
||||||
|
parser = InvalidGenericTypeParser()
|
||||||
|
with self.assertRaises(NotImplementedError):
|
||||||
|
parser.mappings_properties_builder({}, required=False)
|
||||||
@@ -10,6 +10,59 @@ def is_pydantic_model(cls):
|
|||||||
|
|
||||||
|
|
||||||
class TestSchemaConverter(TestCase):
|
class TestSchemaConverter(TestCase):
|
||||||
|
def test_build_expects_title(self):
|
||||||
|
schema = {
|
||||||
|
"description": "A person",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"name": {"type": "string"},
|
||||||
|
"age": {"type": "integer"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
SchemaConverter.build(schema)
|
||||||
|
|
||||||
|
def test_build_expects_valid_schema(self):
|
||||||
|
invalid_schema = {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"name": {
|
||||||
|
"type": "strng"
|
||||||
|
} # typo: "strng" is not a valid JSON Schema type
|
||||||
|
},
|
||||||
|
"required": ["name"],
|
||||||
|
}
|
||||||
|
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
SchemaConverter.build_object("placeholder", invalid_schema)
|
||||||
|
|
||||||
|
def test_build_expects_object(self):
|
||||||
|
schema = {
|
||||||
|
"title": "Person",
|
||||||
|
"description": "A person",
|
||||||
|
"type": "string",
|
||||||
|
}
|
||||||
|
|
||||||
|
with self.assertRaises(TypeError):
|
||||||
|
SchemaConverter.build(schema)
|
||||||
|
|
||||||
|
def test_is_invalid_field(self):
|
||||||
|
schema = {
|
||||||
|
"title": "Person",
|
||||||
|
"description": "A person",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"notType": "string",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
# 'required': ['name', 'age', 'is_active', 'friends', 'address'],
|
||||||
|
}
|
||||||
|
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
SchemaConverter.build(schema)
|
||||||
|
|
||||||
def test_jsonschema_to_pydantic(self):
|
def test_jsonschema_to_pydantic(self):
|
||||||
schema = {
|
schema = {
|
||||||
"title": "Person",
|
"title": "Person",
|
||||||
@@ -281,3 +334,66 @@ class TestSchemaConverter(TestCase):
|
|||||||
|
|
||||||
self.assertEqual(obj.address.street, "123 Main St")
|
self.assertEqual(obj.address.street, "123 Main St")
|
||||||
self.assertEqual(obj.address.city, "Springfield")
|
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": 11},
|
||||||
|
{"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="")
|
||||||
|
|
||||||
|
def test_any_of(self):
|
||||||
|
schema = {
|
||||||
|
"title": "Person",
|
||||||
|
"description": "A person",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"anyOf": [
|
||||||
|
{"type": "string", "maxLength": 11, "minLength": 1},
|
||||||
|
{"type": "integer", "maximum": 10},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
Model = SchemaConverter.build(schema)
|
||||||
|
|
||||||
|
obj = Model(id=1)
|
||||||
|
self.assertEqual(obj.id, 1)
|
||||||
|
|
||||||
|
obj = Model(id="12345678901")
|
||||||
|
self.assertEqual(obj.id, "12345678901")
|
||||||
|
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
Model(id="")
|
||||||
|
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
Model(id="12345678901234567890")
|
||||||
|
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
Model(id=11)
|
||||||
|
|||||||
Reference in New Issue
Block a user