diff --git a/.agents/skills/add-vocab/SKILL.md b/.agents/skills/add-vocab/SKILL.md index 664001e2f..dbabc09bc 100644 --- a/.agents/skills/add-vocab/SKILL.md +++ b/.agents/skills/add-vocab/SKILL.md @@ -204,7 +204,8 @@ Generates `get()` async iterable and optionally a singular accessor. Required: `pluralName`, `singularName`, `uri`, `description`, `range` Optional: `singularAccessor` (default `false`), `compactName`, `subpropertyOf`, -`container` (`"graph"` or `"list"`), `embedContext`, `untyped` +`container` (`"graph"` or `"list"`), `embedContext`, `untyped`, +`redundantProperties`, `redundantPropertiesWrite` ### Functional property (exactly one value) @@ -223,13 +224,21 @@ Generates a single `get()` / `` accessor. Required: `singularName`, `functional: true`, `uri`, `description`, `range` Optional: `compactName`, `subpropertyOf`, `redundantProperties`, `untyped`, -`embedContext` +`embedContext`, `redundantPropertiesWrite` -### Redundant properties (functional only) +### Redundant properties When a property has equivalent URIs from multiple vocabularies, use -`redundantProperties` to write all aliases on serialization and try them in -order on deserialization: +`redundantProperties` on functional or non-functional properties. Reading +selects the first non-empty raw value set, starting with the canonical property +and then aliases in declaration order, before decoding values and containers. +It never merges aliases or retries invalid values. Writing defaults to all +aliases; set `redundantPropertiesWrite: canonical` to write only the canonical +property. An alias without `compactName` uses the JSON-LD processor, as do +writable list and graph aliases. IRI-valued compact terms need +`"@type": "@id"` in the context. + +For example: ~~~~ yaml - singularName: quoteUrl diff --git a/CHANGES.md b/CHANGES.md index 9603a1f41..7ba91684d 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -32,6 +32,18 @@ To be released. [#896]: https://github.com/fedify-dev/fedify/issues/896 [#1204]: https://github.com/fedify-dev/fedify/issues/1204 +### @fedify/vocab-tools + + - Added non-functional `redundantProperties` support with ordered fallback + to the first non-empty value set, preserving lists and graphs without + merging synonyms. Added `redundantPropertiesWrite: canonical` to write + only the canonical property while accepting synonyms. Fixed serialization + of synonyms without a compact name and validation of synonym definitions. + [[#1210], [#1215]] + +[#1210]: https://github.com/fedify-dev/fedify/issues/1210 +[#1215]: https://github.com/fedify-dev/fedify/pull/1215 + Version 2.4.0 ------------- diff --git a/changes.d/vocab-tools/redundant-properties.md b/changes.d/vocab-tools/redundant-properties.md new file mode 100644 index 000000000..96862c2b7 --- /dev/null +++ b/changes.d/vocab-tools/redundant-properties.md @@ -0,0 +1,11 @@ +--- +links: + '#1210': https://github.com/fedify-dev/fedify/issues/1210 + '#1215': https://github.com/fedify-dev/fedify/pull/1215 +--- + - Added non-functional `redundantProperties` support with ordered fallback + to the first non-empty value set, preserving lists and graphs without + merging synonyms. Added `redundantPropertiesWrite: canonical` to write + only the canonical property while accepting synonyms. Fixed serialization + of synonyms without a compact name and validation of synonym definitions. + [[#1210], [#1215]] diff --git a/packages/vocab-tools/README.md b/packages/vocab-tools/README.md index 5d0ddc814..012c9dd52 100644 --- a/packages/vocab-tools/README.md +++ b/packages/vocab-tools/README.md @@ -64,3 +64,43 @@ of trusting a matching origin. Locally constructed values, previously fetched values, and the explicit `crossOrigin: "trust"` option retain their usual behavior. This does not verify id-less embedded objects or establish publishing authority. + + +Redundant properties +-------------------- + +Both functional and non-functional property schemas can declare synonyms in +`redundantProperties`. On reading, the canonical property takes precedence, +followed by synonyms in declaration order. The first non-empty raw value set +is decoded in full, preserving its order and multiplicity. Values from other +synonyms are never merged, even when the selected values fail range validation. +List and graph containers are decoded only after choosing the property. + +`redundantPropertiesWrite` defaults to `all`, which writes the canonical +property and every synonym with the same complete container representation. +Set it to `canonical` to write only the canonical property while still accepting +synonyms on reading: + +~~~~ yaml +- singularName: license + pluralName: licenses + uri: "https://example.com/license" + description: The licenses of the object. + range: + - "http://www.w3.org/2001/XMLSchema#anyURI" + redundantProperties: + - uri: "https://schema.org/license" + - uri: "http://creativecommons.org/ns#license" + redundantPropertiesWrite: canonical +~~~~ + +A synonym's `compactName` is optional. Without it, serialization uses the +JSON-LD processor to select a context term or retain the full URI. For IRI +values, context terms must declare `"@type": "@id"`; container terms must +carry the appropriate `@container`. Writable container synonyms also use the +processor to preserve their representation. Explicit caller contexts take +precedence over the default context. + +As with other properties, an object parsed with `fromJsonLd()` retains its +original JSON-LD document for default serialization. Pass `format: "compact"` +or `format: "expand"` to serialize its decoded values under the write policy. diff --git a/packages/vocab-tools/package.json b/packages/vocab-tools/package.json index 771bfbd09..0e1367196 100644 --- a/packages/vocab-tools/package.json +++ b/packages/vocab-tools/package.json @@ -60,6 +60,9 @@ }, "license": "MIT", "devDependencies": { + "@fedify/vocab-runtime": "workspace:^", + "@logtape/logtape": "catalog:", + "@opentelemetry/api": "catalog:", "@types/node": "catalog:", "tsdown": "catalog:", "typescript": "catalog:" diff --git a/packages/vocab-tools/src/codec.test.ts b/packages/vocab-tools/src/codec.test.ts new file mode 100644 index 000000000..a8b63c8fc --- /dev/null +++ b/packages/vocab-tools/src/codec.test.ts @@ -0,0 +1,409 @@ +import { deepStrictEqual, match, ok, rejects } from "node:assert/strict"; +import { mkdtemp, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { test } from "node:test"; +import { pathToFileURL } from "node:url"; +import { generateClasses } from "./class.ts"; +import type { PropertySchema, TypeSchema } from "./schema.ts"; + +const NS = "https://example.com/"; +const AS = "https://www.w3.org/ns/activitystreams"; +const CONTEXT = `${NS}context`; +const XSD = "http://www.w3.org/2001/XMLSchema#"; +const context = { + type: "@type", + id: "@id", + Fixture: `${NS}Fixture`, + Value: `${NS}Value`, + label: `${NS}label`, + value: `${NS}value`, + first: `${NS}first`, + second: `${NS}second`, +}; + +interface Instance { + values: unknown[]; + value: unknown; + toJsonLd(options?: Record): Promise>; +} +interface FixtureClass { + new (values?: Record): Instance; + fromJsonLd( + document: unknown, + options?: Record, + ): Promise; +} + +async function withFixture( + options: Partial, + run: ( + Fixture: FixtureClass, + loader: (url: string) => Promise, + source: string, + ) => Promise, +) { + const property = { + singularName: "value", + pluralName: "values", + uri: `${NS}value`, + compactName: "value", + description: "A value.", + range: [`${XSD}string`], + redundantProperties: [ + { uri: `${NS}first`, compactName: "first" }, + { uri: `${NS}second`, compactName: "second" }, + ], + ...options, + } as PropertySchema; + const terms: Record = { ...context }; + for (const name of ["value", "first", "second"]) { + const term: Record = { "@id": `${NS}${name}` }; + if (property.range[0] === `${XSD}anyURI`) term["@type"] = "@id"; + if ("container" in property && property.container != null) { + term["@container"] = `@${property.container}`; + } + terms[name] = term; + } + const loader = (url: string) => { + if (url !== AS && url !== CONTEXT) { + throw new Error(`Unexpected context: ${url}`); + } + return Promise.resolve({ + contextUrl: null, + documentUrl: url, + document: { + "@context": url === AS ? { type: "@type", id: "@id" } : terms, + }, + }); + }; + const types: Record = { + [`${NS}Fixture`]: { + name: "Fixture", + uri: `${NS}Fixture`, + compactName: "Fixture", + entity: false, + description: "A fixture.", + defaultContext: [AS, CONTEXT] as unknown as TypeSchema["defaultContext"], + properties: [property], + }, + [`${NS}Value`]: { + name: "Value", + uri: `${NS}Value`, + compactName: "Value", + entity: false, + description: "An embedded value.", + defaultContext: [AS, CONTEXT] as unknown as TypeSchema["defaultContext"], + properties: [{ + singularName: "label", + functional: true, + uri: `${NS}label`, + compactName: "label", + description: "A label.", + range: [`${XSD}string`], + }], + }, + }; + const source = (await Array.fromAsync(generateClasses(types))).join(""); + const resolved = source.replace( + /from\s+"([^"]+)"/g, + (_, specifier: string) => + `from ${JSON.stringify(import.meta.resolve(specifier))}`, + ); + const dir = await mkdtemp(join(tmpdir(), "fedify-codec-")); + try { + const file = join(dir, "fixture.mts"); + await writeFile(file, resolved); + const { Fixture } = await import(pathToFileURL(file).href); + await run(Fixture, loader, source); + } finally { + await rm(dir, { recursive: true, force: true }); + } +} + +const literal = (value: unknown) => ({ "@value": value }); +const document = (properties: Record) => ({ + "@type": [`${NS}Fixture`], + ...properties, +}); + +test("redundant properties select the first non-empty entire set", async () => { + await withFixture({}, async (Fixture, contextLoader) => { + for ( + const [properties, expected] of [ + [{ [`${NS}first`]: [literal("a"), literal("b"), literal("a")] }, [ + "a", + "b", + "a", + ]], + [{ [`${NS}value`]: [], [`${NS}first`]: [literal("a")] }, ["a"]], + [{ [`${NS}first`]: [], [`${NS}second`]: [literal("b")] }, ["b"]], + [{ [`${NS}value`]: [literal("c")], [`${NS}first`]: [literal("a")] }, [ + "c", + ]], + [{ [`${NS}first`]: [literal("a")], [`${NS}second`]: [literal("b")] }, [ + "a", + ]], + [{}, []], + ] as const + ) { + const parsed = await Fixture.fromJsonLd(document(properties), { + contextLoader, + }); + deepStrictEqual(parsed.values, expected); + } + const parsed = await Fixture.fromJsonLd({ + "@context": termsForRead(), + type: "Fixture", + alternate: ["a", "b", "a"], + }, { contextLoader }); + deepStrictEqual(parsed.values, ["a", "b", "a"]); + }); +}); + +function termsForRead() { + return { ...context, alternate: `${NS}first` }; +} + +test("redundant properties do not retry invalid canonical values", async () => { + await withFixture({ + range: [ + `${XSD}string`, + "http://www.w3.org/1999/02/22-rdf-syntax-ns#langString", + ], + }, async (Fixture, contextLoader) => { + const parsed = await Fixture.fromJsonLd( + document({ + [`${NS}value`]: [literal(42)], + [`${NS}first`]: [literal("fallback")], + }), + { contextLoader }, + ); + deepStrictEqual(parsed.values, []); + }); + await withFixture( + { range: ["fedify:url"] }, + async (Fixture, contextLoader) => { + const parsed = await Fixture.fromJsonLd( + document({ + [`${NS}value`]: [literal("http://[")], + [`${NS}first`]: [literal("https://example.com/fallback")], + }), + { contextLoader }, + ); + deepStrictEqual(parsed.values, []); + }, + ); + await withFixture( + { range: [`${XSD}anyURI`] }, + async (Fixture, contextLoader) => { + await rejects(Fixture.fromJsonLd( + document({ + [`${NS}value`]: [{ "@id": "http://[" }], + [`${NS}first`]: [{ "@id": "https://example.com/fallback" }], + }), + { contextLoader }, + )); + }, + ); +}); + +for (const policy of [undefined, "all", "canonical"] as const) { + test(`redundant properties write policy ${policy ?? "default"}`, async () => { + await withFixture( + { redundantPropertiesWrite: policy }, + async (Fixture, contextLoader) => { + for (const values of [[], ["a"], ["a", "b", "a"]]) { + const instance = new Fixture({ values }); + const compact = await instance.toJsonLd({ + format: "compact", + contextLoader, + }); + deepStrictEqual(await instance.toJsonLd({ contextLoader }), compact); + const expanded = await instance.toJsonLd({ + format: "expand", + contextLoader, + }) as unknown as Record[]; + for (const alias of ["first", "second"]) { + if (policy === "canonical" || values.length === 0) { + ok(!(alias in compact)); + ok(!(`${NS}${alias}` in expanded[0])); + } else { + deepStrictEqual(compact[alias], compact.value); + deepStrictEqual( + expanded[0][`${NS}${alias}`], + expanded[0][`${NS}value`], + ); + } + } + const custom = await instance.toJsonLd({ + format: "compact", + contextLoader, + context: { ...context, renamed: `${NS}value` }, + }); + deepStrictEqual( + (await Fixture.fromJsonLd(custom, { contextLoader })).values, + values, + ); + } + const aliasOnly = await Fixture.fromJsonLd( + document({ [`${NS}first`]: [literal("read")] }), + { contextLoader }, + ); + deepStrictEqual(aliasOnly.values, ["read"]); + }, + ); + }); +} + +for (const container of ["list", "graph"] as const) { + for (const policy of ["all", "canonical"] as const) { + test(`redundant ${container} containers with ${policy} writes`, async () => { + await withFixture({ + container, + redundantPropertiesWrite: policy, + range: container === "graph" ? [`${NS}Value`] : [`${XSD}string`], + }, async (Fixture, contextLoader) => { + const items = container === "list" + ? [literal("a"), literal("b"), literal("a")] + : [ + { "@type": [`${NS}Value`], [`${NS}label`]: [literal("a")] }, + { "@type": [`${NS}Value`], [`${NS}label`]: [literal("b")] }, + ]; + const set = container === "list" + ? [{ "@list": items }] + : items.map((item) => ({ "@graph": [item] })); + const parsed = await Fixture.fromJsonLd( + document({ + [`${NS}first`]: set, + [`${NS}second`]: [{ "@list": [literal("other")] }], + }), + { contextLoader }, + ); + deepStrictEqual( + container === "list" + ? parsed.values + : parsed.values.map((v) => (v as { label: string }).label), + container === "list" ? ["a", "b", "a"] : ["a", "b"], + ); + const instance = new Fixture({ values: parsed.values }); + const compact = await instance.toJsonLd({ + format: "compact", + contextLoader, + }); + if (container === "list" || policy === "all") { + deepStrictEqual(await instance.toJsonLd({ contextLoader }), compact); + } else { + // Parsed children retain expanded caches in the heuristic path. + const defaultOutput = await instance.toJsonLd({ contextLoader }); + const decoded = await Fixture.fromJsonLd(defaultOutput, { + contextLoader, + }); + deepStrictEqual( + decoded.values.map((v) => (v as { label: string }).label), + ["a", "b"], + ); + } + const expanded = await instance.toJsonLd({ + format: "expand", + contextLoader, + }) as unknown as Record[]; + for (const alias of ["first", "second"]) { + if (policy === "canonical") { + ok(!(`${NS}${alias}` in expanded[0])); + ok(!(alias in compact)); + } else { + deepStrictEqual( + expanded[0][`${NS}${alias}`], + expanded[0][`${NS}value`], + ); + deepStrictEqual(compact[alias], compact.value); + } + } + const roundtrip = await Fixture.fromJsonLd(compact, { contextLoader }); + deepStrictEqual( + container === "list" + ? roundtrip.values + : roundtrip.values.map((v) => (v as { label: string }).label), + container === "list" ? ["a", "b", "a"] : ["a", "b"], + ); + const empty = await Fixture.fromJsonLd( + document({ + [`${NS}value`]: [{ [`@${container}`]: [] }], + [`${NS}first`]: set, + }), + { contextLoader }, + ); + deepStrictEqual(empty.values, []); + }); + }); + } +} + +for (const functional of [false, true]) { + test(`URI-only aliases and portable read keys (functional=${functional})`, async () => { + for (const policy of ["all", "canonical"] as const) { + await withFixture({ + ...(functional ? { functional: true } : {}), + range: [`${XSD}anyURI`], + redundantPropertiesWrite: policy, + redundantProperties: [{ uri: `${NS}first` }], + }, async (Fixture, contextLoader, source) => { + ok(!source.includes("result[undefined]")); + match(source, /PORTABLE_IRI_KEYS[^\n]+https:\/\/example.com\/first/); + const url = new URL("https://example.com/object"); + const instance = new Fixture( + functional ? { value: url } : { values: [url] }, + ); + const compact = await instance.toJsonLd({ + format: "compact", + contextLoader, + }); + deepStrictEqual(await instance.toJsonLd({ contextLoader }), compact); + const expanded = await instance.toJsonLd({ + format: "expand", + contextLoader, + }) as unknown as Record[]; + if (policy === "all") { + deepStrictEqual(expanded[0][`${NS}first`], [{ "@id": url.href }]); + } else ok(!(`${NS}first` in expanded[0])); + const custom = await instance.toJsonLd({ + format: "compact", + contextLoader, + context: { + type: "@type", + Fixture: `${NS}Fixture`, + value: { "@id": `${NS}value`, "@type": "@id" }, + }, + }); + if (policy === "all") { + deepStrictEqual(custom[`${NS}first`], { "@id": url.href }); + } else { + ok(!(`${NS}first` in custom)); + } + const parsed = await Fixture.fromJsonLd( + document({ + [`${NS}first`]: [{ "@id": "ap://did:key:z6Mkabc/objects/1" }], + }), + { contextLoader }, + ); + const value = functional ? parsed.value : parsed.values[0]; + ok(value instanceof URL); + const cached = await parsed.toJsonLd({ contextLoader }); + deepStrictEqual(cached[`${NS}first`], [{ + "@id": "ap+ef61://did:key:z6Mkabc/objects/1", + }]); + const compactInput = await Fixture.fromJsonLd({ + "@context": [AS, CONTEXT], + type: "Fixture", + first: "ap://did:key:z6Mkabc/objects/1", + }, { contextLoader }); + const compactCache = await compactInput.toJsonLd({ contextLoader }); + deepStrictEqual( + compactCache.first, + "ap+ef61://did:key:z6Mkabc/objects/1", + ); + }); + } + }); +} diff --git a/packages/vocab-tools/src/codec.ts b/packages/vocab-tools/src/codec.ts index b4ff7a0a8..25db645b9 100644 --- a/packages/vocab-tools/src/codec.ts +++ b/packages/vocab-tools/src/codec.ts @@ -162,8 +162,13 @@ export async function* generateEncoder( ? compactItems : compactItems[0]; `; - if (property.functional && property.redundantProperties != null) { + if ( + property.redundantPropertiesWrite !== "canonical" && + property.redundantProperties != null && + !(isNonFunctionalProperty(property) && property.container != null) + ) { for (const prop of property.redundantProperties) { + if (prop.compactName == null) continue; yield ` result[${JSON.stringify(prop.compactName)}] = compactItems.length > 1 @@ -255,7 +260,10 @@ export async function* generateEncoder( ); values[${JSON.stringify(property.uri)}] = propValue; `; - if (property.functional && property.redundantProperties != null) { + if ( + property.redundantPropertiesWrite !== "canonical" && + property.redundantProperties != null + ) { for (const prop of property.redundantProperties) { yield ` values[${JSON.stringify(prop.uri)}] = propValue; @@ -348,6 +356,11 @@ export async function* generateEncoder( 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 ` @@ -544,7 +557,7 @@ export async function* generateDecoder( yield ` let ${arrayVariable} = ${propertyValues}; `; - if (property.functional && property.redundantProperties != null) { + if (property.redundantProperties != null) { for (const prop of property.redundantProperties) { yield ` if (${arrayVariable} == null || ${arrayVariable}.length < 1) { diff --git a/packages/vocab-tools/src/schema.test.ts b/packages/vocab-tools/src/schema.test.ts index 82c1bf6e8..8b91e3b07 100644 --- a/packages/vocab-tools/src/schema.test.ts +++ b/packages/vocab-tools/src/schema.test.ts @@ -1,8 +1,13 @@ -import { deepStrictEqual, ok, throws } from "node:assert"; +import { deepStrictEqual, ok, rejects, throws } from "node:assert"; import { test } from "node:test"; +import { mkdtemp, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { stringify } from "yaml"; import { hasSingularAccessor, isNonFunctionalProperty, + loadSchemaFiles, type PropertySchema, type TypeSchema, type TypeUri, @@ -244,3 +249,71 @@ test("validateTypeSchemas() rejects mixed fedify:vocabEntityType ranges", () => }, ); }); + +test("loadSchemaFiles validates redundant properties on both property kinds", async () => { + const dir = await mkdtemp(join(tmpdir(), "fedify-schema-")); + try { + for (const functional of [false, true]) { + const property = { + singularName: "value", + ...(functional ? { functional: true } : { pluralName: "values" }), + uri: "https://example.com/value", + description: "A value.", + range: ["http://www.w3.org/2001/XMLSchema#string"], + redundantProperties: [{ uri: "https://example.com/alias" }], + }; + const type = { + name: "Fixture", + uri: "https://example.com/Fixture", + entity: false, + description: "A fixture.", + defaultContext: {}, + properties: [property], + }; + for (const policy of [undefined, "all", "canonical"]) { + await writeFile( + join(dir, "fixture.yaml"), + stringify({ + ...type, + properties: [{ ...property, redundantPropertiesWrite: policy }], + }), + ); + const types = await loadSchemaFiles(dir); + deepStrictEqual( + types[type.uri].properties[0].redundantProperties, + property.redundantProperties, + ); + deepStrictEqual( + types[type.uri].properties[0].redundantPropertiesWrite, + policy, + ); + } + for ( + const invalid of [ + { redundantPropertiesWrite: "merge" }, + { redundantPropertiesWrite: 42 }, + { redundantProperties: "alias" }, + { redundantProperties: [{}] }, + { redundantProperties: [{ uri: 42 }] }, + { + redundantProperties: [{ + uri: "https://example.com/alias", + compactName: 42, + }], + }, + ] + ) { + await writeFile( + join(dir, "fixture.yaml"), + stringify({ + ...type, + properties: [{ ...property, ...invalid }], + }), + ); + await rejects(loadSchemaFiles(dir), AggregateError); + } + } + } finally { + await rm(dir, { recursive: true, force: true }); + } +}); diff --git a/packages/vocab-tools/src/schema.ts b/packages/vocab-tools/src/schema.ts index c7ad29c89..aad6214a8 100644 --- a/packages/vocab-tools/src/schema.ts +++ b/packages/vocab-tools/src/schema.ts @@ -85,6 +85,27 @@ export interface TypeSchema { } export interface PropertySchemaBase { + /** + * Synonyms read after the canonical property, in declaration order. + * Deserialization selects the first non-empty raw value set before decoding + * its values and containers; it never merges synonyms or retries invalid + * values. Serialization writes all synonyms by default. + */ + redundantProperties?: { + /** The qualified URI of the synonym. */ + uri: string; + + /** The synonym's key in the default compact JSON-LD context. */ + compactName?: string; + }[]; + + /** + * Whether serialization writes every synonym or only the canonical property. + * Defaults to `"all"`. This does not change deserialization precedence or + * the existing cache of parsed JSON-LD documents. + */ + redundantPropertiesWrite?: "all" | "canonical"; + /** * A context URL added to the default serialization context when this * property's expanded IRI appears in the compacted document. Explicit @@ -229,25 +250,6 @@ export type PropertySchema = * and `singularAccessor` should not be specified. */ functional: true; - - /** - * If it's present, those redundant properties are also filled with - * the same value altogether when the object is serialized into - * JSON-LD. When it's deserialized from JSON-LD, it tries to - * parse the values of the specified properties in order. - */ - redundantProperties?: { - /** - * The qualified URI of the property. - */ - uri: string; - - /** - * The property name used in the compacted JSON-LD document. It is used - * as the key of the property. - */ - compactName?: string; - }[]; }; /** diff --git a/packages/vocab-tools/src/schema.yaml b/packages/vocab-tools/src/schema.yaml index c093b041c..7c670f225 100644 --- a/packages/vocab-tools/src/schema.yaml +++ b/packages/vocab-tools/src/schema.yaml @@ -45,33 +45,41 @@ $defs: functional: description: *functional-description const: true - redundantProperties:: - description: >- - If it's present, those redundant properties are also filled with - the same value altogether when the object is serialized into - JSON-LD. When it's deserialized from JSON-LD, it tries to - parse the values of the specified properties in order. - type: array - items: - type: object - properties: - uri: - description: The qualified URI of the property. - type: string - format: uri - compactName: - description: >- - The property name used in the compacted JSON-LD document. - It is used as the key of the property. - type: string - required: - - uri required: - functional property-base: type: object properties: + redundantProperties: + description: >- + Synonyms read after the canonical property in declaration order. + The first non-empty raw value set is selected before decoding values + and containers. Synonyms are never merged or retried on invalid values. + Serialization writes all synonyms by default. + type: array + items: + type: object + properties: + uri: + description: The qualified URI of the property. + type: string + format: uri + compactName: + description: >- + The property name used in the compacted JSON-LD document. + It is used as the key of the property. + type: string + required: + - uri + redundantPropertiesWrite: + description: >- + Whether serialization writes all synonyms or only the canonical + property. Does not change deserialization precedence. + enum: + - all + - canonical + default: all extraContext: description: >- A context URL added to default serialization when this property's diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 92f03442a..6f3ffe8fe 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2193,6 +2193,15 @@ importers: specifier: ^2.9.0 version: 2.9.0 devDependencies: + '@fedify/vocab-runtime': + specifier: workspace:^ + version: link:../vocab-runtime + '@logtape/logtape': + specifier: 'catalog:' + version: 2.2.0 + '@opentelemetry/api': + specifier: 'catalog:' + version: 1.9.1 '@types/node': specifier: 'catalog:' version: 24.19.0 @@ -26069,7 +26078,7 @@ snapshots: - react-native-b4a - supports-color - '@stackbit/cms-contentful@0.4.56(debug@4.4.1)': + '@stackbit/cms-contentful@0.4.56(debug@4.4.1)(graphql@16.14.2)': dependencies: '@contentful/rich-text-types': 15.15.1 '@stackbit/cms-core': 2.0.1(@types/react@19.1.8)(debug@4.4.1)(graphql@16.14.2) @@ -26083,6 +26092,7 @@ snapshots: - bare-buffer - debug - encoding + - graphql - react-native-b4a - supports-color @@ -26158,47 +26168,57 @@ snapshots: - supports-color - utf-8-validate - '@stackbit/cms-git@0.4.18': + '@stackbit/cms-git@0.4.18(@types/react@19.1.8)(debug@4.4.1)(graphql@16.14.2)': dependencies: '@stackbit/artisanal-names': 1.0.1 - '@stackbit/cms-core': 2.0.1(@types/react@19.1.8)(graphql@16.14.2) + '@stackbit/cms-core': 2.0.1(@types/react@19.1.8)(debug@4.4.1)(graphql@16.14.2) '@stackbit/sdk': 2.0.1 '@stackbit/types': 1.0.1 '@stackbit/utils': 0.4.19 - axios: 0.24.0 + axios: 0.24.0(debug@4.4.1) fs-extra: 11.3.6 git-url-parse: 13.1.1 lodash: 4.18.1 micromatch: 4.0.8 slugify: 1.6.9 transitivePeerDependencies: + - '@types/react' - bare-abort-controller - bare-buffer + - bufferutil + - canvas - debug - encoding + - graphql - react-native-b4a - supports-color + - utf-8-validate - '@stackbit/cms-git@0.4.18(debug@4.4.1)': + '@stackbit/cms-git@0.4.18(@types/react@19.1.8)(graphql@16.14.2)': dependencies: '@stackbit/artisanal-names': 1.0.1 - '@stackbit/cms-core': 2.0.1(@types/react@19.1.8)(debug@4.4.1)(graphql@16.14.2) + '@stackbit/cms-core': 2.0.1(@types/react@19.1.8)(graphql@16.14.2) '@stackbit/sdk': 2.0.1 '@stackbit/types': 1.0.1 '@stackbit/utils': 0.4.19 - axios: 0.24.0(debug@4.4.1) + axios: 0.24.0 fs-extra: 11.3.6 git-url-parse: 13.1.1 lodash: 4.18.1 micromatch: 4.0.8 slugify: 1.6.9 transitivePeerDependencies: + - '@types/react' - bare-abort-controller - bare-buffer + - bufferutil + - canvas - debug - encoding + - graphql - react-native-b4a - supports-color + - utf-8-validate '@stackbit/cms-sanity@0.2.56(@types/react@19.1.8)': dependencies: @@ -26227,7 +26247,7 @@ snapshots: - supports-color - utf-8-validate - '@stackbit/cms-sanity@0.2.56(@types/react@19.1.8)(debug@4.4.1)': + '@stackbit/cms-sanity@0.2.56(@types/react@19.1.8)(debug@4.4.1)(graphql@16.14.2)': dependencies: '@sanity/block-tools': 2.36.6 '@sanity/client': 3.4.1 @@ -26250,6 +26270,7 @@ snapshots: - canvas - debug - encoding + - graphql - react-native-b4a - supports-color - utf-8-validate @@ -26260,7 +26281,7 @@ snapshots: '@stackbit/artisanal-names': 1.0.1 '@stackbit/cms-contentful': 0.4.56 '@stackbit/cms-core': 2.0.1(@types/react@19.1.8)(graphql@16.14.2) - '@stackbit/cms-git': 0.4.18 + '@stackbit/cms-git': 0.4.18(@types/react@19.1.8)(graphql@16.14.2) '@stackbit/cms-sanity': 0.2.56(@types/react@19.1.8) '@stackbit/sdk': 2.0.1 '@stackbit/types': 1.0.1 @@ -26294,14 +26315,14 @@ snapshots: - supports-color - utf-8-validate - '@stackbit/dev-common@0.5.51(@types/react@19.1.8)(debug@4.4.1)': + '@stackbit/dev-common@0.5.51(@types/react@19.1.8)(debug@4.4.1)(graphql@16.14.2)': dependencies: '@iarna/toml': 2.2.5 '@stackbit/artisanal-names': 1.0.1 - '@stackbit/cms-contentful': 0.4.56(debug@4.4.1) + '@stackbit/cms-contentful': 0.4.56(debug@4.4.1)(graphql@16.14.2) '@stackbit/cms-core': 2.0.1(@types/react@19.1.8)(debug@4.4.1)(graphql@16.14.2) - '@stackbit/cms-git': 0.4.18(debug@4.4.1) - '@stackbit/cms-sanity': 0.2.56(@types/react@19.1.8)(debug@4.4.1) + '@stackbit/cms-git': 0.4.18(@types/react@19.1.8)(debug@4.4.1)(graphql@16.14.2) + '@stackbit/cms-sanity': 0.2.56(@types/react@19.1.8)(debug@4.4.1)(graphql@16.14.2) '@stackbit/sdk': 2.0.1 '@stackbit/types': 1.0.1 '@stackbit/utils': 0.4.19 @@ -26330,6 +26351,7 @@ snapshots: - canvas - debug - encoding + - graphql - react-native-b4a - supports-color - utf-8-validate @@ -26337,8 +26359,8 @@ snapshots: '@stackbit/dev@1.0.35(@types/react@19.1.8)(debug@4.4.1)(graphql@16.14.2)': dependencies: '@stackbit/cms-core': 2.0.1(@types/react@19.1.8)(debug@4.4.1)(graphql@16.14.2) - '@stackbit/cms-git': 0.4.18(debug@4.4.1) - '@stackbit/dev-common': 0.5.51(@types/react@19.1.8)(debug@4.4.1) + '@stackbit/cms-git': 0.4.18(@types/react@19.1.8)(debug@4.4.1)(graphql@16.14.2) + '@stackbit/dev-common': 0.5.51(@types/react@19.1.8)(debug@4.4.1)(graphql@16.14.2) '@stackbit/sdk': 2.0.1 axios: 0.25.0(debug@4.4.1) chalk: 4.1.2 @@ -26372,7 +26394,7 @@ snapshots: '@stackbit/dev@1.0.35(@types/react@19.1.8)(graphql@16.14.2)': dependencies: '@stackbit/cms-core': 2.0.1(@types/react@19.1.8)(graphql@16.14.2) - '@stackbit/cms-git': 0.4.18 + '@stackbit/cms-git': 0.4.18(@types/react@19.1.8)(graphql@16.14.2) '@stackbit/dev-common': 0.5.51(@types/react@19.1.8) '@stackbit/sdk': 2.0.1 axios: 0.25.0