feat: Allow vocabularies to be extended

This commit is contained in:
Joachim Van Herwegen 2022-08-23 12:45:09 +02:00
parent 2e1bae90c7
commit 97f7ca027e
2 changed files with 60 additions and 20 deletions

View File

@ -47,8 +47,7 @@ export type VocabularyValue<T> = T extends Vocabulary<any, infer TKey> ? T[TKey]
export type VocabularyTerm<T> = T extends Vocabulary<any, infer TKey> ? 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<TBase extends string, TLocal extends string>(baseUri: TBase, localNames: TLocal[]):
ValueVocabulary<TBase, TLocal> {
@ -64,31 +63,44 @@ ValueVocabulary<TBase, TLocal> {
}
/**
* 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<TBase extends string, TLocal extends string>(namespace: ValueVocabulary<TBase, TLocal>):
function createTermVocabulary<TBase extends string, TLocal extends string>(values: ValueVocabulary<TBase, TLocal>):
TermVocabulary<ValueVocabulary<TBase, TLocal>> {
// 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<ValueVocabulary<TBase, TLocal>>;
}
/**
* 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<TBase extends string, TLocal extends string>(baseUri: TBase,
...localNames: TLocal[]): string extends TLocal ? PartialVocabulary<TBase> : Vocabulary<TBase, TLocal> {
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<TBase extends string, TLocal extends string, TNew extends string>(
vocabulary: Vocabulary<TBase, TLocal>, ...newNames: TNew[]):
ReturnType<typeof createVocabulary<TBase, TLocal | TNew>> {
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',

View File

@ -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<void> => {
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<void> => {
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<void> => {
expect((vocabulary as any).extended).toBeUndefined();
});
});
});