Skip to content

Commit

Permalink
feat: Support union schemas
Browse files Browse the repository at this point in the history
  • Loading branch information
edgarrmondragon committed Mar 24, 2023
1 parent 7ff1beb commit 2ebaa94
Show file tree
Hide file tree
Showing 6 changed files with 187 additions and 0 deletions.
8 changes: 8 additions & 0 deletions docs/classes/typing/singer_sdk.typing.Constant.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
singer_sdk.typing.Constant
==========================

.. currentmodule:: singer_sdk.typing

.. autoclass:: Constant
:members:
:special-members: __init__, __call__
8 changes: 8 additions & 0 deletions docs/classes/typing/singer_sdk.typing.Discriminator.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
singer_sdk.typing.Discriminator
===============================

.. currentmodule:: singer_sdk.typing

.. autoclass:: Discriminator
:members:
:special-members: __init__, __call__
8 changes: 8 additions & 0 deletions docs/classes/typing/singer_sdk.typing.OneOf.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
singer_sdk.typing.OneOf
=======================

.. currentmodule:: singer_sdk.typing

.. autoclass:: OneOf
:members:
:special-members: __init__, __call__
3 changes: 3 additions & 0 deletions docs/reference.rst
Original file line number Diff line number Diff line change
Expand Up @@ -92,9 +92,11 @@ JSON Schema builder classes
typing.PropertiesList
typing.ArrayType
typing.BooleanType
typing.Constant
typing.CustomType
typing.DateTimeType
typing.DateType
typing.Discriminator
typing.DurationType
typing.EmailType
typing.HostnameType
Expand All @@ -104,6 +106,7 @@ JSON Schema builder classes
typing.JSONPointerType
typing.NumberType
typing.ObjectType
typing.OneOf
typing.Property
typing.RegexType
typing.RelativeJSONPointerType
Expand Down
113 changes: 113 additions & 0 deletions singer_sdk/typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -608,6 +608,119 @@ def type_dict(self) -> dict: # type: ignore # OK: @classproperty vs @property
return result


class OneOf(JSONPointerType):
"""OneOf type."""

def __init__(self, *types: W | type[W]) -> None:
"""Initialize OneOf type.
Args:
types: Types to choose from.
"""
self.wrapped = types

@property
def type_dict(self) -> dict:
"""Get type dictionary.
Returns:
A dictionary describing the type.
"""
return {"oneOf": [t.type_dict for t in self.wrapped]}


class Constant(JSONPointerType):
"""A constant property."""

def __init__(self, value: _JsonValue) -> None:
"""Initialize Constant.
Args:
value: Value of the constant.
"""
self.value = value

@property
def type_dict(self) -> dict:
"""Get type dictionary.
Returns:
A dictionary describing the type.
"""
return {"const": self.value}


class Discriminator(OneOf):
"""A discriminator property.
This is a special case of :class:`singer_sdk.typing.OneOf`, where values are
JSON objects, and the type of the object is determined by a property in the
object.
The property is a :class:`singer_sdk.typing.Constant` called the discriminator
property.
"""

def __init__(self, key: str, **options: ObjectType) -> None:
"""Initialize Discriminator.
Args:
key: Name of the discriminator property.
options: Mapping of discriminator values to object types.
Examples:
>>> t = Discriminator("species", cat=ObjectType(), dog=ObjectType())
>>> print(t.to_json(indent=2))
{
"oneOf": [
{
"type": "object",
"properties": {
"species": {
"const": "cat",
"description": "Discriminator for object of type 'cat'."
}
},
"required": [
"species"
]
},
{
"type": "object",
"properties": {
"species": {
"const": "dog",
"description": "Discriminator for object of type 'dog'."
}
},
"required": [
"species"
]
}
]
}
"""
self.key = key
self.options = options

super().__init__(
*(
ObjectType(
Property(
key,
Constant(k),
required=True,
description=f"Discriminator for object of type '{k}'.",
),
*v.wrapped.values(),
additional_properties=v.additional_properties,
pattern_properties=v.pattern_properties,
)
for k, v in options.items()
),
)


class CustomType(JSONTypeHelper):
"""Accepts an arbitrary JSON Schema dictionary."""

Expand Down
47 changes: 47 additions & 0 deletions tests/core/test_jsonschema_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from typing import TYPE_CHECKING, Callable

import pytest
from jsonschema import Draft6Validator

from singer_sdk.helpers._typing import (
JSONSCHEMA_ANNOTATION_SECRET,
Expand All @@ -30,6 +31,7 @@
CustomType,
DateTimeType,
DateType,
Discriminator,
DurationType,
EmailType,
HostnameType,
Expand Down Expand Up @@ -757,3 +759,48 @@ def test_type_check_variations(property_schemas, type_check_functions, results):
for property_schema in property_schemas:
for type_check_function, result in zip(type_check_functions, results):
assert type_check_function(property_schema) == result


def test_one_of_discrimination():
th = Discriminator(
"flow",
oauth=ObjectType(
Property("client_id", StringType, required=True, secret=True),
Property("client_secret", StringType, required=True, secret=True),
additional_properties=False,
),
password=ObjectType(
Property("username", StringType, required=True),
Property("password", StringType, required=True, secret=True),
additional_properties=False,
),
)

validator = Draft6Validator(th.to_dict())

assert validator.is_valid(
{
"flow": "oauth",
"client_id": "123",
"client_secret": "456",
},
)
assert validator.is_valid(
{
"flow": "password",
"password": "123",
"username": "456",
},
)
assert not validator.is_valid(
{
"flow": "oauth",
"client_id": "123",
},
)
assert not validator.is_valid(
{
"flow": "password",
"client_id": "123",
},
)

0 comments on commit 2ebaa94

Please sign in to comment.