mirror of
https://github.com/CommunitySolidServer/CommunitySolidServer.git
synced 2024-10-03 14:55:10 +00:00
feat: Create MetadataSerializer
This commit is contained in:
parent
840965cdef
commit
aebccd45c0
28
src/ldp/http/metadata/LinkRelMetadataWriter.ts
Normal file
28
src/ldp/http/metadata/LinkRelMetadataWriter.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
28
src/ldp/http/metadata/MappedMetadataWriter.ts
Normal file
28
src/ldp/http/metadata/MappedMetadataWriter.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
9
src/ldp/http/metadata/MetadataWriter.ts
Normal file
9
src/ldp/http/metadata/MetadataWriter.ts
Normal 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 }> { }
|
@ -3,6 +3,7 @@ import arrayifyStream from 'arrayify-stream';
|
|||||||
import { DataFactory } from 'n3';
|
import { DataFactory } from 'n3';
|
||||||
import type { Literal, NamedNode, Quad } from 'rdf-js';
|
import type { Literal, NamedNode, Quad } from 'rdf-js';
|
||||||
import { getLoggerFor } from '../logging/LogUtil';
|
import { getLoggerFor } from '../logging/LogUtil';
|
||||||
|
import type { HttpResponse } from '../server/HttpResponse';
|
||||||
|
|
||||||
const logger = getLoggerFor('Util');
|
const logger = getLoggerFor('Util');
|
||||||
|
|
||||||
@ -100,3 +101,25 @@ export const encodeUriPathComponents = (path: string): string => path.split('/')
|
|||||||
export const pushQuad =
|
export const pushQuad =
|
||||||
(quads: Quad[], subject: NamedNode, predicate: NamedNode, object: NamedNode | Literal): number =>
|
(quads: Quad[], subject: NamedNode, predicate: NamedNode, object: NamedNode | Literal): number =>
|
||||||
quads.push(DataFactory.quad(subject, predicate, object));
|
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);
|
||||||
|
};
|
||||||
|
27
test/unit/ldp/http/metadata/LinkRelMetadataWriter.test.ts
Normal file
27
test/unit/ldp/http/metadata/LinkRelMetadataWriter.test.ts
Normal 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"` ]);
|
||||||
|
});
|
||||||
|
});
|
26
test/unit/ldp/http/metadata/MappedMetadataWriter.test.ts
Normal file
26
test/unit/ldp/http/metadata/MappedMetadataWriter.test.ts
Normal 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' ]);
|
||||||
|
});
|
||||||
|
});
|
@ -2,7 +2,9 @@ import { PassThrough } from 'stream';
|
|||||||
import { DataFactory } from 'n3';
|
import { DataFactory } from 'n3';
|
||||||
import type { Quad } from 'rdf-js';
|
import type { Quad } from 'rdf-js';
|
||||||
import streamifyArray from 'streamify-array';
|
import streamifyArray from 'streamify-array';
|
||||||
|
import type { HttpResponse } from '../../../src/server/HttpResponse';
|
||||||
import {
|
import {
|
||||||
|
addHeader,
|
||||||
decodeUriPathComponents,
|
decodeUriPathComponents,
|
||||||
encodeUriPathComponents,
|
encodeUriPathComponents,
|
||||||
ensureTrailingSlash,
|
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' ]);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
Loading…
x
Reference in New Issue
Block a user