@fedify/vocab-tools 2.0.0-pr.458.1785

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/codec.ts ADDED
@@ -0,0 +1,485 @@
1
+ import metadata from "../deno.json" with { type: "json" };
2
+ import { generateField, getFieldName } from "./field.ts";
3
+ import type { TypeSchema } from "./schema.ts";
4
+ import { isNonFunctionalProperty } from "./schema.ts";
5
+ import {
6
+ areAllScalarTypes,
7
+ emitOverride,
8
+ getAllProperties,
9
+ getDecoder,
10
+ getDecoders,
11
+ getEncoders,
12
+ getSubtypes,
13
+ isCompactableType,
14
+ } from "./type.ts";
15
+
16
+ export async function* generateEncoder(
17
+ typeUri: string,
18
+ types: Record<string, TypeSchema>,
19
+ ): AsyncIterable<string> {
20
+ const type = types[typeUri];
21
+ yield `
22
+ /**
23
+ * Converts this object to a JSON-LD structure.
24
+ * @param options The options to use.
25
+ * - \`format\`: The format of the output: \`compact\` or
26
+ \`expand\`.
27
+ * - \`contextLoader\`: The loader for remote JSON-LD contexts.
28
+ * - \`context\`: The JSON-LD context to use. Not applicable
29
+ when \`format\` is set to \`'expand'\`.
30
+ * @returns The JSON-LD representation of this object.
31
+ */
32
+ ${emitOverride(typeUri, types)} async toJsonLd(options: {
33
+ format?: "compact" | "expand",
34
+ contextLoader?: DocumentLoader,
35
+ context?: string | Record<string, string> | (string | Record<string, string>)[],
36
+ } = {}): Promise<unknown> {
37
+ if (options.format == null && this._cachedJsonLd != null) {
38
+ return this._cachedJsonLd;
39
+ }
40
+ if (options.format !== "compact" && options.context != null) {
41
+ throw new TypeError(
42
+ "The context option can only be used when the format option is set " +
43
+ "to 'compact'."
44
+ );
45
+ }
46
+ options = {
47
+ ...options,
48
+ contextLoader: options.contextLoader ?? getDocumentLoader(),
49
+ };
50
+ `;
51
+ if (isCompactableType(typeUri, types)) {
52
+ yield `
53
+ if (options.format == null && this.isCompactable()) {
54
+ `;
55
+ if (type.extends == null) {
56
+ yield "const result: Record<string, unknown> = {};";
57
+ } else {
58
+ yield `
59
+ const result = await super.toJsonLd({
60
+ ...options,
61
+ format: undefined,
62
+ context: undefined,
63
+ }) as Record<string, unknown>;
64
+ `;
65
+ const selfProperties = type.properties.map((p) => p.uri);
66
+ for (const property of getAllProperties(typeUri, types, true)) {
67
+ if (!selfProperties.includes(property.uri)) continue;
68
+ yield `delete result[${JSON.stringify(property.compactName)}];`;
69
+ }
70
+ }
71
+ yield `
72
+ // deno-lint-ignore no-unused-vars
73
+ let compactItems: unknown[];
74
+ `;
75
+ for (const property of type.properties) {
76
+ yield `
77
+ compactItems = [];
78
+ for (const v of this.${await getFieldName(property.uri)}) {
79
+ const item = (
80
+ `;
81
+ if (!areAllScalarTypes(property.range, types)) {
82
+ yield "v instanceof URL ? v.href : ";
83
+ }
84
+ const encoders = getEncoders(
85
+ property.range,
86
+ types,
87
+ "v",
88
+ "options",
89
+ true,
90
+ );
91
+ for (const code of encoders) yield code;
92
+ yield `
93
+ );
94
+ compactItems.push(item);
95
+ }
96
+ if (compactItems.length > 0) {
97
+ `;
98
+ if (
99
+ property.functional ||
100
+ (isNonFunctionalProperty(property) && property.container !== "list")
101
+ ) {
102
+ yield `
103
+ result[${JSON.stringify(property.compactName)}]
104
+ = compactItems.length > 1
105
+ ? compactItems
106
+ : compactItems[0];
107
+ `;
108
+ if (property.functional && property.redundantProperties != null) {
109
+ for (const prop of property.redundantProperties) {
110
+ yield `
111
+ result[${JSON.stringify(prop.compactName)}]
112
+ = compactItems.length > 1
113
+ ? compactItems
114
+ : compactItems[0];
115
+ `;
116
+ }
117
+ }
118
+ } else {
119
+ yield `
120
+ result[${JSON.stringify(property.compactName)}] = compactItems;
121
+ `;
122
+ }
123
+ yield `
124
+ }
125
+ `;
126
+ }
127
+ yield `
128
+ result["type"] = ${JSON.stringify(type.compactName ?? type.uri)};
129
+ if (this.id != null) result["id"] = this.id.href;
130
+ result["@context"] = ${JSON.stringify(type.defaultContext)};
131
+ return result;
132
+ }
133
+ `;
134
+ }
135
+ yield `
136
+ // deno-lint-ignore no-unused-vars prefer-const
137
+ let array: unknown[];
138
+ `;
139
+ if (type.extends == null) {
140
+ yield "const values: Record<string, unknown[] | string> = {};";
141
+ } else {
142
+ yield `
143
+ const baseValues = await super.toJsonLd({
144
+ ...options,
145
+ format: "expand",
146
+ context: undefined,
147
+ }) as unknown[];
148
+ const values = baseValues[0] as Record<
149
+ string,
150
+ unknown[] | { "@list": unknown[] } | string
151
+ >;
152
+ `;
153
+ }
154
+ for (const property of type.properties) {
155
+ yield `
156
+ array = [];
157
+ for (const v of this.${await getFieldName(property.uri)}) {
158
+ const element = (
159
+ `;
160
+ if (!areAllScalarTypes(property.range, types)) {
161
+ yield 'v instanceof URL ? { "@id": v.href } : ';
162
+ }
163
+ for (const code of getEncoders(property.range, types, "v", "options")) {
164
+ yield code;
165
+ }
166
+ yield `
167
+ );
168
+ `;
169
+ if (isNonFunctionalProperty(property) && property.container === "graph") {
170
+ yield `array.push({ "@graph": element });`;
171
+ } else {
172
+ yield `array.push(element);`;
173
+ }
174
+ yield `;
175
+ }
176
+ if (array.length > 0) {
177
+ const propValue = (
178
+ `;
179
+ if (isNonFunctionalProperty(property) && property.container === "list") {
180
+ yield `{ "@list": array }`;
181
+ } else {
182
+ yield `array`;
183
+ }
184
+ yield `
185
+ );
186
+ values[${JSON.stringify(property.uri)}] = propValue;
187
+ `;
188
+ if (property.functional && property.redundantProperties != null) {
189
+ for (const prop of property.redundantProperties) {
190
+ yield `
191
+ values[${JSON.stringify(prop.uri)}] = propValue;
192
+ `;
193
+ }
194
+ }
195
+ yield `
196
+ }
197
+ `;
198
+ }
199
+ yield `
200
+ values["@type"] = [${JSON.stringify(type.uri)}];
201
+ if (this.id != null) values["@id"] = this.id.href;
202
+ if (options.format === "expand") {
203
+ return await jsonld.expand(
204
+ values,
205
+ { documentLoader: options.contextLoader },
206
+ );
207
+ }
208
+ const docContext = options.context ??
209
+ ${JSON.stringify(type.defaultContext)};
210
+ const compacted = await jsonld.compact(
211
+ values,
212
+ docContext,
213
+ { documentLoader: options.contextLoader },
214
+ );
215
+ if (docContext != null) {
216
+ // Embed context
217
+ `;
218
+ const supertypes: string[] = [];
219
+ for (
220
+ let uri: string | undefined = typeUri;
221
+ uri != null;
222
+ uri = types[uri].extends
223
+ ) {
224
+ supertypes.push(uri);
225
+ }
226
+ for (const supertype of supertypes) {
227
+ for (const property of types[supertype].properties) {
228
+ if (property.embedContext == null) continue;
229
+ const compactName = property.embedContext.compactName;
230
+ yield `
231
+ if (${JSON.stringify(compactName)} in compacted &&
232
+ compacted.${compactName} != null) {
233
+ if (Array.isArray(compacted.${compactName})) {
234
+ for (const element of compacted.${compactName}) {
235
+ element["@context"] = docContext;
236
+ }
237
+ } else {
238
+ compacted.${compactName}["@context"] = docContext;
239
+ }
240
+ }
241
+ `;
242
+ }
243
+ }
244
+ yield `
245
+ }
246
+ return compacted;
247
+ }
248
+
249
+ protected ${emitOverride(typeUri, types)} isCompactable(): boolean {
250
+ `;
251
+ for (const property of type.properties) {
252
+ if (!property.range.every((r) => isCompactableType(r, types))) {
253
+ yield `
254
+ if (
255
+ this.${await getFieldName(property.uri)} != null &&
256
+ this.${await getFieldName(property.uri)}.length > 0
257
+ ) return false;
258
+ `;
259
+ }
260
+ }
261
+ yield `
262
+ return ${type.extends == null ? "true" : "super.isCompactable()"};
263
+ }
264
+ `;
265
+ }
266
+
267
+ export async function* generateDecoder(
268
+ typeUri: string,
269
+ types: Record<string, TypeSchema>,
270
+ ): AsyncIterable<string> {
271
+ const type = types[typeUri];
272
+ yield `
273
+ /**
274
+ * Converts a JSON-LD structure to an object of this type.
275
+ * @param json The JSON-LD structure to convert.
276
+ * @param options The options to use.
277
+ * - \`documentLoader\`: The loader for remote JSON-LD documents.
278
+ * - \`contextLoader\`: The loader for remote JSON-LD contexts.
279
+ * - \`tracerProvider\`: The OpenTelemetry tracer provider to use.
280
+ * If omitted, the global tracer provider is used.
281
+ * @returns The object of this type.
282
+ * @throws {TypeError} If the given \`json\` is invalid.
283
+ */
284
+ static ${emitOverride(typeUri, types)} async fromJsonLd(
285
+ json: unknown,
286
+ options: {
287
+ documentLoader?: DocumentLoader,
288
+ contextLoader?: DocumentLoader,
289
+ tracerProvider?: TracerProvider,
290
+ baseUrl?: URL,
291
+ } = {},
292
+ ): Promise<${type.name}> {
293
+ const tracerProvider = options.tracerProvider ?? trace.getTracerProvider();
294
+ const tracer = tracerProvider.getTracer(
295
+ ${JSON.stringify(metadata.name)},
296
+ ${JSON.stringify(metadata.version)},
297
+ );
298
+ return await tracer.startActiveSpan(
299
+ "activitypub.parse_object",
300
+ async (span) => {
301
+ try {
302
+ const object = await this.__fromJsonLd__${type.name}__(
303
+ json, span, options);
304
+ if (object.id != null) {
305
+ span.setAttribute("activitypub.object.id", object.id.href);
306
+ }
307
+ return object;
308
+ } catch (error) {
309
+ span.setStatus({
310
+ code: SpanStatusCode.ERROR,
311
+ message: String(error),
312
+ });
313
+ throw error;
314
+ } finally {
315
+ span.end();
316
+ }
317
+ },
318
+ );
319
+ }
320
+
321
+ protected static async __fromJsonLd__${type.name}__(
322
+ json: unknown,
323
+ span: Span,
324
+ options: {
325
+ documentLoader?: DocumentLoader,
326
+ contextLoader?: DocumentLoader,
327
+ tracerProvider?: TracerProvider,
328
+ baseUrl?: URL,
329
+ } = {},
330
+ ): Promise<${type.name}> {
331
+ if (typeof json === "undefined") {
332
+ throw new TypeError("Invalid JSON-LD: undefined.");
333
+ }
334
+ else if (json === null) throw new TypeError("Invalid JSON-LD: null.");
335
+ options = {
336
+ ...options,
337
+ documentLoader: options.documentLoader ?? getDocumentLoader(),
338
+ contextLoader: options.contextLoader ?? getDocumentLoader(),
339
+ tracerProvider: options.tracerProvider ?? trace.getTracerProvider(),
340
+ };
341
+ // deno-lint-ignore no-explicit-any
342
+ let values: Record<string, any[]> & { "@id"?: string };
343
+ if (globalThis.Object.keys(json).length == 0) {
344
+ values = {};
345
+ } else {
346
+ const expanded = await jsonld.expand(json, {
347
+ documentLoader: options.contextLoader,
348
+ keepFreeFloatingNodes: true,
349
+ });
350
+ values =
351
+ // deno-lint-ignore no-explicit-any
352
+ (expanded[0] ?? {}) as (Record<string, any[]> & { "@id"?: string });
353
+ }
354
+ if (options.baseUrl == null && values["@id"] != null) {
355
+ options = { ...options, baseUrl: new URL(values["@id"]) };
356
+ }
357
+ `;
358
+ const subtypes = getSubtypes(typeUri, types, true);
359
+ yield `
360
+ if ("@type" in values) {
361
+ span.setAttribute("activitypub.object.type", values["@type"]);
362
+ }
363
+ if ("@type" in values &&
364
+ !values["@type"].every(t => t.startsWith("_:"))) {
365
+ `;
366
+ for (const subtypeUri of subtypes) {
367
+ yield `
368
+ if (values["@type"].includes(${JSON.stringify(subtypeUri)})) {
369
+ return await ${types[subtypeUri].name}.fromJsonLd(json, options);
370
+ }
371
+ `;
372
+ }
373
+ yield `
374
+ if (!values["@type"].includes(${JSON.stringify(typeUri)})) {
375
+ throw new TypeError("Invalid type: " + values["@type"]);
376
+ }
377
+ }
378
+ `;
379
+ if (type.extends == null) {
380
+ yield `
381
+ const instance = new this(
382
+ { id: "@id" in values ? new URL(values["@id"] as string) : undefined },
383
+ options,
384
+ );
385
+ `;
386
+ } else {
387
+ yield `
388
+ delete values["@type"];
389
+ const instance = await super.fromJsonLd(values, {
390
+ ...options,
391
+ // @ts-ignore: an internal option
392
+ _fromSubclass: true,
393
+ });
394
+ if (!(instance instanceof ${type.name})) {
395
+ throw new TypeError("Unexpected type: " + instance.constructor.name);
396
+ }
397
+ `;
398
+ }
399
+ for (const property of type.properties) {
400
+ const variable = await getFieldName(property.uri, "");
401
+ yield await generateField(property, types, "const ");
402
+ const arrayVariable = `${variable}__array`;
403
+ yield `
404
+ let ${arrayVariable} = values[${JSON.stringify(property.uri)}];
405
+ `;
406
+ if (property.functional && property.redundantProperties != null) {
407
+ for (const prop of property.redundantProperties) {
408
+ yield `
409
+ if (${arrayVariable} == null || ${arrayVariable}.length < 1) {
410
+ ${arrayVariable} = values[${JSON.stringify(prop.uri)}];
411
+ }
412
+ `;
413
+ }
414
+ }
415
+ yield `
416
+ for (
417
+ const v of ${arrayVariable} == null
418
+ ? []
419
+ : ${arrayVariable}.length === 1 && "@list" in ${arrayVariable}[0]
420
+ ? ${arrayVariable}[0]["@list"]
421
+ : ${arrayVariable}
422
+ ) {
423
+ if (v == null) continue;
424
+ `;
425
+ if (!areAllScalarTypes(property.range, types)) {
426
+ yield `
427
+ if (typeof v === "object" && "@id" in v && !("@type" in v)
428
+ && globalThis.Object.keys(v).length === 1) {
429
+ ${variable}.push(
430
+ !URL.canParse(v["@id"]) && v["@id"].startsWith("at://")
431
+ ? new URL("at://" + encodeURIComponent(v["@id"].substring(5)))
432
+ : new URL(v["@id"])
433
+ );
434
+ continue;
435
+ }
436
+ `;
437
+ }
438
+ if (property.range.length == 1) {
439
+ yield `${variable}.push(${
440
+ getDecoder(
441
+ property.range[0],
442
+ types,
443
+ "v",
444
+ "options",
445
+ `(values["@id"] == null ? options.baseUrl : new URL(values["@id"]))`,
446
+ )
447
+ })`;
448
+ } else {
449
+ yield `
450
+ const decoded =
451
+ `;
452
+ const decoders = getDecoders(
453
+ property.range,
454
+ types,
455
+ "v",
456
+ "options",
457
+ `(values["@id"] == null ? options.baseUrl : new URL(values["@id"]))`,
458
+ );
459
+ for (const code of decoders) yield code;
460
+ yield `
461
+ ;
462
+ if (typeof decoded === "undefined") continue;
463
+ ${variable}.push(decoded);
464
+ `;
465
+ }
466
+ yield `
467
+ }
468
+ instance.${await getFieldName(property.uri)} = ${variable};
469
+ `;
470
+ }
471
+ yield `
472
+ if (!("_fromSubclass" in options) || !options._fromSubclass) {
473
+ try {
474
+ instance._cachedJsonLd = structuredClone(json);
475
+ } catch {
476
+ getLogger(["fedify", "vocab"]).warn(
477
+ "Failed to cache JSON-LD: {json}",
478
+ { json },
479
+ );
480
+ }
481
+ }
482
+ return instance;
483
+ }
484
+ `;
485
+ }