| # Copyright 2025 The Fuchsia Authors. All rights reserved. |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| import collections.abc |
| import json |
| import typing |
| from typing import Iterable |
| |
| Any = typing.Any |
| |
| |
| # JsonMatch (or None) will be produced by JsonGet.match(). |
| # It will have attributes added to it: m.a instead of m["a"] as syntactic sugar. |
| # However, this can't be type-checked (a plain dictionary can't either), |
| # so type annotations should use type Any rather than JsonMatch to keep MyPy happy. |
| class JsonMatch: |
| def __repr__(self) -> str: |
| return f"JsonMatch<{vars(self)}>" |
| |
| |
| # Create a named singleton object to be used in match patterns, |
| # along with Any (=typing.Any) and None. |
| class Maybe: |
| pass |
| |
| |
| class JsonGet: |
| def __init__( |
| self, json_string: str | None = None, value: Any = None |
| ) -> None: |
| """Exactly one of json_string or value must be not None. |
| If it's json_string, that must be valid JSON.""" |
| assert (json_string is not None or value is not None) and ( |
| json_string is None or value is None |
| ) |
| if json_string is not None: |
| self.j = json.loads(json_string) |
| if value is not None: |
| self.j = value |
| |
| # if pattern matches the JSON data, calls callback(match) if given. `match` will be a JsonMatch. |
| # If pattern doesn't match, calls no_match() if given. |
| # Returns the match result. |
| def match( |
| self, |
| pattern: Any, |
| callback: collections.abc.Callable[[Any], None] | None = None, |
| no_match: collections.abc.Callable[[], None] | None = None, |
| ) -> Any: |
| """Matches the contained JSON value against a pattern. |
| |
| Args: |
| pattern: The pattern to match. List and dict patterns can be nested/recursive. |
| - dict: Matches a dict. Keys in the pattern are matched against keys in the data. |
| keys not in the pattern are not added to the match object. |
| - `Any`: matches any value and fails if the key is missing. |
| - `Maybe`: matches any value, or a missing key, in which case the key will be |
| added to the JsonMatch with a value of None. |
| - `None`: fails the match if the key is in the root dict. |
| - any other value must match the value fetched from data by the key. |
| - list: Extracts matches from a list. The pattern list must have exactly one |
| element, which is the pattern to match against each element of the data. |
| - Any: matches any value; requires the key to be present. |
| - Maybe: matches any value; a missing key is added with value None. |
| - None: the match fails if the key is present. |
| - other value: matches that value. |
| callback: (optional) A function to call if the pattern matches. It will be called |
| with a JsonMatch object. |
| no_match: (optional) A function to call if the pattern does not match. |
| |
| Returns: |
| The JsonMatch object if the pattern matches, or None otherwise. |
| |
| Example: |
| j = JsonGet('{"a": 1, "b": 2}') |
| # Match if the JSON has a key "a" with value 1, and a key "b" with any value. |
| # The callback will be called with a JsonMatch object with attribute `a` set to 1 and |
| # `b` set to the value of "b". |
| j.match({"a": 1, "b": Any}, callback=lambda m: print(m.b)) # prints 2 |
| # Match if the JSON has a key "a" with value 1, and a key "c" which |
| # may be missing or have any value. |
| j.match({"a": 1, "c": Maybe}, callback=lambda m: print(m.c)) # prints None |
| # Match if the JSON has a key "a" with value 1, and no key "b". |
| j.match({"a": 1, "b": None}, no_match=lambda: print("no match")) # prints "no match" |
| j = JsonGet('[1, 2, 3]') |
| # Match if the JSON is a list. |
| j.match([Any], callback=lambda m: print(m)) # prints [1, 2, 3] |
| j = JsonGet('[{"t": "a", "n": 1}, {"t": "a", "n": 2}, {"t": "b", "n": {"x": 5}}]) |
| # Fetch the type "a" entries |
| j.match([{"t": "a", "n": Any}], lambda m: print([e.n for e in m])) # prints [1, 2] |
| # Recurse into dictionary |
| j.match([{"n": {"x": Any}}], lambda m: print(len(m), m[0].b.x)) # prints 1, 5 |
| # No need to match all the way down |
| j.match([{"t": "b", "n": Any}], lambda m: print(m[0].n)) # prints {"x": 5} |
| """ |
| m = self._match_toplevel(pattern, self.j) |
| if m and callback: |
| callback(m) |
| if not m and no_match: |
| no_match() |
| return m |
| |
| def _match_toplevel(self, pattern: Any, data: Any) -> Any: |
| if isinstance(pattern, dict) and isinstance(data, dict): |
| return self._match_dict(pattern, data) |
| elif isinstance(pattern, list) and isinstance(data, Iterable): |
| assert len(pattern) == 1 |
| return self._match_list(pattern[0], data) |
| elif pattern is Any or pattern is Maybe or pattern == data: |
| return data |
| else: |
| return None |
| |
| # Returns a list of items of data that match pattern. |
| def _match_list(self, pattern: Any, data: Iterable[Any]) -> Iterable[Any]: |
| return list( |
| filter(bool, [self._match_toplevel(pattern, i) for i in data]) |
| ) |
| |
| # `pattern` is a dict, whose keys are matched to the root dict of `tree`. |
| # Values of `pattern`: |
| # - `Any` matches any value and fails if the key is missing. |
| # - `Maybe` matches any value, or a missing key, in which case the key will be |
| # added to the JsonMatch with a value of None. |
| # - `None` fails the match if the key is in the root dict. |
| # - any other value must match the value fetched from data by the key. |
| # If match fails, returns None. |
| # If match succeeds, returns a JsonMatch with fields corresponding to the filled-in pattern. |
| def _match_dict( |
| self, pattern: dict[str, Any], tree: dict[str, Any] |
| ) -> Any: # -> JsonMatch | None: |
| if not isinstance(tree, dict): |
| return None |
| o = JsonMatch() |
| for k, v in pattern.items(): |
| if v is None: |
| if k in tree: |
| return None |
| elif v is Maybe: |
| if k in tree: |
| matched = tree[k] |
| else: |
| matched = None |
| elif k not in tree: |
| return None |
| elif v == Any: |
| matched = tree[k] |
| else: |
| matched = self._match_toplevel(v, tree[k]) |
| if matched is None: |
| return None |
| setattr(o, k, matched) |
| return o |