Skip to content

Commit a073f1d

Browse files
committed
[WIP] Support JSON-LD annotations over instances
Signed-off-by: Juan Cruz Viotti <jv@jviotti.com>
1 parent 3c4b01a commit a073f1d

11 files changed

Lines changed: 629 additions & 5 deletions

File tree

config.cmake.in

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ endif()
2222
include(CMakeFindDependencyMacro)
2323
find_dependency(Core COMPONENTS
2424
unicode punycode idna regex uri uritemplate json
25-
jsonpointer io yaml crypto html email ip dns time css)
25+
jsonpointer jsonld io yaml crypto html email ip dns time css)
2626

2727
foreach(component ${BLAZE_COMPONENTS})
2828
if(component STREQUAL "foundation")

src/compiler/default_compiler_draft3.h

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -921,7 +921,13 @@ auto compiler_draft3_applicator_properties_with_options(
921921
}
922922

923923
if (context.mode == Mode::FastValidation) {
924-
if (fusion_possible && !fusion_entries.empty()) {
924+
// The fused `AssertionObjectPropertiesSimple` instruction evaluates each
925+
// property subschema without a callback, which silently drops any
926+
// annotation emitted inside a nested applicator such as `oneOf`. Skip the
927+
// fusion when annotations are being collected so those annotations reach
928+
// the callback through the regular property instructions.
929+
if (fusion_possible && !fusion_entries.empty() &&
930+
!annotations_collected(context)) {
925931
for (const auto &req : required) {
926932
const auto &req_name{req.first};
927933
bool already_tracked{false};

src/output/CMakeLists.txt

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
sourcemeta_library(NAMESPACE sourcemeta PROJECT blaze NAME output
22
FOLDER "Blaze/Output"
3-
PRIVATE_HEADERS simple.h trace.h standard.h
3+
PRIVATE_HEADERS simple.h trace.h standard.h jsonld.h
44
SOURCES
55
output_simple.cc
66
output_trace.cc
7-
output_standard.cc)
7+
output_standard.cc
8+
output_jsonld.cc)
89

910
if(BLAZE_INSTALL)
1011
sourcemeta_library_install(NAMESPACE sourcemeta PROJECT blaze NAME output)
@@ -14,6 +15,8 @@ target_link_libraries(sourcemeta_blaze_output PUBLIC
1415
sourcemeta::core::json)
1516
target_link_libraries(sourcemeta_blaze_output PUBLIC
1617
sourcemeta::core::jsonpointer)
18+
target_link_libraries(sourcemeta_blaze_output PRIVATE
19+
sourcemeta::core::jsonld)
1720
target_link_libraries(sourcemeta_blaze_output PUBLIC
1821
sourcemeta::blaze::foundation)
1922
target_link_libraries(sourcemeta_blaze_output PUBLIC

src/output/include/sourcemeta/blaze/output.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#ifndef SOURCEMETA_BLAZE_OUTPUT_H_
22
#define SOURCEMETA_BLAZE_OUTPUT_H_
33

4+
#include <sourcemeta/blaze/output_jsonld.h>
45
#include <sourcemeta/blaze/output_simple.h>
56
#include <sourcemeta/blaze/output_standard.h>
67
#include <sourcemeta/blaze/output_trace.h>
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
#ifndef SOURCEMETA_BLAZE_OUTPUT_JSONLD_H_
2+
#define SOURCEMETA_BLAZE_OUTPUT_JSONLD_H_
3+
4+
#ifndef SOURCEMETA_BLAZE_OUTPUT_EXPORT
5+
#include <sourcemeta/blaze/output_export.h>
6+
#endif
7+
8+
#include <sourcemeta/core/json.h>
9+
#include <sourcemeta/core/jsonpointer.h>
10+
11+
#include <sourcemeta/blaze/evaluator.h>
12+
#include <sourcemeta/blaze/output_simple.h>
13+
14+
#include <string> // std::string
15+
#include <variant> // std::variant
16+
#include <vector> // std::vector
17+
18+
namespace sourcemeta::blaze {
19+
20+
/// @ingroup output
21+
/// The instance conforms and its JSON-LD annotations resolve cleanly. The value
22+
/// is the instance promoted to expanded JSON-LD.
23+
struct JSONLDDocument {
24+
sourcemeta::core::JSON value;
25+
};
26+
27+
/// @ingroup output
28+
/// A single JSON-LD resolution failure at an instance location, distinct from a
29+
/// schema validation error.
30+
struct JSONLDResolutionError {
31+
sourcemeta::core::Pointer instance_location;
32+
std::string facet;
33+
std::string message;
34+
};
35+
36+
/// @ingroup output
37+
/// The instance does not conform to the schema, so no JSON-LD is produced. The
38+
/// entries are the schema validation errors.
39+
struct JSONLDInvalid {
40+
std::vector<SimpleOutput::Entry> errors;
41+
};
42+
43+
/// @ingroup output
44+
/// The instance conforms but its JSON-LD annotations cannot be resolved into a
45+
/// consistent mapping.
46+
struct JSONLDUnresolved {
47+
std::vector<JSONLDResolutionError> errors;
48+
};
49+
50+
/// @ingroup output
51+
/// The tri-state outcome of a JSON-LD evaluation: the expanded document, schema
52+
/// validation errors, or JSON-LD resolution errors.
53+
using JSONLDOutcome =
54+
std::variant<JSONLDDocument, JSONLDInvalid, JSONLDUnresolved>;
55+
56+
/// @ingroup output
57+
///
58+
/// Evaluate an instance against a schema whose atoms carry `x-jsonld-*`
59+
/// annotations, and on success return the instance promoted to expanded
60+
/// JSON-LD. The schema must have been compiled with `Tweaks.annotations`
61+
/// whitelisting the `x-jsonld-*` keywords. Mirrors `standard()`. For example:
62+
///
63+
/// ```cpp
64+
/// #include <sourcemeta/blaze/compiler.h>
65+
/// #include <sourcemeta/blaze/evaluator.h>
66+
/// #include <sourcemeta/blaze/output.h>
67+
///
68+
/// #include <sourcemeta/core/json.h>
69+
/// #include <sourcemeta/blaze/foundation.h>
70+
///
71+
/// #include <unordered_set>
72+
/// #include <variant>
73+
///
74+
/// const sourcemeta::core::JSON schema =
75+
/// sourcemeta::core::parse_json(R"JSON({
76+
/// "$schema": "https://json-schema.org/draft/2020-12/schema",
77+
/// "type": "object",
78+
/// "x-jsonld-type": "https://schema.org/Person",
79+
/// "properties": {
80+
/// "name": { "type": "string", "x-jsonld-id": "https://schema.org/name" }
81+
/// }
82+
/// })JSON");
83+
///
84+
/// sourcemeta::blaze::Tweaks tweaks;
85+
/// tweaks.annotations = std::unordered_set<sourcemeta::core::JSON::StringView>{
86+
/// "x-jsonld-id", "x-jsonld-type"};
87+
///
88+
/// const auto schema_template{sourcemeta::blaze::compile(
89+
/// schema, sourcemeta::blaze::schema_walker,
90+
/// sourcemeta::blaze::schema_resolver,
91+
/// sourcemeta::blaze::default_schema_compiler,
92+
/// sourcemeta::blaze::Mode::FastValidation, "", "", "", tweaks)};
93+
///
94+
/// const sourcemeta::core::JSON instance{
95+
/// sourcemeta::core::parse_json(R"JSON({ "name": "Ada" })JSON")};
96+
///
97+
/// sourcemeta::blaze::Evaluator evaluator;
98+
/// const auto outcome{
99+
/// sourcemeta::blaze::jsonld(evaluator, schema_template, instance)};
100+
/// ```
101+
auto SOURCEMETA_BLAZE_OUTPUT_EXPORT
102+
jsonld(Evaluator &evaluator, const Template &schema,
103+
const sourcemeta::core::JSON &instance) -> JSONLDOutcome;
104+
105+
} // namespace sourcemeta::blaze
106+
107+
#endif

src/output/include/sourcemeta/blaze/output_simple.h

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,13 @@ class SOURCEMETA_BLAZE_OUTPUT_EXPORT SimpleOutput {
103103
return this->annotations_;
104104
}
105105

106+
/// Move out the collected error entries, leaving this output empty. Useful to
107+
/// take ownership of the trace without copying when the output is no longer
108+
/// needed
109+
[[nodiscard]] auto release() && -> container_type {
110+
return std::move(this->output);
111+
}
112+
106113
// NOLINTNEXTLINE(bugprone-exception-escape)
107114
struct Location {
108115
auto operator<(const Location &other) const noexcept -> bool {

src/output/output_jsonld.cc

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
#include <sourcemeta/blaze/output_jsonld.h>
2+
3+
#include <sourcemeta/core/jsonld.h>
4+
#include <sourcemeta/core/jsonpointer.h>
5+
6+
#include <algorithm> // std::ranges::sort, std::ranges::any_of
7+
#include <functional> // std::ref
8+
#include <tuple> // std::tie
9+
#include <unordered_map> // std::unordered_map
10+
#include <utility> // std::move
11+
#include <vector> // std::vector
12+
13+
namespace {
14+
15+
// The JSON-LD facts accumulated at one instance location, before a descriptor
16+
// kind is derived from the instance value shape.
17+
struct Facts {
18+
std::vector<sourcemeta::core::JSONLDEdge> edges;
19+
std::vector<sourcemeta::core::JSON::String> types;
20+
};
21+
22+
auto add_edge(std::vector<sourcemeta::core::JSONLDEdge> &edges,
23+
const sourcemeta::core::JSON::String &predicate,
24+
const bool reverse) -> void {
25+
const auto exists{std::ranges::any_of(
26+
edges,
27+
[&predicate, reverse](const sourcemeta::core::JSONLDEdge &edge) -> bool {
28+
return edge.predicate == predicate && edge.reverse == reverse;
29+
})};
30+
if (!exists) {
31+
edges.push_back({.predicate = predicate, .reverse = reverse});
32+
}
33+
}
34+
35+
auto add_type(std::vector<sourcemeta::core::JSON::String> &types,
36+
const sourcemeta::core::JSON::String &type) -> void {
37+
const auto exists{std::ranges::any_of(
38+
types, [&type](const sourcemeta::core::JSON::String &existing) -> bool {
39+
return existing == type;
40+
})};
41+
if (!exists) {
42+
types.push_back(type);
43+
}
44+
}
45+
46+
// Turn the applicable x-jsonld-* annotations into a resolved annotation map.
47+
// Only x-jsonld-id and x-jsonld-type are handled for now, so neither a
48+
// single-valued facet conflict nor an unbound identity can arise, and there is
49+
// no resolution-error path yet.
50+
auto resolve(const sourcemeta::core::JSON &instance,
51+
const sourcemeta::blaze::SimpleOutput &output)
52+
-> sourcemeta::core::JSONLDWeakAnnotationMap {
53+
std::unordered_map<sourcemeta::core::WeakPointer, Facts,
54+
sourcemeta::core::WeakPointer::Hasher>
55+
accumulator;
56+
57+
for (const auto &[location, values] : output.annotations()) {
58+
if (location.evaluate_path.empty()) {
59+
continue;
60+
}
61+
62+
const auto &keyword{location.evaluate_path.back().to_property()};
63+
auto &facts{accumulator[location.instance_location]};
64+
if (keyword == "x-jsonld-id") {
65+
for (const auto &value : values) {
66+
add_edge(facts.edges, value.to_string(), false);
67+
}
68+
} else if (keyword == "x-jsonld-type") {
69+
for (const auto &value : values) {
70+
if (value.is_array()) {
71+
for (const auto &element : value.as_array()) {
72+
add_type(facts.types, element.to_string());
73+
}
74+
} else {
75+
add_type(facts.types, value.to_string());
76+
}
77+
}
78+
}
79+
}
80+
81+
sourcemeta::core::JSONLDWeakAnnotationMap map;
82+
for (auto &[pointer, facts] : accumulator) {
83+
std::ranges::sort(facts.edges,
84+
[](const sourcemeta::core::JSONLDEdge &left,
85+
const sourcemeta::core::JSONLDEdge &right) -> bool {
86+
return std::tie(left.predicate, left.reverse) <
87+
std::tie(right.predicate, right.reverse);
88+
});
89+
std::ranges::sort(facts.types);
90+
91+
sourcemeta::core::JSONLDDescriptor descriptor;
92+
descriptor.edges = std::move(facts.edges);
93+
const auto &value{sourcemeta::core::get(instance, pointer)};
94+
if (value.is_object()) {
95+
descriptor.value =
96+
sourcemeta::core::JSONLDNode{.types = std::move(facts.types)};
97+
} else if (value.is_array()) {
98+
descriptor.value = sourcemeta::core::JSONLDCollection{};
99+
} else {
100+
descriptor.value = sourcemeta::core::JSONLDLiteral{};
101+
}
102+
103+
map.emplace(pointer, std::move(descriptor));
104+
}
105+
106+
return map;
107+
}
108+
109+
} // namespace
110+
111+
namespace sourcemeta::blaze {
112+
113+
auto jsonld(Evaluator &evaluator, const Template &schema,
114+
const sourcemeta::core::JSON &instance) -> JSONLDOutcome {
115+
SimpleOutput output{instance};
116+
const auto valid{evaluator.validate(schema, instance, std::ref(output))};
117+
if (!valid) {
118+
return JSONLDInvalid{.errors = std::move(output).release()};
119+
}
120+
121+
const auto map{resolve(instance, output)};
122+
return JSONLDDocument{
123+
.value = sourcemeta::core::jsonld_materialize(instance, map)};
124+
}
125+
126+
} // namespace sourcemeta::blaze

0 commit comments

Comments
 (0)