From 91302777135f0099ecc6f87f5944db279640e1df Mon Sep 17 00:00:00 2001 From: Bill Date: Tue, 17 Feb 2026 21:37:36 -0700 Subject: [PATCH] Step 634: schema to C++ generator --- editor/CMakeLists.txt | 9 +++ editor/src/SchemaToCppGenerator.h | 116 +++++++++++++++++++++++++++ editor/tests/step634_test.cpp | 128 ++++++++++++++++++++++++++++++ progress.md | 32 ++++++++ 4 files changed, 285 insertions(+) create mode 100644 editor/src/SchemaToCppGenerator.h create mode 100644 editor/tests/step634_test.cpp diff --git a/editor/CMakeLists.txt b/editor/CMakeLists.txt index 59d303b..ede3460 100644 --- a/editor/CMakeLists.txt +++ b/editor/CMakeLists.txt @@ -4585,4 +4585,13 @@ target_link_libraries(step633_test PRIVATE tree_sitter_javascript tree_sitter_typescript tree_sitter_java tree_sitter_rust tree_sitter_go) +add_executable(step634_test tests/step634_test.cpp) +target_include_directories(step634_test PRIVATE src) +target_link_libraries(step634_test PRIVATE + nlohmann_json::nlohmann_json + unofficial::tree-sitter::tree-sitter + tree_sitter_python tree_sitter_cpp tree_sitter_elisp + tree_sitter_javascript tree_sitter_typescript + tree_sitter_java tree_sitter_rust tree_sitter_go) + # Step 12: Dear ImGui shell scaffolding created (main.cpp exists but not built due to dependencies) diff --git a/editor/src/SchemaToCppGenerator.h b/editor/src/SchemaToCppGenerator.h new file mode 100644 index 0000000..9952649 --- /dev/null +++ b/editor/src/SchemaToCppGenerator.h @@ -0,0 +1,116 @@ +#pragma once +// Step 634: Job schema -> typed C++ structs generator + +#include + +#include +#include +#include + +using json = nlohmann::json; + +struct SchemaToCppOutput { + bool success = false; + std::string headerCode; + std::string cmakeInterfaceTarget; + std::vector errors; +}; + +class SchemaToCppGenerator { +public: + static SchemaToCppOutput generate(const json& schema, + const std::string& headerName, + const std::string& targetName) { + SchemaToCppOutput out; + if (!schema.is_object()) { + out.errors.push_back("schema_invalid"); + return out; + } + if (!schema.contains("title") || + !schema["title"].is_string() || + schema["title"].get().empty()) { + out.errors.push_back("schema_title_missing"); + return out; + } + const std::string structName = sanitizeIdentifier( + schema["title"].get()); + if (structName.empty()) { + out.errors.push_back("schema_title_missing"); + return out; + } + + std::string code; + code += "#pragma once\n"; + code += "#include \n"; + code += "#include \n\n"; + code += "struct " + structName + " {\n"; + + std::vector> fields; + if (schema.contains("properties") && schema["properties"].is_object()) { + for (auto it = schema["properties"].begin(); it != schema["properties"].end(); ++it) { + std::string fieldName = sanitizeIdentifier(it.key()); + std::string cppType = mapType(it.value().value("type", "string")); + if (fieldName.empty()) continue; + fields.push_back({fieldName, cppType}); + code += " " + cppType + " " + fieldName + ";\n"; + } + } + if (fields.empty()) { + fields.push_back({"raw", "std::string"}); + code += " std::string raw;\n"; + } + code += "};\n\n"; + + code += "inline void to_json(nlohmann::json& j, const " + structName + "& v) {\n"; + code += " j = nlohmann::json::object();\n"; + for (const auto& field : fields) { + code += " j[\"" + field.first + "\"] = v." + field.first + ";\n"; + } + code += "}\n\n"; + + code += "inline void from_json(const nlohmann::json& j, " + structName + "& v) {\n"; + for (const auto& field : fields) { + code += " if (j.contains(\"" + field.first + "\")) j.at(\"" + field.first + "\").get_to(v." + field.first + ");\n"; + } + code += "}\n\n"; + + code += "inline bool validate" + structName + "(const " + structName + "& v) {\n"; + code += " (void)v;\n"; + code += " return true;\n"; + code += "}\n"; + + out.success = true; + out.headerCode = code; + out.cmakeInterfaceTarget = + "add_library(" + targetName + " INTERFACE)\n" + "target_include_directories(" + targetName + " INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}/include)\n" + "target_sources(" + targetName + " INTERFACE include/" + headerName + ")\n"; + return out; + } + + static std::string toolName() { + return "whetstone_schema_to_cpp"; + } + +private: + static std::string mapType(const std::string& type) { + if (type == "integer") return "int"; + if (type == "number") return "double"; + if (type == "boolean") return "bool"; + return "std::string"; + } + + static std::string sanitizeIdentifier(const std::string& raw) { + std::string out; + out.reserve(raw.size()); + for (char ch : raw) { + if (std::isalnum(static_cast(ch))) out.push_back(ch); + else if (ch == '_' || ch == '-') out.push_back('_'); + } + if (out.empty()) return out; + if (std::isdigit(static_cast(out.front()))) { + out = "_" + out; + } + return out; + } +}; diff --git a/editor/tests/step634_test.cpp b/editor/tests/step634_test.cpp new file mode 100644 index 0000000..5667276 --- /dev/null +++ b/editor/tests/step634_test.cpp @@ -0,0 +1,128 @@ +// Step 634: Job schema -> typed C++ structs generator (12 tests) + +#include "SchemaToCppGenerator.h" + +#include + +static int passed = 0, failed = 0; +#define TEST(name) { std::cout << " " << #name << "... "; } +#define PASS() { std::cout << "PASS\n"; ++passed; } +#define FAIL(msg) { std::cout << "FAIL: " << msg << "\n"; ++failed; } +#define CHECK(cond, msg) if (!(cond)) { FAIL(msg); return; } + +void test_tool_name_matches_spec() { + TEST(tool_name_matches_spec); + CHECK(SchemaToCppGenerator::toolName() == "whetstone_schema_to_cpp", "tool name mismatch"); + PASS(); +} + +void test_generate_rejects_non_object_schema() { + TEST(generate_rejects_non_object_schema); + auto out = SchemaToCppGenerator::generate(json::array(), "Job.h", "job_schema_types"); + CHECK(!out.success, "non-object should fail"); + CHECK(!out.errors.empty(), "error expected"); + PASS(); +} + +void test_generate_rejects_missing_title() { + TEST(generate_rejects_missing_title); + json schema = {{"title", ""}, {"properties", json::object()}}; + auto out = SchemaToCppGenerator::generate(schema, "Job.h", "job_schema_types"); + CHECK(!out.success, "missing title should fail"); + PASS(); +} + +void test_generate_emits_struct_name_from_title() { + TEST(generate_emits_struct_name_from_title); + json schema = {{"title", "JobPayload"}, {"properties", json::object()}}; + auto out = SchemaToCppGenerator::generate(schema, "Job.h", "job_schema_types"); + CHECK(out.success, "generate should pass"); + CHECK(out.headerCode.find("struct JobPayload") != std::string::npos, "struct missing"); + PASS(); +} + +void test_generate_maps_integer_to_int() { + TEST(generate_maps_integer_to_int); + json schema = {{"title", "JobPayload"}, {"properties", {{"priority", {{"type", "integer"}}}}}}; + auto out = SchemaToCppGenerator::generate(schema, "Job.h", "job_schema_types"); + CHECK(out.headerCode.find("int priority;") != std::string::npos, "int field missing"); + PASS(); +} + +void test_generate_maps_boolean_to_bool() { + TEST(generate_maps_boolean_to_bool); + json schema = {{"title", "JobPayload"}, {"properties", {{"urgent", {{"type", "boolean"}}}}}}; + auto out = SchemaToCppGenerator::generate(schema, "Job.h", "job_schema_types"); + CHECK(out.headerCode.find("bool urgent;") != std::string::npos, "bool field missing"); + PASS(); +} + +void test_generate_defaults_unknown_type_to_string() { + TEST(generate_defaults_unknown_type_to_string); + json schema = {{"title", "JobPayload"}, {"properties", {{"payload", {{"type", "object"}}}}}}; + auto out = SchemaToCppGenerator::generate(schema, "Job.h", "job_schema_types"); + CHECK(out.headerCode.find("std::string payload;") != std::string::npos, "string fallback missing"); + PASS(); +} + +void test_generate_emits_to_json_and_from_json() { + TEST(generate_emits_to_json_and_from_json); + json schema = {{"title", "JobPayload"}, {"properties", {{"id", {{"type", "string"}}}}}}; + auto out = SchemaToCppGenerator::generate(schema, "Job.h", "job_schema_types"); + CHECK(out.headerCode.find("to_json") != std::string::npos, "to_json missing"); + CHECK(out.headerCode.find("from_json") != std::string::npos, "from_json missing"); + PASS(); +} + +void test_generate_emits_validation_stub() { + TEST(generate_emits_validation_stub); + json schema = {{"title", "JobPayload"}, {"properties", {{"id", {{"type", "string"}}}}}}; + auto out = SchemaToCppGenerator::generate(schema, "Job.h", "job_schema_types"); + CHECK(out.headerCode.find("validateJobPayload") != std::string::npos, "validate function missing"); + PASS(); +} + +void test_generate_emits_interface_target_text() { + TEST(generate_emits_interface_target_text); + json schema = {{"title", "JobPayload"}, {"properties", json::object()}}; + auto out = SchemaToCppGenerator::generate(schema, "JobPayload.h", "job_schema_types"); + CHECK(out.cmakeInterfaceTarget.find("add_library(job_schema_types INTERFACE)") != std::string::npos, + "interface target missing"); + PASS(); +} + +void test_generate_sanitizes_hyphenated_field_name() { + TEST(generate_sanitizes_hyphenated_field_name); + json schema = {{"title", "JobPayload"}, {"properties", {{"job-id", {{"type", "string"}}}}}}; + auto out = SchemaToCppGenerator::generate(schema, "Job.h", "job_schema_types"); + CHECK(out.headerCode.find("job_id") != std::string::npos, "sanitized field missing"); + PASS(); +} + +void test_generate_inserts_default_field_when_properties_empty() { + TEST(generate_inserts_default_field_when_properties_empty); + json schema = {{"title", "JobPayload"}, {"properties", json::object()}}; + auto out = SchemaToCppGenerator::generate(schema, "Job.h", "job_schema_types"); + CHECK(out.headerCode.find("std::string raw;") != std::string::npos, "raw fallback field missing"); + PASS(); +} + +int main() { + std::cout << "Step 634: Job schema -> typed C++ structs generator\n"; + + test_tool_name_matches_spec(); + test_generate_rejects_non_object_schema(); + test_generate_rejects_missing_title(); + test_generate_emits_struct_name_from_title(); + test_generate_maps_integer_to_int(); + test_generate_maps_boolean_to_bool(); + test_generate_defaults_unknown_type_to_string(); + test_generate_emits_to_json_and_from_json(); + test_generate_emits_validation_stub(); + test_generate_emits_interface_target_text(); + test_generate_sanitizes_hyphenated_field_name(); + test_generate_inserts_default_field_when_properties_empty(); + + std::cout << "\nResults: " << passed << "/" << (passed + failed) << " passed\n"; + return failed == 0 ? 0 : 1; +} diff --git a/progress.md b/progress.md index c956241..d57b2d0 100644 --- a/progress.md +++ b/progress.md @@ -12825,3 +12825,35 @@ single-function line limit. - `editor/src/AgentTaskStatusOverlay.h` (`79` <= `600`) - `editor/tests/step633_test.cpp` within test-file size guidance (`109` lines) - Header-only architecture and naming conventions remain aligned with `ARCHITECTURE.md` + +### Step 634: Job schema -> typed C++ structs generator +**Status:** PASS (12/12 tests) + +Introduces a schema-to-C++ generator module for shared HiveMind job payload +types with emitted `to_json`/`from_json` helpers, validation stub, and CMake +INTERFACE target text. + +**Files added:** +- `editor/src/SchemaToCppGenerator.h` - schema conversion module: + - schema validation and title/field sanitization + - C++ struct emission with JSON adapter helpers + - interface-target CMake snippet generation +- `editor/tests/step634_test.cpp` - 12 tests covering: + - tool identity and invalid-input rejection + - field type mapping and identifier sanitization + - JSON adapter + validation emission + - empty-properties edge behavior and interface-target text + +**Files modified:** +- `editor/CMakeLists.txt` - `step634_test` target + +**Verification run:** +- `cmake -S editor -B editor/build-native` - PASS +- `cmake --build editor/build-native --target step634_test step633_test` - PASS +- `./editor/build-native/step634_test` - PASS (12/12) +- `./editor/build-native/step633_test` - PASS (8/8) regression coverage + +**Architecture gate check:** +- `editor/src/SchemaToCppGenerator.h` (`116` <= `600`) +- `editor/tests/step634_test.cpp` within test-file size guidance (`128` lines) +- Header-only architecture and naming conventions remain aligned with `ARCHITECTURE.md`