@fedify/vocab-tools 2.2.0-dev.731 → 2.2.0-dev.766
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/deno.json +1 -1
- package/dist/mod.cjs +81 -4
- package/dist/mod.js +81 -4
- package/package.json +1 -1
- package/src/__snapshots__/class.test.ts.deno.snap +5908 -805
- package/src/__snapshots__/class.test.ts.node.snap +5908 -805
- package/src/__snapshots__/class.test.ts.snap +5908 -805
- package/src/class.test.ts +99 -0
- package/src/class.ts +51 -0
- package/src/codec.ts +5 -2
- package/src/schema.test.ts +44 -1
- package/src/schema.ts +12 -0
- package/src/type.ts +32 -2
package/src/class.test.ts
CHANGED
|
@@ -81,6 +81,47 @@ test("generateClasses() imports Decimal helpers for xsd:decimal", async () => {
|
|
|
81
81
|
match(entireCode, /parseDecimal\(v\["@value"\]\)/);
|
|
82
82
|
});
|
|
83
83
|
|
|
84
|
+
test("generateClasses() emits $EntityType helpers for fedify:vocabEntityType", async () => {
|
|
85
|
+
const entireCode = await getEntityTypeFixtureCode();
|
|
86
|
+
match(
|
|
87
|
+
entireCode,
|
|
88
|
+
/export type \$EntityType =\s+\| typeof Entity\s+\| typeof ChildEntity\s+\| typeof Tombstone;/s,
|
|
89
|
+
);
|
|
90
|
+
match(
|
|
91
|
+
entireCode,
|
|
92
|
+
/const entityTypes: readonly \$EntityType\[\] = \[\s*Entity,\s*ChildEntity,\s*Tombstone,\s*\];/s,
|
|
93
|
+
);
|
|
94
|
+
match(
|
|
95
|
+
entireCode,
|
|
96
|
+
/const entityTypeSet: ReadonlySet<\$EntityType> = new Set\(entityTypes\);/,
|
|
97
|
+
);
|
|
98
|
+
match(
|
|
99
|
+
entireCode,
|
|
100
|
+
/export function isEntityType\(value: unknown\): value is \$EntityType/,
|
|
101
|
+
);
|
|
102
|
+
match(
|
|
103
|
+
entireCode,
|
|
104
|
+
/export function getEntityTypeById\(id: string \| URL\): \$EntityType \| undefined/,
|
|
105
|
+
);
|
|
106
|
+
match(
|
|
107
|
+
entireCode,
|
|
108
|
+
/const entityTypeIds: ReadonlyMap<string, \$EntityType> = new Map<string, \$EntityType>\(\s*\[\s*\["https:\/\/example.com\/entity", Entity\],\s*\["https:\/\/example.com\/child-entity", ChildEntity\],\s*\["https:\/\/example.com\/tombstone", Tombstone\],\s*\],\s*\);/s,
|
|
109
|
+
);
|
|
110
|
+
match(
|
|
111
|
+
entireCode,
|
|
112
|
+
/return entityTypeIds\.get\(typeof id === "string" \? id : id\?\.href\);/,
|
|
113
|
+
);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test("generateClasses() uses entity type helpers for fedify:vocabEntityType", async () => {
|
|
117
|
+
const entireCode = await getEntityTypeFixtureCode();
|
|
118
|
+
match(entireCode, /formerType\?: \$EntityType \| null;/);
|
|
119
|
+
match(entireCode, /formerTypes\?: \(\$EntityType\)\[\];/);
|
|
120
|
+
match(entireCode, /isEntityType\(values\.formerType\)/);
|
|
121
|
+
match(entireCode, /v\.typeId\.href/);
|
|
122
|
+
match(entireCode, /getEntityTypeById\(v\["@id"\]\)/);
|
|
123
|
+
});
|
|
124
|
+
|
|
84
125
|
test("getDataCheck() uses canParseDecimal() for xsd:decimal", () => {
|
|
85
126
|
const check = getDataCheck(
|
|
86
127
|
"http://www.w3.org/2001/XMLSchema#decimal",
|
|
@@ -177,6 +218,64 @@ async function getDecimalFixtureCode() {
|
|
|
177
218
|
return (await Array.fromAsync(generateClasses(types))).join("");
|
|
178
219
|
}
|
|
179
220
|
|
|
221
|
+
async function getEntityTypeFixtureCode() {
|
|
222
|
+
const types: Record<string, TypeSchema> = {
|
|
223
|
+
"https://example.com/entity": {
|
|
224
|
+
name: "Entity",
|
|
225
|
+
uri: "https://example.com/entity",
|
|
226
|
+
compactName: "Entity",
|
|
227
|
+
entity: true,
|
|
228
|
+
description: "An entity.",
|
|
229
|
+
properties: [],
|
|
230
|
+
defaultContext:
|
|
231
|
+
"https://example.com/context" as TypeSchema["defaultContext"],
|
|
232
|
+
},
|
|
233
|
+
"https://example.com/child-entity": {
|
|
234
|
+
name: "ChildEntity",
|
|
235
|
+
uri: "https://example.com/child-entity",
|
|
236
|
+
compactName: "ChildEntity",
|
|
237
|
+
extends: "https://example.com/entity",
|
|
238
|
+
entity: true,
|
|
239
|
+
description: "A child entity.",
|
|
240
|
+
properties: [],
|
|
241
|
+
defaultContext:
|
|
242
|
+
"https://example.com/context" as TypeSchema["defaultContext"],
|
|
243
|
+
},
|
|
244
|
+
"https://example.com/value": {
|
|
245
|
+
name: "Value",
|
|
246
|
+
uri: "https://example.com/value",
|
|
247
|
+
compactName: "Value",
|
|
248
|
+
entity: false,
|
|
249
|
+
description: "A value type.",
|
|
250
|
+
properties: [],
|
|
251
|
+
defaultContext:
|
|
252
|
+
"https://example.com/context" as TypeSchema["defaultContext"],
|
|
253
|
+
},
|
|
254
|
+
"https://example.com/tombstone": {
|
|
255
|
+
name: "Tombstone",
|
|
256
|
+
uri: "https://example.com/tombstone",
|
|
257
|
+
compactName: "Tombstone",
|
|
258
|
+
extends: "https://example.com/entity",
|
|
259
|
+
entity: true,
|
|
260
|
+
description: "A tombstone.",
|
|
261
|
+
properties: [
|
|
262
|
+
{
|
|
263
|
+
singularName: "formerType",
|
|
264
|
+
pluralName: "formerTypes",
|
|
265
|
+
singularAccessor: true,
|
|
266
|
+
compactName: "formerType",
|
|
267
|
+
uri: "https://example.com/formerType",
|
|
268
|
+
description: "The former type.",
|
|
269
|
+
range: ["fedify:vocabEntityType"],
|
|
270
|
+
},
|
|
271
|
+
],
|
|
272
|
+
defaultContext:
|
|
273
|
+
"https://example.com/context" as TypeSchema["defaultContext"],
|
|
274
|
+
},
|
|
275
|
+
};
|
|
276
|
+
return (await Array.fromAsync(generateClasses(types))).join("");
|
|
277
|
+
}
|
|
278
|
+
|
|
180
279
|
async function changeNodeSnapshotPath() {
|
|
181
280
|
const { snapshot } = await import("node:test");
|
|
182
281
|
snapshot.setResolveSnapshotPath(
|
package/src/class.ts
CHANGED
|
@@ -109,6 +109,56 @@ async function* generateClass(
|
|
|
109
109
|
}
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
+
function* generateEntityTypeHelpers(
|
|
113
|
+
sortedTypeUris: string[],
|
|
114
|
+
types: Record<string, TypeSchema>,
|
|
115
|
+
): Iterable<string> {
|
|
116
|
+
const entityTypes = sortedTypeUris.filter((typeUri) => types[typeUri].entity)
|
|
117
|
+
.map((typeUri) => ({ name: types[typeUri].name, uri: typeUri }));
|
|
118
|
+
const entityTypeNames = entityTypes.map((entityType) => entityType.name);
|
|
119
|
+
const entityTypeUnion = entityTypeNames.length < 1
|
|
120
|
+
? " never"
|
|
121
|
+
: `\n | typeof ${entityTypeNames.join("\n | typeof ")}`;
|
|
122
|
+
yield `/**
|
|
123
|
+
* Constructor types for all generated vocabulary entity classes.
|
|
124
|
+
*/
|
|
125
|
+
export type $EntityType =${entityTypeUnion};
|
|
126
|
+
|
|
127
|
+
const entityTypes: readonly $EntityType[] = [
|
|
128
|
+
`;
|
|
129
|
+
for (const entityTypeName of entityTypeNames) {
|
|
130
|
+
yield ` ${entityTypeName},\n`;
|
|
131
|
+
}
|
|
132
|
+
yield `];
|
|
133
|
+
|
|
134
|
+
const entityTypeSet: ReadonlySet<$EntityType> = new Set(entityTypes);
|
|
135
|
+
|
|
136
|
+
const entityTypeIds: ReadonlyMap<string, $EntityType> = new Map<string, $EntityType>(
|
|
137
|
+
[
|
|
138
|
+
`;
|
|
139
|
+
for (const entityType of entityTypes) {
|
|
140
|
+
yield ` [${JSON.stringify(entityType.uri)}, ${entityType.name}],\n`;
|
|
141
|
+
}
|
|
142
|
+
yield ` ],
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Checks whether the given value is a generated vocabulary entity class.
|
|
147
|
+
*/
|
|
148
|
+
export function isEntityType(value: unknown): value is $EntityType {
|
|
149
|
+
return entityTypeSet.has(value as $EntityType);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Gets the generated vocabulary entity class for the given type URI.
|
|
154
|
+
*/
|
|
155
|
+
export function getEntityTypeById(id: string | URL): $EntityType | undefined {
|
|
156
|
+
return entityTypeIds.get(typeof id === "string" ? id : id?.href);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
`;
|
|
160
|
+
}
|
|
161
|
+
|
|
112
162
|
/**
|
|
113
163
|
* Generates the TypeScript classes from the given types.
|
|
114
164
|
* @param types The types to generate classes from.
|
|
@@ -147,4 +197,5 @@ export async function* generateClasses(
|
|
|
147
197
|
for (const typeUri of sorted) {
|
|
148
198
|
for await (const code of generateClass(typeUri, types)) yield code;
|
|
149
199
|
}
|
|
200
|
+
for (const code of generateEntityTypeHelpers(sorted, types)) yield code;
|
|
150
201
|
}
|
package/src/codec.ts
CHANGED
|
@@ -440,7 +440,8 @@ export async function* generateDecoder(
|
|
|
440
440
|
`;
|
|
441
441
|
}
|
|
442
442
|
if (property.range.length == 1) {
|
|
443
|
-
yield
|
|
443
|
+
yield `
|
|
444
|
+
const decoded = ${
|
|
444
445
|
getDecoder(
|
|
445
446
|
property.range[0],
|
|
446
447
|
types,
|
|
@@ -448,7 +449,9 @@ export async function* generateDecoder(
|
|
|
448
449
|
"options",
|
|
449
450
|
`(values["@id"] == null ? options.baseUrl : new URL(values["@id"]))`,
|
|
450
451
|
)
|
|
451
|
-
}
|
|
452
|
+
};
|
|
453
|
+
if (typeof decoded === "undefined") continue;
|
|
454
|
+
${variable}.push(decoded);`;
|
|
452
455
|
} else {
|
|
453
456
|
yield `
|
|
454
457
|
const decoded =
|
package/src/schema.test.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
import { deepStrictEqual, ok } from "node:assert";
|
|
1
|
+
import { deepStrictEqual, ok, throws } from "node:assert";
|
|
2
2
|
import { test } from "node:test";
|
|
3
3
|
import {
|
|
4
4
|
hasSingularAccessor,
|
|
5
5
|
isNonFunctionalProperty,
|
|
6
6
|
type PropertySchema,
|
|
7
|
+
type TypeSchema,
|
|
7
8
|
type TypeUri,
|
|
9
|
+
validateTypeSchemas,
|
|
8
10
|
} from "./schema.ts";
|
|
9
11
|
|
|
10
12
|
test(
|
|
@@ -201,3 +203,44 @@ test("Type guard combinations: untyped property", () => {
|
|
|
201
203
|
ok(isNonFunctionalProperty(property));
|
|
202
204
|
ok(!hasSingularAccessor(property));
|
|
203
205
|
});
|
|
206
|
+
|
|
207
|
+
test("validateTypeSchemas() rejects mixed fedify:vocabEntityType ranges", () => {
|
|
208
|
+
const types: Record<string, TypeSchema> = {
|
|
209
|
+
"https://example.com/tombstone": {
|
|
210
|
+
name: "Tombstone",
|
|
211
|
+
uri: "https://example.com/tombstone" as TypeUri,
|
|
212
|
+
compactName: "Tombstone",
|
|
213
|
+
entity: true,
|
|
214
|
+
description: "A tombstone.",
|
|
215
|
+
properties: [
|
|
216
|
+
{
|
|
217
|
+
singularName: "formerType",
|
|
218
|
+
pluralName: "formerTypes",
|
|
219
|
+
uri: "https://example.com/formerType",
|
|
220
|
+
compactName: "formerType",
|
|
221
|
+
description: "The former type.",
|
|
222
|
+
range: [
|
|
223
|
+
"fedify:vocabEntityType",
|
|
224
|
+
"http://www.w3.org/2001/XMLSchema#anyURI",
|
|
225
|
+
] as [TypeUri, TypeUri],
|
|
226
|
+
},
|
|
227
|
+
],
|
|
228
|
+
defaultContext:
|
|
229
|
+
"https://example.com/context" as TypeSchema["defaultContext"],
|
|
230
|
+
},
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
throws(
|
|
234
|
+
() => validateTypeSchemas(types),
|
|
235
|
+
(error) => {
|
|
236
|
+
ok(error instanceof TypeError);
|
|
237
|
+
deepStrictEqual(
|
|
238
|
+
error.message,
|
|
239
|
+
"The property Tombstone.formerType cannot mix fedify:vocabEntityType " +
|
|
240
|
+
"with other range types because the generated decoder cannot " +
|
|
241
|
+
"disambiguate entity type references from ordinary IRIs.",
|
|
242
|
+
);
|
|
243
|
+
return true;
|
|
244
|
+
},
|
|
245
|
+
);
|
|
246
|
+
});
|
package/src/schema.ts
CHANGED
|
@@ -242,6 +242,7 @@ export function hasSingularAccessor(property: PropertySchema): boolean {
|
|
|
242
242
|
|
|
243
243
|
const XSD_STRING_URI = "http://www.w3.org/2001/XMLSchema#string";
|
|
244
244
|
const XSD_DECIMAL_URI = "http://www.w3.org/2001/XMLSchema#decimal";
|
|
245
|
+
const FEDIFY_VOCAB_ENTITY_TYPE_URI = "fedify:vocabEntityType";
|
|
245
246
|
|
|
246
247
|
/**
|
|
247
248
|
* Validates schema combinations that cannot be represented safely by the
|
|
@@ -268,6 +269,17 @@ export function validateTypeSchemas(
|
|
|
268
269
|
`generated encoder cannot disambiguate them at runtime.`,
|
|
269
270
|
);
|
|
270
271
|
}
|
|
272
|
+
if (
|
|
273
|
+
property.range.includes(FEDIFY_VOCAB_ENTITY_TYPE_URI) &&
|
|
274
|
+
property.range.length > 1
|
|
275
|
+
) {
|
|
276
|
+
throw new TypeError(
|
|
277
|
+
`The property ${type.name}.${property.singularName} cannot mix ` +
|
|
278
|
+
`fedify:vocabEntityType with other range types because the ` +
|
|
279
|
+
`generated decoder cannot disambiguate entity type references ` +
|
|
280
|
+
`from ordinary IRIs.`,
|
|
281
|
+
);
|
|
282
|
+
}
|
|
271
283
|
}
|
|
272
284
|
}
|
|
273
285
|
}
|
package/src/type.ts
CHANGED
|
@@ -163,7 +163,7 @@ const scalarTypes: Record<string, ScalarType> = {
|
|
|
163
163
|
return `${v}.href`;
|
|
164
164
|
},
|
|
165
165
|
dataCheck(v) {
|
|
166
|
-
return
|
|
166
|
+
return `${v} != null && typeof ${v} === "object" && "@id" in ${v}
|
|
167
167
|
&& typeof ${v}["@id"] === "string"
|
|
168
168
|
&& ${v}["@id"] !== ""`;
|
|
169
169
|
},
|
|
@@ -403,7 +403,7 @@ const scalarTypes: Record<string, ScalarType> = {
|
|
|
403
403
|
return v;
|
|
404
404
|
},
|
|
405
405
|
dataCheck(v) {
|
|
406
|
-
return
|
|
406
|
+
return `${v} != null && typeof ${v} === "object" && "@id" in ${v}
|
|
407
407
|
&& typeof ${v}["@id"] === "string"
|
|
408
408
|
&& ${v}["@id"].startsWith("https://w3id.org/security#")
|
|
409
409
|
&& [
|
|
@@ -437,6 +437,36 @@ const scalarTypes: Record<string, ScalarType> = {
|
|
|
437
437
|
return `${v}["@value"]`;
|
|
438
438
|
},
|
|
439
439
|
},
|
|
440
|
+
"fedify:vocabEntityType": {
|
|
441
|
+
name: "$EntityType",
|
|
442
|
+
typeGuard(v) {
|
|
443
|
+
return `isEntityType(${v})`;
|
|
444
|
+
},
|
|
445
|
+
encoder(v) {
|
|
446
|
+
return `{ "@id": ${v}.typeId.href }`;
|
|
447
|
+
},
|
|
448
|
+
dataCheck(v) {
|
|
449
|
+
return `${v} != null && typeof ${v} === "object" && "@id" in ${v}
|
|
450
|
+
&& typeof ${v}["@id"] === "string"
|
|
451
|
+
&& ${v}["@id"] !== ""`;
|
|
452
|
+
},
|
|
453
|
+
decoder(v) {
|
|
454
|
+
return `(() => {
|
|
455
|
+
if (${v} == null || typeof ${v} !== "object" || !("@id" in ${v}) ||
|
|
456
|
+
typeof ${v}["@id"] !== "string" || ${v}["@id"] === "") {
|
|
457
|
+
return undefined;
|
|
458
|
+
}
|
|
459
|
+
const entityType = getEntityTypeById(${v}["@id"]);
|
|
460
|
+
if (entityType == null) {
|
|
461
|
+
getLogger(["fedify", "vocab"]).warn(
|
|
462
|
+
"Ignoring unknown vocabulary entity type reference: {typeId}",
|
|
463
|
+
{ typeId: ${v}["@id"] },
|
|
464
|
+
);
|
|
465
|
+
}
|
|
466
|
+
return entityType;
|
|
467
|
+
})()`;
|
|
468
|
+
},
|
|
469
|
+
},
|
|
440
470
|
};
|
|
441
471
|
|
|
442
472
|
export function getTypeName(
|