@fedify/vocab-tools 2.4.0-dev.1570 → 2.4.0-dev.1571

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/src/class.ts CHANGED
@@ -3,9 +3,41 @@ import { generateCloner, generateConstructor } from "./constructor.ts";
3
3
  import { generateFields } from "./field.ts";
4
4
  import { generateInspector, generateInspectorPostClass } from "./inspector.ts";
5
5
  import { generateProperties } from "./property.ts";
6
- import { type TypeSchema, validateTypeSchemas } from "./schema.ts";
6
+ import {
7
+ type PropertySchema,
8
+ type TypeSchema,
9
+ validateTypeSchemas,
10
+ } from "./schema.ts";
7
11
  import { emitOverride } from "./type.ts";
8
12
 
13
+ const XSD_ANY_URI = "http://www.w3.org/2001/XMLSchema#anyURI";
14
+ const FEDIFY_URL = "fedify:url";
15
+ const INTERNAL_RUNTIME_IMPORTS = [
16
+ "compactJsonLdCache",
17
+ "getJsonLdContext",
18
+ "isTrustedIriOrigin",
19
+ "normalizeJsonLdIris",
20
+ ].join(",\n ");
21
+ const RUNTIME_IMPORTS = [
22
+ "canParseDecimal",
23
+ "decodeMultibase",
24
+ "type Decimal",
25
+ "type DocumentLoader",
26
+ "encodeMultibase",
27
+ "exportMultibaseKey",
28
+ "exportSpki",
29
+ "formatIri",
30
+ "getDocumentLoader",
31
+ "importMultibaseKey",
32
+ "importPem",
33
+ "isDecimal",
34
+ "LanguageString",
35
+ "parseDecimal",
36
+ "parseIri",
37
+ "parseJsonLdId",
38
+ "type RemoteDocument",
39
+ ].join(",\n ");
40
+
9
41
  /**
10
42
  * Sorts the given types topologically so that the base types come before the
11
43
  * extended types.
@@ -169,6 +201,40 @@ export function getEntityTypeById(id: string | URL): $EntityType | undefined {
169
201
  `;
170
202
  }
171
203
 
204
+ function canContainIriValue(
205
+ property: PropertySchema,
206
+ types: Record<string, TypeSchema>,
207
+ ): boolean {
208
+ return property.range.some((typeUri) =>
209
+ typeUri === XSD_ANY_URI || typeUri === FEDIFY_URL || types[typeUri]?.entity
210
+ );
211
+ }
212
+
213
+ function addKeys(
214
+ keys: Set<string>,
215
+ property: { uri: string; compactName?: string },
216
+ ): void {
217
+ keys.add(property.uri);
218
+ if (property.compactName != null) keys.add(property.compactName);
219
+ }
220
+
221
+ function addPortableIriKeys(
222
+ keys: Set<string>,
223
+ property: PropertySchema,
224
+ types: Record<string, TypeSchema>,
225
+ ): void {
226
+ if (!canContainIriValue(property, types)) return;
227
+ addKeys(keys, property);
228
+ if (
229
+ "redundantProperties" in property &&
230
+ property.redundantProperties != null
231
+ ) {
232
+ for (const redundantProperty of property.redundantProperties) {
233
+ addKeys(keys, redundantProperty);
234
+ }
235
+ }
236
+ }
237
+
172
238
  /**
173
239
  * Generates the TypeScript classes from the given types.
174
240
  * @param types The types to generate classes from.
@@ -178,35 +244,27 @@ export async function* generateClasses(
178
244
  types: Record<string, TypeSchema>,
179
245
  ): AsyncIterable<string> {
180
246
  validateTypeSchemas(types);
181
- const runtimeImports = [
182
- "canParseDecimal",
183
- "decodeMultibase",
184
- "type Decimal",
185
- "type DocumentLoader",
186
- "encodeMultibase",
187
- "exportMultibaseKey",
188
- "exportSpki",
189
- "getDocumentLoader",
190
- "importMultibaseKey",
191
- "importPem",
192
- "isDecimal",
193
- "LanguageString",
194
- "parseDecimal",
195
- "type RemoteDocument",
196
- ];
197
247
  yield "// deno-lint-ignore-file ban-unused-ignore no-explicit-any no-unused-vars prefer-const verbatim-module-syntax\n";
198
248
  yield 'import jsonld from "@fedify/vocab-runtime/jsonld";\n';
199
249
  yield 'import { getLogger } from "@logtape/logtape";\n';
200
250
  yield `import { type Span, SpanStatusCode, type TracerProvider, trace }
201
251
  from "@opentelemetry/api";\n`;
202
- yield `import {\n ${
203
- runtimeImports.join(",\n ")
204
- }\n} from "@fedify/vocab-runtime";\n`;
252
+ yield `import {\n ${RUNTIME_IMPORTS}\n} from "@fedify/vocab-runtime";\n`;
253
+ yield `import {\n ${INTERNAL_RUNTIME_IMPORTS}\n} from "@fedify/vocab-runtime/internal/jsonld-cache";\n`;
205
254
  yield `import {
206
255
  isTemporalDuration,
207
256
  isTemporalInstant,
208
257
  } from "@fedify/vocab-runtime/temporal";\n`;
209
- yield "\n\n";
258
+ const portableIriKeys = new Set(["@id", "id"]);
259
+ for (const type of Object.values(types)) {
260
+ for (const property of type.properties) {
261
+ addPortableIriKeys(portableIriKeys, property, types);
262
+ }
263
+ }
264
+ yield "const PORTABLE_IRI_PATTERN = /^ap(?:\\+ef61)?:\\/\\//i;\n";
265
+ yield `const PORTABLE_IRI_KEYS: ReadonlySet<string> = new Set(${
266
+ JSON.stringify([...portableIriKeys].sort())
267
+ });\n\n`;
210
268
  const moduleVarNames = new Map<string, string>();
211
269
  const sorted = sortTopologically(types);
212
270
  for (const typeUri of sorted) {
package/src/codec.ts CHANGED
@@ -127,7 +127,7 @@ export async function* generateEncoder(
127
127
  const item = (
128
128
  `;
129
129
  if (!areAllScalarTypes(property.range, types)) {
130
- yield "v instanceof URL ? v.href : ";
130
+ yield "v instanceof URL ? formatIri(v) : ";
131
131
  }
132
132
  const encoders = getEncoders(
133
133
  property.range,
@@ -178,7 +178,7 @@ export async function* generateEncoder(
178
178
  ? ""
179
179
  : `result["type"] = ${JSON.stringify(type.compactName ?? type.uri)};`
180
180
  }
181
- if (this.id != null) result["id"] = this.id.href;
181
+ if (this.id != null) result["id"] = formatIri(this.id);
182
182
  result["@context"] = ${JSON.stringify(type.defaultContext)};
183
183
  return result;
184
184
  }
@@ -210,7 +210,7 @@ export async function* generateEncoder(
210
210
  const element = (
211
211
  `;
212
212
  if (!areAllScalarTypes(property.range, types)) {
213
- yield 'v instanceof URL ? { "@id": v.href } : ';
213
+ yield 'v instanceof URL ? { "@id": formatIri(v) } : ';
214
214
  }
215
215
  for (const code of getEncoders(property.range, types, "v", "options")) {
216
216
  yield code;
@@ -250,7 +250,7 @@ export async function* generateEncoder(
250
250
  }
251
251
  yield `
252
252
  ${type.typeless ? "" : `values["@type"] = [${JSON.stringify(type.uri)}];`}
253
- if (this.id != null) values["@id"] = this.id.href;
253
+ if (this.id != null) values["@id"] = formatIri(this.id);
254
254
  if (options.format === "expand") {
255
255
  return await jsonld.expand(
256
256
  values,
@@ -393,10 +393,12 @@ export async function* generateDecoder(
393
393
  };
394
394
  // deno-lint-ignore no-explicit-any
395
395
  let values: Record<string, any[]> & { "@id"?: string };
396
+ let expanded: unknown[];
396
397
  if (globalThis.Object.keys(json).length == 0) {
397
398
  values = {};
399
+ expanded = [values];
398
400
  } else {
399
- const expanded = await jsonld.expand(json, {
401
+ expanded = await jsonld.expand(json, {
400
402
  documentLoader: options.contextLoader,
401
403
  keepFreeFloatingNodes: true,
402
404
  });
@@ -404,11 +406,9 @@ export async function* generateDecoder(
404
406
  // deno-lint-ignore no-explicit-any
405
407
  (expanded[0] ?? {}) as (Record<string, any[]> & { "@id"?: string });
406
408
  }
407
- if (values["@id"] != null && !values["@id"].startsWith("_:") && !URL.canParse(values["@id"], options.baseUrl)) {
408
- throw new TypeError("Invalid @id: " + values["@id"]);
409
- }
410
- if (options.baseUrl == null && values["@id"] != null && !values["@id"].startsWith("_:") && URL.canParse(values["@id"])) {
411
- options = { ...options, baseUrl: new URL(values["@id"]) };
409
+ const id = parseJsonLdId(values["@id"], options.baseUrl);
410
+ if (id != null && options.baseUrl == null) {
411
+ options = { ...options, baseUrl: id };
412
412
  }
413
413
  `;
414
414
  const subtypes = getSubtypes(typeUri, types, true);
@@ -432,10 +432,24 @@ export async function* generateDecoder(
432
432
  }
433
433
  }
434
434
  `;
435
+ yield `
436
+ let cacheJsonLd = !("_fromSubclass" in options) || !options._fromSubclass
437
+ ? normalizeJsonLdIris(
438
+ expanded,
439
+ PORTABLE_IRI_KEYS,
440
+ PORTABLE_IRI_PATTERN,
441
+ )
442
+ : undefined;
443
+ if (cacheJsonLd != null && cacheJsonLd !== expanded) {
444
+ cacheJsonLd = structuredClone(cacheJsonLd);
445
+ }
446
+ `;
435
447
  if (type.extends == null) {
436
448
  yield `
437
449
  const instance = new this(
438
- { id: values["@id"] != null && !values["@id"].startsWith("_:") && URL.canParse(values["@id"], options.baseUrl) ? new URL(values["@id"], options.baseUrl) : undefined },
450
+ {
451
+ id
452
+ },
439
453
  options,
440
454
  );
441
455
  `;
@@ -491,11 +505,7 @@ export async function* generateDecoder(
491
505
  if (typeof v === "object" && "@id" in v && !("@type" in v)
492
506
  && globalThis.Object.keys(v).length === 1) {
493
507
  if (v["@id"].startsWith("_:")) continue;
494
- ${variable}.push(
495
- !URL.canParse(v["@id"], ${propertyBaseUrl}) && v["@id"].startsWith("at://")
496
- ? new URL("at://" + encodeURIComponent(v["@id"].substring(5)))
497
- : new URL(v["@id"], ${propertyBaseUrl})
498
- );
508
+ ${variable}.push(parseIri(v["@id"], ${propertyBaseUrl}));
499
509
  continue;
500
510
  }
501
511
  `;
@@ -539,7 +549,19 @@ export async function* generateDecoder(
539
549
  yield `
540
550
  if (!("_fromSubclass" in options) || !options._fromSubclass) {
541
551
  try {
542
- instance._cachedJsonLd = structuredClone(json);
552
+ if (cacheJsonLd != null && cacheJsonLd !== expanded) {
553
+ const compactArray = Array.isArray(json) && json.length === 1;
554
+ const jsonLd = compactArray ? json[0] : json;
555
+ const normalized = cacheJsonLd;
556
+ const cachedJsonLd = await compactJsonLdCache(
557
+ normalized,
558
+ jsonLd,
559
+ options.contextLoader,
560
+ );
561
+ instance._cachedJsonLd = compactArray ? [cachedJsonLd] : cachedJsonLd;
562
+ } else {
563
+ instance._cachedJsonLd = structuredClone(json);
564
+ }
543
565
  } catch {
544
566
  getLogger(["fedify", "vocab"]).warn(
545
567
  "Failed to cache JSON-LD: {json}",
package/src/property.ts CHANGED
@@ -86,9 +86,10 @@ async function* generateProperty(
86
86
  ${JSON.stringify(metadata.version)},
87
87
  );
88
88
  return await tracer.startActiveSpan("activitypub.lookup_object", async (span) => {
89
+ const lookupUrl = formatIri(url);
89
90
  let fetchResult: RemoteDocument;
90
91
  try {
91
- fetchResult = await documentLoader(url.href);
92
+ fetchResult = await documentLoader(lookupUrl);
92
93
  } catch (error) {
93
94
  span.setStatus({
94
95
  code: SpanStatusCode.ERROR,
@@ -98,21 +99,20 @@ async function* generateProperty(
98
99
  if (options.suppressError) {
99
100
  getLogger(["fedify", "vocab"]).error(
100
101
  "Failed to fetch {url}: {error}",
101
- { error, url: url.href }
102
+ { error, url: lookupUrl }
102
103
  );
103
104
  return null;
104
105
  }
105
106
  throw error;
106
107
  }
107
108
  const { document, documentUrl } = fetchResult;
108
- const baseUrl = new URL(documentUrl);
109
+ const baseUrl = parseIri(documentUrl);
109
110
  try {
110
111
  const obj = await this.#${property.singularName}_fromJsonLd(
111
112
  document,
112
113
  { documentLoader, contextLoader, tracerProvider, baseUrl }
113
114
  );
114
- if (options.crossOrigin !== "trust" && obj?.id != null &&
115
- obj.id.origin !== baseUrl.origin) {
115
+ if (obj?.id != null && !isTrustedIriOrigin(options, obj.id, baseUrl)) {
116
116
  if (options.crossOrigin === "throw") {
117
117
  throw new Error(
118
118
  "The object's @id (" + obj.id.href + ") has a different origin " +
@@ -141,7 +141,7 @@ async function* generateProperty(
141
141
  if (options.suppressError) {
142
142
  getLogger(["fedify", "vocab"]).error(
143
143
  "Failed to parse {url}: {error}",
144
- { error: e, url: url.href }
144
+ { error: e, url: lookupUrl }
145
145
  );
146
146
  return null;
147
147
  }
@@ -275,8 +275,9 @@ async function* generateProperty(
275
275
  }
276
276
  if (this.${await getFieldName(property.uri)}.length < 1) return null;
277
277
  let v = this.${await getFieldName(property.uri)}[0];
278
- if (options.crossOrigin !== "trust" && !(v instanceof URL) &&
279
- v.id != null && v.id.origin !== this.id?.origin &&
278
+ if (!(v instanceof URL) &&
279
+ v.id != null &&
280
+ !isTrustedIriOrigin(options, v.id, this.id) &&
280
281
  !this.${await getFieldName(property.uri, "#_trust")}.has(0)) {
281
282
  v = v.id;
282
283
  }
@@ -315,8 +316,8 @@ async function* generateProperty(
315
316
  `;
316
317
  }
317
318
  yield `
318
- if (options.crossOrigin !== "trust" && v?.id != null &&
319
- this.id != null && v.id.origin !== this.id.origin &&
319
+ if (v?.id != null &&
320
+ this.id != null && !isTrustedIriOrigin(options, v.id, this.id) &&
320
321
  !this.${await getFieldName(property.uri, "#_trust")}.has(0)) {
321
322
  if (options.crossOrigin === "throw") {
322
323
  throw new Error(
@@ -380,8 +381,9 @@ async function* generateProperty(
380
381
  const vs = this.${await getFieldName(property.uri)};
381
382
  for (let i = 0; i < vs.length; i++) {
382
383
  let v = vs[i];
383
- if (options.crossOrigin !== "trust" && !(v instanceof URL) &&
384
- v.id != null && v.id.origin !== this.id?.origin &&
384
+ if (!(v instanceof URL) &&
385
+ v.id != null &&
386
+ !isTrustedIriOrigin(options, v.id, this.id) &&
385
387
  !this.${await getFieldName(property.uri, "#_trust")}.has(i)) {
386
388
  v = v.id;
387
389
  }
@@ -421,9 +423,9 @@ async function* generateProperty(
421
423
  `;
422
424
  }
423
425
  yield `
424
- if (options.crossOrigin !== "trust" && v?.id != null &&
425
- this.id != null && v.id.origin !== this.id.origin &&
426
- !this.${await getFieldName(property.uri, "#_trust")}.has(0)) {
426
+ if (v?.id != null &&
427
+ this.id != null && !isTrustedIriOrigin(options, v.id, this.id) &&
428
+ !this.${await getFieldName(property.uri, "#_trust")}.has(i)) {
427
429
  if (options.crossOrigin === "throw") {
428
430
  throw new Error(
429
431
  "The property object's @id (" + v.id.href + ") has a different " +
package/src/type.ts CHANGED
@@ -157,10 +157,10 @@ const scalarTypes: Record<string, ScalarType> = {
157
157
  return `${v} instanceof URL`;
158
158
  },
159
159
  encoder(v) {
160
- return `{ "@id": ${v}.href }`;
160
+ return `{ "@id": formatIri(${v}) }`;
161
161
  },
162
162
  compactEncoder(v) {
163
- return `${v}.href`;
163
+ return `formatIri(${v})`;
164
164
  },
165
165
  dataCheck(v) {
166
166
  return `${v} != null && typeof ${v} === "object" && "@id" in ${v}
@@ -168,22 +168,7 @@ const scalarTypes: Record<string, ScalarType> = {
168
168
  && ${v}["@id"] !== ""`;
169
169
  },
170
170
  decoder(v, baseUrlVar) {
171
- return `${v}["@id"].startsWith("at://")
172
- ? new URL("at://" +
173
- encodeURIComponent(
174
- ${v}["@id"].includes("/", 5)
175
- ? ${v}["@id"].slice(5, ${v}["@id"].indexOf("/", 5))
176
- : ${v}["@id"].slice(5)
177
- ) +
178
- (
179
- ${v}["@id"].includes("/", 5)
180
- ? ${v}["@id"].slice(${v}["@id"].indexOf("/", 5))
181
- : ""
182
- )
183
- )
184
- : URL.canParse(${v}["@id"]) && ${baseUrlVar}
185
- ? new URL(${v}["@id"])
186
- : new URL(${v}["@id"], ${baseUrlVar})`;
171
+ return `parseIri(${v}["@id"], ${baseUrlVar})`;
187
172
  },
188
173
  },
189
174
  "http://www.w3.org/1999/02/22-rdf-syntax-ns#langString": {
@@ -325,10 +310,10 @@ const scalarTypes: Record<string, ScalarType> = {
325
310
  return `${v} instanceof URL`;
326
311
  },
327
312
  encoder(v) {
328
- return `{ "@value": ${v}.href }`;
313
+ return `{ "@value": formatIri(${v}) }`;
329
314
  },
330
315
  compactEncoder(v) {
331
- return `${v}.href`;
316
+ return `formatIri(${v})`;
332
317
  },
333
318
  dataCheck(v) {
334
319
  return `typeof ${v} === "object" && "@value" in ${v}
@@ -336,7 +321,7 @@ const scalarTypes: Record<string, ScalarType> = {
336
321
  && ${v}["@value"] !== "" && ${v}["@value"] !== "/"`;
337
322
  },
338
323
  decoder(v) {
339
- return `new URL(${v}["@value"])`;
324
+ return `parseIri(${v}["@value"])`;
340
325
  },
341
326
  },
342
327
  "fedify:publicKey": {