Skip to content

Commit 183f071

Browse files
committed
Add more pointer conversion code and tests.
1 parent 76249bc commit 183f071

File tree

3 files changed

+259
-8
lines changed

3 files changed

+259
-8
lines changed

lib/query/dcql.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,12 +39,14 @@ export function _fromQueryByExampleQuery({
3939

4040
// convert `example` into json pointers and walk to produce DCQL claim paths
4141
const pointers = toJsonPointerMap({obj: example, flat: true});
42+
4243
const pathsMap = new Map();
4344
for(const [pointer, value] of pointers) {
4445
let path = jsonpointer.parse(pointer);
4546
const isContext = path[0] === '@context';
4647

47-
if(!isContext && isNumber(path.at(-1))) {
48+
// check for array value that is not in an array
49+
if(!isContext && isNumber(path.at(-1)) && !isNumber(path.at(-2))) {
4850
// pointer terminates at an array element which means candidate matching
4951
// values are expressed; make sure to share the path for all candidates
5052
path.pop();
@@ -67,7 +69,7 @@ export function _fromQueryByExampleQuery({
6769
}
6870

6971
// add any non-QueryByExample-wildcard as a DCQL match value
70-
if(!(value === '' || value instanceof Map || Array.isArray(value))) {
72+
if(!(value === '' || value instanceof Map || value instanceof Set)) {
7173
entry.valueSet.add(value);
7274
}
7375
}

lib/util.js

Lines changed: 42 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@ const ENCODED_PERIOD = TEXT_ENCODER.encode('.');
1010

1111
export function assert(x, name, type, optional = false) {
1212
const article = type === 'object' ? 'an' : 'a';
13-
if(x !== undefined && typeof x !== type) {
13+
const xType = (type === 'Map' || type === 'Set') ?
14+
(x instanceof type && type) : typeof x;
15+
if(x !== undefined && xType !== type) {
1416
throw new TypeError(
1517
`${optional ? 'When present, ' : ''} ` +
1618
`"${name}" must be ${article} ${type}.`);
@@ -155,26 +157,60 @@ export async function signJWT({payload, protectedHeader, signer} = {}) {
155157
return `${jws.protected}.${jws.payload}.${jws.signature}`;
156158
}
157159

160+
export function _fromJsonPointerMap({map} = {}) {
161+
assert(map, 'map', 'Map');
162+
return _fromPointers({map});
163+
}
164+
158165
// produces a map of deep pointers to primitives and sets; the values in each
159166
// set share the same pointer value and if any value in the set is an object,
160167
// it becomes a new map of deep pointers from that starting place; the pointer
161168
// value for an empty objects will be an empty map
162169
export function toJsonPointerMap({obj, flat = false} = {}) {
163170
assert(obj, 'obj', 'object');
164-
return _nextPointers({cursor: obj, map: new Map(), flat});
171+
return _toPointers({cursor: obj, map: new Map(), flat});
172+
}
173+
174+
export function _fromPointers({map} = {}) {
175+
const result = {};
176+
177+
for(const [pointer, value] of map) {
178+
// convert any non-primitive values
179+
let val = value;
180+
if(value instanceof Map) {
181+
val = _fromPointers({map: value});
182+
} else if(value instanceof Set) {
183+
if(value.size === 0) {
184+
val = [];
185+
} else {
186+
val = [...value].map(map => _fromPointers({map}));
187+
}
188+
}
189+
190+
// if root pointer is used, `value` is result
191+
if(pointer === '/') {
192+
return val;
193+
}
194+
195+
jsonpointer.set(result, pointer, val);
196+
}
197+
198+
return result;
165199
}
166200

167-
function _nextPointers({
201+
function _toPointers({
168202
cursor, map, tokens = [], pointer = '/', flat = false
169203
}) {
170204
if(!flat && Array.isArray(cursor)) {
171205
const set = new Set();
206+
// when `map` is not set, case is array of arrays; return a new map
207+
const result = map ? set : (map = new Map());
172208
map.set(pointer, set);
173209
for(const element of cursor) {
174210
// reset map, tokens, and pointer for array elements
175-
set.add(_nextPointers({cursor: element, flat}));
211+
set.add(_toPointers({cursor: element, flat}));
176212
}
177-
return set;
213+
return result;
178214
}
179215
if(cursor !== null && typeof cursor === 'object') {
180216
map = map ?? new Map();
@@ -186,7 +222,7 @@ function _nextPointers({
186222
for(const [token, value] of entries) {
187223
tokens.push(String(token));
188224
pointer = jsonpointer.compile(tokens);
189-
_nextPointers({cursor: value, map, tokens, pointer, flat});
225+
_toPointers({cursor: value, map, tokens, pointer, flat});
190226
tokens.pop();
191227
}
192228
return map;

tests/unit/query.dcql.spec.js

Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,4 +133,217 @@ describe('query.dcql', () => {
133133
]);
134134
});
135135
});
136+
it('should process array of arrays query', async () => {
137+
const dcqlCredentialQuery = _fromQueryByExampleQuery({
138+
credentialQuery: {
139+
reason: 'Present your geolocation credential to claim the prize.',
140+
example: {
141+
'@context': [
142+
'https://www.w3.org/ns/credentials/v2',
143+
'https://www.w3.org/ns/credentials/examples/v2'
144+
],
145+
type: [
146+
'ExampleGeoLocationCredential'
147+
],
148+
credentialSubject: {
149+
type: 'ExampleGeoLocation',
150+
location: [[0, 1], [2, 3]]
151+
}
152+
}
153+
}
154+
});
155+
expect(dcqlCredentialQuery.id).to.exist;
156+
expect(dcqlCredentialQuery.format).to.eql('ldp_vc');
157+
expect(dcqlCredentialQuery.meta.type_values).to.deep.equal([
158+
'https://www.w3.org/2018/credentials#VerifiableCredential'
159+
]);
160+
expect(dcqlCredentialQuery.claims).to.deep.equal([
161+
{
162+
path: ['@context', 0],
163+
values: ['https://www.w3.org/ns/credentials/v2']
164+
},
165+
{
166+
path: ['@context', 1],
167+
values: ['https://www.w3.org/ns/credentials/examples/v2']
168+
},
169+
{
170+
path: ['type'],
171+
values: ['ExampleGeoLocationCredential']},
172+
{
173+
path: ['credentialSubject', 'type'],
174+
values: ['ExampleGeoLocation']
175+
},
176+
{
177+
path: ['credentialSubject', 'location', 0, 0],
178+
values: [0]
179+
},
180+
{
181+
path: ['credentialSubject', 'location', 0, 1],
182+
values: [1]
183+
},
184+
{
185+
path: ['credentialSubject', 'location', 1, 0],
186+
values: [2]
187+
},
188+
{
189+
path: ['credentialSubject', 'location', 1, 1],
190+
values: [3]
191+
}
192+
]);
193+
});
194+
it('should process array of arrays of arrays query', async () => {
195+
const dcqlCredentialQuery = _fromQueryByExampleQuery({
196+
credentialQuery: {
197+
reason: 'Present your tensor credential to claim the prize.',
198+
example: {
199+
'@context': [
200+
'https://www.w3.org/ns/credentials/v2',
201+
'https://www.w3.org/ns/credentials/examples/v2'
202+
],
203+
type: [
204+
'ExampleTensorCredential'
205+
],
206+
credentialSubject: {
207+
type: 'ExampleTensor',
208+
tensor: [[[0, 1], [2, 3]], [[4, 5], [6, 7]]]
209+
}
210+
}
211+
}
212+
});
213+
expect(dcqlCredentialQuery.id).to.exist;
214+
expect(dcqlCredentialQuery.format).to.eql('ldp_vc');
215+
expect(dcqlCredentialQuery.meta.type_values).to.deep.equal([
216+
'https://www.w3.org/2018/credentials#VerifiableCredential'
217+
]);
218+
expect(dcqlCredentialQuery.claims).to.deep.equal([
219+
{
220+
path: ['@context', 0],
221+
values: ['https://www.w3.org/ns/credentials/v2']
222+
},
223+
{
224+
path: ['@context', 1],
225+
values: ['https://www.w3.org/ns/credentials/examples/v2']
226+
},
227+
{
228+
path: ['type'],
229+
values: ['ExampleTensorCredential']},
230+
{
231+
path: ['credentialSubject', 'type'],
232+
values: ['ExampleTensor']
233+
},
234+
{
235+
path: ['credentialSubject', 'tensor', 0, 0, 0],
236+
values: [0]
237+
},
238+
{
239+
path: ['credentialSubject', 'tensor', 0, 0, 1],
240+
values: [1]
241+
},
242+
{
243+
path: ['credentialSubject', 'tensor', 0, 1, 0],
244+
values: [2]
245+
},
246+
{
247+
path: ['credentialSubject', 'tensor', 0, 1, 1],
248+
values: [3]
249+
},
250+
{
251+
path: ['credentialSubject', 'tensor', 1, 0, 0],
252+
values: [4]
253+
},
254+
{
255+
path: ['credentialSubject', 'tensor', 1, 0, 1],
256+
values: [5]
257+
},
258+
{
259+
path: ['credentialSubject', 'tensor', 1, 1, 0],
260+
values: [6]
261+
},
262+
{
263+
path: ['credentialSubject', 'tensor', 1, 1, 1],
264+
values: [7]
265+
}
266+
]);
267+
});
268+
it.only('should process array of arrays inside objects query', async () => {
269+
const dcqlCredentialQuery = _fromQueryByExampleQuery({
270+
credentialQuery: {
271+
reason: 'Present your dimensions credential to claim the prize.',
272+
example: {
273+
'@context': [
274+
'https://www.w3.org/ns/credentials/v2',
275+
'https://www.w3.org/ns/credentials/examples/v2'
276+
],
277+
type: [
278+
'ExampleDimensionsCredential'
279+
],
280+
credentialSubject: {
281+
type: 'ExampleDimensions',
282+
outer: [
283+
[{
284+
inner: [[0, 1], [2, 3]]
285+
}],
286+
[{
287+
inner: [[4, 5], [6, 7]]
288+
}]
289+
]
290+
}
291+
}
292+
}
293+
});
294+
expect(dcqlCredentialQuery.id).to.exist;
295+
expect(dcqlCredentialQuery.format).to.eql('ldp_vc');
296+
expect(dcqlCredentialQuery.meta.type_values).to.deep.equal([
297+
'https://www.w3.org/2018/credentials#VerifiableCredential'
298+
]);
299+
expect(dcqlCredentialQuery.claims).to.deep.equal([
300+
{
301+
path: ['@context', 0],
302+
values: ['https://www.w3.org/ns/credentials/v2']
303+
},
304+
{
305+
path: ['@context', 1],
306+
values: ['https://www.w3.org/ns/credentials/examples/v2']
307+
},
308+
{
309+
path: ['type'],
310+
values: ['ExampleDimensionsCredential']},
311+
{
312+
path: ['credentialSubject', 'type'],
313+
values: ['ExampleDimensions']
314+
},
315+
{
316+
path: ['credentialSubject', 'outer', 0, 0, 'inner', 0, 0],
317+
values: [0]
318+
},
319+
{
320+
path: ['credentialSubject', 'outer', 0, 0, 'inner', 0, 1],
321+
values: [1]
322+
},
323+
{
324+
path: ['credentialSubject', 'outer', 0, 0, 'inner', 1, 0],
325+
values: [2]
326+
},
327+
{
328+
path: ['credentialSubject', 'outer', 0, 0, 'inner', 1, 1],
329+
values: [3]
330+
},
331+
{
332+
path: ['credentialSubject', 'outer', 1, 0, 'inner', 0, 0],
333+
values: [4]
334+
},
335+
{
336+
path: ['credentialSubject', 'outer', 1, 0, 'inner', 0, 1],
337+
values: [5]
338+
},
339+
{
340+
path: ['credentialSubject', 'outer', 1, 0, 'inner', 1, 0],
341+
values: [6]
342+
},
343+
{
344+
path: ['credentialSubject', 'outer', 1, 0, 'inner', 1, 1],
345+
values: [7]
346+
}
347+
]);
348+
});
136349
});

0 commit comments

Comments
 (0)