UNPKG

@zenfs/core

Version:

A filesystem, anywhere

161 lines (140 loc) 5.09 kB
import type { Entries, RequiredKeys } from 'utilium'; import { ErrnoError, Errno } from '../error.js'; import type { FileSystem } from '../filesystem.js'; import { levenshtein } from '../utils.js'; type OptionType = 'string' | 'number' | 'bigint' | 'boolean' | 'symbol' | 'undefined' | 'object' | 'function'; /** * Resolves the type of Backend.options from the options interface */ export type OptionsConfig<T> = { [K in keyof T]: { /** * The basic JavaScript type(s) for this option. */ type: OptionType | readonly OptionType[]; /** * Description of the option. Used in error messages and documentation. */ description: string; /** * Whether or not the option is required (optional can be set to null or undefined). Defaults to false. */ required: K extends RequiredKeys<T> ? true : false; /** * A custom validation function to check if the option is valid. * When async, resolves if valid and rejects if not. * When sync, it will throw an error if not valid. */ validator?(opt: T[K]): void | Promise<void>; }; }; /** * Configuration options shared by backends and `Configuration` */ export interface SharedConfig { /** * If set, disables the sync cache and sync operations on async file systems. */ disableAsyncCache?: boolean; } /** * A backend */ export interface Backend<FS extends FileSystem = FileSystem, TOptions extends object = object> { /** * Create a new instance of the backend */ create(options: TOptions & Partial<SharedConfig>): FS | Promise<FS>; /** * A name to identify the backend. */ name: string; /** * Describes all of the options available for this backend. */ options: OptionsConfig<TOptions>; /** * Whether the backend is available in the current environment. * It supports checking synchronously and asynchronously * * Returns 'true' if this backend is available in the current * environment. For example, a backend using a browser API will return * 'false' if the API is unavailable * */ isAvailable(): boolean | Promise<boolean>; } /** * Gets the options type of a backend * @internal */ export type OptionsOf<T extends Backend> = T extends Backend<FileSystem, infer TOptions> ? TOptions : never; /** * Gets the FileSystem type for a backend * @internal */ export type FilesystemOf<T extends Backend> = T extends Backend<infer FS> ? FS : never; /** @internal */ export function isBackend(arg: unknown): arg is Backend { return arg != null && typeof arg == 'object' && 'isAvailable' in arg && typeof arg.isAvailable == 'function' && 'create' in arg && typeof arg.create == 'function'; } /** * Checks that `options` object is valid for the file system options. * @internal */ export async function checkOptions<T extends Backend>(backend: T, options: Record<string, unknown>): Promise<void> { if (typeof options != 'object' || options === null) { throw new ErrnoError(Errno.EINVAL, 'Invalid options'); } // Check for required options. for (const [optName, opt] of Object.entries(backend.options) as Entries<OptionsConfig<Record<string, any>>>) { const providedValue = options?.[optName]; if (providedValue === undefined || providedValue === null) { if (!opt.required) { continue; } /* Required option not provided. if any incorrect options provided, which ones are close to the provided one? (edit distance 5 === close)*/ const incorrectOptions = Object.keys(options) .filter(o => !(o in backend.options)) .map((a: string) => { return { str: a, distance: levenshtein(optName, a) }; }) .filter(o => o.distance < 5) .sort((a, b) => a.distance - b.distance); throw new ErrnoError( Errno.EINVAL, `${backend.name}: Required option '${optName}' not provided.${ incorrectOptions.length > 0 ? ` You provided '${incorrectOptions[0].str}', did you mean '${optName}'.` : '' }` ); } // Option provided, check type. const typeMatches = Array.isArray(opt.type) ? opt.type.indexOf(typeof providedValue) != -1 : typeof providedValue == opt.type; if (!typeMatches) { throw new ErrnoError( Errno.EINVAL, `${backend.name}: Value provided for option ${optName} is not the proper type. Expected ${ Array.isArray(opt.type) ? `one of {${opt.type.join(', ')}}` : (opt.type as string) }, but received ${typeof providedValue}` ); } if (opt.validator) { await opt.validator(providedValue); } // Otherwise: All good! } } /** * Specifies a file system backend type and its options. * * Individual options can recursively contain BackendConfiguration objects for values that require file systems. * * The configuration for each file system corresponds to that file system's option object passed to its `create()` method. */ export type BackendConfiguration<T extends Backend> = OptionsOf<T> & Partial<SharedConfig> & { backend: T }; /** @internal */ export function isBackendConfig<T extends Backend>(arg: unknown): arg is BackendConfiguration<T> { return arg != null && typeof arg == 'object' && 'backend' in arg && isBackend(arg.backend); }