summaryrefslogtreecommitdiffstats
path: root/src/ansiblelint/schemas/main.py
diff options
context:
space:
mode:
Diffstat (limited to 'src/ansiblelint/schemas/main.py')
-rw-r--r--src/ansiblelint/schemas/main.py90
1 files changed, 84 insertions, 6 deletions
diff --git a/src/ansiblelint/schemas/main.py b/src/ansiblelint/schemas/main.py
index 590aea3..45b0c48 100644
--- a/src/ansiblelint/schemas/main.py
+++ b/src/ansiblelint/schemas/main.py
@@ -1,8 +1,11 @@
"""Module containing cached JSON schemas."""
+
from __future__ import annotations
import json
import logging
+import re
+import typing
from typing import TYPE_CHECKING
import jsonschema
@@ -18,20 +21,95 @@ if TYPE_CHECKING:
from ansiblelint.file_utils import Lintable
+def find_best_deep_match(
+ errors: jsonschema.ValidationError,
+) -> jsonschema.ValidationError:
+ """Return the deepest schema validation error."""
+
+ def iter_validation_error(
+ err: jsonschema.ValidationError,
+ ) -> typing.Iterator[jsonschema.ValidationError]:
+ if err.context:
+ for e in err.context:
+ yield e
+ yield from iter_validation_error(e)
+
+ return max(iter_validation_error(errors), key=_deep_match_relevance)
+
+
def validate_file_schema(file: Lintable) -> list[str]:
"""Return list of JSON validation errors found."""
+ schema = {}
if file.kind not in JSON_SCHEMAS:
return [f"Unable to find JSON Schema '{file.kind}' for '{file.path}' file."]
try:
# convert yaml to json (keys are converted to strings)
yaml_data = yaml_load_safe(file.content)
json_data = json.loads(json.dumps(yaml_data))
- jsonschema.validate(
- instance=json_data,
- schema=_schema_cache[file.kind],
- )
+ schema = _schema_cache[file.kind]
+
+ validator = jsonschema.validators.validator_for(schema)
+ v = validator(schema)
+ try:
+ error = next(v.iter_errors(json_data))
+ except StopIteration:
+ return []
+ if error.context:
+ error = find_best_deep_match(error)
+ # determine if we want to use our own messages embedded into schemas inside title/markdownDescription fields
+ if "not" in error.schema and len(error.schema["not"]) == 0:
+ message = error.schema["title"]
+ schema = error.schema
+ else:
+ message = f"{error.json_path} {error.message}"
+
+ documentation_url = ""
+ for json_schema in (error.schema, schema):
+ for k in ("description", "markdownDescription"):
+ if k in json_schema:
+ # Find standalone URLs and also markdown urls.
+ match = re.search(
+ r"\[.*?\]\((?P<url>https?://[^\s]+)\)|(?P<url2>https?://[^\s]+)",
+ json_schema[k],
+ )
+ if match:
+ documentation_url = next(
+ x for x in match.groups() if x is not None
+ )
+ break
+ if documentation_url:
+ break
+ if documentation_url:
+ if not message.endswith("."):
+ message += "."
+ message += f" See {documentation_url}"
except yaml.constructor.ConstructorError as exc:
return [f"Failed to load YAML file '{file.path}': {exc.problem}"]
except ValidationError as exc:
- return [exc.message]
- return []
+ message = exc.message
+ documentation_url = ""
+ for k in ("description", "markdownDescription"):
+ if k in schema:
+ # Find standalone URLs and also markdown urls.
+ match = re.search(
+ r"\[.*?\]\((https?://[^\s]+)\)|https?://[^\s]+",
+ schema[k],
+ )
+ if match:
+ documentation_url = match.groups()[0]
+ break
+ if documentation_url:
+ if not message.endswith("."):
+ message += "."
+ message += f" See {documentation_url}"
+ return [message]
+ return [message]
+
+
+def _deep_match_relevance(error: jsonschema.ValidationError) -> tuple[bool | int, ...]:
+ validator = error.validator
+ return (
+ validator not in ("anyOf", "oneOf"), # type: ignore[comparison-overlap]
+ len(error.absolute_path),
+ -len(error.path),
+ )