feat: Create MetadataSerializer

This commit is contained in:
Joachim Van Herwegen 2020-10-27 16:27:58 +01:00
parent 840965cdef
commit aebccd45c0
7 changed files with 180 additions and 0 deletions

View File

@ -0,0 +1,28 @@
import type { HttpResponse } from '../../../server/HttpResponse';
import { addHeader } from '../../../util/Util';
import type { RepresentationMetadata } from '../../representation/RepresentationMetadata';
import { MetadataWriter } from './MetadataWriter';
/**
* A {@link MetadataWriter} that takes a linking metadata predicates to Link header "rel" values.
* The values of the objects will be put in a Link header with the corresponding "rel" value.
*/
export class LinkRelMetadataWriter extends MetadataWriter {
private readonly linkRelMap: Record<string, string>;
// Not supported by Components.js yet
// eslint-disable-next-line @typescript-eslint/consistent-indexed-object-style
public constructor(linkRelMap: { [predicate: string]: string }) {
super();
this.linkRelMap = linkRelMap;
}
public async handle(input: { response: HttpResponse; metadata: RepresentationMetadata }): Promise<void> {
for (const key of Object.keys(this.linkRelMap)) {
const values = input.metadata.getAll(key).map((term): string => `<${term.value}>; rel="${this.linkRelMap[key]}"`);
if (values.length > 0) {
addHeader(input.response, 'link', values);
}
}
}
}

View File

@ -0,0 +1,28 @@
import type { HttpResponse } from '../../../server/HttpResponse';
import { addHeader } from '../../../util/Util';
import type { RepresentationMetadata } from '../../representation/RepresentationMetadata';
import { MetadataWriter } from './MetadataWriter';
/**
* A {@link MetadataWriter} that takes a map directly converting metadata predicates to headers.
* The header value(s) will be the same as the corresponding object value(s).
*/
export class MappedMetadataWriter extends MetadataWriter {
private readonly headerMap: Record<string, string>;
// Not supported by Components.js yet
// eslint-disable-next-line @typescript-eslint/consistent-indexed-object-style
public constructor(headerMap: { [predicate: string]: string }) {
super();
this.headerMap = headerMap;
}
public async handle(input: { response: HttpResponse; metadata: RepresentationMetadata }): Promise<void> {
for (const key of Object.keys(this.headerMap)) {
const values = input.metadata.getAll(key).map((term): string => term.value);
if (values.length > 0) {
addHeader(input.response, this.headerMap[key], values);
}
}
}
}

View File

@ -0,0 +1,9 @@
import type { HttpResponse } from '../../../server/HttpResponse';
import { AsyncHandler } from '../../../util/AsyncHandler';
import type { RepresentationMetadata } from '../../representation/RepresentationMetadata';
/**
* A serializer that converts metadata to headers for an HttpResponse.
*/
export abstract class MetadataWriter
extends AsyncHandler<{ response: HttpResponse; metadata: RepresentationMetadata }> { }

View File

@ -3,6 +3,7 @@ import arrayifyStream from 'arrayify-stream';
import { DataFactory } from 'n3';
import type { Literal, NamedNode, Quad } from 'rdf-js';
import { getLoggerFor } from '../logging/LogUtil';
import type { HttpResponse } from '../server/HttpResponse';
const logger = getLoggerFor('Util');
@ -100,3 +101,25 @@ export const encodeUriPathComponents = (path: string): string => path.split('/')
export const pushQuad =
(quads: Quad[], subject: NamedNode, predicate: NamedNode, object: NamedNode | Literal): number =>
quads.push(DataFactory.quad(subject, predicate, object));
/**
* Adds a header value without overriding previous values.
*/
export const addHeader = (response: HttpResponse, name: string, value: string | string[]): void => {
let allValues: string[] = [];
if (response.hasHeader(name)) {
let oldValues = response.getHeader(name)!;
if (typeof oldValues === 'string') {
oldValues = [ oldValues ];
} else if (typeof oldValues === 'number') {
oldValues = [ `${oldValues}` ];
}
allValues = oldValues;
}
if (Array.isArray(value)) {
allValues.push(...value);
} else {
allValues.push(value);
}
response.setHeader(name, allValues.length === 1 ? allValues[0] : allValues);
};

View File

@ -0,0 +1,27 @@
import { LinkRelMetadataWriter } from '../../../../../src/ldp/http/metadata/LinkRelMetadataWriter';
import { RepresentationMetadata } from '../../../../../src/ldp/representation/RepresentationMetadata';
import { LDP, RDF } from '../../../../../src/util/UriConstants';
import { toNamedNode } from '../../../../../src/util/UriUtil';
import * as util from '../../../../../src/util/Util';
describe('A LinkRelMetadataWriter', (): void => {
const writer = new LinkRelMetadataWriter({ [RDF.type]: 'type', dummy: 'dummy' });
let mock: jest.SpyInstance;
let addHeaderMock: jest.Mock;
beforeEach(async(): Promise<void> => {
addHeaderMock = jest.fn();
mock = jest.spyOn(util, 'addHeader').mockImplementation(addHeaderMock);
});
afterEach(async(): Promise<void> => {
mock.mockRestore();
});
it('adds the correct link headers.', async(): Promise<void> => {
const metadata = new RepresentationMetadata({ [RDF.type]: toNamedNode(LDP.Resource), unused: 'text' });
await expect(writer.handle({ response: 'response' as any, metadata })).resolves.toBeUndefined();
expect(addHeaderMock).toHaveBeenCalledTimes(1);
expect(addHeaderMock).toHaveBeenLastCalledWith('response', 'link', [ `<${LDP.Resource}>; rel="type"` ]);
});
});

View File

@ -0,0 +1,26 @@
import { MappedMetadataWriter } from '../../../../../src/ldp/http/metadata/MappedMetadataWriter';
import { RepresentationMetadata } from '../../../../../src/ldp/representation/RepresentationMetadata';
import { CONTENT_TYPE } from '../../../../../src/util/UriConstants';
import * as util from '../../../../../src/util/Util';
describe('A MappedMetadataWriter', (): void => {
const writer = new MappedMetadataWriter({ [CONTENT_TYPE]: 'content-type', dummy: 'dummy' });
let mock: jest.SpyInstance;
let addHeaderMock: jest.Mock;
beforeEach(async(): Promise<void> => {
addHeaderMock = jest.fn();
mock = jest.spyOn(util, 'addHeader').mockImplementation(addHeaderMock);
});
afterEach(async(): Promise<void> => {
mock.mockRestore();
});
it('adds metadata to the corresponding header.', async(): Promise<void> => {
const metadata = new RepresentationMetadata({ [CONTENT_TYPE]: 'text/turtle', unused: 'text' });
await expect(writer.handle({ response: 'response' as any, metadata })).resolves.toBeUndefined();
expect(addHeaderMock).toHaveBeenCalledTimes(1);
expect(addHeaderMock).toHaveBeenLastCalledWith('response', 'content-type', [ 'text/turtle' ]);
});
});

View File

@ -2,7 +2,9 @@ import { PassThrough } from 'stream';
import { DataFactory } from 'n3';
import type { Quad } from 'rdf-js';
import streamifyArray from 'streamify-array';
import type { HttpResponse } from '../../../src/server/HttpResponse';
import {
addHeader,
decodeUriPathComponents,
encodeUriPathComponents,
ensureTrailingSlash,
@ -96,4 +98,41 @@ describe('Util function', (): void => {
]);
});
});
describe('addHeader', (): void => {
let response: HttpResponse;
beforeEach(async(): Promise<void> => {
const headers: Record<string, string | number | string[]> = {};
response = {
hasHeader: (name: string): boolean => Boolean(headers[name]),
getHeader: (name: string): number | string | string[] | undefined => headers[name],
setHeader(name: string, value: number | string | string[]): void {
headers[name] = value;
},
} as any;
});
it('adds values if there are none already.', async(): Promise<void> => {
expect(addHeader(response, 'name', 'value')).toBeUndefined();
expect(response.getHeader('name')).toBe('value');
expect(addHeader(response, 'names', [ 'value1', 'values2' ])).toBeUndefined();
expect(response.getHeader('names')).toEqual([ 'value1', 'values2' ]);
});
it('appends values to already existing values.', async(): Promise<void> => {
response.setHeader('name', 'oldValue');
expect(addHeader(response, 'name', 'value')).toBeUndefined();
expect(response.getHeader('name')).toEqual([ 'oldValue', 'value' ]);
response.setHeader('number', 5);
expect(addHeader(response, 'number', 'value')).toBeUndefined();
expect(response.getHeader('number')).toEqual([ '5', 'value' ]);
response.setHeader('names', [ 'oldValue1', 'oldValue2' ]);
expect(addHeader(response, 'names', [ 'value1', 'values2' ])).toBeUndefined();
expect(response.getHeader('names')).toEqual([ 'oldValue1', 'oldValue2', 'value1', 'values2' ]);
});
});
});