@fedify/vocab-tools 2.5.0-dev.2289 → 2.5.0-dev.2304
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/README.md +40 -0
- package/deno.json +1 -1
- package/dist/mod.cjs +8 -5
- package/dist/mod.d.cts +16 -17
- package/dist/mod.d.ts +16 -17
- package/dist/mod.js +8 -5
- package/dist/schema.yaml +29 -21
- package/package.json +5 -2
- package/src/codec.test.ts +409 -0
- package/src/codec.ts +16 -3
- package/src/schema.test.ts +74 -1
- package/src/schema.ts +21 -19
- package/src/schema.yaml +29 -21
package/README.md
CHANGED
|
@@ -64,3 +64,43 @@ of trusting a matching origin. Locally constructed values, previously fetched
|
|
|
64
64
|
values, and the explicit `crossOrigin: "trust"` option retain their usual
|
|
65
65
|
behavior. This does not verify id-less embedded objects or establish publishing
|
|
66
66
|
authority.
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
Redundant properties
|
|
70
|
+
--------------------
|
|
71
|
+
|
|
72
|
+
Both functional and non-functional property schemas can declare synonyms in
|
|
73
|
+
`redundantProperties`. On reading, the canonical property takes precedence,
|
|
74
|
+
followed by synonyms in declaration order. The first non-empty raw value set
|
|
75
|
+
is decoded in full, preserving its order and multiplicity. Values from other
|
|
76
|
+
synonyms are never merged, even when the selected values fail range validation.
|
|
77
|
+
List and graph containers are decoded only after choosing the property.
|
|
78
|
+
|
|
79
|
+
`redundantPropertiesWrite` defaults to `all`, which writes the canonical
|
|
80
|
+
property and every synonym with the same complete container representation.
|
|
81
|
+
Set it to `canonical` to write only the canonical property while still accepting
|
|
82
|
+
synonyms on reading:
|
|
83
|
+
|
|
84
|
+
~~~~ yaml
|
|
85
|
+
- singularName: license
|
|
86
|
+
pluralName: licenses
|
|
87
|
+
uri: "https://example.com/license"
|
|
88
|
+
description: The licenses of the object.
|
|
89
|
+
range:
|
|
90
|
+
- "http://www.w3.org/2001/XMLSchema#anyURI"
|
|
91
|
+
redundantProperties:
|
|
92
|
+
- uri: "https://schema.org/license"
|
|
93
|
+
- uri: "http://creativecommons.org/ns#license"
|
|
94
|
+
redundantPropertiesWrite: canonical
|
|
95
|
+
~~~~
|
|
96
|
+
|
|
97
|
+
A synonym's `compactName` is optional. Without it, serialization uses the
|
|
98
|
+
JSON-LD processor to select a context term or retain the full URI. For IRI
|
|
99
|
+
values, context terms must declare `"@type": "@id"`; container terms must
|
|
100
|
+
carry the appropriate `@container`. Writable container synonyms also use the
|
|
101
|
+
processor to preserve their representation. Explicit caller contexts take
|
|
102
|
+
precedence over the default context.
|
|
103
|
+
|
|
104
|
+
As with other properties, an object parsed with `fromJsonLd()` retains its
|
|
105
|
+
original JSON-LD document for default serialization. Pass `format: "compact"`
|
|
106
|
+
or `format: "expand"` to serialize its decoded values under the write policy.
|
package/deno.json
CHANGED
package/dist/mod.cjs
CHANGED
|
@@ -8,7 +8,7 @@ let yaml = require("yaml");
|
|
|
8
8
|
let es_toolkit = require("es-toolkit");
|
|
9
9
|
//#region deno.json
|
|
10
10
|
var name = "@fedify/vocab-tools";
|
|
11
|
-
var version = "2.5.0-dev.
|
|
11
|
+
var version = "2.5.0-dev.2304+3a599380";
|
|
12
12
|
//#endregion
|
|
13
13
|
//#region src/type.ts
|
|
14
14
|
const HEURISTICS_CONTEXTS = [
|
|
@@ -844,12 +844,15 @@ async function* generateEncoder(typeUri, types) {
|
|
|
844
844
|
? compactItems
|
|
845
845
|
: compactItems[0];
|
|
846
846
|
`;
|
|
847
|
-
if (property.
|
|
847
|
+
if (property.redundantPropertiesWrite !== "canonical" && property.redundantProperties != null && !(isNonFunctionalProperty(property) && property.container != null)) for (const prop of property.redundantProperties) {
|
|
848
|
+
if (prop.compactName == null) continue;
|
|
849
|
+
yield `
|
|
848
850
|
result[${JSON.stringify(prop.compactName)}]
|
|
849
851
|
= compactItems.length > 1
|
|
850
852
|
? compactItems
|
|
851
853
|
: compactItems[0];
|
|
852
854
|
`;
|
|
855
|
+
}
|
|
853
856
|
} else yield `
|
|
854
857
|
result[${JSON.stringify(property.compactName)}] = compactItems;
|
|
855
858
|
`;
|
|
@@ -916,7 +919,7 @@ async function* generateEncoder(typeUri, types) {
|
|
|
916
919
|
);
|
|
917
920
|
values[${JSON.stringify(property.uri)}] = propValue;
|
|
918
921
|
`;
|
|
919
|
-
if (property.
|
|
922
|
+
if (property.redundantPropertiesWrite !== "canonical" && property.redundantProperties != null) for (const prop of property.redundantProperties) yield `
|
|
920
923
|
values[${JSON.stringify(prop.uri)}] = propValue;
|
|
921
924
|
`;
|
|
922
925
|
yield `
|
|
@@ -988,7 +991,7 @@ async function* generateEncoder(typeUri, types) {
|
|
|
988
991
|
|
|
989
992
|
protected ${emitOverride$1(typeUri, types)} isCompactable(): boolean {
|
|
990
993
|
`;
|
|
991
|
-
for (const property of type.properties) if (property.extraContext != null || !property.range.every((r) => isCompactableType(r, types))) yield `
|
|
994
|
+
for (const property of type.properties) if (property.extraContext != null || property.redundantPropertiesWrite !== "canonical" && property.redundantProperties != null && property.redundantProperties.length > 0 && (property.redundantProperties.some((p) => p.compactName == null) || isNonFunctionalProperty(property) && property.container != null) || !property.range.every((r) => isCompactableType(r, types))) yield `
|
|
992
995
|
if (
|
|
993
996
|
this.${await getFieldName(property.uri)} != null &&
|
|
994
997
|
this.${await getFieldName(property.uri)}.length > 0
|
|
@@ -1167,7 +1170,7 @@ async function* generateDecoder(typeUri, types, moduleVarNames) {
|
|
|
1167
1170
|
yield `
|
|
1168
1171
|
let ${arrayVariable} = ${property.uri === "https://w3id.org/fep/ef61/gateways" ? "getPortableActorGateways(values) as typeof values[string]" : `values[${JSON.stringify(property.uri)}]`};
|
|
1169
1172
|
`;
|
|
1170
|
-
if (property.
|
|
1173
|
+
if (property.redundantProperties != null) for (const prop of property.redundantProperties) yield `
|
|
1171
1174
|
if (${arrayVariable} == null || ${arrayVariable}.length < 1) {
|
|
1172
1175
|
${arrayVariable} = values[${JSON.stringify(prop.uri)}];
|
|
1173
1176
|
}
|
package/dist/mod.d.cts
CHANGED
|
@@ -68,6 +68,22 @@ interface TypeSchema {
|
|
|
68
68
|
defaultContext: Context;
|
|
69
69
|
}
|
|
70
70
|
interface PropertySchemaBase {
|
|
71
|
+
/**
|
|
72
|
+
* Synonyms read after the canonical property, in declaration order.
|
|
73
|
+
* Deserialization selects the first non-empty raw value set before decoding
|
|
74
|
+
* its values and containers; it never merges synonyms or retries invalid
|
|
75
|
+
* values. Serialization writes all synonyms by default.
|
|
76
|
+
*/
|
|
77
|
+
redundantProperties?: {
|
|
78
|
+
/** The qualified URI of the synonym. */uri: string; /** The synonym's key in the default compact JSON-LD context. */
|
|
79
|
+
compactName?: string;
|
|
80
|
+
}[];
|
|
81
|
+
/**
|
|
82
|
+
* Whether serialization writes every synonym or only the canonical property.
|
|
83
|
+
* Defaults to `"all"`. This does not change deserialization precedence or
|
|
84
|
+
* the existing cache of parsed JSON-LD documents.
|
|
85
|
+
*/
|
|
86
|
+
redundantPropertiesWrite?: "all" | "canonical";
|
|
71
87
|
/**
|
|
72
88
|
* A context URL added to the default serialization context when this
|
|
73
89
|
* property's expanded IRI appears in the compacted document. Explicit
|
|
@@ -193,23 +209,6 @@ type PropertySchema = PropertySchemaBase & PropertySchemaTyping & {
|
|
|
193
209
|
* and `singularAccessor` should not be specified.
|
|
194
210
|
*/
|
|
195
211
|
functional: true;
|
|
196
|
-
/**
|
|
197
|
-
* If it's present, those redundant properties are also filled with
|
|
198
|
-
* the same value altogether when the object is serialized into
|
|
199
|
-
* JSON-LD. When it's deserialized from JSON-LD, it tries to
|
|
200
|
-
* parse the values of the specified properties in order.
|
|
201
|
-
*/
|
|
202
|
-
redundantProperties?: {
|
|
203
|
-
/**
|
|
204
|
-
* The qualified URI of the property.
|
|
205
|
-
*/
|
|
206
|
-
uri: string;
|
|
207
|
-
/**
|
|
208
|
-
* The property name used in the compacted JSON-LD document. It is used
|
|
209
|
-
* as the key of the property.
|
|
210
|
-
*/
|
|
211
|
-
compactName?: string;
|
|
212
|
-
}[];
|
|
213
212
|
};
|
|
214
213
|
/**
|
|
215
214
|
* A JSON-LD context, which is placed in the `@context` property of a JSON-LD
|
package/dist/mod.d.ts
CHANGED
|
@@ -68,6 +68,22 @@ interface TypeSchema {
|
|
|
68
68
|
defaultContext: Context;
|
|
69
69
|
}
|
|
70
70
|
interface PropertySchemaBase {
|
|
71
|
+
/**
|
|
72
|
+
* Synonyms read after the canonical property, in declaration order.
|
|
73
|
+
* Deserialization selects the first non-empty raw value set before decoding
|
|
74
|
+
* its values and containers; it never merges synonyms or retries invalid
|
|
75
|
+
* values. Serialization writes all synonyms by default.
|
|
76
|
+
*/
|
|
77
|
+
redundantProperties?: {
|
|
78
|
+
/** The qualified URI of the synonym. */uri: string; /** The synonym's key in the default compact JSON-LD context. */
|
|
79
|
+
compactName?: string;
|
|
80
|
+
}[];
|
|
81
|
+
/**
|
|
82
|
+
* Whether serialization writes every synonym or only the canonical property.
|
|
83
|
+
* Defaults to `"all"`. This does not change deserialization precedence or
|
|
84
|
+
* the existing cache of parsed JSON-LD documents.
|
|
85
|
+
*/
|
|
86
|
+
redundantPropertiesWrite?: "all" | "canonical";
|
|
71
87
|
/**
|
|
72
88
|
* A context URL added to the default serialization context when this
|
|
73
89
|
* property's expanded IRI appears in the compacted document. Explicit
|
|
@@ -193,23 +209,6 @@ type PropertySchema = PropertySchemaBase & PropertySchemaTyping & {
|
|
|
193
209
|
* and `singularAccessor` should not be specified.
|
|
194
210
|
*/
|
|
195
211
|
functional: true;
|
|
196
|
-
/**
|
|
197
|
-
* If it's present, those redundant properties are also filled with
|
|
198
|
-
* the same value altogether when the object is serialized into
|
|
199
|
-
* JSON-LD. When it's deserialized from JSON-LD, it tries to
|
|
200
|
-
* parse the values of the specified properties in order.
|
|
201
|
-
*/
|
|
202
|
-
redundantProperties?: {
|
|
203
|
-
/**
|
|
204
|
-
* The qualified URI of the property.
|
|
205
|
-
*/
|
|
206
|
-
uri: string;
|
|
207
|
-
/**
|
|
208
|
-
* The property name used in the compacted JSON-LD document. It is used
|
|
209
|
-
* as the key of the property.
|
|
210
|
-
*/
|
|
211
|
-
compactName?: string;
|
|
212
|
-
}[];
|
|
213
212
|
};
|
|
214
213
|
/**
|
|
215
214
|
* A JSON-LD context, which is placed in the `@context` property of a JSON-LD
|
package/dist/mod.js
CHANGED
|
@@ -7,7 +7,7 @@ import { parse } from "yaml";
|
|
|
7
7
|
import { pascalCase } from "es-toolkit";
|
|
8
8
|
//#region deno.json
|
|
9
9
|
var name = "@fedify/vocab-tools";
|
|
10
|
-
var version = "2.5.0-dev.
|
|
10
|
+
var version = "2.5.0-dev.2304+3a599380";
|
|
11
11
|
//#endregion
|
|
12
12
|
//#region src/type.ts
|
|
13
13
|
const HEURISTICS_CONTEXTS = [
|
|
@@ -843,12 +843,15 @@ async function* generateEncoder(typeUri, types) {
|
|
|
843
843
|
? compactItems
|
|
844
844
|
: compactItems[0];
|
|
845
845
|
`;
|
|
846
|
-
if (property.
|
|
846
|
+
if (property.redundantPropertiesWrite !== "canonical" && property.redundantProperties != null && !(isNonFunctionalProperty(property) && property.container != null)) for (const prop of property.redundantProperties) {
|
|
847
|
+
if (prop.compactName == null) continue;
|
|
848
|
+
yield `
|
|
847
849
|
result[${JSON.stringify(prop.compactName)}]
|
|
848
850
|
= compactItems.length > 1
|
|
849
851
|
? compactItems
|
|
850
852
|
: compactItems[0];
|
|
851
853
|
`;
|
|
854
|
+
}
|
|
852
855
|
} else yield `
|
|
853
856
|
result[${JSON.stringify(property.compactName)}] = compactItems;
|
|
854
857
|
`;
|
|
@@ -915,7 +918,7 @@ async function* generateEncoder(typeUri, types) {
|
|
|
915
918
|
);
|
|
916
919
|
values[${JSON.stringify(property.uri)}] = propValue;
|
|
917
920
|
`;
|
|
918
|
-
if (property.
|
|
921
|
+
if (property.redundantPropertiesWrite !== "canonical" && property.redundantProperties != null) for (const prop of property.redundantProperties) yield `
|
|
919
922
|
values[${JSON.stringify(prop.uri)}] = propValue;
|
|
920
923
|
`;
|
|
921
924
|
yield `
|
|
@@ -987,7 +990,7 @@ async function* generateEncoder(typeUri, types) {
|
|
|
987
990
|
|
|
988
991
|
protected ${emitOverride$1(typeUri, types)} isCompactable(): boolean {
|
|
989
992
|
`;
|
|
990
|
-
for (const property of type.properties) if (property.extraContext != null || !property.range.every((r) => isCompactableType(r, types))) yield `
|
|
993
|
+
for (const property of type.properties) if (property.extraContext != null || property.redundantPropertiesWrite !== "canonical" && property.redundantProperties != null && property.redundantProperties.length > 0 && (property.redundantProperties.some((p) => p.compactName == null) || isNonFunctionalProperty(property) && property.container != null) || !property.range.every((r) => isCompactableType(r, types))) yield `
|
|
991
994
|
if (
|
|
992
995
|
this.${await getFieldName(property.uri)} != null &&
|
|
993
996
|
this.${await getFieldName(property.uri)}.length > 0
|
|
@@ -1166,7 +1169,7 @@ async function* generateDecoder(typeUri, types, moduleVarNames) {
|
|
|
1166
1169
|
yield `
|
|
1167
1170
|
let ${arrayVariable} = ${property.uri === "https://w3id.org/fep/ef61/gateways" ? "getPortableActorGateways(values) as typeof values[string]" : `values[${JSON.stringify(property.uri)}]`};
|
|
1168
1171
|
`;
|
|
1169
|
-
if (property.
|
|
1172
|
+
if (property.redundantProperties != null) for (const prop of property.redundantProperties) yield `
|
|
1170
1173
|
if (${arrayVariable} == null || ${arrayVariable}.length < 1) {
|
|
1171
1174
|
${arrayVariable} = values[${JSON.stringify(prop.uri)}];
|
|
1172
1175
|
}
|
package/dist/schema.yaml
CHANGED
|
@@ -45,33 +45,41 @@ $defs:
|
|
|
45
45
|
functional:
|
|
46
46
|
description: *functional-description
|
|
47
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
48
|
required:
|
|
70
49
|
- functional
|
|
71
50
|
|
|
72
51
|
property-base:
|
|
73
52
|
type: object
|
|
74
53
|
properties:
|
|
54
|
+
redundantProperties:
|
|
55
|
+
description: >-
|
|
56
|
+
Synonyms read after the canonical property in declaration order.
|
|
57
|
+
The first non-empty raw value set is selected before decoding values
|
|
58
|
+
and containers. Synonyms are never merged or retried on invalid values.
|
|
59
|
+
Serialization writes all synonyms by default.
|
|
60
|
+
type: array
|
|
61
|
+
items:
|
|
62
|
+
type: object
|
|
63
|
+
properties:
|
|
64
|
+
uri:
|
|
65
|
+
description: The qualified URI of the property.
|
|
66
|
+
type: string
|
|
67
|
+
format: uri
|
|
68
|
+
compactName:
|
|
69
|
+
description: >-
|
|
70
|
+
The property name used in the compacted JSON-LD document.
|
|
71
|
+
It is used as the key of the property.
|
|
72
|
+
type: string
|
|
73
|
+
required:
|
|
74
|
+
- uri
|
|
75
|
+
redundantPropertiesWrite:
|
|
76
|
+
description: >-
|
|
77
|
+
Whether serialization writes all synonyms or only the canonical
|
|
78
|
+
property. Does not change deserialization precedence.
|
|
79
|
+
enum:
|
|
80
|
+
- all
|
|
81
|
+
- canonical
|
|
82
|
+
default: all
|
|
75
83
|
extraContext:
|
|
76
84
|
description: >-
|
|
77
85
|
A context URL added to default serialization when this property's
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fedify/vocab-tools",
|
|
3
|
-
"version": "2.5.0-dev.
|
|
3
|
+
"version": "2.5.0-dev.2304+3a599380",
|
|
4
4
|
"description": "Code generator for Activity Vocabulary APIs",
|
|
5
5
|
"homepage": "https://fedify.dev/",
|
|
6
6
|
"repository": {
|
|
@@ -46,9 +46,12 @@
|
|
|
46
46
|
},
|
|
47
47
|
"license": "MIT",
|
|
48
48
|
"devDependencies": {
|
|
49
|
+
"@logtape/logtape": "^2.2.0",
|
|
50
|
+
"@opentelemetry/api": "^1.9.1",
|
|
49
51
|
"@types/node": "^24.12.2",
|
|
50
52
|
"tsdown": "^0.22.0",
|
|
51
|
-
"typescript": "^6.0.0"
|
|
53
|
+
"typescript": "^6.0.0",
|
|
54
|
+
"@fedify/vocab-runtime": "^2.5.0-dev.2304+3a599380"
|
|
52
55
|
},
|
|
53
56
|
"dependencies": {
|
|
54
57
|
"@cfworker/json-schema": "^4.1.1",
|
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
import { deepStrictEqual, match, ok, rejects } from "node:assert/strict";
|
|
2
|
+
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { test } from "node:test";
|
|
6
|
+
import { pathToFileURL } from "node:url";
|
|
7
|
+
import { generateClasses } from "./class.ts";
|
|
8
|
+
import type { PropertySchema, TypeSchema } from "./schema.ts";
|
|
9
|
+
|
|
10
|
+
const NS = "https://example.com/";
|
|
11
|
+
const AS = "https://www.w3.org/ns/activitystreams";
|
|
12
|
+
const CONTEXT = `${NS}context`;
|
|
13
|
+
const XSD = "http://www.w3.org/2001/XMLSchema#";
|
|
14
|
+
const context = {
|
|
15
|
+
type: "@type",
|
|
16
|
+
id: "@id",
|
|
17
|
+
Fixture: `${NS}Fixture`,
|
|
18
|
+
Value: `${NS}Value`,
|
|
19
|
+
label: `${NS}label`,
|
|
20
|
+
value: `${NS}value`,
|
|
21
|
+
first: `${NS}first`,
|
|
22
|
+
second: `${NS}second`,
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
interface Instance {
|
|
26
|
+
values: unknown[];
|
|
27
|
+
value: unknown;
|
|
28
|
+
toJsonLd(options?: Record<string, unknown>): Promise<Record<string, unknown>>;
|
|
29
|
+
}
|
|
30
|
+
interface FixtureClass {
|
|
31
|
+
new (values?: Record<string, unknown>): Instance;
|
|
32
|
+
fromJsonLd(
|
|
33
|
+
document: unknown,
|
|
34
|
+
options?: Record<string, unknown>,
|
|
35
|
+
): Promise<Instance>;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function withFixture(
|
|
39
|
+
options: Partial<PropertySchema>,
|
|
40
|
+
run: (
|
|
41
|
+
Fixture: FixtureClass,
|
|
42
|
+
loader: (url: string) => Promise<unknown>,
|
|
43
|
+
source: string,
|
|
44
|
+
) => Promise<void>,
|
|
45
|
+
) {
|
|
46
|
+
const property = {
|
|
47
|
+
singularName: "value",
|
|
48
|
+
pluralName: "values",
|
|
49
|
+
uri: `${NS}value`,
|
|
50
|
+
compactName: "value",
|
|
51
|
+
description: "A value.",
|
|
52
|
+
range: [`${XSD}string`],
|
|
53
|
+
redundantProperties: [
|
|
54
|
+
{ uri: `${NS}first`, compactName: "first" },
|
|
55
|
+
{ uri: `${NS}second`, compactName: "second" },
|
|
56
|
+
],
|
|
57
|
+
...options,
|
|
58
|
+
} as PropertySchema;
|
|
59
|
+
const terms: Record<string, unknown> = { ...context };
|
|
60
|
+
for (const name of ["value", "first", "second"]) {
|
|
61
|
+
const term: Record<string, unknown> = { "@id": `${NS}${name}` };
|
|
62
|
+
if (property.range[0] === `${XSD}anyURI`) term["@type"] = "@id";
|
|
63
|
+
if ("container" in property && property.container != null) {
|
|
64
|
+
term["@container"] = `@${property.container}`;
|
|
65
|
+
}
|
|
66
|
+
terms[name] = term;
|
|
67
|
+
}
|
|
68
|
+
const loader = (url: string) => {
|
|
69
|
+
if (url !== AS && url !== CONTEXT) {
|
|
70
|
+
throw new Error(`Unexpected context: ${url}`);
|
|
71
|
+
}
|
|
72
|
+
return Promise.resolve({
|
|
73
|
+
contextUrl: null,
|
|
74
|
+
documentUrl: url,
|
|
75
|
+
document: {
|
|
76
|
+
"@context": url === AS ? { type: "@type", id: "@id" } : terms,
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
};
|
|
80
|
+
const types: Record<string, TypeSchema> = {
|
|
81
|
+
[`${NS}Fixture`]: {
|
|
82
|
+
name: "Fixture",
|
|
83
|
+
uri: `${NS}Fixture`,
|
|
84
|
+
compactName: "Fixture",
|
|
85
|
+
entity: false,
|
|
86
|
+
description: "A fixture.",
|
|
87
|
+
defaultContext: [AS, CONTEXT] as unknown as TypeSchema["defaultContext"],
|
|
88
|
+
properties: [property],
|
|
89
|
+
},
|
|
90
|
+
[`${NS}Value`]: {
|
|
91
|
+
name: "Value",
|
|
92
|
+
uri: `${NS}Value`,
|
|
93
|
+
compactName: "Value",
|
|
94
|
+
entity: false,
|
|
95
|
+
description: "An embedded value.",
|
|
96
|
+
defaultContext: [AS, CONTEXT] as unknown as TypeSchema["defaultContext"],
|
|
97
|
+
properties: [{
|
|
98
|
+
singularName: "label",
|
|
99
|
+
functional: true,
|
|
100
|
+
uri: `${NS}label`,
|
|
101
|
+
compactName: "label",
|
|
102
|
+
description: "A label.",
|
|
103
|
+
range: [`${XSD}string`],
|
|
104
|
+
}],
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
const source = (await Array.fromAsync(generateClasses(types))).join("");
|
|
108
|
+
const resolved = source.replace(
|
|
109
|
+
/from\s+"([^"]+)"/g,
|
|
110
|
+
(_, specifier: string) =>
|
|
111
|
+
`from ${JSON.stringify(import.meta.resolve(specifier))}`,
|
|
112
|
+
);
|
|
113
|
+
const dir = await mkdtemp(join(tmpdir(), "fedify-codec-"));
|
|
114
|
+
try {
|
|
115
|
+
const file = join(dir, "fixture.mts");
|
|
116
|
+
await writeFile(file, resolved);
|
|
117
|
+
const { Fixture } = await import(pathToFileURL(file).href);
|
|
118
|
+
await run(Fixture, loader, source);
|
|
119
|
+
} finally {
|
|
120
|
+
await rm(dir, { recursive: true, force: true });
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const literal = (value: unknown) => ({ "@value": value });
|
|
125
|
+
const document = (properties: Record<string, unknown>) => ({
|
|
126
|
+
"@type": [`${NS}Fixture`],
|
|
127
|
+
...properties,
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test("redundant properties select the first non-empty entire set", async () => {
|
|
131
|
+
await withFixture({}, async (Fixture, contextLoader) => {
|
|
132
|
+
for (
|
|
133
|
+
const [properties, expected] of [
|
|
134
|
+
[{ [`${NS}first`]: [literal("a"), literal("b"), literal("a")] }, [
|
|
135
|
+
"a",
|
|
136
|
+
"b",
|
|
137
|
+
"a",
|
|
138
|
+
]],
|
|
139
|
+
[{ [`${NS}value`]: [], [`${NS}first`]: [literal("a")] }, ["a"]],
|
|
140
|
+
[{ [`${NS}first`]: [], [`${NS}second`]: [literal("b")] }, ["b"]],
|
|
141
|
+
[{ [`${NS}value`]: [literal("c")], [`${NS}first`]: [literal("a")] }, [
|
|
142
|
+
"c",
|
|
143
|
+
]],
|
|
144
|
+
[{ [`${NS}first`]: [literal("a")], [`${NS}second`]: [literal("b")] }, [
|
|
145
|
+
"a",
|
|
146
|
+
]],
|
|
147
|
+
[{}, []],
|
|
148
|
+
] as const
|
|
149
|
+
) {
|
|
150
|
+
const parsed = await Fixture.fromJsonLd(document(properties), {
|
|
151
|
+
contextLoader,
|
|
152
|
+
});
|
|
153
|
+
deepStrictEqual(parsed.values, expected);
|
|
154
|
+
}
|
|
155
|
+
const parsed = await Fixture.fromJsonLd({
|
|
156
|
+
"@context": termsForRead(),
|
|
157
|
+
type: "Fixture",
|
|
158
|
+
alternate: ["a", "b", "a"],
|
|
159
|
+
}, { contextLoader });
|
|
160
|
+
deepStrictEqual(parsed.values, ["a", "b", "a"]);
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
function termsForRead() {
|
|
165
|
+
return { ...context, alternate: `${NS}first` };
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
test("redundant properties do not retry invalid canonical values", async () => {
|
|
169
|
+
await withFixture({
|
|
170
|
+
range: [
|
|
171
|
+
`${XSD}string`,
|
|
172
|
+
"http://www.w3.org/1999/02/22-rdf-syntax-ns#langString",
|
|
173
|
+
],
|
|
174
|
+
}, async (Fixture, contextLoader) => {
|
|
175
|
+
const parsed = await Fixture.fromJsonLd(
|
|
176
|
+
document({
|
|
177
|
+
[`${NS}value`]: [literal(42)],
|
|
178
|
+
[`${NS}first`]: [literal("fallback")],
|
|
179
|
+
}),
|
|
180
|
+
{ contextLoader },
|
|
181
|
+
);
|
|
182
|
+
deepStrictEqual(parsed.values, []);
|
|
183
|
+
});
|
|
184
|
+
await withFixture(
|
|
185
|
+
{ range: ["fedify:url"] },
|
|
186
|
+
async (Fixture, contextLoader) => {
|
|
187
|
+
const parsed = await Fixture.fromJsonLd(
|
|
188
|
+
document({
|
|
189
|
+
[`${NS}value`]: [literal("http://[")],
|
|
190
|
+
[`${NS}first`]: [literal("https://example.com/fallback")],
|
|
191
|
+
}),
|
|
192
|
+
{ contextLoader },
|
|
193
|
+
);
|
|
194
|
+
deepStrictEqual(parsed.values, []);
|
|
195
|
+
},
|
|
196
|
+
);
|
|
197
|
+
await withFixture(
|
|
198
|
+
{ range: [`${XSD}anyURI`] },
|
|
199
|
+
async (Fixture, contextLoader) => {
|
|
200
|
+
await rejects(Fixture.fromJsonLd(
|
|
201
|
+
document({
|
|
202
|
+
[`${NS}value`]: [{ "@id": "http://[" }],
|
|
203
|
+
[`${NS}first`]: [{ "@id": "https://example.com/fallback" }],
|
|
204
|
+
}),
|
|
205
|
+
{ contextLoader },
|
|
206
|
+
));
|
|
207
|
+
},
|
|
208
|
+
);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
for (const policy of [undefined, "all", "canonical"] as const) {
|
|
212
|
+
test(`redundant properties write policy ${policy ?? "default"}`, async () => {
|
|
213
|
+
await withFixture(
|
|
214
|
+
{ redundantPropertiesWrite: policy },
|
|
215
|
+
async (Fixture, contextLoader) => {
|
|
216
|
+
for (const values of [[], ["a"], ["a", "b", "a"]]) {
|
|
217
|
+
const instance = new Fixture({ values });
|
|
218
|
+
const compact = await instance.toJsonLd({
|
|
219
|
+
format: "compact",
|
|
220
|
+
contextLoader,
|
|
221
|
+
});
|
|
222
|
+
deepStrictEqual(await instance.toJsonLd({ contextLoader }), compact);
|
|
223
|
+
const expanded = await instance.toJsonLd({
|
|
224
|
+
format: "expand",
|
|
225
|
+
contextLoader,
|
|
226
|
+
}) as unknown as Record<string, unknown>[];
|
|
227
|
+
for (const alias of ["first", "second"]) {
|
|
228
|
+
if (policy === "canonical" || values.length === 0) {
|
|
229
|
+
ok(!(alias in compact));
|
|
230
|
+
ok(!(`${NS}${alias}` in expanded[0]));
|
|
231
|
+
} else {
|
|
232
|
+
deepStrictEqual(compact[alias], compact.value);
|
|
233
|
+
deepStrictEqual(
|
|
234
|
+
expanded[0][`${NS}${alias}`],
|
|
235
|
+
expanded[0][`${NS}value`],
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
const custom = await instance.toJsonLd({
|
|
240
|
+
format: "compact",
|
|
241
|
+
contextLoader,
|
|
242
|
+
context: { ...context, renamed: `${NS}value` },
|
|
243
|
+
});
|
|
244
|
+
deepStrictEqual(
|
|
245
|
+
(await Fixture.fromJsonLd(custom, { contextLoader })).values,
|
|
246
|
+
values,
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
const aliasOnly = await Fixture.fromJsonLd(
|
|
250
|
+
document({ [`${NS}first`]: [literal("read")] }),
|
|
251
|
+
{ contextLoader },
|
|
252
|
+
);
|
|
253
|
+
deepStrictEqual(aliasOnly.values, ["read"]);
|
|
254
|
+
},
|
|
255
|
+
);
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
for (const container of ["list", "graph"] as const) {
|
|
260
|
+
for (const policy of ["all", "canonical"] as const) {
|
|
261
|
+
test(`redundant ${container} containers with ${policy} writes`, async () => {
|
|
262
|
+
await withFixture({
|
|
263
|
+
container,
|
|
264
|
+
redundantPropertiesWrite: policy,
|
|
265
|
+
range: container === "graph" ? [`${NS}Value`] : [`${XSD}string`],
|
|
266
|
+
}, async (Fixture, contextLoader) => {
|
|
267
|
+
const items = container === "list"
|
|
268
|
+
? [literal("a"), literal("b"), literal("a")]
|
|
269
|
+
: [
|
|
270
|
+
{ "@type": [`${NS}Value`], [`${NS}label`]: [literal("a")] },
|
|
271
|
+
{ "@type": [`${NS}Value`], [`${NS}label`]: [literal("b")] },
|
|
272
|
+
];
|
|
273
|
+
const set = container === "list"
|
|
274
|
+
? [{ "@list": items }]
|
|
275
|
+
: items.map((item) => ({ "@graph": [item] }));
|
|
276
|
+
const parsed = await Fixture.fromJsonLd(
|
|
277
|
+
document({
|
|
278
|
+
[`${NS}first`]: set,
|
|
279
|
+
[`${NS}second`]: [{ "@list": [literal("other")] }],
|
|
280
|
+
}),
|
|
281
|
+
{ contextLoader },
|
|
282
|
+
);
|
|
283
|
+
deepStrictEqual(
|
|
284
|
+
container === "list"
|
|
285
|
+
? parsed.values
|
|
286
|
+
: parsed.values.map((v) => (v as { label: string }).label),
|
|
287
|
+
container === "list" ? ["a", "b", "a"] : ["a", "b"],
|
|
288
|
+
);
|
|
289
|
+
const instance = new Fixture({ values: parsed.values });
|
|
290
|
+
const compact = await instance.toJsonLd({
|
|
291
|
+
format: "compact",
|
|
292
|
+
contextLoader,
|
|
293
|
+
});
|
|
294
|
+
if (container === "list" || policy === "all") {
|
|
295
|
+
deepStrictEqual(await instance.toJsonLd({ contextLoader }), compact);
|
|
296
|
+
} else {
|
|
297
|
+
// Parsed children retain expanded caches in the heuristic path.
|
|
298
|
+
const defaultOutput = await instance.toJsonLd({ contextLoader });
|
|
299
|
+
const decoded = await Fixture.fromJsonLd(defaultOutput, {
|
|
300
|
+
contextLoader,
|
|
301
|
+
});
|
|
302
|
+
deepStrictEqual(
|
|
303
|
+
decoded.values.map((v) => (v as { label: string }).label),
|
|
304
|
+
["a", "b"],
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
const expanded = await instance.toJsonLd({
|
|
308
|
+
format: "expand",
|
|
309
|
+
contextLoader,
|
|
310
|
+
}) as unknown as Record<string, unknown>[];
|
|
311
|
+
for (const alias of ["first", "second"]) {
|
|
312
|
+
if (policy === "canonical") {
|
|
313
|
+
ok(!(`${NS}${alias}` in expanded[0]));
|
|
314
|
+
ok(!(alias in compact));
|
|
315
|
+
} else {
|
|
316
|
+
deepStrictEqual(
|
|
317
|
+
expanded[0][`${NS}${alias}`],
|
|
318
|
+
expanded[0][`${NS}value`],
|
|
319
|
+
);
|
|
320
|
+
deepStrictEqual(compact[alias], compact.value);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
const roundtrip = await Fixture.fromJsonLd(compact, { contextLoader });
|
|
324
|
+
deepStrictEqual(
|
|
325
|
+
container === "list"
|
|
326
|
+
? roundtrip.values
|
|
327
|
+
: roundtrip.values.map((v) => (v as { label: string }).label),
|
|
328
|
+
container === "list" ? ["a", "b", "a"] : ["a", "b"],
|
|
329
|
+
);
|
|
330
|
+
const empty = await Fixture.fromJsonLd(
|
|
331
|
+
document({
|
|
332
|
+
[`${NS}value`]: [{ [`@${container}`]: [] }],
|
|
333
|
+
[`${NS}first`]: set,
|
|
334
|
+
}),
|
|
335
|
+
{ contextLoader },
|
|
336
|
+
);
|
|
337
|
+
deepStrictEqual(empty.values, []);
|
|
338
|
+
});
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
for (const functional of [false, true]) {
|
|
344
|
+
test(`URI-only aliases and portable read keys (functional=${functional})`, async () => {
|
|
345
|
+
for (const policy of ["all", "canonical"] as const) {
|
|
346
|
+
await withFixture({
|
|
347
|
+
...(functional ? { functional: true } : {}),
|
|
348
|
+
range: [`${XSD}anyURI`],
|
|
349
|
+
redundantPropertiesWrite: policy,
|
|
350
|
+
redundantProperties: [{ uri: `${NS}first` }],
|
|
351
|
+
}, async (Fixture, contextLoader, source) => {
|
|
352
|
+
ok(!source.includes("result[undefined]"));
|
|
353
|
+
match(source, /PORTABLE_IRI_KEYS[^\n]+https:\/\/example.com\/first/);
|
|
354
|
+
const url = new URL("https://example.com/object");
|
|
355
|
+
const instance = new Fixture(
|
|
356
|
+
functional ? { value: url } : { values: [url] },
|
|
357
|
+
);
|
|
358
|
+
const compact = await instance.toJsonLd({
|
|
359
|
+
format: "compact",
|
|
360
|
+
contextLoader,
|
|
361
|
+
});
|
|
362
|
+
deepStrictEqual(await instance.toJsonLd({ contextLoader }), compact);
|
|
363
|
+
const expanded = await instance.toJsonLd({
|
|
364
|
+
format: "expand",
|
|
365
|
+
contextLoader,
|
|
366
|
+
}) as unknown as Record<string, unknown>[];
|
|
367
|
+
if (policy === "all") {
|
|
368
|
+
deepStrictEqual(expanded[0][`${NS}first`], [{ "@id": url.href }]);
|
|
369
|
+
} else ok(!(`${NS}first` in expanded[0]));
|
|
370
|
+
const custom = await instance.toJsonLd({
|
|
371
|
+
format: "compact",
|
|
372
|
+
contextLoader,
|
|
373
|
+
context: {
|
|
374
|
+
type: "@type",
|
|
375
|
+
Fixture: `${NS}Fixture`,
|
|
376
|
+
value: { "@id": `${NS}value`, "@type": "@id" },
|
|
377
|
+
},
|
|
378
|
+
});
|
|
379
|
+
if (policy === "all") {
|
|
380
|
+
deepStrictEqual(custom[`${NS}first`], { "@id": url.href });
|
|
381
|
+
} else {
|
|
382
|
+
ok(!(`${NS}first` in custom));
|
|
383
|
+
}
|
|
384
|
+
const parsed = await Fixture.fromJsonLd(
|
|
385
|
+
document({
|
|
386
|
+
[`${NS}first`]: [{ "@id": "ap://did:key:z6Mkabc/objects/1" }],
|
|
387
|
+
}),
|
|
388
|
+
{ contextLoader },
|
|
389
|
+
);
|
|
390
|
+
const value = functional ? parsed.value : parsed.values[0];
|
|
391
|
+
ok(value instanceof URL);
|
|
392
|
+
const cached = await parsed.toJsonLd({ contextLoader });
|
|
393
|
+
deepStrictEqual(cached[`${NS}first`], [{
|
|
394
|
+
"@id": "ap+ef61://did:key:z6Mkabc/objects/1",
|
|
395
|
+
}]);
|
|
396
|
+
const compactInput = await Fixture.fromJsonLd({
|
|
397
|
+
"@context": [AS, CONTEXT],
|
|
398
|
+
type: "Fixture",
|
|
399
|
+
first: "ap://did:key:z6Mkabc/objects/1",
|
|
400
|
+
}, { contextLoader });
|
|
401
|
+
const compactCache = await compactInput.toJsonLd({ contextLoader });
|
|
402
|
+
deepStrictEqual(
|
|
403
|
+
compactCache.first,
|
|
404
|
+
"ap+ef61://did:key:z6Mkabc/objects/1",
|
|
405
|
+
);
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
});
|
|
409
|
+
}
|
package/src/codec.ts
CHANGED
|
@@ -162,8 +162,13 @@ export async function* generateEncoder(
|
|
|
162
162
|
? compactItems
|
|
163
163
|
: compactItems[0];
|
|
164
164
|
`;
|
|
165
|
-
if (
|
|
165
|
+
if (
|
|
166
|
+
property.redundantPropertiesWrite !== "canonical" &&
|
|
167
|
+
property.redundantProperties != null &&
|
|
168
|
+
!(isNonFunctionalProperty(property) && property.container != null)
|
|
169
|
+
) {
|
|
166
170
|
for (const prop of property.redundantProperties) {
|
|
171
|
+
if (prop.compactName == null) continue;
|
|
167
172
|
yield `
|
|
168
173
|
result[${JSON.stringify(prop.compactName)}]
|
|
169
174
|
= compactItems.length > 1
|
|
@@ -255,7 +260,10 @@ export async function* generateEncoder(
|
|
|
255
260
|
);
|
|
256
261
|
values[${JSON.stringify(property.uri)}] = propValue;
|
|
257
262
|
`;
|
|
258
|
-
if (
|
|
263
|
+
if (
|
|
264
|
+
property.redundantPropertiesWrite !== "canonical" &&
|
|
265
|
+
property.redundantProperties != null
|
|
266
|
+
) {
|
|
259
267
|
for (const prop of property.redundantProperties) {
|
|
260
268
|
yield `
|
|
261
269
|
values[${JSON.stringify(prop.uri)}] = propValue;
|
|
@@ -348,6 +356,11 @@ export async function* generateEncoder(
|
|
|
348
356
|
for (const property of type.properties) {
|
|
349
357
|
if (
|
|
350
358
|
property.extraContext != null ||
|
|
359
|
+
(property.redundantPropertiesWrite !== "canonical" &&
|
|
360
|
+
property.redundantProperties != null &&
|
|
361
|
+
property.redundantProperties.length > 0 &&
|
|
362
|
+
(property.redundantProperties.some((p) => p.compactName == null) ||
|
|
363
|
+
(isNonFunctionalProperty(property) && property.container != null))) ||
|
|
351
364
|
!property.range.every((r) => isCompactableType(r, types))
|
|
352
365
|
) {
|
|
353
366
|
yield `
|
|
@@ -544,7 +557,7 @@ export async function* generateDecoder(
|
|
|
544
557
|
yield `
|
|
545
558
|
let ${arrayVariable} = ${propertyValues};
|
|
546
559
|
`;
|
|
547
|
-
if (property.
|
|
560
|
+
if (property.redundantProperties != null) {
|
|
548
561
|
for (const prop of property.redundantProperties) {
|
|
549
562
|
yield `
|
|
550
563
|
if (${arrayVariable} == null || ${arrayVariable}.length < 1) {
|
package/src/schema.test.ts
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
|
-
import { deepStrictEqual, ok, throws } from "node:assert";
|
|
1
|
+
import { deepStrictEqual, ok, rejects, throws } from "node:assert";
|
|
2
2
|
import { test } from "node:test";
|
|
3
|
+
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { stringify } from "yaml";
|
|
3
7
|
import {
|
|
4
8
|
hasSingularAccessor,
|
|
5
9
|
isNonFunctionalProperty,
|
|
10
|
+
loadSchemaFiles,
|
|
6
11
|
type PropertySchema,
|
|
7
12
|
type TypeSchema,
|
|
8
13
|
type TypeUri,
|
|
@@ -244,3 +249,71 @@ test("validateTypeSchemas() rejects mixed fedify:vocabEntityType ranges", () =>
|
|
|
244
249
|
},
|
|
245
250
|
);
|
|
246
251
|
});
|
|
252
|
+
|
|
253
|
+
test("loadSchemaFiles validates redundant properties on both property kinds", async () => {
|
|
254
|
+
const dir = await mkdtemp(join(tmpdir(), "fedify-schema-"));
|
|
255
|
+
try {
|
|
256
|
+
for (const functional of [false, true]) {
|
|
257
|
+
const property = {
|
|
258
|
+
singularName: "value",
|
|
259
|
+
...(functional ? { functional: true } : { pluralName: "values" }),
|
|
260
|
+
uri: "https://example.com/value",
|
|
261
|
+
description: "A value.",
|
|
262
|
+
range: ["http://www.w3.org/2001/XMLSchema#string"],
|
|
263
|
+
redundantProperties: [{ uri: "https://example.com/alias" }],
|
|
264
|
+
};
|
|
265
|
+
const type = {
|
|
266
|
+
name: "Fixture",
|
|
267
|
+
uri: "https://example.com/Fixture",
|
|
268
|
+
entity: false,
|
|
269
|
+
description: "A fixture.",
|
|
270
|
+
defaultContext: {},
|
|
271
|
+
properties: [property],
|
|
272
|
+
};
|
|
273
|
+
for (const policy of [undefined, "all", "canonical"]) {
|
|
274
|
+
await writeFile(
|
|
275
|
+
join(dir, "fixture.yaml"),
|
|
276
|
+
stringify({
|
|
277
|
+
...type,
|
|
278
|
+
properties: [{ ...property, redundantPropertiesWrite: policy }],
|
|
279
|
+
}),
|
|
280
|
+
);
|
|
281
|
+
const types = await loadSchemaFiles(dir);
|
|
282
|
+
deepStrictEqual(
|
|
283
|
+
types[type.uri].properties[0].redundantProperties,
|
|
284
|
+
property.redundantProperties,
|
|
285
|
+
);
|
|
286
|
+
deepStrictEqual(
|
|
287
|
+
types[type.uri].properties[0].redundantPropertiesWrite,
|
|
288
|
+
policy,
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
for (
|
|
292
|
+
const invalid of [
|
|
293
|
+
{ redundantPropertiesWrite: "merge" },
|
|
294
|
+
{ redundantPropertiesWrite: 42 },
|
|
295
|
+
{ redundantProperties: "alias" },
|
|
296
|
+
{ redundantProperties: [{}] },
|
|
297
|
+
{ redundantProperties: [{ uri: 42 }] },
|
|
298
|
+
{
|
|
299
|
+
redundantProperties: [{
|
|
300
|
+
uri: "https://example.com/alias",
|
|
301
|
+
compactName: 42,
|
|
302
|
+
}],
|
|
303
|
+
},
|
|
304
|
+
]
|
|
305
|
+
) {
|
|
306
|
+
await writeFile(
|
|
307
|
+
join(dir, "fixture.yaml"),
|
|
308
|
+
stringify({
|
|
309
|
+
...type,
|
|
310
|
+
properties: [{ ...property, ...invalid }],
|
|
311
|
+
}),
|
|
312
|
+
);
|
|
313
|
+
await rejects(loadSchemaFiles(dir), AggregateError);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
} finally {
|
|
317
|
+
await rm(dir, { recursive: true, force: true });
|
|
318
|
+
}
|
|
319
|
+
});
|
package/src/schema.ts
CHANGED
|
@@ -85,6 +85,27 @@ export interface TypeSchema {
|
|
|
85
85
|
}
|
|
86
86
|
|
|
87
87
|
export interface PropertySchemaBase {
|
|
88
|
+
/**
|
|
89
|
+
* Synonyms read after the canonical property, in declaration order.
|
|
90
|
+
* Deserialization selects the first non-empty raw value set before decoding
|
|
91
|
+
* its values and containers; it never merges synonyms or retries invalid
|
|
92
|
+
* values. Serialization writes all synonyms by default.
|
|
93
|
+
*/
|
|
94
|
+
redundantProperties?: {
|
|
95
|
+
/** The qualified URI of the synonym. */
|
|
96
|
+
uri: string;
|
|
97
|
+
|
|
98
|
+
/** The synonym's key in the default compact JSON-LD context. */
|
|
99
|
+
compactName?: string;
|
|
100
|
+
}[];
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Whether serialization writes every synonym or only the canonical property.
|
|
104
|
+
* Defaults to `"all"`. This does not change deserialization precedence or
|
|
105
|
+
* the existing cache of parsed JSON-LD documents.
|
|
106
|
+
*/
|
|
107
|
+
redundantPropertiesWrite?: "all" | "canonical";
|
|
108
|
+
|
|
88
109
|
/**
|
|
89
110
|
* A context URL added to the default serialization context when this
|
|
90
111
|
* property's expanded IRI appears in the compacted document. Explicit
|
|
@@ -229,25 +250,6 @@ export type PropertySchema =
|
|
|
229
250
|
* and `singularAccessor` should not be specified.
|
|
230
251
|
*/
|
|
231
252
|
functional: true;
|
|
232
|
-
|
|
233
|
-
/**
|
|
234
|
-
* If it's present, those redundant properties are also filled with
|
|
235
|
-
* the same value altogether when the object is serialized into
|
|
236
|
-
* JSON-LD. When it's deserialized from JSON-LD, it tries to
|
|
237
|
-
* parse the values of the specified properties in order.
|
|
238
|
-
*/
|
|
239
|
-
redundantProperties?: {
|
|
240
|
-
/**
|
|
241
|
-
* The qualified URI of the property.
|
|
242
|
-
*/
|
|
243
|
-
uri: string;
|
|
244
|
-
|
|
245
|
-
/**
|
|
246
|
-
* The property name used in the compacted JSON-LD document. It is used
|
|
247
|
-
* as the key of the property.
|
|
248
|
-
*/
|
|
249
|
-
compactName?: string;
|
|
250
|
-
}[];
|
|
251
253
|
};
|
|
252
254
|
|
|
253
255
|
/**
|
package/src/schema.yaml
CHANGED
|
@@ -45,33 +45,41 @@ $defs:
|
|
|
45
45
|
functional:
|
|
46
46
|
description: *functional-description
|
|
47
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
48
|
required:
|
|
70
49
|
- functional
|
|
71
50
|
|
|
72
51
|
property-base:
|
|
73
52
|
type: object
|
|
74
53
|
properties:
|
|
54
|
+
redundantProperties:
|
|
55
|
+
description: >-
|
|
56
|
+
Synonyms read after the canonical property in declaration order.
|
|
57
|
+
The first non-empty raw value set is selected before decoding values
|
|
58
|
+
and containers. Synonyms are never merged or retried on invalid values.
|
|
59
|
+
Serialization writes all synonyms by default.
|
|
60
|
+
type: array
|
|
61
|
+
items:
|
|
62
|
+
type: object
|
|
63
|
+
properties:
|
|
64
|
+
uri:
|
|
65
|
+
description: The qualified URI of the property.
|
|
66
|
+
type: string
|
|
67
|
+
format: uri
|
|
68
|
+
compactName:
|
|
69
|
+
description: >-
|
|
70
|
+
The property name used in the compacted JSON-LD document.
|
|
71
|
+
It is used as the key of the property.
|
|
72
|
+
type: string
|
|
73
|
+
required:
|
|
74
|
+
- uri
|
|
75
|
+
redundantPropertiesWrite:
|
|
76
|
+
description: >-
|
|
77
|
+
Whether serialization writes all synonyms or only the canonical
|
|
78
|
+
property. Does not change deserialization precedence.
|
|
79
|
+
enum:
|
|
80
|
+
- all
|
|
81
|
+
- canonical
|
|
82
|
+
default: all
|
|
75
83
|
extraContext:
|
|
76
84
|
description: >-
|
|
77
85
|
A context URL added to default serialization when this property's
|