1 Commits
Author SHA1 Message Date
weaselbot 595be0c338 schemagen: deduplicate enum constants that collide after sanitization
Distinct JSON enum values can sanitize to the same C++ identifier
(e.g. "foo-bar" and "foo_bar" both become `foo_bar`), producing an
invalid `enum class` with duplicate constants.

Add `unique_enum_identifiers()` which appends a numeric suffix to
later collisions while preserving enum declaration order, so the
index-to-JSON-value mapping used by the generated parser stays intact.

Also add contrib/schemagen/test_schemagen.py and wire the schemagen
Python tests plus the existing example.schema.json/test_gen.cpp example
into ctest via CMakeLists.txt.

Closes #4
2026-06-18 11:00:32 -04:00
3 changed files with 11 additions and 120 deletions
-7
View File
@@ -163,13 +163,6 @@ target_link_libraries(mytest PRIVATE ${PROJECT_NAME} doctest nanobench simdjson)
target_compile_options(mytest PRIVATE ${TEST_FLAGS}) target_compile_options(mytest PRIVATE ${TEST_FLAGS})
doctest_discover_tests(mytest WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}) doctest_discover_tests(mytest WORKING_DIRECTORY ${CMAKE_SOURCE_DIR})
find_package(Python3 COMPONENTS Interpreter)
if(Python3_Interpreter_FOUND)
add_test(NAME python_bindings
COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/test_python_bindings.py)
endif()
# schemagen tests # schemagen tests
add_test( add_test(
NAME schemagen_python_tests NAME schemagen_python_tests
-90
View File
@@ -1,90 +0,0 @@
import json
import weaseljson
class Recorder(weaseljson.WeaselJsonCallbacksBase):
def __init__(self):
self.keys = []
self.strings = []
self.numbers = []
self.events = []
self._current = bytearray()
def _flush(self, target, data, done):
self._current.extend(data)
if done:
target.append(bytes(self._current))
self._current = bytearray()
def on_begin_object(self):
self.events.append("begin_object")
def on_end_object(self):
self.events.append("end_object")
def on_begin_array(self):
self.events.append("begin_array")
def on_end_array(self):
self.events.append("end_array")
def on_key_data(self, data, done):
self._flush(self.keys, data, done)
def on_string_data(self, data, done):
self._flush(self.strings, data, done)
def on_number_data(self, data, done):
self._flush(self.numbers, data, done)
def on_true_literal(self):
self.events.append("true")
def on_false_literal(self):
self.events.append("false")
def on_null_literal(self):
self.events.append("null")
def parse_all(parser, data):
for i in range(len(data)):
status = parser.parse(data[i : i + 1])
if status != weaseljson.WeaselJsonStatus.AGAIN:
return status
return parser.parse(b"")
def test_object_keys_routed_correctly():
recorder = Recorder()
with weaseljson.WeaselJsonParser(recorder) as parser:
status = parse_all(
parser, json.dumps({"hello": "world", "foo": "bar"}).encode()
)
assert status == weaseljson.WeaselJsonStatus.OK, status
assert recorder.keys == [b"hello", b"foo"], recorder.keys
assert recorder.strings == [b"world", b"bar"], recorder.strings
def test_mixed_values():
recorder = Recorder()
with weaseljson.WeaselJsonParser(recorder) as parser:
status = parse_all(
parser,
json.dumps({"answer": 42, "yes": True, "no": False, "nil": None}).encode(),
)
assert status == weaseljson.WeaselJsonStatus.OK, status
assert recorder.keys == [b"answer", b"yes", b"no", b"nil"], recorder.keys
assert recorder.numbers == [b"42"], recorder.numbers
assert recorder.events.count("true") == 1
assert recorder.events.count("false") == 1
assert recorder.events.count("null") == 1
if __name__ == "__main__":
test_object_keys_routed_correctly()
test_mixed_values()
print("python bindings ok")
+11 -23
View File
@@ -15,7 +15,6 @@ class WeaselJsonCallbacks(ctypes.Structure):
("on_begin_object", event_callback), ("on_begin_object", event_callback),
("on_end_object", event_callback), ("on_end_object", event_callback),
("on_string_data", data_callback), ("on_string_data", data_callback),
("on_key_data", data_callback),
("on_begin_array", event_callback), ("on_begin_array", event_callback),
("on_end_array", event_callback), ("on_end_array", event_callback),
("on_number_data", data_callback), ("on_number_data", data_callback),
@@ -42,9 +41,6 @@ class WeaselJsonCallbacksBase:
def on_string_data(self, data, done): def on_string_data(self, data, done):
pass pass
def on_key_data(self, data, done):
pass
def on_begin_array(self): def on_begin_array(self):
pass pass
@@ -155,12 +151,6 @@ def on_string_data(p, buf, len, done):
self.on_string_data(bytes(ctypes.string_at(buf, len)), bool(done)) self.on_string_data(bytes(ctypes.string_at(buf, len)), bool(done))
@ctypes.CFUNCTYPE(None, ctypes.c_void_p, ctypes.c_void_p, ctypes.c_int, ctypes.c_int)
def on_key_data(p, buf, len, done):
self = ctypes.cast(p, ctypes.POINTER(ctypes.py_object)).contents.value
self.on_key_data(bytes(ctypes.string_at(buf, len)), bool(done))
@ctypes.CFUNCTYPE(None, ctypes.c_void_p) @ctypes.CFUNCTYPE(None, ctypes.c_void_p)
def on_begin_array(p): def on_begin_array(p):
self = ctypes.cast(p, ctypes.POINTER(ctypes.py_object)).contents.value self = ctypes.cast(p, ctypes.POINTER(ctypes.py_object)).contents.value
@@ -201,7 +191,6 @@ c_callbacks = WeaselJsonCallbacks(
on_begin_object, on_begin_object,
on_end_object, on_end_object,
on_string_data, on_string_data,
on_key_data,
on_begin_array, on_begin_array,
on_end_array, on_end_array,
on_number_data, on_number_data,
@@ -217,15 +206,14 @@ class MyCallbacks(WeaselJsonCallbacksBase):
print(data) print(data)
if __name__ == "__main__": with WeaselJsonParser(MyCallbacks()) as parser:
with WeaselJsonParser(MyCallbacks()) as parser: raw = json.dumps({"hello": "world", "foo": 42}).encode()
raw = json.dumps({"hello": "world", "foo": 42}).encode() i = 0
i = 0 stride = 1
stride = 1 while True:
while True: slice = raw[i : i + stride]
slice = raw[i : i + stride] s = parser.parse(slice)
s = parser.parse(slice) if s != WeaselJsonStatus.AGAIN:
if s != WeaselJsonStatus.AGAIN: break
break i += stride
i += stride print(s)
print(s)