def check_schema_schemas_dict(schema: types.Schema, schemas: types.Schemas) -> None: """Check that schema and schemas are dict.""" # Check schema and schemas are dict if not isinstance(schema, dict): raise exceptions.MalformedSchemaError("The schema must be a dictionary.") if not isinstance(schemas, dict): raise exceptions.MalformedSchemaError("The schemas must be a dictionary.")
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 _resolve( name: str, schema: types.Schema, schemas: types.Schemas, seen_refs: typing.Set[str], skip_name: typing.Optional[str], ) -> NameSchema: """Implement resolve.""" # Checking whether schema is a reference schema ref = schema.get(types.OpenApiProperties.REF) if ref is None: return name, schema # Check that ref is string if not isinstance(ref, str): raise exceptions.MalformedSchemaError( "The value of $ref must be a string.") # Check for circular $ref if ref in seen_refs: raise exceptions.MalformedSchemaError( "Circular reference chain detected.") seen_refs.add(ref) ref_name, ref_schema = get_ref(ref=ref, schemas=schemas) # Check if schema should be skipped if ref_name == skip_name: return name, {} return _resolve(ref_name, ref_schema, schemas, seen_refs, skip_name)
def convert(*, value: types.TColumnDefault, type_: str, format_: typing.Optional[str]) -> types.TPyColumnDefault: """ Convert an OpenAPI value to it's python type based on the format. Args: value: The value to convert. type_: The OpenAPI type. format_: The OpenAPI format. Returns: The value converted to its equivalent Python type. """ if type_ in {"object", "array"}: raise exceptions.MalformedSchemaError( "Cannot convert object nor array types to Python equivalent values." ) if isinstance(value, str) and format_ == "date": try: return datetime.date.fromisoformat(value) except ValueError: raise exceptions.MalformedSchemaError("Invalid date string.") if isinstance(value, str) and format_ == "date-time": try: return datetime.datetime.fromisoformat(value) except ValueError: raise exceptions.MalformedSchemaError("Invalid date-time string.") if isinstance(value, str) and format_ == "binary": return value.encode() if format_ == "double": raise exceptions.MalformedSchemaError( "Double format is not supported.") return value
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 _check_artifacts(*, artifacts: types.ColumnArtifacts) -> None: """ Check that the artifacts comply with overall rules. Raise MalformedSchemaError for: 1. maxLength with a. integer b. number c. boolean d. JSON e. string with the format of i. date ii. date-time 2. autoincrement with a. number b. string c. boolean d. JSON 3. format with a. boolean 4. default with JSON Args: artifacts: The artifacts to check. """ # Check whether max length was used incorrectly if artifacts.open_api.max_length is not None: if artifacts.open_api.type != "string": raise exceptions.MalformedSchemaError( f"maxLength is not supported for {artifacts.open_api.type}" ) # Must be string type if artifacts.open_api.format in {"date", "date-time"}: raise exceptions.MalformedSchemaError( "maxLength is not supported for string with the format " f"{artifacts.open_api.format}" ) # Check whether autoincrement was used incorrectly if artifacts.extension.autoincrement is not None: if artifacts.open_api.type != "integer": raise exceptions.MalformedSchemaError( f"autoincrement is not supported for {artifacts.open_api.type}" ) # Check whether format was used with boolean if artifacts.open_api.type == "boolean" and artifacts.open_api.format is not None: raise exceptions.MalformedSchemaError("format is not supported for boolean") # Check whether default was used with JSON column if artifacts.extension.json and artifacts.open_api.default is not None: raise exceptions.FeatureNotImplementedError( "default is not supported for JSON column" )
def _check_kwargs(*, value: typing.Any, key: str) -> typing.Dict[str, typing.Any]: """Check the kwargs value.""" # Check value if not isinstance(value, dict): raise exceptions.MalformedSchemaError( f"The {key} property must be of type dict.") # Check keys not_str_keys = filter(lambda key: not isinstance(key, str), value.keys()) if next(not_str_keys, None) is not None: raise exceptions.MalformedSchemaError( f"The {key} property must have string keys.") return value
def _resolve( name: str, schema: types.Schema, schemas: types.Schemas, seen_refs: typing.Set[str], skip_name: typing.Optional[str], ) -> NameSchema: """Implement resolve.""" # Checking whether schema is a reference schema ref = schema.get("$ref") if ref is None: return name, schema # Check for circular $ref if ref in seen_refs: raise exceptions.MalformedSchemaError( "Circular reference chain detected.") seen_refs.add(ref) ref_name, ref_schema = get_ref(ref=ref, schemas=schemas) # Check if schema should be skipped if ref_name == skip_name: return name, {} return _resolve(ref_name, ref_schema, schemas, seen_refs, skip_name)
def default(*, schema: types.Schema, schemas: types.Schemas) -> types.TColumnDefault: """ Retrieve the default value and check it against the schema. Args: schema: The schema to retrieve the default value from. """ # Retrieve value value = peek_key(schema=schema, schemas=schemas, key="default") if value is None: return None # Assemble schema resolved_schema: types.ColumnSchema = { "type": type_(schema=schema, schemas=schemas) } format_value = format_(schema=schema, schemas=schemas) max_length_value = max_length(schema=schema, schemas=schemas) if format_value is not None: resolved_schema["format"] = format_value if max_length_value is not None: resolved_schema["maxLength"] = max_length_value try: facades.jsonschema.validate(value, resolved_schema) except facades.jsonschema.ValidationError: raise exceptions.MalformedSchemaError( f"The default value does not conform to the schema. The value is: {value}" ) return value
def _peek_key(schema: types.Schema, schemas: types.Schemas, key: str, seen_refs: typing.Set[str]) -> typing.Any: """Implement peek_key.""" # Base case, look for type key value = schema.get(key) if value is not None: return value # Recursive case, look for $ref ref_value = schema.get("$ref") if ref_value is not None: # Check for circular $ref if ref_value in seen_refs: raise exceptions.MalformedSchemaError( "Circular reference detected.") seen_refs.add(ref_value) _, ref_schema = ref.get_ref(ref=ref_value, schemas=schemas) return _peek_key(ref_schema, schemas, key, seen_refs) # Recursive case, look for allOf all_of = schema.get("allOf") if all_of is not None: for sub_schema in all_of: value = _peek_key(sub_schema, schemas, key, seen_refs) if value is not None: return value # Base case, type or ref not found or no type in allOf return None
def inherits( *, schema: types.Schema, schemas: types.Schemas) -> typing.Optional[typing.Union[str, bool]]: """ Retrieve the value of the x-inherits extension property of the schema. Raises MalformedSchemaError if the value is not a string nor a boolean. Args: schema: The schema to get x-inherits from. schemas: The schemas for $ref lookup. Returns: The inherits or None. """ value = peek_key(schema=schema, schemas=schemas, key=types.ExtensionProperties.INHERITS) if value is None: return None if not isinstance(value, (str, bool)): raise exceptions.MalformedSchemaError( "The x-inherits property must be of type string or boolean.") return value
def nullable(*, schema: types.Schema, schemas: types.Schemas) -> typing.Optional[bool]: """ Retrieve the nullable property from a property schema or null from the type array. Raises MalformedSchemaError if the nullable value is not a boolean. Args: schema: The schema to get the nullable from. schemas: The schemas for $ref lookup. Returns: The nullable value or whether 'null' is in the type array. """ nullable_value = peek_key(schema=schema, schemas=schemas, key=types.OpenApiProperties.NULLABLE) if nullable_value is not None and not isinstance(nullable_value, bool): raise exceptions.MalformedSchemaError( "A nullable value must be of type boolean.") type_value = peek_key(schema=schema, schemas=schemas, key=types.OpenApiProperties.TYPE) if nullable_value is None and not isinstance(type_value, list): return None return nullable_value is True or (isinstance(type_value, list) and "null" in type_value)
def check_sub_schema_dict(sub_schema: typing.Any) -> dict: """Check that a sub schema in an allOf is a dict.""" if not isinstance(sub_schema, dict): raise exceptions.MalformedSchemaError( "The elements of allOf must be dictionaries." ) return sub_schema
def _check_artifacts(*, artifacts: types.ColumnArtifacts) -> None: """ Check that the artifacts comply with overall rules. Raise MalformedSchemaError for: 1. maxLength with a. integer b. number c. boolean d. string with the format of i. date ii. date-time 2. autoincrement with a. number b. string c. boolean 3. format with a. boolean Args: artifacts: The artifacts to check. """ if artifacts.open_api.max_length is not None: if artifacts.open_api.type in {"integer", "number", "boolean"}: raise exceptions.MalformedSchemaError( f"maxLength is not supported for {artifacts.open_api.type}") # Must be string type if artifacts.open_api.format in {"date", "date-time"}: raise exceptions.MalformedSchemaError( "maxLength is not supported for string with the format " f"{artifacts.open_api.format}") if artifacts.extension.autoincrement is not None: if artifacts.open_api.type in {"number", "string", "boolean"}: raise exceptions.MalformedSchemaError( f"autoincrement is not supported for {artifacts.open_api.type}" ) if artifacts.open_api.type == "boolean" and artifacts.open_api.format is not None: raise exceptions.MalformedSchemaError( "format is not supported for boolean")
def _prepare_schema( *, schema: types.Schema, schemas: types.Schemas ) -> types.ReadOnlySchema: """ 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 readOnly if not helpers.peek.read_only(schema=schema, schemas=schemas): raise exceptions.MalformedSchemaError( "A readOnly property must set readOnly to True." ) # 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 type_ == "object": return _prepare_schema_object(schema=schema, schemas=schemas) if type_ == "array": return _prepare_schema_array(schema=schema, schemas=schemas) raise exceptions.MalformedSchemaError( "A readOnly property can only be an object or array." )
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 _separate_context_path(*, ref: str) -> typing.Tuple[str, str]: """ Separate the context and path of a reference. Raise MalformedSchemaError if the reference does not contain #. Args: ref: The reference to separate. Returns: The context and path to the schema as a tuple. """ try: ref_context, ref_schema = ref.split("#") except ValueError: raise exceptions.MalformedSchemaError( f"A reference must contain exactly one #. Actual reference: {ref}") return ref_context, ref_schema
def format_(*, schema: types.Schema, schemas: types.Schemas) -> typing.Optional[str]: """ Retrieve the format property from a property schema. Raises MalformedSchemaError if the format value is not a string. Args: schema: The schema to get the format from. schemas: The schemas for $ref lookup. Returns: The format value or None if it was not found. """ value = peek_key(schema=schema, schemas=schemas, key="format") if value is None: return None if not isinstance(value, str): raise exceptions.MalformedSchemaError("A format value must be of type string.") return value
def primary_key(*, schema: types.Schema, schemas: types.Schemas) -> bool: """ Determine whether property schema is for a primary key. Raises MalformedSchemaError if the x-primary-key value is not a boolean. Args: schema: The schema to get x-primary-key from. schemas: The schemas for $ref lookup. Returns: Whether the schema is for a primary key property. """ value = peek_key(schema=schema, schemas=schemas, key="x-primary-key") if value is None: return False if not isinstance(value, bool): raise exceptions.MalformedSchemaError( "The x-primary-key property must be of type boolean.") return value
def read_only(*, schema: types.Schema, schemas: types.Schemas) -> bool: """ Determine whether schema is readOnly. Raises MalformedSchemaError if the readOnly value is not a boolean. Args: schema: The schema to get readOnly from. schemas: The schemas for $ref lookup. Returns: Whether the schema is readOnly. """ value = peek_key(schema=schema, schemas=schemas, key="readOnly") if value is None: return False if not isinstance(value, bool): raise exceptions.MalformedSchemaError( "A readOnly property must be of type boolean.") return value
def tablename(*, schema: types.Schema, schemas: types.Schemas) -> typing.Optional[str]: """ Retrieve the tablename of the schema. Raises MalformedSchemaError if the tablename value is not a string. Args: schema: The schema to get tablename from. schemas: The schemas for $ref lookup. Returns: The tablename or None. """ value = peek_key(schema=schema, schemas=schemas, key="x-tablename") if value is None: return None if not isinstance(value, str): raise exceptions.MalformedSchemaError( "The x-tablename property must be of type string." ) return value
def default(*, schema: types.Schema, schemas: types.Schemas) -> types.TColumnDefault: """ Retrieve the default value and check it against the schema. Raises MalformedSchemaError if the default value does not conform with the schema. Args: schema: The schema to retrieve the default value from. Returns: The default or None. """ # Retrieve value value = peek_key(schema=schema, schemas=schemas, key=types.OpenApiProperties.DEFAULT) if value is None: return None # Assemble schema resolved_schema: types.ColumnSchema = { types.OpenApiProperties.TYPE.value: type_(schema=schema, schemas=schemas) } format_value = format_(schema=schema, schemas=schemas) max_length_value = max_length(schema=schema, schemas=schemas) if format_value is not None: resolved_schema[types.OpenApiProperties.FORMAT.value] = format_value if max_length_value is not None: resolved_schema[ types.OpenApiProperties.MAX_LENGTH.value] = max_length_value try: jsonschema.validate(value, resolved_schema) except jsonschema.ValidationError as exc: raise exceptions.MalformedSchemaError( "The default value does not conform to the schema. " f"The value is: {repr(value)}") from exc return value
def write_only(*, schema: types.Schema, schemas: types.Schemas) -> typing.Optional[bool]: """ Determine whether schema is writeOnly. Raises MalformedSchemaError if the writeOnly value is not a boolean. Args: schema: The schema to get writeOnly from. schemas: The schemas for $ref lookup. Returns: Whether the schema is writeOnly. """ value = peek_key(schema=schema, schemas=schemas, key="writeOnly") if value is None: return None if not isinstance(value, bool): raise exceptions.MalformedSchemaError( "A writeOnly property must be of type boolean.") return value
def json(*, schema: types.Schema, schemas: types.Schemas) -> typing.Optional[bool]: """ Retrieve the value of the x-json extension property of the schema. Raises MalformedSchemaError if the value is not a boolean. Args: schema: The schema to get x-json from. schemas: The schemas for $ref lookup. Returns: The x-json value or None if the schema does not have the key. """ value = peek_key(schema=schema, schemas=schemas, key="x-json") if value is None: return None if not isinstance(value, bool): raise exceptions.MalformedSchemaError( "The x-json property must be of type boolean.") return value
def max_length(*, schema: types.Schema, schemas: types.Schemas) -> typing.Optional[int]: """ Retrieve the maxLength property from a property schema. Raises MalformedSchemaError if the maxLength value is not an integer. Args: schema: The schema to get the maxLength from. schemas: The schemas for $ref lookup. Returns: The maxLength value or None if it was not found. """ value = peek_key(schema=schema, schemas=schemas, key="maxLength") if value is None: return None if not isinstance(value, int): raise exceptions.MalformedSchemaError( "A maxLength value must be of type integer." ) return value
def nullable(*, schema: types.Schema, schemas: types.Schemas) -> typing.Optional[bool]: """ Retrieve the nullable property from a property schema. Raises MalformedSchemaError if the nullable value is not a boolean. Args: schema: The schema to get the nullable from. schemas: The schemas for $ref lookup. Returns: The nullable value. """ value = peek_key(schema=schema, schemas=schemas, key="nullable") if value is None: return None if not isinstance(value, bool): raise exceptions.MalformedSchemaError( "A nullable value must be of type boolean." ) return value
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 dict_ignore(*, schema: types.Schema, schemas: types.Schemas) -> typing.Optional[bool]: """ Retrieve the x-dict-ignore property from a property schema. Raises MalformedSchemaError if the x-dict-ignore value is not a boolean. Args: schema: The schema to get the x-dict-ignore from. schemas: The schemas for $ref lookup. Returns: The x-dict-ignore value. """ value = peek_key(schema=schema, schemas=schemas, key=types.ExtensionProperties.DICT_IGNORE) if value is None: return None if not isinstance(value, bool): raise exceptions.MalformedSchemaError( "A x-dict-ignore value must be of type boolean.") 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=types.ExtensionProperties.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 foreign_key_column(*, schema: types.Schema, schemas: types.Schemas) -> typing.Optional[str]: """ Retrieve the x-foreign-key-column of the schema. Raises MalformedSchemaError if the x-foreign-key-column value is not a string. Args: schema: The schema to get x-foreign-key-column from. schemas: The schemas for $ref lookup. Returns: The x-foreign-key-column or None. """ value = peek_key(schema=schema, schemas=schemas, key=types.ExtensionProperties.FOREIGN_KEY_COLUMN) if value is None: return None if not isinstance(value, str): raise exceptions.MalformedSchemaError( "The x-foreign-key-column property must be of type string.") return value