diff --git a/contrib/schemagen/CMakeLists.txt b/contrib/schemagen/CMakeLists.txt index bb50128..1566c83 100644 --- a/contrib/schemagen/CMakeLists.txt +++ b/contrib/schemagen/CMakeLists.txt @@ -46,6 +46,49 @@ target_link_libraries(schemagen_big PRIVATE ${PROJECT_NAME}) target_compile_options(schemagen_big PRIVATE -Wno-switch-enum) 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( NAME schemagen_example COMMAND schemagen_example @@ -55,3 +98,8 @@ add_test( NAME schemagen_big COMMAND schemagen_big WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}) + +add_test( + NAME schemagen_nullable_root + COMMAND schemagen_nullable_root + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}) diff --git a/contrib/schemagen/nullable_array.schema.json b/contrib/schemagen/nullable_array.schema.json new file mode 100644 index 0000000..a582584 --- /dev/null +++ b/contrib/schemagen/nullable_array.schema.json @@ -0,0 +1,9 @@ +{ + "type": [ + "array", + "null" + ], + "items": { + "type": "integer" + } +} diff --git a/contrib/schemagen/nullable_object.schema.json b/contrib/schemagen/nullable_object.schema.json new file mode 100644 index 0000000..7b94b30 --- /dev/null +++ b/contrib/schemagen/nullable_object.schema.json @@ -0,0 +1,15 @@ +{ + "type": [ + "object", + "null" + ], + "additionalProperties": false, + "required": [ + "x" + ], + "properties": { + "x": { + "type": "string" + } + } +} diff --git a/contrib/schemagen/nullable_string.schema.json b/contrib/schemagen/nullable_string.schema.json new file mode 100644 index 0000000..830edfe --- /dev/null +++ b/contrib/schemagen/nullable_string.schema.json @@ -0,0 +1,6 @@ +{ + "type": [ + "string", + "null" + ] +} diff --git a/contrib/schemagen/test_nullable_root.cpp b/contrib/schemagen/test_nullable_root.cpp new file mode 100644 index 0000000..11a9a88 --- /dev/null +++ b/contrib/schemagen/test_nullable_root.cpp @@ -0,0 +1,158 @@ +// Regression test for issue #13: nullable root types. +#include +#include +#include + +#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; +} diff --git a/contrib/schemagen/weaseljson_schemagen.py b/contrib/schemagen/weaseljson_schemagen.py index 11b268b..93fe43d 100644 --- a/contrib/schemagen/weaseljson_schemagen.py +++ b/contrib/schemagen/weaseljson_schemagen.py @@ -493,6 +493,18 @@ class Emitter: self.root_ty, self.root_nullable = Builder._unpack( self.b.build_type(self.b.root_schema, "Root") ) + + # A nullable root object would otherwise produce + # using Root = std::optional; + # 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) # register all array kinds (walk every field + root) @@ -582,9 +594,7 @@ namespace {ns} {{""" def _kind_enum(self): kinds = list(self.b.objects.keys()) kinds += [n for n, _ in self.arr_types] - root_is_container = ( - isinstance(self.root_ty, (TObj, TArr)) and not self.root_nullable - ) + root_is_container = isinstance(self.root_ty, (TObj, TArr)) if not root_is_container: kinds.append("RootScalar") kinds.append("Skip") @@ -593,9 +603,9 @@ namespace {ns} {{""" def _root_info(self): """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}") - 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 (self.cat(self.root_ty), None) @@ -665,7 +675,12 @@ namespace {ns} {{""" lines.append(" }") root_cat, root_kind = self._root_info() if root_kind is None: - lines.append(" case Kind::RootScalar: return &result_;") + 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(" default: return nullptr;") lines.append(" }") lines.append(" }") @@ -807,7 +822,12 @@ namespace {ns} {{""" lines = [" if (error_) return;"] if root_kind is not None and root_cat == event_cat: 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": lines.append(" {") lines.append(f" int n = fieldCount({root_kind});") @@ -839,6 +859,9 @@ namespace {ns} {{""" root_cat, root_kind = self._root_info() begin_obj = self._begin_container("Obj", 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""" class RootBuilder {{ @@ -1065,6 +1088,9 @@ private: void cbNull() {{ if (error_) return; + if (stack_.empty()) {{ + {null_at_root} + }} Frame &f = stack_.back(); if (f.kind == Kind::Skip) return; SlotInfo si = slotInfoG(f);