Step 634: schema to C++ generator
This commit is contained in:
@@ -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)
|
||||
|
||||
116
editor/src/SchemaToCppGenerator.h
Normal file
116
editor/src/SchemaToCppGenerator.h
Normal file
@@ -0,0 +1,116 @@
|
||||
#pragma once
|
||||
// Step 634: Job schema -> typed C++ structs generator
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
#include <cctype>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
using json = nlohmann::json;
|
||||
|
||||
struct SchemaToCppOutput {
|
||||
bool success = false;
|
||||
std::string headerCode;
|
||||
std::string cmakeInterfaceTarget;
|
||||
std::vector<std::string> 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<std::string>().empty()) {
|
||||
out.errors.push_back("schema_title_missing");
|
||||
return out;
|
||||
}
|
||||
const std::string structName = sanitizeIdentifier(
|
||||
schema["title"].get<std::string>());
|
||||
if (structName.empty()) {
|
||||
out.errors.push_back("schema_title_missing");
|
||||
return out;
|
||||
}
|
||||
|
||||
std::string code;
|
||||
code += "#pragma once\n";
|
||||
code += "#include <nlohmann/json.hpp>\n";
|
||||
code += "#include <string>\n\n";
|
||||
code += "struct " + structName + " {\n";
|
||||
|
||||
std::vector<std::pair<std::string, std::string>> 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<unsigned char>(ch))) out.push_back(ch);
|
||||
else if (ch == '_' || ch == '-') out.push_back('_');
|
||||
}
|
||||
if (out.empty()) return out;
|
||||
if (std::isdigit(static_cast<unsigned char>(out.front()))) {
|
||||
out = "_" + out;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
};
|
||||
128
editor/tests/step634_test.cpp
Normal file
128
editor/tests/step634_test.cpp
Normal file
@@ -0,0 +1,128 @@
|
||||
// Step 634: Job schema -> typed C++ structs generator (12 tests)
|
||||
|
||||
#include "SchemaToCppGenerator.h"
|
||||
|
||||
#include <iostream>
|
||||
|
||||
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;
|
||||
}
|
||||
32
progress.md
32
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`
|
||||
|
||||
Reference in New Issue
Block a user