clonegur/backend/server/RestRequest.ts

113 lines
3.8 KiB
TypeScript

export type ParamDict = Record<string, string>;
export type Headers = Record<string, string | string[] | undefined>;
function splitUrl(url: string) {
return url.split('/').map(v => v.trim()).filter(v => v !== '');
}
function sanitizeUrl(url: string, forceAbsolute = true) {
url = url.trim();
if (forceAbsolute || url.startsWith('/')) {
return '/' + splitUrl(url).join('/');
}
else {
return '/' + splitUrl(url).join('/');
}
}
export default class RestRequest {
public readonly body: unknown;
public readonly method: string;
public readonly url: string;
public readonly pathParams: ParamDict;
public readonly queryParams: ParamDict;
public readonly headers: Headers;
public get params() {
return { ...this.queryParams, ...this.pathParams };
}
public constructor(
body: unknown,
headers: Headers,
method: string,
url: string,
pathParams: ParamDict = {},
queryParams: ParamDict = {}
) {
this.body = body;
this.headers = headers;
this.pathParams = { ...pathParams };
this.queryParams = { ...queryParams };
this.method = method.toLowerCase();
this.url = sanitizeUrl(url);
if (this.url.includes('?')) {
const questionIndex = this.url.indexOf('?');
this.url = this.url.substring(0, questionIndex);
const params = this.url
.substring(questionIndex + 1)
.split('&')
.map(v => v.trim())
.filter(v => v !== '');
for (const rawParam of params) {
const i = rawParam.indexOf('=');
if (i < 0) continue;
const name = rawParam.substring(0, i);
const val = rawParam.substring(i + 1);
if (name === '') continue;
this.queryParams[name] = val;
}
}
}
public match(predicate: string) {
const urlSegments = splitUrl(this.url);
const predSegments = splitUrl(predicate);
const wildcardIndex = predSegments.indexOf('*');
const hasWildcard = wildcardIndex >= 0;
const pathParams: ParamDict = { ...this.pathParams };
if (wildcardIndex >= 0) {
if (predSegments.includes('*', wildcardIndex + 1)) throw new Error("A path predicate may not have more than one wildcard.");
if (predSegments.splice(wildcardIndex).length > 1) throw new Error("A path predicate must be the last segment.");
}
for (const predSeg of predSegments) {
const urlSeg = urlSegments.shift();
if (urlSeg === undefined) return undefined;
else if (predSeg.startsWith(':')) {
const name = predSeg.substring(1);
if (name.length === 0) throw new Error('Invalid path predicate - a segment may not be ":".');
pathParams[name] = decodeURI(urlSeg);
}
else if (predSeg === urlSeg) continue;
else return undefined;
}
if (!hasWildcard && urlSegments.length > 0) return undefined;
return new RestRequest(
this.body, this.headers, this.method, '/' + urlSegments.join('/'),
{ ...this.pathParams, ...pathParams }, this.queryParams
);
}
public static fromMessage(msg: Deno.RequestEvent) {
const raw = msg.request.body;
const headers = {} as Headers;
for (const entry of msg.request.headers.entries()) {
headers[entry[0]] = entry[1];
}
const url = new URL(msg.request.url);
const params = {} as ParamDict;
for (const entry of url.searchParams.entries()) params[entry[0]] = entry[1];
return new RestRequest(raw, headers, msg.request.method, url.pathname, {}, params);
}
}