コード例 #1
0
def test_multiple_same_key():
    """
    GIVEN schema that has multiple schemas under allOf with the same key
    WHEN merge_all_of is called with the schema
    THEN the value of the last schema is assigned to the key in the returned schema.
    """
    schema = {"allOf": [{"key": "value_1"}, {"key": "value_2"}]}

    return_schema = helpers.merge_all_of(schema=schema, schemas={})

    assert return_schema == {"key": "value_2"}
コード例 #2
0
def test_nested_all_of():
    """
    GIVEN schema that has allOf statement with an allOf statement with a single schema
    WHEN merge_all_of is called with the schema
    THEN the schema in allOf is returned.
    """
    schema = {"allOf": [{"allOf": [{"key": "value"}]}]}

    return_schema = helpers.merge_all_of(schema=schema, schemas={})

    assert return_schema == {"key": "value"}
コード例 #3
0
def test_not_all_of():
    """
    GIVEN schema that does not have the allOf statement
    WHEN merge_all_of is called with the schema
    THEN the schema is returned.
    """
    schema = {"key": "value"}

    return_schema = helpers.merge_all_of(schema=schema, schemas={})

    assert return_schema == {"key": "value"}
コード例 #4
0
def test_multiple():
    """
    GIVEN schema that has multiple schemas under allOf
    WHEN merge_all_of is called with the schema
    THEN the merged schema of all schemas under allOf is returned.
    """
    schema = {"allOf": [{"key_1": "value_1"}, {"key_2": "value_2"}]}

    return_schema = helpers.merge_all_of(schema=schema, schemas={})

    assert return_schema == {"key_1": "value_1", "key_2": "value_2"}
コード例 #5
0
def test_ref():
    """
    GIVEN schema that has allOf statement with $ref to another schema
    WHEN merge_all_of is called with the schema
    THEN the $ref schema in allOf is returned.
    """
    schema = {"allOf": [{"$ref": "#/components/schemas/RefSchema"}]}
    schemas = {"RefSchema": {"key": "value"}}

    return_schema = helpers.merge_all_of(schema=schema, schemas=schemas)

    assert return_schema == {"key": "value"}
コード例 #6
0
def _handle_ref(
    *, logical_name: str, schema: types.Schema, schemas: types.Schemas
) -> _IntermediaryObjectArtifacts:
    """
    Gather artifacts from a $ref.

    Args:
        schema: The schema of the object reference.
        logical_name: The property name of the object reference.
        schemas: Used to resolve any $ref.

    Returns:
        The name of the referenced schema.

    """
    ref_model_name, ref_schema = helpers.ref.resolve(
        name=logical_name, schema=schema, schemas=schemas
    )
    ref_schema = helpers.merge_all_of(schema=ref_schema, schemas=schemas)

    # Check referenced schema
    try:
        type_ = helpers.peek.type_(schema=ref_schema, schemas=schemas)
    except exceptions.TypeMissingError:
        raise exceptions.MalformedRelationshipError(
            "The referenced schema does not have a type."
        )
    if type_ != "object":
        raise exceptions.MalformedRelationshipError(
            "A reference in a relationship must resolve to an object."
        )

    # Read other parameters
    backref = helpers.ext_prop.get(source=ref_schema, name="x-backref")
    uselist = helpers.ext_prop.get(source=ref_schema, name="x-uselist")
    secondary = helpers.ext_prop.get(source=ref_schema, name="x-secondary")
    fk_column_name = helpers.ext_prop.get(
        source=ref_schema, name="x-foreign-key-column"
    )
    if fk_column_name is None:
        fk_column_name = "id"
    nullable = helpers.peek.nullable(schema=ref_schema, schemas={})

    return _IntermediaryObjectArtifacts(
        ref_model_name=ref_model_name,
        fk_column_name=fk_column_name,
        ref_schema=ref_schema,
        backref=backref,
        uselist=uselist,
        secondary=secondary,
        nullable=nullable,
    )
コード例 #7
0
def test_properties(all_of_schema, expected_properties):
    """
    GIVEN schema that has allOf with schemas with given properties and expected
        properties
    WHEN merge_all_of is called with the schema
    THEN the returned schema has the expected properties.
    """
    schema = {"allOf": all_of_schema}
    schemas = {}

    return_schema = helpers.merge_all_of(schema=schema, schemas=schemas)

    assert return_schema["properties"] == expected_properties
コード例 #8
0
def test_required(all_of_schema, expected_required):
    """
    GIVEN schema that has allOf with schemas with given required properties and expected
        final required
    WHEN merge_all_of is called with the schema
    THEN the returned schema has the expected required property.
    """
    schema = {"allOf": all_of_schema}
    schemas = {}

    return_schema = helpers.merge_all_of(schema=schema, schemas=schemas)

    assert sorted(return_schema["required"]) == sorted(expected_required)
コード例 #9
0
def test_backrefs(all_of_schema, expected_backrefs):
    """
    GIVEN schema that has allOf with schemas with given backrefs and expected
        backrefs
    WHEN merge_all_of is called with the schema
    THEN the returned schema has the expected backrefs.
    """
    schema = {"allOf": all_of_schema}
    schemas = {}

    return_schema = helpers.merge_all_of(schema=schema, schemas=schemas)

    assert return_schema["x-backrefs"] == expected_backrefs
コード例 #10
0
def set_(
    *,
    ref_model_name: str,
    model_schema: types.Schema,
    schemas: types.Schemas,
    fk_column: str,
) -> None:
    """
    Set the foreign key on an existing model or add it to the schemas.

    For an array reference that is defined in the form of a one to many relationship,
    the referenced model requires the addition of a foreign key that would not generally
    be defined by the user. Therefore, the appropriate schema has to be calculated and
    then somehow added to the referenced model. At the time of processing, the
    referenced model may already have been constructed. This requires a check on
    open_alchemy.models for the referenced model. If it is there, it is modified by
    adding a new column to it. Otherwise, the schema of the referenced model is altered
    to include the column so that it is constructed when that model is processed.

    Raise MalformedRelationshipError of the referenced model is not found in the
    schemas.

    Args:
        ref_model_name: The name of the referenced model.
        model_schema: The schema which contains the one to many relationship.
        schemas: All the model schemas used to look for the referenced model and to
            resolve any $ref.
        fk_column: The name of the foreign key column to be added.

    """
    # Check that referenced model is in schemas
    ref_schema = schemas.get(ref_model_name)
    if ref_schema is None:
        raise exceptions.MalformedRelationshipError(
            f"{ref_model_name} referenced in relationship was not found in the "
            "schemas.")
    # Prepare schema for construction. Note any top level $ref must already be resolved.
    ref_schema = helpers.merge_all_of(schema=ref_schema, schemas=schemas)

    # Calculate foreign key artifacts
    fk_logical_name, fk_artifacts = object_ref.foreign_key.gather_artifacts(
        model_schema=model_schema, schemas=schemas, fk_column=fk_column)

    # Check whether the foreign key has already been defined in the referenced model
    fk_required = object_ref.foreign_key.check_required(
        artifacts=fk_artifacts,
        fk_logical_name=fk_logical_name,
        model_schema=ref_schema,
        schemas=schemas,
    )
    if not fk_required:
        return

    # Handle model already constructed by altering the model on open_aclehmy.model
    ref_model: TOptUtilityBase = facades.models.get_model(name=ref_model_name)
    if ref_model is not None:
        fk_column = column.construct_column(artifacts=fk_artifacts)
        setattr(ref_model, fk_logical_name, fk_column)
        return

    # Handle model not constructed by adding the foreign key schema to the model schema
    fk_schema: types.Schema = column.calculate_schema(  # type: ignore
        artifacts=fk_artifacts, dict_ignore=True)
    fk_object_schema = {
        "type": "object",
        "properties": {
            fk_logical_name: {
                **fk_schema,
                "x-foreign-key": fk_artifacts.extension.foreign_key,
            }
        },
    }
    if "allOf" not in schemas[ref_model_name]:
        # Add new top level allOf
        schemas[ref_model_name] = {
            "allOf": [schemas[ref_model_name], fk_object_schema]
        }
        return
    # Append to existing allOf
    schemas[ref_model_name]["allOf"].append(fk_object_schema)
コード例 #11
0
def _set_foreign_key(
    *,
    ref_model_name: str,
    model_schema: types.Schema,
    schemas: types.Schemas,
    fk_column: str,
) -> None:
    """
    Set the foreign key on an existing model or add it to the schemas.

    Args:
        ref_model_name: The name of the referenced model.
        model_schema: The schema of the one to many parent.
        schemas: All the model schemas.
        fk_column: The name of the foreign key column.

    """
    # Check that model is in schemas
    if ref_model_name not in schemas:
        raise exceptions.MalformedRelationshipError(
            f"{ref_model_name} referenced in relationship was not found in the "
            "schemas.")

    # Calculate foreign key specification
    fk_spec = object_ref.handle_object_reference(spec=model_schema,
                                                 schemas=schemas,
                                                 fk_column=fk_column)

    # Calculate values for foreign key
    tablename = helpers.get_ext_prop(source=model_schema, name="x-tablename")
    fk_logical_name = f"{tablename}_{fk_column}"

    # Gather referenced schema
    ref_schema = schemas[ref_model_name]
    # Any top level $ref must already be resolved
    ref_schema = helpers.merge_all_of(schema=ref_schema, schemas=schemas)
    fk_required = object_ref.check_foreign_key_required(
        fk_spec=fk_spec,
        fk_logical_name=fk_logical_name,
        model_schema=ref_schema,
        schemas=schemas,
    )
    if not fk_required:
        return

    # Handle model already constructed
    ref_model: TOptUtilityBase = facades.models.get_model(name=ref_model_name)
    if ref_model is not None:
        # Construct foreign key
        _, fk_column = column.handle_column(schema=fk_spec)
        setattr(ref_model, fk_logical_name, fk_column)
        return

    # Handle model not constructed
    schemas[ref_model_name] = {
        "allOf": [
            schemas[ref_model_name],
            {
                "type": "object",
                "properties": {
                    fk_logical_name: {
                        **fk_spec, "x-dict-ignore": True
                    }
                },
            },
        ]
    }
コード例 #12
0
def _handle_object(
    *,
    spec: types.Schema,
    schemas: types.Schemas,
    required: typing.Optional[bool] = None,
    logical_name: str,
) -> typing.List[typing.Tuple[str, typing.Union[sqlalchemy.Column,
                                                typing.Type]]]:
    """
    Generate properties for a reference to another object.

    Assume that, when any $ref and allOf are resolved, the schema is an object.

    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 for the foreign key and the logical
        name and relationship for the reference to the object.

    """
    # Default backref
    backref = None

    # Checking for $ref and allOf
    ref = spec.get("$ref")
    all_of = spec.get("allOf")

    if ref is not None:
        # Handling $ref
        ref_logical_name, spec = helpers.resolve_ref(name=logical_name,
                                                     schema=spec,
                                                     schemas=schemas)
        backref = helpers.get_ext_prop(source=spec, name="x-backref")
    elif all_of is not None:
        # Checking for $ref and x-backref counts
        ref_count = 0
        backref_count = 0
        for sub_spec in all_of:
            if sub_spec.get("$ref") is not None:
                ref_count += 1
            if sub_spec.get("x-backref") is not None:
                backref_count += 1
        if ref_count != 1:
            raise exceptions.MalformedManyToOneRelationshipError(
                "Many to One relationships defined with allOf must have exactly one "
                "$ref in the allOf list.")
        if backref_count > 1:
            raise exceptions.MalformedManyToOneRelationshipError(
                "Many to One relationships may have at most 1 x-backref defined."
            )

        # Handling allOf
        for sub_spec in all_of:
            backref = helpers.get_ext_prop(source=sub_spec, name="x-backref")
            if sub_spec.get("$ref") is not None:
                ref_logical_name, spec = helpers.resolve_ref(name=logical_name,
                                                             schema=sub_spec,
                                                             schemas=schemas)
    else:
        raise exceptions.MalformedManyToOneRelationshipError(
            "Many to One relationships are defined using either $ref or allOf."
        )

    # Resolving allOf
    spec = helpers.merge_all_of(schema=spec, schemas=schemas)

    # Handling object
    foreign_key_spec = _handle_object_reference(spec=spec, schemas=schemas)
    return_value = _handle_column(logical_name=f"{logical_name}_id",
                                  spec=foreign_key_spec,
                                  required=required)

    # Creating relationship
    return_value.append((logical_name,
                         sqlalchemy.orm.relationship(ref_logical_name,
                                                     backref=backref)))
    return return_value
コード例 #13
0
def gather_object_artifacts(*, spec: types.Schema, logical_name: str,
                            schemas: types.Schemas) -> ObjectArtifacts:
    """
    Collect artifacts from a specification for constructing an object reference.

    Get the prepared specification, reference logical name, back reference and foreign
    key column name from a raw object specification.

    Raise MalformedRelationshipError if neither $ref nor $allOf is found.
    Raise MalformedRelationshipError if uselist is defined but backref is not.
    Raise MalformedRelationshipError if multiple $ref, x-backref, x-secondary,
    x-foreign-key-column or x-uselist are found.

    Args:
        spec: The schema for the column.
        schemas: Used to resolve any $ref.
        logical_name: The logical name in the specification for the schema.

    Returns:
        The prepared specification, reference logical name, back reference and foreign
        key column.

    """
    # Default backref
    backref = None
    # Default uselist
    uselist = None
    # Default secondary
    secondary = None
    # Initial foreign key column
    fk_column = None

    # Checking for $ref and allOf
    ref = spec.get("$ref")
    all_of = spec.get("allOf")

    if ref is not None:
        # Handle $ref
        ref_logical_name, spec = helpers.resolve_ref(name=logical_name,
                                                     schema=spec,
                                                     schemas=schemas)
    elif all_of is not None:
        # Checking for $ref, and x-backref and x-foreign-key-column counts
        _check_object_all_of(all_of_spec=all_of)

        # Handle allOf
        for sub_spec in all_of:
            backref = helpers.get_ext_prop(source=sub_spec,
                                           name="x-backref",
                                           default=backref)
            uselist = helpers.get_ext_prop(source=sub_spec,
                                           name="x-uselist",
                                           default=uselist)
            secondary = helpers.get_ext_prop(source=sub_spec,
                                             name="x-secondary",
                                             default=secondary)
            fk_column = helpers.get_ext_prop(source=sub_spec,
                                             name="x-foreign-key-column",
                                             default=fk_column)
            if sub_spec.get("$ref") is not None:
                ref_logical_name, spec = helpers.resolve_ref(name=logical_name,
                                                             schema=sub_spec,
                                                             schemas=schemas)
    else:
        raise exceptions.MalformedRelationshipError(
            "Relationships are defined using either $ref or allOf.")

    # Resolving allOf
    spec = helpers.merge_all_of(schema=spec, schemas=schemas)

    # If backref has not been found look in referenced schema
    if backref is None:
        backref = helpers.get_ext_prop(source=spec, name="x-backref")
    # If uselist has not been found look in referenced schema
    if uselist is None:
        uselist = helpers.get_ext_prop(source=spec, name="x-uselist")
    # If secondary has not been found look in referenced schema
    if secondary is None:
        secondary = helpers.get_ext_prop(source=spec, name="x-secondary")
    # If foreign key column has not been found look in referenced schema
    if fk_column is None:
        fk_column = helpers.get_ext_prop(source=spec,
                                         name="x-foreign-key-column")
    # If foreign key column is still None, default to id
    if fk_column is None:
        fk_column = "id"

    # Check if uselist is defined and backref is not
    if uselist is not None and backref is None:
        raise exceptions.MalformedRelationshipError(
            "Relationships with x-uselist defined must also define x-backref.")

    return ObjectArtifacts(spec, ref_logical_name, backref, fk_column, uselist,
                           secondary)