forked from weaselab/weaseljson
Merge pull request 'schemagen: fix nullable root types (#13)' (#29) from weaselbot/weaseljson:weaselbot/issue-13 into main
Reviewed-on: weaselab/weaseljson#29
This commit is contained in:
@@ -46,6 +46,49 @@ target_link_libraries(schemagen_big PRIVATE ${PROJECT_NAME})
|
|||||||
target_compile_options(schemagen_big PRIVATE -Wno-switch-enum)
|
target_compile_options(schemagen_big PRIVATE -Wno-switch-enum)
|
||||||
add_dependencies(schemagen_big schemagen_big_h)
|
add_dependencies(schemagen_big schemagen_big_h)
|
||||||
|
|
||||||
|
set(NULLABLE_OBJECT_SCHEMA
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/nullable_object.schema.json)
|
||||||
|
set(NULLABLE_STRING_SCHEMA
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/nullable_string.schema.json)
|
||||||
|
set(NULLABLE_ARRAY_SCHEMA
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/nullable_array.schema.json)
|
||||||
|
set(NULLABLE_OBJECT_H ${CMAKE_CURRENT_BINARY_DIR}/nullable_object.h)
|
||||||
|
set(NULLABLE_STRING_H ${CMAKE_CURRENT_BINARY_DIR}/nullable_string.h)
|
||||||
|
set(NULLABLE_ARRAY_H ${CMAKE_CURRENT_BINARY_DIR}/nullable_array.h)
|
||||||
|
|
||||||
|
add_custom_command(
|
||||||
|
OUTPUT ${NULLABLE_OBJECT_H}
|
||||||
|
COMMAND ${Python3_EXECUTABLE} ${SCHEMAGEN_SCRIPT} ${NULLABLE_OBJECT_SCHEMA} -o
|
||||||
|
${NULLABLE_OBJECT_H} --namespace nullable_object
|
||||||
|
DEPENDS ${SCHEMAGEN_SCRIPT} ${NULLABLE_OBJECT_SCHEMA}
|
||||||
|
COMMENT "Generating nullable_object.h")
|
||||||
|
|
||||||
|
add_custom_command(
|
||||||
|
OUTPUT ${NULLABLE_STRING_H}
|
||||||
|
COMMAND ${Python3_EXECUTABLE} ${SCHEMAGEN_SCRIPT} ${NULLABLE_STRING_SCHEMA} -o
|
||||||
|
${NULLABLE_STRING_H} --namespace nullable_string
|
||||||
|
DEPENDS ${SCHEMAGEN_SCRIPT} ${NULLABLE_STRING_SCHEMA}
|
||||||
|
COMMENT "Generating nullable_string.h")
|
||||||
|
|
||||||
|
add_custom_command(
|
||||||
|
OUTPUT ${NULLABLE_ARRAY_H}
|
||||||
|
COMMAND ${Python3_EXECUTABLE} ${SCHEMAGEN_SCRIPT} ${NULLABLE_ARRAY_SCHEMA} -o
|
||||||
|
${NULLABLE_ARRAY_H} --namespace nullable_array
|
||||||
|
DEPENDS ${SCHEMAGEN_SCRIPT} ${NULLABLE_ARRAY_SCHEMA}
|
||||||
|
COMMENT "Generating nullable_array.h")
|
||||||
|
|
||||||
|
add_custom_target(
|
||||||
|
schemagen_nullable_h DEPENDS ${NULLABLE_OBJECT_H} ${NULLABLE_STRING_H}
|
||||||
|
${NULLABLE_ARRAY_H})
|
||||||
|
|
||||||
|
add_executable(schemagen_nullable_root
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/test_nullable_root.cpp)
|
||||||
|
target_include_directories(schemagen_nullable_root
|
||||||
|
PRIVATE include ${CMAKE_CURRENT_BINARY_DIR})
|
||||||
|
target_link_libraries(schemagen_nullable_root PRIVATE ${PROJECT_NAME})
|
||||||
|
target_compile_options(schemagen_nullable_root PRIVATE -Wno-switch-enum)
|
||||||
|
add_dependencies(schemagen_nullable_root schemagen_nullable_h)
|
||||||
|
|
||||||
add_test(
|
add_test(
|
||||||
NAME schemagen_example
|
NAME schemagen_example
|
||||||
COMMAND schemagen_example
|
COMMAND schemagen_example
|
||||||
@@ -55,3 +98,8 @@ add_test(
|
|||||||
NAME schemagen_big
|
NAME schemagen_big
|
||||||
COMMAND schemagen_big
|
COMMAND schemagen_big
|
||||||
WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR})
|
WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR})
|
||||||
|
|
||||||
|
add_test(
|
||||||
|
NAME schemagen_nullable_root
|
||||||
|
COMMAND schemagen_nullable_root
|
||||||
|
WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR})
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"type": [
|
||||||
|
"array",
|
||||||
|
"null"
|
||||||
|
],
|
||||||
|
"items": {
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"type": [
|
||||||
|
"object",
|
||||||
|
"null"
|
||||||
|
],
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": [
|
||||||
|
"x"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"x": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"type": [
|
||||||
|
"string",
|
||||||
|
"null"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,158 @@
|
|||||||
|
// Regression test for issue #13: nullable root types.
|
||||||
|
#include <cassert>
|
||||||
|
#include <cstdio>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include "nullable_array.h"
|
||||||
|
#include "nullable_object.h"
|
||||||
|
#include "nullable_string.h"
|
||||||
|
|
||||||
|
static int failures = 0;
|
||||||
|
#define CHECK(cond) \
|
||||||
|
do { \
|
||||||
|
if (!(cond)) { \
|
||||||
|
printf("FAIL %s:%d: %s\n", __FILE__, __LINE__, #cond); \
|
||||||
|
++failures; \
|
||||||
|
} \
|
||||||
|
} while (0)
|
||||||
|
|
||||||
|
static WeaselJsonStatus parseStrided(nullable_object::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 WeaselJsonStatus parseStrided(nullable_string::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 WeaselJsonStatus parseStrided(nullable_array::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 void expectReject(nullable_object::RootBuilder &b, std::string in,
|
||||||
|
const char *what) {
|
||||||
|
WeaselJsonStatus s = parseStrided(b, in);
|
||||||
|
if (s == WeaselJson_REJECT) {
|
||||||
|
printf("ok reject: %s\n", what);
|
||||||
|
} else {
|
||||||
|
printf("FAIL expected reject (%s) got status %d for: %s\n", what, s,
|
||||||
|
in.c_str());
|
||||||
|
++failures;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
// ---- nullable root object: valid document ----
|
||||||
|
{
|
||||||
|
nullable_object::RootBuilder b;
|
||||||
|
WeaselJsonStatus s = parseStrided(b, R"({"x":"hello"})");
|
||||||
|
CHECK(s == WeaselJson_OK);
|
||||||
|
if (s == WeaselJson_OK) {
|
||||||
|
nullable_object::Root r = b.take();
|
||||||
|
CHECK(r.has_value());
|
||||||
|
CHECK(r->x == "hello");
|
||||||
|
printf("ok nullable root object accepts object\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- nullable root object: null document ----
|
||||||
|
{
|
||||||
|
nullable_object::RootBuilder b;
|
||||||
|
WeaselJsonStatus s = parseStrided(b, "null");
|
||||||
|
CHECK(s == WeaselJson_OK);
|
||||||
|
if (s == WeaselJson_OK) {
|
||||||
|
nullable_object::Root r = b.take();
|
||||||
|
CHECK(!r.has_value());
|
||||||
|
printf("ok nullable root object accepts null\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- nullable root object: schema checks still run ----
|
||||||
|
{
|
||||||
|
nullable_object::RootBuilder b;
|
||||||
|
expectReject(b, R"({"x":"hello","extra":1})",
|
||||||
|
"unknown key in strict nullable root object");
|
||||||
|
}
|
||||||
|
{
|
||||||
|
nullable_object::RootBuilder b;
|
||||||
|
expectReject(b, R"({})", "missing required field in nullable root object");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- nullable root string: valid value ----
|
||||||
|
{
|
||||||
|
nullable_string::RootBuilder b;
|
||||||
|
WeaselJsonStatus s = parseStrided(b, R"("hello")");
|
||||||
|
CHECK(s == WeaselJson_OK);
|
||||||
|
if (s == WeaselJson_OK) {
|
||||||
|
nullable_string::Root r = b.take();
|
||||||
|
CHECK(r.has_value());
|
||||||
|
CHECK(*r == "hello");
|
||||||
|
printf("ok nullable root string accepts string\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- nullable root string: null value ----
|
||||||
|
{
|
||||||
|
nullable_string::RootBuilder b;
|
||||||
|
WeaselJsonStatus s = parseStrided(b, "null");
|
||||||
|
CHECK(s == WeaselJson_OK);
|
||||||
|
if (s == WeaselJson_OK) {
|
||||||
|
nullable_string::Root r = b.take();
|
||||||
|
CHECK(!r.has_value());
|
||||||
|
printf("ok nullable root string accepts null\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- nullable root array: valid value ----
|
||||||
|
{
|
||||||
|
nullable_array::RootBuilder b;
|
||||||
|
WeaselJsonStatus s = parseStrided(b, "[1,2,3]");
|
||||||
|
CHECK(s == WeaselJson_OK);
|
||||||
|
if (s == WeaselJson_OK) {
|
||||||
|
nullable_array::Root r = b.take();
|
||||||
|
CHECK(r.has_value());
|
||||||
|
CHECK(r->size() == 3);
|
||||||
|
CHECK((*r)[0] == 1 && (*r)[1] == 2 && (*r)[2] == 3);
|
||||||
|
printf("ok nullable root array accepts array\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- nullable root array: null value ----
|
||||||
|
{
|
||||||
|
nullable_array::RootBuilder b;
|
||||||
|
WeaselJsonStatus s = parseStrided(b, "null");
|
||||||
|
CHECK(s == WeaselJson_OK);
|
||||||
|
if (s == WeaselJson_OK) {
|
||||||
|
nullable_array::Root r = b.take();
|
||||||
|
CHECK(!r.has_value());
|
||||||
|
printf("ok nullable root array accepts null\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (failures == 0) {
|
||||||
|
printf("\nALL TESTS PASSED\n");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
printf("\n%d FAILURE(S)\n", failures);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
@@ -493,6 +493,18 @@ class Emitter:
|
|||||||
self.root_ty, self.root_nullable = Builder._unpack(
|
self.root_ty, self.root_nullable = Builder._unpack(
|
||||||
self.b.build_type(self.b.root_schema, "Root")
|
self.b.build_type(self.b.root_schema, "Root")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# A nullable root object would otherwise produce
|
||||||
|
# using Root = std::optional<Root>;
|
||||||
|
# which conflicts with the struct named Root. Rename the inner struct.
|
||||||
|
if isinstance(self.root_ty, TObj) and self.root_nullable:
|
||||||
|
old_name = self.root_ty.name
|
||||||
|
new_name = self.b.unique_name("RootInner")
|
||||||
|
obj = self.b.objects.pop(old_name)
|
||||||
|
obj.name = new_name
|
||||||
|
self.b.objects[new_name] = obj
|
||||||
|
self.root_ty = TObj(new_name)
|
||||||
|
|
||||||
break_cycles(self.b.objects)
|
break_cycles(self.b.objects)
|
||||||
|
|
||||||
# register all array kinds (walk every field + root)
|
# register all array kinds (walk every field + root)
|
||||||
@@ -582,9 +594,7 @@ namespace {ns} {{"""
|
|||||||
def _kind_enum(self):
|
def _kind_enum(self):
|
||||||
kinds = list(self.b.objects.keys())
|
kinds = list(self.b.objects.keys())
|
||||||
kinds += [n for n, _ in self.arr_types]
|
kinds += [n for n, _ in self.arr_types]
|
||||||
root_is_container = (
|
root_is_container = isinstance(self.root_ty, (TObj, TArr))
|
||||||
isinstance(self.root_ty, (TObj, TArr)) and not self.root_nullable
|
|
||||||
)
|
|
||||||
if not root_is_container:
|
if not root_is_container:
|
||||||
kinds.append("RootScalar")
|
kinds.append("RootScalar")
|
||||||
kinds.append("Skip")
|
kinds.append("Skip")
|
||||||
@@ -593,9 +603,9 @@ namespace {ns} {{"""
|
|||||||
|
|
||||||
def _root_info(self):
|
def _root_info(self):
|
||||||
"""Return (root_cat, root_container_kind_or_None)."""
|
"""Return (root_cat, root_container_kind_or_None)."""
|
||||||
if isinstance(self.root_ty, TObj) and not self.root_nullable:
|
if isinstance(self.root_ty, TObj):
|
||||||
return ("Obj", f"Kind::{self.root_ty.name}")
|
return ("Obj", f"Kind::{self.root_ty.name}")
|
||||||
if isinstance(self.root_ty, TArr) and not self.root_nullable:
|
if isinstance(self.root_ty, TArr):
|
||||||
return ("Arr", f"Kind::{self.arr_kind(self.root_ty)}")
|
return ("Arr", f"Kind::{self.arr_kind(self.root_ty)}")
|
||||||
return (self.cat(self.root_ty), None)
|
return (self.cat(self.root_ty), None)
|
||||||
|
|
||||||
@@ -665,6 +675,11 @@ namespace {ns} {{"""
|
|||||||
lines.append(" }")
|
lines.append(" }")
|
||||||
root_cat, root_kind = self._root_info()
|
root_cat, root_kind = self._root_info()
|
||||||
if root_kind is None:
|
if root_kind is None:
|
||||||
|
if self.root_nullable:
|
||||||
|
lines.append(
|
||||||
|
" case Kind::RootScalar: { if (!result_) result_.emplace(); return &*result_; }"
|
||||||
|
)
|
||||||
|
else:
|
||||||
lines.append(" case Kind::RootScalar: return &result_;")
|
lines.append(" case Kind::RootScalar: return &result_;")
|
||||||
lines.append(" default: return nullptr;")
|
lines.append(" default: return nullptr;")
|
||||||
lines.append(" }")
|
lines.append(" }")
|
||||||
@@ -807,7 +822,12 @@ namespace {ns} {{"""
|
|||||||
lines = [" if (error_) return;"]
|
lines = [" if (error_) return;"]
|
||||||
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_}});")
|
if self.root_nullable:
|
||||||
|
lines.append(" result_.emplace();")
|
||||||
|
dest = "&*result_"
|
||||||
|
else:
|
||||||
|
dest = "&result_"
|
||||||
|
lines.append(f" stack_.push_back(Frame{{{root_kind}, {dest}}});")
|
||||||
if event_cat == "Obj":
|
if event_cat == "Obj":
|
||||||
lines.append(" {")
|
lines.append(" {")
|
||||||
lines.append(f" int n = fieldCount({root_kind});")
|
lines.append(f" int n = fieldCount({root_kind});")
|
||||||
@@ -839,6 +859,9 @@ namespace {ns} {{"""
|
|||||||
root_cat, root_kind = self._root_info()
|
root_cat, root_kind = self._root_info()
|
||||||
begin_obj = self._begin_container("Obj", root_kind)
|
begin_obj = self._begin_container("Obj", root_kind)
|
||||||
begin_arr = self._begin_container("Arr", root_kind)
|
begin_arr = self._begin_container("Arr", root_kind)
|
||||||
|
null_at_root = (
|
||||||
|
"done_ = true; return;" if self.root_nullable else "reject(); return;"
|
||||||
|
)
|
||||||
|
|
||||||
return f"""
|
return f"""
|
||||||
class RootBuilder {{
|
class RootBuilder {{
|
||||||
@@ -1065,6 +1088,9 @@ private:
|
|||||||
|
|
||||||
void cbNull() {{
|
void cbNull() {{
|
||||||
if (error_) return;
|
if (error_) return;
|
||||||
|
if (stack_.empty()) {{
|
||||||
|
{null_at_root}
|
||||||
|
}}
|
||||||
Frame &f = stack_.back();
|
Frame &f = stack_.back();
|
||||||
if (f.kind == Kind::Skip) return;
|
if (f.kind == Kind::Skip) return;
|
||||||
SlotInfo si = slotInfoG(f);
|
SlotInfo si = slotInfoG(f);
|
||||||
|
|||||||
Reference in New Issue
Block a user