diff --git a/CHANGELOG.md b/CHANGELOG.md index e95f37c..c08c14a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,11 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ## [Unreleased] +### Added + +- URN features +-- To CompileToJsonSchema class: load_urn_schema_filenames option +-- To CLI: --urn-schema-filename option ### Removed diff --git a/compiletojsonschema/cli/__main__.py b/compiletojsonschema/cli/__main__.py index 993fb33..7db05e6 100644 --- a/compiletojsonschema/cli/__main__.py +++ b/compiletojsonschema/cli/__main__.py @@ -18,6 +18,12 @@ def main(): "--codelist-base-directory", help="Which directory we should look in for codelists", ) + parser.add_argument( + "-u", + "--urn-schema-filename", + help="Filenames of additional schemas to load and refer to by URN later while processing the input file", + action="append", + ) args = parser.parse_args() @@ -25,5 +31,6 @@ def main(): input_filename=args.input_file, set_additional_properties_false_everywhere=args.set_additional_properties_false_everywhere, codelist_base_directory=args.codelist_base_directory, + load_urn_schema_filenames=args.urn_schema_filename or [], ) print(ctjs.get_as_string()) diff --git a/compiletojsonschema/compiletojsonschema.py b/compiletojsonschema/compiletojsonschema.py index a670635..820b65c 100644 --- a/compiletojsonschema/compiletojsonschema.py +++ b/compiletojsonschema/compiletojsonschema.py @@ -1,4 +1,5 @@ import csv +import functools import json import os import pathlib @@ -6,6 +7,16 @@ from copy import deepcopy import jsonref +import referencing + + +def _jsonref_loader(uri, urn_registry=None): + if uri.startswith("urn:"): + if urn_registry and uri in urn_registry: + return urn_registry.contents(uri) + else: + raise Exception("URN {} not found".format(uri)) + return jsonref.jsonloader(uri) class CompileToJsonSchema: @@ -15,6 +26,7 @@ def __init__( set_additional_properties_false_everywhere=False, codelist_base_directory=None, input_schema=None, + load_urn_schema_filenames=[], ): if not isinstance(input_schema, dict) and not input_filename: raise Exception("Must pass input_filename or input_schema") @@ -27,19 +39,35 @@ def __init__( self.codelist_base_directory = os.path.expanduser(codelist_base_directory) else: self.codelist_base_directory = os.getcwd() + self.load_urn_schema_filenames = load_urn_schema_filenames def get(self): + urn_registry = None + if self.load_urn_schema_filenames: + urn_registry = referencing.Registry() + for urn_schema_filename in self.load_urn_schema_filenames: + with open(urn_schema_filename) as fp: + urn_schema_json = json.load(fp) + urn_schema_obj = referencing.Resource.from_contents(urn_schema_json) + urn_registry = urn_schema_obj @ urn_registry + if self.input_filename: with open(self.input_filename) as fp: resolved = jsonref.load( fp, + loader=functools.partial( + _jsonref_loader, urn_registry=urn_registry + ), object_pairs_hook=OrderedDict, base_uri=pathlib.Path( os.path.realpath(self.input_filename) ).as_uri(), ) elif isinstance(self.input_schema, dict): - resolved = jsonref.JsonRef.replace_refs(self.input_schema) + resolved = jsonref.JsonRef.replace_refs( + self.input_schema, + loader=functools.partial(_jsonref_loader, urn_registry=urn_registry), + ) else: raise Exception("Must pass input_filename or input_schema") diff --git a/docs/cli.rst b/docs/cli.rst index 6dc3e1f..2488e1a 100644 --- a/docs/cli.rst +++ b/docs/cli.rst @@ -37,3 +37,15 @@ To enable this mode, pass the `--set-additional-properties-false-everywhere` or compiletojsonschema -s input.json compiletojsonschema --set-additional-properties-false-everywhere input.json + + +URN Refs +-------- + +Pass the names of files to load. This can be used multiple times. + +.. code-block:: shell-session + + compiletojsonschema -u library.json -u components.json schema.json + compiletojsonschema --urn-schema-filename library.json --urn-schema-filename components.json schema.json + diff --git a/docs/features.rst b/docs/features.rst index 580618b..45a2c48 100644 --- a/docs/features.rst +++ b/docs/features.rst @@ -155,3 +155,52 @@ you may want to generate a strict version of your schema that doesn't allow any This can be used for testing - for example, checking your sample data does not have any additional properties. This is an optional mode, which defaults to off. + +URN Refs +-------- + +You can use URN's in refs. + +For example, if you have the `library.json` schema: + +.. code-block:: json + + { + "$id": "urn:library", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$defs": { + "address": { + "type": "object", + "properties": { + "address": { + "type": "string" + } + } + } + } + } + +And a schema `schema.json`: + +.. code-block:: json + + { + "$id": "urn:schema", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { + "home_address": { + "$ref": "urn:library#/$defs/address" + }, + "work_address": { + "$ref": "urn:library#/$defs/address" + } + } + } + +This will compile. + +To do so: + +* you should pass the actual schema file `schema.json` to the tool where processing should start. +* you will need to make sure the tool knows about the other `library.json` file. + diff --git a/setup.py b/setup.py index 502e2ed..fa4cb78 100644 --- a/setup.py +++ b/setup.py @@ -15,6 +15,7 @@ install_requires=[ "jsonref", "jsonschema", + "referencing", ], extras_require={ "test": extras_require_test, diff --git a/tests/fixtures/urn/library.json b/tests/fixtures/urn/library.json new file mode 100644 index 0000000..96d19d9 --- /dev/null +++ b/tests/fixtures/urn/library.json @@ -0,0 +1,19 @@ +{ + "$id": "urn:library", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$defs": { + "address": { + "title": "An Address", + "description": "A Description", + "type": "object", + "properties": { + "address": { + "type": "string" + }, + "country": { + "type": "string" + } + } + } + } +} diff --git a/tests/fixtures/urn/schema.json b/tests/fixtures/urn/schema.json new file mode 100644 index 0000000..c607a15 --- /dev/null +++ b/tests/fixtures/urn/schema.json @@ -0,0 +1,16 @@ +{ + "$id": "urn:schema", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { + "home_address": { + "title": "Home Address", + "description": "Where the person lives", + "$ref": "urn:library#/$defs/address" + }, + "work_address": { + "title": "Work Address", + "description": "Where the person works", + "$ref": "urn:library#/$defs/address" + } + } +} diff --git a/tests/test_urn.py b/tests/test_urn.py new file mode 100644 index 0000000..6fd0ffb --- /dev/null +++ b/tests/test_urn.py @@ -0,0 +1,56 @@ +import json +import os + +import jsonref +import pytest + +from compiletojsonschema.compiletojsonschema import CompileToJsonSchema + + +def test_urn(): + + input_filename = os.path.join( + os.path.dirname(os.path.realpath(__file__)), + "fixtures", + "urn", + "schema.json", + ) + + lib_filename = os.path.join( + os.path.dirname(os.path.realpath(__file__)), + "fixtures", + "urn", + "library.json", + ) + + ctjs = CompileToJsonSchema( + input_filename=input_filename, load_urn_schema_filenames=[lib_filename] + ) + out_string = ctjs.get_as_string() + out = json.loads(out_string) + + assert out["properties"]["work_address"]["title"] == "Work Address" + assert out["properties"]["work_address"]["description"] == "Where the person works" + assert ( + out["properties"]["work_address"]["properties"]["address"]["type"] == "string" + ) + assert out["properties"]["home_address"]["title"] == "Home Address" + assert out["properties"]["home_address"]["description"] == "Where the person lives" + assert ( + out["properties"]["home_address"]["properties"]["address"]["type"] == "string" + ) + + +def test_urn_not_loaded(): + + input_filename = os.path.join( + os.path.dirname(os.path.realpath(__file__)), + "fixtures", + "urn", + "schema.json", + ) + + ctjs = CompileToJsonSchema(input_filename=input_filename) + + with pytest.raises(jsonref.JsonRefError): + ctjs.get_as_string()