Esempio n. 1
0
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"}
Esempio n. 2
0
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
Esempio n. 3
0
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)
Esempio n. 4
0
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}
Esempio n. 5
0
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
Esempio n. 6
0
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}
Esempio n. 7
0
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}"}
Esempio n. 8
0
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)
Esempio n. 9
0
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
Esempio n. 10
0
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
Esempio n. 11
0
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