@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/schema.ts ADDED
@@ -0,0 +1,321 @@
1
+ import { type Schema as JsonSchema, Validator } from "@cfworker/json-schema";
2
+ import { readFile } from "node:fs/promises";
3
+ import { join, posix as url } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { parse } from "yaml";
6
+ import { readDirRecursive } from "./fs.ts";
7
+
8
+ /**
9
+ * The qualified URI of a type. It is used as the range of a property.
10
+ */
11
+ export type TypeUri =
12
+ | `https://${string}`
13
+ | `http://${string}`
14
+ | `fedify:${string}`;
15
+
16
+ /**
17
+ * The schema of a type. It is used to generate a class.
18
+ */
19
+ export interface TypeSchema {
20
+ /**
21
+ * The type name. It is used as the name of the generated class.
22
+ */
23
+ name: string;
24
+
25
+ /**
26
+ * The qualified URI of the type.
27
+ */
28
+ uri: TypeUri;
29
+
30
+ /**
31
+ * The qualified URIs of the base type of the type (if any).
32
+ */
33
+ extends?: TypeUri;
34
+
35
+ /**
36
+ * The type name used in the compacted JSON-LD document. It is used as the
37
+ * value of the `type` field.
38
+ */
39
+ compactName?: string;
40
+
41
+ /**
42
+ * Marks the type an entity type rather than a value type. Turning on this
43
+ * flag will make property accessors for the type asynchronous, so that they
44
+ * can load the values of the properties from the remote server.
45
+ *
46
+ * The extended subtypes must have the consistent value of this flag.
47
+ */
48
+ entity: boolean;
49
+
50
+ /**
51
+ * The description of the type. It is used as the doc comment of
52
+ * the generated class.
53
+ */
54
+ description: string;
55
+
56
+ /**
57
+ * The possible properties of the type.
58
+ */
59
+ properties: PropertySchema[];
60
+
61
+ /**
62
+ * The default JSON-LD context of the type. It is used as the default
63
+ * context of the generated `toJsonLd()` method.
64
+ */
65
+ defaultContext: Context;
66
+ }
67
+
68
+ export interface PropertySchemaBase {
69
+ /**
70
+ * The singular form of the property name. It is used as the name of the
71
+ * generated property accessors.
72
+ */
73
+ singularName: string;
74
+
75
+ /**
76
+ * The qualified URI of the property.
77
+ */
78
+ uri: string;
79
+
80
+ /**
81
+ * The property name used in the compacted JSON-LD document. It is used as
82
+ * the key of the property.
83
+ */
84
+ compactName?: string;
85
+
86
+ /**
87
+ * The qualified URI of the superproperty of the property (if any).
88
+ * It means that the property is a specialization of the referenced property.
89
+ */
90
+ subpropertyOf?: string;
91
+
92
+ /**
93
+ * The description of the property. It is used as the doc comment of
94
+ * the generated property accessors.
95
+ */
96
+ description: string;
97
+
98
+ /**
99
+ * Whether the enclosed object should have its own context when the document
100
+ * is compacted.
101
+ */
102
+ embedContext?: {
103
+ /**
104
+ * The compact name of the property that contains the context.
105
+ */
106
+ compactName: string;
107
+
108
+ /**
109
+ * Whether the embedded context should be the same as the context of
110
+ * the enclosing document.
111
+ */
112
+ inherit: true;
113
+ };
114
+ }
115
+
116
+ export type PropertySchemaTyping = {
117
+ /**
118
+ * Whether the property value has `@type` field. If `true`, the `range` must
119
+ * have only one element.
120
+ */
121
+ untyped?: false;
122
+
123
+ /**
124
+ * The qualified URIs of all possible types of the property values.
125
+ */
126
+ range: [TypeUri] | [TypeUri, ...TypeUri[]];
127
+ } | {
128
+ /**
129
+ * Whether the property value has `@type` field. If `true`, the `range` must
130
+ * have only one element.
131
+ */
132
+ untyped: true;
133
+
134
+ /**
135
+ * The qualified URIs of all possible types of the property values.
136
+ */
137
+ range: [TypeUri];
138
+ };
139
+
140
+ /**
141
+ * The schema of a property. It is used to generate property accessors of
142
+ * a class.
143
+ */
144
+ export type PropertySchema =
145
+ | PropertySchemaBase & PropertySchemaTyping & {
146
+ /**
147
+ * Marks the property that it can have only one value. Turning on this
148
+ * flag will generate only singular property accessors, so `pluralName`
149
+ * and `singularAccessor` should not be specified.
150
+ */
151
+ functional?: false;
152
+
153
+ /**
154
+ * The plural form of the property name. It is used as the name of the
155
+ * generated property accessors.
156
+ */
157
+ pluralName: string;
158
+
159
+ /**
160
+ * Whether to generate singular property accessors. Regardless of this
161
+ * flag, plural property accessors are generated (unless `functional` is
162
+ * turned on).
163
+ */
164
+ singularAccessor?: boolean;
165
+
166
+ /**
167
+ * The container type of the property values. It can be unspecified.
168
+ */
169
+ container?: "graph" | "list";
170
+ }
171
+ | PropertySchemaBase & PropertySchemaTyping & {
172
+ /**
173
+ * Marks the property that it can have only one value. Turning on this
174
+ * flag will generate only singular property accessors, so `pluralName`
175
+ * and `singularAccessor` should not be specified.
176
+ */
177
+ functional: true;
178
+
179
+ /**
180
+ * If it's present, those redundant properties are also filled with
181
+ * the same value altogether when the object is serialized into
182
+ * JSON-LD. When it's deserialized from JSON-LD, it tries to
183
+ * parse the values of the specified properties in order.
184
+ */
185
+ redundantProperties?: {
186
+ /**
187
+ * The qualified URI of the property.
188
+ */
189
+ uri: string;
190
+
191
+ /**
192
+ * The property name used in the compacted JSON-LD document. It is used
193
+ * as the key of the property.
194
+ */
195
+ compactName?: string;
196
+ }[];
197
+ };
198
+
199
+ /**
200
+ * A JSON-LD context, which is placed in the `@context` property of a JSON-LD
201
+ * document.
202
+ */
203
+ export type Context = Uri | EmbeddedContext | (Uri | EmbeddedContext)[];
204
+
205
+ type Uri = "http://{string}" | "https://{string}";
206
+ type EmbeddedContext = Record<string, TermDefinition>;
207
+ type TermDefinition = Uri | Record<string, Uri | "@id">;
208
+
209
+ /**
210
+ * Type guard to check if a property is not functional (has pluralName).
211
+ */
212
+ export function isNonFunctionalProperty(
213
+ property: PropertySchema,
214
+ ): property is PropertySchemaBase & PropertySchemaTyping & {
215
+ functional?: false;
216
+ pluralName: string;
217
+ singularAccessor?: boolean;
218
+ container?: "graph" | "list";
219
+ } {
220
+ return property.functional !== true;
221
+ }
222
+
223
+ /**
224
+ * Type guard to check if a property has singular accessor.
225
+ */
226
+ export function hasSingularAccessor(property: PropertySchema): boolean {
227
+ if (property.functional === true) return true;
228
+ return isNonFunctionalProperty(property) &&
229
+ property.singularAccessor === true;
230
+ }
231
+
232
+ /**
233
+ * An error that occurred while loading a schema file.
234
+ */
235
+ export class SchemaError extends Error {
236
+ /**
237
+ * The path of the schema file.
238
+ */
239
+ readonly path: string;
240
+
241
+ /**
242
+ * Constructs a new `SchemaError`.
243
+ * @param path The path of the schema file.
244
+ * @param message The error message.
245
+ */
246
+ constructor(path: string, message?: string) {
247
+ super(message);
248
+ this.path = path;
249
+ }
250
+ }
251
+
252
+ async function loadSchemaValidator(): Promise<Validator> {
253
+ const thisFile = import.meta.url;
254
+ const schemaFile = new URL(url.join(url.dirname(thisFile), "schema.yaml"));
255
+ let content: string;
256
+ if (schemaFile.protocol !== "file:") {
257
+ const response = await fetch(schemaFile);
258
+ content = await response.text();
259
+ } else {
260
+ content = await readFile(fileURLToPath(schemaFile), { encoding: "utf-8" });
261
+ }
262
+ const schemaObject = parse(content);
263
+ return new Validator(schemaObject as JsonSchema);
264
+ }
265
+
266
+ let schemaValidator: Validator | undefined = undefined;
267
+
268
+ async function loadSchema(path: string): Promise<TypeSchema> {
269
+ const content = await readFile(path, { encoding: "utf-8" });
270
+ const schema = parse(content);
271
+ if (schemaValidator == null) schemaValidator = await loadSchemaValidator();
272
+ const result = schemaValidator.validate(schema);
273
+ const errors: SchemaError[] = [];
274
+ if (result.valid) return schema as TypeSchema;
275
+ for (const e of result.errors) {
276
+ errors.push(
277
+ new SchemaError(path, `${path}:${e.instanceLocation}: ${e.error}`),
278
+ );
279
+ }
280
+ throw new AggregateError(errors);
281
+ }
282
+
283
+ /**
284
+ * Loads all schema files in the directory.
285
+ * @param dir The path of the directory to load schema files from.
286
+ * @returns A map from the qualified URI of a type to its {@link SchemaFile}.
287
+ * @throws {@link AggregateError} if any schema file is invalid. It contains
288
+ * all {@link SchemaError}s of the invalid schema files.
289
+ */
290
+ export async function loadSchemaFiles(
291
+ dir: string,
292
+ ): Promise<Record<string, TypeSchema>> {
293
+ if (typeof dir !== "string") {
294
+ throw new TypeError("Expected a directory path in string");
295
+ }
296
+ const result: Record<string, TypeSchema> = {};
297
+ const errors: SchemaError[] = [];
298
+ for await (const relPath of readDirRecursive(dir)) {
299
+ if (!relPath.match(/\.ya?ml$/i)) continue;
300
+ if (relPath.match(/(^|[/\\])schema.yaml$/i)) continue;
301
+ const path = join(dir, relPath);
302
+ let schema: TypeSchema;
303
+ try {
304
+ schema = await loadSchema(path);
305
+ } catch (e) {
306
+ if (
307
+ e instanceof AggregateError && e.errors.length > 0 &&
308
+ e.errors[0] instanceof SchemaError
309
+ ) {
310
+ errors.push(...e.errors);
311
+ continue;
312
+ }
313
+ throw e;
314
+ }
315
+ result[schema.uri] = schema;
316
+ }
317
+ if (errors.length > 0) throw new AggregateError(errors);
318
+ const entries = Object.entries(result);
319
+ entries.sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
320
+ return Object.fromEntries(entries);
321
+ }
@@ -0,0 +1,247 @@
1
+ $schema: "https://json-schema.org/draft/2020-12/schema"
2
+ title: Fedify model schema
3
+
4
+ $defs:
5
+ property:
6
+ description: >-
7
+ The schema of a property. It is used to generate property accessors of
8
+ a class.
9
+ anyOf:
10
+ - allOf:
11
+ - $ref: "#/$defs/property-base"
12
+ - $ref: "#/$defs/property-typing"
13
+ - type: object
14
+ properties:
15
+ functional:
16
+ description: &functional-description >-
17
+ Marks the property that it can have only one value. Turning on
18
+ this flag will generate only singular property accessors,
19
+ so pluralName and singularAccessor should not be specified.
20
+ const: false
21
+ pluralName:
22
+ description: >-
23
+ The plural form of the property name. It is used as the name of
24
+ the generated property accessors.
25
+ type: string
26
+ singularAccessor:
27
+ description: >-
28
+ Whether to generate singular property accessors. Regardless of
29
+ this flag, plural property accessors are generated
30
+ (unless functional is turned on).
31
+ type: boolean
32
+ container:
33
+ description: >-
34
+ The container type of the property values. It can be unspecified.
35
+ enum:
36
+ - graph
37
+ - list
38
+ required:
39
+ - pluralName
40
+ - allOf:
41
+ - $ref: "#/$defs/property-base"
42
+ - $ref: "#/$defs/property-typing"
43
+ - type: object
44
+ properties:
45
+ functional:
46
+ description: *functional-description
47
+ const: true
48
+ redundantProperties::
49
+ description: >-
50
+ If it's present, those redundant properties are also filled with
51
+ the same value altogether when the object is serialized into
52
+ JSON-LD. When it's deserialized from JSON-LD, it tries to
53
+ parse the values of the specified properties in order.
54
+ type: array
55
+ items:
56
+ type: object
57
+ properties:
58
+ uri:
59
+ description: The qualified URI of the property.
60
+ type: string
61
+ format: uri
62
+ compactName:
63
+ description: >-
64
+ The property name used in the compacted JSON-LD document.
65
+ It is used as the key of the property.
66
+ type: string
67
+ required:
68
+ - uri
69
+ required:
70
+ - functional
71
+
72
+ property-base:
73
+ type: object
74
+ properties:
75
+ singularName:
76
+ description: >-
77
+ The singular form of the property name. It is used as the name of the
78
+ generated property accessors.
79
+ type: string
80
+ uri:
81
+ description: The qualified URI of the property.
82
+ type: string
83
+ format: uri
84
+ compactName:
85
+ description: >-
86
+ The property name used in the compacted JSON-LD document. It is used
87
+ as the key of the property.
88
+ type: string
89
+ subpropertyOf:
90
+ description: >-
91
+ The qualified URI of the superproperty of the property (if any).
92
+ It means that the property is a specialization of the referenced
93
+ property.
94
+ type: string
95
+ format: uri
96
+ description:
97
+ description: >-
98
+ The description of the property. It is used as the doc comment of
99
+ the generated property accessors.
100
+ type: string
101
+ embedContext:
102
+ description: >-
103
+ Whether the enclosed object should have its own context when
104
+ the document is compacted.
105
+ type: object
106
+ properties:
107
+ compactName:
108
+ description: >-
109
+ The compact name of the property that contains the context.
110
+ type: string
111
+ inherit:
112
+ description: >-
113
+ Whether the embedded context should be the same as the context of
114
+ the enclosing document.
115
+ const: true
116
+ required:
117
+ - singularName
118
+ - uri
119
+ - description
120
+
121
+ property-typing:
122
+ anyOf:
123
+ - type: object
124
+ properties:
125
+ untyped:
126
+ description: >-
127
+ Whether the property value has @type field. If true, the range must
128
+ have only one element.
129
+ const: false
130
+ range:
131
+ description: >-
132
+ The qualified URIs of all possible types of the property values.
133
+ type: array
134
+ items:
135
+ $ref: "#/$defs/typeUri"
136
+ minItems: 1
137
+ uniqueItems: true
138
+ required:
139
+ - range
140
+ - type: object
141
+ properties:
142
+ untyped:
143
+ description: >-
144
+ Whether the property value has @type field. If true, the range must
145
+ have only one element.
146
+ const: true
147
+ range:
148
+ description: >-
149
+ The qualified URIs of all possible types of the property values.
150
+ type: array
151
+ items:
152
+ $ref: "#/$defs/typeUri"
153
+ minItems: 1
154
+ maxItems: 1
155
+ required:
156
+ - untyped
157
+ - range
158
+
159
+ typeUri:
160
+ description: >-
161
+ The qualified URI of a type. It is used as the range of a property.
162
+ anyOf:
163
+ - type: string
164
+ format: uri
165
+ - enum:
166
+ - fedify:langTag
167
+
168
+ ldContext:
169
+ anyOf:
170
+ - type: string
171
+ format: uri
172
+ - $ref: "#/$defs/ldEmbeddedContext"
173
+ - type: array
174
+ items:
175
+ anyOf:
176
+ - type: string
177
+ format: uri
178
+ - $ref: "#/$defs/ldEmbeddedContext"
179
+ uniqueItems: true
180
+
181
+ ldEmbeddedContext:
182
+ type: object
183
+ patternProperties:
184
+ "^.+$":
185
+ $ref: "#/$defs/ldTermDefinition"
186
+
187
+ ldTermDefinition:
188
+ anyOf:
189
+ - type: string
190
+ format: uri
191
+ - type: object
192
+ patternProperties:
193
+ "^.+$":
194
+ anyOf:
195
+ - type: string
196
+ format: uri
197
+ - const: "@id"
198
+
199
+ description: The schema of a type. It is used to generate a class.
200
+ type: object
201
+ properties:
202
+ name:
203
+ description: The type name. It is used as the name of the generated class.
204
+ type: string
205
+ uri:
206
+ description: The qualified URI of the type.
207
+ type: string
208
+ format: uri
209
+ compactName:
210
+ description: >-
211
+ The type name used in the compacted JSON-LD document. It is used as the
212
+ value of the `type` field.
213
+ type: string
214
+ extends:
215
+ description: The qualified URIs of the base type of the type (if any).
216
+ type: string
217
+ format: uri
218
+ entity:
219
+ description: >-
220
+ Marks the type an entity type rather than a value type. Turning on this
221
+ flag will make property accessors for the type asynchronous, so that they
222
+ can load the values of the properties from the remote server.
223
+
224
+ The extended subtypes must have the consistent value of this flag.
225
+ type: boolean
226
+ description:
227
+ description: >-
228
+ The description of the type. It is used as the doc comment of
229
+ the generated class.
230
+ type: string
231
+ properties:
232
+ description: The possible properties of the type.
233
+ type: array
234
+ items:
235
+ $ref: "#/$defs/property"
236
+ uniqueItems: true
237
+ defaultContext:
238
+ description: >-
239
+ The default JSON-LD context of the type. It is used as the default
240
+ context of the generated toJsonLd() method.
241
+ $ref: "#/$defs/ldContext"
242
+ required:
243
+ - name
244
+ - uri
245
+ - entity
246
+ - description
247
+ - properties