mirror of
https://github.com/HeyPuter/firefox.git
synced 2026-11-13 20:14:57 -10:00
We are using features from modern JSONSchema drafts, like dependantRequired, in the Firefox Messaging System schemas. jsonschema 3.x does not support these features. Experimenter is also being updated to 4.17.3 and we want to keep our validation tests in sync with Experimenter. Differential Revision: https://phabricator.services.mozilla.com/D172457
300 lines
8.3 KiB
Python
300 lines
8.3 KiB
Python
"""
|
|
The ``jsonschema`` command line.
|
|
"""
|
|
|
|
from json import JSONDecodeError
|
|
from textwrap import dedent
|
|
import argparse
|
|
import json
|
|
import sys
|
|
import traceback
|
|
import warnings
|
|
|
|
try:
|
|
from importlib import metadata
|
|
except ImportError:
|
|
import importlib_metadata as metadata # type: ignore
|
|
|
|
try:
|
|
from pkgutil import resolve_name
|
|
except ImportError:
|
|
from pkgutil_resolve_name import resolve_name # type: ignore
|
|
|
|
import attr
|
|
|
|
from jsonschema.exceptions import SchemaError
|
|
from jsonschema.validators import RefResolver, validator_for
|
|
|
|
warnings.warn(
|
|
(
|
|
"The jsonschema CLI is deprecated and will be removed in a future "
|
|
"version. Please use check-jsonschema instead, which can be installed "
|
|
"from https://pypi.org/project/check-jsonschema/"
|
|
),
|
|
DeprecationWarning,
|
|
stacklevel=2,
|
|
)
|
|
|
|
|
|
class _CannotLoadFile(Exception):
|
|
pass
|
|
|
|
|
|
@attr.s
|
|
class _Outputter:
|
|
|
|
_formatter = attr.ib()
|
|
_stdout = attr.ib()
|
|
_stderr = attr.ib()
|
|
|
|
@classmethod
|
|
def from_arguments(cls, arguments, stdout, stderr):
|
|
if arguments["output"] == "plain":
|
|
formatter = _PlainFormatter(arguments["error_format"])
|
|
elif arguments["output"] == "pretty":
|
|
formatter = _PrettyFormatter()
|
|
return cls(formatter=formatter, stdout=stdout, stderr=stderr)
|
|
|
|
def load(self, path):
|
|
try:
|
|
file = open(path)
|
|
except FileNotFoundError:
|
|
self.filenotfound_error(path=path, exc_info=sys.exc_info())
|
|
raise _CannotLoadFile()
|
|
|
|
with file:
|
|
try:
|
|
return json.load(file)
|
|
except JSONDecodeError:
|
|
self.parsing_error(path=path, exc_info=sys.exc_info())
|
|
raise _CannotLoadFile()
|
|
|
|
def filenotfound_error(self, **kwargs):
|
|
self._stderr.write(self._formatter.filenotfound_error(**kwargs))
|
|
|
|
def parsing_error(self, **kwargs):
|
|
self._stderr.write(self._formatter.parsing_error(**kwargs))
|
|
|
|
def validation_error(self, **kwargs):
|
|
self._stderr.write(self._formatter.validation_error(**kwargs))
|
|
|
|
def validation_success(self, **kwargs):
|
|
self._stdout.write(self._formatter.validation_success(**kwargs))
|
|
|
|
|
|
@attr.s
|
|
class _PrettyFormatter:
|
|
|
|
_ERROR_MSG = dedent(
|
|
"""\
|
|
===[{type}]===({path})===
|
|
|
|
{body}
|
|
-----------------------------
|
|
""",
|
|
)
|
|
_SUCCESS_MSG = "===[SUCCESS]===({path})===\n"
|
|
|
|
def filenotfound_error(self, path, exc_info):
|
|
return self._ERROR_MSG.format(
|
|
path=path,
|
|
type="FileNotFoundError",
|
|
body="{!r} does not exist.".format(path),
|
|
)
|
|
|
|
def parsing_error(self, path, exc_info):
|
|
exc_type, exc_value, exc_traceback = exc_info
|
|
exc_lines = "".join(
|
|
traceback.format_exception(exc_type, exc_value, exc_traceback),
|
|
)
|
|
return self._ERROR_MSG.format(
|
|
path=path,
|
|
type=exc_type.__name__,
|
|
body=exc_lines,
|
|
)
|
|
|
|
def validation_error(self, instance_path, error):
|
|
return self._ERROR_MSG.format(
|
|
path=instance_path,
|
|
type=error.__class__.__name__,
|
|
body=error,
|
|
)
|
|
|
|
def validation_success(self, instance_path):
|
|
return self._SUCCESS_MSG.format(path=instance_path)
|
|
|
|
|
|
@attr.s
|
|
class _PlainFormatter:
|
|
|
|
_error_format = attr.ib()
|
|
|
|
def filenotfound_error(self, path, exc_info):
|
|
return "{!r} does not exist.\n".format(path)
|
|
|
|
def parsing_error(self, path, exc_info):
|
|
return "Failed to parse {}: {}\n".format(
|
|
"<stdin>" if path == "<stdin>" else repr(path),
|
|
exc_info[1],
|
|
)
|
|
|
|
def validation_error(self, instance_path, error):
|
|
return self._error_format.format(file_name=instance_path, error=error)
|
|
|
|
def validation_success(self, instance_path):
|
|
return ""
|
|
|
|
|
|
def _resolve_name_with_default(name):
|
|
if "." not in name:
|
|
name = "jsonschema." + name
|
|
return resolve_name(name)
|
|
|
|
|
|
parser = argparse.ArgumentParser(
|
|
description="JSON Schema Validation CLI",
|
|
)
|
|
parser.add_argument(
|
|
"-i", "--instance",
|
|
action="append",
|
|
dest="instances",
|
|
help="""
|
|
a path to a JSON instance (i.e. filename.json) to validate (may
|
|
be specified multiple times). If no instances are provided via this
|
|
option, one will be expected on standard input.
|
|
""",
|
|
)
|
|
parser.add_argument(
|
|
"-F", "--error-format",
|
|
help="""
|
|
the format to use for each validation error message, specified
|
|
in a form suitable for str.format. This string will be passed
|
|
one formatted object named 'error' for each ValidationError.
|
|
Only provide this option when using --output=plain, which is the
|
|
default. If this argument is unprovided and --output=plain is
|
|
used, a simple default representation will be used.
|
|
""",
|
|
)
|
|
parser.add_argument(
|
|
"-o", "--output",
|
|
choices=["plain", "pretty"],
|
|
default="plain",
|
|
help="""
|
|
an output format to use. 'plain' (default) will produce minimal
|
|
text with one line for each error, while 'pretty' will produce
|
|
more detailed human-readable output on multiple lines.
|
|
""",
|
|
)
|
|
parser.add_argument(
|
|
"-V", "--validator",
|
|
type=_resolve_name_with_default,
|
|
help="""
|
|
the fully qualified object name of a validator to use, or, for
|
|
validators that are registered with jsonschema, simply the 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",
|
|
version=metadata.version("jsonschema"),
|
|
)
|
|
parser.add_argument(
|
|
"schema",
|
|
help="the path to a JSON Schema to validate with (i.e. schema.json)",
|
|
)
|
|
|
|
|
|
def parse_args(args):
|
|
arguments = vars(parser.parse_args(args=args or ["--help"]))
|
|
if arguments["output"] != "plain" and arguments["error_format"]:
|
|
raise parser.error(
|
|
"--error-format can only be used with --output plain",
|
|
)
|
|
if arguments["output"] == "plain" and arguments["error_format"] is None:
|
|
arguments["error_format"] = "{error.instance}: {error.message}\n"
|
|
return arguments
|
|
|
|
|
|
def _validate_instance(instance_path, instance, validator, outputter):
|
|
invalid = False
|
|
for error in validator.iter_errors(instance):
|
|
invalid = True
|
|
outputter.validation_error(instance_path=instance_path, error=error)
|
|
|
|
if not invalid:
|
|
outputter.validation_success(instance_path=instance_path)
|
|
return invalid
|
|
|
|
|
|
def main(args=sys.argv[1:]):
|
|
sys.exit(run(arguments=parse_args(args=args)))
|
|
|
|
|
|
def run(arguments, stdout=sys.stdout, stderr=sys.stderr, stdin=sys.stdin):
|
|
outputter = _Outputter.from_arguments(
|
|
arguments=arguments,
|
|
stdout=stdout,
|
|
stderr=stderr,
|
|
)
|
|
|
|
try:
|
|
schema = outputter.load(arguments["schema"])
|
|
except _CannotLoadFile:
|
|
return 1
|
|
|
|
if arguments["validator"] is None:
|
|
arguments["validator"] = validator_for(schema)
|
|
|
|
try:
|
|
arguments["validator"].check_schema(schema)
|
|
except SchemaError as error:
|
|
outputter.validation_error(
|
|
instance_path=arguments["schema"],
|
|
error=error,
|
|
)
|
|
return 1
|
|
|
|
if arguments["instances"]:
|
|
load, instances = outputter.load, arguments["instances"]
|
|
else:
|
|
def load(_):
|
|
try:
|
|
return json.load(stdin)
|
|
except JSONDecodeError:
|
|
outputter.parsing_error(
|
|
path="<stdin>", exc_info=sys.exc_info(),
|
|
)
|
|
raise _CannotLoadFile()
|
|
instances = ["<stdin>"]
|
|
|
|
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:
|
|
instance = load(each)
|
|
except _CannotLoadFile:
|
|
exit_code = 1
|
|
else:
|
|
exit_code |= _validate_instance(
|
|
instance_path=each,
|
|
instance=instance,
|
|
validator=validator,
|
|
outputter=outputter,
|
|
)
|
|
|
|
return exit_code
|