@nuggetslife/vc 0.0.29 → 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 +48 -0
- package/bench/frame_compare.mjs +203 -0
- package/bench/v2_internals.mjs +115 -0
- package/bench/vc_ops.mjs +308 -0
- package/package.json +10 -7
- package/scripts/fetch-w3c-tests.sh +36 -0
- package/src/jsonld.rs +210 -140
- package/test_jsonld_crossverify.mjs +110 -2
- package/test_w3c_conformance.mjs +419 -0
- package/w3c-baseline.json +1398 -0
- package/w3c-denylist.json +14 -0
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": {
|
|
@@ -34,15 +34,18 @@
|
|
|
34
34
|
"prepublishOnly": "napi prepublish -t npm",
|
|
35
35
|
"test": "node test.mjs && node test_jose.mjs && node test_sd_jwt.mjs && node test_bbs_ietf.mjs && node test_bbs_2023.mjs && node test_jsonld_crossverify.mjs && node test_backward_compat.mjs",
|
|
36
36
|
"universal": "napi universal",
|
|
37
|
-
"version": "napi version"
|
|
37
|
+
"version": "napi version",
|
|
38
|
+
"test:w3c": "node test_w3c_conformance.mjs --check",
|
|
39
|
+
"test:w3c:update": "node test_w3c_conformance.mjs --update",
|
|
40
|
+
"fetch:w3c-tests": "bash scripts/fetch-w3c-tests.sh"
|
|
38
41
|
},
|
|
39
42
|
"packageManager": "yarn@4.3.1",
|
|
40
43
|
"optionalDependencies": {
|
|
41
|
-
"@nuggetslife/vc-darwin-arm64": "0.0
|
|
42
|
-
"@nuggetslife/vc-linux-arm64-gnu": "0.0
|
|
43
|
-
"@nuggetslife/vc-linux-arm64-musl": "0.0
|
|
44
|
-
"@nuggetslife/vc-linux-x64-gnu": "0.0
|
|
45
|
-
"@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"
|
|
46
49
|
},
|
|
47
50
|
"dependencies": {}
|
|
48
51
|
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Clone the W3C JSON-LD 1.1 API and RDF Dataset Canonicalization test suites
|
|
3
|
+
# into vc/js/tmp-w3c-tests/. Pinned to specific commits for reproducibility.
|
|
4
|
+
|
|
5
|
+
set -euo pipefail
|
|
6
|
+
|
|
7
|
+
cd "$(dirname "$0")/.."
|
|
8
|
+
|
|
9
|
+
JLD_API_REPO="https://github.com/w3c/json-ld-api.git"
|
|
10
|
+
JLD_API_SHA="04a4eb7dc7cbc313f3f5be7ad9a3b06e87741693"
|
|
11
|
+
|
|
12
|
+
RDF_CANON_REPO="https://github.com/w3c/rdf-canon.git"
|
|
13
|
+
RDF_CANON_SHA="15619df2fda7a4ca88308733789b6774517f9638"
|
|
14
|
+
|
|
15
|
+
JLD_FRAMING_REPO="https://github.com/w3c/json-ld-framing.git"
|
|
16
|
+
JLD_FRAMING_SHA="fa228743e890499c35bc61aabf01e44cf5bbc3bc"
|
|
17
|
+
|
|
18
|
+
mkdir -p tmp-w3c-tests
|
|
19
|
+
cd tmp-w3c-tests
|
|
20
|
+
|
|
21
|
+
if [ ! -d json-ld-api ]; then
|
|
22
|
+
git clone --filter=blob:none "$JLD_API_REPO"
|
|
23
|
+
fi
|
|
24
|
+
(cd json-ld-api && git fetch --depth=1 origin "$JLD_API_SHA" && git checkout --detach "$JLD_API_SHA")
|
|
25
|
+
|
|
26
|
+
if [ ! -d rdf-canon ]; then
|
|
27
|
+
git clone --filter=blob:none "$RDF_CANON_REPO"
|
|
28
|
+
fi
|
|
29
|
+
(cd rdf-canon && git fetch --depth=1 origin "$RDF_CANON_SHA" && git checkout --detach "$RDF_CANON_SHA")
|
|
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
|
+
|
|
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
|
|
|
@@ -328,3 +334,105 @@ test('cross-verify: canonize preserves nested typed objects without @id', async
|
|
|
328
334
|
|
|
329
335
|
assert.strictEqual(napiResult, jsResult);
|
|
330
336
|
});
|
|
337
|
+
|
|
338
|
+
//
|
|
339
|
+
// Regression: terms with @type: "@json" must produce JSON literal quads
|
|
340
|
+
// (parity with jsonld@3.1.0 / jsonld@8.3.3 — the libs that sign Nuggets VCs)
|
|
341
|
+
//
|
|
342
|
+
|
|
343
|
+
test('cross-verify: expand emits JSON literal for term with @type: @json (flat context)', async () => {
|
|
344
|
+
const doc = {
|
|
345
|
+
'@context': {
|
|
346
|
+
'@version': 1.1,
|
|
347
|
+
inner: { '@id': 'https://example.com/inner', '@type': '@json' }
|
|
348
|
+
},
|
|
349
|
+
inner: { hello: 'world' }
|
|
350
|
+
};
|
|
351
|
+
|
|
352
|
+
const proc = new JsonLd();
|
|
353
|
+
const napiResult = await proc.expand(doc);
|
|
354
|
+
const jsResult = await jsonld.expand(doc);
|
|
355
|
+
|
|
356
|
+
assert.deepStrictEqual(napiResult, jsResult);
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
test('cross-verify: expand emits JSON literal for term with @type: @json (term-scoped context)', async () => {
|
|
360
|
+
// Mirrors the KYB v1 shape where `dnbData` is defined inside `result`'s
|
|
361
|
+
// term-scoped context. See packages/document-loader/context/kybV1.json.
|
|
362
|
+
const doc = {
|
|
363
|
+
'@context': {
|
|
364
|
+
'@version': 1.1,
|
|
365
|
+
outer: {
|
|
366
|
+
'@id': 'https://example.com/outer',
|
|
367
|
+
'@context': {
|
|
368
|
+
'@version': 1.1,
|
|
369
|
+
inner: { '@id': 'https://example.com/inner', '@type': '@json' }
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
},
|
|
373
|
+
outer: { inner: { organization: { primaryName: 'Acme' } } }
|
|
374
|
+
};
|
|
375
|
+
|
|
376
|
+
const proc = new JsonLd();
|
|
377
|
+
const napiResult = await proc.expand(doc);
|
|
378
|
+
const jsResult = await jsonld.expand(doc);
|
|
379
|
+
|
|
380
|
+
assert.deepStrictEqual(napiResult, jsResult);
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
test('cross-verify: canonize emits JSON literal quad for term with @type: @json', async () => {
|
|
384
|
+
const doc = {
|
|
385
|
+
'@context': {
|
|
386
|
+
'@version': 1.1,
|
|
387
|
+
inner: { '@id': 'https://example.com/inner', '@type': '@json' }
|
|
388
|
+
},
|
|
389
|
+
inner: { b: 2, a: 1 }
|
|
390
|
+
};
|
|
391
|
+
|
|
392
|
+
const proc = new JsonLd();
|
|
393
|
+
const napiResult = await proc.canonize(doc);
|
|
394
|
+
const jsResult = await jsonld.canonize(doc, {
|
|
395
|
+
algorithm: 'URDNA2015',
|
|
396
|
+
format: 'application/n-quads',
|
|
397
|
+
safe: false,
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
assert.strictEqual(napiResult, jsResult);
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
//
|
|
404
|
+
// Regression: blank-node objects (no @id) in arrays must each produce a
|
|
405
|
+
// distinct quad. Earlier `add_value` deduped equal serde Values, collapsing
|
|
406
|
+
// `[{}, {}, {}]` to a single blank node and silently shrinking signed
|
|
407
|
+
// message sets — breaking any BBS+ proof over arrays that contain repeats.
|
|
408
|
+
//
|
|
409
|
+
|
|
410
|
+
test('cross-verify: array of empty objects keeps each blank node distinct', async () => {
|
|
411
|
+
const doc = {
|
|
412
|
+
'@context': { items: { '@id': 'https://example.com/items' } },
|
|
413
|
+
items: [{}, {}, {}]
|
|
414
|
+
};
|
|
415
|
+
|
|
416
|
+
const proc = new JsonLd();
|
|
417
|
+
const napiResult = await proc.expand(doc);
|
|
418
|
+
const jsResult = await jsonld.expand(doc);
|
|
419
|
+
|
|
420
|
+
assert.deepStrictEqual(napiResult, jsResult);
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
test('cross-verify: toRDF with array of empty objects emits one quad per element', async () => {
|
|
424
|
+
const doc = {
|
|
425
|
+
'@context': { items: { '@id': 'https://example.com/items' } },
|
|
426
|
+
items: [{}, {}, 'a', {}]
|
|
427
|
+
};
|
|
428
|
+
|
|
429
|
+
const proc = new JsonLd();
|
|
430
|
+
const napiNquads = await proc.toRDF(doc, { format: 'application/n-quads' });
|
|
431
|
+
const jsNquads = await jsonld.toRDF(doc, { format: 'application/n-quads' });
|
|
432
|
+
|
|
433
|
+
const napiCount = napiNquads.split('\n').filter(Boolean).length;
|
|
434
|
+
const jsCount = jsNquads.split('\n').filter(Boolean).length;
|
|
435
|
+
|
|
436
|
+
assert.strictEqual(napiCount, jsCount,
|
|
437
|
+
`expected ${jsCount} quads (one per array element), got ${napiCount}`);
|
|
438
|
+
});
|