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

@@ -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;