@noeldemartin/solid-utils 0.1.1 → 0.2.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/.nvmrc +1 -0
- package/.semaphore/semaphore.yml +1 -1
- package/dist/noeldemartin-solid-utils.cjs.js +1 -1
- package/dist/noeldemartin-solid-utils.cjs.js.map +1 -1
- package/dist/noeldemartin-solid-utils.d.ts +94 -16
- package/dist/noeldemartin-solid-utils.esm.js +1 -1
- package/dist/noeldemartin-solid-utils.esm.js.map +1 -1
- package/dist/noeldemartin-solid-utils.umd.js +90 -0
- package/dist/noeldemartin-solid-utils.umd.js.map +1 -0
- package/noeldemartin.config.js +4 -2
- package/package.json +11 -11
- package/src/errors/MalformedSolidDocumentError.ts +2 -2
- package/src/errors/NetworkRequestError.ts +5 -4
- package/src/errors/NotFoundError.ts +2 -2
- package/src/errors/UnauthorizedError.ts +2 -2
- package/src/errors/UnsuccessfulNetworkRequestError.ts +23 -0
- package/src/errors/UnsupportedAuthorizationProtocolError.ts +16 -0
- package/src/errors/index.ts +2 -0
- package/src/helpers/auth.ts +91 -18
- package/src/helpers/identifiers.ts +56 -0
- package/src/helpers/index.ts +2 -0
- package/src/helpers/interop.ts +68 -30
- package/src/helpers/io.ts +147 -60
- package/src/helpers/jsonld.ts +25 -1
- package/src/helpers/testing.ts +103 -28
- package/src/helpers/vocabs.ts +4 -2
- package/src/helpers/wac.ts +55 -0
- package/src/models/SolidDocument.ts +41 -35
- package/src/models/SolidStore.ts +61 -0
- package/src/models/index.ts +2 -1
- package/src/plugins/chai/assertions.ts +11 -2
- package/src/plugins/cypress/types.d.ts +1 -0
- package/src/plugins/jest/matchers.ts +45 -32
- package/src/plugins/jest/types.d.ts +1 -0
- package/src/types/n3.d.ts +9 -0
package/src/helpers/io.ts
CHANGED
|
@@ -1,8 +1,15 @@
|
|
|
1
|
-
import { arr, arrayFilter, arrayReplace,objectWithoutEmpty } from '@noeldemartin/utils';
|
|
2
|
-
import { BlankNode as N3BlankNode, Quad as N3Quad, Parser as TurtleParser, Writer as TurtleWriter } from 'n3';
|
|
3
1
|
import md5 from 'md5';
|
|
2
|
+
import {
|
|
3
|
+
TurtleParser,
|
|
4
|
+
TurtleWriter,
|
|
5
|
+
createBlankNode,
|
|
6
|
+
createQuad,
|
|
7
|
+
jsonLDFromRDF,
|
|
8
|
+
jsonLDToRDF,
|
|
9
|
+
} from '@noeldemartin/solid-utils-external';
|
|
10
|
+
import { arr, arrayFilter, arrayReplace, objectWithoutEmpty, stringMatchAll, tap } from '@noeldemartin/utils';
|
|
4
11
|
import type { Quad } from 'rdf-js';
|
|
5
|
-
import type { Term
|
|
12
|
+
import type { JsonLdDocument, Term } from '@noeldemartin/solid-utils-external';
|
|
6
13
|
|
|
7
14
|
import SolidDocument from '@/models/SolidDocument';
|
|
8
15
|
|
|
@@ -10,9 +17,11 @@ import MalformedSolidDocumentError, { SolidDocumentFormat } from '@/errors/Malfo
|
|
|
10
17
|
import NetworkRequestError from '@/errors/NetworkRequestError';
|
|
11
18
|
import NotFoundError from '@/errors/NotFoundError';
|
|
12
19
|
import UnauthorizedError from '@/errors/UnauthorizedError';
|
|
20
|
+
import { isJsonLDGraph } from '@/helpers/jsonld';
|
|
21
|
+
import type { JsonLD, JsonLDGraph, JsonLDResource } from '@/helpers/jsonld';
|
|
13
22
|
|
|
14
23
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
15
|
-
export declare type AnyFetch = (input: any, options?: any) => Promise<
|
|
24
|
+
export declare type AnyFetch = (input: any, options?: any) => Promise<Response>;
|
|
16
25
|
export declare type TypedFetch = (input: RequestInfo, options?: RequestInit) => Promise<Response>;
|
|
17
26
|
export declare type Fetch = TypedFetch | AnyFetch;
|
|
18
27
|
|
|
@@ -40,7 +49,10 @@ async function fetchRawSolidDocument(url: string, fetch: Fetch): Promise<{ body:
|
|
|
40
49
|
if (error instanceof UnauthorizedError)
|
|
41
50
|
throw error;
|
|
42
51
|
|
|
43
|
-
|
|
52
|
+
if (error instanceof NotFoundError)
|
|
53
|
+
throw error;
|
|
54
|
+
|
|
55
|
+
throw new NetworkRequestError(url, { cause: error });
|
|
44
56
|
}
|
|
45
57
|
}
|
|
46
58
|
|
|
@@ -48,26 +60,23 @@ function normalizeBlankNodes(quads: Quad[]): Quad[] {
|
|
|
48
60
|
const normalizedQuads = quads.slice(0);
|
|
49
61
|
const quadsIndexes: Record<string, Set<number>> = {};
|
|
50
62
|
const blankNodeIds = arr(quads)
|
|
51
|
-
.flatMap(
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
return ids;
|
|
63
|
-
})
|
|
63
|
+
.flatMap(
|
|
64
|
+
(quad, index) => tap(
|
|
65
|
+
arrayFilter([
|
|
66
|
+
quad.object.termType === 'BlankNode' ? quad.object.value : null,
|
|
67
|
+
quad.subject.termType === 'BlankNode' ? quad.subject.value : null,
|
|
68
|
+
]),
|
|
69
|
+
ids => ids.forEach(id => (quadsIndexes[id] ??= new Set()).add(index)),
|
|
70
|
+
),
|
|
71
|
+
)
|
|
64
72
|
.filter()
|
|
65
73
|
.unique();
|
|
66
74
|
|
|
67
75
|
for (const originalId of blankNodeIds) {
|
|
76
|
+
const quadIndexes = quadsIndexes[originalId] as Set<number>;
|
|
68
77
|
const normalizedId = md5(
|
|
69
|
-
arr(
|
|
70
|
-
.map(index => quads[index])
|
|
78
|
+
arr(quadIndexes)
|
|
79
|
+
.map(index => quads[index] as Quad)
|
|
71
80
|
.filter(({ subject: { termType, value } }) => termType === 'BlankNode' && value === originalId)
|
|
72
81
|
.map(
|
|
73
82
|
({ predicate, object }) => object.termType === 'BlankNode'
|
|
@@ -78,18 +87,29 @@ function normalizeBlankNodes(quads: Quad[]): Quad[] {
|
|
|
78
87
|
.join(),
|
|
79
88
|
);
|
|
80
89
|
|
|
81
|
-
for (const index of
|
|
82
|
-
const quad = normalizedQuads[index];
|
|
83
|
-
const terms: Record<string,
|
|
90
|
+
for (const index of quadIndexes) {
|
|
91
|
+
const quad = normalizedQuads[index] as Quad;
|
|
92
|
+
const terms: Record<string, Term> = {
|
|
93
|
+
subject: quad.subject as Term,
|
|
94
|
+
object: quad.object as Term,
|
|
95
|
+
};
|
|
84
96
|
|
|
85
97
|
for (const [termName, termValue] of Object.entries(terms)) {
|
|
86
98
|
if (termValue.termType !== 'BlankNode' || termValue.value !== originalId)
|
|
87
99
|
continue;
|
|
88
100
|
|
|
89
|
-
terms[termName] =
|
|
101
|
+
terms[termName] = createBlankNode(normalizedId) as Term;
|
|
90
102
|
}
|
|
91
103
|
|
|
92
|
-
arrayReplace(
|
|
104
|
+
arrayReplace(
|
|
105
|
+
normalizedQuads,
|
|
106
|
+
quad,
|
|
107
|
+
createQuad(
|
|
108
|
+
terms.subject as Term,
|
|
109
|
+
quad.predicate as Term,
|
|
110
|
+
terms.object as Term,
|
|
111
|
+
),
|
|
112
|
+
);
|
|
93
113
|
}
|
|
94
114
|
}
|
|
95
115
|
|
|
@@ -97,12 +117,17 @@ function normalizeBlankNodes(quads: Quad[]): Quad[] {
|
|
|
97
117
|
}
|
|
98
118
|
|
|
99
119
|
export interface ParsingOptions {
|
|
100
|
-
|
|
120
|
+
baseIRI: string;
|
|
101
121
|
normalizeBlankNodes: boolean;
|
|
102
122
|
}
|
|
103
123
|
|
|
124
|
+
export interface RDFGraphData {
|
|
125
|
+
quads: Quad[];
|
|
126
|
+
containsRelativeIRIs: boolean;
|
|
127
|
+
}
|
|
128
|
+
|
|
104
129
|
export async function createSolidDocument(url: string, body: string, fetch?: Fetch): Promise<SolidDocument> {
|
|
105
|
-
fetch = fetch ?? window.fetch;
|
|
130
|
+
fetch = fetch ?? window.fetch.bind(window);
|
|
106
131
|
|
|
107
132
|
const statements = await turtleToQuads(body);
|
|
108
133
|
|
|
@@ -117,11 +142,34 @@ export async function createSolidDocument(url: string, body: string, fetch?: Fet
|
|
|
117
142
|
|
|
118
143
|
export async function fetchSolidDocument(url: string, fetch?: Fetch): Promise<SolidDocument> {
|
|
119
144
|
const { body: data, headers } = await fetchRawSolidDocument(url, fetch ?? window.fetch);
|
|
120
|
-
const statements = await turtleToQuads(data, {
|
|
145
|
+
const statements = await turtleToQuads(data, { baseIRI: url });
|
|
121
146
|
|
|
122
147
|
return new SolidDocument(url, statements, headers);
|
|
123
148
|
}
|
|
124
149
|
|
|
150
|
+
export async function fetchSolidDocumentIfFound(url: string, fetch?: Fetch): Promise<SolidDocument | null> {
|
|
151
|
+
try {
|
|
152
|
+
const document = await fetchSolidDocument(url, fetch);
|
|
153
|
+
|
|
154
|
+
return document;
|
|
155
|
+
} catch (error) {
|
|
156
|
+
if (!(error instanceof NotFoundError))
|
|
157
|
+
throw error;
|
|
158
|
+
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export async function jsonldToQuads(jsonld: JsonLD, baseIRI?: string): Promise<Quad[]> {
|
|
164
|
+
if (isJsonLDGraph(jsonld)) {
|
|
165
|
+
const graphQuads = await Promise.all(jsonld['@graph'].map(resource => jsonldToQuads(resource, baseIRI)));
|
|
166
|
+
|
|
167
|
+
return graphQuads.flat();
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return jsonLDToRDF(jsonld as JsonLdDocument, { base: baseIRI }) as Promise<Quad[]>;
|
|
171
|
+
}
|
|
172
|
+
|
|
125
173
|
export function normalizeSparql(sparql: string): string {
|
|
126
174
|
const quads = sparqlToQuadsSync(sparql);
|
|
127
175
|
|
|
@@ -135,6 +183,66 @@ export function normalizeSparql(sparql: string): string {
|
|
|
135
183
|
.join(' ;\n');
|
|
136
184
|
}
|
|
137
185
|
|
|
186
|
+
export function parseTurtle(turtle: string, options: Partial<ParsingOptions> = {}): Promise<RDFGraphData> {
|
|
187
|
+
const parserOptions = objectWithoutEmpty({ baseIRI: options.baseIRI });
|
|
188
|
+
const parser = new TurtleParser(parserOptions);
|
|
189
|
+
const data: RDFGraphData = {
|
|
190
|
+
quads: [],
|
|
191
|
+
containsRelativeIRIs: false,
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
return new Promise((resolve, reject) => {
|
|
195
|
+
const resolveRelativeIRI = parser._resolveRelativeIRI;
|
|
196
|
+
|
|
197
|
+
parser._resolveRelativeIRI = (...args) => {
|
|
198
|
+
data.containsRelativeIRIs = true;
|
|
199
|
+
parser._resolveRelativeIRI = resolveRelativeIRI;
|
|
200
|
+
|
|
201
|
+
return parser._resolveRelativeIRI(...args);
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
parser.parse(turtle, (error, quad) => {
|
|
205
|
+
if (error) {
|
|
206
|
+
reject(
|
|
207
|
+
new MalformedSolidDocumentError(
|
|
208
|
+
options.baseIRI ?? null,
|
|
209
|
+
SolidDocumentFormat.Turtle,
|
|
210
|
+
error.message,
|
|
211
|
+
),
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (!quad) {
|
|
218
|
+
// data.quads = options.normalizeBlankNodes
|
|
219
|
+
// ? normalizeBlankNodes(data.quads)
|
|
220
|
+
// : data.quads;
|
|
221
|
+
|
|
222
|
+
resolve(data);
|
|
223
|
+
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
data.quads.push(quad);
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export async function quadsToJsonLD(quads: Quad[]): Promise<JsonLDGraph> {
|
|
233
|
+
const graph = await jsonLDFromRDF(quads);
|
|
234
|
+
|
|
235
|
+
return {
|
|
236
|
+
'@graph': graph as JsonLDResource[],
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
export function quadsToTurtle(quads: Quad[]): string {
|
|
241
|
+
const writer = new TurtleWriter;
|
|
242
|
+
|
|
243
|
+
return writer.quadsToString(quads);
|
|
244
|
+
}
|
|
245
|
+
|
|
138
246
|
export function quadToTurtle(quad: Quad): string {
|
|
139
247
|
const writer = new TurtleWriter;
|
|
140
248
|
|
|
@@ -155,7 +263,7 @@ export async function sparqlToQuads(
|
|
|
155
263
|
sparql: string,
|
|
156
264
|
options: Partial<ParsingOptions> = {},
|
|
157
265
|
): Promise<Record<string, Quad[]>> {
|
|
158
|
-
const operations = sparql
|
|
266
|
+
const operations = stringMatchAll<3>(sparql, /(\w+) DATA {([^}]+)}/g);
|
|
159
267
|
const quads: Record<string, Quad[]> = {};
|
|
160
268
|
|
|
161
269
|
await Promise.all([...operations].map(async operation => {
|
|
@@ -169,7 +277,7 @@ export async function sparqlToQuads(
|
|
|
169
277
|
}
|
|
170
278
|
|
|
171
279
|
export function sparqlToQuadsSync(sparql: string, options: Partial<ParsingOptions> = {}): Record<string, Quad[]> {
|
|
172
|
-
const operations = sparql
|
|
280
|
+
const operations = stringMatchAll<3>(sparql, /(\w+) DATA {([^}]+)}/g);
|
|
173
281
|
const quads: Record<string, Quad[]> = {};
|
|
174
282
|
|
|
175
283
|
for (const operation of operations) {
|
|
@@ -183,38 +291,13 @@ export function sparqlToQuadsSync(sparql: string, options: Partial<ParsingOption
|
|
|
183
291
|
}
|
|
184
292
|
|
|
185
293
|
export async function turtleToQuads(turtle: string, options: Partial<ParsingOptions> = {}): Promise<Quad[]> {
|
|
186
|
-
const
|
|
187
|
-
const parser = new TurtleParser(parserOptions);
|
|
188
|
-
const quads: Quad[] = [];
|
|
189
|
-
|
|
190
|
-
return new Promise((resolve, reject) => {
|
|
191
|
-
parser.parse(turtle, (error, quad) => {
|
|
192
|
-
if (error) {
|
|
193
|
-
reject(
|
|
194
|
-
new MalformedSolidDocumentError(
|
|
195
|
-
options.documentUrl ?? null,
|
|
196
|
-
SolidDocumentFormat.Turtle,
|
|
197
|
-
error.message,
|
|
198
|
-
),
|
|
199
|
-
);
|
|
200
|
-
return;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
if (!quad) {
|
|
204
|
-
options.normalizeBlankNodes
|
|
205
|
-
? resolve(normalizeBlankNodes(quads))
|
|
206
|
-
: resolve(quads);
|
|
207
|
-
|
|
208
|
-
return;
|
|
209
|
-
}
|
|
294
|
+
const { quads } = await parseTurtle(turtle, options);
|
|
210
295
|
|
|
211
|
-
|
|
212
|
-
});
|
|
213
|
-
});
|
|
296
|
+
return quads;
|
|
214
297
|
}
|
|
215
298
|
|
|
216
299
|
export function turtleToQuadsSync(turtle: string, options: Partial<ParsingOptions> = {}): Quad[] {
|
|
217
|
-
const parserOptions = objectWithoutEmpty({ baseIRI: options.
|
|
300
|
+
const parserOptions = objectWithoutEmpty({ baseIRI: options.baseIRI });
|
|
218
301
|
const parser = new TurtleParser(parserOptions);
|
|
219
302
|
|
|
220
303
|
try {
|
|
@@ -224,12 +307,16 @@ export function turtleToQuadsSync(turtle: string, options: Partial<ParsingOption
|
|
|
224
307
|
? normalizeBlankNodes(quads)
|
|
225
308
|
: quads;
|
|
226
309
|
} catch (error) {
|
|
227
|
-
throw new MalformedSolidDocumentError(
|
|
310
|
+
throw new MalformedSolidDocumentError(
|
|
311
|
+
options.baseIRI ?? null,
|
|
312
|
+
SolidDocumentFormat.Turtle,
|
|
313
|
+
(error as Error).message ?? '',
|
|
314
|
+
);
|
|
228
315
|
}
|
|
229
316
|
}
|
|
230
317
|
|
|
231
318
|
export async function updateSolidDocument(url: string, body: string, fetch?: Fetch): Promise<void> {
|
|
232
|
-
fetch = fetch ?? window.fetch;
|
|
319
|
+
fetch = fetch ?? window.fetch.bind(window);
|
|
233
320
|
|
|
234
321
|
await fetch(url, {
|
|
235
322
|
method: 'PATCH',
|
package/src/helpers/jsonld.ts
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { compactJsonLD } from '@noeldemartin/solid-utils-external';
|
|
2
|
+
import type { JsonLdDocument } from '@noeldemartin/solid-utils-external';
|
|
3
|
+
|
|
1
4
|
export type JsonLD = Partial<{
|
|
2
5
|
'@context': Record<string, unknown>;
|
|
3
6
|
'@id': string;
|
|
@@ -5,4 +8,25 @@ export type JsonLD = Partial<{
|
|
|
5
8
|
}> & { [k: string]: unknown };
|
|
6
9
|
|
|
7
10
|
export type JsonLDResource = Omit<JsonLD, '@id'> & { '@id': string };
|
|
8
|
-
export type JsonLDGraph = {
|
|
11
|
+
export type JsonLDGraph = {
|
|
12
|
+
'@context'?: Record<string, unknown>;
|
|
13
|
+
'@graph': JsonLDResource[];
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export async function compactJsonLDGraph(jsonld: JsonLDGraph): Promise<JsonLDGraph> {
|
|
17
|
+
const compactedJsonLD = await compactJsonLD(jsonld as JsonLdDocument, {});
|
|
18
|
+
|
|
19
|
+
if ('@graph' in compactedJsonLD) {
|
|
20
|
+
return compactedJsonLD as JsonLDGraph;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if ('@id' in compactedJsonLD) {
|
|
24
|
+
return { '@graph': [compactedJsonLD] } as JsonLDGraph;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return { '@graph': [] };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function isJsonLDGraph(jsonld: JsonLD): jsonld is JsonLDGraph {
|
|
31
|
+
return '@graph' in jsonld;
|
|
32
|
+
}
|
package/src/helpers/testing.ts
CHANGED
|
@@ -1,39 +1,67 @@
|
|
|
1
|
-
import { pull } from '@noeldemartin/utils';
|
|
1
|
+
import { JSError, arrayRemove, pull, stringMatchAll } from '@noeldemartin/utils';
|
|
2
|
+
import type { JsonLD } from '@/helpers/jsonld';
|
|
2
3
|
import type { Quad, Quad_Object } from 'rdf-js';
|
|
3
4
|
|
|
4
|
-
import { quadToTurtle, sparqlToQuadsSync, turtleToQuadsSync } from './io';
|
|
5
|
+
import { jsonldToQuads, quadToTurtle, quadsToTurtle, sparqlToQuadsSync, turtleToQuadsSync } from './io';
|
|
5
6
|
|
|
6
|
-
|
|
7
|
+
let patternsRegExpsIndex: Record<string, RegExp> = {};
|
|
8
|
+
const builtInPatterns: Record<string, string> = {
|
|
9
|
+
'%uuid%': '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}',
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
class ExpectedQuadAssertionError extends JSError {
|
|
13
|
+
|
|
14
|
+
constructor(public readonly expectedQuad: Quad) {
|
|
15
|
+
super(`Couldn't find the following triple: ${quadToTurtle(expectedQuad)}`);
|
|
16
|
+
}
|
|
7
17
|
|
|
8
|
-
function containsPatterns(value: string): boolean {
|
|
9
|
-
return /\[\[([^\]]+)\]\]/.test(value);
|
|
10
18
|
}
|
|
11
19
|
|
|
12
|
-
function
|
|
13
|
-
|
|
14
|
-
|
|
20
|
+
function assertExpectedQuadsExist(expectedQuads: Quad[], actualQuads: Quad[]): void {
|
|
21
|
+
for (const expectedQuad of expectedQuads) {
|
|
22
|
+
const matchingQuad = actualQuads.find(actualQuad => quadEquals(expectedQuad, actualQuad));
|
|
15
23
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
const patterns: string[] = [];
|
|
19
|
-
let expectedRegExp = expected;
|
|
24
|
+
if (!matchingQuad)
|
|
25
|
+
throw new ExpectedQuadAssertionError(expectedQuad);
|
|
20
26
|
|
|
21
|
-
|
|
22
|
-
|
|
27
|
+
arrayRemove(actualQuads, matchingQuad);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
23
30
|
|
|
24
|
-
|
|
25
|
-
|
|
31
|
+
function containsPatterns(value: string): boolean {
|
|
32
|
+
return /\[\[(.*\]\[)?([^\]]+)\]\]/.test(value);
|
|
33
|
+
}
|
|
26
34
|
|
|
27
|
-
|
|
35
|
+
function createPatternRegexp(expected: string): RegExp {
|
|
36
|
+
const patternAliases = [];
|
|
37
|
+
const patternMatches = stringMatchAll<4, 1 | 2>(
|
|
38
|
+
expected,
|
|
39
|
+
/\[\[((.*?)\]\[)?([^\]]+)\]\]/g,
|
|
40
|
+
);
|
|
41
|
+
const patterns: string[] = [];
|
|
42
|
+
let expectedRegExp = expected;
|
|
28
43
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
44
|
+
for (const patternMatch of patternMatches) {
|
|
45
|
+
patternMatch[2] && patternAliases.push(patternMatch[2]);
|
|
46
|
+
|
|
47
|
+
patterns.push(patternMatch[3]);
|
|
32
48
|
|
|
33
|
-
|
|
49
|
+
expectedRegExp = expectedRegExp.replace(patternMatch[0], `%PATTERN${patterns.length - 1}%`);
|
|
34
50
|
}
|
|
35
51
|
|
|
36
|
-
|
|
52
|
+
expectedRegExp = expectedRegExp.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&');
|
|
53
|
+
|
|
54
|
+
for (const [patternIndex, pattern] of Object.entries(patterns)) {
|
|
55
|
+
expectedRegExp = expectedRegExp.replace(`%PATTERN${patternIndex}%`, builtInPatterns[pattern] ?? pattern);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return new RegExp(expectedRegExp);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function quadValueEquals(expected: string, actual: string): boolean {
|
|
62
|
+
return containsPatterns(expected)
|
|
63
|
+
? (patternsRegExpsIndex[expected] ??= createPatternRegexp(expected)).test(actual)
|
|
64
|
+
: expected === actual;
|
|
37
65
|
}
|
|
38
66
|
|
|
39
67
|
function quadObjectEquals(expected: Quad_Object, actual: Quad_Object): boolean {
|
|
@@ -59,6 +87,10 @@ function quadEquals(expected: Quad, actual: Quad): boolean {
|
|
|
59
87
|
&& quadValueEquals(expected.predicate.value, actual.predicate.value);
|
|
60
88
|
}
|
|
61
89
|
|
|
90
|
+
function resetPatterns(): void {
|
|
91
|
+
patternsRegExpsIndex = {};
|
|
92
|
+
}
|
|
93
|
+
|
|
62
94
|
export interface EqualityResult {
|
|
63
95
|
success: boolean;
|
|
64
96
|
message: string;
|
|
@@ -66,8 +98,39 @@ export interface EqualityResult {
|
|
|
66
98
|
actual: string;
|
|
67
99
|
}
|
|
68
100
|
|
|
101
|
+
export async function jsonldEquals(expected: JsonLD, actual: JsonLD): Promise<EqualityResult> {
|
|
102
|
+
// TODO catch parsing errors and improve message.
|
|
103
|
+
resetPatterns();
|
|
104
|
+
|
|
105
|
+
const expectedQuads = await jsonldToQuads(expected);
|
|
106
|
+
const actualQuads = await jsonldToQuads(actual);
|
|
107
|
+
const expectedTurtle = quadsToTurtle(expectedQuads);
|
|
108
|
+
const actualTurtle = quadsToTurtle(actualQuads);
|
|
109
|
+
const result = (success: boolean, message: string) => ({
|
|
110
|
+
success,
|
|
111
|
+
message,
|
|
112
|
+
expected: expectedTurtle,
|
|
113
|
+
actual: actualTurtle,
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
if (expectedQuads.length !== actualQuads.length)
|
|
117
|
+
return result(false, `Expected ${expectedQuads.length} triples, found ${actualQuads.length}.`);
|
|
118
|
+
|
|
119
|
+
try {
|
|
120
|
+
assertExpectedQuadsExist(expectedQuads, actualQuads);
|
|
121
|
+
} catch (error) {
|
|
122
|
+
if (!(error instanceof ExpectedQuadAssertionError))
|
|
123
|
+
throw error;
|
|
124
|
+
|
|
125
|
+
return result(false, error.message);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return result(true, 'jsonld matches');
|
|
129
|
+
}
|
|
130
|
+
|
|
69
131
|
export function sparqlEquals(expected: string, actual: string): EqualityResult {
|
|
70
132
|
// TODO catch parsing errors and improve message.
|
|
133
|
+
resetPatterns();
|
|
71
134
|
|
|
72
135
|
const expectedOperations = sparqlToQuadsSync(expected, { normalizeBlankNodes: true });
|
|
73
136
|
const actualOperations = sparqlToQuadsSync(actual, { normalizeBlankNodes: true });
|
|
@@ -83,9 +146,16 @@ export function sparqlEquals(expected: string, actual: string): EqualityResult {
|
|
|
83
146
|
if (expectedQuads.length !== actualQuads.length)
|
|
84
147
|
return result(false, `Expected ${expectedQuads.length} ${operation} triples, found ${actualQuads.length}.`);
|
|
85
148
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
149
|
+
try {
|
|
150
|
+
assertExpectedQuadsExist(expectedQuads, actualQuads);
|
|
151
|
+
} catch (error) {
|
|
152
|
+
if (!(error instanceof ExpectedQuadAssertionError))
|
|
153
|
+
throw error;
|
|
154
|
+
|
|
155
|
+
return result(
|
|
156
|
+
false,
|
|
157
|
+
`Couldn't find the following ${operation} triple: ${quadToTurtle(error.expectedQuad)}`,
|
|
158
|
+
);
|
|
89
159
|
}
|
|
90
160
|
}
|
|
91
161
|
|
|
@@ -98,6 +168,7 @@ export function sparqlEquals(expected: string, actual: string): EqualityResult {
|
|
|
98
168
|
|
|
99
169
|
export function turtleEquals(expected: string, actual: string): EqualityResult {
|
|
100
170
|
// TODO catch parsing errors and improve message.
|
|
171
|
+
resetPatterns();
|
|
101
172
|
|
|
102
173
|
const expectedQuads = turtleToQuadsSync(expected, { normalizeBlankNodes: true });
|
|
103
174
|
const actualQuads = turtleToQuadsSync(actual, { normalizeBlankNodes: true });
|
|
@@ -106,9 +177,13 @@ export function turtleEquals(expected: string, actual: string): EqualityResult {
|
|
|
106
177
|
if (expectedQuads.length !== actualQuads.length)
|
|
107
178
|
return result(false, `Expected ${expectedQuads.length} triples, found ${actualQuads.length}.`);
|
|
108
179
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
180
|
+
try {
|
|
181
|
+
assertExpectedQuadsExist(expectedQuads, actualQuads);
|
|
182
|
+
} catch (error) {
|
|
183
|
+
if (!(error instanceof ExpectedQuadAssertionError))
|
|
184
|
+
throw error;
|
|
185
|
+
|
|
186
|
+
return result(false, error.message);
|
|
112
187
|
}
|
|
113
188
|
|
|
114
189
|
return result(true, 'turtle matches');
|
package/src/helpers/vocabs.ts
CHANGED
|
@@ -6,10 +6,12 @@ export interface ExpandIRIOptions {
|
|
|
6
6
|
}
|
|
7
7
|
|
|
8
8
|
const knownPrefixes: RDFContext = {
|
|
9
|
+
acl: 'http://www.w3.org/ns/auth/acl#',
|
|
9
10
|
foaf: 'http://xmlns.com/foaf/0.1/',
|
|
10
11
|
pim: 'http://www.w3.org/ns/pim/space#',
|
|
11
12
|
purl: 'http://purl.org/dc/terms/',
|
|
12
|
-
|
|
13
|
+
rdf: 'http://www.w3.org/1999/02/22-rdf-syntax-ns#',
|
|
14
|
+
rdfs: 'http://www.w3.org/2000/01/rdf-schema#',
|
|
13
15
|
schema: 'https://schema.org/',
|
|
14
16
|
solid: 'http://www.w3.org/ns/solid/terms#',
|
|
15
17
|
vcard: 'http://www.w3.org/2006/vcard/ns#',
|
|
@@ -25,7 +27,7 @@ export function expandIRI(iri: string, options: Partial<ExpandIRIOptions> = {}):
|
|
|
25
27
|
|
|
26
28
|
const [prefix, name] = iri.split(':');
|
|
27
29
|
|
|
28
|
-
if (name) {
|
|
30
|
+
if (prefix && name) {
|
|
29
31
|
const expandedPrefix = knownPrefixes[prefix] ?? options.extraContext?.[prefix] ?? null;
|
|
30
32
|
|
|
31
33
|
if (!expandedPrefix)
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { objectWithoutEmpty, requireUrlParentDirectory, urlResolve } from '@noeldemartin/utils';
|
|
2
|
+
|
|
3
|
+
import UnsupportedAuthorizationProtocolError from '@/errors/UnsupportedAuthorizationProtocolError';
|
|
4
|
+
import { fetchSolidDocumentIfFound } from '@/helpers/io';
|
|
5
|
+
import type SolidDocument from '@/models/SolidDocument';
|
|
6
|
+
import type { Fetch } from '@/helpers/io';
|
|
7
|
+
|
|
8
|
+
async function fetchACLResourceUrl(resourceUrl: string, fetch: Fetch): Promise<string> {
|
|
9
|
+
fetch = fetch ?? window.fetch.bind(window);
|
|
10
|
+
|
|
11
|
+
const resourceHead = await fetch(resourceUrl, { method: 'HEAD' });
|
|
12
|
+
const linkHeader = resourceHead.headers.get('Link') ?? '';
|
|
13
|
+
const url = linkHeader.match(/<([^>]+)>;\s*rel="acl"/)?.[1] ?? null;
|
|
14
|
+
|
|
15
|
+
if (!url) {
|
|
16
|
+
throw new Error(`Could not find ACL Resource for '${resourceUrl}'`);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return urlResolve(requireUrlParentDirectory(resourceUrl), url);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async function fetchEffectiveACL(
|
|
23
|
+
resourceUrl: string,
|
|
24
|
+
fetch: Fetch,
|
|
25
|
+
aclResourceUrl?: string | null,
|
|
26
|
+
): Promise<SolidDocument> {
|
|
27
|
+
aclResourceUrl = aclResourceUrl ?? await fetchACLResourceUrl(resourceUrl, fetch);
|
|
28
|
+
|
|
29
|
+
const aclDocument = await fetchSolidDocumentIfFound(aclResourceUrl ?? '', fetch);
|
|
30
|
+
|
|
31
|
+
if (!aclDocument) {
|
|
32
|
+
return fetchEffectiveACL(requireUrlParentDirectory(resourceUrl), fetch);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (aclDocument.isACPResource()) {
|
|
36
|
+
throw new UnsupportedAuthorizationProtocolError(resourceUrl, 'ACP');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return aclDocument;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function fetchSolidDocumentACL(documentUrl: string, fetch: Fetch): Promise<{
|
|
43
|
+
url: string;
|
|
44
|
+
effectiveUrl: string;
|
|
45
|
+
document: SolidDocument;
|
|
46
|
+
}> {
|
|
47
|
+
const url = await fetchACLResourceUrl(documentUrl, fetch);
|
|
48
|
+
const document = await fetchEffectiveACL(documentUrl, fetch, url);
|
|
49
|
+
|
|
50
|
+
return objectWithoutEmpty({
|
|
51
|
+
url,
|
|
52
|
+
effectiveUrl: document.url,
|
|
53
|
+
document,
|
|
54
|
+
});
|
|
55
|
+
}
|