Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Support union schemas #1525

Merged
merged 27 commits into from
Jun 27, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
842e9f3
feat: Support union schemas
edgarrmondragon Mar 24, 2023
c8b04ad
Merge branch 'main' into feat/one-of-discriminator
edgarrmondragon Mar 24, 2023
4b90dee
Rename `Discriminator` to `DiscriminatedUnion`
edgarrmondragon Mar 24, 2023
55b99e7
Update docs
edgarrmondragon Mar 24, 2023
bb2f1c6
Merge branch 'main' into feat/one-of-discriminator
edgarrmondragon Mar 24, 2023
90b42ad
Merge branch 'main' into feat/one-of-discriminator
edgarrmondragon Mar 24, 2023
dc61977
Merge branch 'main' into feat/one-of-discriminator
edgarrmondragon Mar 25, 2023
d60d737
Merge branch 'main' into feat/one-of-discriminator
edgarrmondragon Mar 30, 2023
7a9b775
Merge branch 'main' into feat/one-of-discriminator
edgarrmondragon Mar 30, 2023
1a698dd
Merge branch 'main' into feat/one-of-discriminator
edgarrmondragon Mar 30, 2023
76c581e
Merge branch 'main' into feat/one-of-discriminator
edgarrmondragon Apr 13, 2023
7646366
Merge branch 'main' into feat/one-of-discriminator
edgarrmondragon Apr 13, 2023
11ac79d
Merge branch 'main' into feat/one-of-discriminator
edgarrmondragon Apr 14, 2023
03658f5
Add more docstrings
edgarrmondragon Apr 14, 2023
54c1c0d
Merge branch 'main' into feat/one-of-discriminator
edgarrmondragon Apr 18, 2023
832cf11
Merge branch 'main' into feat/one-of-discriminator
edgarrmondragon Apr 18, 2023
6f79caa
Merge branch 'main' into feat/one-of-discriminator
edgarrmondragon Apr 18, 2023
a0b6ffb
Merge branch 'main' into feat/one-of-discriminator
edgarrmondragon May 2, 2023
d795df2
Merge branch 'main' into feat/one-of-discriminator
edgarrmondragon May 3, 2023
e0746d4
Merge branch 'main' into feat/one-of-discriminator
edgarrmondragon May 19, 2023
6d9b84e
Merge branch 'main' into feat/one-of-discriminator
edgarrmondragon May 24, 2023
4d484e5
Merge branch 'main' into feat/one-of-discriminator
edgarrmondragon May 24, 2023
4567341
Merge branch 'main' into feat/one-of-discriminator
edgarrmondragon May 30, 2023
b804dc3
Merge branch 'main' into feat/one-of-discriminator
edgarrmondragon May 31, 2023
80aecf6
Merge branch 'main' into feat/one-of-discriminator
edgarrmondragon Jun 5, 2023
650c18c
Merge branch 'main' into feat/one-of-discriminator
edgarrmondragon Jun 20, 2023
7384302
Merge branch 'main' into feat/one-of-discriminator
Jun 27, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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__
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
singer_sdk.typing.DiscriminatedUnion
====================================

.. currentmodule:: singer_sdk.typing

.. autoclass:: DiscriminatedUnion
: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.DiscriminatedUnion
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
144 changes: 144 additions & 0 deletions singer_sdk/typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -711,6 +711,150 @@ def type_dict(self) -> dict: # type: ignore[override]
return result


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

This type allows for a value to be one of a set of types.

Examples:
>>> t = OneOf(StringType, IntegerType)
>>> print(t.to_json(indent=2))
{
"oneOf": [
{
"type": [
"string"
]
},
{
"type": [
"integer"
]
}
]
}
"""

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: # type: ignore[override]
"""Get type dictionary.

Returns:
A dictionary describing the type.
"""
return {"oneOf": [t.type_dict for t in self.wrapped]}


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

A property that is always the same value.

Examples:
>>> t = Constant("foo")
>>> print(t.to_json(indent=2))
{
"const": "foo"
}
"""

def __init__(self, value: _JsonValue) -> None:
"""Initialize Constant.

Args:
value: Value of the constant.
"""
self.value = value

@property
def type_dict(self) -> dict: # type: ignore[override]
"""Get type dictionary.

Returns:
A dictionary describing the type.
"""
return {"const": self.value}


class DiscriminatedUnion(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 a discriminated union type.

Args:
key: Name of the discriminator property.
options: Mapping of discriminator values to object types.

Examples:
>>> t = DiscriminatedUnion("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 textwrap import dedent

import pytest
from jsonschema import Draft6Validator

from singer_sdk.helpers._typing import (
JSONSCHEMA_ANNOTATION_SECRET,
Expand All @@ -30,6 +31,7 @@
CustomType,
DateTimeType,
DateType,
DiscriminatedUnion,
DurationType,
EmailType,
HostnameType,
Expand Down Expand Up @@ -813,3 +815,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_discriminated_union():
th = DiscriminatedUnion(
"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",
},
)