feat: Support writer prefixes.

Closes https://github.com/solid/community-server/issues/470
This commit is contained in:
Ruben Verborgh 2021-01-05 23:40:05 +01:00
parent 3b63786ae0
commit 87752ddf20
10 changed files with 108 additions and 24 deletions

View File

@ -59,6 +59,8 @@ module.exports = {
'new-cap': 'off',
// Necessary in constructor overloading
'no-param-reassign': 'off',
// Checked by @typescript-eslint/no-redeclare
'no-redeclare': 'off',
// Conflicts with external libraries
'no-underscore-dangle': 'off',
// Already checked by @typescript-eslint/no-unused-vars

12
package-lock.json generated
View File

@ -6803,9 +6803,9 @@
"dev": true
},
"n3": {
"version": "1.6.4",
"resolved": "https://registry.npmjs.org/n3/-/n3-1.6.4.tgz",
"integrity": "sha512-qiiBhW2nJ59cfQzi0nvZs5tSXkXgDXedIy3zNNuKjTwE8Bcvv95DTFJpOY9geg6of5T7z6cg+ZWcaHIij3svrA==",
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/n3/-/n3-1.7.0.tgz",
"integrity": "sha512-8R0Qj545WnVLQxOfxxyFKzOpO13hF3jhSMJfO0FNqvbsPZDiR9ZDmGGjXAlcoZDf/88OsCYd7rHML284vm1h6A==",
"requires": {
"queue-microtask": "^1.1.2",
"readable-stream": "^3.6.0"
@ -7629,9 +7629,9 @@
"integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA=="
},
"queue-microtask": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.0.tgz",
"integrity": "sha512-J95OVUiS4b8qqmpqhCodN8yPpHG2mpZUPQ8tDGyIY0VhM+kBHszOuvsMJVGNQ1OH2BnTFbqz45i+2jGpDw9H0w=="
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.2.tgz",
"integrity": "sha512-dB15eXv3p2jDlbOiNLyMabYg1/sXvppd8DP2J3EOCQ0AkuSXCW2tP7mnVouVLJKgUMY6yP0kcQDVpLCN13h4Xg=="
},
"quick-lru": {
"version": "4.0.1",

View File

@ -94,7 +94,7 @@
"fetch-sparql-endpoint": "^1.8.0",
"handlebars": "^4.7.6",
"mime-types": "^2.1.27",
"n3": "^1.6.4",
"n3": "^1.7.0",
"rdf-parse": "^1.5.0",
"rdf-serialize": "^1.0.0",
"rdf-terms": "^1.5.1",

View File

@ -1,7 +1,7 @@
import { DataFactory, Store } from 'n3';
import type { BlankNode, Literal, NamedNode, Quad, Term } from 'rdf-js';
import { getLoggerFor } from '../../logging/LogUtil';
import { toObjectTerm, toCachedNamedNode, isTerm } from '../../util/TermUtil';
import { toSubjectTerm, toObjectTerm, toCachedNamedNode, isTerm } from '../../util/TermUtil';
import { CONTENT_TYPE_TERM } from '../../util/Vocabularies';
import type { ResourceIdentifier } from './ResourceIdentifier';
import { isResourceIdentifier } from './ResourceIdentifier';
@ -133,6 +133,18 @@ export class RepresentationMetadata {
return this;
}
/**
* @param quads - Quad to add to the metadata.
*/
public addQuad(
subject: NamedNode | BlankNode | string,
predicate: NamedNode | string,
object: NamedNode | BlankNode | Literal | string,
): this {
this.store.addQuad(toSubjectTerm(subject), toCachedNamedNode(predicate), toObjectTerm(object, true));
return this;
}
/**
* @param quads - Quads to add to the metadata.
*/
@ -141,6 +153,18 @@ export class RepresentationMetadata {
return this;
}
/**
* @param quads - Quad to remove from the metadata.
*/
public removeQuad(
subject: NamedNode | BlankNode | string,
predicate: NamedNode | string,
object: NamedNode | BlankNode | Literal | string,
): this {
this.store.removeQuad(toSubjectTerm(subject), toCachedNamedNode(predicate), toObjectTerm(object, true));
return this;
}
/**
* @param quads - Quads to remove from the metadata.
*/
@ -155,8 +179,7 @@ export class RepresentationMetadata {
* @param object - Value to add.
*/
public add(predicate: NamedNode | string, object: NamedNode | Literal | string): this {
this.store.addQuad(this.id, toCachedNamedNode(predicate), toObjectTerm(object, true));
return this;
return this.addQuad(this.id, predicate, object);
}
/**
@ -165,8 +188,7 @@ export class RepresentationMetadata {
* @param object - Value to remove.
*/
public remove(predicate: NamedNode | string, object: NamedNode | Literal | string): this {
this.store.removeQuad(this.id, toCachedNamedNode(predicate), toObjectTerm(object, true));
return this;
return this.removeQuad(this.id, predicate, object);
}
/**

View File

@ -1,4 +1,5 @@
import type { Readable } from 'stream';
import { StreamWriter } from 'n3';
import rdfSerializer from 'rdf-serialize';
import type { Representation } from '../../ldp/representation/Representation';
import { RepresentationMetadata } from '../../ldp/representation/RepresentationMetadata';
@ -8,7 +9,8 @@ import type {
} from '../../ldp/representation/RepresentationPreferences';
import { INTERNAL_QUADS } from '../../util/ContentTypes';
import { guardStream } from '../../util/GuardedStream';
import { CONTENT_TYPE } from '../../util/Vocabularies';
import { pipeSafely } from '../../util/StreamUtil';
import { CONTENT_TYPE, PREFERRED_PREFIX_TERM } from '../../util/Vocabularies';
import { matchingMediaTypes } from './ConversionUtil';
import type { RepresentationConverterArgs } from './RepresentationConverter';
import { TypedRepresentationConverter } from './TypedRepresentationConverter';
@ -41,9 +43,20 @@ export class QuadToRdfConverter extends TypedRepresentationConverter {
private async quadsToRdf(quads: Representation, { type }: RepresentationPreferences): Promise<Representation> {
const contentType = matchingMediaTypes(type, await this.getOutputTypes())[0];
const metadata = new RepresentationMetadata(quads.metadata, { [CONTENT_TYPE]: contentType });
let data: Readable;
// Use prefixes if possible (see https://github.com/rubensworks/rdf-serialize.js/issues/1)
if (/(?:turtle|trig)$/u.test(contentType)) {
const prefixes = Object.fromEntries(metadata.quads(null, PREFERRED_PREFIX_TERM, null)
.map(({ subject, object }): [string, string] => [ object.value, subject.value ]));
data = pipeSafely(quads.data, new StreamWriter({ format: contentType, prefixes }));
// Otherwise, write without prefixes
} else {
data = rdfSerializer.serialize(quads.data, { contentType }) as Readable;
}
return {
binary: true,
data: guardStream(rdfSerializer.serialize(quads.data, { contentType }) as Readable),
data: guardStream(data),
metadata,
};
}

View File

@ -1,5 +1,5 @@
import { DataFactory } from 'n3';
import type { Literal, NamedNode, Term } from 'rdf-js';
import type { NamedNode, Literal, Term } from 'rdf-js';
import { CONTENT_TYPE_TERM } from './Vocabularies';
const { namedNode, literal } = DataFactory;
@ -41,7 +41,10 @@ export function isTerm(input?: any): input is Term {
* Converts a subject to a named node when needed.
* @param subject - Subject to potentially transform.
*/
export function toSubjectTerm(subject: NamedNode | string): NamedNode {
export function toSubjectTerm(subject: string): NamedNode;
export function toSubjectTerm<T extends Term>(subject: T): T;
export function toSubjectTerm<T extends Term>(subject: T | string): T | NamedNode;
export function toSubjectTerm(subject: Term | string): Term {
return typeof subject === 'string' ? namedNode(subject) : subject;
}
@ -52,7 +55,10 @@ export const toPredicateTerm = toSubjectTerm;
* @param object - Object to potentially transform.
* @param preferLiteral - Whether strings are converted to literals or named nodes.
*/
export function toObjectTerm<T extends Term>(object: T | string, preferLiteral = false): T {
export function toObjectTerm(object: string, preferLiteral?: boolean): NamedNode;
export function toObjectTerm<T extends Term>(object: T, preferLiteral?: boolean): T;
export function toObjectTerm<T extends Term>(object: T | string, preferLiteral?: boolean): T | NamedNode;
export function toObjectTerm(object: Term | string, preferLiteral = false): Term {
if (typeof object === 'string') {
return (preferLiteral ? literal(object) : namedNode(object)) as any;
}

View File

@ -2,7 +2,7 @@
import { namedNode } from '@rdfjs/data-model';
import type { NamedNode } from 'rdf-js';
type PrefixResolver<T> = (localName: string) => T;
type PrefixResolver<T> = (localName?: string) => T;
type RecordOf<TKey extends any[], TValue> = Record<TKey[number], TValue>;
export type Namespace<TKey extends any[], TValue> =
@ -19,7 +19,7 @@ export function createNamespace<TKey extends string, TValue>(
Namespace<typeof localNames, TValue> {
// Create a function that expands local names
const expanded = {} as Record<string, TValue>;
const namespace = ((localName: string): TValue => {
const namespace = ((localName = ''): TValue => {
if (!(localName in expanded)) {
expanded[localName] = toValue(`${baseUri}${localName}`);
}
@ -114,11 +114,17 @@ export const RDF = createUriAndTermNamespace('http://www.w3.org/1999/02/22-rdf-s
'type',
);
export const VANN = createUriAndTermNamespace('http://purl.org/vocab/vann/',
'preferredNamespacePrefix',
);
export const XSD = createUriAndTermNamespace('http://www.w3.org/2001/XMLSchema#',
'dateTime',
'integer',
);
// Alias for most commonly used URI
// Alias for commonly used types
export const CONTENT_TYPE = MA.format;
export const CONTENT_TYPE_TERM = MA.terms.format;
export const PREFERRED_PREFIX = VANN.preferredNamespacePrefix;
export const PREFERRED_PREFIX_TERM = VANN.terms.preferredNamespacePrefix;

View File

@ -107,12 +107,12 @@ describe('An integrated AuthenticatedLdpHandler', (): void => {
[],
);
expect(response.statusCode).toBe(200);
expect(response._getBuffer().toString()).toContain(
expect(response._getData()).toContain(
'<http://test.com/s2> <http://test.com/p2> <http://test.com/o2>.',
);
expect(response.getHeaders().link).toBe(`<${LDP.Resource}>; rel="type"`);
const parser = new Parser();
const triples = parser.parse(response._getBuffer().toString());
const triples = parser.parse(response._getData());
expect(triples).toBeRdfIsomorphic([
quad(
namedNode('http://test.com/s2'),

View File

@ -8,12 +8,16 @@ import type { RepresentationPreferences } from '../../../../src/ldp/representati
import type { ResourceIdentifier } from '../../../../src/ldp/representation/ResourceIdentifier';
import { QuadToRdfConverter } from '../../../../src/storage/conversion/QuadToRdfConverter';
import { INTERNAL_QUADS } from '../../../../src/util/ContentTypes';
import { CONTENT_TYPE } from '../../../../src/util/Vocabularies';
import { CONTENT_TYPE, DC, PREFERRED_PREFIX_TERM } from '../../../../src/util/Vocabularies';
describe('A QuadToRdfConverter', (): void => {
const converter = new QuadToRdfConverter();
const identifier: ResourceIdentifier = { path: 'path' };
const metadata = new RepresentationMetadata({ [CONTENT_TYPE]: INTERNAL_QUADS });
let metadata: RepresentationMetadata;
beforeEach((): void => {
metadata = new RepresentationMetadata({ [CONTENT_TYPE]: INTERNAL_QUADS });
});
it('supports parsing quads.', async(): Promise<void> => {
await expect(new QuadToRdfConverter().getInputTypes())
@ -71,6 +75,33 @@ describe('A QuadToRdfConverter', (): void => {
);
});
it('converts quads with prefixes to turtle.', async(): Promise<void> => {
metadata.addQuad(DC.terms(), PREFERRED_PREFIX_TERM, 'dc');
metadata.addQuad('http://test.com/', PREFERRED_PREFIX_TERM, 'test');
const representation = {
data: streamifyArray([ triple(
namedNode('http://test.com/s'),
DC.terms.modified,
namedNode('http://test.com/o'),
) ]),
metadata,
} as Representation;
const preferences: RepresentationPreferences = { type: { 'text/turtle': 1 }};
const result = await converter.handle({ identifier, representation, preferences });
expect(result).toMatchObject({
binary: true,
metadata: expect.any(RepresentationMetadata),
});
expect(result.metadata.contentType).toEqual('text/turtle');
await expect(stringifyStream(result.data)).resolves.toEqual(
`@prefix dc: <http://purl.org/dc/terms/>.
@prefix test: <http://test.com/>.
test:s dc:modified test:o.
`,
);
});
it('converts quads to JSON-LD.', async(): Promise<void> => {
metadata.contentType = INTERNAL_QUADS;
const representation = {

View File

@ -3,6 +3,10 @@ import { LDP } from '../../../src/util/Vocabularies';
describe('Vocabularies', (): void => {
describe('LDP', (): void => {
it('can return its own URI.', (): void => {
expect(LDP()).toBe('http://www.w3.org/ns/ldp#');
});
it('can create new properties.', (): void => {
expect(LDP('new')).toBe('http://www.w3.org/ns/ldp#new');
});