@oada/well-known-json
Version:
Middleware for JSON Well-Known resources
129 lines (110 loc) • 3.36 kB
text/typescript
/**
* @license
* Copyright 2014-2022 Open Ag Data Alliance
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
const RELATIVE = /^\.\.?\//;
export interface Options {
baseUri?: string;
}
type Collection<T> = Record<string, T> | Iterable<[string, T]>;
type Resources = Collection<Collection<unknown>>;
function isIterable<T>(value: unknown): value is Iterable<T> {
return typeof value === 'object' && Symbol.iterator in (value ?? {});
}
interface Entries<K, V> {
entries(): Iterable<[K, V]>;
}
function hasEntries<K, V>(value: unknown): value is Entries<K, V> {
return (
typeof value === 'object' &&
// @ts-expect-error stuff
typeof value?.entries === 'function'
);
}
function normalize<T>(
collection: Collection<Collection<T>>
): Map<string, Map<string, T>> {
const entries = isIterable(collection)
? collection
: Object.entries(collection);
return new Map(
Array.from(entries, ([k, v]) => [
k,
new Map(isIterable(v) ? v : Object.entries(v)),
])
);
}
export class WellKnownJSON<In = unknown, Out = unknown> {
// eslint-disable-next-line @typescript-eslint/naming-convention
static readonly METHODS = ['OPTIONS', 'GET', 'HEAD'] as const;
#base;
#json;
constructor(resources: Resources, options: Options = {}) {
this.#base = options.baseUri;
this.#json = normalize(resources);
}
/**
* Get a particular well-known resource
*
* @param name
* @param base
* @returns
*/
getResource(name: string, baseIn?: string, ...rest: [In, Out]) {
const base = this.#base ?? baseIn;
const match = this.#json.get(name);
return resourceify(match);
function resourceify(value: unknown): unknown {
if (!value) {
return value;
}
switch (typeof value) {
case 'string': {
return RELATIVE.test(value) ? new URL(value, base).toString() : value;
}
case 'object': {
if (!value) {
return value;
}
if (Array.isArray(value)) {
return value.map((element) => resourceify(element));
}
const entries = hasEntries(value)
? value.entries()
: Object.entries(value);
return Object.fromEntries(
Array.from(entries, ([k, v]) => [k, resourceify(v)])
);
}
case 'function': {
/* Call functions */
return resourceify(value(...rest));
}
default: {
return value;
}
}
}
}
/**
* Add Well-Known resource, merging with any preexisting ones
*/
addResource(uri: string, collection: Collection<unknown>) {
const entries = isIterable(collection)
? collection
: Object.entries(collection);
this.#json.set(uri, new Map([...(this.#json.get(uri) ?? []), ...entries]));
}
}