Skip to main content

src/javascript_data_files/decoder.py

1"""
2Pure functions for converting JSON strings to Python values.
4Because I expect some of this JSON to be written by me, and I can
5make copy-paste mistakes, there are a couple of ways it tries
6to catch errors.
7"""
9import json
10import re
11import typing
14def decode_from_js(js_string: str, *, varname: str) -> typing.Any:
15 """
16 Parse a string as a JavaScript value.
17 """
18 # Matches 'const varname = ' or 'var varname = ' at the start
19 # of a string.
20 m = re.compile(r"^(?:const |var )?%s = " % varname)
22 if not m.match(js_string):
23 raise ValueError("Does not start with JavaScript `const` declaration!")
25 json_string = m.sub(repl="", string=js_string).rstrip().rstrip(";")
27 return decode_from_json(json_string)
30def dict_with_unique_names(
31 pairs: list[tuple[str, typing.Any]],
32) -> dict[str, typing.Any]:
33 """
34 Convert a list of name/value pairs to a dict, but only if the
35 names are unique.
37 This is similar to the builtin parser, but it will look for
38 duplicate names and throw a ValueError if they're found; this is
39 a protection against me making a copy/paste error in my JavaScript.
40 """
41 # First try to parse the object as a dictionary; if it's the same
42 # length as the pairs, then we know all the keys were unique and
43 # we can return.
44 pairs_as_dict = dict(pairs)
46 if len(pairs_as_dict) == len(pairs):
47 return pairs_as_dict
49 # Otherwise, let's work out what the duplicate name(s) were, so we
50 # can throw an appropriate error message for the user.
51 import collections
53 name_tally = collections.Counter(k for k, _ in pairs)
55 duplicate_names = [k for k, count in name_tally.items() if count > 1]
56 assert len(duplicate_names) > 0
58 if len(duplicate_names) == 1:
59 raise ValueError(f"Found duplicate name in JSON object: {duplicate_names[0]}")
60 else:
61 raise ValueError(
62 f"Found duplicate names in JSON object: {', '.join(duplicate_names)}"
63 )
66def decode_from_json(json_string: str) -> typing.Any:
67 """
68 Parse a string as a JSON value.
69 """
70 return json.loads(json_string, object_pairs_hook=dict_with_unique_names)