Skip to content

Commit

Permalink
feat: Support maxLength and similar keywords in string, integer and…
Browse files Browse the repository at this point in the history
… number JSON schema helpers
  • Loading branch information
edgarrmondragon committed Feb 20, 2024
1 parent 2c90d03 commit 8ac041d
Show file tree
Hide file tree
Showing 6 changed files with 183 additions and 26 deletions.
3 changes: 3 additions & 0 deletions docs/_templates/class.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,6 @@
.. autoclass:: {{ name }}
:members:
:special-members: __init__, __call__
{%- if name in ('IntegerType', 'NumberType') %}
:inherited-members: JSONTypeHelper
{%- endif %}
3 changes: 2 additions & 1 deletion docs/classes/typing/singer_sdk.typing.IntegerType.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@

.. autoclass:: IntegerType
:members:
:special-members: __init__, __call__
:special-members: __init__, __call__
:inherited-members: JSONTypeHelper
3 changes: 2 additions & 1 deletion docs/classes/typing/singer_sdk.typing.NumberType.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@

.. autoclass:: NumberType
:members:
:special-members: __init__, __call__
:special-members: __init__, __call__
:inherited-members: JSONTypeHelper
11 changes: 11 additions & 0 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
"sphinx.ext.napoleon",
"sphinx.ext.autosectionlabel",
"sphinx.ext.autosummary",
"sphinx.ext.extlinks",
"sphinx.ext.intersphinx",
"sphinx.ext.linkcode",
"sphinx_copybutton",
Expand Down Expand Up @@ -138,6 +139,16 @@
"porting.html": "guides/porting.html",
}

# -- Options for extlinks -----------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/extensions/extlinks.html

extlinks = {
"jsonschema": (
"https://json-schema.org/understanding-json-schema/reference/%s",
"%s",
),
}

# -- Options for intersphinx -----------------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/extensions/intersphinx.html#configuration
intersphinx_mapping = {
Expand Down
145 changes: 121 additions & 24 deletions singer_sdk/typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -266,22 +266,46 @@ class StringType(JSONTypeHelper[str]):
{'type': ['string']}
>>> StringType(allowed_values=["a", "b"]).type_dict
{'type': ['string'], 'enum': ['a', 'b']}
>>> StringType(max_length=10).type_dict
{'type': ['string'], 'maxLength': 10}
"""

string_format: str | None = None
"""String format.
See the `formats built into the JSON Schema specification`_.
See the :jsonschema:`JSON Schema reference <string#built-in-formats>` for a list of
all the built-in formats.
Returns:
A string describing the format.
.. _`formats built into the JSON Schema specification`:
https://json-schema.org/understanding-json-schema/reference/string.html#built-in-formats
"""

def __init__(
self,
*,
min_length: int | None = None,
max_length: int | None = None,
pattern: str | None = None,
**kwargs: t.Any,
) -> None:
"""Initialize StringType.
Args:
min_length: Minimum length of the string. See the
:jsonschema:`JSON Schema reference <string#length>` for details.
max_length: Maximum length of the string. See the
:jsonschema:`JSON Schema reference <string#length>` for details.
pattern: A regular expression pattern that the string must match. See the
:jsonschema:`JSON Schema reference <string#regexp>` for details.
**kwargs: Additional keyword arguments to pass to the parent class.
"""
super().__init__(**kwargs)
self.min_length = min_length
self.max_length = max_length
self.pattern = pattern

@property
def _format(self) -> dict:
def _format(self) -> dict[str, t.Any]:
return {"format": self.string_format} if self.string_format else {}

@DefaultInstanceProperty
Expand All @@ -291,12 +315,23 @@ def type_dict(self) -> dict:
Returns:
A dictionary describing the type.
"""
return {
result = {
"type": ["string"],
**self._format,
**self.extras,
}

if self.max_length is not None:
result["maxLength"] = self.max_length

if self.min_length is not None:
result["minLength"] = self.min_length

if self.pattern is not None:
result["pattern"] = self.pattern

return result


class DateTimeType(StringType):
"""DateTime type.
Expand Down Expand Up @@ -423,7 +458,71 @@ def type_dict(self) -> dict:
return {"type": ["boolean"], **self.extras}


class IntegerType(JSONTypeHelper):
class _NumericType(JSONTypeHelper[T]):
"""Abstract numeric type for integers and numbers."""

__type_name__: str

def __init__(
self,
*,
minimum: int | None = None,
maximum: int | None = None,
exclusive_minimum: int | None = None,
exclusive_maximum: int | None = None,
multiple_of: int | None = None,
**kwargs: t.Any,
) -> None:
"""Initialize IntegerType.
Args:
minimum: Minimum numeric value. See the
:jsonschema:`JSON Schema reference <numeric#range>` for details.
maximum: Maximum numeric value.
:jsonschema:`JSON Schema reference <numeric#range>` for details.
exclusive_minimum: Exclusive minimum numeric value.
:jsonschema:`JSON Schema reference <numeric#range>` for details.
exclusive_maximum: Exclusive maximum numeric value. See the
:jsonschema:`JSON Schema reference <numeric#range>` for details.
multiple_of: A number that the value must be a multiple of. See the
:jsonschema:`JSON Schema reference <numeric#multiples>` for details.
**kwargs: Additional keyword arguments to pass to the parent class.
"""
super().__init__(**kwargs)
self.minimum = minimum
self.maximum = maximum
self.exclusive_minimum = exclusive_minimum
self.exclusive_maximum = exclusive_maximum
self.multiple_of = multiple_of

@DefaultInstanceProperty
def type_dict(self) -> dict:
"""Get type dictionary.
Returns:
A dictionary describing the type.
"""
result = {"type": [self.__type_name__], **self.extras}

if self.minimum is not None:
result["minimum"] = self.minimum

if self.maximum is not None:
result["maximum"] = self.maximum

if self.exclusive_minimum is not None:
result["exclusiveMinimum"] = self.exclusive_minimum

if self.exclusive_maximum is not None:
result["exclusiveMaximum"] = self.exclusive_maximum

if self.multiple_of is not None:
result["multipleOf"] = self.multiple_of

return result


class IntegerType(_NumericType[int]):
"""Integer type.
Examples:
Expand All @@ -433,19 +532,18 @@ class IntegerType(JSONTypeHelper):
{'type': ['integer']}
>>> IntegerType(allowed_values=[1, 2]).type_dict
{'type': ['integer'], 'enum': [1, 2]}
>>> IntegerType(minimum=0, maximum=10).type_dict
{'type': ['integer'], 'minimum': 0, 'maximum': 10}
>>> IntegerType(exclusive_minimum=0, exclusive_maximum=10).type_dict
{'type': ['integer'], 'exclusiveMinimum': 0, 'exclusiveMaximum': 10}
>>> IntegerType(multiple_of=2).type_dict
{'type': ['integer'], 'multipleOf': 2}
"""

@DefaultInstanceProperty
def type_dict(self) -> dict:
"""Get type dictionary.
Returns:
A dictionary describing the type.
"""
return {"type": ["integer"], **self.extras}
__type_name__ = "integer"


class NumberType(JSONTypeHelper[float]):
class NumberType(_NumericType[float]):
"""Number type.
Examples:
Expand All @@ -455,16 +553,15 @@ class NumberType(JSONTypeHelper[float]):
{'type': ['number']}
>>> NumberType(allowed_values=[1.0, 2.0]).type_dict
{'type': ['number'], 'enum': [1.0, 2.0]}
>>> NumberType(minimum=0, maximum=10).type_dict
{'type': ['number'], 'minimum': 0, 'maximum': 10}
>>> NumberType(exclusive_minimum=0, exclusive_maximum=10).type_dict
{'type': ['number'], 'exclusiveMinimum': 0, 'exclusiveMaximum': 10}
>>> NumberType(multiple_of=2).type_dict
{'type': ['number'], 'multipleOf': 2}
"""

@DefaultInstanceProperty
def type_dict(self) -> dict:
"""Get type dictionary.
Returns:
A dictionary describing the type.
"""
return {"type": ["number"], **self.extras}
__type_name__ = "number"


W = t.TypeVar("W", bound=JSONTypeHelper)
Expand Down
44 changes: 44 additions & 0 deletions tests/core/test_jsonschema_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -502,6 +502,50 @@ def test_inbuilt_type(json_type: JSONTypeHelper, expected_json_schema: dict):
},
{is_array_type, is_string_array_type},
),
(
Property(
"my_prop12",
StringType(min_length=5, max_length=10, pattern="^a.*b$"),
),
{
"my_prop12": {
"type": ["string", "null"],
"minLength": 5,
"maxLength": 10,
"pattern": "^a.*b$",
},
},
{is_string_type},
),
(
Property(
"my_prop13",
IntegerType(minimum=5, maximum=10),
),
{
"my_prop13": {
"type": ["integer", "null"],
"minimum": 5,
"maximum": 10,
},
},
{is_integer_type},
),
(
Property(
"my_prop14",
IntegerType(exclusive_minimum=5, exclusive_maximum=10, multiple_of=2),
),
{
"my_prop14": {
"type": ["integer", "null"],
"exclusiveMinimum": 5,
"exclusiveMaximum": 10,
"multipleOf": 2,
},
},
{is_integer_type},
),
],
)
def test_property_creation(
Expand Down

0 comments on commit 8ac041d

Please sign in to comment.