json_schema.py 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138
  1. """Functions that help us generate and use info.json files.
  2. """
  3. import json
  4. import hjson
  5. import jsonschema
  6. from collections.abc import Mapping
  7. from functools import lru_cache
  8. from typing import OrderedDict
  9. from pathlib import Path
  10. from milc import cli
  11. def _dict_raise_on_duplicates(ordered_pairs):
  12. """Reject duplicate keys."""
  13. d = {}
  14. for k, v in ordered_pairs:
  15. if k in d:
  16. raise ValueError("duplicate key: %r" % (k,))
  17. else:
  18. d[k] = v
  19. return d
  20. def json_load(json_file, strict=True):
  21. """Load a json file from disk.
  22. Note: file must be a Path object.
  23. """
  24. try:
  25. # Get the IO Stream for Path objects
  26. # Not necessary if the data is provided via stdin
  27. if isinstance(json_file, Path):
  28. json_file = json_file.open(encoding='utf-8')
  29. return hjson.load(json_file, object_pairs_hook=_dict_raise_on_duplicates if strict else None)
  30. except (json.decoder.JSONDecodeError, hjson.HjsonDecodeError) as e:
  31. cli.log.error('Invalid JSON encountered attempting to load {fg_cyan}%s{fg_reset}:\n\t{fg_red}%s', json_file, e)
  32. exit(1)
  33. except Exception as e:
  34. cli.log.error('Unknown error attempting to load {fg_cyan}%s{fg_reset}:\n\t{fg_red}%s', json_file, e)
  35. exit(1)
  36. @lru_cache(maxsize=0)
  37. def load_jsonschema(schema_name):
  38. """Read a jsonschema file from disk.
  39. """
  40. if Path(schema_name).exists():
  41. return json_load(schema_name)
  42. schema_path = Path(f'data/schemas/{schema_name}.jsonschema')
  43. if not schema_path.exists():
  44. schema_path = Path('data/schemas/false.jsonschema')
  45. return json_load(schema_path)
  46. @lru_cache(maxsize=0)
  47. def compile_schema_store():
  48. """Compile all our schemas into a schema store.
  49. """
  50. schema_store = {}
  51. for schema_file in Path('data/schemas').glob('*.jsonschema'):
  52. schema_data = load_jsonschema(schema_file)
  53. if not isinstance(schema_data, dict):
  54. cli.log.debug('Skipping schema file %s', schema_file)
  55. continue
  56. schema_store[schema_data['$id']] = schema_data
  57. return schema_store
  58. @lru_cache(maxsize=0)
  59. def create_validator(schema):
  60. """Creates a validator for the given schema id.
  61. """
  62. schema_store = compile_schema_store()
  63. resolver = jsonschema.RefResolver.from_schema(schema_store[schema], store=schema_store)
  64. return jsonschema.Draft202012Validator(schema_store[schema], resolver=resolver).validate
  65. def validate(data, schema):
  66. """Validates data against a schema.
  67. """
  68. validator = create_validator(schema)
  69. return validator(data)
  70. def deep_update(origdict, newdict):
  71. """Update a dictionary in place, recursing to do a depth-first deep copy.
  72. """
  73. for key, value in newdict.items():
  74. if isinstance(value, Mapping):
  75. origdict[key] = deep_update(origdict.get(key, {}), value)
  76. else:
  77. origdict[key] = value
  78. return origdict
  79. def merge_ordered_dicts(dicts):
  80. """Merges nested OrderedDict objects resulting from reading a hjson file.
  81. Later input dicts overrides earlier dicts for plain values.
  82. If any value is "!delete!", the existing value will be removed from its parent.
  83. Arrays will be appended. If the first entry of an array is "!reset!", the contents of the array will be cleared and replaced with RHS.
  84. Dictionaries will be recursively merged. If any entry is "!reset!", the contents of the dictionary will be cleared and replaced with RHS.
  85. """
  86. result = OrderedDict()
  87. def add_entry(target, k, v):
  88. if k in target and isinstance(v, (OrderedDict, dict)):
  89. if "!reset!" in v:
  90. target[k] = v
  91. else:
  92. target[k] = merge_ordered_dicts([target[k], v])
  93. if "!reset!" in target[k]:
  94. del target[k]["!reset!"]
  95. elif k in target and isinstance(v, list):
  96. if v[0] == '!reset!':
  97. target[k] = v[1:]
  98. else:
  99. target[k] = target[k] + v
  100. elif v == "!delete!" and isinstance(target, (OrderedDict, dict)):
  101. del target[k]
  102. else:
  103. target[k] = v
  104. for d in dicts:
  105. for (k, v) in d.items():
  106. add_entry(result, k, v)
  107. return result