yamlschema
This commit is contained in:
parent
4e8ac14536
commit
6f8a1bf831
3 changed files with 126 additions and 0 deletions
12
projects/yamlschema/BUILD
Normal file
12
projects/yamlschema/BUILD
Normal file
|
@ -0,0 +1,12 @@
|
|||
py_library(
|
||||
name = "yamlschema",
|
||||
srcs = [
|
||||
"yamlschema.py",
|
||||
],
|
||||
imports = [
|
||||
".",
|
||||
],
|
||||
deps = [
|
||||
py_requirement("PyYAML"),
|
||||
]
|
||||
)
|
3
projects/yamlschema/README.md
Normal file
3
projects/yamlschema/README.md
Normal file
|
@ -0,0 +1,3 @@
|
|||
# YAML Schema
|
||||
|
||||
A pocket library that implements some amount of jsonschema validation against YAML documents.
|
111
projects/yamlschema/yamlschema.py
Normal file
111
projects/yamlschema/yamlschema.py
Normal file
|
@ -0,0 +1,111 @@
|
|||
"""
|
||||
JSONSchema linting for YAML documents.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import typing as t
|
||||
|
||||
from yaml.nodes import MappingNode, Node, ScalarNode, SequenceNode
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def lint_mapping(schema, node: Node) -> t.List[str]:
|
||||
lint: t.List[str] = []
|
||||
if schema["type"] != "object" or not isinstance(node, MappingNode):
|
||||
raise TypeError(
|
||||
f"Expected {schema['type']}, got {node.id} {str(node.start_mark).lstrip()}"
|
||||
)
|
||||
|
||||
additional_allowed: bool = schema.get("additionalProperties", False) != False
|
||||
additional_type: t.Union[dict, bool] = (
|
||||
schema.get("additionalProperties") if additional_allowed
|
||||
else {}
|
||||
)
|
||||
properties: dict = schema.get("properties", {})
|
||||
required: t.List[str] = schema.get("required", [])
|
||||
|
||||
for k in required:
|
||||
if k not in [_k.value for _k, _v in node.value]:
|
||||
raise TypeError(
|
||||
f"Required key {k!r} absent from mapping {str(node.start_mark).lstrip()}"
|
||||
)
|
||||
|
||||
for k, v in node.value:
|
||||
if k.value in properties:
|
||||
lint.extend(lint_document(properties.get(k.value), v))
|
||||
|
||||
elif additional_allowed:
|
||||
# 'true' is a way to encode the any type.
|
||||
if additional_type == True:
|
||||
pass
|
||||
else:
|
||||
lint.extend(lint_document(additional_type, v))
|
||||
else:
|
||||
lint.append(
|
||||
f"Key {k.value!r} is not allowed by schema {str(node.start_mark).lstrip()}"
|
||||
)
|
||||
|
||||
return lint
|
||||
|
||||
|
||||
def lint_sequence(schema, node: Node) -> t.List[str]:
|
||||
""""FIXME.
|
||||
|
||||
There aren't sequences we need to lint in the current schema design, punting.
|
||||
|
||||
"""
|
||||
|
||||
if schema["type"] != "array" or not isinstance(node, SequenceNode):
|
||||
raise TypeError(
|
||||
f"Expected {schema['type']}, got {node.id} {str(node.start_mark).lstrip()}"
|
||||
)
|
||||
|
||||
lint = []
|
||||
subschema = schema.get("items")
|
||||
if subschema:
|
||||
for item in node.value:
|
||||
lint.extend(lint_document(subschema, item))
|
||||
return lint
|
||||
|
||||
|
||||
def lint_scalar(schema, node: Node) -> t.List[str]:
|
||||
"""FIXME.
|
||||
|
||||
The only terminal we care about linting in the current schema is {"type": "string"}.
|
||||
|
||||
"""
|
||||
if schema["type"] not in ["string", "number"] or not isinstance(node, ScalarNode):
|
||||
raise TypeError(
|
||||
f"Expected {schema['type']}, got {node.id} {str(node.start_mark).lstrip()}"
|
||||
)
|
||||
|
||||
lint = []
|
||||
if schema["type"] == "string":
|
||||
if not isinstance(node.value, str):
|
||||
lint.append(f"Expected string, got {node.id} {str(node.start_mark).lstrip()}")
|
||||
else:
|
||||
log.info(f"Ignoring unlintable scalar, schema {schema!r} {str(node.start_mark).lstrip()}")
|
||||
|
||||
return lint
|
||||
|
||||
|
||||
def lint_document(schema, node):
|
||||
"""Lint a document.
|
||||
|
||||
Given a Node within a document (or the root of a document!), return a
|
||||
(possibly empty!) list of lint or raise in case of fatal errors.
|
||||
|
||||
"""
|
||||
|
||||
if schema == True or schema == {}:
|
||||
return []
|
||||
elif isinstance(node, MappingNode):
|
||||
return lint_mapping(schema, node)
|
||||
elif isinstance(node, SequenceNode):
|
||||
return lint_sequence(schema, node)
|
||||
elif isinstance(node, ScalarNode):
|
||||
return lint_scalar(schema, node)
|
||||
else:
|
||||
return []
|
Loading…
Reference in a new issue