diff --git a/README.markdown b/README.markdown new file mode 100644 index 0000000..33be88d --- /dev/null +++ b/README.markdown @@ -0,0 +1,6 @@ +Codegen +======= + +- Each input schema corresponds to a single `.d.ts` file +- 100% structurally map to TypeScript. Additional constraints might belong to + validation diff --git a/src/ir/include/sourcemeta/codegen/ir.h b/src/ir/include/sourcemeta/codegen/ir.h index 8e0c7f1..ea8f49d 100644 --- a/src/ir/include/sourcemeta/codegen/ir.h +++ b/src/ir/include/sourcemeta/codegen/ir.h @@ -6,11 +6,58 @@ #endif #include +#include #include +#include // std::uint8_t +#include // std::optional, std::nullopt +#include // std::unordered_map +#include // std::variant +#include // std::vector + /// @defgroup ir IR /// @brief The codegen JSON Schema intermediary format -namespace sourcemeta::codegen {} // namespace sourcemeta::codegen +namespace sourcemeta::codegen { + +/// @ingroup ir +enum class IRScalarType : std::uint8_t { String }; + +/// @ingroup ir +struct IRObjectValue { + bool required; + bool immutable; + sourcemeta::core::PointerTemplate pointer; +}; + +/// @ingroup ir +struct IRScalar { + sourcemeta::core::PointerTemplate pointer; + IRScalarType value; +}; + +/// @ingroup ir +struct IRObject { + sourcemeta::core::PointerTemplate pointer; + std::unordered_map members; +}; + +/// @ingroup ir +using IREntity = std::variant; + +/// @ingroup ir +using IRResult = std::vector; + +/// @ingroup ir +SOURCEMETA_CODEGEN_IR_EXPORT +auto compile(const sourcemeta::core::JSON &schema, + const sourcemeta::core::SchemaWalker &walker, + const sourcemeta::core::SchemaResolver &resolver, + const std::optional + &default_dialect = std::nullopt, + const std::optional &default_id = + std::nullopt) -> IRResult; + +} // namespace sourcemeta::codegen #endif diff --git a/src/ir/ir.cc b/src/ir/ir.cc index 44108e9..defe2c1 100644 --- a/src/ir/ir.cc +++ b/src/ir/ir.cc @@ -1,3 +1,140 @@ #include -namespace sourcemeta::codegen {} // namespace sourcemeta::codegen +#include // std::ranges::sort +#include // std::reference_wrapper +#include // std::map +#include // std::vector + +namespace sourcemeta::codegen { + +auto compile( + const sourcemeta::core::JSON &schema, + const sourcemeta::core::SchemaWalker &walker, + const sourcemeta::core::SchemaResolver &resolver, + const std::optional &default_dialect, + const std::optional &default_id) + -> IRResult { + sourcemeta::core::SchemaFrame frame{ + sourcemeta::core::SchemaFrame::Mode::Instances}; + frame.analyse(schema, walker, resolver, default_dialect, default_id); + std::map>> + instance_to_locations; + for (const auto &[key, location] : frame.locations()) { + if (location.type == + sourcemeta::core::SchemaFrame::LocationType::Resource || + location.type == + sourcemeta::core::SchemaFrame::LocationType::Subschema) { + for (const auto &instance_location : frame.instance_locations(location)) { + instance_to_locations[instance_location].emplace_back( + std::cref(location)); + } + } + } + + IRResult result; + + // Process each instance location group + for (const auto &[instance_location, locations] : instance_to_locations) { + for (const auto &location_ref : locations) { + const auto &location{location_ref.get()}; + const auto &subschema{sourcemeta::core::get(schema, location.pointer)}; + if (!subschema.is_object()) { + continue; + } + + const auto vocabularies{frame.vocabularies(location, resolver)}; + + if (subschema.defines("type")) { + const auto &type_result{walker("type", vocabularies)}; + if (type_result.type != + sourcemeta::core::SchemaKeywordType::Assertion) { + continue; + } + + const auto &type_value{subschema.at("type")}; + if (!type_value.is_string()) { + continue; + } + + const auto &type_string{type_value.to_string()}; + + if (type_string == "string") { + result.emplace_back(IRScalar{.pointer = instance_location, + .value = IRScalarType::String}); + } else if (type_string == "object") { + IRObject object; + object.pointer = instance_location; + + // Find child instance locations (one property token deeper) + for (const auto &[child_instance, child_locations] : + instance_to_locations) { + if (!child_instance.trivial() || child_instance.empty()) { + continue; + } + + // Check if child is exactly one property token deeper + auto child_size{ + std::distance(child_instance.begin(), child_instance.end())}; + auto parent_size{std::distance(instance_location.begin(), + instance_location.end())}; + if (child_size != parent_size + 1) { + continue; + } + + // Verify parent prefix matches + auto matches{true}; + auto child_iter{child_instance.begin()}; + for (const auto &parent_token : instance_location) { + if (*child_iter != parent_token) { + matches = false; + break; + } + ++child_iter; + } + + if (!matches) { + continue; + } + + // Get the property name from the last token + const auto &last_token{*child_instance.rbegin()}; + if (!std::holds_alternative( + last_token)) { + continue; + } + + const auto &property_token{ + std::get(last_token)}; + if (!property_token.is_property()) { + continue; + } + + object.members.emplace(property_token.to_property(), + IRObjectValue{.required = false, + .immutable = false, + .pointer = child_instance}); + } + + result.emplace_back(std::move(object)); + } + } + } + } + + // Sort by pointer template (longer paths come first, so dependencies + // appear before their parents) + const auto get_pointer{ + [](const auto &entry) -> const sourcemeta::core::PointerTemplate & { + return entry.pointer; + }}; + std::ranges::sort( + result, [&get_pointer](const IREntity &a, const IREntity &b) -> bool { + return std::visit(get_pointer, b) < std::visit(get_pointer, a); + }); + + return result; +} + +} // namespace sourcemeta::codegen diff --git a/test/ir/ir_test.cc b/test/ir/ir_test.cc index ede5198..ce2d7c0 100644 --- a/test/ir/ir_test.cc +++ b/test/ir/ir_test.cc @@ -2,4 +2,57 @@ #include -TEST(IR, test_1) { EXPECT_TRUE(true); } +TEST(IR, test_1) { + const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "string" + })JSON")}; + + const auto result{ + sourcemeta::codegen::compile(schema, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver)}; + + using namespace sourcemeta::codegen; + + EXPECT_EQ(result.size(), 1); + + EXPECT_TRUE(std::holds_alternative(result.at(0))); + EXPECT_TRUE(std::get(result.at(0)).pointer.empty()); + EXPECT_EQ(std::get(result.at(0)).value, IRScalarType::String); +} + +TEST(IR, test_2) { + const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "foo": { + "type": "string" + } + } + })JSON")}; + + const auto result{ + sourcemeta::codegen::compile(schema, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver)}; + + using namespace sourcemeta::codegen; + + EXPECT_EQ(result.size(), 2); + + EXPECT_TRUE(std::holds_alternative(result.at(0))); + EXPECT_EQ( + std::get(result.at(0)).pointer, + sourcemeta::core::PointerTemplate{sourcemeta::core::Pointer{"foo"}}); + EXPECT_EQ(std::get(result.at(0)).value, IRScalarType::String); + + EXPECT_TRUE(std::holds_alternative(result.at(1))); + EXPECT_TRUE(std::get(result.at(1)).pointer.empty()); + EXPECT_EQ(std::get(result.at(1)).members.size(), 1); + EXPECT_TRUE(std::get(result.at(1)).members.contains("foo")); + EXPECT_FALSE(std::get(result.at(1)).members.at("foo").required); + EXPECT_FALSE(std::get(result.at(1)).members.at("foo").immutable); + EXPECT_EQ( + std::get(result.at(1)).members.at("foo").pointer, + sourcemeta::core::PointerTemplate{sourcemeta::core::Pointer{"foo"}}); +}