refactor: Clean up utility functions

This commit is contained in:
Joachim Van Herwegen
2020-11-18 15:52:07 +01:00
parent 82f3aa0cd8
commit 1073c2ff4c
54 changed files with 482 additions and 646 deletions

View File

@@ -1,4 +1,5 @@
import { getLoggerFor } from '../logging/LogUtil';
import type { HttpResponse } from '../server/HttpResponse';
import { UnsupportedHttpError } from './errors/UnsupportedHttpError';
const logger = getLoggerFor('HeaderUtil');
@@ -355,3 +356,25 @@ export const parseAcceptLanguage = (input: string): AcceptLanguage[] => {
});
return results;
};
/**
* 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

@@ -1,61 +0,0 @@
import type { Readable } from 'stream';
import arrayifyStream from 'arrayify-stream';
import { DataFactory, StreamParser, StreamWriter } from 'n3';
import type { NamedNode, Quad } from 'rdf-js';
import streamifyArray from 'streamify-array';
import { RepresentationMetadata } from '../ldp/representation/RepresentationMetadata';
import { TEXT_TURTLE } from './ContentTypes';
import { LDP, RDF } from './UriConstants';
import { toNamedNode } from './UriUtil';
import { pipeSafe, pushQuad } from './Util';
export class MetadataController {
/**
* Helper function to generate type quads for a Container or Resource.
* @param subject - Subject for the new quads.
* @param isContainer - If the identifier corresponds to a container.
*
* @returns The generated quads.
*/
public generateResourceQuads(subject: NamedNode, isContainer: boolean): Quad[] {
const quads: Quad[] = [];
if (isContainer) {
pushQuad(quads, subject, toNamedNode(RDF.type), toNamedNode(LDP.Container));
pushQuad(quads, subject, toNamedNode(RDF.type), toNamedNode(LDP.BasicContainer));
}
pushQuad(quads, subject, toNamedNode(RDF.type), toNamedNode(LDP.Resource));
return quads;
}
/**
* Helper function to generate the quads describing that the resource URIs are children of the container URI.
* @param containerURI - The URI of the container.
* @param childURIs - The URI of the child resources.
*
* @returns The generated quads.
*/
public generateContainerContainsResourceQuads(containerURI: NamedNode, childURIs: string[]): Quad[] {
return new RepresentationMetadata(containerURI, { [LDP.contains]: childURIs.map(DataFactory.namedNode) }).quads();
}
/**
* Helper function for serializing an array of quads, with as result a Readable object.
* @param quads - The array of quads.
*
* @returns The Readable object.
*/
public serializeQuads(quads: Quad[]): Readable {
return pipeSafe(streamifyArray(quads), new StreamWriter({ format: TEXT_TURTLE }));
}
/**
* Helper function to convert a Readable into an array of quads.
* @param readable - The readable object.
*
* @returns A promise containing the array of quads.
*/
public async parseQuads(readable: Readable): Promise<Quad[]> {
return await arrayifyStream(pipeSafe(readable, new StreamParser({ format: TEXT_TURTLE })));
}
}

60
src/util/PathUtil.ts Normal file
View File

@@ -0,0 +1,60 @@
import type { ResourceIdentifier } from '../ldp/representation/ResourceIdentifier';
import { InternalServerError } from './errors/InternalServerError';
/**
* Makes sure the input path has exactly 1 slash at the end.
* Multiple slashes will get merged into one.
* If there is no slash it will be added.
*
* @param path - Path to check.
*
* @returns The potentially changed path.
*/
export const ensureTrailingSlash = (path: string): string => path.replace(/\/*$/u, '/');
/**
* Makes sure the input path has no slashes at the end.
*
* @param path - Path to check.
*
* @returns The potentially changed path.
*/
export const trimTrailingSlashes = (path: string): string => path.replace(/\/+$/u, '');
/**
* Converts a URI path to the canonical version by splitting on slashes,
* decoding any percent-based encodings,
* and then encoding any special characters.
*/
export const toCanonicalUriPath = (path: string): string => path.split('/').map((part): string =>
encodeURIComponent(decodeURIComponent(part))).join('/');
/**
* Decodes all components of a URI path.
*/
export const decodeUriPathComponents = (path: string): string => path.split('/').map(decodeURIComponent).join('/');
/**
* Encodes all (non-slash) special characters in a URI path.
*/
export const encodeUriPathComponents = (path: string): string => path.split('/').map(encodeURIComponent).join('/');
/**
* Finds the container containing the given resource.
* This does not ensure either the container or resource actually exist.
*
* @param id - Identifier to find container of.
*
* @returns The identifier of the container this resource is in.
*/
export const getParentContainer = (id: ResourceIdentifier): ResourceIdentifier => {
// Trailing slash is necessary for URL library
const parentPath = new URL('..', ensureTrailingSlash(id.path)).toString();
// This probably means there is an issue with the root
if (parentPath === id.path) {
throw new InternalServerError('URL root reached');
}
return { path: parentPath };
};

32
src/util/QuadUtil.ts Normal file
View File

@@ -0,0 +1,32 @@
import type { Readable } from 'stream';
import arrayifyStream from 'arrayify-stream';
import { DataFactory, StreamParser, StreamWriter } from 'n3';
import type { Literal, NamedNode, Quad } from 'rdf-js';
import streamifyArray from 'streamify-array';
import { TEXT_TURTLE } from './ContentTypes';
import { pipeSafely } from './StreamUtil';
/**
* Generates a quad with the given subject/predicate/object and pushes it to the given array.
*/
export const pushQuad =
(quads: Quad[], subject: NamedNode, predicate: NamedNode, object: NamedNode | Literal): number =>
quads.push(DataFactory.quad(subject, predicate, object));
/**
* Helper function for serializing an array of quads, with as result a Readable object.
* @param quads - The array of quads.
*
* @returns The Readable object.
*/
export const serializeQuads = (quads: Quad[]): Readable =>
pipeSafely(streamifyArray(quads), new StreamWriter({ format: TEXT_TURTLE }));
/**
* Helper function to convert a Readable into an array of quads.
* @param readable - The readable object.
*
* @returns A promise containing the array of quads.
*/
export const parseQuads = async(readable: Readable): Promise<Quad[]> =>
arrayifyStream(pipeSafely(readable, new StreamParser({ format: TEXT_TURTLE })));

34
src/util/ResourceUtil.ts Normal file
View File

@@ -0,0 +1,34 @@
import { DataFactory } from 'n3';
import type { NamedNode, Quad } from 'rdf-js';
import { RepresentationMetadata } from '../ldp/representation/RepresentationMetadata';
import { pushQuad } from './QuadUtil';
import { LDP, RDF } from './UriConstants';
import { toNamedNode } from './UriUtil';
/**
* Helper function to generate type quads for a Container or Resource.
* @param subject - Subject for the new quads.
* @param isContainer - If the identifier corresponds to a container.
*
* @returns The generated quads.
*/
export const generateResourceQuads = (subject: NamedNode, isContainer: boolean): Quad[] => {
const quads: Quad[] = [];
if (isContainer) {
pushQuad(quads, subject, toNamedNode(RDF.type), toNamedNode(LDP.Container));
pushQuad(quads, subject, toNamedNode(RDF.type), toNamedNode(LDP.BasicContainer));
}
pushQuad(quads, subject, toNamedNode(RDF.type), toNamedNode(LDP.Resource));
return quads;
};
/**
* Helper function to generate the quads describing that the resource URIs are children of the container URI.
* @param containerURI - The URI of the container.
* @param childURIs - The URI of the child resources.
*
* @returns The generated quads.
*/
export const generateContainmentQuads = (containerURI: NamedNode, childURIs: string[]): Quad[] =>
new RepresentationMetadata(containerURI, { [LDP.contains]: childURIs.map(DataFactory.namedNode) }).quads();

38
src/util/StreamUtil.ts Normal file
View File

@@ -0,0 +1,38 @@
import type { Readable, Writable } from 'stream';
import arrayifyStream from 'arrayify-stream';
import { getLoggerFor } from '../logging/LogUtil';
const logger = getLoggerFor('StreamUtil');
/**
* Joins all strings of a stream.
* @param stream - Stream of strings.
*
* @returns The joined string.
*/
export const readableToString = async(stream: Readable): Promise<string> => (await arrayifyStream(stream)).join('');
/**
* Pipes one stream into another and emits errors of the first stream with the second.
* In case of an error in the first stream the second one will be destroyed with the given error.
* @param readable - Initial readable stream.
* @param destination - The destination for writing data.
* @param mapError - Optional function that takes the error and converts it to a new error.
*
* @returns The destination stream.
*/
export const pipeSafely = <T extends Writable>(readable: NodeJS.ReadableStream, destination: T,
mapError?: (error: Error) => Error): T => {
// Not using `stream.pipeline` since the result there only emits an error event if the last stream has the error
readable.pipe(destination);
readable.on('error', (error): void => {
logger.warn(`Piped stream errored with ${error.message}`);
// From https://nodejs.org/api/stream.html#stream_readable_pipe_destination_options :
// "One important caveat is that if the Readable stream emits an error during processing, the Writable destination
// is not closed automatically. If an error occurs, it will be necessary to manually close each stream
// in order to prevent memory leaks."
destination.destroy(mapError ? mapError(error) : error);
});
return destination;
};

View File

@@ -1,131 +0,0 @@
import type { Readable, Writable } from 'stream';
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');
/**
* Makes sure the input path has exactly 1 slash at the end.
* Multiple slashes will get merged into one.
* If there is no slash it will be added.
*
* @param path - Path to check.
*
* @returns The potentially changed path.
*/
export const ensureTrailingSlash = (path: string): string => path.replace(/\/*$/u, '/');
/**
* Joins all strings of a stream.
* @param stream - Stream of strings.
*
* @returns The joined string.
*/
export const readableToString = async(stream: Readable): Promise<string> => (await arrayifyStream(stream)).join('');
/**
* Makes sure the input path has no slashes at the end.
*
* @param path - Path to check.
*
* @returns The potentially changed path.
*/
export const trimTrailingSlashes = (path: string): string => path.replace(/\/+$/u, '');
/**
* Checks if the given two media types/ranges match each other.
* Takes wildcards into account.
* @param mediaA - Media type to match.
* @param mediaB - Media type to match.
*
* @returns True if the media type patterns can match each other.
*/
export const matchingMediaType = (mediaA: string, mediaB: string): boolean => {
const [ typeA, subTypeA ] = mediaA.split('/');
const [ typeB, subTypeB ] = mediaB.split('/');
if (typeA === '*' || typeB === '*') {
return true;
}
if (typeA !== typeB) {
return false;
}
if (subTypeA === '*' || subTypeB === '*') {
return true;
}
return subTypeA === subTypeB;
};
/**
* Pipes one stream into another and emits errors of the first stream with the second.
* In case of an error in the first stream the second one will be destroyed with the given error.
* @param readable - Initial readable stream.
* @param destination - The destination for writing data.
* @param mapError - Optional function that takes the error and converts it to a new error.
*
* @returns The destination stream.
*/
export const pipeSafe = <T extends Writable>(readable: NodeJS.ReadableStream, destination: T,
mapError?: (error: Error) => Error): T => {
// Not using `stream.pipeline` since the result there only emits an error event if the last stream has the error
readable.pipe(destination);
readable.on('error', (error): void => {
logger.warn(`Piped stream errored with ${error.message}`);
// From https://nodejs.org/api/stream.html#stream_readable_pipe_destination_options :
// "One important caveat is that if the Readable stream emits an error during processing, the Writable destination
// is not closed automatically. If an error occurs, it will be necessary to manually close each stream
// in order to prevent memory leaks."
destination.destroy(mapError ? mapError(error) : error);
});
return destination;
};
/**
* Converts a URI path to the canonical version by splitting on slashes,
* decoding any percent-based encodings,
* and then encoding any special characters.
*/
export const toCanonicalUriPath = (path: string): string => path.split('/').map((part): string =>
encodeURIComponent(decodeURIComponent(part))).join('/');
/**
* Decodes all components of a URI path.
*/
export const decodeUriPathComponents = (path: string): string => path.split('/').map(decodeURIComponent).join('/');
/**
* Encodes all (non-slash) special characters in a URI path.
*/
export const encodeUriPathComponents = (path: string): string => path.split('/').map(encodeURIComponent).join('/');
/**
* Generates a quad with the given subject/predicate/object and pushes it to the given array.
*/
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);
};