Skip to content

Commit

Permalink
feat: Integrate MetadataHandler
Browse files Browse the repository at this point in the history
  • Loading branch information
joachimvh committed Oct 6, 2020
1 parent 71a7a93 commit 4edd4a3
Show file tree
Hide file tree
Showing 12 changed files with 133 additions and 119 deletions.
1 change: 1 addition & 0 deletions config/config-default.json
Expand Up @@ -5,6 +5,7 @@
"files-scs:config/presets/http.json",
"files-scs:config/presets/ldp.json",
"files-scs:config/presets/ldp/credentials-extractor.json",
"files-scs:config/presets/ldp/metadata-handler.json",
"files-scs:config/presets/ldp/operation-handler.json",
"files-scs:config/presets/ldp/permissions-extractor.json",
"files-scs:config/presets/ldp/request-parser.json",
Expand Down
20 changes: 20 additions & 0 deletions config/presets/ldp/metadata-handler.json
@@ -0,0 +1,20 @@
{
"@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^1.0.0/components/context.jsonld",
"@graph": [
{
"@id": "urn:solid-server:default:MetadataHandler",
"@type": "BasicMetadataHandler",
"BasicMetadataHandler:_parsers": [
{
"@type": "ContentTypeParser"
},
{
"@type": "LinkTypeParser"
},
{
"@type": "SlugParser"
}
]
}
]
}
10 changes: 8 additions & 2 deletions config/presets/ldp/request-parser.json
Expand Up @@ -14,10 +14,16 @@
"@type": "CompositeAsyncHandler",
"CompositeAsyncHandler:_handlers": [
{
"@type": "SparqlUpdateBodyParser"
"@type": "SparqlUpdateBodyParser",
"SparqlUpdateBodyParser:_metadataHandler": {
"@id": "urn:solid-server:default:MetadataHandler"
}
},
{
"@type": "RawBodyParser"
"@type": "RawBodyParser",
"RawBodyParser:_metadataHandler": {
"@id": "urn:solid-server:default:MetadataHandler"
}
}
]
}
Expand Down
8 changes: 8 additions & 0 deletions index.ts
Expand Up @@ -14,6 +14,14 @@ export * from './src/authorization/WebAclAuthorizer';
export * from './src/init/CliRunner';
export * from './src/init/Setup';

// LDP/HTTP/Metadata
export * from './src/ldp/http/metadata/BasicMetadataHandler';
export * from './src/ldp/http/metadata/ContentTypeParser';
export * from './src/ldp/http/metadata/LinkTypeParser';
export * from './src/ldp/http/metadata/MetadataHandler';
export * from './src/ldp/http/metadata/MetadataParser';
export * from './src/ldp/http/metadata/SlugParser';

// LDP/HTTP
export * from './src/ldp/http/AcceptPreferenceParser';
export * from './src/ldp/http/BasicRequestParser';
Expand Down
45 changes: 9 additions & 36 deletions src/ldp/http/RawBodyParser.ts
@@ -1,16 +1,22 @@
import type { HttpRequest } from '../../server/HttpRequest';
import { UnsupportedHttpError } from '../../util/errors/UnsupportedHttpError';
import { CONTENT_TYPE, HTTP, RDF } from '../../util/UriConstants';
import type { Representation } from '../representation/Representation';
import { RepresentationMetadata } from '../representation/RepresentationMetadata';
import { BodyParser } from './BodyParser';
import type { MetadataHandler } from './metadata/MetadataHandler';

/**
* Converts incoming {@link HttpRequest} to a Representation without any further parsing.
* Naively parses the mediatype from the content-type header.
* Some other metadata is also generated, but this should probably be done in an external handler.
*/
export class RawBodyParser extends BodyParser {
private readonly metadataHandler: MetadataHandler;

public constructor(metadataHandler: MetadataHandler) {
super();
this.metadataHandler = metadataHandler;
}

public async canHandle(): Promise<void> {
// All content-types are supported
}
Expand All @@ -33,40 +39,7 @@ export class RawBodyParser extends BodyParser {
return {
binary: true,
data: input,
metadata: this.parseMetadata(input),
metadata: await this.metadataHandler.handleSafe(input),
};
}

private parseMetadata(input: HttpRequest): RepresentationMetadata {
const contentType = /^[^;]*/u.exec(input.headers['content-type']!)![0];

const metadata = new RepresentationMetadata({ [CONTENT_TYPE]: contentType });

const { link, slug } = input.headers;

if (slug) {
if (Array.isArray(slug)) {
throw new UnsupportedHttpError('At most 1 slug header is allowed.');
}
metadata.set(HTTP.slug, slug);
}

// There are similarities here to Accept header parsing so that library should become more generic probably
if (link) {
const linkArray = Array.isArray(link) ? link : [ link ];
const parsedLinks = linkArray.map((entry): { url: string; rel: string } => {
const [ , url, rest ] = /^<([^>]*)>(.*)$/u.exec(entry) ?? [];
const [ , rel ] = /^ *; *rel="(.*)"$/u.exec(rest) ?? [];
return { url, rel };
});
for (const entry of parsedLinks) {
if (entry.rel === 'type') {
metadata.set(RDF.type, entry.url);
break;
}
}
}

return metadata;
}
}
48 changes: 27 additions & 21 deletions src/ldp/http/SparqlUpdateBodyParser.ts
@@ -1,13 +1,13 @@
import { PassThrough } from 'stream';
import type { Algebra } from 'sparqlalgebrajs';
import { translate } from 'sparqlalgebrajs';
import type { HttpRequest } from '../../server/HttpRequest';
import { APPLICATION_SPARQL_UPDATE } from '../../util/ContentTypes';
import { UnsupportedHttpError } from '../../util/errors/UnsupportedHttpError';
import { UnsupportedMediaTypeHttpError } from '../../util/errors/UnsupportedMediaTypeHttpError';
import { CONTENT_TYPE } from '../../util/UriConstants';
import { readableToString } from '../../util/Util';
import { RepresentationMetadata } from '../representation/RepresentationMetadata';
import { pipeStreamsAndErrors, readableToString } from '../../util/Util';
import { BodyParser } from './BodyParser';
import type { MetadataHandler } from './metadata/MetadataHandler';
import type { SparqlUpdatePatch } from './SparqlUpdatePatch';

/**
Expand All @@ -16,38 +16,44 @@ import type { SparqlUpdatePatch } from './SparqlUpdatePatch';
* Still needs access to a handler for parsing metadata.
*/
export class SparqlUpdateBodyParser extends BodyParser {
private readonly metadataHandler: MetadataHandler;

public constructor(metadataHandler: MetadataHandler) {
super();
this.metadataHandler = metadataHandler;
}

public async canHandle(input: HttpRequest): Promise<void> {
if (input.headers['content-type'] !== APPLICATION_SPARQL_UPDATE) {
throw new UnsupportedMediaTypeHttpError('This parser only supports SPARQL UPDATE data.');
}
}

public async handle(input: HttpRequest): Promise<SparqlUpdatePatch> {
// Note that readableObjectMode is only defined starting from Node 12
// It is impossible to check if object mode is enabled in Node 10 (without accessing private variables)
const options = { objectMode: input.readableObjectMode };
const toAlgebraStream = new PassThrough(options);
const dataCopy = new PassThrough(options);
pipeStreamsAndErrors(input, toAlgebraStream);
pipeStreamsAndErrors(input, dataCopy);
let algebra: Algebra.Operation;
try {
// Note that readableObjectMode is only defined starting from Node 12
// It is impossible to check if object mode is enabled in Node 10 (without accessing private variables)
const options = { objectMode: input.readableObjectMode };
const toAlgebraStream = new PassThrough(options);
const dataCopy = new PassThrough(options);
input.pipe(toAlgebraStream);
input.pipe(dataCopy);
const sparql = await readableToString(toAlgebraStream);
const algebra = translate(sparql, { quads: true });

const metadata = new RepresentationMetadata({ [CONTENT_TYPE]: APPLICATION_SPARQL_UPDATE });

// Prevent body from being requested again
return {
algebra,
binary: true,
data: dataCopy,
metadata,
};
algebra = translate(sparql, { quads: true });
} catch (error: unknown) {
if (error instanceof Error) {
throw new UnsupportedHttpError(error.message);
}
throw new UnsupportedHttpError();
}

// Prevent body from being requested again
return {
algebra,
binary: true,
data: dataCopy,
metadata: await this.metadataHandler.handleSafe(input),
};
}
}
13 changes: 9 additions & 4 deletions test/configs/BasicHandlersConfig.ts
Expand Up @@ -15,10 +15,14 @@ import {
} from '../../index';

import type { ServerConfig } from './ServerConfig';
import { getInMemoryResourceStore,
import {
getInMemoryResourceStore,
getOperationHandler,
getConvertingStore,
getPatchingStore, getBasicRequestParser } from './Util';
getPatchingStore,
getBasicRequestParser,
getBasicMetadataHandler,
} from './Util';

/**
* BasicHandlersConfig works with
Expand All @@ -39,9 +43,10 @@ export class BasicHandlersConfig implements ServerConfig {
}

public getHttpHandler(): HttpHandler {
const metadataHandler = getBasicMetadataHandler();
const requestParser = getBasicRequestParser([
new SparqlUpdateBodyParser(),
new RawBodyParser(),
new SparqlUpdateBodyParser(metadataHandler),
new RawBodyParser(metadataHandler),
]);

const credentialsExtractor = new UnsecureWebIdExtractor();
Expand Down
10 changes: 8 additions & 2 deletions test/configs/FileResourceStoreConfig.ts
Expand Up @@ -12,7 +12,13 @@ import {
UnsecureWebIdExtractor,
} from '../../index';
import type { ServerConfig } from './ServerConfig';
import { getFileResourceStore, getOperationHandler, getConvertingStore, getBasicRequestParser } from './Util';
import {
getFileResourceStore,
getOperationHandler,
getConvertingStore,
getBasicRequestParser,
getBasicMetadataHandler,
} from './Util';

/**
* FileResourceStoreConfig works with
Expand All @@ -33,7 +39,7 @@ export class FileResourceStoreConfig implements ServerConfig {

public getHttpHandler(): HttpHandler {
// This is for the sake of test coverage, as it could also be just getBasicRequestParser()
const requestParser = getBasicRequestParser([ new RawBodyParser() ]);
const requestParser = getBasicRequestParser([ new RawBodyParser(getBasicMetadataHandler()) ]);

const credentialsExtractor = new UnsecureWebIdExtractor();
const permissionsExtractor = new CompositeAsyncHandler([
Expand Down
19 changes: 15 additions & 4 deletions test/configs/Util.ts
@@ -1,21 +1,22 @@
import { join } from 'path';
import type { BodyParser,
HttpRequest,
Operation,
Representation,
RepresentationConverter,
ResourceStore,
ResponseDescription } from '../../index';
import {
AcceptPreferenceParser,
BasicMetadataHandler,
BasicRequestParser,
BasicTargetExtractor,
CompositeAsyncHandler,
ContentTypeParser,
DeleteOperationHandler,
FileResourceStore,
GetOperationHandler,
InMemoryResourceStore,
InteractionController,
LinkTypeParser,
MetadataController,
PatchingStore,
PatchOperationHandler,
Expand All @@ -24,6 +25,7 @@ import {
RawBodyParser,
RepresentationConvertingStore,
SingleThreadedResourceLocker,
SlugParser,
SparqlUpdatePatchHandler,
UrlBasedAclManager,
UrlContainerManager,
Expand Down Expand Up @@ -102,6 +104,15 @@ export const getOperationHandler = (store: ResourceStore): CompositeAsyncHandler
return new CompositeAsyncHandler<Operation, ResponseDescription>(handlers);
};

/**
* Creates a BasicMetadataHandler with parsers for content-type, slugs and link types.
*/
export const getBasicMetadataHandler = (): BasicMetadataHandler => new BasicMetadataHandler([
new ContentTypeParser(),
new SlugParser(),
new LinkTypeParser(),
]);

/**
* Gives a basic request parser based on some body parses.
* @param bodyParsers - Optional list of body parsers, default is RawBodyParser.
Expand All @@ -114,9 +125,9 @@ export const getBasicRequestParser = (bodyParsers: BodyParser[] = []): BasicRequ
bodyParser = bodyParsers[0];
} else if (bodyParsers.length === 0) {
// If no body parser is given (array is empty), default to RawBodyParser
bodyParser = new RawBodyParser();
bodyParser = new RawBodyParser(getBasicMetadataHandler());
} else {
bodyParser = new CompositeAsyncHandler<HttpRequest, Representation | undefined>(bodyParsers);
bodyParser = new CompositeAsyncHandler(bodyParsers);
}
return new BasicRequestParser({
targetExtractor: new BasicTargetExtractor(),
Expand Down
4 changes: 3 additions & 1 deletion test/integration/RequestParser.test.ts
Expand Up @@ -4,13 +4,15 @@ import streamifyArray from 'streamify-array';
import { AcceptPreferenceParser } from '../../src/ldp/http/AcceptPreferenceParser';
import { BasicRequestParser } from '../../src/ldp/http/BasicRequestParser';
import { BasicTargetExtractor } from '../../src/ldp/http/BasicTargetExtractor';
import { BasicMetadataHandler } from '../../src/ldp/http/metadata/BasicMetadataHandler';
import { ContentTypeParser } from '../../src/ldp/http/metadata/ContentTypeParser';
import { RawBodyParser } from '../../src/ldp/http/RawBodyParser';
import { RepresentationMetadata } from '../../src/ldp/representation/RepresentationMetadata';
import type { HttpRequest } from '../../src/server/HttpRequest';

describe('A BasicRequestParser with simple input parsers', (): void => {
const targetExtractor = new BasicTargetExtractor();
const bodyParser = new RawBodyParser();
const bodyParser = new RawBodyParser(new BasicMetadataHandler([ new ContentTypeParser() ]));
const preferenceParser = new AcceptPreferenceParser();
const requestParser = new BasicRequestParser({ targetExtractor, bodyParser, preferenceParser });

Expand Down

0 comments on commit 4edd4a3

Please sign in to comment.