Skip to content

Commit

Permalink
feat: Integrate data conversion with rest of server
Browse files Browse the repository at this point in the history
  • Loading branch information
joachimvh committed Aug 17, 2020
1 parent 5e1bb10 commit 4403421
Show file tree
Hide file tree
Showing 8 changed files with 102 additions and 226 deletions.
21 changes: 13 additions & 8 deletions bin/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,13 @@ import yargs from 'yargs';
import {
AcceptPreferenceParser,
AuthenticatedLdpHandler,
BodyParser,
CompositeAsyncHandler,
ExpressHttpServer,
HttpRequest,
Operation,
PatchingStore,
QuadToTurtleConverter,
Representation,
ResponseDescription,
RepresentationConvertingStore,
SimpleAuthorizer,
SimpleBodyParser,
SimpleCredentialsExtractor,
Expand All @@ -25,6 +24,7 @@ import {
SimpleSparqlUpdatePatchHandler,
SimpleTargetExtractor,
SingleThreadedResourceLocker,
TurtleToQuadConverter,
} from '..';

const { argv } = yargs
Expand All @@ -37,9 +37,9 @@ const { argv } = yargs
const { port } = argv;

// This is instead of the dependency injection that still needs to be added
const bodyParser: BodyParser = new CompositeAsyncHandler<HttpRequest, Representation | undefined>([
new SimpleBodyParser(),
const bodyParser = new CompositeAsyncHandler<HttpRequest, Representation | undefined>([
new SimpleSparqlUpdateBodyParser(),
new SimpleBodyParser(),
]);
const requestParser = new SimpleRequestParser({
targetExtractor: new SimpleTargetExtractor(),
Expand All @@ -53,11 +53,16 @@ const authorizer = new SimpleAuthorizer();

// Will have to see how to best handle this
const store = new SimpleResourceStore(`http://localhost:${port}/`);
const converter = new CompositeAsyncHandler([
new TurtleToQuadConverter(),
new QuadToTurtleConverter(),
]);
const convertingStore = new RepresentationConvertingStore(store, converter);
const locker = new SingleThreadedResourceLocker();
const patcher = new SimpleSparqlUpdatePatchHandler(store, locker);
const patchingStore = new PatchingStore(store, patcher);
const patcher = new SimpleSparqlUpdatePatchHandler(convertingStore, locker);
const patchingStore = new PatchingStore(convertingStore, patcher);

const operationHandler = new CompositeAsyncHandler<Operation, ResponseDescription>([
const operationHandler = new CompositeAsyncHandler([
new SimpleDeleteOperationHandler(patchingStore),
new SimpleGetOperationHandler(patchingStore),
new SimplePatchOperationHandler(patchingStore),
Expand Down
42 changes: 10 additions & 32 deletions src/ldp/http/SimpleBodyParser.ts
Original file line number Diff line number Diff line change
@@ -1,37 +1,22 @@
import { BinaryRepresentation } from '../representation/BinaryRepresentation';
import { BodyParser } from './BodyParser';
import { DATA_TYPE_QUAD } from '../../util/ContentTypes';
import { DATA_TYPE_BINARY } from '../../util/ContentTypes';
import { HttpRequest } from '../../server/HttpRequest';
import { PassThrough } from 'stream';
import { QuadRepresentation } from '../representation/QuadRepresentation';
import { RepresentationMetadata } from '../representation/RepresentationMetadata';
import { StreamParser } from 'n3';
import { UnsupportedHttpError } from '../../util/errors/UnsupportedHttpError';
import { UnsupportedMediaTypeHttpError } from '../../util/errors/UnsupportedMediaTypeHttpError';

/**
* Parses the incoming {@link HttpRequest} if there is no body or if it contains turtle (or similar) RDF data.
* Naively parses the content-type header to determine the body type.
* Converts incoming {@link HttpRequest} to a Representation without any further parsing.
* Naively parses the mediatype from the content-type header.
* Metadata is not generated (yet).
*/
export class SimpleBodyParser extends BodyParser {
private static readonly contentTypes = [
'application/n-quads',
'application/trig',
'application/n-triples',
'text/turtle',
'text/n3',
];

public async canHandle(input: HttpRequest): Promise<void> {
const contentType = input.headers['content-type'];

if (contentType && !SimpleBodyParser.contentTypes.some((type): boolean => contentType.includes(type))) {
throw new UnsupportedMediaTypeHttpError('This parser only supports RDF data.');
}
public async canHandle(): Promise<void> {
// Default BodyParser supports all content-types
}

// Note that the only reason this is a union is in case the body is empty.
// If this check gets moved away from the BodyParsers this union could be removed
public async handle(input: HttpRequest): Promise<QuadRepresentation | undefined> {
public async handle(input: HttpRequest): Promise<BinaryRepresentation | undefined> {
const contentType = input.headers['content-type'];

if (!contentType) {
Expand All @@ -46,16 +31,9 @@ export class SimpleBodyParser extends BodyParser {
contentType: mediaType,
};

// Catch parsing errors and emit correct error
// Node 10 requires both writableObjectMode and readableObjectMode
const errorStream = new PassThrough({ writableObjectMode: true, readableObjectMode: true });
const data = input.pipe(new StreamParser());
data.pipe(errorStream);
data.on('error', (error): boolean => errorStream.emit('error', new UnsupportedHttpError(error.message)));

return {
dataType: DATA_TYPE_QUAD,
data: errorStream,
dataType: DATA_TYPE_BINARY,
data: input,
metadata,
};
}
Expand Down
104 changes: 37 additions & 67 deletions src/storage/SimpleResourceStore.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,18 @@
import arrayifyStream from 'arrayify-stream';
import { BinaryRepresentation } from '../ldp/representation/BinaryRepresentation';
import { DATA_TYPE_BINARY } from '../util/ContentTypes';
import { ensureTrailingSlash } from '../util/Util';
import { NotFoundHttpError } from '../util/errors/NotFoundHttpError';
import { Quad } from 'rdf-js';
import { QuadRepresentation } from '../ldp/representation/QuadRepresentation';
import { Representation } from '../ldp/representation/Representation';
import { RepresentationPreferences } from '../ldp/representation/RepresentationPreferences';
import { ResourceIdentifier } from '../ldp/representation/ResourceIdentifier';
import { ResourceStore } from './ResourceStore';
import streamifyArray from 'streamify-array';
import { StreamWriter } from 'n3';
import { UnsupportedMediaTypeHttpError } from '../util/errors/UnsupportedMediaTypeHttpError';
import { CONTENT_TYPE_QUADS, DATA_TYPE_BINARY, DATA_TYPE_QUAD } from '../util/ContentTypes';

/**
* Resource store storing its data as Quads in an in-memory map.
* Resource store storing its data in an in-memory map.
* All requests will throw an {@link NotFoundHttpError} if unknown identifiers get passed.
*/
export class SimpleResourceStore implements ResourceStore {
private readonly store: { [id: string]: Quad[] } = { '': []};
private readonly store: { [id: string]: Representation };
private readonly base: string;
private index = 0;

Expand All @@ -27,21 +21,30 @@ export class SimpleResourceStore implements ResourceStore {
*/
public constructor(base: string) {
this.base = base;

this.store = {
// Default root entry (what you get when the identifier is equal to the base)
'': {
dataType: DATA_TYPE_BINARY,
data: streamifyArray([]),
metadata: { raw: [], profiles: []},
},
};
}

/**
* Stores the incoming data under a new URL corresponding to `container.path + number`.
* Slash added when needed.
* @param container - The identifier to store the new data under.
* @param representation - Data to store. Only Quad streams are supported.
* @param representation - Data to store.
*
* @returns The newly generated identifier.
*/
public async addResource(container: ResourceIdentifier, representation: Representation): Promise<ResourceIdentifier> {
const containerPath = this.parseIdentifier(container);
const newPath = `${ensureTrailingSlash(containerPath)}${this.index}`;
this.index += 1;
this.store[newPath] = await this.parseRepresentation(representation);
this.store[newPath] = await this.copyRepresentation(representation);
return { path: `${this.base}${newPath}` };
}

Expand All @@ -57,18 +60,15 @@ export class SimpleResourceStore implements ResourceStore {

/**
* Returns the stored representation for the given identifier.
* The only preference that is supported is `type === 'text/turtle'`.
* In all other cases a stream of Quads will be returned.
* Preferences will be ignored, data will be returned as it was received.
*
* @param identifier - Identifier to retrieve.
* @param preferences - Preferences for resulting Representation.
*
* @returns The corresponding Representation.
*/
public async getRepresentation(identifier: ResourceIdentifier,
preferences: RepresentationPreferences): Promise<Representation> {
public async getRepresentation(identifier: ResourceIdentifier): Promise<Representation> {
const path = this.parseIdentifier(identifier);
return this.generateRepresentation(this.store[path], preferences);
return this.generateRepresentation(path);
}

/**
Expand All @@ -85,7 +85,7 @@ export class SimpleResourceStore implements ResourceStore {
*/
public async setRepresentation(identifier: ResourceIdentifier, representation: Representation): Promise<void> {
const path = this.parseIdentifier(identifier);
this.store[path] = await this.parseRepresentation(representation);
this.store[path] = await this.copyRepresentation(representation);
}

/**
Expand All @@ -106,66 +106,36 @@ export class SimpleResourceStore implements ResourceStore {
}

/**
* Converts the Representation to an array of Quads.
* @param representation - Incoming Representation.
* @throws {@link UnsupportedMediaTypeHttpError}
* If the representation is not a Quad stream.
* Copies the Representation by draining the original data stream and creating a new one.
*
* @returns Promise of array of Quads pulled from the stream.
* @param data - Incoming Representation.
*/
private async parseRepresentation(representation: Representation): Promise<Quad[]> {
if (representation.dataType !== DATA_TYPE_QUAD) {
throw new UnsupportedMediaTypeHttpError('SimpleResourceStore only supports quad representations.');
}
return arrayifyStream(representation.data);
private async copyRepresentation(source: Representation): Promise<Representation> {
const arr = await arrayifyStream(source.data);
return {
dataType: source.dataType,
data: streamifyArray([ ...arr ]),
metadata: source.metadata,
};
}

/**
* Converts an array of Quads to a Representation.
* If preferences.type contains 'text/turtle' the result will be a stream of turtle strings,
* otherwise a stream of Quads.
* Generates a Representation that is identical to the one stored,
* but makes sure to duplicate the data stream so it stays readable for later calls.
*
* Note that in general this should be done by resource store specifically made for converting to turtle,
* this is just here to make this simple resource store work.
*
* @param data - Quads to transform.
* @param preferences - Requested preferences.
* @param path - Path in store of Representation.
*
* @returns The resulting Representation.
*/
private generateRepresentation(data: Quad[], preferences: RepresentationPreferences): Representation {
// Always return turtle unless explicitly asked for quads
if (preferences.type?.some((preference): boolean => preference.value.includes(CONTENT_TYPE_QUADS))) {
return this.generateQuadRepresentation(data);
}
return this.generateBinaryRepresentation(data);
}

/**
* Creates a {@link BinaryRepresentation} of the incoming Quads.
* @param data - Quads to transform to text/turtle.
*
* @returns The resulting binary Representation.
*/
private generateBinaryRepresentation(data: Quad[]): BinaryRepresentation {
return {
dataType: DATA_TYPE_BINARY,
data: streamifyArray([ ...data ]).pipe(new StreamWriter({ format: 'text/turtle' })),
metadata: { raw: [], profiles: [], contentType: 'text/turtle' },
};
}
private async generateRepresentation(path: string): Promise<Representation> {
const source = this.store[path];
const arr = await arrayifyStream(source.data);
source.data = streamifyArray([ ...arr ]);

/**
* Creates a {@link QuadRepresentation} of the incoming Quads.
* @param data - Quads to transform to a stream of Quads.
*
* @returns The resulting quad Representation.
*/
private generateQuadRepresentation(data: Quad[]): QuadRepresentation {
return {
dataType: DATA_TYPE_QUAD,
data: streamifyArray([ ...data ]),
metadata: { raw: [], profiles: []},
dataType: source.dataType,
data: streamifyArray([ ...arr ]),
metadata: source.metadata,
};
}
}
2 changes: 2 additions & 0 deletions src/util/CompositeAsyncHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import { UnsupportedHttpError } from './errors/UnsupportedHttpError';
/**
* Handler that combines several other handlers,
* thereby allowing other classes that depend on a single handler to still use multiple.
* The handlers will be checked in the order they appear in the input array,
* allowing for more fine-grained handlers to check before catch-all handlers.
*/
export class CompositeAsyncHandler<TIn, TOut> implements AsyncHandler<TIn, TOut> {
private readonly handlers: AsyncHandler<TIn, TOut>[];
Expand Down
12 changes: 10 additions & 2 deletions test/integration/AuthenticatedLdpHandler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ import { IncomingHttpHeaders } from 'http';
import { Operation } from '../../src/ldp/operations/Operation';
import { Parser } from 'n3';
import { PatchingStore } from '../../src/storage/PatchingStore';
import { QuadToTurtleConverter } from '../../src/storage/conversion/QuadToTurtleConverter';
import { Representation } from '../../src/ldp/representation/Representation';
import { RepresentationConvertingStore } from '../../src/storage/RepresentationConvertingStore';
import { ResponseDescription } from '../../src/ldp/operations/ResponseDescription';
import { SimpleAuthorizer } from '../../src/authorization/SimpleAuthorizer';
import { SimpleBodyParser } from '../../src/ldp/http/SimpleBodyParser';
Expand All @@ -27,6 +29,7 @@ import { SimpleSparqlUpdatePatchHandler } from '../../src/storage/patch/SimpleSp
import { SimpleTargetExtractor } from '../../src/ldp/http/SimpleTargetExtractor';
import { SingleThreadedResourceLocker } from '../../src/storage/SingleThreadedResourceLocker';
import streamifyArray from 'streamify-array';
import { TurtleToQuadConverter } from '../../src/storage/conversion/TurtleToQuadConverter';
import { createResponse, MockResponse } from 'node-mocks-http';
import { namedNode, quad } from '@rdfjs/data-model';
import * as url from 'url';
Expand Down Expand Up @@ -134,9 +137,14 @@ describe('An AuthenticatedLdpHandler', (): void => {
const authorizer = new SimpleAuthorizer();

const store = new SimpleResourceStore('http://test.com/');
const converter = new CompositeAsyncHandler([
new QuadToTurtleConverter(),
new TurtleToQuadConverter(),
]);
const convertingStore = new RepresentationConvertingStore(store, converter);
const locker = new SingleThreadedResourceLocker();
const patcher = new SimpleSparqlUpdatePatchHandler(store, locker);
const patchingStore = new PatchingStore(store, patcher);
const patcher = new SimpleSparqlUpdatePatchHandler(convertingStore, locker);
const patchingStore = new PatchingStore(convertingStore, patcher);

const operationHandler = new CompositeAsyncHandler<Operation, ResponseDescription>([
new SimpleGetOperationHandler(patchingStore),
Expand Down
13 changes: 5 additions & 8 deletions test/integration/RequestParser.test.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
import { AcceptPreferenceParser } from '../../src/ldp/http/AcceptPreferenceParser';
import arrayifyStream from 'arrayify-stream';
import { DATA_TYPE_QUAD } from '../../src/util/ContentTypes';
import { DATA_TYPE_BINARY } from '../../src/util/ContentTypes';
import { HttpRequest } from '../../src/server/HttpRequest';
import { Readable } from 'stream';
import { SimpleBodyParser } from '../../src/ldp/http/SimpleBodyParser';
import { SimpleRequestParser } from '../../src/ldp/http/SimpleRequestParser';
import { SimpleTargetExtractor } from '../../src/ldp/http/SimpleTargetExtractor';
import streamifyArray from 'streamify-array';
import { namedNode, triple } from '@rdfjs/data-model';

describe('A SimpleRequestParser with simple input parsers', (): void => {
const targetExtractor = new SimpleTargetExtractor();
Expand Down Expand Up @@ -36,7 +35,7 @@ describe('A SimpleRequestParser with simple input parsers', (): void => {
},
body: {
data: expect.any(Readable),
dataType: DATA_TYPE_QUAD,
dataType: DATA_TYPE_BINARY,
metadata: {
contentType: 'text/turtle',
profiles: [],
Expand All @@ -45,10 +44,8 @@ describe('A SimpleRequestParser with simple input parsers', (): void => {
},
});

await expect(arrayifyStream(result.body!.data)).resolves.toEqualRdfQuadArray([ triple(
namedNode('http://test.com/s'),
namedNode('http://test.com/p'),
namedNode('http://test.com/o'),
) ]);
await expect(arrayifyStream(result.body!.data)).resolves.toEqual(
[ '<http://test.com/s> <http://test.com/p> <http://test.com/o>.' ],
);
});
});
Loading

0 comments on commit 4403421

Please sign in to comment.