From 97f7ca027ea55a5ee9a80582671743f33237c571 Mon Sep 17 00:00:00 2001 From: Joachim Van Herwegen Date: Tue, 23 Aug 2022 12:45:09 +0200 Subject: [PATCH] feat: Allow vocabularies to be extended --- src/util/Vocabularies.ts | 36 +++++++++++++++-------- test/unit/util/Vocabularies.test.ts | 44 +++++++++++++++++++++++------ 2 files changed, 60 insertions(+), 20 deletions(-) diff --git a/src/util/Vocabularies.ts b/src/util/Vocabularies.ts index efc6a26ec..93363a659 100644 --- a/src/util/Vocabularies.ts +++ b/src/util/Vocabularies.ts @@ -47,8 +47,7 @@ export type VocabularyValue = T extends Vocabulary ? T[TKey] export type VocabularyTerm = T extends Vocabulary ? T['terms'][TKey] : never; /** - * Creates a function that expands local names from the given base URI, - * and exports the given local names as properties on the returned object. + * Creates a {@link ValueVocabulary} with the given `baseUri` as namespace and all `localNames` as entries. */ function createValueVocabulary(baseUri: TBase, localNames: TLocal[]): ValueVocabulary { @@ -64,31 +63,44 @@ ValueVocabulary { } /** - * Creates a function that expands local names from the given base URI into named nodes, - * and exports the given local names as properties on the returned object. + * Creates a {@link TermVocabulary} based on the provided {@link ValueVocabulary}. */ -function createTermVocabulary(namespace: ValueVocabulary): +function createTermVocabulary(values: ValueVocabulary): TermVocabulary> { // Need to cast since `fromEntries` typings aren't strict enough return Object.fromEntries( - Object.entries(namespace).map(([ key, value ]): [string, NamedNode] => [ key, DataFactory.namedNode(value) ]), + Object.entries(values).map(([ key, value ]): [string, NamedNode] => [ key, DataFactory.namedNode(value) ]), ) as TermVocabulary>; } /** - * Creates a function that expands local names from the given base URI into string, - * and exports the given local names as properties on the returned object. - * Under the `terms` property, it exposes the expanded local names as named nodes. + * Creates a {@link Vocabulary} with the given `baseUri` as namespace and all `localNames` as entries. + * The values are the local names expanded from the given base URI as strings. + * The `terms` field contains all the same values but as {@link NamedNode} instead. */ export function createVocabulary(baseUri: TBase, ...localNames: TLocal[]): string extends TLocal ? PartialVocabulary : Vocabulary { - const namespace = createValueVocabulary(baseUri, localNames); + const values = createValueVocabulary(baseUri, localNames); return { - ...namespace, - terms: createTermVocabulary(namespace), + ...values, + terms: createTermVocabulary(values), }; } +/** + * Creates a new {@link Vocabulary} that extends an existing one by adding new local names. + * @param vocabulary - The {@link Vocabulary} to extend. + * @param newNames - The new local names that need to be added. + */ +export function extendVocabulary( + vocabulary: Vocabulary, ...newNames: TNew[]): + ReturnType> { + const localNames = Object.keys(vocabulary) + .filter((key): boolean => key !== 'terms' && key !== 'namespace') as TLocal[]; + const allNames = [ ...localNames, ...newNames ]; + return createVocabulary(vocabulary.namespace, ...allNames); +} + export const ACL = createVocabulary('http://www.w3.org/ns/auth/acl#', 'accessTo', 'agent', diff --git a/test/unit/util/Vocabularies.test.ts b/test/unit/util/Vocabularies.test.ts index 28003a679..4accb4ec7 100644 --- a/test/unit/util/Vocabularies.test.ts +++ b/test/unit/util/Vocabularies.test.ts @@ -1,22 +1,50 @@ import { DataFactory } from 'n3'; -import { LDP } from '../../../src/util/Vocabularies'; +import { createVocabulary, extendVocabulary } from '../../../src/util/Vocabularies'; describe('Vocabularies', (): void => { - describe('LDP', (): void => { + const vocabulary = createVocabulary('http://www.w3.org/ns/ldp#', 'contains', 'Container'); + + describe('createVocabulary', (): void => { it('contains its own URI.', (): void => { - expect(LDP.namespace).toBe('http://www.w3.org/ns/ldp#'); + expect(vocabulary.namespace).toBe('http://www.w3.org/ns/ldp#'); }); it('contains its own URI as a term.', (): void => { - expect(LDP.terms.namespace).toEqual(DataFactory.namedNode('http://www.w3.org/ns/ldp#')); + expect(vocabulary.terms.namespace).toEqual(DataFactory.namedNode('http://www.w3.org/ns/ldp#')); }); - it('exposes ldp:contains.', (): void => { - expect(LDP.contains).toBe('http://www.w3.org/ns/ldp#contains'); + it('exposes the defined URIs.', (): void => { + expect(vocabulary.contains).toBe('http://www.w3.org/ns/ldp#contains'); + expect(vocabulary.Container).toBe('http://www.w3.org/ns/ldp#Container'); }); - it('exposes ldp:contains as a term.', (): void => { - expect(LDP.terms.contains).toEqual(DataFactory.namedNode('http://www.w3.org/ns/ldp#contains')); + it('exposes the defined URIs as terms.', (): void => { + expect(vocabulary.terms.contains).toEqual(DataFactory.namedNode('http://www.w3.org/ns/ldp#contains')); + expect(vocabulary.terms.Container).toEqual(DataFactory.namedNode('http://www.w3.org/ns/ldp#Container')); + }); + }); + + describe('extendVocabulary', (): void => { + const extended = extendVocabulary(vocabulary, 'extended', 'extra'); + + it('still contains all the original values.', async(): Promise => { + expect(extended.namespace).toBe('http://www.w3.org/ns/ldp#'); + expect(extended.terms.namespace).toEqual(DataFactory.namedNode('http://www.w3.org/ns/ldp#')); + expect(extended.contains).toBe('http://www.w3.org/ns/ldp#contains'); + expect(extended.Container).toBe('http://www.w3.org/ns/ldp#Container'); + expect(extended.terms.contains).toEqual(DataFactory.namedNode('http://www.w3.org/ns/ldp#contains')); + expect(extended.terms.Container).toEqual(DataFactory.namedNode('http://www.w3.org/ns/ldp#Container')); + }); + + it('contains the new values.', async(): Promise => { + expect(extended.extended).toBe('http://www.w3.org/ns/ldp#extended'); + expect(extended.extra).toBe('http://www.w3.org/ns/ldp#extra'); + expect(extended.terms.extended).toEqual(DataFactory.namedNode('http://www.w3.org/ns/ldp#extended')); + expect(extended.terms.extra).toEqual(DataFactory.namedNode('http://www.w3.org/ns/ldp#extra')); + }); + + it('does not modify the original vocabulary.', async(): Promise => { + expect((vocabulary as any).extended).toBeUndefined(); }); }); });