@nuggetslife/vc 0.0.30 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Cargo.toml +11 -0
- package/W3C_CONFORMANCE.md +6 -5
- package/bench/frame_compare.mjs +203 -0
- package/bench/v2_internals.mjs +115 -0
- package/bench/vc_ops.mjs +308 -0
- package/package.json +6 -6
- package/scripts/fetch-w3c-tests.sh +8 -0
- package/src/jsonld.rs +210 -140
- package/test_jsonld_crossverify.mjs +8 -2
- package/test_w3c_conformance.mjs +80 -7
- package/w3c-baseline.json +883 -262
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nuggetslife/vc",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.1.0",
|
|
4
4
|
"main": "index.js",
|
|
5
5
|
"types": "index.d.ts",
|
|
6
6
|
"napi": {
|
|
@@ -41,11 +41,11 @@
|
|
|
41
41
|
},
|
|
42
42
|
"packageManager": "yarn@4.3.1",
|
|
43
43
|
"optionalDependencies": {
|
|
44
|
-
"@nuggetslife/vc-darwin-arm64": "0.0
|
|
45
|
-
"@nuggetslife/vc-linux-arm64-gnu": "0.0
|
|
46
|
-
"@nuggetslife/vc-linux-arm64-musl": "0.0
|
|
47
|
-
"@nuggetslife/vc-linux-x64-gnu": "0.0
|
|
48
|
-
"@nuggetslife/vc-linux-x64-musl": "0.0
|
|
44
|
+
"@nuggetslife/vc-darwin-arm64": "0.1.0",
|
|
45
|
+
"@nuggetslife/vc-linux-arm64-gnu": "0.1.0",
|
|
46
|
+
"@nuggetslife/vc-linux-arm64-musl": "0.1.0",
|
|
47
|
+
"@nuggetslife/vc-linux-x64-gnu": "0.1.0",
|
|
48
|
+
"@nuggetslife/vc-linux-x64-musl": "0.1.0"
|
|
49
49
|
},
|
|
50
50
|
"dependencies": {}
|
|
51
51
|
}
|
|
@@ -12,6 +12,9 @@ JLD_API_SHA="04a4eb7dc7cbc313f3f5be7ad9a3b06e87741693"
|
|
|
12
12
|
RDF_CANON_REPO="https://github.com/w3c/rdf-canon.git"
|
|
13
13
|
RDF_CANON_SHA="15619df2fda7a4ca88308733789b6774517f9638"
|
|
14
14
|
|
|
15
|
+
JLD_FRAMING_REPO="https://github.com/w3c/json-ld-framing.git"
|
|
16
|
+
JLD_FRAMING_SHA="fa228743e890499c35bc61aabf01e44cf5bbc3bc"
|
|
17
|
+
|
|
15
18
|
mkdir -p tmp-w3c-tests
|
|
16
19
|
cd tmp-w3c-tests
|
|
17
20
|
|
|
@@ -25,4 +28,9 @@ if [ ! -d rdf-canon ]; then
|
|
|
25
28
|
fi
|
|
26
29
|
(cd rdf-canon && git fetch --depth=1 origin "$RDF_CANON_SHA" && git checkout --detach "$RDF_CANON_SHA")
|
|
27
30
|
|
|
31
|
+
if [ ! -d json-ld-framing ]; then
|
|
32
|
+
git clone --filter=blob:none "$JLD_FRAMING_REPO"
|
|
33
|
+
fi
|
|
34
|
+
(cd json-ld-framing && git fetch --depth=1 origin "$JLD_FRAMING_SHA" && git checkout --detach "$JLD_FRAMING_SHA")
|
|
35
|
+
|
|
28
36
|
echo "W3C suites fetched at pinned SHAs."
|
package/src/jsonld.rs
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
use serde_json::{json, Value};
|
|
2
|
+
use std::collections::HashMap;
|
|
2
3
|
use std::sync::Arc;
|
|
3
4
|
|
|
4
5
|
use vc::{
|
|
5
|
-
|
|
6
|
-
|
|
6
|
+
document_loader::nuggets::DocumentLoader,
|
|
7
|
+
jsonld::{self, context_resolver::ContextResolver},
|
|
7
8
|
};
|
|
8
9
|
|
|
9
|
-
use crate::ld_signatures::
|
|
10
|
+
use crate::ld_signatures::create_document_loader;
|
|
10
11
|
|
|
11
12
|
/// JSON-LD processor — drop-in replacement for the jsonld.js API.
|
|
12
13
|
///
|
|
@@ -20,159 +21,228 @@ use crate::ld_signatures::loader_from_contexts;
|
|
|
20
21
|
/// ```
|
|
21
22
|
#[napi]
|
|
22
23
|
pub struct JsonLd {
|
|
23
|
-
|
|
24
|
-
|
|
24
|
+
loader: Arc<DocumentLoader>,
|
|
25
|
+
cr: Arc<ContextResolver>,
|
|
26
|
+
additional_contexts: Arc<HashMap<String, Value>>,
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
impl JsonLd {
|
|
30
|
+
fn additional_contexts(&self) -> Arc<HashMap<String, Value>> {
|
|
31
|
+
Arc::clone(&self.additional_contexts)
|
|
32
|
+
}
|
|
25
33
|
}
|
|
26
34
|
|
|
27
35
|
#[napi]
|
|
28
36
|
impl JsonLd {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
37
|
+
/// Create a new JSON-LD processor.
|
|
38
|
+
///
|
|
39
|
+
/// Accepts optional `{ contexts?: Record<string, any> }` to register
|
|
40
|
+
/// additional URL → document mappings in the document loader (on top
|
|
41
|
+
/// of the built-in nuggets contexts).
|
|
42
|
+
#[napi(constructor)]
|
|
43
|
+
pub fn new(options: Option<Value>) -> Self {
|
|
44
|
+
let additional_contexts: HashMap<String, Value> = options
|
|
45
|
+
.as_ref()
|
|
46
|
+
.and_then(|o| o.get("contexts"))
|
|
47
|
+
.and_then(|v| v.as_object())
|
|
48
|
+
.map(|obj| obj.iter().map(|(k, v)| (k.clone(), v.clone())).collect())
|
|
49
|
+
.unwrap_or_default();
|
|
50
|
+
let (loader, cr) = create_document_loader(additional_contexts.clone());
|
|
51
|
+
Self {
|
|
52
|
+
loader,
|
|
53
|
+
cr,
|
|
54
|
+
additional_contexts: Arc::new(additional_contexts),
|
|
39
55
|
}
|
|
56
|
+
}
|
|
40
57
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
58
|
+
/// Expand a JSON-LD document.
|
|
59
|
+
///
|
|
60
|
+
/// `options` can include: `base`, `expandContext`, `keepFreeFloatingNodes`,
|
|
61
|
+
/// `processingMode`.
|
|
62
|
+
///
|
|
63
|
+
/// Returns an array of expanded JSON-LD objects.
|
|
64
|
+
#[napi]
|
|
65
|
+
pub async fn expand(&self, input: Value, options: Option<Value>) -> napi::Result<Value> {
|
|
66
|
+
let opts = options.unwrap_or(json!({}));
|
|
67
|
+
if vc::jsonld_v2::use_v2() {
|
|
68
|
+
let additional = self.additional_contexts();
|
|
69
|
+
vc::jsonld_v2::expand_v2_sync(input, &additional, opts)
|
|
70
|
+
.map_err(|e| napi::Error::from_reason(format!("expand (v2) failed: {e}")))
|
|
71
|
+
} else {
|
|
72
|
+
jsonld::expand(input, self.loader.clone(), self.cr.clone(), opts)
|
|
73
|
+
.await
|
|
74
|
+
.map_err(|e| napi::Error::from_reason(format!("expand failed: {e}")))
|
|
57
75
|
}
|
|
76
|
+
}
|
|
58
77
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
78
|
+
/// Compact a JSON-LD document using a context.
|
|
79
|
+
///
|
|
80
|
+
/// `ctx` — the compaction context (object, array, or string URL).
|
|
81
|
+
/// `options` can include: `base`, `compactArrays`, `graph`,
|
|
82
|
+
/// `skipExpansion`, `processingMode`.
|
|
83
|
+
///
|
|
84
|
+
/// Returns the compacted JSON-LD object.
|
|
85
|
+
#[napi]
|
|
86
|
+
pub async fn compact(
|
|
87
|
+
&self,
|
|
88
|
+
input: Value,
|
|
89
|
+
ctx: Value,
|
|
90
|
+
options: Option<Value>,
|
|
91
|
+
) -> napi::Result<Value> {
|
|
92
|
+
let opts = options.unwrap_or(json!({}));
|
|
93
|
+
if vc::jsonld_v2::use_v2() {
|
|
94
|
+
let additional = self.additional_contexts();
|
|
95
|
+
vc::jsonld_v2::compact_v2_sync(input, ctx, &additional, opts)
|
|
96
|
+
.map_err(|e| napi::Error::from_reason(format!("compact (v2) failed: {e}")))
|
|
97
|
+
} else {
|
|
98
|
+
jsonld::compact_api(input, ctx, self.loader.clone(), self.cr.clone(), opts)
|
|
99
|
+
.await
|
|
100
|
+
.map_err(|e| napi::Error::from_reason(format!("compact failed: {e}")))
|
|
77
101
|
}
|
|
102
|
+
}
|
|
78
103
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
104
|
+
/// Flatten a JSON-LD document.
|
|
105
|
+
///
|
|
106
|
+
/// `ctx` — optional context for compacting the flattened result.
|
|
107
|
+
/// Pass `null` to get the flattened expanded form.
|
|
108
|
+
/// `options` can include: `base`, `expandContext`, `processingMode`.
|
|
109
|
+
///
|
|
110
|
+
/// Returns flattened array (no context) or compacted object (with context).
|
|
111
|
+
#[napi]
|
|
112
|
+
pub async fn flatten(
|
|
113
|
+
&self,
|
|
114
|
+
input: Value,
|
|
115
|
+
ctx: Option<Value>,
|
|
116
|
+
options: Option<Value>,
|
|
117
|
+
) -> napi::Result<Value> {
|
|
118
|
+
let opts = options.unwrap_or(json!({}));
|
|
119
|
+
// Filter out null ctx (JS passes null, Rust expects None)
|
|
120
|
+
let ctx = ctx.filter(|v| !v.is_null());
|
|
121
|
+
if vc::jsonld_v2::use_v2() {
|
|
122
|
+
let additional = self.additional_contexts();
|
|
123
|
+
vc::jsonld_v2::flatten_v2_sync(input, ctx, &additional, opts)
|
|
124
|
+
.map_err(|e| napi::Error::from_reason(format!("flatten (v2) failed: {e}")))
|
|
125
|
+
} else {
|
|
126
|
+
jsonld::flatten(input, ctx, self.loader.clone(), self.cr.clone(), opts)
|
|
127
|
+
.await
|
|
128
|
+
.map_err(|e| napi::Error::from_reason(format!("flatten failed: {e}")))
|
|
99
129
|
}
|
|
130
|
+
}
|
|
100
131
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
132
|
+
/// Frame a JSON-LD document.
|
|
133
|
+
///
|
|
134
|
+
/// `frame` — the framing template.
|
|
135
|
+
/// `options` can include: `base`, `embed`, `explicit`, `requireAll`,
|
|
136
|
+
/// `omitDefault`, `omitGraph`, `processingMode`.
|
|
137
|
+
///
|
|
138
|
+
/// Returns the framed JSON-LD object.
|
|
139
|
+
#[napi]
|
|
140
|
+
pub async fn frame(
|
|
141
|
+
&self,
|
|
142
|
+
input: Value,
|
|
143
|
+
frame: Value,
|
|
144
|
+
options: Option<Value>,
|
|
145
|
+
) -> napi::Result<Value> {
|
|
146
|
+
let opts = options.unwrap_or(json!({}));
|
|
147
|
+
if vc::jsonld_v2::use_v2() {
|
|
148
|
+
// Sprint 3 Phase D cutover: V2 frame is always the native typed-tree
|
|
149
|
+
// walker. CLIENTFFI_FRAME_NATIVE / CLIENTFFI_FRAME_FAST flags are no
|
|
150
|
+
// longer consulted — frame_v2 / frame_v2_fast public APIs were
|
|
151
|
+
// retired in this commit.
|
|
152
|
+
let loader = self.loader.clone();
|
|
153
|
+
let cr = self.cr.clone();
|
|
154
|
+
let join = tokio::task::spawn_blocking(move || {
|
|
155
|
+
vc::jsonld_v2::frame_native_sync(input, frame, loader, cr, opts)
|
|
156
|
+
.map_err(|e| napi::Error::from_reason(format!("frame (native) failed: {e}")))
|
|
157
|
+
});
|
|
158
|
+
join
|
|
159
|
+
.await
|
|
160
|
+
.map_err(|e| napi::Error::from_reason(format!("frame (v2) join: {e}")))?
|
|
161
|
+
} else {
|
|
162
|
+
jsonld::frame(input, frame, self.loader.clone(), self.cr.clone(), opts)
|
|
163
|
+
.await
|
|
164
|
+
.map_err(|e| napi::Error::from_reason(format!("frame failed: {e}")))
|
|
119
165
|
}
|
|
166
|
+
}
|
|
120
167
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
168
|
+
/// Convert a JSON-LD document to an RDF dataset.
|
|
169
|
+
///
|
|
170
|
+
/// `options` can include: `base`, `expandContext`, `skipExpansion`,
|
|
171
|
+
/// `format` (`"application/n-quads"` for string output), `processingMode`.
|
|
172
|
+
///
|
|
173
|
+
/// Returns an array of quads or an N-Quads string (depending on `format`).
|
|
174
|
+
#[napi(js_name = "toRDF")]
|
|
175
|
+
pub async fn to_rdf(&self, input: Value, options: Option<Value>) -> napi::Result<Value> {
|
|
176
|
+
let opts = options.unwrap_or(json!({}));
|
|
177
|
+
// v2 only produces N-Quads string output. If caller expects
|
|
178
|
+
// a quads-array (default when format is omitted), fall back to v1.
|
|
179
|
+
let wants_nquads = opts
|
|
180
|
+
.get("format")
|
|
181
|
+
.and_then(|v| v.as_str())
|
|
182
|
+
.map(|s| s == "application/n-quads")
|
|
183
|
+
.unwrap_or(false);
|
|
184
|
+
if vc::jsonld_v2::use_v2() && wants_nquads {
|
|
185
|
+
let additional = self.additional_contexts();
|
|
186
|
+
let nquads = vc::jsonld_v2::to_nquads_v2_sync(input, &additional, opts)
|
|
187
|
+
.map_err(|e| napi::Error::from_reason(format!("toRDF (v2) failed: {e}")))?;
|
|
188
|
+
Ok(Value::String(nquads))
|
|
189
|
+
} else {
|
|
190
|
+
jsonld::to_rdf(input, self.loader.clone(), self.cr.clone(), opts)
|
|
191
|
+
.await
|
|
192
|
+
.map_err(|e| napi::Error::from_reason(format!("toRDF failed: {e}")))
|
|
137
193
|
}
|
|
194
|
+
}
|
|
138
195
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
196
|
+
/// Convert an RDF dataset to a JSON-LD document.
|
|
197
|
+
///
|
|
198
|
+
/// `dataset` — N-Quads string or array of quads.
|
|
199
|
+
/// `options` can include: `useRdfType`, `useNativeTypes`, `rdfDirection`.
|
|
200
|
+
///
|
|
201
|
+
/// Returns an array of JSON-LD objects.
|
|
202
|
+
#[napi(js_name = "fromRDF")]
|
|
203
|
+
pub fn from_rdf(&self, dataset: Value, options: Option<Value>) -> napi::Result<Value> {
|
|
204
|
+
let opts = options.unwrap_or(json!({}));
|
|
205
|
+
jsonld::from_rdf_api(dataset, opts)
|
|
206
|
+
.map_err(|e| napi::Error::from_reason(format!("fromRDF failed: {e}")))
|
|
207
|
+
}
|
|
151
208
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
opts["algorithm"] = json!("RDFC-1.0");
|
|
169
|
-
}
|
|
170
|
-
if opts.get("format").is_none() {
|
|
171
|
-
opts["format"] = json!("application/n-quads");
|
|
172
|
-
}
|
|
173
|
-
let result = jsonld::canonize(input, self.loader.clone(), self.cr.clone(), opts)
|
|
174
|
-
.await
|
|
175
|
-
.map_err(|e| napi::Error::from_reason(format!("canonize failed: {e}")))?;
|
|
176
|
-
Ok(Value::String(result))
|
|
209
|
+
/// Canonize (normalize) a JSON-LD document.
|
|
210
|
+
///
|
|
211
|
+
/// `options` can include: `base`, `expandContext`, `skipExpansion`,
|
|
212
|
+
/// `inputFormat` (`"application/n-quads"`), `format`, `algorithm`,
|
|
213
|
+
/// `processingMode`.
|
|
214
|
+
///
|
|
215
|
+
/// Returns the canonical N-Quads string.
|
|
216
|
+
#[napi]
|
|
217
|
+
pub async fn canonize(&self, input: Value, options: Option<Value>) -> napi::Result<Value> {
|
|
218
|
+
let mut opts = options.unwrap_or(json!({}));
|
|
219
|
+
// Defaults matching JS jsonld.js canonize()
|
|
220
|
+
if opts.get("algorithm").is_none() {
|
|
221
|
+
opts["algorithm"] = json!("RDFC-1.0");
|
|
222
|
+
}
|
|
223
|
+
if opts.get("format").is_none() {
|
|
224
|
+
opts["format"] = json!("application/n-quads");
|
|
177
225
|
}
|
|
226
|
+
let input_is_nquads = opts
|
|
227
|
+
.get("inputFormat")
|
|
228
|
+
.and_then(|v| v.as_str())
|
|
229
|
+
.map(|s| s == "application/n-quads")
|
|
230
|
+
.unwrap_or(false);
|
|
231
|
+
let result = if vc::jsonld_v2::use_v2() && !input_is_nquads {
|
|
232
|
+
let additional = self.additional_contexts();
|
|
233
|
+
vc::jsonld_v2::canonize_v2_sync(
|
|
234
|
+
input,
|
|
235
|
+
&additional,
|
|
236
|
+
self.loader.clone(),
|
|
237
|
+
self.cr.clone(),
|
|
238
|
+
opts,
|
|
239
|
+
)
|
|
240
|
+
.map_err(|e| napi::Error::from_reason(format!("canonize (v2) failed: {e}")))?
|
|
241
|
+
} else {
|
|
242
|
+
jsonld::canonize(input, self.loader.clone(), self.cr.clone(), opts)
|
|
243
|
+
.await
|
|
244
|
+
.map_err(|e| napi::Error::from_reason(format!("canonize failed: {e}")))?
|
|
245
|
+
};
|
|
246
|
+
Ok(Value::String(result))
|
|
247
|
+
}
|
|
178
248
|
}
|
|
@@ -135,7 +135,10 @@ test('cross-verify: flatten(inputDocument, null) matches JS reference', async ()
|
|
|
135
135
|
const napiResult = await proc.flatten(inputDocument, null);
|
|
136
136
|
const jsResult = await jsonld.flatten(inputDocument, null, { documentLoader });
|
|
137
137
|
|
|
138
|
-
|
|
138
|
+
// Flatten without compaction returns an unordered set of node objects
|
|
139
|
+
// (each keyed by @id). Sort by @id before comparing.
|
|
140
|
+
const sortById = arr => [...arr].sort((a, b) => String(a['@id'] || '').localeCompare(String(b['@id'] || '')));
|
|
141
|
+
assert.deepStrictEqual(sortById(napiResult), sortById(jsResult));
|
|
139
142
|
});
|
|
140
143
|
|
|
141
144
|
|
|
@@ -171,7 +174,10 @@ test('cross-verify: toRDF(inputDocument, n-quads) matches JS reference', async (
|
|
|
171
174
|
const napiResult = await proc.toRDF(inputDocument, { format: 'application/n-quads' });
|
|
172
175
|
const jsResult = await jsonld.toRDF(inputDocument, { documentLoader, format: 'application/n-quads' });
|
|
173
176
|
|
|
174
|
-
|
|
177
|
+
// toRDF triple order is implementation-defined (RDFC-1.0 normalises it later).
|
|
178
|
+
// Compare as a set: split into lines, drop empties, sort.
|
|
179
|
+
const norm = s => s.split('\n').filter(l => l.length).sort().join('\n');
|
|
180
|
+
assert.strictEqual(norm(napiResult), norm(jsResult));
|
|
175
181
|
});
|
|
176
182
|
|
|
177
183
|
|
package/test_w3c_conformance.mjs
CHANGED
|
@@ -32,6 +32,7 @@ const updateBaseline = flag('--update');
|
|
|
32
32
|
|
|
33
33
|
const TESTS_ROOT = resolve(__dirname, 'tmp-w3c-tests');
|
|
34
34
|
const JLD_TESTS = join(TESTS_ROOT, 'json-ld-api', 'tests');
|
|
35
|
+
const JLD_FRAMING_TESTS = join(TESTS_ROOT, 'json-ld-framing', 'tests');
|
|
35
36
|
const RDFC_TESTS = join(TESTS_ROOT, 'rdf-canon', 'tests');
|
|
36
37
|
const JLD_BASE = 'https://w3c.github.io/json-ld-api/tests/';
|
|
37
38
|
const BASELINE_PATH = join(__dirname, 'w3c-baseline.json');
|
|
@@ -70,6 +71,7 @@ function preloadJldContexts() {
|
|
|
70
71
|
}
|
|
71
72
|
}
|
|
72
73
|
walk(JLD_TESTS, '');
|
|
74
|
+
if (existsSync(JLD_FRAMING_TESTS)) walk(JLD_FRAMING_TESTS, '');
|
|
73
75
|
return ctxs;
|
|
74
76
|
}
|
|
75
77
|
|
|
@@ -84,6 +86,25 @@ const normNQuads = (s) => typeof s === 'string'
|
|
|
84
86
|
? s.split('\n').map(l => l.trim()).filter(Boolean).sort().join('\n')
|
|
85
87
|
: s;
|
|
86
88
|
|
|
89
|
+
// W3C toRdf expected fixtures contain post-URDNA2015 blank-node labels
|
|
90
|
+
// (e.g. `_:c14n0`), but the spec itself defines toRdf as producing
|
|
91
|
+
// pre-canonical labels (e.g. `_:b0`). jsonld.js exhibits the same
|
|
92
|
+
// pre-canonical output. To compare semantically we run URDNA2015 on both
|
|
93
|
+
// sides before string-matching. Falls back to normNQuads if either side
|
|
94
|
+
// can't be canonicalized (so a bug in the processor still surfaces).
|
|
95
|
+
const canonProc = new JsonLd();
|
|
96
|
+
async function nquadsEqual(got, expected) {
|
|
97
|
+
if (typeof got !== 'string' || typeof expected !== 'string') return false;
|
|
98
|
+
try {
|
|
99
|
+
const opts = { algorithm: 'URDNA2015', inputFormat: 'application/n-quads', format: 'application/n-quads' };
|
|
100
|
+
const a = await canonProc.canonize(got, opts);
|
|
101
|
+
const b = await canonProc.canonize(expected, opts);
|
|
102
|
+
return a === b;
|
|
103
|
+
} catch {
|
|
104
|
+
return normNQuads(got) === normNQuads(expected);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
87
108
|
const DENYLIST_PATH = join(__dirname, 'w3c-denylist.json');
|
|
88
109
|
let DENYLIST = {};
|
|
89
110
|
if (existsSync(DENYLIST_PATH)) {
|
|
@@ -144,10 +165,18 @@ async function runJld(algo, methodName) {
|
|
|
144
165
|
const opt = entry.option || {};
|
|
145
166
|
const callOpts = { base: opt.base || inputUrl };
|
|
146
167
|
if (opt.processingMode) callOpts.processingMode = opt.processingMode;
|
|
168
|
+
else if (opt.specVersion) callOpts.processingMode = opt.specVersion;
|
|
147
169
|
if (opt.expandContext) {
|
|
148
170
|
const ctxUrl = JLD_BASE + opt.expandContext;
|
|
149
171
|
callOpts.expandContext = contexts[ctxUrl] || ctxUrl;
|
|
150
172
|
}
|
|
173
|
+
// toRdf-specific options from W3C test entries
|
|
174
|
+
if (opt.rdfDirection) callOpts.rdfDirection = opt.rdfDirection;
|
|
175
|
+
if (opt.produceGeneralizedRdf != null) callOpts.produceGeneralizedRdf = opt.produceGeneralizedRdf;
|
|
176
|
+
if (opt.compactArrays != null) callOpts.compactArrays = opt.compactArrays;
|
|
177
|
+
// fromRDF-specific options
|
|
178
|
+
if (opt.useNativeTypes != null) callOpts.useNativeTypes = opt.useNativeTypes;
|
|
179
|
+
if (opt.useRdfType != null) callOpts.useRdfType = opt.useRdfType;
|
|
151
180
|
|
|
152
181
|
try {
|
|
153
182
|
const input = methodName === 'fromRDF' ? loadText(inputPath) : loadJson(inputPath);
|
|
@@ -172,7 +201,7 @@ async function runJld(algo, methodName) {
|
|
|
172
201
|
const expected = methodName === 'toRDF' ? loadText(expectPath) : loadJson(expectPath);
|
|
173
202
|
|
|
174
203
|
const ok = methodName === 'toRDF'
|
|
175
|
-
?
|
|
204
|
+
? await nquadsEqual(napiResult, expected)
|
|
176
205
|
: jsonEqual(napiResult, expected);
|
|
177
206
|
record(algo, id, ok ? 'pass' : 'fail');
|
|
178
207
|
} catch (e) {
|
|
@@ -229,20 +258,64 @@ async function runOnce(algo) {
|
|
|
229
258
|
else if (algo === 'flatten') await runJld('flatten', 'flatten');
|
|
230
259
|
else if (algo === 'toRdf') await runJld('toRdf', 'toRDF');
|
|
231
260
|
else if (algo === 'fromRdf') await runJld('fromRdf', 'fromRDF');
|
|
261
|
+
else if (algo === 'frame') await runFraming();
|
|
232
262
|
}
|
|
233
263
|
|
|
264
|
+
async function runFraming() {
|
|
265
|
+
const manifestPath = join(JLD_FRAMING_TESTS, 'frame-manifest.jsonld');
|
|
266
|
+
if (!existsSync(manifestPath)) return;
|
|
267
|
+
const manifest = loadJson(manifestPath);
|
|
268
|
+
const contexts = preloadJldContexts();
|
|
269
|
+
const proc = new JsonLd({ contexts });
|
|
270
|
+
let seen = 0;
|
|
271
|
+
for (const entry of manifest.sequence) {
|
|
272
|
+
const types = Array.isArray(entry['@type']) ? entry['@type'] : [entry['@type']];
|
|
273
|
+
if (!types.includes('jld:PositiveEvaluationTest') && !types.includes('jld:NegativeEvaluationTest')) continue;
|
|
274
|
+
const id = entry['@id'];
|
|
275
|
+
if (isDenied('frame', id)) continue;
|
|
276
|
+
if (!isInCheckScope('frame', id)) continue;
|
|
277
|
+
seen += 1;
|
|
278
|
+
if (seen % PROGRESS_EVERY === 0) process.stderr.write(` frame: ${seen} tests processed\n`);
|
|
279
|
+
if (!entry.input || !entry.frame) { record('frame', id, 'err'); continue; }
|
|
280
|
+
const inputPath = join(JLD_FRAMING_TESTS, entry.input);
|
|
281
|
+
const framePath = join(JLD_FRAMING_TESTS, entry.frame);
|
|
282
|
+
const expectPath = entry.expect ? join(JLD_FRAMING_TESTS, entry.expect) : null;
|
|
283
|
+
if (!existsSync(inputPath) || !existsSync(framePath)) { record('frame', id, 'err'); continue; }
|
|
284
|
+
const opt = entry.option || {};
|
|
285
|
+
const inputUrl = 'https://w3c.github.io/json-ld-framing/tests/' + entry.input;
|
|
286
|
+
const callOpts = { base: opt.base || inputUrl };
|
|
287
|
+
if (opt.processingMode) callOpts.processingMode = opt.processingMode;
|
|
288
|
+
else if (opt.specVersion) callOpts.processingMode = opt.specVersion;
|
|
289
|
+
if (opt.omitGraph != null) callOpts.omitGraph = opt.omitGraph;
|
|
290
|
+
if (opt.requireAll != null) callOpts.requireAll = opt.requireAll;
|
|
291
|
+
try {
|
|
292
|
+
const input = loadJson(inputPath);
|
|
293
|
+
const frame = loadJson(framePath);
|
|
294
|
+
const napiResult = await withTimeout(proc.frame(input, frame, callOpts), id);
|
|
295
|
+
if (!expectPath || !existsSync(expectPath)) { record('frame', id, 'err'); continue; }
|
|
296
|
+
const expected = loadJson(expectPath);
|
|
297
|
+
record('frame', id, jsonEqual(napiResult, expected) ? 'pass' : 'fail');
|
|
298
|
+
} catch (e) {
|
|
299
|
+
const isTimeout = e && /^timeout:/.test(e.message || '');
|
|
300
|
+
record('frame', id, isTimeout ? 'err' : 'err');
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
|
|
234
306
|
async function main() {
|
|
235
307
|
if (!existsSync(JLD_TESTS) || !existsSync(RDFC_TESTS)) {
|
|
236
308
|
console.error('W3C test suites not found at ' + TESTS_ROOT);
|
|
237
309
|
console.error('Run: vc/js/scripts/fetch-w3c-tests.sh');
|
|
238
310
|
process.exit(2);
|
|
239
311
|
}
|
|
240
|
-
// flatten and toRdf are
|
|
241
|
-
//
|
|
242
|
-
//
|
|
243
|
-
//
|
|
244
|
-
|
|
245
|
-
const
|
|
312
|
+
// flatten and toRdf are gated by V2: the v1 path has Rust-level deadlocks
|
|
313
|
+
// (sync block_on bridging into tokio::sync locks in the document loader and
|
|
314
|
+
// node_map). The v2 path (jsonld_v2::flatten_v2 / to_nquads_v2) uses a
|
|
315
|
+
// pre-resolved HashMap loader with no tokio sync primitives and runs the
|
|
316
|
+
// suite without hangs, so it is included in the default list.
|
|
317
|
+
const algos = ['canonize', 'fromRdf', 'expand', 'compact', 'flatten', 'toRdf', 'frame'];
|
|
318
|
+
const allAlgos = ['expand', 'compact', 'flatten', 'toRdf', 'fromRdf', 'canonize', 'frame'];
|
|
246
319
|
if (onlyArg && !allAlgos.includes(onlyArg)) {
|
|
247
320
|
console.error(`Invalid --only value: ${onlyArg}. Expected one of: ${allAlgos.join(', ')}`);
|
|
248
321
|
process.exit(2);
|