def _handle_object_reference(*, spec: types.Schema, schemas: types.Schemas) -> types.Schema: """ Determine the foreign key schema for an object reference. Args: spec: The schema of the object reference. schemas: All defined schemas. Returns: The foreign key schema. """ tablename = helpers.get_ext_prop(source=spec, name="x-tablename") if not tablename: raise exceptions.MalformedSchemaError( "Referenced object is missing x-tablename property.") properties = spec.get("properties") if properties is None: raise exceptions.MalformedSchemaError( "Referenced object does not have any properties.") logical_name = "id" id_spec = properties.get(logical_name) if id_spec is None: raise exceptions.MalformedSchemaError( "Referenced object does not have id property.") # Preparing specification prepared_id_spec = helpers.prepare_schema(schema=id_spec, schemas=schemas) id_type = prepared_id_spec.get("type") if id_type is None: raise exceptions.MalformedSchemaError( "Referenced object id property does not have a type.") return {"type": id_type, "x-foreign-key": f"{tablename}.id"}
def handle_column( *, schema: types.Schema, schemas: types.Schemas, required: typing.Optional[bool] = None, ) -> typing.Tuple[types.ColumnSchema, facades.sqlalchemy.column.Column]: """ Generate column based on OpenAPI schema property. Assume any $ref and allOf has already been resolved. Args: schema: The schema for the column. schemas: Used to resolve any $ref. required: Whether the object property is required. Returns: The logical name and the SQLAlchemy column based on the schema. """ schema = helpers.prepare_schema(schema=schema, schemas=schemas) artifacts = check_schema(schema=schema, required=required) column_schema = _calculate_column_schema(artifacts=artifacts, schema=schema) column = construct_column(artifacts=artifacts) return column_schema, column
def column_factory( *, spec: types.Schema, schemas: types.Schemas, required: typing.Optional[bool] = None, logical_name: str, ) -> typing.List[typing.Tuple[str, sqlalchemy.Column]]: """ Generate column based on OpenAPI schema property. Args: spec: The schema for the column. schemas: Used to resolve any $ref. required: Whether the object property is required. logical_name: The logical name in the specification for the schema. Returns: The logical name and the SQLAlchemy column based on the schema. """ # Checking for the type type_ = helpers.peek_type(schema=spec, schemas=schemas) # CHandling columns if type_ != "object": spec = helpers.prepare_schema(schema=spec, schemas=schemas) return _handle_column(logical_name=logical_name, spec=spec, required=required) # Handling objects return _handle_object(spec=spec, schemas=schemas, required=required, logical_name=logical_name)
def _prepare_schema_object_common( *, schema: types.Schema, schemas: types.Schemas, array_context: bool ) -> types.ReadOnlySchemaObjectCommon: """ Check and transform readOnly schema to consistent format. Args: schema: The readOnly schema to operate on. schemas: Used to resolve any $ref. array_context: Whether checking is being done at the array items level. Changes exception messages and schema validation. Returns: The schema in a consistent format. """ # Check type try: type_ = helpers.peek.type_(schema=schema, schemas=schemas) except exceptions.TypeMissingError: raise exceptions.MalformedSchemaError( "Every readOnly property must have a type." if not array_context else "Array readOnly items must have a type." ) schema = helpers.prepare_schema(schema=schema, schemas=schemas) if type_ != "object": raise exceptions.MalformedSchemaError( "readOnly array item type must be an object." if array_context else "readyOnly property must be of type array or object." ) # Handle object properties = schema.get("properties") if properties is None: raise exceptions.MalformedSchemaError( "readOnly object definition must include properties." ) if not properties: raise exceptions.MalformedSchemaError( "readOnly object definition must include at least 1 property." ) # Initialize schema properties to return properties_schema: types.Schema = {} # Process properties for property_name, property_schema in properties.items(): property_type = helpers.peek.type_(schema=property_schema, schemas=schemas) if property_type in {"array", "object"}: raise exceptions.MalformedSchemaError( "readOnly object properties cannot be of type array nor object." ) properties_schema[property_name] = {"type": property_type} return {"type": "object", "properties": properties_schema}
def test_prepare_schema(schema, schemas, expected_schema): """ GIVEN schema, schemas and expected schema WHEN prepare_schema is called with the schema and schemas THEN the expected schema is returned. """ returned_schema = helpers.prepare_schema(schema=schema, schemas=schemas) assert returned_schema == expected_schema
def _prepare_schema_array( *, schema: types.Schema, schemas: types.Schemas ) -> types.ReadOnlyArraySchema: """ Check and transform readOnly schema to consistent format. Args: schema: The readOnly schema to operate on. schemas: Used to resolve any $ref. Returns: The schema in a consistent format. """ schema = helpers.prepare_schema(schema=schema, schemas=schemas) items_schema = schema.get("items") if items_schema is None: raise exceptions.MalformedSchemaError("A readOnly array must define its items.") array_object_schema = _prepare_schema_object_common( schema=items_schema, schemas=schemas, array_context=True ) return {"type": "array", "readOnly": True, "items": array_object_schema}
def handle_object_reference(*, spec: types.Schema, schemas: types.Schemas, fk_column: str) -> types.Schema: """ Determine the foreign key schema for an object reference. Args: spec: The schema of the object reference. schemas: All defined schemas. fk_column: The foreign column name to use. Returns: The foreign key schema. """ tablename = helpers.get_ext_prop(source=spec, name="x-tablename") if not tablename: raise exceptions.MalformedSchemaError( "Referenced object is missing x-tablename property.") properties = spec.get("properties") if properties is None: raise exceptions.MalformedSchemaError( "Referenced object does not have any properties.") fk_logical_name = fk_column if fk_column is not None else "id" fk_spec = properties.get(fk_logical_name) if fk_spec is None: raise exceptions.MalformedSchemaError( f"Referenced object does not have {fk_logical_name} property.") # Preparing specification prepared_fk_spec = helpers.prepare_schema(schema=fk_spec, schemas=schemas) fk_type = prepared_fk_spec.get("type") if fk_type is None: raise exceptions.MalformedSchemaError( f"Referenced object {fk_logical_name} property does not have a type." ) return {"type": fk_type, "x-foreign-key": f"{tablename}.{fk_logical_name}"}
def _many_to_many_column_artifacts( *, model_schema: types.Schema, schemas: types.Schemas) -> _ManyToManyColumnArtifacts: """ Retrieve column artifacts of a secondary table for a many to many relationship. Args: model_schema: The schema for one side of the many to many relationship. schemas: Used to resolve any $ref. Returns: The artifacts needed to construct a column of the secondary table in a many to many relationship. """ # Resolve $ref and merge allOf model_schema = helpers.prepare_schema(schema=model_schema, schemas=schemas) # Check schema type model_type = model_schema.get("type") if model_type is None: raise exceptions.MalformedSchemaError("Every schema must have a type.") if model_type != "object": raise exceptions.MalformedSchemaError( "A schema that is part of a many to many relationship must be of type " "object.") # Retrieve table name tablename = helpers.get_ext_prop(source=model_schema, name="x-tablename") if tablename is None: raise exceptions.MalformedSchemaError( "A schema that is part of a many to many relationship must set the " "x-tablename property.") # Find primary key properties = model_schema.get("properties") if properties is None: raise exceptions.MalformedSchemaError( "A schema that is part of a many to many relationship must have properties." ) if not properties: raise exceptions.MalformedSchemaError( "A schema that is part of a many to many relationship must have at least 1 " "property.") type_ = None format_ = None for property_name, property_schema in properties.items(): if helpers.peek.primary_key(schema=property_schema, schemas=schemas): if type_ is not None: raise exceptions.MalformedSchemaError( "A schema that is part of a many to many relationship must have " "exactly 1 primary key.") try: type_ = helpers.peek.type_(schema=property_schema, schemas=schemas) except exceptions.TypeMissingError: raise exceptions.MalformedSchemaError( "A schema that is part of a many to many relationship must define " "a type for the primary key.") format_ = helpers.peek.format_(schema=property_schema, schemas=schemas) max_length = helpers.peek.max_length(schema=property_schema, schemas=schemas) column_name = property_name if type_ is None: raise exceptions.MalformedSchemaError( "A schema that is part of a many to many relationship must have " "exactly 1 primary key.") if type_ in {"object", "array"}: raise exceptions.MalformedSchemaError( "A schema that is part of a many to many relationship cannot define it's " "primary key to be of type object nor array.") return _ManyToManyColumnArtifacts(type_, format_, tablename, column_name, max_length)
def handle_array( *, spec: types.Schema, model_schema: types.Schema, schemas: types.Schemas, logical_name: str, ) -> typing.Tuple[typing.List[typing.Tuple[str, typing.Type]], types.Schema]: """ Generate properties for a reference to another object through an array. Assume that when any allOf and $ref are resolved in the root spec the type is array. Args: spec: The schema for the column. model_schema: The schema of the one to many parent. schemas: Used to resolve any $ref. logical_name: The logical name in the specification for the schema. Returns: The logical name and the relationship for the referenced object. """ # Resolve any allOf and $ref spec = helpers.prepare_schema(schema=spec, schemas=schemas) # Get item specification item_spec = spec.get("items") if item_spec is None: raise exceptions.MalformedRelationshipError( "An array property must include items property.") obj_artifacts = object_ref.gather_object_artifacts( spec=item_spec, logical_name=logical_name, schemas=schemas) # Check for uselist if obj_artifacts.uselist is not None: raise exceptions.MalformedRelationshipError( "x-uselist is not supported for one to many relationships.") # Check referenced specification ref_spec = helpers.prepare_schema(schema=obj_artifacts.spec, schemas=schemas) ref_type = ref_spec.get("type") if ref_type != "object": raise exceptions.MalformedRelationshipError( "One to many relationships must reference an object type schema.") ref_tablename = helpers.get_ext_prop(source=ref_spec, name="x-tablename") if ref_tablename is None: raise exceptions.MalformedRelationshipError( "One to many relationships must reference a schema with " "x-tablename defined.") # Construct relationship relationship_return = ( logical_name, sqlalchemy.orm.relationship( obj_artifacts.ref_logical_name, backref=obj_artifacts.backref, secondary=obj_artifacts.secondary, ), ) # Construct entry for the addition for the model schema spec_return = { "type": "array", "items": { "type": "object", "x-de-$ref": obj_artifacts.ref_logical_name }, } # Add foreign key to referenced schema if obj_artifacts.secondary is None: _set_foreign_key( ref_model_name=obj_artifacts.ref_logical_name, model_schema=model_schema, schemas=schemas, fk_column=obj_artifacts.fk_column, ) else: table = _construct_association_table( parent_schema=model_schema, child_schema=obj_artifacts.spec, schemas=schemas, tablename=obj_artifacts.secondary, ) facades.models.set_association(table=table, name=obj_artifacts.secondary) return [relationship_return], spec_return
def gather( *, schema: types.Schema, schemas: types.Schemas, logical_name: str ) -> types.ObjectArtifacts: """ Gather artifacts for constructing a reference to another model from within an array. Args: schema: The schema of the array reference. schemas: All the model schemas used to resolve any $ref within the array reference schema. logical_name: The name of thearray reference within its parent schema. Returns: The artifacts required to construct the array reference. """ # Resolve any allOf and $ref schema = helpers.prepare_schema(schema=schema, schemas=schemas) # Get item schema item_schema = schema.get("items") if item_schema is None: raise exceptions.MalformedRelationshipError( "An array property must include items property." ) # Retrieve artifacts for the object reference within the array artifacts = object_ref.artifacts.gather( schema=item_schema, logical_name=logical_name, schemas=schemas ) # Check for uselist if ( artifacts.relationship.back_reference is not None and artifacts.relationship.back_reference.uselist is not None ): raise exceptions.MalformedRelationshipError( "x-uselist is not supported for one to many nor many to many relationships." ) # Check for nullable if artifacts.nullable is not None: raise exceptions.MalformedRelationshipError( "nullable is not supported for one to many nor many to many relationships." ) # Check referenced specification ref_schema = helpers.prepare_schema(schema=artifacts.spec, schemas=schemas) ref_tablename = helpers.ext_prop.get(source=ref_schema, name="x-tablename") if ref_tablename is None: raise exceptions.MalformedRelationshipError( "One to many relationships must reference a schema with " "x-tablename defined." ) # Add description try: description = helpers.peek.description(schema=schema, schemas={}) except exceptions.MalformedSchemaError as exc: raise exceptions.MalformedRelationshipError(str(exc)) if description is not None: artifacts.description = description return artifacts
def check_foreign_key_required( *, fk_spec: types.Schema, fk_logical_name: str, model_schema: types.Schema, schemas: types.Schemas, ) -> bool: """ Check whether a foreign key has already been defined. Assume model_schema has already resolved any $ref and allOf at the object level. They may not have been resolved at the property level. Check whether the proposed logical name is already defined on the model schema. If it has been, check that the type is correct and that the foreign key reference has been defined and points to the correct column. Raise MalformedRelationshipError if a property has already been defined with the same name as is proposed for the foreign key but it has the wrong type or does not define the correct foreign key constraint. Args: fk_spec: The proposed foreign key specification. fk_logical_name: The proposed name for the foreign key property. model_schema: The schema for the model on which the foreign key is proposed to be added. schemas: Used to resolve any $ref at the property level. Returns: Whether defining the foreign key is necessary given the model schema. """ properties = model_schema["properties"] model_fk_spec = properties.get(fk_logical_name) if model_fk_spec is None: return True model_fk_spec = helpers.prepare_schema(schema=model_fk_spec, schemas=schemas) # Check type model_fk_type = model_fk_spec.get("type") if model_fk_type is None: raise exceptions.MalformedRelationshipError( f"{fk_logical_name} does not have a type. ") fk_type = fk_spec["type"] if model_fk_type != fk_type: raise exceptions.MalformedRelationshipError( "The foreign key required for the relationship has a different type than " "the property already defined under that name. " f"The required type is {fk_type}. " f"The {fk_logical_name} property has the {model_fk_type} type.") # Check foreign key constraint model_foreign_key = helpers.get_ext_prop(source=model_fk_spec, name="x-foreign-key") foreign_key = fk_spec["x-foreign-key"] if model_foreign_key is None: raise exceptions.MalformedRelationshipError( f"The property already defined under {fk_logical_name} does not define a " 'foreign key constraint. Use the "x-foreign-key" extension property to ' f'define a foreign key constraint, for example: "{foreign_key}".') if model_foreign_key != foreign_key: raise exceptions.MalformedRelationshipError( "The foreign key required for the relationship has a different foreign " "key constraint than the property already defined under that name. " f"The required constraint is {foreign_key}. " f"The {fk_logical_name} property has the {model_foreign_key} constraint." ) return False