Initial Fields Validators #1
66
.github/workflows/build.yml
vendored
Normal file
66
.github/workflows/build.yml
vendored
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
name: Example
|
||||||
|
|
||||||
|
|
||||||
|
on:
|
||||||
|
push
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
name: run-tests
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
python-version:
|
||||||
|
- "3.10"
|
||||||
|
- "3.11"
|
||||||
|
- "3.12"
|
||||||
|
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install uv
|
||||||
|
uses: astral-sh/setup-uv@v5
|
||||||
|
with:
|
||||||
|
# Install a specific version of uv.
|
||||||
|
version: "0.6.14"
|
||||||
|
enable-cache: true
|
||||||
|
cache-dependency-glob: "uv.lock"
|
||||||
|
python-version: ${{ matrix.python-version }}
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: uv sync --all-extras --dev
|
||||||
|
|
||||||
|
- name: Run tests
|
||||||
|
run: uv run poe tests
|
||||||
|
|
||||||
|
publish:
|
||||||
|
name: publish
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: startsWith(github.ref, 'refs/tags/v')
|
||||||
|
needs: [test]
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Install uv
|
||||||
|
uses: astral-sh/setup-uv@v5
|
||||||
|
with:
|
||||||
|
# Install a specific version of uv.
|
||||||
|
version: "0.6.14"
|
||||||
|
enable-cache: true
|
||||||
|
cache-dependency-glob: "uv.lock"
|
||||||
|
|
||||||
|
- name: Set up Python
|
||||||
|
run: uv python install
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: uv build
|
||||||
|
|
||||||
|
- name: Publish
|
||||||
|
run: uv publish -t ${{ secrets.PYPI_TOKEN }}
|
||||||
@@ -1,5 +1,8 @@
|
|||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from typing import Generic, Self, TypeVar
|
from typing import Generic, TypeVar
|
||||||
|
from typing_extensions import Self
|
||||||
|
|
||||||
|
from pydantic import Field
|
||||||
|
|
||||||
T = TypeVar("T")
|
T = TypeVar("T")
|
||||||
|
|
||||||
@@ -17,7 +20,7 @@ class GenericTypeParser(ABC, Generic[T]):
|
|||||||
@abstractmethod
|
@abstractmethod
|
||||||
def from_properties(
|
def from_properties(
|
||||||
name: str, properties: dict[str, any]
|
name: str, properties: dict[str, any]
|
||||||
) -> tuple[type[T], dict[str, any]]: ...
|
) -> tuple[type[T], Field]: ...
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_impl(cls, type_name: str) -> Self:
|
def get_impl(cls, type_name: str) -> Self:
|
||||||
32
jambo/parser/array_type_parser.py
Normal file
32
jambo/parser/array_type_parser.py
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
from jambo.parser._type_parser import GenericTypeParser
|
||||||
|
|
||||||
|
from typing import TypeVar
|
||||||
|
|
||||||
|
from jambo.utils.properties_builder.mappings_properties_builder import (
|
||||||
|
mappings_properties_builder,
|
||||||
|
)
|
||||||
|
|
||||||
|
V = TypeVar("V")
|
||||||
|
|
||||||
|
|
||||||
|
class ArrayTypeParser(GenericTypeParser):
|
||||||
|
mapped_type = list
|
||||||
|
|
||||||
|
json_schema_type = "array"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_properties(cls, name, properties):
|
||||||
|
_item_type, _item_args = GenericTypeParser.get_impl(
|
||||||
|
properties["items"]["type"]
|
||||||
|
).from_properties(name, properties["items"])
|
||||||
|
|
||||||
|
_mappings = {
|
||||||
|
"maxItems": "max_length",
|
||||||
|
"minItems": "min_length",
|
||||||
|
}
|
||||||
|
|
||||||
|
wrapper_type = set if properties.get("uniqueItems", False) else list
|
||||||
|
|
||||||
|
return wrapper_type[_item_type], mappings_properties_builder(
|
||||||
|
properties, _mappings
|
||||||
|
)
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
from jambo.types._type_parser import GenericTypeParser
|
from jambo.parser._type_parser import GenericTypeParser
|
||||||
|
|
||||||
|
|
||||||
class BooleanTypeParser(GenericTypeParser):
|
class BooleanTypeParser(GenericTypeParser):
|
||||||
@@ -8,4 +8,4 @@ class BooleanTypeParser(GenericTypeParser):
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def from_properties(name, properties):
|
def from_properties(name, properties):
|
||||||
return bool, {}
|
return bool, {} # The second argument is not used in this case
|
||||||
12
jambo/parser/float_type_parser.py
Normal file
12
jambo/parser/float_type_parser.py
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
from jambo.parser._type_parser import GenericTypeParser
|
||||||
|
from jambo.utils.properties_builder.numeric_properties_builder import numeric_properties_builder
|
||||||
|
|
||||||
|
|
||||||
|
class FloatTypeParser(GenericTypeParser):
|
||||||
|
mapped_type = float
|
||||||
|
|
||||||
|
json_schema_type = "number"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def from_properties(name, properties):
|
||||||
|
return float, numeric_properties_builder(properties)
|
||||||
12
jambo/parser/int_type_parser.py
Normal file
12
jambo/parser/int_type_parser.py
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
from jambo.parser._type_parser import GenericTypeParser
|
||||||
|
from jambo.utils.properties_builder.numeric_properties_builder import numeric_properties_builder
|
||||||
|
|
||||||
|
|
||||||
|
class IntTypeParser(GenericTypeParser):
|
||||||
|
mapped_type = int
|
||||||
|
|
||||||
|
json_schema_type = "integer"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def from_properties(name, properties):
|
||||||
|
return int, numeric_properties_builder(properties)
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
from jambo.types._type_parser import GenericTypeParser
|
from jambo.parser._type_parser import GenericTypeParser
|
||||||
|
|
||||||
|
|
||||||
class ObjectTypeParser(GenericTypeParser):
|
class ObjectTypeParser(GenericTypeParser):
|
||||||
@@ -10,5 +10,7 @@ class ObjectTypeParser(GenericTypeParser):
|
|||||||
def from_properties(name, properties):
|
def from_properties(name, properties):
|
||||||
from jambo.schema_converter import SchemaConverter
|
from jambo.schema_converter import SchemaConverter
|
||||||
|
|
||||||
_type = SchemaConverter.build_object(name, properties)
|
return (
|
||||||
return _type, {}
|
SchemaConverter.build_object(name, properties),
|
||||||
|
{}, # The second argument is not used in this case
|
||||||
|
)
|
||||||
20
jambo/parser/string_type_parser.py
Normal file
20
jambo/parser/string_type_parser.py
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
from jambo.parser._type_parser import GenericTypeParser
|
||||||
|
from jambo.utils.properties_builder.mappings_properties_builder import (
|
||||||
|
mappings_properties_builder,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class StringTypeParser(GenericTypeParser):
|
||||||
|
mapped_type = str
|
||||||
|
|
||||||
|
json_schema_type = "string"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def from_properties(name, properties):
|
||||||
|
_mappings = {
|
||||||
|
"maxLength": "max_length",
|
||||||
|
"minLength": "min_length",
|
||||||
|
"pattern": "pattern",
|
||||||
|
}
|
||||||
|
|
||||||
|
return str, mappings_properties_builder(properties, _mappings)
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
from jambo.types import GenericTypeParser
|
from jambo.parser import GenericTypeParser
|
||||||
|
|
||||||
from jsonschema.exceptions import SchemaError
|
from jsonschema.exceptions import SchemaError
|
||||||
from jsonschema.protocols import Validator
|
from jsonschema.protocols import Validator
|
||||||
@@ -7,27 +7,47 @@ from pydantic.fields import Field
|
|||||||
|
|
||||||
from typing import Type
|
from typing import Type
|
||||||
|
|
||||||
|
from jambo.types.json_schema_type import JSONSchema
|
||||||
|
|
||||||
|
|
||||||
class SchemaConverter:
|
class SchemaConverter:
|
||||||
@staticmethod
|
"""
|
||||||
def build(schema):
|
Converts JSON Schema to Pydantic models.
|
||||||
try:
|
|
||||||
Validator.check_schema(schema)
|
|
||||||
except SchemaError as e:
|
|
||||||
raise ValueError(f"Invalid JSON Schema: {e}")
|
|
||||||
|
|
||||||
if schema["type"] != "object":
|
This class is responsible for converting JSON Schema definitions into Pydantic models.
|
||||||
raise TypeError(
|
It validates the schema and generates the corresponding Pydantic model with appropriate
|
||||||
f"Invalid JSON Schema: {schema['type']}. Only 'object' can be converted to Pydantic models."
|
fields and types. The generated model can be used for data validation and serialization.
|
||||||
)
|
"""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def build(schema: JSONSchema) -> Type:
|
||||||
|
"""
|
||||||
|
Converts a JSON Schema to a Pydantic model.
|
||||||
|
:param schema: The JSON Schema to convert.
|
||||||
|
:return: A Pydantic model class.
|
||||||
|
"""
|
||||||
|
if "title" not in schema:
|
||||||
|
raise ValueError("JSON Schema must have a title.")
|
||||||
|
|
||||||
return SchemaConverter.build_object(schema["title"], schema)
|
return SchemaConverter.build_object(schema["title"], schema)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def build_object(
|
def build_object(
|
||||||
name: str,
|
name: str,
|
||||||
schema: dict,
|
schema: JSONSchema,
|
||||||
):
|
) -> Type:
|
||||||
|
"""
|
||||||
|
Converts a JSON Schema object to a Pydantic model given a name.
|
||||||
|
:param name:
|
||||||
|
:param schema:
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
Validator.check_schema(schema)
|
||||||
|
except SchemaError as e:
|
||||||
|
raise ValueError(f"Invalid JSON Schema: {e}")
|
||||||
|
|
||||||
if schema["type"] != "object":
|
if schema["type"] != "object":
|
||||||
raise TypeError(
|
raise TypeError(
|
||||||
f"Invalid JSON Schema: {schema['type']}. Only 'object' can be converted to Pydantic models."
|
f"Invalid JSON Schema: {schema['type']}. Only 'object' can be converted to Pydantic models."
|
||||||
@@ -60,7 +80,7 @@ class SchemaConverter:
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def _build_field(
|
def _build_field(
|
||||||
name, properties: dict, required_keys: list[str]
|
name, properties: dict, required_keys: list[str]
|
||||||
) -> tuple[type, Field]:
|
) -> tuple[type, dict]:
|
||||||
_field_type, _field_args = GenericTypeParser.get_impl(
|
_field_type, _field_args = GenericTypeParser.get_impl(
|
||||||
properties["type"]
|
properties["type"]
|
||||||
).from_properties(name, properties)
|
).from_properties(name, properties)
|
||||||
|
|||||||
@@ -1,19 +0,0 @@
|
|||||||
from jambo.types._type_parser import GenericTypeParser
|
|
||||||
|
|
||||||
from typing import TypeVar
|
|
||||||
|
|
||||||
V = TypeVar("V")
|
|
||||||
|
|
||||||
|
|
||||||
class ArrayTypeParser(GenericTypeParser):
|
|
||||||
mapped_type = list
|
|
||||||
|
|
||||||
json_schema_type = "array"
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def from_properties(cls, name, properties):
|
|
||||||
_item_type, _item_args = GenericTypeParser.get_impl(
|
|
||||||
properties["items"]["type"]
|
|
||||||
).from_properties(name, properties["items"])
|
|
||||||
|
|
||||||
return list[_item_type], {}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
from jambo.types._type_parser import GenericTypeParser
|
|
||||||
|
|
||||||
|
|
||||||
class FloatTypeParser(GenericTypeParser):
|
|
||||||
mapped_type = float
|
|
||||||
|
|
||||||
json_schema_type = "number"
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def from_properties(name, properties):
|
|
||||||
return float, {}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
from jambo.types._type_parser import GenericTypeParser
|
|
||||||
|
|
||||||
|
|
||||||
class IntTypeParser(GenericTypeParser):
|
|
||||||
mapped_type = int
|
|
||||||
|
|
||||||
json_schema_type = "integer"
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def from_properties(name, properties):
|
|
||||||
return int, {}
|
|
||||||
80
jambo/types/json_schema_type.py
Normal file
80
jambo/types/json_schema_type.py
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
from typing import List, Dict, Union, TypedDict, Literal
|
||||||
|
|
||||||
|
|
||||||
|
JSONSchemaType = Literal[
|
||||||
|
"string", "number", "integer", "boolean", "object", "array", "null"
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
JSONType = Union[str, int, float, bool, None, Dict[str, "JSONType"], List["JSONType"]]
|
||||||
|
|
||||||
|
|
||||||
|
class JSONSchema(TypedDict, total=False):
|
||||||
|
# Basic metadata
|
||||||
|
title: str
|
||||||
|
description: str
|
||||||
|
default: JSONType
|
||||||
|
examples: List[JSONType]
|
||||||
|
|
||||||
|
# Type definitions
|
||||||
|
type: Union[JSONSchemaType, List[JSONSchemaType]]
|
||||||
|
|
||||||
|
# Object-specific keywords
|
||||||
|
properties: Dict[str, "JSONSchema"]
|
||||||
|
required: List[str]
|
||||||
|
additionalProperties: Union[bool, "JSONSchema"]
|
||||||
|
minProperties: int
|
||||||
|
maxProperties: int
|
||||||
|
patternProperties: Dict[str, "JSONSchema"]
|
||||||
|
dependencies: Dict[str, Union[List[str], "JSONSchema"]]
|
||||||
|
|
||||||
|
# Array-specific keywords
|
||||||
|
items: Union["JSONSchema", List["JSONSchema"]]
|
||||||
|
additionalItems: Union[bool, "JSONSchema"]
|
||||||
|
minItems: int
|
||||||
|
maxItems: int
|
||||||
|
uniqueItems: bool
|
||||||
|
|
||||||
|
# String-specific keywords
|
||||||
|
minLength: int
|
||||||
|
maxLength: int
|
||||||
|
pattern: str
|
||||||
|
format: str
|
||||||
|
|
||||||
|
# Number-specific keywords
|
||||||
|
minimum: float
|
||||||
|
maximum: float
|
||||||
|
exclusiveMinimum: float
|
||||||
|
exclusiveMaximum: float
|
||||||
|
multipleOf: float
|
||||||
|
|
||||||
|
# Enum and const
|
||||||
|
enum: List[JSONType]
|
||||||
|
const: JSONType
|
||||||
|
|
||||||
|
# Conditionals
|
||||||
|
if_: "JSONSchema" # 'if' is a reserved word in Python
|
||||||
|
then: "JSONSchema"
|
||||||
|
else_: "JSONSchema" # 'else' is also a reserved word
|
||||||
|
|
||||||
|
# Combination keywords
|
||||||
|
allOf: List["JSONSchema"]
|
||||||
|
anyOf: List["JSONSchema"]
|
||||||
|
oneOf: List["JSONSchema"]
|
||||||
|
not_: "JSONSchema" # 'not' is a reserved word
|
||||||
|
|
||||||
|
|
||||||
|
# Fix forward references
|
||||||
|
JSONSchema.__annotations__["properties"] = Dict[str, JSONSchema]
|
||||||
|
JSONSchema.__annotations__["items"] = Union[JSONSchema, List[JSONSchema]]
|
||||||
|
JSONSchema.__annotations__["additionalItems"] = Union[bool, JSONSchema]
|
||||||
|
JSONSchema.__annotations__["additionalProperties"] = Union[bool, JSONSchema]
|
||||||
|
JSONSchema.__annotations__["patternProperties"] = Dict[str, JSONSchema]
|
||||||
|
JSONSchema.__annotations__["dependencies"] = Dict[str, Union[List[str], JSONSchema]]
|
||||||
|
JSONSchema.__annotations__["if_"] = JSONSchema
|
||||||
|
JSONSchema.__annotations__["then"] = JSONSchema
|
||||||
|
JSONSchema.__annotations__["else_"] = JSONSchema
|
||||||
|
JSONSchema.__annotations__["allOf"] = List[JSONSchema]
|
||||||
|
JSONSchema.__annotations__["anyOf"] = List[JSONSchema]
|
||||||
|
JSONSchema.__annotations__["oneOf"] = List[JSONSchema]
|
||||||
|
JSONSchema.__annotations__["not_"] = JSONSchema
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
from jambo.types._type_parser import GenericTypeParser
|
|
||||||
|
|
||||||
|
|
||||||
class StringTypeParser(GenericTypeParser):
|
|
||||||
mapped_type = str
|
|
||||||
|
|
||||||
json_schema_type = "string"
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def from_properties(name, properties):
|
|
||||||
return str, {}
|
|
||||||
0
jambo/utils/__init__.py
Normal file
0
jambo/utils/__init__.py
Normal file
0
jambo/utils/properties_builder/__init__.py
Normal file
0
jambo/utils/properties_builder/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
def mappings_properties_builder(properties, mappings):
|
||||||
|
return {
|
||||||
|
mappings[key]: value for key, value in properties.items() if key in mappings
|
||||||
|
}
|
||||||
15
jambo/utils/properties_builder/numeric_properties_builder.py
Normal file
15
jambo/utils/properties_builder/numeric_properties_builder.py
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
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",
|
||||||
|
}
|
||||||
|
|
||||||
|
return mappings_properties_builder(properties, _mappings)
|
||||||
0
jambo/utils/types/__init__.py
Normal file
0
jambo/utils/types/__init__.py
Normal file
@@ -21,7 +21,7 @@ dev = [
|
|||||||
# POE Tasks
|
# POE Tasks
|
||||||
[tool.poe.tasks]
|
[tool.poe.tasks]
|
||||||
create-hooks = "bash .githooks/set-hooks.sh"
|
create-hooks = "bash .githooks/set-hooks.sh"
|
||||||
|
tests = "python -m unittest discover -s tests -v"
|
||||||
|
|
||||||
# Build System
|
# Build System
|
||||||
[tool.hatch.version]
|
[tool.hatch.version]
|
||||||
|
|||||||
@@ -32,7 +32,13 @@ class TestSchemaConverter(TestCase):
|
|||||||
"description": "A person",
|
"description": "A person",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"name": {"type": "string"},
|
"name": {"type": "string", "maxLength": 4, "minLength": 1},
|
||||||
|
"email": {
|
||||||
|
"type": "string",
|
||||||
|
"maxLength": 50,
|
||||||
|
"minLength": 5,
|
||||||
|
"pattern": r"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
"required": ["name"],
|
"required": ["name"],
|
||||||
}
|
}
|
||||||
@@ -41,13 +47,29 @@ class TestSchemaConverter(TestCase):
|
|||||||
|
|
||||||
self.assertEqual(model(name="John", age=30).name, "John")
|
self.assertEqual(model(name="John", age=30).name, "John")
|
||||||
|
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
model(name=123, age=30, email="teste@hideyoshi.com")
|
||||||
|
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
model(name="John Invalid", age=45, email="teste@hideyoshi.com")
|
||||||
|
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
model(name="", age=45, email="teste@hideyoshi.com")
|
||||||
|
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
model(name="John", age=45, email="hideyoshi.com")
|
||||||
|
|
||||||
def test_validation_integer(self):
|
def test_validation_integer(self):
|
||||||
schema = {
|
schema = {
|
||||||
"title": "Person",
|
"title": "Person",
|
||||||
"description": "A person",
|
"description": "A person",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"age": {"type": "integer"},
|
"age": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 0,
|
||||||
|
"maximum": 120,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
"required": ["age"],
|
"required": ["age"],
|
||||||
}
|
}
|
||||||
@@ -56,7 +78,11 @@ class TestSchemaConverter(TestCase):
|
|||||||
|
|
||||||
self.assertEqual(model(age=30).age, 30)
|
self.assertEqual(model(age=30).age, 30)
|
||||||
|
|
||||||
self.assertEqual(model(age="30").age, 30)
|
with self.assertRaises(ValueError):
|
||||||
|
model(age=-1)
|
||||||
|
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
model(age=121)
|
||||||
|
|
||||||
def test_validation_float(self):
|
def test_validation_float(self):
|
||||||
schema = {
|
schema = {
|
||||||
@@ -64,7 +90,11 @@ class TestSchemaConverter(TestCase):
|
|||||||
"description": "A person",
|
"description": "A person",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"age": {"type": "number"},
|
"age": {
|
||||||
|
"type": "number",
|
||||||
|
"minimum": 0,
|
||||||
|
"maximum": 120,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
"required": ["age"],
|
"required": ["age"],
|
||||||
}
|
}
|
||||||
@@ -73,7 +103,11 @@ class TestSchemaConverter(TestCase):
|
|||||||
|
|
||||||
self.assertEqual(model(age=30).age, 30.0)
|
self.assertEqual(model(age=30).age, 30.0)
|
||||||
|
|
||||||
self.assertEqual(model(age="30").age, 30.0)
|
with self.assertRaises(ValueError):
|
||||||
|
model(age=-1.0)
|
||||||
|
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
model(age=121.0)
|
||||||
|
|
||||||
def test_validation_boolean(self):
|
def test_validation_boolean(self):
|
||||||
schema = {
|
schema = {
|
||||||
@@ -98,14 +132,28 @@ class TestSchemaConverter(TestCase):
|
|||||||
"description": "A person",
|
"description": "A person",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"friends": {"type": "array", "items": {"type": "string"}},
|
"friends": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {"type": "string"},
|
||||||
|
"minItems": 1,
|
||||||
|
"maxItems": 2,
|
||||||
|
"uniqueItems": True,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
"required": ["friends"],
|
"required": ["friends"],
|
||||||
}
|
}
|
||||||
|
|
||||||
model = SchemaConverter.build(schema)
|
model = SchemaConverter.build(schema)
|
||||||
|
|
||||||
self.assertEqual(model(friends=["John", "Jane"]).friends, ["John", "Jane"])
|
self.assertEqual(
|
||||||
|
model(friends=["John", "Jane", "John"]).friends, {"John", "Jane"}
|
||||||
|
)
|
||||||
|
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
model(friends=[])
|
||||||
|
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
model(friends=["John", "Jane", "Invalid"])
|
||||||
|
|
||||||
def test_validation_object(self):
|
def test_validation_object(self):
|
||||||
schema = {
|
schema = {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
from jambo.types import (
|
from jambo.parser import (
|
||||||
ArrayTypeParser,
|
ArrayTypeParser,
|
||||||
FloatTypeParser,
|
FloatTypeParser,
|
||||||
GenericTypeParser,
|
GenericTypeParser,
|
||||||
@@ -21,21 +21,65 @@ class TestTypeParser(unittest.TestCase):
|
|||||||
|
|
||||||
def test_int_parser(self):
|
def test_int_parser(self):
|
||||||
parser = IntTypeParser()
|
parser = IntTypeParser()
|
||||||
expected_definition = (int, {})
|
|
||||||
|
|
||||||
self.assertEqual(parser.from_properties("placeholder", {}), expected_definition)
|
type_parsing, type_validator = parser.from_properties(
|
||||||
|
"placeholder",
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 0,
|
||||||
|
"exclusiveMinimum": 1,
|
||||||
|
"maximum": 10,
|
||||||
|
"exclusiveMaximum": 11,
|
||||||
|
"multipleOf": 2,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(type_parsing, int)
|
||||||
|
self.assertEqual(type_validator["ge"], 0)
|
||||||
|
self.assertEqual(type_validator["gt"], 1)
|
||||||
|
self.assertEqual(type_validator["le"], 10)
|
||||||
|
self.assertEqual(type_validator["lt"], 11)
|
||||||
|
self.assertEqual(type_validator["multiple_of"], 2)
|
||||||
|
|
||||||
def test_float_parser(self):
|
def test_float_parser(self):
|
||||||
parser = FloatTypeParser()
|
parser = FloatTypeParser()
|
||||||
expected_definition = (float, {})
|
|
||||||
|
|
||||||
self.assertEqual(parser.from_properties("placeholder", {}), expected_definition)
|
type_parsing, type_validator = parser.from_properties(
|
||||||
|
"placeholder",
|
||||||
|
{
|
||||||
|
"type": "number",
|
||||||
|
"minimum": 0,
|
||||||
|
"exclusiveMinimum": 1,
|
||||||
|
"maximum": 10,
|
||||||
|
"exclusiveMaximum": 11,
|
||||||
|
"multipleOf": 2,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(type_parsing, float)
|
||||||
|
self.assertEqual(type_validator["ge"], 0)
|
||||||
|
self.assertEqual(type_validator["gt"], 1)
|
||||||
|
self.assertEqual(type_validator["le"], 10)
|
||||||
|
self.assertEqual(type_validator["lt"], 11)
|
||||||
|
self.assertEqual(type_validator["multiple_of"], 2)
|
||||||
|
|
||||||
def test_string_parser(self):
|
def test_string_parser(self):
|
||||||
parser = StringTypeParser()
|
parser = StringTypeParser()
|
||||||
expected_definition = (str, {})
|
|
||||||
|
|
||||||
self.assertEqual(parser.from_properties("placeholder", {}), expected_definition)
|
type_parsing, type_validator = parser.from_properties(
|
||||||
|
"placeholder",
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"maxLength": 10,
|
||||||
|
"minLength": 1,
|
||||||
|
"pattern": "[a-zA-Z0-9]",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(type_parsing, str)
|
||||||
|
self.assertEqual(type_validator["max_length"], 10)
|
||||||
|
self.assertEqual(type_validator["min_length"], 1)
|
||||||
|
self.assertEqual(type_validator["pattern"], "[a-zA-Z0-9]")
|
||||||
|
|
||||||
def test_object_parser(self):
|
def test_object_parser(self):
|
||||||
parser = ObjectTypeParser()
|
parser = ObjectTypeParser()
|
||||||
@@ -69,18 +113,26 @@ class TestTypeParser(unittest.TestCase):
|
|||||||
parser = ArrayTypeParser()
|
parser = ArrayTypeParser()
|
||||||
|
|
||||||
properties = {
|
properties = {
|
||||||
|
"type": "array",
|
||||||
"items": {
|
"items": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"name": {"type": "string"},
|
"name": {"type": "string"},
|
||||||
"age": {"type": "integer"},
|
"age": {"type": "integer"},
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
|
"maxItems": 10,
|
||||||
|
"minItems": 1,
|
||||||
|
"uniqueItems": True,
|
||||||
}
|
}
|
||||||
|
|
||||||
_type, _args = parser.from_properties("placeholder", properties)
|
type_parsing, type_validator = parser.from_properties("placeholder", properties)
|
||||||
|
|
||||||
Model = get_args(_type)[0]
|
self.assertEqual(type_parsing.__origin__, set)
|
||||||
|
self.assertEqual(type_validator["max_length"], 10)
|
||||||
|
self.assertEqual(type_validator["min_length"], 1)
|
||||||
|
|
||||||
|
Model = get_args(type_parsing)[0]
|
||||||
obj = Model(name="name", age=10)
|
obj = Model(name="name", age=10)
|
||||||
|
|
||||||
self.assertEqual(obj.name, "name")
|
self.assertEqual(obj.name, "name")
|
||||||
|
|||||||
Reference in New Issue
Block a user