"""JSON files."""
from __future__ import annotations
import json
from itertools import chain
from typing import TYPE_CHECKING, ClassVar, Iterator
from loguru import logger
from nitpick import fields
from nitpick.blender import BaseDoc, Comparison, JsonDoc, flatten_quotes, unflatten_quotes
from nitpick.plugins import hookimpl
from nitpick.plugins.base import NitpickPlugin
from nitpick.schemas import BaseNitpickSchema
from nitpick.violations import Fuss, SharedViolations, ViolationEnum
if TYPE_CHECKING:
from nitpick.plugins.info import FileInfo
from nitpick.typedefs import JsonDict
KEY_CONTAINS_KEYS = "contains_keys"
KEY_CONTAINS_JSON = "contains_json"
VALUE_PLACEHOLDER = "<some value here>"
[docs]class JsonFileSchema(BaseNitpickSchema):
"""Validation schema for any JSON file added to the style."""
contains_keys = fields.List(fields.NonEmptyString)
contains_json = fields.Dict(fields.NonEmptyString, fields.JsonString)
[docs]class JsonPlugin(NitpickPlugin):
"""Enforce configurations and autofix JSON files.
Add the configurations for the file name you wish to check.
Style example: :gitref:`the default config for package.json <src/nitpick/resources/javascript/package-json.toml>`.
"""
validation_schema = JsonFileSchema
identify_tags: ClassVar = {"json"}
violation_base_code = 340
fixable = True
[docs] def enforce_rules(self) -> Iterator[Fuss]:
"""Enforce rules for missing keys and JSON content."""
json_doc = JsonDoc(path=self.file_path)
blender: JsonDict = json_doc.as_object.copy() if self.autofix else {}
comparison = Comparison(json_doc, self.expected_dict_from_contains_keys(), self.special_config)()
if comparison.missing:
yield from self.report(SharedViolations.MISSING_VALUES, blender, comparison.missing)
comparison = Comparison(json_doc, self.expected_dict_from_contains_json(), self.special_config)()
if comparison.has_changes:
yield from chain(
self.report(SharedViolations.DIFFERENT_VALUES, blender, comparison.diff),
self.report(SharedViolations.MISSING_VALUES, blender, comparison.missing),
)
if self.autofix and self.dirty and blender:
self.file_path.write_text(JsonDoc(obj=unflatten_quotes(blender)).reformatted)
[docs] def expected_dict_from_contains_keys(self):
"""Expected dict created from "contains_keys" values."""
return unflatten_quotes(
{key: VALUE_PLACEHOLDER for key in set(self.expected_config.get(KEY_CONTAINS_KEYS) or [])}
)
[docs] def expected_dict_from_contains_json(self):
"""Expected dict created from "contains_json" values."""
expected_config = {}
# TODO: feat: accept key as a jmespath expression, value is valid JSON
for key, json_string in (self.expected_config.get(KEY_CONTAINS_JSON) or {}).items():
try:
expected_config[key] = json.loads(json_string)
except json.JSONDecodeError as err: # noqa: PERF203
# This should not happen, because the style was already validated before.
# Maybe the NIP??? code was disabled by the user?
logger.error(f"{err} on {KEY_CONTAINS_JSON} while checking {self.file_path}")
continue
return expected_config
[docs] def report(self, violation: ViolationEnum, blender: JsonDict, change: BaseDoc | None):
"""Report a violation while optionally modifying the JSON dict."""
if not change:
return
if blender:
blender.update(flatten_quotes(change.as_object))
self.dirty = True
yield self.reporter.make_fuss(violation, change.reformatted, prefix="", fixed=self.autofix)
@property
def initial_contents(self) -> str:
"""Suggest the initial content for this missing file."""
suggestion = flatten_quotes(self.expected_dict_from_contains_keys())
suggestion.update(flatten_quotes(self.expected_dict_from_contains_json()))
return self.write_initial_contents(JsonDoc, unflatten_quotes(suggestion))
[docs]@hookimpl
def plugin_class() -> type[NitpickPlugin]:
"""Handle JSON files."""
return JsonPlugin
[docs]@hookimpl
def can_handle(info: FileInfo) -> type[NitpickPlugin] | None:
"""Handle JSON files."""
if JsonPlugin.identify_tags & info.tags:
return JsonPlugin
return None