diff --git a/jsonschema/cli.py b/jsonschema/cli.py index 713193979..a6ca43c8e 100644 --- a/jsonschema/cli.py +++ b/jsonschema/cli.py @@ -15,7 +15,7 @@ from jsonschema import __version__ from jsonschema._reflect import namedAny from jsonschema.exceptions import SchemaError -from jsonschema.validators import validator_for +from jsonschema.validators import RefResolver, validator_for class _CannotLoadFile(Exception): @@ -178,6 +178,14 @@ def _namedAnyWithDefault(name): of the class. """, ) +parser.add_argument( + "--base-uri", + help=""" + a base URI to assign to the provided schema, even if it does not + declare one (via e.g. $id). This option can be used if you wish to + resolve relative references to a particular URI (or local path) + """, +) parser.add_argument( "--version", action="version", @@ -252,7 +260,12 @@ def load(_): raise _CannotLoadFile() instances = [""] - validator = arguments["validator"](schema) + resolver = RefResolver( + base_uri=arguments["base_uri"], + referrer=schema, + ) if arguments["base_uri"] is not None else None + + validator = arguments["validator"](schema, resolver=resolver) exit_code = 0 for each in instances: try: diff --git a/jsonschema/tests/test_cli.py b/jsonschema/tests/test_cli.py index 42167599f..22528feb5 100644 --- a/jsonschema/tests/test_cli.py +++ b/jsonschema/tests/test_cli.py @@ -1,5 +1,6 @@ from io import StringIO from json import JSONDecodeError +from pathlib import Path from textwrap import dedent from unittest import TestCase import errno @@ -7,9 +8,14 @@ import os import subprocess import sys +import tempfile from jsonschema import Draft4Validator, Draft7Validator, __version__, cli -from jsonschema.exceptions import SchemaError, ValidationError +from jsonschema.exceptions import ( + RefResolutionError, + SchemaError, + ValidationError, +) from jsonschema.tests._helpers import captured_output from jsonschema.validators import _LATEST_VERSION, validate @@ -683,6 +689,87 @@ def test_successful_validation_of_just_the_schema_pretty_output(self): stderr="", ) + def test_successful_validation_via_explicit_base_uri(self): + ref_schema_file = tempfile.NamedTemporaryFile(delete=False) + self.addCleanup(os.remove, ref_schema_file.name) + + ref_path = Path(ref_schema_file.name) + ref_path.write_text('{"definitions": {"num": {"type": "integer"}}}') + + schema = f'{{"$ref": "{ref_path.name}#definitions/num"}}' + + self.assertOutputs( + files=dict(some_schema=schema, some_instance='1'), + argv=[ + "-i", "some_instance", + "--base-uri", ref_path.parent.as_uri() + "/", + "some_schema", + ], + stdout="", + stderr="", + ) + + def test_unsuccessful_validation_via_explicit_base_uri(self): + ref_schema_file = tempfile.NamedTemporaryFile(delete=False) + self.addCleanup(os.remove, ref_schema_file.name) + + ref_path = Path(ref_schema_file.name) + ref_path.write_text('{"definitions": {"num": {"type": "integer"}}}') + + schema = f'{{"$ref": "{ref_path.name}#definitions/num"}}' + + self.assertOutputs( + files=dict(some_schema=schema, some_instance='"1"'), + argv=[ + "-i", "some_instance", + "--base-uri", ref_path.parent.as_uri() + "/", + "some_schema", + ], + exit_code=1, + stdout="", + stderr="1: '1' is not of type 'integer'\n", + ) + + def test_nonexistent_file_with_explicit_base_uri(self): + schema = '{"$ref": "someNonexistentFile.json#definitions/num"}' + instance = "1" + + with self.assertRaises(RefResolutionError) as e: + self.assertOutputs( + files=dict( + some_schema=schema, + some_instance=instance, + ), + argv=[ + "-i", "some_instance", + "--base-uri", Path.cwd().as_uri(), + "some_schema", + ], + ) + error = str(e.exception) + self.assertIn("/someNonexistentFile.json'", error) + + def test_invalid_exlicit_base_uri(self): + schema = '{"$ref": "foo.json#definitions/num"}' + instance = "1" + + with self.assertRaises(RefResolutionError) as e: + self.assertOutputs( + files=dict( + some_schema=schema, + some_instance=instance, + ), + argv=[ + "-i", "some_instance", + "--base-uri", "not@UR1", + "some_schema", + ], + ) + error = str(e.exception) + self.assertEqual( + error, "unknown url type: 'foo.json'", + ) + def test_it_validates_using_the_latest_validator_when_unspecified(self): # There isn't a better way now I can think of to ensure that the # latest version was used, given that the call to validator_for