diff --git a/CHANGELOG.md b/CHANGELOG.md index a15d7e8b..62a1a7a9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,42 +9,43 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- Add check enforcing unique `x-tablename` values. -- Add check enforcing unique `x-secondary` values. -- Add custom association schemas validation -- Add support for custom association tables -- Add `openalchemy` CLI with a first subcommand to build a Python package from a - specification file. [#201] -- Add a CLI subcommand to regenerate models. [#202] +- Add check enforcing unique `x-tablename` values. [#189] +- Add check enforcing unique `x-secondary` values. [#189] +- Add custom association schemas validation [#189] +- Add support for custom association tables [#189] +- Add `openalchemy` CLI with a first sub command to build a Python package + from a specification file. [#201] +- Add a CLI sub command to regenerate models. [#202] +- Add support for database default values using `x-server-default`. [#196] ### Changed - Change the association table to no longer be noted on the models based on the `x-secondary` value and instead be noted based on converting the `x-secondary` value from snake_case to PascalCase. Name clashes are avoided - by pre-pending `Autogen` as many times as required. + by pre-pending `Autogen` as many times as required. [#189] - Change the association table to no longer be constructed as a table and - instead to be constructed as another model. -- Refactor column factory to use the schemas artifacts -- Refactor model factory to use the schemas artifacts + instead to be constructed as another model. [#189] +- Refactor column factory to use the schemas artifacts [#196] +- Refactor model factory to use the schemas artifacts [#196] ### Fixed - Fix bug where the association table defined for `many-to-many` relationships did not make the foreign key columns referencing the two sides of the relationship primary keys. _This may require a database migration if alembic - was used to generate the database schema._ + was used to generate the database schema._ [#189] - Fix bug where some properties were incorrectly picked from a reference even though they existed locally (only impacts relationship properties where, for example, `x-secondary` was defined both on the relationship property in - `allOf` and on the referenced model). + `allOf` and on the referenced model). [#189] ### Removed - Remove `define_all` parameter for `init_model_factory`, `init_json` and `init_yaml`. OpenAlchemy now behaves as though `define_all` is set to `True`. _This means that a pure model reference (a schema with only the - `$ref` key) can no longer be used to change the name of a model._ + `$ref` key) can no longer be used to change the name of a model._ [#189] ## [1.6.0] - 2020-10-10 diff --git a/README.md b/README.md index f721c0c3..385f4f46 100644 --- a/README.md +++ b/README.md @@ -147,7 +147,7 @@ An example API has been defined using connexion and Flask here: - composite unique constraints, - column nullability, - foreign keys, -- default values for columns, +- default values for columns (both application and database side), - many to one relationships, - one to one relationships, - one to many relationships, diff --git a/docs/source/examples/index.rst b/docs/source/examples/index.rst index 3b636749..53726ac9 100644 --- a/docs/source/examples/index.rst +++ b/docs/source/examples/index.rst @@ -9,6 +9,7 @@ Examples alembic simple default + server_default read_only write_only relationship/index diff --git a/docs/source/examples/server_default.rst b/docs/source/examples/server_default.rst new file mode 100644 index 00000000..85373bc3 --- /dev/null +++ b/docs/source/examples/server_default.rst @@ -0,0 +1,44 @@ +Server Default +============== + +OpenAlchemy supports defining a default value generated by the database +through the :samp:`x-server-default` extension property. It is similar to the +OpenAPI :samp:`default` property except that the default value is calculated +by the database rather than the application. + +.. seealso:: + + :ref:`server-default` + OpenAlchemy documentation for the server default value. + + :ref:`default` + OpenAlchemy documentation for the default value. + + `SQLAlchemy Server Default `_ + Documentation for the SQLAlchemy server default. + +The following example defines a default value for the :samp:`name` property of +:samp:`Employee`: + +.. literalinclude:: ../../../examples/server_default/example-spec.yml + :language: yaml + :linenos: + +The following file uses OpenAlchemy to generate the SQLAlchemy models: + +.. literalinclude:: ../../../examples/server_default/models.py + :language: python + :linenos: + +The SQLAlchemy models generated by OpenAlchemy are equivalent to the following +traditional models file: + +.. literalinclude:: ../../../examples/server_default/models_traditional.py + :language: python + :linenos: + +OpenAlchemy will generate the following typed models: + +.. literalinclude:: ../../../examples/server_default/models_auto.py + :language: python + :linenos: diff --git a/docs/source/technical_details/column_modifiers.rst b/docs/source/technical_details/column_modifiers.rst index 4f624434..747adc86 100644 --- a/docs/source/technical_details/column_modifiers.rst +++ b/docs/source/technical_details/column_modifiers.rst @@ -457,6 +457,45 @@ OpenAlchemy. `SQLAlchemy "Scalar Default" `_ Documentation for the scalar default value in SQLAlchemy. +.. _server-default: + +Server Default +-------------- + +To add a default value for a column to be generated by the database, use the +:samp:`x-server-default` extension property: + +.. code-block:: yaml + :linenos: + + Employee: + type: object + x-tablename: employee + properties: + id: + type: integer + name: + type: string + x-server-default: Unknown + +The default value is added to the column constructor using the "Server +Default" in SQLAlchemy. The following property types support a server default +value (including all their formats supported by OpenAlchemy): + +* :samp:`integer`, +* :samp:`number`, +* :samp:`string` and +* :samp:`boolean`. + +Adding a server default to a :samp:`object` or :samp:`array` type is not valid +in OpenAlchemy. Server default is also not supported by any property that sets +:samp:`x-json` to :samp:`true`. + +.. seealso:: + + `SQLAlchemy Server Default `_ + Documentation for the SQLAlchemy server default. + .. _read-only: readOnly diff --git a/examples/server_default/example-spec.yml b/examples/server_default/example-spec.yml new file mode 100644 index 00000000..83ca42a9 --- /dev/null +++ b/examples/server_default/example-spec.yml @@ -0,0 +1,38 @@ +openapi: "3.0.0" + +info: + title: Test Schema + description: API to illustrate the OpenAlchemy default feature. + version: "0.1" + +paths: + /employee: + get: + summary: Used to retrieve all employees. + responses: + 200: + description: Return all employees from the database. + content: + application/json: + schema: + type: array + items: + "$ref": "#/components/schemas/Employee" + +components: + schemas: + Employee: + description: Person that works for a company. + type: object + x-tablename: employee + properties: + id: + type: integer + description: Unique identifier for the employee. + example: 0 + x-primary-key: true + name: + type: string + description: The name of the employee. + example: David Andersson + x-server-default: Unknown diff --git a/examples/server_default/models.py b/examples/server_default/models.py new file mode 100644 index 00000000..e4dd867e --- /dev/null +++ b/examples/server_default/models.py @@ -0,0 +1,3 @@ +from open_alchemy import init_yaml + +init_yaml("example-spec.yml", models_filename="models_auto.py") diff --git a/examples/server_default/models_auto.py b/examples/server_default/models_auto.py new file mode 100644 index 00000000..b8d495ee --- /dev/null +++ b/examples/server_default/models_auto.py @@ -0,0 +1,104 @@ +"""Autogenerated SQLAlchemy models based on OpenAlchemy models.""" +# pylint: disable=no-member,super-init-not-called,unused-argument + +import typing + +import sqlalchemy +from sqlalchemy import orm + +from open_alchemy import models + +Base = models.Base # type: ignore + + +class EmployeeDict(typing.TypedDict, total=False): + """TypedDict for properties that are not required.""" + + id: typing.Optional[int] + name: str + + +class TEmployee(typing.Protocol): + """ + SQLAlchemy model protocol. + + Person that works for a company. + + Attrs: + id: Unique identifier for the employee. + name: The name of the employee. + + """ + + # SQLAlchemy properties + __table__: sqlalchemy.Table + __tablename__: str + query: orm.Query + + # Model properties + id: "sqlalchemy.Column[typing.Optional[int]]" + name: "sqlalchemy.Column[str]" + + def __init__( + self, id: typing.Optional[int] = None, name: typing.Optional[str] = None + ) -> None: + """ + Construct. + + Args: + id: Unique identifier for the employee. + name: The name of the employee. + + """ + ... + + @classmethod + def from_dict( + cls, id: typing.Optional[int] = None, name: typing.Optional[str] = None + ) -> "TEmployee": + """ + Construct from a dictionary (eg. a POST payload). + + Args: + id: Unique identifier for the employee. + name: The name of the employee. + + Returns: + Model instance based on the dictionary. + + """ + ... + + @classmethod + def from_str(cls, value: str) -> "TEmployee": + """ + Construct from a JSON string (eg. a POST payload). + + Returns: + Model instance based on the JSON string. + + """ + ... + + def to_dict(self) -> EmployeeDict: + """ + Convert to a dictionary (eg. to send back for a GET request). + + Returns: + Dictionary based on the model instance. + + """ + ... + + def to_str(self) -> str: + """ + Convert to a JSON string (eg. to send back for a GET request). + + Returns: + JSON string based on the model instance. + + """ + ... + + +Employee: typing.Type[TEmployee] = models.Employee # type: ignore diff --git a/examples/server_default/models_traditional.py b/examples/server_default/models_traditional.py new file mode 100644 index 00000000..35ca1c7f --- /dev/null +++ b/examples/server_default/models_traditional.py @@ -0,0 +1,12 @@ +import sqlalchemy as sa +from sqlalchemy.ext.declarative import declarative_base + +Base = declarative_base() + + +class Employee(Base): + """Person that works for a company.""" + + __tablename__ = "employee" + id = sa.Column(sa.Integer, primary_key=True) + name = sa.Column(sa.String, server_default=sa.text("Unknown")) diff --git a/open_alchemy/facades/sqlalchemy/simple.py b/open_alchemy/facades/sqlalchemy/simple.py index 1c74872b..5a84c6d8 100644 --- a/open_alchemy/facades/sqlalchemy/simple.py +++ b/open_alchemy/facades/sqlalchemy/simple.py @@ -2,6 +2,8 @@ import typing +import sqlalchemy + from ... import exceptions from ... import helpers from ... import types as oa_types @@ -37,11 +39,16 @@ def construct(*, artifacts: oa_types.SimplePropertyArtifacts) -> types.Column: format_=artifacts.open_api.format, ) + # Calculate server default + server_default = None + if artifacts.extension.server_default is not None: + server_default = sqlalchemy.text(artifacts.extension.server_default) + # Calculate nullable nullable = helpers.calculate_nullable( nullable=artifacts.open_api.nullable, generated=artifacts.extension.autoincrement is True, - defaulted=default is not None, + defaulted=default is not None or artifacts.extension.server_default is not None, required=artifacts.required, ) @@ -64,6 +71,7 @@ def construct(*, artifacts: oa_types.SimplePropertyArtifacts) -> types.Column: foreign_key, nullable=nullable, default=default, + server_default=server_default, **opt_kwargs, **kwargs, ) diff --git a/open_alchemy/helpers/ext_prop/extension-schemas.json b/open_alchemy/helpers/ext_prop/extension-schemas.json index c2b34e15..1cfe8c47 100644 --- a/open_alchemy/helpers/ext_prop/extension-schemas.json +++ b/open_alchemy/helpers/ext_prop/extension-schemas.json @@ -53,6 +53,10 @@ "type": "object", "additionalProperties": true }, + "x-server-default": { + "description": "Get the server to calculate a default value.", + "type": "string" + }, "x-tablename": { "description": "Define the name of a table.", "type": "string" diff --git a/open_alchemy/helpers/peek.py b/open_alchemy/helpers/peek.py index c1dbe7bf..e6edf0c1 100644 --- a/open_alchemy/helpers/peek.py +++ b/open_alchemy/helpers/peek.py @@ -678,6 +678,32 @@ def default(*, schema: types.Schema, schemas: types.Schemas) -> types.TColumnDef return value +def server_default( + *, schema: types.Schema, schemas: types.Schemas +) -> typing.Optional[str]: + """ + Retrieve the x-server-default property from a property schema. + + Raises MalformedSchemaError if the x-server-default value is not a string. + + Args: + schema: The schema to get the x-server-default from. + schemas: The schemas for $ref lookup. + + Returns: + The x-server-default value. + + """ + value = peek_key(schema=schema, schemas=schemas, key="x-server-default") + if value is None: + return None + if not isinstance(value, str): + raise exceptions.MalformedSchemaError( + "A x-server-default value must be of type string." + ) + return value + + def mixins( *, schema: types.Schema, schemas: types.Schemas ) -> typing.Optional[typing.List[str]]: diff --git a/open_alchemy/models_file/artifacts/type_.py b/open_alchemy/models_file/artifacts/type_.py index 287adbb5..78a93ad0 100644 --- a/open_alchemy/models_file/artifacts/type_.py +++ b/open_alchemy/models_file/artifacts/type_.py @@ -31,7 +31,8 @@ def _model_simple_property( nullable=artifacts.open_api.nullable, generated=artifacts.extension.autoincrement is True, required=artifacts.required, - defaulted=artifacts.open_api.default is not None, + defaulted=artifacts.open_api.default is not None + or artifacts.extension.server_default is not None, ) if optional: diff --git a/open_alchemy/schemas/artifacts/property_/simple.py b/open_alchemy/schemas/artifacts/property_/simple.py index 60a3a911..6f4d2cbf 100644 --- a/open_alchemy/schemas/artifacts/property_/simple.py +++ b/open_alchemy/schemas/artifacts/property_/simple.py @@ -38,6 +38,9 @@ def get( default = oa_helpers.peek.prefer_local( get_value=oa_helpers.peek.default, schema=schema, schemas=schemas ) + server_default = oa_helpers.peek.prefer_local( + get_value=oa_helpers.peek.server_default, schema=schema, schemas=schemas + ) read_only = oa_helpers.peek.prefer_local( get_value=oa_helpers.peek.read_only, schema=schema, schemas=schemas @@ -112,6 +115,7 @@ def get( autoincrement=autoincrement, index=index, unique=unique, + server_default=server_default, foreign_key=foreign_key, kwargs=kwargs, foreign_key_kwargs=foreign_key_kwargs, diff --git a/open_alchemy/schemas/foreign_key.py b/open_alchemy/schemas/foreign_key.py index ae2257e5..6c71f27d 100644 --- a/open_alchemy/schemas/foreign_key.py +++ b/open_alchemy/schemas/foreign_key.py @@ -174,10 +174,13 @@ def _calculate_foreign_key_property_artifacts( if relationship_type != types.RelationshipType.ONE_TO_MANY: nullable = oa_helpers.peek.nullable(schema=property_schema, schemas=schemas) default = oa_helpers.peek.default(schema=foreign_key_target_schema, schemas=schemas) + server_default = oa_helpers.peek.server_default( + schema=foreign_key_target_schema, schemas=schemas + ) nullable = oa_helpers.calculate_nullable( nullable=nullable, generated=False, - defaulted=default is not None, + defaulted=default is not None or server_default is not None, required=required, ) @@ -208,6 +211,8 @@ def _calculate_foreign_key_property_artifacts( foreign_key_property_schema["maxLength"] = max_length if default is not None: foreign_key_property_schema["default"] = default + if server_default is not None: + foreign_key_property_schema["x-server-default"] = server_default # Calculate other artifacts modify_name = oa_helpers.foreign_key.get_modify_name( diff --git a/open_alchemy/schemas/validation/property_/json.py b/open_alchemy/schemas/validation/property_/json.py index ed3a8f0d..7c85cf33 100644 --- a/open_alchemy/schemas/validation/property_/json.py +++ b/open_alchemy/schemas/validation/property_/json.py @@ -20,19 +20,40 @@ def check(schemas: oa_types.Schemas, schema: oa_types.Schema) -> types.Result: """ try: - helpers.peek.nullable(schema=schema, schemas=schemas) - helpers.peek.description(schema=schema, schemas=schemas) - helpers.peek.read_only(schema=schema, schemas=schemas) - helpers.peek.write_only(schema=schema, schemas=schemas) - helpers.peek.index(schema=schema, schemas=schemas) - helpers.peek.unique(schema=schema, schemas=schemas) - helpers.peek.primary_key(schema=schema, schemas=schemas) + helpers.peek.prefer_local( + get_value=helpers.peek.nullable, schema=schema, schemas=schemas + ) + helpers.peek.prefer_local( + get_value=helpers.peek.description, schema=schema, schemas=schemas + ) + helpers.peek.prefer_local( + get_value=helpers.peek.read_only, schema=schema, schemas=schemas + ) + helpers.peek.prefer_local( + get_value=helpers.peek.write_only, schema=schema, schemas=schemas + ) + helpers.peek.prefer_local( + get_value=helpers.peek.index, schema=schema, schemas=schemas + ) + helpers.peek.prefer_local( + get_value=helpers.peek.unique, schema=schema, schemas=schemas + ) + helpers.peek.prefer_local( + get_value=helpers.peek.primary_key, schema=schema, schemas=schemas + ) autoincrement = helpers.peek.peek_key( schema=schema, schemas=schemas, key="x-autoincrement" ) if autoincrement is not None: return types.Result(False, "json properties do not support x-autoincrement") - helpers.peek.foreign_key(schema=schema, schemas=schemas) + server_default = helpers.peek.peek_key( + schema=schema, schemas=schemas, key="x-server-default" + ) + if server_default is not None: + return types.Result( + False, "json properties do not support x-server-default" + ) + # Checks kwargs, foreign key and foreign key kwargs kwargs_result = simple.check_kwargs(schema=schema, schemas=schemas) if kwargs_result is not None: return kwargs_result diff --git a/open_alchemy/schemas/validation/property_/relationship/property_.py b/open_alchemy/schemas/validation/property_/relationship/property_.py index bb53fa60..ecb85de1 100644 --- a/open_alchemy/schemas/validation/property_/relationship/property_.py +++ b/open_alchemy/schemas/validation/property_/relationship/property_.py @@ -19,7 +19,12 @@ def _check_type( if type_ not in {"object", "array"}: return types.Result(False, "type not an object nor array") # Check for JSON - if helpers.peek.json(schema=schema, schemas=schemas) is True: + if ( + helpers.peek.prefer_local( + get_value=helpers.peek.json, schema=schema, schemas=schemas + ) + is True + ): return types.Result(False, "property is JSON") except exceptions.SchemaNotFoundError as exc: return types.Result(False, f"reference :: {exc}") @@ -56,9 +61,13 @@ def _check_object_backref_uselist( ) -> types.OptResult: """Check backref and uselist for an object.""" # Check backref - backref = helpers.peek.backref(schema=schema, schemas=schemas) + backref = helpers.peek.prefer_local( + get_value=helpers.peek.backref, schema=schema, schemas=schemas + ) # Check uselist - uselist = helpers.peek.uselist(schema=schema, schemas=schemas) + uselist = helpers.peek.prefer_local( + get_value=helpers.peek.uselist, schema=schema, schemas=schemas + ) if uselist is False and backref is None: return types.Result( False, "a one-to-one relationship must define a back reference" @@ -71,7 +80,9 @@ def _check_kwargs( *, schema: oa_types.Schema, schemas: oa_types.Schemas ) -> types.OptResult: """Check the value of x-kwargs.""" - kwargs = helpers.peek.kwargs(schema=schema, schemas=schemas) + kwargs = helpers.peek.prefer_local( + get_value=helpers.peek.kwargs, schema=schema, schemas=schemas + ) # Check for unexpected keys if kwargs is not None: unexpected_keys = {"backref", "secondary"} @@ -89,11 +100,17 @@ def _check_object_values( ) -> types.OptResult: """Check the values of the relationship.""" # Check nullable - helpers.peek.nullable(schema=schema, schemas=schemas) + helpers.peek.prefer_local( + get_value=helpers.peek.nullable, schema=schema, schemas=schemas + ) # Check description - helpers.peek.description(schema=schema, schemas=schemas) + helpers.peek.prefer_local( + get_value=helpers.peek.description, schema=schema, schemas=schemas + ) # Check writeOnly - helpers.peek.write_only(schema=schema, schemas=schemas) + helpers.peek.prefer_local( + get_value=helpers.peek.write_only, schema=schema, schemas=schemas + ) # Check backref and uselist backref_uselist_result = _check_object_backref_uselist( schema=schema, schemas=schemas @@ -101,7 +118,9 @@ def _check_object_values( if backref_uselist_result is not None: return backref_uselist_result # Check foreign-key-column - helpers.peek.foreign_key_column(schema=schema, schemas=schemas) + helpers.peek.prefer_local( + get_value=helpers.peek.foreign_key_column, schema=schema, schemas=schemas + ) # Check kwargs kwargs_result = _check_kwargs(schema=schema, schemas=schemas) if kwargs_result is not None: diff --git a/open_alchemy/schemas/validation/property_/simple.py b/open_alchemy/schemas/validation/property_/simple.py index efe23242..52e077c5 100644 --- a/open_alchemy/schemas/validation/property_/simple.py +++ b/open_alchemy/schemas/validation/property_/simple.py @@ -28,7 +28,9 @@ def _check_modifiers( return types.Result(False, f"{type_} type is not supported") # check format - format_ = oa_helpers.peek.format_(schema=schema, schemas=schemas) + format_ = oa_helpers.peek.prefer_local( + get_value=oa_helpers.peek.format_, schema=schema, schemas=schemas + ) type_format = (type_, format_) # Check type and format combination if type_ != "string" and type_format not in _VALID_TYPE_FORMAT: @@ -40,7 +42,9 @@ def _check_modifiers( format_str = f" {format_} format" # Check maxLength - max_length = oa_helpers.peek.max_length(schema=schema, schemas=schemas) + max_length = oa_helpers.peek.prefer_local( + get_value=oa_helpers.peek.max_length, schema=schema, schemas=schemas + ) if max_length is not None: if type_ != "string" or format_ in {"date", "date-time"}: return types.Result( @@ -48,7 +52,9 @@ def _check_modifiers( ) # Check autoincrement - autoincrement = oa_helpers.peek.autoincrement(schema=schema, schemas=schemas) + autoincrement = oa_helpers.peek.prefer_local( + get_value=oa_helpers.peek.autoincrement, schema=schema, schemas=schemas + ) if autoincrement is not None and type_ != "integer": return types.Result( False, f"{type_}{format_str} does not support x-autoincrement" @@ -62,12 +68,15 @@ def check_kwargs( ) -> types.OptResult: """Check the value of kwargs.""" # Check kwargs - kwargs = oa_helpers.peek.kwargs(schema=schema, schemas=schemas) + kwargs = oa_helpers.peek.prefer_local( + get_value=oa_helpers.peek.kwargs, schema=schema, schemas=schemas + ) # Check for unexpected keys if kwargs is not None: unexpected_keys = { "nullable", "default", + "server_default", "primary_key", "autoincrement", "index", @@ -80,11 +89,13 @@ def check_kwargs( ) # Check foreign_key - foreign_key = oa_helpers.peek.foreign_key(schema=schema, schemas=schemas) + foreign_key = oa_helpers.peek.prefer_local( + get_value=oa_helpers.peek.foreign_key, schema=schema, schemas=schemas + ) # Check foreign key kwargs - foreign_key_kwargs = oa_helpers.peek.foreign_key_kwargs( - schema=schema, schemas=schemas + foreign_key_kwargs = oa_helpers.peek.prefer_local( + get_value=oa_helpers.peek.foreign_key_kwargs, schema=schema, schemas=schemas ) if foreign_key_kwargs is not None and foreign_key is None: return types.Result( @@ -118,19 +129,41 @@ def check(schemas: oa_types.Schemas, schema: oa_types.Schema) -> types.Result: return kwargs_result # Check nullable - oa_helpers.peek.nullable(schema=schema, schemas=schemas) + oa_helpers.peek.prefer_local( + get_value=oa_helpers.peek.nullable, schema=schema, schemas=schemas + ) # Check primary_key - oa_helpers.peek.primary_key(schema=schema, schemas=schemas) + oa_helpers.peek.prefer_local( + get_value=oa_helpers.peek.primary_key, schema=schema, schemas=schemas + ) # Check index - oa_helpers.peek.index(schema=schema, schemas=schemas) + oa_helpers.peek.prefer_local( + get_value=oa_helpers.peek.index, schema=schema, schemas=schemas + ) # Check unique - oa_helpers.peek.unique(schema=schema, schemas=schemas) + oa_helpers.peek.prefer_local( + get_value=oa_helpers.peek.unique, schema=schema, schemas=schemas + ) # Check description - oa_helpers.peek.description(schema=schema, schemas=schemas) + oa_helpers.peek.prefer_local( + get_value=oa_helpers.peek.description, schema=schema, schemas=schemas + ) # Check default - oa_helpers.peek.default(schema=schema, schemas=schemas) + oa_helpers.peek.prefer_local( + get_value=oa_helpers.peek.default, schema=schema, schemas=schemas + ) + # Check server default + oa_helpers.peek.prefer_local( + get_value=oa_helpers.peek.server_default, schema=schema, schemas=schemas + ) + # Check readOnly + oa_helpers.peek.prefer_local( + get_value=oa_helpers.peek.read_only, schema=schema, schemas=schemas + ) # Check writeOnly - oa_helpers.peek.write_only(schema=schema, schemas=schemas) + oa_helpers.peek.prefer_local( + get_value=oa_helpers.peek.write_only, schema=schema, schemas=schemas + ) except exceptions.SchemaNotFoundError as exc: return types.Result(False, f"reference :: {exc}") diff --git a/open_alchemy/types.py b/open_alchemy/types.py index 0b523084..07f0e25a 100644 --- a/open_alchemy/types.py +++ b/open_alchemy/types.py @@ -93,6 +93,7 @@ class Index(_IndexBase, total=False): "description": str, "x-json": bool, "default": TColumnDefault, + "x-server-default": str, "x-generated": bool, "readOnly": bool, "writeOnly": bool, @@ -256,6 +257,7 @@ class _ExtensionSimplePropertyTypedDictBase(TypedDict, total=False): autoincrement: bool index: bool unique: bool + server_default: str foreign_key: str @@ -279,6 +281,7 @@ class ExtensionSimplePropertyArtifacts: autoincrement: typing.Optional[bool] index: typing.Optional[bool] unique: typing.Optional[bool] + server_default: typing.Optional[str] foreign_key: typing.Optional[str] @@ -298,6 +301,7 @@ def to_dict(self) -> ExtensionSimplePropertyTypedDict: "autoincrement", "index", "unique", + "server_default", "foreign_key", "kwargs", "foreign_key_kwargs", @@ -306,6 +310,7 @@ def to_dict(self) -> ExtensionSimplePropertyTypedDict: "autoincrement", "index", "unique", + "server_default", "foreign_key", "kwargs", "foreign_key_kwargs", diff --git a/tests/examples/test_example_specs.py b/tests/examples/test_example_specs.py index 360e6c2d..65b73942 100644 --- a/tests/examples/test_example_specs.py +++ b/tests/examples/test_example_specs.py @@ -107,6 +107,13 @@ def cleanup_models(): {"name": "Unknown"}, id="default Employee", ), + pytest.param( + "server_default/example-spec.yml", + "Employee", + {"id": 1}, + {"name": "Unknown"}, + id="server_default Employee", + ), pytest.param( "read_only/example-spec.yml", "Employee", diff --git a/tests/open_alchemy/column_factory/test_integration.py b/tests/open_alchemy/column_factory/test_integration.py index d67b4e70..3b5cdf08 100644 --- a/tests/open_alchemy/column_factory/test_integration.py +++ b/tests/open_alchemy/column_factory/test_integration.py @@ -30,6 +30,7 @@ def test_integration_simple(): autoincrement=None, index=None, unique=None, + server_default=None, foreign_key=None, kwargs=None, foreign_key_kwargs=None, diff --git a/tests/open_alchemy/column_factory/test_simple.py b/tests/open_alchemy/column_factory/test_simple.py index 5defe698..b102df6a 100644 --- a/tests/open_alchemy/column_factory/test_simple.py +++ b/tests/open_alchemy/column_factory/test_simple.py @@ -31,6 +31,7 @@ def test_integration(): autoincrement=None, index=None, unique=None, + server_default=None, foreign_key=None, kwargs=None, foreign_key_kwargs=None, diff --git a/tests/open_alchemy/facades/sqlalchemy/test_simple.py b/tests/open_alchemy/facades/sqlalchemy/test_simple.py index 1374965f..66842555 100644 --- a/tests/open_alchemy/facades/sqlalchemy/test_simple.py +++ b/tests/open_alchemy/facades/sqlalchemy/test_simple.py @@ -29,6 +29,7 @@ def _create_artifacts(): autoincrement=None, index=None, unique=None, + server_default=None, foreign_key=None, kwargs=None, foreign_key_kwargs=None, @@ -109,10 +110,74 @@ def test_construct_foreign_key_kwargs(): CONSTRUCT_ARGS_TESTS = [ pytest.param("open_api", "nullable", True, "nullable", True, id="nullable true"), pytest.param("open_api", "nullable", False, "nullable", False, id="nullable false"), + pytest.param( + "open_api", + "default", + None, + "nullable", + True, + id="default None expect nullable true", + ), + pytest.param( + "open_api", + "default", + 1, + "nullable", + False, + id="default set expect nullable false", + ), + pytest.param( + "extension", + "server_default", + None, + "nullable", + True, + id="server_default None expect nullable true", + ), + pytest.param( + "extension", + "server_default", + "value 1", + "nullable", + False, + id="server_default set expect nullable false", + ), + pytest.param( + "extension", + "server_default", + None, + "nullable", + True, + id="autoincrement None expect nullable true", + ), + pytest.param( + "extension", + "autoincrement", + True, + "nullable", + False, + id="autoincrement set expect nullable false", + ), pytest.param("open_api", "default", None, "default", None, id="default None"), pytest.param( "open_api", "default", "value 1", "default.arg", "value 1", id="default set" ), + pytest.param( + "extension", + "server_default", + None, + "server_default", + None, + id="server_default None", + ), + pytest.param( + "extension", + "server_default", + "value 1", + "server_default.arg.text", + "value 1", + id="server_default set", + ), pytest.param( "extension", "primary_key", None, "primary_key", False, id="primary key None" ), @@ -156,6 +221,29 @@ def test_construct_args( ) +@pytest.mark.parametrize( + "required, expected_nullable", + [ + pytest.param(False, True, id="required False"), + pytest.param(True, False, id="required True"), + ], +) +@pytest.mark.facade +@pytest.mark.sqlalchemy +def test_construct_args_required_nullable(required, expected_nullable): + """ + GIVEN artifacts with given required + WHEN construct is called with the artifacts + THEN a column with the expected nullable value is returned. + """ + artifacts = _create_artifacts() + artifacts.required = required + + returned_column = simple.construct(artifacts=artifacts) + + assert returned_column.nullable == expected_nullable + + @pytest.mark.facade @pytest.mark.sqlalchemy def test_construct_default_type_mapped(): diff --git a/tests/open_alchemy/helpers/test_get_ext_prop.py b/tests/open_alchemy/helpers/test_get_ext_prop.py index ed80ebe8..d3d421b3 100644 --- a/tests/open_alchemy/helpers/test_get_ext_prop.py +++ b/tests/open_alchemy/helpers/test_get_ext_prop.py @@ -46,6 +46,7 @@ def test_miss_default(): pytest.param("x-foreign-key", True, id="x-foreign-key invalid type"), pytest.param("x-foreign-key", "no column", id="x-foreign-key invalid format"), pytest.param("x-foreign-key-column", True, id="x-foreign-key-column"), + pytest.param("x-server-default", True, id="x-server-default"), pytest.param("x-tablename", True, id="x-tablename"), pytest.param("x-tablename", None, id="x-tablename None"), pytest.param("x-de-$ref", True, id="x-de-$ref"), @@ -120,6 +121,11 @@ def test_invalid(name, value): "column 1", id="x-foreign-key-column", ), + pytest.param( + "x-server-default", + "value 1", + id="x-server-default", + ), pytest.param( "x-tablename", "table 1", diff --git a/tests/open_alchemy/helpers/test_peek/test_peek_extension.py b/tests/open_alchemy/helpers/test_peek/test_peek_extension.py index e202ba07..f0f96496 100644 --- a/tests/open_alchemy/helpers/test_peek/test_peek_extension.py +++ b/tests/open_alchemy/helpers/test_peek/test_peek_extension.py @@ -529,6 +529,38 @@ def test_composite_unique(schema, expected_composite_unique): assert returned_composite_unique == expected_composite_unique +@pytest.mark.helper +def test_server_default_wrong_type(): + """ + GIVEN schema with server_default defined as a string + WHEN server_default is called with the schema + THEN MalformedSchemaError is raised. + """ + schema = {"x-server-default": True} + + with pytest.raises(exceptions.MalformedSchemaError): + helpers.peek.server_default(schema=schema, schemas={}) + + +@pytest.mark.parametrize( + "schema, expected_server_default", + [ + pytest.param({}, None, id="missing"), + pytest.param({"x-server-default": "value 1"}, "value 1", id="defined"), + ], +) +@pytest.mark.helper +def test_server_default(schema, expected_server_default): + """ + GIVEN schema and expected server_default + WHEN server_default is called with the schema + THEN the expected server_default is returned. + """ + returned_server_default = helpers.peek.server_default(schema=schema, schemas={}) + + assert returned_server_default == expected_server_default + + @pytest.mark.parametrize( "schema", [ diff --git a/tests/open_alchemy/integration/test_database/test_simple.py b/tests/open_alchemy/integration/test_database/test_simple.py index cabda6be..e6cc1f50 100644 --- a/tests/open_alchemy/integration/test_database/test_simple.py +++ b/tests/open_alchemy/integration/test_database/test_simple.py @@ -275,6 +275,54 @@ def test_types_default(engine, sessionmaker, type_, format_, default, expected_v assert queried_model.column == expected_value +@pytest.mark.integration +def test_types_server_default(engine, sessionmaker): + """ + GIVEN specification with a schema with a column that has a server default + WHEN schema is created and an instance is added to the session + THEN the instance with the default value is returned when the session is queried for + it. + """ + # Defining specification + column_schema = { + "type": "string", + "format": "date-time", + "x-server-default": "CURRENT_TIMESTAMP", + } + spec = { + "components": { + "schemas": { + "Table": { + "properties": { + "id": {"type": "integer", "x-primary-key": True}, + "column": column_schema, + }, + "x-tablename": "table", + "type": "object", + } + } + } + } + # Creating model factory + base = declarative.declarative_base() + model_factory = open_alchemy.init_model_factory(spec=spec, base=base) + model = model_factory(name="Table") + + # Creating models + base.metadata.create_all(engine) + # Creating model instance + model_instance = model(id=1) + session = sessionmaker() + session.add(model_instance) + session.flush() + + # Querying session + queried_model = session.query(model).first() + assert queried_model.column.timestamp() == pytest.approx( + datetime.datetime.utcnow().timestamp(), abs=10 + ) + + @pytest.mark.parametrize("index", ["x-primary-key", "x-index", "x-unique"]) @pytest.mark.integration def test_indexes(engine, index: str): diff --git a/tests/open_alchemy/models_file/artifacts/test_args.py b/tests/open_alchemy/models_file/artifacts/test_args.py index 6e9e68b1..1c50f9b8 100644 --- a/tests/open_alchemy/models_file/artifacts/test_args.py +++ b/tests/open_alchemy/models_file/artifacts/test_args.py @@ -45,6 +45,7 @@ def _construct_simple_property_artifacts( autoincrement=None, index=None, unique=None, + server_default=None, foreign_key=None, kwargs=None, foreign_key_kwargs=None, diff --git a/tests/open_alchemy/models_file/artifacts/test_artifacts.py b/tests/open_alchemy/models_file/artifacts/test_artifacts.py index e1173b54..dd1805e9 100644 --- a/tests/open_alchemy/models_file/artifacts/test_artifacts.py +++ b/tests/open_alchemy/models_file/artifacts/test_artifacts.py @@ -45,6 +45,7 @@ def _construct_simple_property_artifacts(required): autoincrement=None, index=None, unique=None, + server_default=None, foreign_key=None, kwargs=None, foreign_key_kwargs=None, diff --git a/tests/open_alchemy/models_file/artifacts/test_column.py b/tests/open_alchemy/models_file/artifacts/test_column.py index 0a7ba729..e4eff683 100644 --- a/tests/open_alchemy/models_file/artifacts/test_column.py +++ b/tests/open_alchemy/models_file/artifacts/test_column.py @@ -43,6 +43,7 @@ def _construct_simple_property_artifacts(dict_ignore, description): autoincrement=None, index=None, unique=None, + server_default=None, foreign_key=None, kwargs=None, foreign_key_kwargs=None, diff --git a/tests/open_alchemy/models_file/artifacts/test_type_.py b/tests/open_alchemy/models_file/artifacts/test_type_.py index 835bc988..3ffb9cfe 100644 --- a/tests/open_alchemy/models_file/artifacts/test_type_.py +++ b/tests/open_alchemy/models_file/artifacts/test_type_.py @@ -9,7 +9,14 @@ def _construct_simple_artifacts( - *, type_, format_=None, nullable=None, generated=None, default=None, required=False + *, + type_, + format_=None, + nullable=None, + generated=None, + default=None, + required=False, + server_default=None ): """Construct the artifacts for a simple property.""" return schemas_artifacts.types.SimplePropertyArtifacts( @@ -28,6 +35,7 @@ def _construct_simple_artifacts( autoincrement=generated, index=None, unique=None, + server_default=server_default, foreign_key=None, kwargs=None, foreign_key_kwargs=None, @@ -232,6 +240,7 @@ def _construct_backref_property_artifacts(sub_type): required=False, generated=None, default=None, + server_default=None, ), "typing.Optional[int]", id="simple nullable and required None", @@ -243,6 +252,7 @@ def _construct_backref_property_artifacts(sub_type): required=True, generated=None, default=None, + server_default=None, ), "int", id="simple nullable None required True", @@ -254,6 +264,7 @@ def _construct_backref_property_artifacts(sub_type): required=False, generated=True, default=None, + server_default=None, ), "int", id="simple nullable None generated True", @@ -265,6 +276,7 @@ def _construct_backref_property_artifacts(sub_type): required=False, generated=False, default=None, + server_default=None, ), "typing.Optional[int]", id="simple nullable None generated False", @@ -276,6 +288,19 @@ def _construct_backref_property_artifacts(sub_type): required=False, generated=None, default=1, + server_default=None, + ), + "int", + id="simple nullable None default given", + ), + pytest.param( + _construct_simple_artifacts( + type_="integer", + nullable=None, + required=False, + generated=None, + default=None, + server_default="value 1", ), "int", id="simple nullable None default given", diff --git a/tests/open_alchemy/models_file/artifacts/test_typed_dict.py b/tests/open_alchemy/models_file/artifacts/test_typed_dict.py index 798328c2..3c57147d 100644 --- a/tests/open_alchemy/models_file/artifacts/test_typed_dict.py +++ b/tests/open_alchemy/models_file/artifacts/test_typed_dict.py @@ -45,6 +45,7 @@ def _construct_simple_property_artifacts( autoincrement=None, index=None, unique=None, + server_default=None, foreign_key=None, kwargs=None, foreign_key_kwargs=None, diff --git a/tests/open_alchemy/models_file/model/test_integration.py b/tests/open_alchemy/models_file/model/test_integration.py index d419d9ba..fdbc91b8 100644 --- a/tests/open_alchemy/models_file/model/test_integration.py +++ b/tests/open_alchemy/models_file/model/test_integration.py @@ -53,6 +53,7 @@ def test_generate(): autoincrement=None, index=None, unique=None, + server_default=None, foreign_key=None, kwargs=None, foreign_key_kwargs=None, diff --git a/tests/open_alchemy/models_file/test_integration.py b/tests/open_alchemy/models_file/test_integration.py index 940d4846..b6e4a546 100644 --- a/tests/open_alchemy/models_file/test_integration.py +++ b/tests/open_alchemy/models_file/test_integration.py @@ -56,6 +56,7 @@ def _construct_simple_property_artifacts(type_, nullable): autoincrement=None, index=None, unique=None, + server_default=None, foreign_key=None, kwargs=None, foreign_key_kwargs=None, diff --git a/tests/open_alchemy/schemas/artifacts/property_/test_simple.py b/tests/open_alchemy/schemas/artifacts/property_/test_simple.py index fd721a68..c109da6a 100644 --- a/tests/open_alchemy/schemas/artifacts/property_/test_simple.py +++ b/tests/open_alchemy/schemas/artifacts/property_/test_simple.py @@ -55,6 +55,7 @@ "x-primary-key": True, "x-index": True, "x-unique": True, + "x-server-default": "value 1", "x-foreign-key": "foreign.key", "x-kwargs": {"key": "value"}, "x-foreign-key-kwargs": {"key": "value"}, @@ -276,6 +277,49 @@ 3, id="allOf default prefer local", ), + pytest.param( + None, + {**DEFAULT_SCHEMA, "type": "integer"}, + {}, + "extension.server_default", + None, + id="server default undefined", + ), + pytest.param( + None, + {**DEFAULT_SCHEMA, "type": "integer", "x-server-default": "value 1"}, + {}, + "extension.server_default", + "value 1", + id="server default", + ), + pytest.param( + None, + {"$ref": "#/components/schemas/RefSchema"}, + { + "RefSchema": { + **DEFAULT_SCHEMA, + "type": "integer", + "x-server-default": "value 2", + } + }, + "extension.server_default", + "value 2", + id="$ref server default", + ), + pytest.param( + None, + { + "allOf": [ + {"$ref": "#/components/schemas/RefSchema"}, + {**DEFAULT_SCHEMA, "type": "integer", "x-server-default": "value 3"}, + ] + }, + {"RefSchema": {"server default": 4}}, + "extension.server_default", + "value 3", + id="allOf server default prefer local", + ), pytest.param( None, {**DEFAULT_SCHEMA}, diff --git a/tests/open_alchemy/schemas/artifacts/test_artifacts.py b/tests/open_alchemy/schemas/artifacts/test_artifacts.py index 02180f8a..9b72a837 100644 --- a/tests/open_alchemy/schemas/artifacts/test_artifacts.py +++ b/tests/open_alchemy/schemas/artifacts/test_artifacts.py @@ -396,6 +396,7 @@ def _construct_simple_property_artifacts(type_, required): autoincrement=None, index=None, unique=None, + server_default=None, foreign_key=None, kwargs=None, foreign_key_kwargs=None, diff --git a/tests/open_alchemy/schemas/artifacts/test_types.py b/tests/open_alchemy/schemas/artifacts/test_types.py index e3f23d8b..e2ef9853 100644 --- a/tests/open_alchemy/schemas/artifacts/test_types.py +++ b/tests/open_alchemy/schemas/artifacts/test_types.py @@ -49,6 +49,7 @@ autoincrement=None, index=None, unique=None, + server_default=None, foreign_key=None, kwargs=None, foreign_key_kwargs=None, @@ -63,6 +64,7 @@ autoincrement=False, index=False, unique=True, + server_default=None, foreign_key="foreign.key", kwargs={"key_1": "value 1"}, foreign_key_kwargs={"key_2": "value 2"}, @@ -97,6 +99,7 @@ autoincrement=None, index=None, unique=None, + server_default=None, foreign_key=None, kwargs=None, foreign_key_kwargs=None, @@ -132,6 +135,7 @@ autoincrement=None, index=None, unique=None, + server_default=None, foreign_key=None, kwargs=None, foreign_key_kwargs=None, diff --git a/tests/open_alchemy/schemas/foreign_key/test_calculate_foreign_key_property_artifacts.py b/tests/open_alchemy/schemas/foreign_key/test_calculate_foreign_key_property_artifacts.py index 8ed52898..be5a6389 100644 --- a/tests/open_alchemy/schemas/foreign_key/test_calculate_foreign_key_property_artifacts.py +++ b/tests/open_alchemy/schemas/foreign_key/test_calculate_foreign_key_property_artifacts.py @@ -200,6 +200,33 @@ ), id="many-to-one default", ), + pytest.param( + "Schema", + {}, + "ref_schema", + {"$ref": "#/components/schemas/RefSchema"}, + { + "RefSchema": { + "x-tablename": "ref_schema", + "type": "object", + "properties": { + "id": {"type": "integer", "x-server-default": "value 1"} + }, + } + }, + ( + "Schema", + "ref_schema_id", + { + "type": "integer", + "x-server-default": "value 1", + "x-foreign-key": "ref_schema.id", + "x-dict-ignore": True, + "nullable": False, + }, + ), + id="many-to-one server default", + ), pytest.param( "Schema", {}, diff --git a/tests/open_alchemy/schemas/validation/property_/relationship/property_/test_x_to_one.py b/tests/open_alchemy/schemas/validation/property_/relationship/property_/test_x_to_one.py index be916149..28137500 100644 --- a/tests/open_alchemy/schemas/validation/property_/relationship/property_/test_x_to_one.py +++ b/tests/open_alchemy/schemas/validation/property_/relationship/property_/test_x_to_one.py @@ -62,6 +62,12 @@ (False, "property is JSON"), id="many to one $ref JSON", ), + pytest.param( + {"allOf": [{"$ref": "#/components/schemas/RefSchema"}, {"x-json": False}]}, + {"RefSchema": {"type": "object", "x-tablename": "ref_schema", "x-json": True}}, + (True, None), + id="many to one allOf prefer local json False", + ), pytest.param( {"$ref": "#/components/schemas/RefSchema"}, {"RefSchema": {"type": "object", "x-tablename": "ref_schema"}}, @@ -127,6 +133,18 @@ (True, None), id="many to one nullable allOf", ), + pytest.param( + {"allOf": [{"$ref": "#/components/schemas/RefSchema"}, {"nullable": True}]}, + { + "RefSchema": { + "type": "object", + "x-tablename": "ref_schema", + "nullable": "True", + } + }, + (True, None), + id="many to one nullable allOf prefer local", + ), pytest.param( { "allOf": [ @@ -174,6 +192,23 @@ (True, None), id="many to one description allOf", ), + pytest.param( + { + "allOf": [ + {"$ref": "#/components/schemas/RefSchema"}, + {"description": "description 1"}, + ] + }, + { + "RefSchema": { + "type": "object", + "x-tablename": "ref_schema", + "description": True, + } + }, + (True, None), + id="many to one description allOf prefer local", + ), pytest.param( { "allOf": [ @@ -216,6 +251,18 @@ (True, None), id="many to one writeOnly allOf", ), + pytest.param( + {"allOf": [{"$ref": "#/components/schemas/RefSchema"}, {"writeOnly": True}]}, + { + "RefSchema": { + "type": "object", + "x-tablename": "ref_schema", + "writeOnly": "True", + } + }, + (True, None), + id="many to one writeOnly allOf prefer local", + ), pytest.param( { "allOf": [ @@ -263,6 +310,23 @@ (True, None), id="many to one backref allOf", ), + pytest.param( + { + "allOf": [ + {"$ref": "#/components/schemas/RefSchema"}, + {"x-backref": "schema"}, + ] + }, + { + "RefSchema": { + "type": "object", + "x-tablename": "ref_schema", + "x-backref": True, + } + }, + (True, None), + id="many to one backref allOf prefer local", + ), pytest.param( { "allOf": [ @@ -314,6 +378,23 @@ (True, None), id="many to one foreign-key-column allOf", ), + pytest.param( + { + "allOf": [ + {"$ref": "#/components/schemas/RefSchema"}, + {"x-foreign-key-column": "id"}, + ] + }, + { + "RefSchema": { + "type": "object", + "x-tablename": "ref_schema", + "x-foreign-key-column": True, + } + }, + (True, None), + id="many to one foreign-key-column allOf prefer local", + ), pytest.param( { "allOf": [ @@ -355,6 +436,23 @@ (False, "malformed schema :: The x-kwargs property must be of type dict. "), id="many to one allOf kwargs not dict", ), + pytest.param( + { + "allOf": [ + {"$ref": "#/components/schemas/RefSchema"}, + {"x-kwargs": {"key": "value"}}, + ] + }, + { + "RefSchema": { + "type": "object", + "x-tablename": "ref_schema", + "x-kwargs": True, + } + }, + (True, None), + id="many to one allOf kwargs prefer local", + ), pytest.param( { "allOf": [ @@ -389,6 +487,30 @@ (True, None), id="many to one $ref uselist", ), + pytest.param( + {"$ref": "#/components/schemas/RefSchema"}, + { + "RefSchema": { + "type": "object", + "x-tablename": "ref_schema", + "x-uselist": "True", + } + }, + (False, "malformed schema :: The x-uselist property must be of type boolean. "), + id="many to one $ref uselist invalid", + ), + pytest.param( + {"allOf": [{"$ref": "#/components/schemas/RefSchema"}, {"x-uselist": True}]}, + { + "RefSchema": { + "type": "object", + "x-tablename": "ref_schema", + "x-uselist": "True", + } + }, + (True, None), + id="many to one $ref uselist prefer local", + ), pytest.param( {"allOf": [{"$ref": "#/components/schemas/RefSchema"}, {"x-uselist": True}]}, {"RefSchema": {"type": "object", "x-tablename": "ref_schema"}}, diff --git a/tests/open_alchemy/schemas/validation/property_/test_json.py b/tests/open_alchemy/schemas/validation/property_/test_json.py index 936fa805..b9b73629 100644 --- a/tests/open_alchemy/schemas/validation/property_/test_json.py +++ b/tests/open_alchemy/schemas/validation/property_/test_json.py @@ -12,30 +12,85 @@ (False, "malformed schema :: A nullable value must be of type boolean. "), id="malformed nullable", ), + pytest.param( + { + "allOf": [ + {"$ref": "#/components/schemas/RefSchema"}, + {"nullable": "True"}, + ] + }, + {"RefSchema": {"nullable": True}}, + (False, "malformed schema :: A nullable value must be of type boolean. "), + id="integer nullable prefer local not boolean", + ), pytest.param( {"description": True}, {}, (False, "malformed schema :: A description value must be of type string. "), id="malformed description", ), + pytest.param( + { + "allOf": [ + {"$ref": "#/components/schemas/RefSchema"}, + {"description": True}, + ] + }, + {"RefSchema": {"description": "description 1"}}, + (False, "malformed schema :: A description value must be of type string. "), + id="integer description prefer local not string", + ), pytest.param( {"readOnly": "False"}, {}, (False, "malformed schema :: A readOnly property must be of type boolean. "), id="malformed readOnly", ), + pytest.param( + { + "allOf": [ + {"$ref": "#/components/schemas/RefSchema"}, + {"readOnly": "True"}, + ] + }, + {"RefSchema": {"readOnly": True}}, + (False, "malformed schema :: A readOnly property must be of type boolean. "), + id="integer readOnly prefer local not boolean", + ), pytest.param( {"writeOnly": "False"}, {}, (False, "malformed schema :: A writeOnly property must be of type boolean. "), id="malformed writeOnly", ), + pytest.param( + { + "allOf": [ + {"$ref": "#/components/schemas/RefSchema"}, + {"writeOnly": "True"}, + ] + }, + {"RefSchema": {"writeOnly": True}}, + (False, "malformed schema :: A writeOnly property must be of type boolean. "), + id="integer writeOnly prefer local not boolean", + ), pytest.param( {"x-index": "False"}, {}, (False, "malformed schema :: A index value must be of type boolean. "), id="malformed x-index", ), + pytest.param( + { + "allOf": [ + {"$ref": "#/components/schemas/RefSchema"}, + {"x-index": "True"}, + ] + }, + {"RefSchema": {"x-index": True}}, + (False, "malformed schema :: A index value must be of type boolean. "), + id="integer x-index prefer local not boolean", + ), pytest.param( {"$ref": "#/components/schemas/RefSchema"}, {}, @@ -61,6 +116,17 @@ (False, "malformed schema :: A unique value must be of type boolean. "), id="malformed x-unique", ), + pytest.param( + { + "allOf": [ + {"$ref": "#/components/schemas/RefSchema"}, + {"x-unique": "True"}, + ] + }, + {"RefSchema": {"x-unique": True}}, + (False, "malformed schema :: A unique value must be of type boolean. "), + id="integer x-unique prefer local not boolean", + ), pytest.param({"x-unique": True}, {}, (True, None), id="x-unique"), pytest.param( {"x-primary-key": "False"}, @@ -71,6 +137,20 @@ ), id="malformed x-primary-key", ), + pytest.param( + { + "allOf": [ + {"$ref": "#/components/schemas/RefSchema"}, + {"x-primary-key": "True"}, + ] + }, + {"RefSchema": {"x-primary-key": True}}, + ( + False, + "malformed schema :: The x-primary-key property must be of type boolean. ", + ), + id="integer x-primary-key prefer local not boolean", + ), pytest.param({"x-primary-key": True}, {}, (True, None), id="x-primary-key"), pytest.param( {"x-autoincrement": "False"}, @@ -78,6 +158,12 @@ (False, "json properties do not support x-autoincrement"), id="x-autoincrement defined", ), + pytest.param( + {"x-server-default": "False"}, + {}, + (False, "json properties do not support x-server-default"), + id="x-server-default defined", + ), pytest.param( {"x-foreign-key": True}, {}, diff --git a/tests/open_alchemy/schemas/validation/property_/test_simple.py b/tests/open_alchemy/schemas/validation/property_/test_simple.py index 8422b8fc..3cdf1a5d 100644 --- a/tests/open_alchemy/schemas/validation/property_/test_simple.py +++ b/tests/open_alchemy/schemas/validation/property_/test_simple.py @@ -110,6 +110,17 @@ (False, "malformed schema :: A format value must be of type string. "), id="format not string", ), + pytest.param( + { + "allOf": [ + {"$ref": "#/components/schemas/RefSchema"}, + {"type": "integer", "format": True}, + ] + }, + {"RefSchema": {"type": "integer", "format": "int32"}}, + (False, "malformed schema :: A format value must be of type string. "), + id="format prefer local not string", + ), pytest.param( {"type": "integer", "format": "unsupported"}, {}, @@ -188,6 +199,17 @@ (False, "malformed schema :: A maxLength value must be of type integer. "), id="string maxLength not integer", ), + pytest.param( + { + "allOf": [ + {"$ref": "#/components/schemas/RefSchema"}, + {"type": "string", "maxLength": "1"}, + ] + }, + {"RefSchema": {"type": "string", "maxLength": 1}}, + (False, "malformed schema :: A maxLength value must be of type integer. "), + id="string maxLength prefer local not integer", + ), pytest.param( {"type": "string", "maxLength": 1}, {}, @@ -230,6 +252,17 @@ (False, "malformed schema :: A nullable value must be of type boolean. "), id="integer nullable not boolean", ), + pytest.param( + { + "allOf": [ + {"$ref": "#/components/schemas/RefSchema"}, + {"type": "integer", "nullable": "True"}, + ] + }, + {"RefSchema": {"type": "integer", "nullable": True}}, + (False, "malformed schema :: A nullable value must be of type boolean. "), + id="integer nullable prefer local not boolean", + ), pytest.param( {"type": "integer", "nullable": True}, {}, @@ -260,6 +293,17 @@ (False, "malformed schema :: A description value must be of type string. "), id="integer description not string", ), + pytest.param( + { + "allOf": [ + {"$ref": "#/components/schemas/RefSchema"}, + {"type": "integer", "description": True}, + ] + }, + {"RefSchema": {"type": "integer", "description": "description 1"}}, + (False, "malformed schema :: A description value must be of type string. "), + id="integer description prefer local not string", + ), pytest.param( {"type": "integer", "description": "description 1"}, {}, @@ -293,6 +337,20 @@ ), id="integer x-primary-key not boolean", ), + pytest.param( + { + "allOf": [ + {"$ref": "#/components/schemas/RefSchema"}, + {"type": "integer", "x-primary-key": "True"}, + ] + }, + {"RefSchema": {"type": "integer", "x-primary-key": True}}, + ( + False, + "malformed schema :: The x-primary-key property must be of type boolean. ", + ), + id="integer x-primary-key prefer local not boolean", + ), pytest.param( {"type": "integer", "x-primary-key": True}, {}, @@ -323,6 +381,17 @@ (False, "malformed schema :: A autoincrement value must be of type boolean. "), id="integer x-autoincrement not boolean", ), + pytest.param( + { + "allOf": [ + {"$ref": "#/components/schemas/RefSchema"}, + {"type": "integer", "x-autoincrement": "True"}, + ] + }, + {"RefSchema": {"type": "integer", "x-autoincrement": True}}, + (False, "malformed schema :: A autoincrement value must be of type boolean. "), + id="integer x-autoincrement prefer local not boolean", + ), pytest.param( {"type": "integer", "x-autoincrement": True}, {}, @@ -353,6 +422,17 @@ (False, "malformed schema :: A index value must be of type boolean. "), id="integer x-index not boolean", ), + pytest.param( + { + "allOf": [ + {"$ref": "#/components/schemas/RefSchema"}, + {"type": "integer", "x-index": "True"}, + ] + }, + {"RefSchema": {"type": "integer", "x-index": True}}, + (False, "malformed schema :: A index value must be of type boolean. "), + id="integer x-index prefer local not boolean", + ), pytest.param( {"type": "integer", "x-index": True}, {}, @@ -383,6 +463,17 @@ (False, "malformed schema :: A unique value must be of type boolean. "), id="integer x-unique not boolean", ), + pytest.param( + { + "allOf": [ + {"$ref": "#/components/schemas/RefSchema"}, + {"type": "integer", "x-unique": "True"}, + ] + }, + {"RefSchema": {"type": "integer", "x-unique": True}}, + (False, "malformed schema :: A unique value must be of type boolean. "), + id="integer x-unique prefer local not boolean", + ), pytest.param( {"type": "integer", "x-unique": True}, {}, @@ -416,6 +507,20 @@ ), id="integer x-foreign-key not string", ), + pytest.param( + { + "allOf": [ + {"$ref": "#/components/schemas/RefSchema"}, + {"type": "integer", "x-foreign-key": True}, + ] + }, + {"RefSchema": {"type": "integer", "x-foreign-key": "foreign.key"}}, + ( + False, + "malformed schema :: The x-foreign-key property must be of type string. ", + ), + id="integer x-foreign-key prefer local not string", + ), pytest.param( {"type": "integer", "x-foreign-key": "foreign.key"}, {}, @@ -450,6 +555,21 @@ ), id="integer default invalid", ), + pytest.param( + { + "allOf": [ + {"$ref": "#/components/schemas/RefSchema"}, + {"type": "integer", "default": True}, + ] + }, + {"RefSchema": {"type": "integer", "default": 1}}, + ( + False, + "malformed schema :: The default value does not conform to the schema. The " + "value is: True ", + ), + id="integer default prefer local invalid", + ), pytest.param( {"type": "integer", "default": 1}, {}, @@ -510,12 +630,152 @@ (True, None), id="boolean default", ), + pytest.param( + {"type": "integer", "x-server-default": True}, + {}, + ( + False, + "malformed schema :: A x-server-default value must be of type string. ", + ), + id="integer x-server-default not string", + ), + pytest.param( + { + "allOf": [ + {"$ref": "#/components/schemas/RefSchema"}, + {"type": "integer", "x-server-default": True}, + ] + }, + {"RefSchema": {"type": "integer", "x-server-default": "value 1"}}, + ( + False, + "malformed schema :: A x-server-default value must be of type string. ", + ), + id="integer x-server-default prefer local not string", + ), + pytest.param( + {"type": "integer", "x-server-default": "value 1"}, + {}, + (True, None), + id="integer x-server-default", + ), + pytest.param( + {"type": "number", "x-server-default": "value 1"}, + {}, + (True, None), + id="number x-server-default", + ), + pytest.param( + {"type": "string", "x-server-default": "value 1"}, + {}, + (True, None), + id="string x-server-default", + ), + pytest.param( + {"type": "boolean", "x-server-default": "value 1"}, + {}, + (True, None), + id="boolean x-server-default", + ), + pytest.param( + {"type": "integer", "readOnly": "True"}, + {}, + (False, "malformed schema :: A readOnly property must be of type boolean. "), + id="integer readOnly not boolean", + ), + pytest.param( + { + "allOf": [ + {"$ref": "#/components/schemas/RefSchema"}, + {"type": "integer", "readOnly": "True"}, + ] + }, + {"RefSchema": {"type": "integer", "readOnly": True}}, + (False, "malformed schema :: A readOnly property must be of type boolean. "), + id="integer readOnly prefer local not boolean", + ), + pytest.param( + {"type": "integer", "readOnly": True}, + {}, + (True, None), + id="integer readOnly", + ), + pytest.param( + {"type": "number", "readOnly": True}, + {}, + (True, None), + id="number readOnly", + ), + pytest.param( + {"type": "string", "readOnly": True}, + {}, + (True, None), + id="string readOnly", + ), + pytest.param( + {"type": "boolean", "readOnly": True}, + {}, + (True, None), + id="boolean readOnly", + ), + pytest.param( + {"type": "integer", "writeOnly": "True"}, + {}, + (False, "malformed schema :: A writeOnly property must be of type boolean. "), + id="integer writeOnly not boolean", + ), + pytest.param( + { + "allOf": [ + {"$ref": "#/components/schemas/RefSchema"}, + {"type": "integer", "writeOnly": "True"}, + ] + }, + {"RefSchema": {"type": "integer", "writeOnly": True}}, + (False, "malformed schema :: A writeOnly property must be of type boolean. "), + id="integer writeOnly prefer local not boolean", + ), + pytest.param( + {"type": "integer", "writeOnly": True}, + {}, + (True, None), + id="integer writeOnly", + ), + pytest.param( + {"type": "number", "writeOnly": True}, + {}, + (True, None), + id="number writeOnly", + ), + pytest.param( + {"type": "string", "writeOnly": True}, + {}, + (True, None), + id="string writeOnly", + ), + pytest.param( + {"type": "boolean", "writeOnly": True}, + {}, + (True, None), + id="boolean writeOnly", + ), pytest.param( {"type": "integer", "x-kwargs": 1}, {}, (False, "malformed schema :: The x-kwargs property must be of type dict. "), id="x-kwargs not dict", ), + pytest.param( + { + "allOf": [ + {"$ref": "#/components/schemas/RefSchema"}, + {"type": "integer", "x-kwargs": 1}, + ] + }, + {"RefSchema": {"type": "integer", "x-kwargs": {"key": "value"}}}, + (False, "malformed schema :: The x-kwargs property must be of type dict. "), + id="x-kwargs prefer local not dict", + ), pytest.param( {"type": "integer", "x-kwargs": {1: True}}, {}, @@ -534,6 +794,12 @@ (False, "x-kwargs :: may not contain the default key"), id="x-kwargs has default", ), + pytest.param( + {"type": "integer", "x-kwargs": {"server_default": "value 1"}}, + {}, + (False, "x-kwargs :: may not contain the server_default key"), + id="x-kwargs has server_default", + ), pytest.param( {"type": "integer", "x-kwargs": {"primary_key": True}}, {}, @@ -580,6 +846,31 @@ ), id="x-foreign-key-kwargs not dict", ), + pytest.param( + { + "allOf": [ + {"$ref": "#/components/schemas/RefSchema"}, + { + "type": "integer", + "x-foreign-key-kwargs": 1, + "x-foreign-key": "foreign.key", + }, + ] + }, + { + "RefSchema": { + "type": "integer", + "x-foreign-key-kwargs": {"key": "value"}, + "x-foreign-key": "foreign.key", + } + }, + ( + False, + "malformed schema :: The x-foreign-key-kwargs property must be of type " + "dict. ", + ), + id="x-foreign-key-kwargs prefer local not dict", + ), pytest.param( { "type": "integer", diff --git a/tests/test_examples b/tests/test_examples index 9f1491f5..503ca165 100755 --- a/tests/test_examples +++ b/tests/test_examples @@ -24,6 +24,10 @@ cd examples/default python models.py python models_traditional.py cd - +cd examples/server_default +python models.py +python models_traditional.py +cd - cd examples/read_only python models.py python models_traditional.py