Skip to content

Commit d4f70d9

Browse files
committedJun 17, 2020
feat: add simple body parser
1 parent 70af469 commit d4f70d9

12 files changed

+213
-1
lines changed
 

‎.eslintignore

+2
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,5 @@ coverage
77
**/*.js
88
**/*.d.ts
99
**/*.js.map
10+
11+
!external-types/*.d.ts

‎.eslintrc.js

+1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ module.exports = {
1616
'@typescript-eslint/space-before-function-paren': [ 'error', 'never' ],
1717
'class-methods-use-this': 'off',
1818
'comma-dangle': ['error', 'always-multiline'],
19+
'dot-location': ['error', 'property'],
1920
'lines-between-class-members': ['error', 'always', { exceptAfterSingleLine: true }],
2021
'padding-line-between-statements': 'off',
2122
'tsdoc/syntax': 'error',

‎.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,4 @@ coverage
99
!.eslintrc.js
1010
!test/eslintrc.js
1111
!jest.config.js
12+
!external-types/*.d.ts

‎external-types/arrayifyStream.d.ts

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
declare module 'arrayify-stream' {
2+
import { Readable } from 'stream';
3+
4+
function arrayifyStream(input: Readable): Promise<any[]>;
5+
export = arrayifyStream;
6+
}

‎external-types/streamifyArray.d.ts

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
declare module 'streamify-array' {
2+
import { Readable } from 'stream';
3+
4+
function streamifyArray(input: any[]): Readable;
5+
export = streamifyArray;
6+
}

‎jest.config.js

+1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ module.exports = {
1313
"js"
1414
],
1515
"testEnvironment": "node",
16+
"setupFilesAfterEnv": ["jest-rdf"],
1617
"collectCoverage": true,
1718
"coveragePathIgnorePatterns": [
1819
"/node_modules/"

‎package-lock.json

+71
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎package.json

+6-1
Original file line numberDiff line numberDiff line change
@@ -22,18 +22,23 @@
2222
],
2323
"dependencies": {
2424
"@rdfjs/data-model": "^1.1.2",
25+
"@types/n3": "^1.1.6",
2526
"@types/node": "^14.0.1",
26-
"@types/rdf-js": "^3.0.0"
27+
"@types/rdf-js": "^3.0.0",
28+
"n3": "^1.3.7"
2729
},
2830
"devDependencies": {
2931
"@types/jest": "^25.2.1",
3032
"@typescript-eslint/eslint-plugin": "^2.33.0",
3133
"@typescript-eslint/parser": "^2.33.0",
34+
"arrayify-stream": "^1.0.0",
3235
"eslint": "^7.0.0",
3336
"eslint-config-es": "^3.19.61",
3437
"eslint-plugin-tsdoc": "^0.2.4",
3538
"husky": "^4.2.5",
3639
"jest": "^26.0.1",
40+
"jest-rdf": "^1.5.0",
41+
"streamify-array": "^1.0.1",
3742
"ts-jest": "^26.0.0",
3843
"typescript": "^3.9.2"
3944
}

‎src/ldp/http/SimpleBodyParser.ts

+52
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { BodyParser } from './BodyParser';
2+
import { HttpRequest } from '../../server/HttpRequest';
3+
import { Quad } from 'rdf-js';
4+
import { QuadRepresentation } from '../representation/QuadRepresentation';
5+
import { RepresentationMetadata } from '../representation/RepresentationMetadata';
6+
import { StreamParser } from 'n3';
7+
import { TypedReadable } from '../../util/TypedReadable';
8+
import { UnsupportedMediaTypeHttpError } from '../../util/errors/UnsupportedMediaTypeHttpError';
9+
import 'jest-rdf';
10+
11+
export class SimpleBodyParser extends BodyParser {
12+
private static readonly contentTypes = [
13+
'application/n-quads',
14+
'application/trig',
15+
'application/n-triples',
16+
'text/turtle',
17+
'text/n3',
18+
];
19+
20+
public async canHandle(input: HttpRequest): Promise<void> {
21+
const contentType = input.headers['content-type'];
22+
23+
if (contentType && !SimpleBodyParser.contentTypes.some((type): boolean => contentType.includes(type))) {
24+
throw new UnsupportedMediaTypeHttpError('This parser only supports RDF data.');
25+
}
26+
}
27+
28+
public async handle(input: HttpRequest): Promise<QuadRepresentation> {
29+
const contentType = input.headers['content-type'];
30+
31+
if (!contentType) {
32+
return undefined;
33+
}
34+
35+
const specificType = contentType.split(';')[0];
36+
37+
const metadata: RepresentationMetadata = {
38+
raw: [],
39+
profiles: [],
40+
contentType: specificType,
41+
};
42+
43+
// StreamParser is a Readable but typings are incorrect at time of writing
44+
const quads: TypedReadable<Quad> = input.pipe(new StreamParser()) as unknown as TypedReadable<Quad>;
45+
46+
return {
47+
dataType: 'quad',
48+
data: quads,
49+
metadata,
50+
};
51+
}
52+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { HttpError } from './HttpError';
2+
3+
export class UnsupportedMediaTypeHttpError extends HttpError {
4+
public constructor(message?: string) {
5+
super(415, 'UnsupportedHttpError', message);
6+
}
7+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import arrayifyStream from 'arrayify-stream';
2+
import { HttpRequest } from '../../../../src/server/HttpRequest';
3+
import { SimpleBodyParser } from '../../../../src/ldp/http/SimpleBodyParser';
4+
import streamifyArray from 'streamify-array';
5+
import { StreamParser } from 'n3';
6+
import { UnsupportedMediaTypeHttpError } from '../../../../src/util/errors/UnsupportedMediaTypeHttpError';
7+
import { namedNode, triple } from '@rdfjs/data-model';
8+
9+
const contentTypes = [
10+
'application/n-quads',
11+
'application/trig',
12+
'application/n-triples',
13+
'text/turtle',
14+
'text/n3',
15+
];
16+
17+
describe('A SimpleBodyparser', (): void => {
18+
const bodyParser = new SimpleBodyParser();
19+
20+
it('rejects input with unsupported content type.', async(): Promise<void> => {
21+
await expect(bodyParser.canHandle({ headers: { 'content-type': 'application/rdf+xml' }} as HttpRequest))
22+
.rejects.toThrow(new UnsupportedMediaTypeHttpError('This parser only supports RDF data.'));
23+
});
24+
25+
it('accepts input with no content type.', async(): Promise<void> => {
26+
await expect(bodyParser.canHandle({ headers: { }} as HttpRequest)).resolves.toBeUndefined();
27+
});
28+
29+
it('accepts turtle and similar content types.', async(): Promise<void> => {
30+
for (const type of contentTypes) {
31+
await expect(bodyParser.canHandle({ headers: { 'content-type': type }} as HttpRequest)).resolves.toBeUndefined();
32+
}
33+
});
34+
35+
it('returns empty output if there was no content-type.', async(): Promise<void> => {
36+
await expect(bodyParser.handle({ headers: { }} as HttpRequest)).resolves.toBeUndefined();
37+
});
38+
39+
it('returns a stream of quads if there was data.', async(): Promise<void> => {
40+
const input = streamifyArray([ '<http://test.com/s> <http://test.com/p> <http://test.com/o>.' ]) as HttpRequest;
41+
input.headers = { 'content-type': 'text/turtle' };
42+
const result = await bodyParser.handle(input);
43+
expect(result).toEqual({
44+
data: expect.any(StreamParser),
45+
dataType: 'quad',
46+
metadata: {
47+
contentType: 'text/turtle',
48+
profiles: [],
49+
raw: [],
50+
},
51+
});
52+
await expect(arrayifyStream(result.data)).resolves.toEqualRdfQuadArray([ triple(
53+
namedNode('http://test.com/s'),
54+
namedNode('http://test.com/p'),
55+
namedNode('http://test.com/o'),
56+
) ]);
57+
});
58+
});

‎tsconfig.json

+2
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
"newLine": "lf",
66
"alwaysStrict": true,
77
"declaration": true,
8+
"esModuleInterop": true,
89
"inlineSources": true,
910
"noImplicitAny": true,
1011
"noImplicitThis": true,
@@ -14,6 +15,7 @@
1415
"stripInternal": true
1516
},
1617
"include": [
18+
"external-types/**/*.ts",
1719
"src/**/*.ts",
1820
"test/**/*.ts"
1921
],

0 commit comments

Comments
 (0)
Please sign in to comment.