@noeldemartin/solid-utils 0.6.0-next.cccdc9c7e033588e2df9d1887ceae49788344d84 → 0.6.0-next.cea357c45d71b0c9b778bcf63049fce19ae3129f

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.
@@ -1,6 +1,13 @@
1
1
  import { describe, expect, it } from 'vitest';
2
2
 
3
- import { jsonldToQuads, normalizeSparql, quadsToJsonLD, sparqlToQuadsSync, turtleToQuadsSync } from './io';
3
+ import {
4
+ jsonldToQuads,
5
+ normalizeJsonLD,
6
+ normalizeSparql,
7
+ quadsToJsonLD,
8
+ sparqlToQuadsSync,
9
+ turtleToQuadsSync,
10
+ } from './io';
4
11
  import type { Quad } from '@rdfjs/types';
5
12
 
6
13
  describe('IO', () => {
@@ -200,6 +207,45 @@ describe('IO', () => {
200
207
  expect(quads[1].object.value).toEqual('John Doe');
201
208
  });
202
209
 
210
+ it('normalizes jsonld', async () => {
211
+ // Arrange
212
+ const json = {
213
+ '@graph': [
214
+ {
215
+ '@context': { '@vocab': 'https://schema.org/' },
216
+ '@id': '#it',
217
+ '@type': 'Movie',
218
+ 'name': 'Spirited Away',
219
+ },
220
+ {
221
+ '@context': { '@vocab': 'https://vocab.noeldemartin.com/crdt/' },
222
+ '@id': '#it-metadata',
223
+ '@type': 'Metadata',
224
+ 'resource': { '@id': '#it' },
225
+ },
226
+ ],
227
+ };
228
+
229
+ // Act
230
+ const normalized = await normalizeJsonLD(json);
231
+
232
+ // Assert
233
+ expect(normalized).toEqual({
234
+ '@graph': [
235
+ {
236
+ '@id': '#it',
237
+ '@type': ['https://schema.org/Movie'],
238
+ 'https://schema.org/name': [{ '@value': 'Spirited Away' }],
239
+ },
240
+ {
241
+ '@id': '#it-metadata',
242
+ '@type': ['https://vocab.noeldemartin.com/crdt/Metadata'],
243
+ 'https://vocab.noeldemartin.com/crdt/resource': [{ '@id': '#it' }],
244
+ },
245
+ ],
246
+ });
247
+ });
248
+
203
249
  it('converts quads to jsonld', async () => {
204
250
  // Arrange
205
251
  const quads = turtleToQuadsSync(`
package/src/helpers/io.ts CHANGED
@@ -1,7 +1,7 @@
1
+ import jsonld from 'jsonld';
1
2
  import md5 from 'md5';
2
3
  import { arr, arrayFilter, arrayReplace, objectWithoutEmpty, stringMatchAll, tap } from '@noeldemartin/utils';
3
4
  import { BlankNode as N3BlankNode, Quad as N3Quad, Parser, Writer } from 'n3';
4
- import { fromRDF, toRDF } from 'jsonld';
5
5
  import type { JsonLdDocument } from 'jsonld';
6
6
  import type { Quad } from '@rdfjs/types';
7
7
  import type { Term } from 'n3';
@@ -117,21 +117,31 @@ function normalizeQuads(quads: Quad[]): string {
117
117
  .join('\n');
118
118
  }
119
119
 
120
- function preprocessSubjects(jsonld: JsonLD): void {
121
- if (!jsonld['@id']?.startsWith('#')) {
120
+ function preprocessSubjects(json: JsonLD): void {
121
+ if (!json['@id']?.startsWith('#')) {
122
122
  return;
123
123
  }
124
124
 
125
- jsonld['@id'] = ANONYMOUS_PREFIX + jsonld['@id'];
125
+ json['@id'] = ANONYMOUS_PREFIX + json['@id'];
126
+
127
+ for (const [field, value] of Object.entries(json)) {
128
+ if (typeof value !== 'object' || value === null || !('@id' in value)) {
129
+ continue;
130
+ }
131
+
132
+ preprocessSubjects(json[field] as JsonLD);
133
+ }
126
134
  }
127
135
 
128
136
  function postprocessSubjects(quads: Quad[]): void {
129
137
  for (const quad of quads) {
130
- if (!quad.subject.value.startsWith(ANONYMOUS_PREFIX)) {
131
- continue;
138
+ if (quad.subject.value.startsWith(ANONYMOUS_PREFIX)) {
139
+ quad.subject.value = quad.subject.value.slice(ANONYMOUS_PREFIX_LENGTH);
132
140
  }
133
141
 
134
- quad.subject.value = quad.subject.value.slice(ANONYMOUS_PREFIX_LENGTH);
142
+ if (quad.object.value.startsWith(ANONYMOUS_PREFIX)) {
143
+ quad.object.value = quad.object.value.slice(ANONYMOUS_PREFIX_LENGTH);
144
+ }
135
145
  }
136
146
  }
137
147
 
@@ -186,22 +196,28 @@ export async function fetchSolidDocumentIfFound(
186
196
  }
187
197
  }
188
198
 
189
- export async function jsonldToQuads(jsonld: JsonLD, baseIRI?: string): Promise<Quad[]> {
190
- if (isJsonLDGraph(jsonld)) {
191
- const graphQuads = await Promise.all(jsonld['@graph'].map((resource) => jsonldToQuads(resource, baseIRI)));
199
+ export async function jsonldToQuads(json: JsonLD, baseIRI?: string): Promise<Quad[]> {
200
+ if (isJsonLDGraph(json)) {
201
+ const graphQuads = await Promise.all(json['@graph'].map((resource) => jsonldToQuads(resource, baseIRI)));
192
202
 
193
203
  return graphQuads.flat();
194
204
  }
195
205
 
196
- preprocessSubjects(jsonld);
206
+ preprocessSubjects(json);
197
207
 
198
- const quads = await (toRDF(jsonld as JsonLdDocument, { base: baseIRI }) as Promise<Quad[]>);
208
+ const quads = await (jsonld.toRDF(json as JsonLdDocument, { base: baseIRI }) as Promise<Quad[]>);
199
209
 
200
210
  postprocessSubjects(quads);
201
211
 
202
212
  return quads;
203
213
  }
204
214
 
215
+ export async function normalizeJsonLD(json: JsonLD, baseIRI?: string): Promise<JsonLD> {
216
+ const quads = await jsonldToQuads(json, baseIRI);
217
+
218
+ return quadsToJsonLD(quads);
219
+ }
220
+
205
221
  export function normalizeSparql(sparql: string): string {
206
222
  const quads = sparqlToQuadsSync(sparql);
207
223
 
@@ -263,7 +279,7 @@ export function parseTurtle(turtle: string, options: Partial<ParsingOptions> = {
263
279
  }
264
280
 
265
281
  export async function quadsToJsonLD(quads: Quad[]): Promise<JsonLDGraph> {
266
- const graph = await fromRDF(quads);
282
+ const graph = await jsonld.fromRDF(quads);
267
283
 
268
284
  return {
269
285
  '@graph': graph as JsonLDResource[],
@@ -1,4 +1,4 @@
1
- import { compact } from 'jsonld';
1
+ import jsonld from 'jsonld';
2
2
  import type { JsonLdDocument } from 'jsonld';
3
3
 
4
4
  export type JsonLD = Partial<{
@@ -13,8 +13,8 @@ export type JsonLDGraph = {
13
13
  '@graph': JsonLDResource[];
14
14
  };
15
15
 
16
- export async function compactJsonLDGraph(jsonld: JsonLDGraph): Promise<JsonLDGraph> {
17
- const compactedJsonLD = await compact(jsonld as JsonLdDocument, {});
16
+ export async function compactJsonLDGraph(json: JsonLDGraph): Promise<JsonLDGraph> {
17
+ const compactedJsonLD = await jsonld.compact(json as JsonLdDocument, {});
18
18
 
19
19
  if ('@graph' in compactedJsonLD) {
20
20
  return compactedJsonLD as JsonLDGraph;
@@ -27,6 +27,6 @@ export async function compactJsonLDGraph(jsonld: JsonLDGraph): Promise<JsonLDGra
27
27
  return { '@graph': [] };
28
28
  }
29
29
 
30
- export function isJsonLDGraph(jsonld: JsonLD): jsonld is JsonLDGraph {
31
- return '@graph' in jsonld;
30
+ export function isJsonLDGraph(json: JsonLD): json is JsonLDGraph {
31
+ return '@graph' in json;
32
32
  }
@@ -23,22 +23,19 @@ export function defineIRIPrefix(name: string, value: string): void {
23
23
  }
24
24
 
25
25
  export function expandIRI(iri: string, options: Partial<ExpandIRIOptions> = {}): string {
26
- if (iri.startsWith('http'))
27
- return iri;
26
+ if (iri.startsWith('http')) return iri;
28
27
 
29
28
  const [prefix, name] = iri.split(':');
30
29
 
31
30
  if (prefix && name) {
32
31
  const expandedPrefix = knownPrefixes[prefix] ?? options.extraContext?.[prefix] ?? null;
33
32
 
34
- if (!expandedPrefix)
35
- throw new Error(`Can't expand IRI with unknown prefix: '${iri}'`);
33
+ if (!expandedPrefix) throw new Error(`Can't expand IRI with unknown prefix: '${iri}'`);
36
34
 
37
35
  return expandedPrefix + name;
38
36
  }
39
37
 
40
- if (!options.defaultPrefix)
41
- throw new Error(`Can't expand IRI without a default prefix: '${iri}'`);
38
+ if (!options.defaultPrefix) throw new Error(`Can't expand IRI without a default prefix: '${iri}'`);
42
39
 
43
40
  return options.defaultPrefix + prefix;
44
41
  }
package/src/index.ts CHANGED
@@ -1,3 +1,4 @@
1
1
  export * from './errors';
2
2
  export * from './helpers';
3
3
  export * from './models';
4
+ export * from './types';
@@ -0,0 +1,187 @@
1
+ import { JSError, arrayRemove, pull, stringMatchAll } from '@noeldemartin/utils';
2
+ import type { Quad, Quad_Object } from '@rdfjs/types';
3
+
4
+ import {
5
+ jsonldToQuads,
6
+ quadToTurtle,
7
+ quadsToTurtle,
8
+ sparqlToQuadsSync,
9
+ turtleToQuadsSync,
10
+ } from '@noeldemartin/solid-utils/helpers/io';
11
+ import type { JsonLD } from '@noeldemartin/solid-utils/helpers/jsonld';
12
+
13
+ let patternsRegExpsIndex: Record<string, RegExp> = {};
14
+ const builtInPatterns: Record<string, string> = {
15
+ '%uuid%': '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}',
16
+ };
17
+
18
+ class ExpectedQuadAssertionError extends JSError {
19
+
20
+ constructor(public readonly expectedQuad: Quad) {
21
+ super(`Couldn't find the following triple: ${quadToTurtle(expectedQuad)}`);
22
+ }
23
+
24
+ }
25
+
26
+ function assertExpectedQuadsExist(expectedQuads: Quad[], actualQuads: Quad[]): void {
27
+ for (const expectedQuad of expectedQuads) {
28
+ const matchingQuad = actualQuads.find((actualQuad) => quadEquals(expectedQuad, actualQuad));
29
+
30
+ if (!matchingQuad) throw new ExpectedQuadAssertionError(expectedQuad);
31
+
32
+ arrayRemove(actualQuads, matchingQuad);
33
+ }
34
+ }
35
+
36
+ function containsPatterns(value: string): boolean {
37
+ return /\[\[(.*\]\[)?([^\]]+)\]\]/.test(value);
38
+ }
39
+
40
+ function createPatternRegexp(expected: string): RegExp {
41
+ const patternAliases = [];
42
+ const patternMatches = stringMatchAll<4, 1 | 2>(expected, /\[\[((.*?)\]\[)?([^\]]+)\]\]/g);
43
+ const patterns: string[] = [];
44
+ let expectedRegExp = expected;
45
+
46
+ for (const patternMatch of patternMatches) {
47
+ patternMatch[2] && patternAliases.push(patternMatch[2]);
48
+
49
+ patterns.push(patternMatch[3]);
50
+
51
+ expectedRegExp = expectedRegExp.replace(patternMatch[0], `%PATTERN${patterns.length - 1}%`);
52
+ }
53
+
54
+ expectedRegExp = expectedRegExp.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&');
55
+
56
+ for (const [patternIndex, pattern] of Object.entries(patterns)) {
57
+ expectedRegExp = expectedRegExp.replace(`%PATTERN${patternIndex}%`, builtInPatterns[pattern] ?? pattern);
58
+ }
59
+
60
+ return new RegExp(expectedRegExp);
61
+ }
62
+
63
+ function quadValueEquals(expected: string, actual: string): boolean {
64
+ return containsPatterns(expected)
65
+ ? (patternsRegExpsIndex[expected] ??= createPatternRegexp(expected)).test(actual)
66
+ : expected === actual;
67
+ }
68
+
69
+ function quadObjectEquals(expected: Quad_Object, actual: Quad_Object): boolean {
70
+ if (expected.termType !== actual.termType) return false;
71
+
72
+ if (expected.termType === 'Literal' && actual.termType === 'Literal') {
73
+ if (expected.datatype.value !== actual.datatype.value) return false;
74
+
75
+ if (!containsPatterns(expected.value))
76
+ return expected.datatype.value === 'http://www.w3.org/2001/XMLSchema#dateTime'
77
+ ? new Date(expected.value).getTime() === new Date(actual.value).getTime()
78
+ : expected.value === actual.value;
79
+ }
80
+
81
+ return quadValueEquals(expected.value, actual.value);
82
+ }
83
+
84
+ function quadEquals(expected: Quad, actual: Quad): boolean {
85
+ return (
86
+ quadObjectEquals(expected.object, actual.object) &&
87
+ quadValueEquals(expected.subject.value, actual.subject.value) &&
88
+ quadValueEquals(expected.predicate.value, actual.predicate.value)
89
+ );
90
+ }
91
+
92
+ function resetPatterns(): void {
93
+ patternsRegExpsIndex = {};
94
+ }
95
+
96
+ export interface EqualityResult {
97
+ success: boolean;
98
+ message: string;
99
+ expected: string;
100
+ actual: string;
101
+ }
102
+
103
+ export async function jsonldEquals(expected: JsonLD, actual: JsonLD): Promise<EqualityResult> {
104
+ // TODO catch parsing errors and improve message.
105
+ resetPatterns();
106
+
107
+ const expectedQuads = await jsonldToQuads(expected);
108
+ const actualQuads = await jsonldToQuads(actual);
109
+ const expectedTurtle = quadsToTurtle(expectedQuads);
110
+ const actualTurtle = quadsToTurtle(actualQuads);
111
+ const result = (success: boolean, message: string) => ({
112
+ success,
113
+ message,
114
+ expected: expectedTurtle,
115
+ actual: actualTurtle,
116
+ });
117
+
118
+ if (expectedQuads.length !== actualQuads.length)
119
+ return result(false, `Expected ${expectedQuads.length} triples, found ${actualQuads.length}.`);
120
+
121
+ try {
122
+ assertExpectedQuadsExist(expectedQuads, actualQuads);
123
+ } catch (error) {
124
+ if (!(error instanceof ExpectedQuadAssertionError)) throw error;
125
+
126
+ return result(false, error.message);
127
+ }
128
+
129
+ return result(true, 'jsonld matches');
130
+ }
131
+
132
+ export function sparqlEquals(expected: string, actual: string): EqualityResult {
133
+ // TODO catch parsing errors and improve message.
134
+ resetPatterns();
135
+
136
+ const expectedOperations = sparqlToQuadsSync(expected, { normalizeBlankNodes: true });
137
+ const actualOperations = sparqlToQuadsSync(actual, { normalizeBlankNodes: true });
138
+ const result = (success: boolean, message: string) => ({ success, message, expected, actual });
139
+
140
+ for (const operation of Object.keys(expectedOperations)) {
141
+ if (!(operation in actualOperations)) return result(false, `Couldn't find expected ${operation} operation.`);
142
+
143
+ const expectedQuads = pull(expectedOperations, operation);
144
+ const actualQuads = pull(actualOperations, operation);
145
+
146
+ if (expectedQuads.length !== actualQuads.length)
147
+ return result(false, `Expected ${expectedQuads.length} ${operation} triples, found ${actualQuads.length}.`);
148
+
149
+ try {
150
+ assertExpectedQuadsExist(expectedQuads, actualQuads);
151
+ } catch (error) {
152
+ if (!(error instanceof ExpectedQuadAssertionError)) throw error;
153
+
154
+ return result(
155
+ false,
156
+ `Couldn't find the following ${operation} triple: ${quadToTurtle(error.expectedQuad)}`,
157
+ );
158
+ }
159
+ }
160
+
161
+ const unexpectedOperation = Object.keys(actualOperations)[0] ?? null;
162
+ if (unexpectedOperation) return result(false, `Did not expect to find ${unexpectedOperation} triples.`);
163
+
164
+ return result(true, 'sparql matches');
165
+ }
166
+
167
+ export function turtleEquals(expected: string, actual: string): EqualityResult {
168
+ // TODO catch parsing errors and improve message.
169
+ resetPatterns();
170
+
171
+ const expectedQuads = turtleToQuadsSync(expected, { normalizeBlankNodes: true });
172
+ const actualQuads = turtleToQuadsSync(actual, { normalizeBlankNodes: true });
173
+ const result = (success: boolean, message: string) => ({ success, message, expected, actual });
174
+
175
+ if (expectedQuads.length !== actualQuads.length)
176
+ return result(false, `Expected ${expectedQuads.length} triples, found ${actualQuads.length}.`);
177
+
178
+ try {
179
+ assertExpectedQuadsExist(expectedQuads, actualQuads);
180
+ } catch (error) {
181
+ if (!(error instanceof ExpectedQuadAssertionError)) throw error;
182
+
183
+ return result(false, error.message);
184
+ }
185
+
186
+ return result(true, 'turtle matches');
187
+ }