@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.
@@ -0,0 +1,397 @@
1
+ import { pascalCase } from "es-toolkit";
2
+ import metadata from "../deno.json" with { type: "json" };
3
+ import { getFieldName } from "./field.ts";
4
+ import type { PropertySchema, TypeSchema } from "./schema.ts";
5
+ import { hasSingularAccessor, isNonFunctionalProperty } from "./schema.ts";
6
+ import { areAllScalarTypes, getTypeNames } from "./type.ts";
7
+
8
+ function emitOverride(
9
+ typeUri: string,
10
+ types: Record<string, TypeSchema>,
11
+ property: PropertySchema,
12
+ ): string {
13
+ const type = types[typeUri];
14
+ let supertypeUri = type.extends;
15
+ while (supertypeUri != null) {
16
+ const st = types[supertypeUri];
17
+ if (st.properties.find((p) => p.singularName === property.singularName)) {
18
+ return "override";
19
+ }
20
+ supertypeUri = st.extends;
21
+ }
22
+ return "";
23
+ }
24
+
25
+ async function* generateProperty(
26
+ type: TypeSchema,
27
+ property: PropertySchema,
28
+ types: Record<string, TypeSchema>,
29
+ ): AsyncIterable<string> {
30
+ const override = emitOverride(type.uri, types, property);
31
+ const doc = `\n/** ${property.description.replaceAll("\n", "\n * ")}\n */\n`;
32
+ if (areAllScalarTypes(property.range, types)) {
33
+ if (hasSingularAccessor(property)) {
34
+ yield doc;
35
+ yield `${override} get ${property.singularName}(): (${
36
+ getTypeNames(property.range, types)
37
+ } | null) {
38
+ if (this._warning != null) {
39
+ getLogger(this._warning.category).warn(
40
+ this._warning.message,
41
+ this._warning.values
42
+ );
43
+ }
44
+ if (this.${await getFieldName(property.uri)}.length < 1) return null;
45
+ return this.${await getFieldName(property.uri)}[0];
46
+ }
47
+ `;
48
+ }
49
+ if (isNonFunctionalProperty(property)) {
50
+ yield doc;
51
+ yield `get ${property.pluralName}(): (${
52
+ getTypeNames(property.range, types, true)
53
+ })[] {
54
+ return this.${await getFieldName(property.uri)};
55
+ }
56
+ `;
57
+ }
58
+ } else {
59
+ yield `
60
+ async #fetch${pascalCase(property.singularName)}(
61
+ url: URL,
62
+ options: {
63
+ documentLoader?: DocumentLoader,
64
+ contextLoader?: DocumentLoader,
65
+ suppressError?: boolean,
66
+ tracerProvider?: TracerProvider,
67
+ crossOrigin?: "ignore" | "throw" | "trust";
68
+ } = {},
69
+ ): Promise<${getTypeNames(property.range, types)} | null> {
70
+ const documentLoader =
71
+ options.documentLoader ?? this._documentLoader ?? getDocumentLoader();
72
+ const contextLoader =
73
+ options.contextLoader ?? this._contextLoader ?? getDocumentLoader();
74
+ const tracerProvider = options.tracerProvider ??
75
+ this._tracerProvider ?? trace.getTracerProvider();
76
+ const tracer = tracerProvider.getTracer(
77
+ ${JSON.stringify(metadata.name)},
78
+ ${JSON.stringify(metadata.version)},
79
+ );
80
+ return await tracer.startActiveSpan("activitypub.lookup_object", async (span) => {
81
+ let fetchResult: RemoteDocument;
82
+ try {
83
+ fetchResult = await documentLoader(url.href);
84
+ } catch (error) {
85
+ span.setStatus({
86
+ code: SpanStatusCode.ERROR,
87
+ message: String(error),
88
+ });
89
+ span.end();
90
+ if (options.suppressError) {
91
+ getLogger(["fedify", "vocab"]).error(
92
+ "Failed to fetch {url}: {error}",
93
+ { error, url: url.href }
94
+ );
95
+ return null;
96
+ }
97
+ throw error;
98
+ }
99
+ const { document, documentUrl } = fetchResult;
100
+ const baseUrl = new URL(documentUrl);
101
+ try {
102
+ const obj = await this.#${property.singularName}_fromJsonLd(
103
+ document,
104
+ { documentLoader, contextLoader, tracerProvider, baseUrl }
105
+ );
106
+ if (options.crossOrigin !== "trust" && obj?.id != null &&
107
+ obj.id.origin !== baseUrl.origin) {
108
+ if (options.crossOrigin === "throw") {
109
+ throw new Error(
110
+ "The object's @id (" + obj.id.href + ") has a different origin " +
111
+ "than the document URL (" + baseUrl.href + "); refusing to return " +
112
+ "the object. If you want to bypass this check and are aware of" +
113
+ 'the security implications, set the crossOrigin option to "trust".'
114
+ );
115
+ }
116
+ getLogger(["fedify", "vocab"]).warn(
117
+ "The object's @id ({objectId}) has a different origin than the document " +
118
+ "URL ({documentUrl}); refusing to return the object. If you want to " +
119
+ "bypass this check and are aware of the security implications, " +
120
+ 'set the crossOrigin option to "trust".',
121
+ { ...fetchResult, objectId: obj.id.href },
122
+ );
123
+ return null;
124
+ }
125
+ span.setAttribute("activitypub.object.id", (obj.id ?? url).href);
126
+ span.setAttribute(
127
+ "activitypub.object.type",
128
+ // @ts-ignore: obj.constructor always has a typeId.
129
+ obj.constructor.typeId.href
130
+ );
131
+ return obj;
132
+ } catch (e) {
133
+ if (options.suppressError) {
134
+ getLogger(["fedify", "vocab"]).error(
135
+ "Failed to parse {url}: {error}",
136
+ { error: e, url: url.href }
137
+ );
138
+ return null;
139
+ }
140
+ span.setStatus({
141
+ code: SpanStatusCode.ERROR,
142
+ message: String(e),
143
+ });
144
+ throw e;
145
+ } finally {
146
+ span.end();
147
+ }
148
+ });
149
+ }
150
+
151
+ async #${property.singularName}_fromJsonLd(
152
+ jsonLd: unknown,
153
+ options: {
154
+ documentLoader?: DocumentLoader,
155
+ contextLoader?: DocumentLoader,
156
+ tracerProvider?: TracerProvider,
157
+ baseUrl?: URL
158
+ }
159
+ ): Promise<${getTypeNames(property.range, types)}> {
160
+ const documentLoader =
161
+ options.documentLoader ?? this._documentLoader ?? getDocumentLoader();
162
+ const contextLoader =
163
+ options.contextLoader ?? this._contextLoader ?? getDocumentLoader();
164
+ const tracerProvider = options.tracerProvider ??
165
+ this._tracerProvider ?? trace.getTracerProvider();
166
+ const baseUrl = options.baseUrl;
167
+ `;
168
+ for (const range of property.range) {
169
+ if (!(range in types)) continue;
170
+ const rangeType = types[range];
171
+ yield `
172
+ try {
173
+ return await ${rangeType.name}.fromJsonLd(
174
+ jsonLd,
175
+ { documentLoader, contextLoader, tracerProvider, baseUrl },
176
+ );
177
+ } catch (e) {
178
+ if (!(e instanceof TypeError)) throw e;
179
+ }
180
+ `;
181
+ }
182
+ yield `
183
+ throw new TypeError("Expected an object of any type of: " +
184
+ ${JSON.stringify(property.range)}.join(", "));
185
+ }
186
+
187
+ `;
188
+ if (hasSingularAccessor(property)) {
189
+ yield `
190
+ /**
191
+ * Similar to
192
+ * {@link ${type.name}.get${pascalCase(property.singularName)}},
193
+ * but returns its \`@id\` URL instead of the object itself.
194
+ */
195
+ ${override} get ${property.singularName}Id(): URL | null {
196
+ if (this._warning != null) {
197
+ getLogger(this._warning.category).warn(
198
+ this._warning.message,
199
+ this._warning.values
200
+ );
201
+ }
202
+ if (this.${await getFieldName(property.uri)}.length < 1) return null;
203
+ const v = this.${await getFieldName(property.uri)}[0];
204
+ if (v instanceof URL) return v;
205
+ return v.id;
206
+ }
207
+ `;
208
+ yield doc;
209
+ yield `
210
+ ${override} async get${pascalCase(property.singularName)}(
211
+ options: {
212
+ documentLoader?: DocumentLoader,
213
+ contextLoader?: DocumentLoader,
214
+ suppressError?: boolean,
215
+ tracerProvider?: TracerProvider,
216
+ crossOrigin?: "ignore" | "throw" | "trust";
217
+ } = {}
218
+ ): Promise<${getTypeNames(property.range, types)} | null> {
219
+ if (this._warning != null) {
220
+ getLogger(this._warning.category).warn(
221
+ this._warning.message,
222
+ this._warning.values
223
+ );
224
+ }
225
+ if (this.${await getFieldName(property.uri)}.length < 1) return null;
226
+ let v = this.${await getFieldName(property.uri)}[0];
227
+ if (options.crossOrigin !== "trust" && !(v instanceof URL) &&
228
+ v.id != null && v.id.origin !== this.id?.origin &&
229
+ !this.${await getFieldName(property.uri, "#_trust")}.has(0)) {
230
+ v = v.id;
231
+ }
232
+ if (v instanceof URL) {
233
+ const fetched =
234
+ await this.#fetch${pascalCase(property.singularName)}(v, options);
235
+ if (fetched == null) return null;
236
+ this.${await getFieldName(property.uri)}[0] = fetched;
237
+ this.${await getFieldName(property.uri, "#_trust")}.add(0);
238
+ this._cachedJsonLd = undefined;
239
+ return fetched;
240
+ }
241
+ `;
242
+ if (property.compactName != null) {
243
+ yield `
244
+ if (
245
+ this._cachedJsonLd != null &&
246
+ typeof this._cachedJsonLd === "object" &&
247
+ "@context" in this._cachedJsonLd &&
248
+ ${JSON.stringify(property.compactName)} in this._cachedJsonLd
249
+ ) {
250
+ const prop = this._cachedJsonLd[
251
+ ${JSON.stringify(property.compactName)}];
252
+ const doc = Array.isArray(prop) ? prop[0] : prop;
253
+ if (doc != null && typeof doc === "object" && "@context" in doc) {
254
+ v = await this.#${property.singularName}_fromJsonLd(doc, options);
255
+ }
256
+ }
257
+ `;
258
+ }
259
+ yield `
260
+ if (options.crossOrigin !== "trust" && v?.id != null &&
261
+ this.id != null && v.id.origin !== this.id.origin &&
262
+ !this.${await getFieldName(property.uri, "#_trust")}.has(0)) {
263
+ if (options.crossOrigin === "throw") {
264
+ throw new Error(
265
+ "The property object's @id (" + v.id.href + ") has a different " +
266
+ "origin than the property owner's @id (" + this.id.href + "); " +
267
+ "refusing to return the object. If you want to bypass this " +
268
+ "check and are aware of the security implications, set the " +
269
+ 'crossOrigin option to "trust".'
270
+ );
271
+ }
272
+ getLogger(["fedify", "vocab"]).warn(
273
+ "The property object's @id ({objectId}) has a different origin " +
274
+ "than the property owner's @id ({parentObjectId}); refusing to " +
275
+ "return the object. If you want to bypass this check and are " +
276
+ "aware of the security implications, set the crossOrigin option " +
277
+ 'to "trust".',
278
+ { objectId: v.id.href, parentObjectId: this.id.href },
279
+ );
280
+ return null;
281
+ }
282
+ return v;
283
+ }
284
+ `;
285
+ }
286
+ if (isNonFunctionalProperty(property)) {
287
+ yield `
288
+ /**
289
+ * Similar to
290
+ * {@link ${type.name}.get${pascalCase(property.pluralName)}},
291
+ * but returns their \`@id\`s instead of the objects themselves.
292
+ */
293
+ ${override} get ${property.singularName}Ids(): URL[] {
294
+ if (this._warning != null) {
295
+ getLogger(this._warning.category).warn(
296
+ this._warning.message,
297
+ this._warning.values
298
+ );
299
+ }
300
+ return this.${await getFieldName(property.uri)}.map((v) =>
301
+ v instanceof URL ? v : v.id!
302
+ ).filter(id => id !== null);
303
+ }
304
+ `;
305
+ yield doc;
306
+ yield `
307
+ ${override} async* get${pascalCase(property.pluralName)}(
308
+ options: {
309
+ documentLoader?: DocumentLoader,
310
+ contextLoader?: DocumentLoader,
311
+ suppressError?: boolean,
312
+ tracerProvider?: TracerProvider,
313
+ crossOrigin?: "ignore" | "throw" | "trust";
314
+ } = {}
315
+ ): AsyncIterable<${getTypeNames(property.range, types)}> {
316
+ if (this._warning != null) {
317
+ getLogger(this._warning.category).warn(
318
+ this._warning.message,
319
+ this._warning.values
320
+ );
321
+ }
322
+ const vs = this.${await getFieldName(property.uri)};
323
+ for (let i = 0; i < vs.length; i++) {
324
+ let v = vs[i];
325
+ if (options.crossOrigin !== "trust" && !(v instanceof URL) &&
326
+ v.id != null && v.id.origin !== this.id?.origin &&
327
+ !this.${await getFieldName(property.uri, "#_trust")}.has(i)) {
328
+ v = v.id;
329
+ }
330
+ if (v instanceof URL) {
331
+ const fetched =
332
+ await this.#fetch${pascalCase(property.singularName)}(v, options);
333
+ if (fetched == null) continue;
334
+ vs[i] = fetched;
335
+ this.${await getFieldName(property.uri, "#_trust")}.add(i);
336
+ this._cachedJsonLd = undefined;
337
+ yield fetched;
338
+ continue;
339
+ }
340
+ `;
341
+ if (property.compactName != null) {
342
+ yield `
343
+ if (
344
+ this._cachedJsonLd != null &&
345
+ typeof this._cachedJsonLd === "object" &&
346
+ "@context" in this._cachedJsonLd &&
347
+ ${JSON.stringify(property.compactName)} in this._cachedJsonLd
348
+ ) {
349
+ const prop = this._cachedJsonLd[
350
+ ${JSON.stringify(property.compactName)}];
351
+ const obj = Array.isArray(prop) ? prop[i] : prop;
352
+ if (obj != null && typeof obj === "object" && "@context" in obj) {
353
+ v = await this.#${property.singularName}_fromJsonLd(obj, options);
354
+ }
355
+ }
356
+ `;
357
+ }
358
+ yield `
359
+ if (options.crossOrigin !== "trust" && v?.id != null &&
360
+ this.id != null && v.id.origin !== this.id.origin &&
361
+ !this.${await getFieldName(property.uri, "#_trust")}.has(0)) {
362
+ if (options.crossOrigin === "throw") {
363
+ throw new Error(
364
+ "The property object's @id (" + v.id.href + ") has a different " +
365
+ "origin than the property owner's @id (" + this.id.href + "); " +
366
+ "refusing to return the object. If you want to bypass this " +
367
+ "check and are aware of the security implications, set the " +
368
+ 'crossOrigin option to "trust".'
369
+ );
370
+ }
371
+ getLogger(["fedify", "vocab"]).warn(
372
+ "The property object's @id ({objectId}) has a different origin " +
373
+ "than the property owner's @id ({parentObjectId}); refusing to " +
374
+ "return the object. If you want to bypass this check and are " +
375
+ "aware of the security implications, set the crossOrigin " +
376
+ 'option to "trust".',
377
+ { objectId: v.id.href, parentObjectId: this.id.href },
378
+ );
379
+ continue;
380
+ }
381
+ yield v;
382
+ }
383
+ }
384
+ `;
385
+ }
386
+ }
387
+ }
388
+
389
+ export async function* generateProperties(
390
+ typeUri: string,
391
+ types: Record<string, TypeSchema>,
392
+ ): AsyncIterable<string> {
393
+ const type = types[typeUri];
394
+ for (const property of type.properties) {
395
+ yield* generateProperty(type, property, types);
396
+ }
397
+ }
@@ -0,0 +1,203 @@
1
+ import { deepStrictEqual, ok } from "node:assert";
2
+ import { test } from "node:test";
3
+ import {
4
+ hasSingularAccessor,
5
+ isNonFunctionalProperty,
6
+ type PropertySchema,
7
+ type TypeUri,
8
+ } from "./schema.ts";
9
+
10
+ test(
11
+ "isNonFunctionalProperty: " +
12
+ "returns true for non-functional property",
13
+ () => {
14
+ const property: PropertySchema = {
15
+ singularName: "name",
16
+ pluralName: "names",
17
+ uri: "https://example.com/name",
18
+ description: "A name property",
19
+ range: ["https://example.com/Text"] as [TypeUri],
20
+ functional: false,
21
+ };
22
+
23
+ ok(isNonFunctionalProperty(property));
24
+
25
+ // Type narrowing test - this should compile without errors
26
+ if (isNonFunctionalProperty(property)) {
27
+ // These properties should be accessible
28
+ const _pluralName = property.pluralName;
29
+ const _singularAccessor = property.singularAccessor;
30
+ const _container = property.container;
31
+ }
32
+ },
33
+ );
34
+
35
+ test(
36
+ "isNonFunctionalProperty: " +
37
+ "returns true for property without functional field",
38
+ () => {
39
+ const property: PropertySchema = {
40
+ singularName: "name",
41
+ pluralName: "names",
42
+ uri: "https://example.com/name",
43
+ description: "A name property",
44
+ range: ["https://example.com/Text"] as [TypeUri],
45
+ // functional is optional and defaults to false
46
+ };
47
+
48
+ ok(isNonFunctionalProperty(property));
49
+ },
50
+ );
51
+
52
+ test("isNonFunctionalProperty: returns false for functional property", () => {
53
+ const property: PropertySchema = {
54
+ singularName: "id",
55
+ uri: "https://example.com/id",
56
+ description: "An ID property",
57
+ range: ["https://example.com/ID"] as [TypeUri],
58
+ functional: true,
59
+ };
60
+
61
+ ok(!isNonFunctionalProperty(property));
62
+ });
63
+
64
+ test("hasSingularAccessor: returns true for functional property", () => {
65
+ const property: PropertySchema = {
66
+ singularName: "id",
67
+ uri: "https://example.com/id",
68
+ description: "An ID property",
69
+ range: ["https://example.com/ID"] as [TypeUri],
70
+ functional: true,
71
+ };
72
+
73
+ ok(hasSingularAccessor(property));
74
+ });
75
+
76
+ test(
77
+ "hasSingularAccessor: " +
78
+ "returns true for non-functional property with singularAccessor",
79
+ () => {
80
+ const property: PropertySchema = {
81
+ singularName: "name",
82
+ pluralName: "names",
83
+ uri: "https://example.com/name",
84
+ description: "A name property",
85
+ range: ["https://example.com/Text"] as [TypeUri],
86
+ functional: false,
87
+ singularAccessor: true,
88
+ };
89
+
90
+ ok(hasSingularAccessor(property));
91
+ },
92
+ );
93
+
94
+ test(
95
+ "hasSingularAccessor: " +
96
+ "returns false for non-functional property without singularAccessor",
97
+ () => {
98
+ const property: PropertySchema = {
99
+ singularName: "name",
100
+ pluralName: "names",
101
+ uri: "https://example.com/name",
102
+ description: "A name property",
103
+ range: ["https://example.com/Text"] as [TypeUri],
104
+ functional: false,
105
+ singularAccessor: false,
106
+ };
107
+
108
+ ok(!hasSingularAccessor(property));
109
+ },
110
+ );
111
+
112
+ test(
113
+ "hasSingularAccessor: " +
114
+ "returns false for non-functional property with undefined singularAccessor",
115
+ () => {
116
+ const property: PropertySchema = {
117
+ singularName: "name",
118
+ pluralName: "names",
119
+ uri: "https://example.com/name",
120
+ description: "A name property",
121
+ range: ["https://example.com/Text"] as [TypeUri],
122
+ // functional defaults to false, singularAccessor is undefined
123
+ };
124
+
125
+ ok(!hasSingularAccessor(property));
126
+ },
127
+ );
128
+
129
+ test(
130
+ "Type guard combinations: " + "functional property with redundantProperties",
131
+ () => {
132
+ const property: PropertySchema = {
133
+ singularName: "type",
134
+ uri: "https://www.w3.org/ns/activitystreams#type",
135
+ description: "The type of the object",
136
+ range: ["https://example.com/Type"] as [TypeUri],
137
+ functional: true,
138
+ redundantProperties: [
139
+ { uri: "https://www.w3.org/1999/02/22-rdf-syntax-ns#type" },
140
+ ],
141
+ };
142
+
143
+ ok(!isNonFunctionalProperty(property));
144
+ ok(hasSingularAccessor(property));
145
+ },
146
+ );
147
+
148
+ test("Type guard combinations: non-functional property with container", () => {
149
+ const property: PropertySchema = {
150
+ singularName: "item",
151
+ pluralName: "items",
152
+ uri: "https://example.com/item",
153
+ description: "List of items",
154
+ range: ["https://example.com/Item"] as [TypeUri],
155
+ functional: false,
156
+ container: "list",
157
+ };
158
+
159
+ ok(isNonFunctionalProperty(property));
160
+ ok(!hasSingularAccessor(property));
161
+
162
+ // Type narrowing test
163
+ if (isNonFunctionalProperty(property)) {
164
+ deepStrictEqual(property.container, "list");
165
+ }
166
+ });
167
+
168
+ test(
169
+ "Type guard combinations: non-functional property with graph container",
170
+ () => {
171
+ const property: PropertySchema = {
172
+ singularName: "member",
173
+ pluralName: "members",
174
+ uri: "https://example.com/member",
175
+ description: "Graph of members",
176
+ range: ["https://example.com/Member"] as [TypeUri],
177
+ functional: false,
178
+ container: "graph",
179
+ };
180
+
181
+ ok(isNonFunctionalProperty(property));
182
+ ok(!hasSingularAccessor(property));
183
+
184
+ // Type narrowing test
185
+ if (isNonFunctionalProperty(property)) {
186
+ deepStrictEqual(property.container, "graph");
187
+ }
188
+ },
189
+ );
190
+
191
+ test("Type guard combinations: untyped property", () => {
192
+ const property: PropertySchema = {
193
+ singularName: "value",
194
+ pluralName: "values",
195
+ uri: "https://example.com/value",
196
+ description: "Untyped values",
197
+ untyped: true,
198
+ range: ["https://example.com/Value"] as [TypeUri],
199
+ };
200
+
201
+ ok(isNonFunctionalProperty(property));
202
+ ok(!hasSingularAccessor(property));
203
+ });