beam: add canonical JSON serializer with tests
This commit is contained in:
1 parent
e1b413ae67
commit
3081128e3c
2 files changed
+78
No files matched your search
@@ -0,0 +1,39 @@
|
|||||||
|
"""Canonical JSON serialization (RFC 8785-ish): sorted keys, compact separators,
|
||||||
|
no NaN/Infinity, deterministic float repr, UTF-8, ensure_ascii=False."""
|
||||||
|
from __future__ import annotations
|
||||||
|
import json
|
||||||
|
import math
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
|
||||||
|
class CanonicalError(ValueError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize(obj):
|
||||||
|
if obj is None or isinstance(obj, bool) or isinstance(obj, int) or isinstance(obj, str):
|
||||||
|
return obj
|
||||||
|
if isinstance(obj, float):
|
||||||
|
if not math.isfinite(obj):
|
||||||
|
raise CanonicalError("non-finite float: %r" % obj)
|
||||||
|
return obj
|
||||||
|
if isinstance(obj, (list, tuple)):
|
||||||
|
return [_normalize(x) for x in obj]
|
||||||
|
if isinstance(obj, dict):
|
||||||
|
out = {}
|
||||||
|
for k, v in obj.items():
|
||||||
|
if not isinstance(k, str):
|
||||||
|
raise CanonicalError("non-string key: %r" % (k,))
|
||||||
|
out[k] = _normalize(v)
|
||||||
|
return out
|
||||||
|
raise CanonicalError("unsupported type: %r" % (type(obj),))
|
||||||
|
|
||||||
|
|
||||||
|
def canonical(obj) -> str:
|
||||||
|
n = _normalize(obj)
|
||||||
|
return json.dumps(n, sort_keys=True, separators=(",", ":"), ensure_ascii=False,
|
||||||
|
allow_nan=False)
|
||||||
|
|
||||||
|
|
||||||
|
def canonical_bytes(obj) -> bytes:
|
||||||
|
return canonical(obj).encode("utf-8")
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import sys, os, json
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
from json_canonical import canonical, canonical_bytes, CanonicalError
|
||||||
|
|
||||||
|
def test_basic():
|
||||||
|
assert canonical({"b":2,"a":1}) == '{"a":1,"b":2}'
|
||||||
|
assert canonical([3,1,2]) == '[3,1,2]'
|
||||||
|
assert canonical(True) == 'true'
|
||||||
|
assert canonical(None) == 'null'
|
||||||
|
assert canonical("x") == '"x"'
|
||||||
|
|
||||||
|
def test_nested():
|
||||||
|
assert canonical({"z":{"y":1,"x":2},"a":[1,2]}) == '{"a":[1,2],"z":{"x":2,"y":1}}'
|
||||||
|
|
||||||
|
def test_unicode():
|
||||||
|
assert canonical({"é":1}) == '{"é":1}'
|
||||||
|
|
||||||
|
def test_reject_nan():
|
||||||
|
try:
|
||||||
|
canonical(float('nan'))
|
||||||
|
raise AssertionError("expected error")
|
||||||
|
except CanonicalError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def test_reject_nonstr_key():
|
||||||
|
try:
|
||||||
|
canonical({1:2})
|
||||||
|
raise AssertionError("expected error")
|
||||||
|
except CanonicalError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def test_bytes():
|
||||||
|
assert canonical_bytes({"a":1}) == b'{"a":1}'
|
||||||
|
|
||||||
|
def test_roundtrip():
|
||||||
|
o = {"a":[1,2,3],"b":{"c":True}}
|
||||||
|
assert json.loads(canonical(o)) == o
|
||||||
|
|
||||||
|
print("ALL PASS")
|
||||||
Reference in new issue
Block a user