Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions README.markdown
Original file line number Diff line number Diff line change
@@ -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
49 changes: 48 additions & 1 deletion src/ir/include/sourcemeta/codegen/ir.h
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,58 @@
#endif

#include <sourcemeta/core/json.h>
#include <sourcemeta/core/jsonpointer.h>
#include <sourcemeta/core/jsonschema.h>

#include <cstdint> // std::uint8_t
#include <optional> // std::optional, std::nullopt
#include <unordered_map> // std::unordered_map
#include <variant> // std::variant
#include <vector> // 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<sourcemeta::core::JSON::String, IRObjectValue> members;
};

/// @ingroup ir
using IREntity = std::variant<IRObject, IRScalar>;

/// @ingroup ir
using IRResult = std::vector<IREntity>;

/// @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<sourcemeta::core::JSON::String>
&default_dialect = std::nullopt,
const std::optional<sourcemeta::core::JSON::String> &default_id =
std::nullopt) -> IRResult;

} // namespace sourcemeta::codegen

#endif
139 changes: 138 additions & 1 deletion src/ir/ir.cc
Original file line number Diff line number Diff line change
@@ -1,3 +1,140 @@
#include <sourcemeta/codegen/ir.h>

namespace sourcemeta::codegen {} // namespace sourcemeta::codegen
#include <algorithm> // std::ranges::sort
#include <functional> // std::reference_wrapper
#include <map> // std::map
#include <vector> // 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<sourcemeta::core::JSON::String> &default_dialect,
const std::optional<sourcemeta::core::JSON::String> &default_id)
-> IRResult {
sourcemeta::core::SchemaFrame frame{
sourcemeta::core::SchemaFrame::Mode::Instances};
frame.analyse(schema, walker, resolver, default_dialect, default_id);
std::map<sourcemeta::core::PointerTemplate,
std::vector<std::reference_wrapper<
const sourcemeta::core::SchemaFrame::Location>>>
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<sourcemeta::core::Pointer::Token>(
last_token)) {
continue;
}

const auto &property_token{
std::get<sourcemeta::core::Pointer::Token>(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
55 changes: 54 additions & 1 deletion test/ir/ir_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,57 @@

#include <sourcemeta/codegen/ir.h>

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<IRScalar>(result.at(0)));
EXPECT_TRUE(std::get<IRScalar>(result.at(0)).pointer.empty());
EXPECT_EQ(std::get<IRScalar>(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<IRScalar>(result.at(0)));
EXPECT_EQ(
std::get<IRScalar>(result.at(0)).pointer,
sourcemeta::core::PointerTemplate{sourcemeta::core::Pointer{"foo"}});
EXPECT_EQ(std::get<IRScalar>(result.at(0)).value, IRScalarType::String);

EXPECT_TRUE(std::holds_alternative<IRObject>(result.at(1)));
EXPECT_TRUE(std::get<IRObject>(result.at(1)).pointer.empty());
EXPECT_EQ(std::get<IRObject>(result.at(1)).members.size(), 1);
EXPECT_TRUE(std::get<IRObject>(result.at(1)).members.contains("foo"));
EXPECT_FALSE(std::get<IRObject>(result.at(1)).members.at("foo").required);
EXPECT_FALSE(std::get<IRObject>(result.at(1)).members.at("foo").immutable);
EXPECT_EQ(
std::get<IRObject>(result.at(1)).members.at("foo").pointer,
sourcemeta::core::PointerTemplate{sourcemeta::core::Pointer{"foo"}});
}