Skip to content
Draft
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
2 changes: 1 addition & 1 deletion config.cmake.in
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ endif()
include(CMakeFindDependencyMacro)
find_dependency(Core COMPONENTS
unicode punycode idna regex uri uritemplate json
jsonpointer io yaml crypto html email ip dns time css)
jsonpointer jsonld io yaml crypto html email ip dns time css)

foreach(component ${BLAZE_COMPONENTS})
if(component STREQUAL "foundation")
Expand Down
7 changes: 5 additions & 2 deletions src/output/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
sourcemeta_library(NAMESPACE sourcemeta PROJECT blaze NAME output
FOLDER "Blaze/Output"
PRIVATE_HEADERS simple.h trace.h standard.h
PRIVATE_HEADERS simple.h trace.h standard.h jsonld.h
SOURCES
output_simple.cc
output_trace.cc
output_standard.cc)
output_standard.cc
output_jsonld.cc)

if(BLAZE_INSTALL)
sourcemeta_library_install(NAMESPACE sourcemeta PROJECT blaze NAME output)
Expand All @@ -14,6 +15,8 @@ target_link_libraries(sourcemeta_blaze_output PUBLIC
sourcemeta::core::json)
target_link_libraries(sourcemeta_blaze_output PUBLIC
sourcemeta::core::jsonpointer)
target_link_libraries(sourcemeta_blaze_output PRIVATE
sourcemeta::core::jsonld)
target_link_libraries(sourcemeta_blaze_output PUBLIC
sourcemeta::blaze::foundation)
target_link_libraries(sourcemeta_blaze_output PUBLIC
Expand Down
1 change: 1 addition & 0 deletions src/output/include/sourcemeta/blaze/output.h
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#ifndef SOURCEMETA_BLAZE_OUTPUT_H_
#define SOURCEMETA_BLAZE_OUTPUT_H_

#include <sourcemeta/blaze/output_jsonld.h>
#include <sourcemeta/blaze/output_simple.h>
#include <sourcemeta/blaze/output_standard.h>
#include <sourcemeta/blaze/output_trace.h>
Expand Down
107 changes: 107 additions & 0 deletions src/output/include/sourcemeta/blaze/output_jsonld.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
#ifndef SOURCEMETA_BLAZE_OUTPUT_JSONLD_H_
#define SOURCEMETA_BLAZE_OUTPUT_JSONLD_H_

#ifndef SOURCEMETA_BLAZE_OUTPUT_EXPORT
#include <sourcemeta/blaze/output_export.h>
#endif

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

#include <sourcemeta/blaze/evaluator.h>
#include <sourcemeta/blaze/output_simple.h>

#include <string> // std::string
#include <variant> // std::variant
#include <vector> // std::vector

namespace sourcemeta::blaze {

/// @ingroup output
/// The instance conforms and its JSON-LD annotations resolve cleanly. The value
/// is the instance promoted to expanded JSON-LD.
struct JSONLDDocument {
sourcemeta::core::JSON value;
};

/// @ingroup output
/// A single JSON-LD resolution failure at an instance location, distinct from a
/// schema validation error.
struct JSONLDResolutionError {
sourcemeta::core::Pointer instance_location;
std::string facet;
std::string message;
};

/// @ingroup output
/// The instance does not conform to the schema, so no JSON-LD is produced. The
/// entries are the schema validation errors.
struct JSONLDInvalid {
std::vector<SimpleOutput::Entry> errors;
};

/// @ingroup output
/// The instance conforms but its JSON-LD annotations cannot be resolved into a
/// consistent mapping.
struct JSONLDUnresolved {
std::vector<JSONLDResolutionError> errors;
};

/// @ingroup output
/// The tri-state outcome of a JSON-LD evaluation: the expanded document, schema
/// validation errors, or JSON-LD resolution errors.
using JSONLDOutcome =
std::variant<JSONLDDocument, JSONLDInvalid, JSONLDUnresolved>;

/// @ingroup output
///
/// Evaluate an instance against a schema whose atoms carry `x-jsonld-*`
/// annotations, and on success return the instance promoted to expanded
/// JSON-LD. The schema must have been compiled with `Tweaks.annotations`
/// whitelisting the `x-jsonld-*` keywords. Mirrors `standard()`. For example:
///
/// ```cpp
/// #include <sourcemeta/blaze/compiler.h>
/// #include <sourcemeta/blaze/evaluator.h>
/// #include <sourcemeta/blaze/output.h>
///
/// #include <sourcemeta/core/json.h>
/// #include <sourcemeta/blaze/foundation.h>
///
/// #include <unordered_set>
/// #include <variant>
///
/// const sourcemeta::core::JSON schema =
/// sourcemeta::core::parse_json(R"JSON({
/// "$schema": "https://json-schema.org/draft/2020-12/schema",
/// "type": "object",
/// "x-jsonld-type": "https://schema.org/Person",
/// "properties": {
/// "name": { "type": "string", "x-jsonld-id": "https://schema.org/name" }
/// }
/// })JSON");
///
/// sourcemeta::blaze::Tweaks tweaks;
/// tweaks.annotations = std::unordered_set<sourcemeta::core::JSON::StringView>{
/// "x-jsonld-id", "x-jsonld-type"};
///
/// const auto schema_template{sourcemeta::blaze::compile(
/// schema, sourcemeta::blaze::schema_walker,
/// sourcemeta::blaze::schema_resolver,
/// sourcemeta::blaze::default_schema_compiler,
/// sourcemeta::blaze::Mode::FastValidation, "", "", "", tweaks)};
///
/// const sourcemeta::core::JSON instance{
/// sourcemeta::core::parse_json(R"JSON({ "name": "Ada" })JSON")};
///
/// sourcemeta::blaze::Evaluator evaluator;
/// const auto outcome{
/// sourcemeta::blaze::jsonld(evaluator, schema_template, instance)};
/// ```
auto SOURCEMETA_BLAZE_OUTPUT_EXPORT
jsonld(Evaluator &evaluator, const Template &schema,
const sourcemeta::core::JSON &instance) -> JSONLDOutcome;

} // namespace sourcemeta::blaze

#endif
7 changes: 7 additions & 0 deletions src/output/include/sourcemeta/blaze/output_simple.h
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,13 @@ class SOURCEMETA_BLAZE_OUTPUT_EXPORT SimpleOutput {
return this->annotations_;
}

/// Move out the collected error entries, leaving this output empty. Useful to
/// take ownership of the trace without copying when the output is no longer
/// needed
[[nodiscard]] auto release() && -> container_type {
return std::move(this->output);
}

// NOLINTNEXTLINE(bugprone-exception-escape)
struct Location {
auto operator<(const Location &other) const noexcept -> bool {
Expand Down
126 changes: 126 additions & 0 deletions src/output/output_jsonld.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
#include <sourcemeta/blaze/output_jsonld.h>

#include <sourcemeta/core/jsonld.h>
#include <sourcemeta/core/jsonpointer.h>

#include <algorithm> // std::ranges::sort, std::ranges::any_of
#include <functional> // std::ref
#include <tuple> // std::tie
#include <unordered_map> // std::unordered_map
#include <utility> // std::move
#include <vector> // std::vector

namespace {

// The JSON-LD facts accumulated at one instance location, before a descriptor
// kind is derived from the instance value shape.
struct Facts {
std::vector<sourcemeta::core::JSONLDEdge> edges;
std::vector<sourcemeta::core::JSON::String> types;
};

auto add_edge(std::vector<sourcemeta::core::JSONLDEdge> &edges,
const sourcemeta::core::JSON::String &predicate,
const bool reverse) -> void {
const auto exists{std::ranges::any_of(
edges,
[&predicate, reverse](const sourcemeta::core::JSONLDEdge &edge) -> bool {
return edge.predicate == predicate && edge.reverse == reverse;
})};
if (!exists) {
edges.push_back({.predicate = predicate, .reverse = reverse});
}
}

auto add_type(std::vector<sourcemeta::core::JSON::String> &types,
const sourcemeta::core::JSON::String &type) -> void {
const auto exists{std::ranges::any_of(
types, [&type](const sourcemeta::core::JSON::String &existing) -> bool {
return existing == type;
})};
if (!exists) {
types.push_back(type);
}
}

// Turn the applicable x-jsonld-* annotations into a resolved annotation map.
// Only x-jsonld-id and x-jsonld-type are handled for now, so neither a
// single-valued facet conflict nor an unbound identity can arise, and there is
// no resolution-error path yet.
auto resolve(const sourcemeta::core::JSON &instance,
const sourcemeta::blaze::SimpleOutput &output)
-> sourcemeta::core::JSONLDWeakAnnotationMap {
std::unordered_map<sourcemeta::core::WeakPointer, Facts,
sourcemeta::core::WeakPointer::Hasher>
accumulator;

for (const auto &[location, values] : output.annotations()) {
if (location.evaluate_path.empty()) {
continue;
}

const auto &keyword{location.evaluate_path.back().to_property()};
auto &facts{accumulator[location.instance_location]};
if (keyword == "x-jsonld-id") {
for (const auto &value : values) {
add_edge(facts.edges, value.to_string(), false);
}
} else if (keyword == "x-jsonld-type") {
for (const auto &value : values) {
if (value.is_array()) {
for (const auto &element : value.as_array()) {
add_type(facts.types, element.to_string());
}
} else {
add_type(facts.types, value.to_string());
}
}
}
}

sourcemeta::core::JSONLDWeakAnnotationMap map;
for (auto &[pointer, facts] : accumulator) {
std::ranges::sort(facts.edges,
[](const sourcemeta::core::JSONLDEdge &left,
const sourcemeta::core::JSONLDEdge &right) -> bool {
return std::tie(left.predicate, left.reverse) <
std::tie(right.predicate, right.reverse);
});
std::ranges::sort(facts.types);

sourcemeta::core::JSONLDDescriptor descriptor;
descriptor.edges = std::move(facts.edges);
const auto &value{sourcemeta::core::get(instance, pointer)};
if (value.is_object()) {
descriptor.value =
sourcemeta::core::JSONLDNode{.types = std::move(facts.types)};
} else if (value.is_array()) {
descriptor.value = sourcemeta::core::JSONLDCollection{};
} else {
descriptor.value = sourcemeta::core::JSONLDLiteral{};
}

map.emplace(pointer, std::move(descriptor));
}

return map;
}

} // namespace

namespace sourcemeta::blaze {

auto jsonld(Evaluator &evaluator, const Template &schema,
const sourcemeta::core::JSON &instance) -> JSONLDOutcome {
SimpleOutput output{instance};
const auto valid{evaluator.validate(schema, instance, std::ref(output))};
if (!valid) {
return JSONLDInvalid{.errors = std::move(output).release()};
}

const auto map{resolve(instance, output)};
return JSONLDDocument{
.value = sourcemeta::core::jsonld_materialize(instance, map)};
}

} // namespace sourcemeta::blaze
5 changes: 4 additions & 1 deletion test/output/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@ sourcemeta_test(NAMESPACE sourcemeta PROJECT blaze NAME output
SOURCES
output_simple_test.cc
output_standard_basic_test.cc
output_trace_test.cc)
output_trace_test.cc
output_jsonld_test.cc)

target_link_libraries(sourcemeta_blaze_output_unit
PRIVATE sourcemeta::core::json)
target_link_libraries(sourcemeta_blaze_output_unit
PRIVATE sourcemeta::core::jsonld)
target_link_libraries(sourcemeta_blaze_output_unit
PRIVATE sourcemeta::blaze::foundation)
target_link_libraries(sourcemeta_blaze_output_unit
Expand Down
Loading
Loading