Skip to content

Commit

Permalink
feat: Support boolean additional_properties in JSON schema helper o…
Browse files Browse the repository at this point in the history
…bjects (#1188)

* feat: Support boolean `additional_properties` in JSON schema helper objects

* Add more test cases for `ObjectType`

* Remove re-implementation of to_json

* Re-add test case

Co-authored-by: Cody J. Hanson <[email protected]>
  • Loading branch information
edgarrmondragon and cjohnhanson authored Dec 7, 2022
1 parent 9d6a48a commit 747a202
Show file tree
Hide file tree
Showing 6 changed files with 304 additions and 16 deletions.
107 changes: 91 additions & 16 deletions singer_sdk/typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,17 @@ def to_dict(self) -> dict:
"""
return cast(dict, self.type_dict)

def to_json(self, **kwargs: Any) -> str:
"""Convert to JSON.
Args:
kwargs: Additional keyword arguments to pass to json.dumps().
Returns:
A JSON string describing the object.
"""
return json.dumps(self.to_dict(), **kwargs)


class StringType(JSONTypeHelper):
"""String type."""
Expand Down Expand Up @@ -449,17 +460,89 @@ class ObjectType(JSONTypeHelper):
def __init__(
self,
*properties: Property,
additional_properties: W | type[W] | None = None,
additional_properties: W | type[W] | bool | None = None,
pattern_properties: Mapping[str, W | type[W]] | None = None,
) -> None:
"""Initialize ObjectType from its list of properties.
Args:
properties: Zero or more attributes for this JSON object.
additional_properties: A schema to match against unnamed properties in
this object.
this object, or a boolean indicating if extra properties are allowed.
pattern_properties: A dictionary of regex patterns to match against
property names, and the schema to match against the values.
Examples:
>>> t = ObjectType(
... Property("name", StringType, required=True),
... Property("age", IntegerType),
... Property("height", NumberType),
... additional_properties=False,
... )
>>> print(t.to_json(indent=2))
{
"type": "object",
"properties": {
"name": {
"type": [
"string"
]
},
"age": {
"type": [
"integer",
"null"
]
},
"height": {
"type": [
"number",
"null"
]
}
},
"required": [
"name"
],
"additionalProperties": false
}
>>> t = ObjectType(
... Property("name", StringType, required=True),
... Property("age", IntegerType),
... Property("height", NumberType),
... additional_properties=StringType,
... )
>>> print(t.to_json(indent=2))
{
"type": "object",
"properties": {
"name": {
"type": [
"string"
]
},
"age": {
"type": [
"integer",
"null"
]
},
"height": {
"type": [
"number",
"null"
]
}
},
"required": [
"name"
],
"additionalProperties": {
"type": [
"string"
]
}
}
"""
self.wrapped: list[Property] = list(properties)
self.additional_properties = additional_properties
Expand All @@ -478,13 +561,16 @@ def type_dict(self) -> dict: # type: ignore # OK: @classproperty vs @property
merged_props.update(w.to_dict())
if not w.optional:
required.append(w.name)
result = {"type": "object", "properties": merged_props}
result: dict = {"type": "object", "properties": merged_props}

if required:
result["required"] = required

if self.additional_properties:
result["additionalProperties"] = self.additional_properties.type_dict
if self.additional_properties is not None:
if isinstance(self.additional_properties, bool):
result["additionalProperties"] = self.additional_properties
else:
result["additionalProperties"] = self.additional_properties.type_dict

if self.pattern_properties:
result["patternProperties"] = {
Expand All @@ -493,17 +579,6 @@ def type_dict(self) -> dict: # type: ignore # OK: @classproperty vs @property

return result

def to_json(self, **kwargs: Any) -> str:
"""Return a JSON string representation of the object.
Args:
**kwargs: Additional keyword arguments to pass to `json.dumps`.
Returns:
A JSON string.
"""
return json.dumps(self.type_dict, **kwargs)


class CustomType(JSONTypeHelper):
"""Accepts an arbitrary JSON Schema dictionary."""
Expand Down
88 changes: 88 additions & 0 deletions tests/core/test_jsonschema_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import re
from pathlib import Path
from textwrap import dedent
from typing import Callable

import pytest
Expand Down Expand Up @@ -79,6 +80,48 @@ def discover_streams(self) -> list[Stream]:
return []


def test_to_json():
schema = PropertiesList(
Property(
"test_property",
StringType,
description="A test property",
required=True,
),
Property(
"test_property_2",
StringType,
description="A test property",
),
additional_properties=False,
)
assert schema.to_json(indent=4) == dedent(
"""\
{
"type": "object",
"properties": {
"test_property": {
"type": [
"string"
],
"description": "A test property"
},
"test_property_2": {
"type": [
"string",
"null"
],
"description": "A test property"
}
},
"required": [
"test_property"
],
"additionalProperties": false
}"""
)


def test_nested_complex_objects():
test1a = Property(
"Datasets",
Expand Down Expand Up @@ -467,6 +510,16 @@ def test_array_type():
"additional_properties.json",
id="no required, no duplicates, additional properties",
),
pytest.param(
ObjectType(
Property("id", StringType),
Property("email", StringType),
Property("username", StringType),
Property("phone_number", StringType),
additional_properties=False,
),
"no_additional_properties.json",
),
pytest.param(
ObjectType(
Property("id", StringType),
Expand All @@ -490,6 +543,18 @@ def test_array_type():
"duplicates_additional_properties.json",
id="no required, duplicates, additional properties",
),
pytest.param(
ObjectType(
Property("id", StringType),
Property("id", StringType),
Property("email", StringType),
Property("username", StringType),
Property("phone_number", StringType),
additional_properties=False,
),
"duplicates_no_additional_properties.json",
id="no required, duplicates, no additional properties allowed",
),
pytest.param(
ObjectType(
Property("id", StringType),
Expand All @@ -511,6 +576,17 @@ def test_array_type():
"required_additional_properties.json",
id="required, no duplicates, additional properties",
),
pytest.param(
ObjectType(
Property("id", StringType),
Property("email", StringType, required=True),
Property("username", StringType, required=True),
Property("phone_number", StringType),
additional_properties=False,
),
"required_no_additional_properties.json",
id="required, no duplicates, no additional properties allowed",
),
pytest.param(
ObjectType(
Property("id", StringType),
Expand All @@ -534,6 +610,18 @@ def test_array_type():
"required_duplicates_additional_properties.json",
id="required, duplicates, additional properties",
),
pytest.param(
ObjectType(
Property("id", StringType),
Property("email", StringType, True),
Property("email", StringType, True),
Property("username", StringType, True),
Property("phone_number", StringType),
additional_properties=False,
),
"required_duplicates_no_additional_properties.json",
id="required, duplicates, no additional properties allowed",
),
pytest.param(
ObjectType(
Property("id", StringType),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"type": "object",
"properties": {
"id": {
"type": [
"string",
"null"
]
},
"email": {
"type": [
"string",
"null"
]
},
"username": {
"type": [
"string",
"null"
]
},
"phone_number": {
"type": [
"string",
"null"
]
}
},
"additionalProperties": false
}
30 changes: 30 additions & 0 deletions tests/snapshots/jsonschema/no_additional_properties.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"type": "object",
"properties": {
"id": {
"type": [
"string",
"null"
]
},
"email": {
"type": [
"string",
"null"
]
},
"username": {
"type": [
"string",
"null"
]
},
"phone_number": {
"type": [
"string",
"null"
]
}
},
"additionalProperties": false
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
{
"type": "object",
"properties": {
"id": {
"type": [
"string",
"null"
]
},
"email": {
"type": [
"string"
]
},
"username": {
"type": [
"string"
]
},
"phone_number": {
"type": [
"string",
"null"
]
}
},
"required": [
"email",
"email",
"username"
],
"additionalProperties": false
}
Loading

0 comments on commit 747a202

Please sign in to comment.