"""
Methods for interacting with or reasoning about JSON Schema and CSV codelists.
"""
from collections import UserDict
from copy import deepcopy
import json_merge_patch
from jscc.exceptions import DuplicateKeyError
from jscc.testing.util import http_get
[docs]
def is_codelist(fieldnames):
"""
:param list fieldnames: the fieldnames of the CSV
:returns: whether the CSV is a codelist
:rtype: bool
"""
# OCDS uses titlecase. BODS uses lowercase.
return 'Code' in fieldnames or 'code' in fieldnames
[docs]
def is_json_schema(data):
"""
:param dict data: JSON data
:returns: whether the JSON data is a JSON Schema
:rtype: bool
"""
return '$schema' in data or 'definitions' in data or '$defs' in data or 'properties' in data
[docs]
def is_json_merge_patch(data):
"""
:param dict data: JSON data
:returns: whether the JSON data is a JSON Merge Patch
:rtype: bool
"""
return '$schema' not in data and ('definitions' in data or '$defs' in data or 'properties' in data)
[docs]
def is_array_of_objects(field):
"""
:param dict field: the field
:returns: whether a field is an array of objects
:rtype: bool
"""
return 'array' in field.get('type', []) and any(key in field.get('items', {}) for key in ('$ref', 'properties'))
[docs]
def is_missing_property(field, prop):
"""
:param dict field: the field
:param str prop: the property
:returns: whether a field's property isn't set, is empty, or is whitespace
:rtype: bool
"""
return prop not in field or not field[prop] and not isinstance(field[prop], (bool, int, float)) or \
isinstance(field[prop], str) and not field[prop].strip()
[docs]
def get_types(field):
"""
Returns a field's "type" as a list.
:param dict field: the field
:returns: a field's "type"
:rtype: list
"""
if 'type' not in field:
return []
if isinstance(field['type'], str):
return [field['type']]
return field['type']
[docs]
def extend_schema(basename, schema, metadata, codelists=None):
"""
Patches a JSON Schema with an extension's dependencies, recursively.
If :code:`codelists` is provided, it will be updated with the codelists from the dependencies.
:param str basename: the JSON Schema file's basename
:param dict schema: the JSON Schema file's parsed contents
:param dict metadata: the extension metadata file's parsed contents
:param set codelists: any set
:returns: the patched schema
:rtype: dict
"""
def recurse(metadata):
urls = metadata.get('dependencies', []) + metadata.get('testDependencies', [])
for metadata_url in urls:
patch_url = f"{metadata_url.rsplit('/', 1)[0]}/{basename}"
metadata = http_get(metadata_url).json()
patch = http_get(patch_url).json()
if codelists is not None:
codelists.update(metadata.get('codelists', []))
json_merge_patch.merge(patched, patch)
recurse(metadata)
patched = deepcopy(schema)
recurse(metadata)
return patched
[docs]
class RejectingDict(UserDict):
"""
A ``dict`` that raises an error if a key is set more than once.
"""
# See https://tools.ietf.org/html/rfc7493#section-2.3
def __setitem__(self, k, v):
if k in self:
raise DuplicateKeyError(k)
return super().__setitem__(k, v)
[docs]
def rejecting_dict(pairs):
"""
An ``object_pairs_hook`` method that allows a key to be set at most once.
"""
# Return the wrapped dict, not the RejectingDict itself, because jsonschema checks the type.
return RejectingDict(pairs).data