forked from weaselab/weaseljson
Compare commits
12
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
46ff8e2164 | ||
|
|
8d37b9b602 | ||
|
|
3d7dc97471 | ||
|
|
a26e101191 | ||
|
|
43e3c9904f | ||
|
|
5e18347e35 | ||
|
|
9cd74631b6 | ||
|
|
05185eb3ee | ||
|
|
9839104635 | ||
|
|
b641489a59 | ||
|
|
2ad15708eb | ||
|
|
ca474e3f99 |
@@ -1,2 +1,6 @@
|
|||||||
build
|
build
|
||||||
.cache
|
.cache
|
||||||
|
contrib/schemagen/gen.h
|
||||||
|
contrib/schemagen/big.h
|
||||||
|
contrib/schemagen/test_gen
|
||||||
|
contrib/schemagen/test_big
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ add_test(
|
|||||||
set(SCHEMAGEN_SCRIPT ${CMAKE_CURRENT_SOURCE_DIR}/weaseljson_schemagen.py)
|
set(SCHEMAGEN_SCRIPT ${CMAKE_CURRENT_SOURCE_DIR}/weaseljson_schemagen.py)
|
||||||
set(EXAMPLE_SCHEMA ${CMAKE_CURRENT_SOURCE_DIR}/example.schema.json)
|
set(EXAMPLE_SCHEMA ${CMAKE_CURRENT_SOURCE_DIR}/example.schema.json)
|
||||||
set(GEN_H ${CMAKE_CURRENT_BINARY_DIR}/gen.h)
|
set(GEN_H ${CMAKE_CURRENT_BINARY_DIR}/gen.h)
|
||||||
|
set(BIG_SCHEMA ${CMAKE_CURRENT_SOURCE_DIR}/big.schema.json)
|
||||||
|
set(BIG_H ${CMAKE_CURRENT_BINARY_DIR}/big.h)
|
||||||
|
|
||||||
add_custom_command(
|
add_custom_command(
|
||||||
OUTPUT ${GEN_H}
|
OUTPUT ${GEN_H}
|
||||||
@@ -20,7 +22,15 @@ add_custom_command(
|
|||||||
DEPENDS ${SCHEMAGEN_SCRIPT} ${EXAMPLE_SCHEMA}
|
DEPENDS ${SCHEMAGEN_SCRIPT} ${EXAMPLE_SCHEMA}
|
||||||
COMMENT "Generating gen.h from example.schema.json")
|
COMMENT "Generating gen.h from example.schema.json")
|
||||||
|
|
||||||
|
add_custom_command(
|
||||||
|
OUTPUT ${BIG_H}
|
||||||
|
COMMAND ${Python3_EXECUTABLE} ${SCHEMAGEN_SCRIPT} ${BIG_SCHEMA} -o ${BIG_H}
|
||||||
|
--namespace big_schema
|
||||||
|
DEPENDS ${SCHEMAGEN_SCRIPT} ${BIG_SCHEMA}
|
||||||
|
COMMENT "Generating big.h from big.schema.json")
|
||||||
|
|
||||||
add_custom_target(schemagen_gen_h DEPENDS ${GEN_H})
|
add_custom_target(schemagen_gen_h DEPENDS ${GEN_H})
|
||||||
|
add_custom_target(schemagen_big_h DEPENDS ${BIG_H})
|
||||||
|
|
||||||
add_executable(schemagen_example ${CMAKE_CURRENT_SOURCE_DIR}/test_gen.cpp)
|
add_executable(schemagen_example ${CMAKE_CURRENT_SOURCE_DIR}/test_gen.cpp)
|
||||||
target_include_directories(schemagen_example
|
target_include_directories(schemagen_example
|
||||||
@@ -29,7 +39,19 @@ target_link_libraries(schemagen_example PRIVATE ${PROJECT_NAME})
|
|||||||
target_compile_options(schemagen_example PRIVATE -Wno-switch-enum)
|
target_compile_options(schemagen_example PRIVATE -Wno-switch-enum)
|
||||||
add_dependencies(schemagen_example schemagen_gen_h)
|
add_dependencies(schemagen_example schemagen_gen_h)
|
||||||
|
|
||||||
|
add_executable(schemagen_big ${CMAKE_CURRENT_SOURCE_DIR}/test_big.cpp)
|
||||||
|
target_include_directories(schemagen_big PRIVATE include
|
||||||
|
${CMAKE_CURRENT_BINARY_DIR})
|
||||||
|
target_link_libraries(schemagen_big PRIVATE ${PROJECT_NAME})
|
||||||
|
target_compile_options(schemagen_big PRIVATE -Wno-switch-enum)
|
||||||
|
add_dependencies(schemagen_big schemagen_big_h)
|
||||||
|
|
||||||
add_test(
|
add_test(
|
||||||
NAME schemagen_example
|
NAME schemagen_example
|
||||||
COMMAND schemagen_example
|
COMMAND schemagen_example
|
||||||
WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR})
|
WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR})
|
||||||
|
|
||||||
|
add_test(
|
||||||
|
NAME schemagen_big
|
||||||
|
COMMAND schemagen_big
|
||||||
|
WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR})
|
||||||
|
|||||||
@@ -73,3 +73,28 @@ union `type` lists other than `["T", "null"]`, non-string enums, and remote
|
|||||||
cannot hold partial digits.
|
cannot hold partial digits.
|
||||||
- `test_gen.cpp` generates from `example.schema.json` and exercises the parser
|
- `test_gen.cpp` generates from `example.schema.json` and exercises the parser
|
||||||
byte-by-byte (covering chunked strings/numbers), plus the rejection cases.
|
byte-by-byte (covering chunked strings/numbers), plus the rejection cases.
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
The schemagen tests are registered with CTest and run as part of the default
|
||||||
|
`ctest` invocation from the build directory:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cmake -S . -B build
|
||||||
|
make -C build -j "$(nproc)"
|
||||||
|
cd build
|
||||||
|
ctest --output-on-failure
|
||||||
|
```
|
||||||
|
|
||||||
|
For local development you can also use the convenience script:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cd contrib/schemagen
|
||||||
|
./run_tests.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Both regenerate the example parser (`gen.h`) and a regression parser with 40
|
||||||
|
required properties (`big.h`), compile `test_gen.cpp` and `test_big.cpp`, and run
|
||||||
|
them. `test_big.cpp` specifically covers issue #3: it checks that a 40-property
|
||||||
|
object accepts all fields, rejects a missing field at index 32, and rejects
|
||||||
|
duplicate keys around the 32-bit boundary.
|
||||||
|
|||||||
@@ -0,0 +1,168 @@
|
|||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"p0": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"p1": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"p2": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"p3": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"p4": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"p5": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"p6": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"p7": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"p8": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"p9": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"p10": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"p11": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"p12": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"p13": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"p14": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"p15": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"p16": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"p17": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"p18": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"p19": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"p20": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"p21": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"p22": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"p23": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"p24": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"p25": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"p26": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"p27": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"p28": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"p29": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"p30": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"p31": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"p32": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"p33": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"p34": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"p35": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"p36": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"p37": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"p38": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"p39": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"p0",
|
||||||
|
"p1",
|
||||||
|
"p2",
|
||||||
|
"p3",
|
||||||
|
"p4",
|
||||||
|
"p5",
|
||||||
|
"p6",
|
||||||
|
"p7",
|
||||||
|
"p8",
|
||||||
|
"p9",
|
||||||
|
"p10",
|
||||||
|
"p11",
|
||||||
|
"p12",
|
||||||
|
"p13",
|
||||||
|
"p14",
|
||||||
|
"p15",
|
||||||
|
"p16",
|
||||||
|
"p17",
|
||||||
|
"p18",
|
||||||
|
"p19",
|
||||||
|
"p20",
|
||||||
|
"p21",
|
||||||
|
"p22",
|
||||||
|
"p23",
|
||||||
|
"p24",
|
||||||
|
"p25",
|
||||||
|
"p26",
|
||||||
|
"p27",
|
||||||
|
"p28",
|
||||||
|
"p29",
|
||||||
|
"p30",
|
||||||
|
"p31",
|
||||||
|
"p32",
|
||||||
|
"p33",
|
||||||
|
"p34",
|
||||||
|
"p35",
|
||||||
|
"p36",
|
||||||
|
"p37",
|
||||||
|
"p38",
|
||||||
|
"p39"
|
||||||
|
]
|
||||||
|
}
|
||||||
Executable
+22
@@ -0,0 +1,22 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Build and run the schemagen regression tests.
|
||||||
|
# Expects weaseljson to be built at ../../build (the default CMake build dir).
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
cd "$(dirname "$0")"
|
||||||
|
|
||||||
|
python3 weaseljson_schemagen.py example.schema.json -o gen.h --namespace weasel_schema
|
||||||
|
python3 weaseljson_schemagen.py big.schema.json -o big.h --namespace big_schema
|
||||||
|
|
||||||
|
g++ -std=c++20 -I../../include -I. test_gen.cpp \
|
||||||
|
-L../../build -lweaseljson \
|
||||||
|
-Wl,-rpath,'$ORIGIN'/../../build \
|
||||||
|
-o test_gen
|
||||||
|
|
||||||
|
g++ -std=c++20 -I../../include -I. test_big.cpp \
|
||||||
|
-L../../build -lweaseljson \
|
||||||
|
-Wl,-rpath,'$ORIGIN'/../../build \
|
||||||
|
-o test_big
|
||||||
|
|
||||||
|
./test_gen
|
||||||
|
./test_big
|
||||||
@@ -0,0 +1,140 @@
|
|||||||
|
// Regression test for issue #3: objects with more than 32 properties.
|
||||||
|
#include <cassert>
|
||||||
|
#include <cstdio>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include "big.h"
|
||||||
|
|
||||||
|
using namespace big_schema;
|
||||||
|
|
||||||
|
static WeaselJsonStatus parseStrided(RootBuilder &b, std::string in) {
|
||||||
|
for (size_t i = 0; i < in.size(); ++i) {
|
||||||
|
char c = in[i];
|
||||||
|
WeaselJsonStatus s = b.feed(&c, 1);
|
||||||
|
if (s != WeaselJson_AGAIN)
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
return b.finish();
|
||||||
|
}
|
||||||
|
|
||||||
|
static int failures = 0;
|
||||||
|
#define CHECK(cond) \
|
||||||
|
do { \
|
||||||
|
if (!(cond)) { \
|
||||||
|
printf("FAIL %s:%d: %s\n", __FILE__, __LINE__, #cond); \
|
||||||
|
++failures; \
|
||||||
|
} \
|
||||||
|
} while (0)
|
||||||
|
|
||||||
|
static void expectReject(const std::string &json, const char *what) {
|
||||||
|
RootBuilder b;
|
||||||
|
WeaselJsonStatus s = parseStrided(b, json);
|
||||||
|
if (s == WeaselJson_REJECT) {
|
||||||
|
printf("ok reject: %s\n", what);
|
||||||
|
} else {
|
||||||
|
printf("FAIL expected reject (%s) got status %d for: %s\n", what, s,
|
||||||
|
json.c_str());
|
||||||
|
++failures;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static std::string all40() {
|
||||||
|
std::string json = "{";
|
||||||
|
for (int i = 0; i < 40; ++i) {
|
||||||
|
if (i)
|
||||||
|
json += ",";
|
||||||
|
json += "\"p" + std::to_string(i) + "\":\"v" + std::to_string(i) + "\"";
|
||||||
|
}
|
||||||
|
json += "}";
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
// All 40 required fields present -> accepted, values land in right slots.
|
||||||
|
{
|
||||||
|
RootBuilder b;
|
||||||
|
std::string json = all40();
|
||||||
|
WeaselJsonStatus s = parseStrided(b, json);
|
||||||
|
CHECK(s == WeaselJson_OK);
|
||||||
|
if (s == WeaselJson_OK) {
|
||||||
|
Root r = b.take();
|
||||||
|
CHECK(r.p0 == "v0");
|
||||||
|
CHECK(r.p31 == "v31");
|
||||||
|
CHECK(r.p32 == "v32");
|
||||||
|
CHECK(r.p39 == "v39");
|
||||||
|
printf("ok all 40 required fields accepted\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Missing field p32 (index 32) must be detected, not aliased to bit 0.
|
||||||
|
{
|
||||||
|
std::string json = all40();
|
||||||
|
// Remove the p32 entry: find its substring and erase it.
|
||||||
|
std::string entry = "\"p32\":\"v32\"";
|
||||||
|
size_t pos = json.find(entry);
|
||||||
|
assert(pos != std::string::npos);
|
||||||
|
// Remove the trailing comma before it if present, or the leading comma
|
||||||
|
// after it.
|
||||||
|
if (pos > 0 && json[pos - 1] == ',') {
|
||||||
|
json.erase(pos - 1, entry.size() + 1);
|
||||||
|
} else if (pos + entry.size() < json.size() &&
|
||||||
|
json[pos + entry.size()] == ',') {
|
||||||
|
json.erase(pos, entry.size() + 1);
|
||||||
|
} else {
|
||||||
|
json.erase(pos, entry.size());
|
||||||
|
}
|
||||||
|
expectReject(json, "missing required field p32 (index 32)");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Missing p0 still rejected.
|
||||||
|
{
|
||||||
|
std::string json = all40();
|
||||||
|
std::string entry = "\"p0\":\"v0\"";
|
||||||
|
size_t pos = json.find(entry);
|
||||||
|
assert(pos != std::string::npos);
|
||||||
|
if (json[pos + entry.size()] == ',') {
|
||||||
|
json.erase(pos, entry.size() + 1);
|
||||||
|
} else {
|
||||||
|
json.erase(pos, entry.size());
|
||||||
|
}
|
||||||
|
expectReject(json, "missing required field p0");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Duplicate keys p0 and p32 must both be detected.
|
||||||
|
{
|
||||||
|
std::string json = all40();
|
||||||
|
// Insert p32 again right after the existing p32 entry.
|
||||||
|
std::string entry = "\"p32\":\"v32\"";
|
||||||
|
size_t pos = json.find(entry);
|
||||||
|
assert(pos != std::string::npos);
|
||||||
|
json.insert(pos + entry.size(), ",\"p32\":\"dup\"");
|
||||||
|
expectReject(json, "duplicate key p32 (index 32)");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Duplicate keys p0 and p31 cover boundary bits.
|
||||||
|
{
|
||||||
|
std::string json = all40();
|
||||||
|
std::string entry = "\"p0\":\"v0\"";
|
||||||
|
size_t pos = json.find(entry);
|
||||||
|
assert(pos != std::string::npos);
|
||||||
|
json.insert(pos + entry.size(), ",\"p0\":\"dup\"");
|
||||||
|
expectReject(json, "duplicate key p0");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Duplicate p31/p32 around the 32-bit boundary.
|
||||||
|
{
|
||||||
|
std::string json = all40();
|
||||||
|
std::string entry = "\"p31\":\"v31\"";
|
||||||
|
size_t pos = json.find(entry);
|
||||||
|
assert(pos != std::string::npos);
|
||||||
|
json.insert(pos + entry.size(), ",\"p31\":\"dup\"");
|
||||||
|
expectReject(json, "duplicate key p31");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (failures == 0) {
|
||||||
|
printf("\nALL TESTS PASSED\n");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
printf("\n%d FAILURE(S)\n", failures);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
@@ -3,6 +3,8 @@
|
|||||||
|
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
@@ -49,5 +51,148 @@ class SchemagenEnumTest(unittest.TestCase):
|
|||||||
self.assertIn("enum class Role : int { admin, user, guest };", stdout)
|
self.assertIn("enum class Role : int { admin, user, guest };", stdout)
|
||||||
|
|
||||||
|
|
||||||
|
class SchemagenKeywordTest(unittest.TestCase):
|
||||||
|
def run_schemagen(self, schema, args=None):
|
||||||
|
"""Run schemagen on a schema dict. Returns (returncode, stdout, stderr)."""
|
||||||
|
with tempfile.NamedTemporaryFile("w", suffix=".json", delete=False) as fp:
|
||||||
|
json.dump(schema, fp)
|
||||||
|
schema_path = fp.name
|
||||||
|
try:
|
||||||
|
cmd = [sys.executable, SCRIPT, schema_path]
|
||||||
|
if args:
|
||||||
|
cmd.extend(args)
|
||||||
|
result = subprocess.run(cmd, capture_output=True, text=True, check=False)
|
||||||
|
return result.returncode, result.stdout, result.stderr
|
||||||
|
finally:
|
||||||
|
os.unlink(schema_path)
|
||||||
|
|
||||||
|
def test_cpp20_keywords_are_sanitized(self):
|
||||||
|
"""C++20 keywords used as JSON property names must be suffixed."""
|
||||||
|
keywords = [
|
||||||
|
"concept",
|
||||||
|
"consteval",
|
||||||
|
"constinit",
|
||||||
|
"co_await",
|
||||||
|
"co_return",
|
||||||
|
"co_yield",
|
||||||
|
"requires",
|
||||||
|
"module",
|
||||||
|
"import",
|
||||||
|
]
|
||||||
|
schema = {"type": "object", "properties": {}}
|
||||||
|
for kw in keywords:
|
||||||
|
schema["properties"][kw] = {"type": "string"}
|
||||||
|
rc, stdout, stderr = self.run_schemagen(schema)
|
||||||
|
self.assertEqual(rc, 0, msg=stderr)
|
||||||
|
for kw in keywords:
|
||||||
|
self.assertIn(f"std::optional<std::string> {kw}_;", stdout)
|
||||||
|
self.assertNotIn(f"std::optional<std::string> {kw};", stdout)
|
||||||
|
|
||||||
|
|
||||||
|
class SchemagenCollisionTest(unittest.TestCase):
|
||||||
|
"""Regression tests for issue #21: generated Root alias / Kind enum collisions."""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.repo_root = os.path.dirname(
|
||||||
|
os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
)
|
||||||
|
self.include_dir = os.path.join(self.repo_root, "include")
|
||||||
|
self.compiler = shutil.which("c++")
|
||||||
|
|
||||||
|
def generate_and_compile(self, schema):
|
||||||
|
"""Run schemagen on schema and syntax-check the resulting header."""
|
||||||
|
with tempfile.TemporaryDirectory() as tmpdir:
|
||||||
|
schema_path = os.path.join(tmpdir, "schema.json")
|
||||||
|
with open(schema_path, "w") as fp:
|
||||||
|
json.dump(schema, fp)
|
||||||
|
header_path = os.path.join(tmpdir, "gen.h")
|
||||||
|
cmd = [
|
||||||
|
sys.executable,
|
||||||
|
SCRIPT,
|
||||||
|
schema_path,
|
||||||
|
"-o",
|
||||||
|
header_path,
|
||||||
|
"--namespace",
|
||||||
|
"test_schema",
|
||||||
|
]
|
||||||
|
result = subprocess.run(cmd, capture_output=True, text=True, check=False)
|
||||||
|
self.assertEqual(result.returncode, 0, msg=result.stderr)
|
||||||
|
|
||||||
|
if self.compiler:
|
||||||
|
cpp_path = os.path.join(tmpdir, "test.cpp")
|
||||||
|
with open(cpp_path, "w") as fp:
|
||||||
|
fp.write(
|
||||||
|
'#include "gen.h"\n'
|
||||||
|
"int main() {\n"
|
||||||
|
" test_schema::RootBuilder b;\n"
|
||||||
|
" test_schema::Root r = b.take();\n"
|
||||||
|
" (void)r;\n"
|
||||||
|
"}\n"
|
||||||
|
)
|
||||||
|
comp = subprocess.run(
|
||||||
|
[
|
||||||
|
self.compiler,
|
||||||
|
"-std=c++20",
|
||||||
|
"-fsyntax-only",
|
||||||
|
"-I",
|
||||||
|
self.include_dir,
|
||||||
|
"-I",
|
||||||
|
tmpdir,
|
||||||
|
cpp_path,
|
||||||
|
],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
check=False,
|
||||||
|
)
|
||||||
|
self.assertEqual(comp.returncode, 0, msg=comp.stderr)
|
||||||
|
|
||||||
|
with open(header_path) as fp:
|
||||||
|
return fp.read()
|
||||||
|
|
||||||
|
def test_root_alias_does_not_collide_with_user_type(self):
|
||||||
|
schema = {
|
||||||
|
"type": "array",
|
||||||
|
"items": {"$ref": "#/$defs/Root"},
|
||||||
|
"$defs": {"Root": {"enum": ["a", "b"]}},
|
||||||
|
}
|
||||||
|
out = self.generate_and_compile(schema)
|
||||||
|
self.assertIn("enum class Root1 : int { a, b };", out)
|
||||||
|
self.assertIn("using Root = std::vector<Root1>;", out)
|
||||||
|
self.assertNotIn("using Root = std::vector<Root>;", out)
|
||||||
|
|
||||||
|
def test_kind_enum_does_not_duplicate_arr0(self):
|
||||||
|
schema = {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"arr": {"type": "array", "items": {"type": "string"}},
|
||||||
|
"obj": {"$ref": "#/$defs/Arr0"},
|
||||||
|
},
|
||||||
|
"$defs": {"Arr0": {"type": "object", "properties": {}}},
|
||||||
|
}
|
||||||
|
out = self.generate_and_compile(schema)
|
||||||
|
self.assertIn("struct Arr0", out)
|
||||||
|
m = re.search(r"enum class Kind : uint8_t \{([^}]+)\}", out)
|
||||||
|
self.assertIsNotNone(m)
|
||||||
|
enumerators = [e.strip() for e in m.group(1).split(",")]
|
||||||
|
self.assertIn("Arr0", enumerators)
|
||||||
|
self.assertIn("Arr1", enumerators)
|
||||||
|
self.assertEqual(len(enumerators), len(set(enumerators)))
|
||||||
|
|
||||||
|
def test_skip_is_reserved(self):
|
||||||
|
schema = {
|
||||||
|
"type": "array",
|
||||||
|
"items": {"$ref": "#/$defs/Skip"},
|
||||||
|
"$defs": {"Skip": {"type": "object", "properties": {}}},
|
||||||
|
}
|
||||||
|
out = self.generate_and_compile(schema)
|
||||||
|
self.assertIn("struct Skip1", out)
|
||||||
|
m = re.search(r"enum class Kind : uint8_t \{([^}]+)\}", out)
|
||||||
|
self.assertIsNotNone(m)
|
||||||
|
enumerators = [e.strip() for e in m.group(1).split(",")]
|
||||||
|
self.assertIn("Skip", enumerators)
|
||||||
|
self.assertIn("Skip1", enumerators)
|
||||||
|
self.assertEqual(len(enumerators), len(set(enumerators)))
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
@@ -133,9 +133,15 @@ _CPP_KEYWORDS = {
|
|||||||
"catch",
|
"catch",
|
||||||
"char",
|
"char",
|
||||||
"class",
|
"class",
|
||||||
|
"concept",
|
||||||
"const",
|
"const",
|
||||||
|
"consteval",
|
||||||
|
"constinit",
|
||||||
"constexpr",
|
"constexpr",
|
||||||
"continue",
|
"continue",
|
||||||
|
"co_await",
|
||||||
|
"co_return",
|
||||||
|
"co_yield",
|
||||||
"decltype",
|
"decltype",
|
||||||
"default",
|
"default",
|
||||||
"delete",
|
"delete",
|
||||||
@@ -152,9 +158,11 @@ _CPP_KEYWORDS = {
|
|||||||
"friend",
|
"friend",
|
||||||
"goto",
|
"goto",
|
||||||
"if",
|
"if",
|
||||||
|
"import",
|
||||||
"inline",
|
"inline",
|
||||||
"int",
|
"int",
|
||||||
"long",
|
"long",
|
||||||
|
"module",
|
||||||
"namespace",
|
"namespace",
|
||||||
"new",
|
"new",
|
||||||
"not",
|
"not",
|
||||||
@@ -166,6 +174,7 @@ _CPP_KEYWORDS = {
|
|||||||
"public",
|
"public",
|
||||||
"register",
|
"register",
|
||||||
"return",
|
"return",
|
||||||
|
"requires",
|
||||||
"short",
|
"short",
|
||||||
"signed",
|
"signed",
|
||||||
"sizeof",
|
"sizeof",
|
||||||
@@ -207,12 +216,26 @@ class Builder:
|
|||||||
base = camel(hint)
|
base = camel(hint)
|
||||||
name = base
|
name = base
|
||||||
i = 1
|
i = 1
|
||||||
while name in self._used_names:
|
while self._name_taken(name):
|
||||||
|
candidate = f"{base}{i}"
|
||||||
|
# A numeric suffix can itself land on a reserved generated name
|
||||||
|
# (e.g. hint "Arr0" -> "Arr01"). Use an underscore separator so
|
||||||
|
# we never loop through the reserved block.
|
||||||
|
if self._is_reserved(candidate):
|
||||||
|
candidate = f"{base}_{i}"
|
||||||
|
name = candidate
|
||||||
i += 1
|
i += 1
|
||||||
name = f"{base}{i}"
|
|
||||||
self._used_names.add(name)
|
self._used_names.add(name)
|
||||||
return name
|
return name
|
||||||
|
|
||||||
|
def _name_taken(self, name):
|
||||||
|
return name in self._used_names or self._is_reserved(name)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _is_reserved(name):
|
||||||
|
"""Names generated internally that must not collide with user types."""
|
||||||
|
return name in ("Root", "Skip", "RootScalar")
|
||||||
|
|
||||||
def ref_name(self, ref):
|
def ref_name(self, ref):
|
||||||
if not ref.startswith("#/"):
|
if not ref.startswith("#/"):
|
||||||
raise GenError(f"only local $ref supported, got: {ref}")
|
raise GenError(f"only local $ref supported, got: {ref}")
|
||||||
@@ -426,6 +449,7 @@ class Emitter:
|
|||||||
self.kind_order = [] # all Kind enumerators in declaration order
|
self.kind_order = [] # all Kind enumerators in declaration order
|
||||||
self.root_ty = None
|
self.root_ty = None
|
||||||
self.root_nullable = False
|
self.root_nullable = False
|
||||||
|
self._arr_counter = 0
|
||||||
|
|
||||||
# -- type strings -------------------------------------------------------
|
# -- type strings -------------------------------------------------------
|
||||||
def base_cpp(self, ty):
|
def base_cpp(self, ty):
|
||||||
@@ -456,11 +480,19 @@ class Emitter:
|
|||||||
def arr_kind(self, tarr):
|
def arr_kind(self, tarr):
|
||||||
sig = self.base_cpp(tarr)
|
sig = self.base_cpp(tarr)
|
||||||
if sig not in self.arr_kinds:
|
if sig not in self.arr_kinds:
|
||||||
name = f"Arr{len(self.arr_kinds)}"
|
name = self._fresh_arr_kind_name()
|
||||||
self.arr_kinds[sig] = name
|
self.arr_kinds[sig] = name
|
||||||
self.arr_types.append((name, tarr))
|
self.arr_types.append((name, tarr))
|
||||||
|
self.b._used_names.add(name)
|
||||||
return self.arr_kinds[sig]
|
return self.arr_kinds[sig]
|
||||||
|
|
||||||
|
def _fresh_arr_kind_name(self):
|
||||||
|
while True:
|
||||||
|
name = f"Arr{self._arr_counter}"
|
||||||
|
self._arr_counter += 1
|
||||||
|
if not self.b._name_taken(name):
|
||||||
|
return name
|
||||||
|
|
||||||
def cat(self, ty):
|
def cat(self, ty):
|
||||||
if isinstance(ty, TScalar):
|
if isinstance(ty, TScalar):
|
||||||
return {"str": "Str", "int": "Int", "dbl": "Dbl", "bool": "Bool"}[ty.kind]
|
return {"str": "Str", "int": "Int", "dbl": "Dbl", "bool": "Bool"}[ty.kind]
|
||||||
@@ -690,19 +722,35 @@ namespace {ns} {{"""
|
|||||||
lines.append(" }")
|
lines.append(" }")
|
||||||
return "\n".join(lines)
|
return "\n".join(lines)
|
||||||
|
|
||||||
def _bitmask(self, obj, pred):
|
def _fieldcount(self):
|
||||||
mask = 0
|
lines = [" int fieldCount(Kind k) const {", " switch (k) {"]
|
||||||
for i, fld in enumerate(obj.fields):
|
for name, obj in self.b.objects.items():
|
||||||
if pred(fld):
|
lines.append(f" case Kind::{name}: return {len(obj.fields)};")
|
||||||
mask |= 1 << i
|
lines.append(" default: return 0;")
|
||||||
return mask
|
lines.append(" }")
|
||||||
|
lines.append(" }")
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
def _reqmask(self):
|
def _reqmask(self):
|
||||||
lines = [" uint32_t requiredMask(Kind k) const {", " switch (k) {"]
|
lines = [
|
||||||
|
" const std::vector<uint64_t> &requiredMask(Kind k) const {",
|
||||||
|
" switch (k) {",
|
||||||
|
]
|
||||||
for name, obj in self.b.objects.items():
|
for name, obj in self.b.objects.items():
|
||||||
m = self._bitmask(obj, lambda f: f.required)
|
words = (len(obj.fields) + 63) // 64
|
||||||
lines.append(f" case Kind::{name}: return {hex(m)}u;")
|
req = [0] * words
|
||||||
lines.append(" default: return 0u;")
|
for i, fld in enumerate(obj.fields):
|
||||||
|
if fld.required:
|
||||||
|
req[i >> 6] |= 1 << (i & 63)
|
||||||
|
init = ", ".join(f"{hex(w)}u" for w in req) if words else ""
|
||||||
|
lines.append(f" case Kind::{name}: {{")
|
||||||
|
lines.append(f" static const std::vector<uint64_t> m = {{ {init} }};")
|
||||||
|
lines.append(" return m;")
|
||||||
|
lines.append(" }")
|
||||||
|
lines.append(" default: {")
|
||||||
|
lines.append(" static const std::vector<uint64_t> empty;")
|
||||||
|
lines.append(" return empty;")
|
||||||
|
lines.append(" }")
|
||||||
lines.append(" }")
|
lines.append(" }")
|
||||||
lines.append(" }")
|
lines.append(" }")
|
||||||
return "\n".join(lines)
|
return "\n".join(lines)
|
||||||
@@ -783,6 +831,11 @@ namespace {ns} {{"""
|
|||||||
if root_kind is not None and root_cat == event_cat:
|
if root_kind is not None and root_cat == event_cat:
|
||||||
lines.append(" if (stack_.empty()) {")
|
lines.append(" if (stack_.empty()) {")
|
||||||
lines.append(f" stack_.push_back(Frame{{{root_kind}, &result_}});")
|
lines.append(f" stack_.push_back(Frame{{{root_kind}, &result_}});")
|
||||||
|
if event_cat == "Obj":
|
||||||
|
lines.append(" {")
|
||||||
|
lines.append(f" int n = fieldCount({root_kind});")
|
||||||
|
lines.append(" stack_.back().seen.assign((n + 63) / 64, 0);")
|
||||||
|
lines.append(" }")
|
||||||
lines.append(" return;")
|
lines.append(" return;")
|
||||||
lines.append(" }")
|
lines.append(" }")
|
||||||
else:
|
else:
|
||||||
@@ -798,6 +851,10 @@ namespace {ns} {{"""
|
|||||||
lines.append(f" if (si.cat != Cat::{event_cat}) {{ reject(); return; }}")
|
lines.append(f" if (si.cat != Cat::{event_cat}) {{ reject(); return; }}")
|
||||||
lines.append(" void *p = engage(f, true);")
|
lines.append(" void *p = engage(f, true);")
|
||||||
lines.append(" stack_.push_back(Frame{si.child, p});")
|
lines.append(" stack_.push_back(Frame{si.child, p});")
|
||||||
|
lines.append(" if (isObjectKind(si.child)) {")
|
||||||
|
lines.append(" int n = fieldCount(si.child);")
|
||||||
|
lines.append(" stack_.back().seen.assign((n + 63) / 64, 0);")
|
||||||
|
lines.append(" }")
|
||||||
return "\n".join(lines)
|
return "\n".join(lines)
|
||||||
|
|
||||||
def _builder(self, order):
|
def _builder(self, order):
|
||||||
@@ -850,7 +907,7 @@ private:
|
|||||||
Kind kind;
|
Kind kind;
|
||||||
void *dest;
|
void *dest;
|
||||||
int field = -1; // object: selected field (-1 want key, -2 skip)
|
int field = -1; // object: selected field (-1 want key, -2 skip)
|
||||||
uint32_t seen = 0; // bitmask of populated fields
|
std::vector<uint64_t> seen; // populated field bitset (object frames)
|
||||||
}};
|
}};
|
||||||
static constexpr int kWantKey = -1;
|
static constexpr int kWantKey = -1;
|
||||||
static constexpr int kSkip = -2;
|
static constexpr int kSkip = -2;
|
||||||
@@ -885,7 +942,7 @@ private:
|
|||||||
scratch_.clear();
|
scratch_.clear();
|
||||||
{(" if (p.kind == Kind::RootScalar) { stack_.pop_back(); done_ = true; return; }" if root_kind is None else "")}
|
{(" if (p.kind == Kind::RootScalar) { stack_.pop_back(); done_ = true; return; }" if root_kind is None else "")}
|
||||||
if (isObjectKind(p.kind)) {{
|
if (isObjectKind(p.kind)) {{
|
||||||
if (p.field >= 0) p.seen |= (1u << p.field);
|
if (p.field >= 0) p.seen[p.field >> 6] |= (1ull << (p.field & 63));
|
||||||
p.field = kWantKey;
|
p.field = kWantKey;
|
||||||
}}
|
}}
|
||||||
}}
|
}}
|
||||||
@@ -895,16 +952,19 @@ private:
|
|||||||
}}
|
}}
|
||||||
void cbEndObject() {{
|
void cbEndObject() {{
|
||||||
if (error_) return;
|
if (error_) return;
|
||||||
Frame f = stack_.back();
|
Frame &f = stack_.back();
|
||||||
if (f.kind == Kind::Skip) {{
|
if (f.kind == Kind::Skip) {{
|
||||||
stack_.pop_back();
|
stack_.pop_back();
|
||||||
if (stack_.empty() || stack_.back().kind != Kind::Skip) valueComplete();
|
if (stack_.empty() || stack_.back().kind != Kind::Skip) valueComplete();
|
||||||
return;
|
return;
|
||||||
}}
|
}}
|
||||||
if (isObjectKind(f.kind) &&
|
if (isObjectKind(f.kind)) {{
|
||||||
(f.seen & requiredMask(f.kind)) != requiredMask(f.kind)) {{
|
const auto &req = requiredMask(f.kind);
|
||||||
reject();
|
bool missing = false;
|
||||||
return;
|
for (size_t i = 0; i < req.size(); ++i) {{
|
||||||
|
if ((f.seen[i] & req[i]) != req[i]) {{ missing = true; break; }}
|
||||||
|
}}
|
||||||
|
if (missing) {{ reject(); return; }}
|
||||||
}}
|
}}
|
||||||
stack_.pop_back();
|
stack_.pop_back();
|
||||||
valueComplete();
|
valueComplete();
|
||||||
@@ -937,7 +997,9 @@ private:
|
|||||||
f.field = kSkip;
|
f.field = kSkip;
|
||||||
return;
|
return;
|
||||||
}}
|
}}
|
||||||
if (f.seen & (1u << idx)) {{ reject(); return; }} // duplicate key
|
if (f.seen[idx >> 6] & (1ull << (idx & 63))) {{
|
||||||
|
reject(); return; // duplicate key
|
||||||
|
}}
|
||||||
f.field = idx;
|
f.field = idx;
|
||||||
}}
|
}}
|
||||||
|
|
||||||
@@ -1062,6 +1124,8 @@ private:
|
|||||||
|
|
||||||
{self._matchkey()}
|
{self._matchkey()}
|
||||||
|
|
||||||
|
{self._fieldcount()}
|
||||||
|
|
||||||
{self._reqmask()}
|
{self._reqmask()}
|
||||||
|
|
||||||
{self._is_object_kind()}
|
{self._is_object_kind()}
|
||||||
|
|||||||
@@ -64,7 +64,8 @@ void WeaselJsonParser_destroy(WeaselJsonParser *parser);
|
|||||||
|
|
||||||
/** Incrementally parse `len` more bytes starting at `buf`. `buf` may be
|
/** Incrementally parse `len` more bytes starting at `buf`. `buf` may be
|
||||||
* modified. Call with `len` 0 to indicate end of data. `buf` may be null if
|
* modified. Call with `len` 0 to indicate end of data. `buf` may be null if
|
||||||
* `len` is 0 */
|
* `len` is 0. `len` must not be negative; a negative length is treated as a
|
||||||
|
* rejected input. */
|
||||||
WeaselJsonStatus WeaselJsonParser_parse(WeaselJsonParser *parser, char *buf,
|
WeaselJsonStatus WeaselJsonParser_parse(WeaselJsonParser *parser, char *buf,
|
||||||
int len);
|
int len);
|
||||||
|
|
||||||
|
|||||||
+20
-2
@@ -139,6 +139,7 @@ struct Parser3 {
|
|||||||
stackPtr = stack();
|
stackPtr = stack();
|
||||||
std::ignore = push({N_VALUE, N_WHITESPACE, T_EOF});
|
std::ignore = push({N_VALUE, N_WHITESPACE, T_EOF});
|
||||||
inKey = false;
|
inKey = false;
|
||||||
|
rejected = false;
|
||||||
utf8Codepoint = 0;
|
utf8Codepoint = 0;
|
||||||
utf16Surrogate = 0;
|
utf16Surrogate = 0;
|
||||||
minCodepoint = 0;
|
minCodepoint = 0;
|
||||||
@@ -161,6 +162,7 @@ struct Parser3 {
|
|||||||
NumDfa numDfa;
|
NumDfa numDfa;
|
||||||
Utf8Dfa strDfa;
|
Utf8Dfa strDfa;
|
||||||
bool inKey = false;
|
bool inKey = false;
|
||||||
|
bool rejected = false;
|
||||||
|
|
||||||
#ifndef HAS_MUSTTAIL
|
#ifndef HAS_MUSTTAIL
|
||||||
char *stashBufForTrampoline;
|
char *stashBufForTrampoline;
|
||||||
@@ -1062,18 +1064,34 @@ constexpr inline struct ContinuationTable {
|
|||||||
inline WeaselJsonStatus Parser3::parse(char *buf, int len) {
|
inline WeaselJsonStatus Parser3::parse(char *buf, int len) {
|
||||||
this->dataBegin = this->writeBuf = buf;
|
this->dataBegin = this->writeBuf = buf;
|
||||||
|
|
||||||
|
if (this->rejected) [[unlikely]] {
|
||||||
|
return WeaselJson_REJECT;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (len < 0) [[unlikely]] {
|
||||||
|
this->rejected = true;
|
||||||
|
return WeaselJson_REJECT;
|
||||||
|
}
|
||||||
|
|
||||||
#ifdef HAS_MUSTTAIL
|
#ifdef HAS_MUSTTAIL
|
||||||
// The continuation returns a value in 0..3 here (kBounce is only used by the
|
// The continuation returns a value in 0..3 here (kBounce is only used by the
|
||||||
// no-musttail trampoline below), so the conversion back to the enum is in
|
// no-musttail trampoline below), so the conversion back to the enum is in
|
||||||
// range.
|
// range.
|
||||||
return WeaselJsonStatus(
|
ContinuationStatus status =
|
||||||
symbolTables.continuations[top()](this, buf, buf + len));
|
symbolTables.continuations[top()](this, buf, buf + len);
|
||||||
|
if (status == WeaselJson_REJECT) {
|
||||||
|
this->rejected = true;
|
||||||
|
}
|
||||||
|
return WeaselJsonStatus(status);
|
||||||
#else
|
#else
|
||||||
this->stashBufForTrampoline = buf;
|
this->stashBufForTrampoline = buf;
|
||||||
ContinuationStatus result;
|
ContinuationStatus result;
|
||||||
while ((result = symbolTables.continuations[top()](
|
while ((result = symbolTables.continuations[top()](
|
||||||
this, stashBufForTrampoline, buf + len)) == kBounce)
|
this, stashBufForTrampoline, buf + len)) == kBounce)
|
||||||
;
|
;
|
||||||
|
if (result == WeaselJson_REJECT) {
|
||||||
|
this->rejected = true;
|
||||||
|
}
|
||||||
return WeaselJsonStatus(result);
|
return WeaselJsonStatus(result);
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -202,6 +202,31 @@ TEST_CASE("parser3") {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST_CASE("rejected state is sticky") {
|
||||||
|
auto c = noopCallbacks();
|
||||||
|
auto *parser = WeaselJsonParser_create(1024, &c, nullptr, 0);
|
||||||
|
REQUIRE(parser != nullptr);
|
||||||
|
|
||||||
|
// Feed a valid prefix, then an invalid byte. The parser must reject.
|
||||||
|
std::string chunk1 = "1";
|
||||||
|
REQUIRE(WeaselJsonParser_parse(parser, chunk1.data(), chunk1.size()) ==
|
||||||
|
WeaselJson_AGAIN);
|
||||||
|
std::string chunk2 = "x";
|
||||||
|
REQUIRE(WeaselJsonParser_parse(parser, chunk2.data(), chunk2.size()) ==
|
||||||
|
WeaselJson_REJECT);
|
||||||
|
|
||||||
|
// After a reject, every later call must also reject, including the EOF
|
||||||
|
// finish call that would otherwise return OK.
|
||||||
|
REQUIRE(WeaselJsonParser_parse(parser, nullptr, 0) == WeaselJson_REJECT);
|
||||||
|
|
||||||
|
// Further data chunks must also stay rejected.
|
||||||
|
std::string chunk3 = " ";
|
||||||
|
REQUIRE(WeaselJsonParser_parse(parser, chunk3.data(), chunk3.size()) ==
|
||||||
|
WeaselJson_REJECT);
|
||||||
|
|
||||||
|
WeaselJsonParser_destroy(parser);
|
||||||
|
}
|
||||||
|
|
||||||
TEST_CASE("create rejects too-small stack") {
|
TEST_CASE("create rejects too-small stack") {
|
||||||
auto c = noopCallbacks();
|
auto c = noopCallbacks();
|
||||||
// The parser needs room for the bootstrap symbols pushed by reset(); a stack
|
// The parser needs room for the bootstrap symbols pushed by reset(); a stack
|
||||||
@@ -221,6 +246,20 @@ TEST_CASE("create rejects too-small stack") {
|
|||||||
WeaselJsonParser_destroy(parser);
|
WeaselJsonParser_destroy(parser);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST_CASE("parse rejects negative length") {
|
||||||
|
auto c = noopCallbacks();
|
||||||
|
auto *parser = WeaselJsonParser_create(1024, &c, nullptr, 0);
|
||||||
|
REQUIRE(parser != nullptr);
|
||||||
|
|
||||||
|
// A negative length must not cause pointer arithmetic UB. It should be
|
||||||
|
// rejected, and the rejected state should remain sticky.
|
||||||
|
char buf[10] = "hello";
|
||||||
|
REQUIRE(WeaselJsonParser_parse(parser, buf, -1) == WeaselJson_REJECT);
|
||||||
|
REQUIRE(WeaselJsonParser_parse(parser, nullptr, 0) == WeaselJson_REJECT);
|
||||||
|
|
||||||
|
WeaselJsonParser_destroy(parser);
|
||||||
|
}
|
||||||
|
|
||||||
TEST_CASE("streaming") { testStreaming(json); }
|
TEST_CASE("streaming") { testStreaming(json); }
|
||||||
|
|
||||||
TEST_CASE("reset clears inKey and transient state") {
|
TEST_CASE("reset clears inKey and transient state") {
|
||||||
|
|||||||
@@ -84,7 +84,19 @@ def test_mixed_values():
|
|||||||
assert recorder.events.count("null") == 1
|
assert recorder.events.count("null") == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_create_rejects_too_small_stack():
|
||||||
|
for stack_size in (-1, 0, 1, 2):
|
||||||
|
try:
|
||||||
|
parser = weaseljson.WeaselJsonParser(Recorder(), stackSize=stack_size)
|
||||||
|
except ValueError:
|
||||||
|
continue
|
||||||
|
# If creation unexpectedly succeeds, close it cleanly and fail the test.
|
||||||
|
parser.close()
|
||||||
|
raise AssertionError(f"expected ValueError for stackSize={stack_size}")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
test_object_keys_routed_correctly()
|
test_object_keys_routed_correctly()
|
||||||
test_mixed_values()
|
test_mixed_values()
|
||||||
|
test_create_rejects_too_small_stack()
|
||||||
print("python bindings ok")
|
print("python bindings ok")
|
||||||
|
|||||||
@@ -117,12 +117,24 @@ class WeaselJsonParser:
|
|||||||
self.voidp_callbacks,
|
self.voidp_callbacks,
|
||||||
0,
|
0,
|
||||||
)
|
)
|
||||||
|
if self.p is None:
|
||||||
|
raise ValueError(
|
||||||
|
"WeaselJsonParser_create returned NULL; "
|
||||||
|
"check stackSize (must be positive and large enough) "
|
||||||
|
"and available memory"
|
||||||
|
)
|
||||||
|
|
||||||
|
def _check_open(self):
|
||||||
|
if self.p is None:
|
||||||
|
raise RuntimeError("parser has been closed or creation failed")
|
||||||
|
|
||||||
def parse(self, data: bytes) -> WeaselJsonStatus:
|
def parse(self, data: bytes) -> WeaselJsonStatus:
|
||||||
|
self._check_open()
|
||||||
buf = (ctypes.c_ubyte * len(data)).from_buffer(bytearray(data))
|
buf = (ctypes.c_ubyte * len(data)).from_buffer(bytearray(data))
|
||||||
return self._lib.WeaselJsonParser_parse(self.p, buf, len(data))
|
return self._lib.WeaselJsonParser_parse(self.p, buf, len(data))
|
||||||
|
|
||||||
def reset(self):
|
def reset(self):
|
||||||
|
self._check_open()
|
||||||
self._lib.WeaselJsonParser_reset(self.p)
|
self._lib.WeaselJsonParser_reset(self.p)
|
||||||
|
|
||||||
def __enter__(self):
|
def __enter__(self):
|
||||||
|
|||||||
Reference in New Issue
Block a user