From 297844fcfd88ffc0a35e82fcbb292d4612f41c36 Mon Sep 17 00:00:00 2001 From: John Vilk Date: Mon, 7 Aug 2017 13:02:10 -0700 Subject: [PATCH] Better file system options checking and error messages Error messages now provide actionable feedback on FS configuration options that may have been mistyped or specified incorrectly. Options objects are now optional for all `Create()` methods (of course, it will fail if the FS requires any options!). Adds tests for the new error messages. Fixes #183. --- karma.config.js | 5 +- src/backend/AsyncMirror.ts | 19 +++- src/backend/Dropbox.ts | 22 +++- src/backend/Emscripten.ts | 11 +- src/backend/FolderAdapter.ts | 17 ++- src/backend/HTML5FS.ts | 34 ++++-- src/backend/InMemory.ts | 15 +-- src/backend/IndexedDB.ts | 23 ++-- src/backend/IsoFS.ts | 32 +++--- src/backend/LocalStorage.ts | 15 +-- src/backend/MountableFileSystem.ts | 11 +- src/backend/OverlayFS.ts | 19 +++- src/backend/WorkerFS.ts | 25 +++- src/backend/XmlHttpRequest.ts | 31 ++++- src/backend/ZipFS.ts | 25 +++- src/core/backends.ts | 31 ++++- src/core/file_system.ts | 33 ++++++ src/core/levenshtein.ts | 102 +++++++++++++++++ src/core/util.ts | 91 ++++++++++++++- src/generic/file_index.ts | 8 +- src/generic/key_value_filesystem.ts | 3 +- src/rollup.config.js | 7 +- src/tslint.json | 12 +- test/rollup.config.js | 6 +- test/tests/general/check-options-test.ts | 138 +++++++++++++++++++++++ 25 files changed, 640 insertions(+), 95 deletions(-) create mode 100644 src/core/levenshtein.ts create mode 100644 test/tests/general/check-options-test.ts diff --git a/karma.config.js b/karma.config.js index ef0ac15d..38610427 100644 --- a/karma.config.js +++ b/karma.config.js @@ -23,7 +23,10 @@ let karmaFiles = [ // Main module and fixtures loader 'test/harness/test.js', // WebWorker script. - { pattern: 'test/harness/factories/workerfs_worker.js', included: false, watched: true } + { pattern: 'test/harness/factories/workerfs_worker.js', included: false, watched: true }, + // Source map support + { pattern: 'src/**/*', included: false, watched: false }, + { pattern: 'test/**/*', included: false, watched: false } ]; // The presence of the Dropbox library dynamically toggles the tests. diff --git a/src/backend/AsyncMirror.ts b/src/backend/AsyncMirror.ts index 22348880..b33bf18e 100644 --- a/src/backend/AsyncMirror.ts +++ b/src/backend/AsyncMirror.ts @@ -1,4 +1,4 @@ -import {FileSystem, SynchronousFileSystem, BFSOneArgCallback, BFSCallback} from '../core/file_system'; +import {FileSystem, SynchronousFileSystem, BFSOneArgCallback, BFSCallback, FileSystemOptions} from '../core/file_system'; import {ApiError, ErrorCode} from '../core/api_error'; import {FileFlag} from '../core/file_flag'; import {File} from '../core/file'; @@ -87,6 +87,19 @@ export interface AsyncMirrorOptions { * ``` */ export default class AsyncMirror extends SynchronousFileSystem implements FileSystem { + public static readonly Name = "AsyncMirror"; + + public static readonly Options: FileSystemOptions = { + sync: { + type: "object", + description: "The synchronous file system to mirror the asynchronous file system to." + }, + async: { + type: "object", + description: "The asynchronous file system to mirror." + } + }; + /** * Constructs and initializes an AsyncMirror file system with the given options. */ @@ -135,11 +148,11 @@ export default class AsyncMirror extends SynchronousFileSystem implements FileSy if (!sync.supportsSynch()) { throw new Error("The first argument to AsyncMirror needs to be a synchronous file system."); } - deprecationMessage(deprecateMsg, "AsyncMirror", { sync: "sync file system instance", async: "async file system instance"}); + deprecationMessage(deprecateMsg, AsyncMirror.Name, { sync: "sync file system instance", async: "async file system instance"}); } public getName(): string { - return "AsyncMirror"; + return AsyncMirror.Name; } public _syncSync(fd: PreloadFile) { diff --git a/src/backend/Dropbox.ts b/src/backend/Dropbox.ts index f43e3bfd..14d47472 100644 --- a/src/backend/Dropbox.ts +++ b/src/backend/Dropbox.ts @@ -1,5 +1,5 @@ import PreloadFile from '../generic/preload_file'; -import {BaseFileSystem, FileSystem, BFSOneArgCallback, BFSCallback} from '../core/file_system'; +import {BaseFileSystem, FileSystem, BFSOneArgCallback, BFSCallback, FileSystemOptions} from '../core/file_system'; import {FileFlag} from '../core/file_flag'; import {default as Stats, FileType} from '../core/node_fs_stats'; import {ApiError, ErrorCode} from '../core/api_error'; @@ -352,6 +352,22 @@ export interface DropboxFileSystemOptions { * NOTE: You must use the v0.10 version of the [Dropbox JavaScript SDK](https://www.npmjs.com/package/dropbox). */ export default class DropboxFileSystem extends BaseFileSystem implements FileSystem { + public static readonly Name = "Dropbox"; + + public static readonly Options: FileSystemOptions = { + client: { + type: "object", + description: "An *authenticated* Dropbox client. Must be from the 0.10 JS SDK.", + validator: (opt: Dropbox.Client, cb: BFSOneArgCallback): void => { + if (opt.isAuthenticated && opt.isAuthenticated()) { + cb(); + } else { + cb(new ApiError(ErrorCode.EINVAL, `'client' option must be an authenticated Dropbox client from the v0.10 JS SDK.`)); + } + } + } + }; + /** * Creates a new DropboxFileSystem instance with the given options. * Must be given an *authenticated* DropboxJS client from the old v0.10 version of the Dropbox JS SDK. @@ -378,12 +394,12 @@ export default class DropboxFileSystem extends BaseFileSystem implements FileSys constructor(client: Dropbox.Client, deprecateMsg = true) { super(); this._client = new CachedDropboxClient(client); - deprecationMessage(deprecateMsg, "Dropbox", { client: "authenticated dropbox client instance" }); + deprecationMessage(deprecateMsg, DropboxFileSystem.Name, { client: "authenticated dropbox client instance" }); constructErrorCodeLookup(); } public getName(): string { - return 'Dropbox'; + return DropboxFileSystem.Name; } public isReadOnly(): boolean { diff --git a/src/backend/Emscripten.ts b/src/backend/Emscripten.ts index 1dfe6258..f1ca13f6 100644 --- a/src/backend/Emscripten.ts +++ b/src/backend/Emscripten.ts @@ -1,4 +1,4 @@ -import {SynchronousFileSystem, BFSOneArgCallback, BFSCallback, BFSThreeArgCallback} from '../core/file_system'; +import {SynchronousFileSystem, BFSOneArgCallback, BFSCallback, BFSThreeArgCallback, FileSystemOptions} from '../core/file_system'; import {default as Stats, FileType} from '../core/node_fs_stats'; import {FileFlag} from '../core/file_flag'; import {BaseFile, File} from '../core/file'; @@ -192,6 +192,15 @@ export interface EmscriptenFileSystemOptions { * Mounts an Emscripten file system into the BrowserFS file system. */ export default class EmscriptenFileSystem extends SynchronousFileSystem { + public static readonly Name = "EmscriptenFileSystem"; + + public static readonly Options: FileSystemOptions = { + FS: { + type: "object", + description: "The Emscripten file system to use (the `FS` variable)" + } + }; + /** * Create an EmscriptenFileSystem instance with the given options. */ diff --git a/src/backend/FolderAdapter.ts b/src/backend/FolderAdapter.ts index f4265cd7..0ae14a24 100644 --- a/src/backend/FolderAdapter.ts +++ b/src/backend/FolderAdapter.ts @@ -1,4 +1,4 @@ -import {BaseFileSystem, FileSystem, BFSCallback} from '../core/file_system'; +import {BaseFileSystem, FileSystem, BFSCallback, FileSystemOptions} from '../core/file_system'; import * as path from 'path'; import {ApiError} from '../core/api_error'; @@ -31,6 +31,19 @@ export interface FolderAdapterOptions { * ``` */ export default class FolderAdapter extends BaseFileSystem implements FileSystem { + public static readonly Name = "FolderAdapter"; + + public static readonly Options: FileSystemOptions = { + folder: { + type: "string", + description: "The folder to use as the root directory" + }, + wrapped: { + type: "object", + description: "The file system to wrap" + } + }; + /** * Creates a FolderAdapter instance with the given options. */ @@ -46,8 +59,6 @@ export default class FolderAdapter extends BaseFileSystem implements FileSystem /** * Wraps a file system, and uses the given folder as its root. * - * - * * @param folder The folder to use as the root directory. * @param wrapped The file system to wrap. */ diff --git a/src/backend/HTML5FS.ts b/src/backend/HTML5FS.ts index 12b8a941..0ebcdd9a 100644 --- a/src/backend/HTML5FS.ts +++ b/src/backend/HTML5FS.ts @@ -1,5 +1,5 @@ import PreloadFile from '../generic/preload_file'; -import {BaseFileSystem, FileSystem as IFileSystem, BFSOneArgCallback, BFSCallback} from '../core/file_system'; +import {BaseFileSystem, FileSystem as IFileSystem, BFSOneArgCallback, BFSCallback, FileSystemOptions} from '../core/file_system'; import {ApiError, ErrorCode} from '../core/api_error'; import {FileFlag, ActionType} from '../core/file_flag'; import {default as Stats, FileType} from '../core/node_fs_stats'; @@ -156,6 +156,21 @@ export interface HTML5FSOptions { * only available in Chrome. */ export default class HTML5FS extends BaseFileSystem implements IFileSystem { + public static readonly Name = "HTML5FS"; + + public static readonly Options: FileSystemOptions = { + size: { + type: "number", + optional: true, + description: "Storage quota to request, in megabytes. Allocated value may be less. Defaults to 5." + }, + type: { + type: "number", + optional: true, + description: "window.PERSISTENT or window.TEMPORARY. Defaults to PERSISTENT." + } + }; + /** * Creates an HTML5FS instance with the given options. */ @@ -188,11 +203,11 @@ export default class HTML5FS extends BaseFileSystem implements IFileSystem { // Convert MB to bytes. this.size = 1024 * 1024 * size; this.type = type; - deprecationMessage(deprecateMsg, "HTML5FS", {size: size, type: type}); + deprecationMessage(deprecateMsg, HTML5FS.Name, {size: size, type: type}); } public getName(): string { - return 'HTML5 FileSystem'; + return HTML5FS.Name; } public isReadOnly(): boolean { @@ -436,14 +451,15 @@ export default class HTML5FS extends BaseFileSystem implements IFileSystem { */ public readdir(path: string, cb: BFSCallback): void { this._readdir(path, (e: ApiError, entries?: Entry[]): void => { - if (e) { + if (entries) { + const rv: string[] = []; + for (const entry of entries) { + rv.push(entry.name); + } + cb(null, rv); + } else { return cb(e); } - const rv: string[] = []; - for (let i = 0; i < entries!.length; i++) { - rv.push(entries![i].name); - } - cb(null, rv); }); } diff --git a/src/backend/InMemory.ts b/src/backend/InMemory.ts index f0d074c1..eda98ade 100644 --- a/src/backend/InMemory.ts +++ b/src/backend/InMemory.ts @@ -1,4 +1,4 @@ -import {BFSCallback} from '../core/file_system'; +import {BFSCallback, FileSystemOptions} from '../core/file_system'; import {SyncKeyValueStore, SimpleSyncStore, SimpleSyncRWTransaction, SyncKeyValueRWTransaction, SyncKeyValueFileSystem} from '../generic/key_value_filesystem'; /** @@ -7,7 +7,7 @@ import {SyncKeyValueStore, SimpleSyncStore, SimpleSyncRWTransaction, SyncKeyValu export class InMemoryStore implements SyncKeyValueStore, SimpleSyncStore { private store: { [key: string]: Buffer } = {}; - public name() { return 'In-memory'; } + public name() { return InMemoryFileSystem.Name; } public clear() { this.store = {}; } public beginTransaction(type: string): SyncKeyValueRWTransaction { @@ -36,14 +36,15 @@ export class InMemoryStore implements SyncKeyValueStore, SimpleSyncStore { * Files are not persisted across page loads. */ export default class InMemoryFileSystem extends SyncKeyValueFileSystem { + public static readonly Name = "InMemory"; + + public static readonly Options: FileSystemOptions = {}; + /** * Creates an InMemoryFileSystem instance. */ - public static Create(cb: BFSCallback): void; - public static Create(options: any, cb: BFSCallback): void; - public static Create(options: any, cb?: any): void { - const normalizedCb: BFSCallback = cb ? cb : options; - normalizedCb(null, new InMemoryFileSystem()); + public static Create(options: any, cb: BFSCallback): void { + cb(null, new InMemoryFileSystem()); } constructor() { super({ store: new InMemoryStore() }); diff --git a/src/backend/IndexedDB.ts b/src/backend/IndexedDB.ts index fca8300f..4ce3584c 100644 --- a/src/backend/IndexedDB.ts +++ b/src/backend/IndexedDB.ts @@ -1,4 +1,4 @@ -import {BFSOneArgCallback, BFSCallback} from '../core/file_system'; +import {BFSOneArgCallback, BFSCallback, FileSystemOptions} from '../core/file_system'; import {AsyncKeyValueROTransaction, AsyncKeyValueRWTransaction, AsyncKeyValueStore, AsyncKeyValueFileSystem} from '../generic/key_value_filesystem'; import {ApiError, ErrorCode} from '../core/api_error'; import global from '../core/global'; @@ -155,7 +155,7 @@ export class IndexedDBStore implements AsyncKeyValueStore { } public name(): string { - return "IndexedDB - " + this.storeName; + return IndexedDBFileSystem.Name + " - " + this.storeName; } public clear(cb: BFSOneArgCallback): void { @@ -201,15 +201,22 @@ export interface IndexedDBFileSystemOptions { * A file system that uses the IndexedDB key value file system. */ export default class IndexedDBFileSystem extends AsyncKeyValueFileSystem { + public static readonly Name = "IndexedDB"; + + public static readonly Options: FileSystemOptions = { + storeName: { + type: "string", + optional: true, + description: "The name of this file system. You can have multiple IndexedDB file systems operating at once, but each must have a different name." + } + }; + /** * Constructs an IndexedDB file system with the given options. */ - public static Create(cb: BFSCallback): void; - public static Create(opts: IndexedDBFileSystemOptions, cb: BFSCallback): void; - public static Create(opts: any, cb?: any): void { - const normalizedCb = cb ? cb : opts; + public static Create(opts: IndexedDBFileSystemOptions, cb: BFSCallback): void { // tslint:disable-next-line:no-unused-new - new IndexedDBFileSystem(normalizedCb, cb && opts ? opts['storeName'] : undefined, false); + new IndexedDBFileSystem(cb, opts.storeName, false); // tslint:enable-next-line:no-unused-new } public static isAvailable(): boolean { @@ -244,6 +251,6 @@ export default class IndexedDBFileSystem extends AsyncKeyValueFileSystem { }); } }, storeName); - deprecationMessage(deprecateMsg, "IndexedDB", {storeName: storeName}); + deprecationMessage(deprecateMsg, IndexedDBFileSystem.Name, {storeName: storeName}); } } diff --git a/src/backend/IsoFS.ts b/src/backend/IsoFS.ts index 4e1a5983..11d5b647 100644 --- a/src/backend/IsoFS.ts +++ b/src/backend/IsoFS.ts @@ -1,10 +1,10 @@ import {ApiError, ErrorCode} from '../core/api_error'; import {default as Stats, FileType} from '../core/node_fs_stats'; -import {SynchronousFileSystem, FileSystem, BFSCallback} from '../core/file_system'; +import {SynchronousFileSystem, FileSystem, BFSCallback, FileSystemOptions} from '../core/file_system'; import {File} from '../core/file'; import {FileFlag, ActionType} from '../core/file_flag'; import {NoSyncFile} from '../generic/preload_file'; -import {copyingSlice, deprecationMessage} from '../core/util'; +import {copyingSlice, deprecationMessage, bufferValidator} from '../core/util'; import * as path from 'path'; /** @@ -447,12 +447,10 @@ abstract class DirectoryRecord { let p = ""; const entries = this.getSUEntries(isoData); const getStr = this._getGetString(); - for (let i = 0; i < entries.length; i++) { - const entry = entries[i]; + for (const entry of entries) { if (entry instanceof SLEntry) { const components = entry.componentRecords(); - for (let j = 0; j < components.length; j++) { - const component = components[j]; + for (const component of components) { const flags = component.flags(); if (flags & SLComponentFlags.CURRENT) { p += "./"; @@ -514,8 +512,7 @@ abstract class DirectoryRecord { } let str = ''; const getString = this._getGetString(); - for (let i = 0; i < nmEntries.length; i++) { - const e = nmEntries[i]; + for (const e of nmEntries) { str += e.name(getString); if (!(e.flags() & NMFlags.CONTINUE)) { break; @@ -1157,6 +1154,16 @@ export interface IsoFSOptions { * * Microsoft Joliet and Rock Ridge extensions to the ISO9660 standard */ export default class IsoFS extends SynchronousFileSystem implements FileSystem { + public static readonly Name = "IsoFS"; + + public static readonly Options: FileSystemOptions = { + data: { + type: "object", + description: "The ISO file in a buffer", + validator: bufferValidator + } + }; + /** * Creates an IsoFS instance with the given options. */ @@ -1190,7 +1197,7 @@ export default class IsoFS extends SynchronousFileSystem implements FileSystem { constructor(data: Buffer, name: string = "", deprecateMsg = true) { super(); this._data = data; - deprecationMessage(deprecateMsg, "IsoFS", {data: "ISO data as a Buffer", name: name}); + deprecationMessage(deprecateMsg, IsoFS.Name, {data: "ISO data as a Buffer", name: name}); // Skip first 16 sectors. let vdTerminatorFound = false; let i = 16 * 2048; @@ -1326,9 +1333,9 @@ export default class IsoFS extends SynchronousFileSystem implements FileSystem { } const components = path.split('/').slice(1); let dir = this._root; - for (let i = 0; i < components.length; i++) { + for (const component of components) { if (dir.isDirectory(this._data)) { - dir = dir.getDirectory(this._data).getRecord(components[i]); + dir = dir.getDirectory(this._data).getRecord(component); if (!dir) { return null; } @@ -1356,8 +1363,7 @@ export default class IsoFS extends SynchronousFileSystem implements FileSystem { let ctime = date; if (record.hasRockRidge()) { const entries = record.getSUEntries(this._data); - for (let i = 0; i < entries.length; i++) { - const entry = entries[i]; + for (const entry of entries) { if (entry instanceof PXEntry) { mode = entry.mode(); } else if (entry instanceof TFEntry) { diff --git a/src/backend/LocalStorage.ts b/src/backend/LocalStorage.ts index 23464b2c..fb94e6fc 100644 --- a/src/backend/LocalStorage.ts +++ b/src/backend/LocalStorage.ts @@ -1,4 +1,4 @@ -import {BFSCallback} from '../core/file_system'; +import {BFSCallback, FileSystemOptions} from '../core/file_system'; import {SyncKeyValueStore, SimpleSyncStore, SyncKeyValueFileSystem, SimpleSyncRWTransaction, SyncKeyValueRWTransaction} from '../generic/key_value_filesystem'; import {ApiError, ErrorCode} from '../core/api_error'; import global from '../core/global'; @@ -30,7 +30,7 @@ if (!Buffer.isEncoding(binaryEncoding)) { */ export class LocalStorageStore implements SyncKeyValueStore, SimpleSyncStore { public name(): string { - return 'LocalStorage'; + return LocalStorageFileSystem.Name; } public clear(): void { @@ -82,14 +82,15 @@ export class LocalStorageStore implements SyncKeyValueStore, SimpleSyncStore { * LocalStorageStore to our SyncKeyValueFileSystem. */ export default class LocalStorageFileSystem extends SyncKeyValueFileSystem { + public static readonly Name = "LocalStorage"; + + public static readonly Options: FileSystemOptions = {}; + /** * Creates a LocalStorageFileSystem instance. */ - public static Create(cb: BFSCallback): void; - public static Create(options: any, cb: BFSCallback): void; - public static Create(options: any, cb?: any): void { - const normalizedCb: BFSCallback = cb ? cb : options; - normalizedCb(null, new LocalStorageFileSystem()); + public static Create(options: any, cb: BFSCallback): void { + cb(null, new LocalStorageFileSystem()); } public static isAvailable(): boolean { return typeof global.localStorage !== 'undefined'; diff --git a/src/backend/MountableFileSystem.ts b/src/backend/MountableFileSystem.ts index 6a488098..d2a1dae5 100644 --- a/src/backend/MountableFileSystem.ts +++ b/src/backend/MountableFileSystem.ts @@ -1,4 +1,4 @@ -import {FileSystem, BaseFileSystem, BFSOneArgCallback, BFSCallback} from '../core/file_system'; +import {FileSystem, BaseFileSystem, BFSOneArgCallback, BFSCallback, FileSystemOptions} from '../core/file_system'; import InMemoryFileSystem from './InMemory'; import {ApiError, ErrorCode} from '../core/api_error'; import fs from '../core/node_fs'; @@ -58,6 +58,10 @@ export interface MountableFileSystemOptions { * With no mounted file systems, `MountableFileSystem` acts as a simple `InMemory` filesystem. */ export default class MountableFileSystem extends BaseFileSystem implements FileSystem { + public static readonly Name = "MountableFileSystem"; + + public static readonly Options: FileSystemOptions = {}; + /** * Creates a MountableFileSystem instance with the given options. */ @@ -151,7 +155,7 @@ export default class MountableFileSystem extends BaseFileSystem implements FileS // Global information methods public getName(): string { - return 'MountableFileSystem'; + return MountableFileSystem.Name; } public diskSpace(path: string, cb: (total: number, free: number) => void): void { @@ -397,8 +401,7 @@ const fsCmdMap = [ for (let i = 0; i < fsCmdMap.length; i++) { const cmds = fsCmdMap[i]; - for (let j = 0; j < cmds.length; j++) { - const fnName = cmds[j]; + for (const fnName of cmds) { ( MountableFileSystem.prototype)[fnName] = defineFcn(fnName, false, i + 1); ( MountableFileSystem.prototype)[fnName + 'Sync'] = defineFcn(fnName + 'Sync', true, i + 1); } diff --git a/src/backend/OverlayFS.ts b/src/backend/OverlayFS.ts index 96285015..ba90c383 100644 --- a/src/backend/OverlayFS.ts +++ b/src/backend/OverlayFS.ts @@ -1,4 +1,4 @@ -import {FileSystem, BaseFileSystem, BFSOneArgCallback, BFSCallback} from '../core/file_system'; +import {FileSystem, BaseFileSystem, BFSOneArgCallback, BFSCallback, FileSystemOptions} from '../core/file_system'; import {ApiError, ErrorCode} from '../core/api_error'; import {FileFlag, ActionType} from '../core/file_flag'; import {File} from '../core/file'; @@ -119,7 +119,7 @@ export class UnlockedOverlayFS extends BaseFileSystem implements FileSystem { } public getName() { - return "OverlayFS"; + return OverlayFS.Name; } /** @@ -987,6 +987,19 @@ export interface OverlayFSOptions { * file system. */ export default class OverlayFS extends LockedFS { + public static readonly Name = "OverlayFS"; + + public static readonly Options: FileSystemOptions = { + writable: { + type: "object", + description: "The file system to write modified files to." + }, + readable: { + type: "object", + description: "The file system that initially populates this file system." + } + }; + /** * Constructs and initializes an OverlayFS instance with the given options. */ @@ -1011,7 +1024,7 @@ export default class OverlayFS extends LockedFS { */ constructor(writable: FileSystem, readable: FileSystem, deprecateMsg = true) { super(new UnlockedOverlayFS(writable, readable)); - deprecationMessage(deprecateMsg, "OverlayFS", {readable: "readable file system", writable: "writable file system"}); + deprecationMessage(deprecateMsg, OverlayFS.Name, {readable: "readable file system", writable: "writable file system"}); } /** diff --git a/src/backend/WorkerFS.ts b/src/backend/WorkerFS.ts index fed3433c..70fb68a7 100644 --- a/src/backend/WorkerFS.ts +++ b/src/backend/WorkerFS.ts @@ -1,5 +1,5 @@ -import {BaseFileSystem, FileSystem, BFSOneArgCallback, BFSCallback} from '../core/file_system'; -import {ApiError} from '../core/api_error'; +import {BaseFileSystem, FileSystem, BFSOneArgCallback, BFSCallback, FileSystemOptions} from '../core/file_system'; +import {ApiError, ErrorCode} from '../core/api_error'; import {FileFlag} from '../core/file_flag'; import {buffer2ArrayBuffer, arrayBuffer2Buffer, emptyBuffer, deprecationMessage} from '../core/util'; import {File, BaseFile} from '../core/file'; @@ -489,6 +489,23 @@ export interface WorkerFSOptions { * of the configuration option of the remote FS. */ export default class WorkerFS extends BaseFileSystem implements FileSystem { + public static readonly Name = "WorkerFS"; + + public static readonly Options: FileSystemOptions = { + worker: { + type: "object", + description: "The target worker that you want to connect to, or the current worker if in a worker context.", + validator: function(v: object, cb: BFSOneArgCallback): void { + // Check for a `postMessage` function. + if (( v)['postMessage']) { + cb(); + } else { + cb(new ApiError(ErrorCode.EINVAL, `option must be a Web Worker instance.`)); + } + } + } + }; + public static Create(opts: WorkerFSOptions, cb: BFSCallback): void { const fs = new WorkerFS(opts.worker, false); fs.initialize(() => { @@ -683,7 +700,7 @@ export default class WorkerFS extends BaseFileSystem implements FileSystem { constructor(worker: Worker, deprecateMsg = true) { super(); this._worker = worker; - deprecationMessage(deprecateMsg, "WorkerFS", {worker: "Web Worker instance"}); + deprecationMessage(deprecateMsg, WorkerFS.Name, {worker: "Web Worker instance"}); this._worker.addEventListener('message', (e: MessageEvent) => { const resp: object = e.data; if (isAPIResponse(resp)) { @@ -700,7 +717,7 @@ export default class WorkerFS extends BaseFileSystem implements FileSystem { } public getName(): string { - return 'WorkerFS'; + return WorkerFS.Name; } /** diff --git a/src/backend/XmlHttpRequest.ts b/src/backend/XmlHttpRequest.ts index 431e2fed..81fda452 100644 --- a/src/backend/XmlHttpRequest.ts +++ b/src/backend/XmlHttpRequest.ts @@ -1,4 +1,4 @@ -import {BaseFileSystem, FileSystem, BFSCallback} from '../core/file_system'; +import {BaseFileSystem, FileSystem, BFSCallback, FileSystemOptions} from '../core/file_system'; import {ApiError, ErrorCode} from '../core/api_error'; import {FileFlag, ActionType} from '../core/file_flag'; import {copyingSlice, deprecationMessage} from '../core/util'; @@ -26,10 +26,11 @@ function tryToString(buff: Buffer, encoding: string, cb: BFSCallback) { * Configuration options for an XmlHttpRequest file system. */ export interface XmlHttpRequestOptions { - // URL to a file index as a JSON file or the file index object itself, generated with the make_xhrfs_index script - index: string | object; + // URL to a file index as a JSON file or the file index object itself, generated with the make_xhrfs_index script. + // Defaults to `index.json`. + index?: string | object; // Used as the URL prefix for fetched files. - // Default: Fetch files relative to `url`. + // Default: Fetch files relative to the index. baseUrl?: string; } @@ -62,10 +63,28 @@ export interface XmlHttpRequestOptions { * *This example has the folder `/home/jvilk` with subfile `someFile.txt` and subfolder `someDir`.* */ export default class XmlHttpRequest extends BaseFileSystem implements FileSystem { + public static readonly Name = "XmlHttpRequest"; + + public static readonly Options: FileSystemOptions = { + index: { + type: ["string", "object"], + optional: true, + description: "URL to a file index as a JSON file or the file index object itself, generated with the make_xhrfs_index script. Defaults to `index.json`." + }, + baseUrl: { + type: "string", + optional: true, + description: "Used as the URL prefix for fetched files. Default: Fetch files relative to the index." + } + }; + /** * Construct an XmlHttpRequest file system backend with the given options. */ public static Create(opts: XmlHttpRequestOptions, cb: BFSCallback): void { + if (opts.index === undefined) { + opts.index = `index.json`; + } if (typeof(opts.index) === "string") { XmlHttpRequest.FromURL(opts.index, cb, opts.baseUrl, false); } else { @@ -133,7 +152,7 @@ export default class XmlHttpRequest extends BaseFileSystem implements FileSystem } else { listing = listingUrlOrObj; } - deprecationMessage(deprecateMsg, "XmlHttpRequest", { index: typeof(listingUrlOrObj) === "string" ? listingUrlOrObj : "file index as an object", baseUrl: prefixUrl}); + deprecationMessage(deprecateMsg, XmlHttpRequest.Name, { index: typeof(listingUrlOrObj) === "string" ? listingUrlOrObj : "file index as an object", baseUrl: prefixUrl}); this._index = FileIndex.fromListing(listing); } @@ -145,7 +164,7 @@ export default class XmlHttpRequest extends BaseFileSystem implements FileSystem } public getName(): string { - return 'XmlHttpRequest'; + return XmlHttpRequest.Name; } public diskSpace(path: string, cb: (total: number, free: number) => void): void { diff --git a/src/backend/ZipFS.ts b/src/backend/ZipFS.ts index ff14cf95..e20ffd3e 100644 --- a/src/backend/ZipFS.ts +++ b/src/backend/ZipFS.ts @@ -1,10 +1,10 @@ import {ApiError, ErrorCode} from '../core/api_error'; import {default as Stats, FileType} from '../core/node_fs_stats'; -import {SynchronousFileSystem, FileSystem, BFSCallback} from '../core/file_system'; +import {SynchronousFileSystem, FileSystem, BFSCallback, FileSystemOptions} from '../core/file_system'; import {File} from '../core/file'; import {FileFlag, ActionType} from '../core/file_flag'; import {NoSyncFile} from '../generic/preload_file'; -import {Arrayish, arrayish2Buffer, copyingSlice, deprecationMessage} from '../core/util'; +import {Arrayish, arrayish2Buffer, copyingSlice, deprecationMessage, bufferValidator} from '../core/util'; import ExtendedASCII from '../generic/extended_ascii'; import setImmediate from '../generic/setImmediate'; /** @@ -522,9 +522,22 @@ export interface ZipFSOptions { * This isn't that bad, so we might do this at a later date. */ export default class ZipFS extends SynchronousFileSystem implements FileSystem { - /* tslint:disable:variable-name */ + public static readonly Name = "ZipFS"; + + public static readonly Options: FileSystemOptions = { + zipData: { + type: "object", + description: "The zip file as a Buffer object.", + validator: bufferValidator + }, + name: { + type: "string", + optional: true, + description: "The name of the zip file (optional)." + } + }; + public static readonly CompressionMethod = CompressionMethod; - /* tslint:enable:variable-name */ /** * Constructs a ZipFS instance with the given options. @@ -638,7 +651,7 @@ export default class ZipFS extends SynchronousFileSystem implements FileSystem { */ constructor(input: Buffer | ZipTOC, private name: string = '', deprecateMsg = true) { super(); - deprecationMessage(deprecateMsg, "ZipFS", {zipData: "zip data as a Buffer", name: name}); + deprecationMessage(deprecateMsg, ZipFS.Name, {zipData: "zip data as a Buffer", name: name}); if (input instanceof ZipTOC) { this._index = input.index; this._directoryEntries = input.directoryEntries; @@ -651,7 +664,7 @@ export default class ZipFS extends SynchronousFileSystem implements FileSystem { } public getName(): string { - return 'ZipFS' + (this.name !== '' ? ' ' + this.name : ''); + return ZipFS.Name + (this.name !== '' ? ` ${this.name}` : ''); } /** diff --git a/src/core/backends.ts b/src/core/backends.ts index 2197f204..a5c64143 100644 --- a/src/core/backends.ts +++ b/src/core/backends.ts @@ -1,3 +1,6 @@ +import {FileSystemConstructor, BFSCallback, FileSystem} from './file_system'; +import {ApiError} from './api_error'; +import {checkOptions} from './util'; import AsyncMirror from '../backend/AsyncMirror'; import Dropbox from '../backend/Dropbox'; import Emscripten from '../backend/Emscripten'; @@ -12,10 +15,34 @@ import WorkerFS from '../backend/WorkerFS'; import XmlHttpRequest from '../backend/XmlHttpRequest'; import ZipFS from '../backend/ZipFS'; import IsoFS from '../backend/IsoFS'; -/* tslint:disable:variable-name */ + +// Monkey-patch `Create` functions to check options before file system initialization. +[AsyncMirror, Dropbox, Emscripten, FolderAdapter, HTML5FS, InMemory, IndexedDB, IsoFS, LocalStorage, MountableFileSystem, OverlayFS, WorkerFS, XmlHttpRequest, ZipFS].forEach((fsType: FileSystemConstructor) => { + const create = fsType.Create; + fsType.Create = function(opts?: any, cb?: BFSCallback): void { + const oneArg = typeof(opts) === "function"; + const normalizedCb = oneArg ? opts : cb; + const normalizedOpts = oneArg ? {} : opts; + + function wrappedCb(e?: ApiError): void { + if (e) { + normalizedCb(e); + } else { + create.call(fsType, normalizedOpts, normalizedCb); + } + } + + checkOptions(fsType, normalizedOpts, wrappedCb); + }; +}); + /** * @hidden */ const Backends = { AsyncMirror, Dropbox, Emscripten, FolderAdapter, HTML5FS, InMemory, IndexedDB, IsoFS, LocalStorage, MountableFileSystem, OverlayFS, WorkerFS, XmlHttpRequest, ZipFS }; +// Make sure all backends cast to FileSystemConstructor (for type checking) +const _: {[name: string]: FileSystemConstructor} = Backends; +// tslint:disable-next-line:no-unused-expression +_; +// tslint:enable-next-line:no-unused-expression export default Backends; -/* tslint:enable:variable-name */ diff --git a/src/core/file_system.ts b/src/core/file_system.ts index 83e2e2ba..1de93a21 100644 --- a/src/core/file_system.ts +++ b/src/core/file_system.ts @@ -326,10 +326,43 @@ export interface FileSystem { readlinkSync(p: string): string; } +/** + * Describes a file system option. + */ +export interface FileSystemOption { + // The basic JavaScript type(s) for this option. + type: string | string[]; + // Whether or not the option is optional (e.g., can be set to null or undefined). + // Defaults to `false`. + optional?: boolean; + // Description of the option. Used in error messages and documentation. + description: string; + // A custom validation function to check if the option is valid. + // Calls the callback with an error object on an error. + // (Can call callback synchronously.) + // Defaults to `(opt, cb) => cb()`. + validator?(opt: T, cb: BFSOneArgCallback): void; +} + +/** + * Describes all of the options available in a file system. + */ +export interface FileSystemOptions { + [name: string]: FileSystemOption; +} + /** * Contains typings for static functions on the file system constructor. */ export interface FileSystemConstructor { + /** + * **Core**: Name to identify this particular file system. + */ + Name: string; + /** + * **Core**: Describes all of the options available for this file system. + */ + Options: FileSystemOptions; /** * **Core**: Creates a file system of this given type with the given * options. diff --git a/src/core/levenshtein.ts b/src/core/levenshtein.ts new file mode 100644 index 00000000..2a5c80e0 --- /dev/null +++ b/src/core/levenshtein.ts @@ -0,0 +1,102 @@ +/* + * Levenshtein distance, from the `js-levenshtein` NPM module. + * Copied here to avoid complexity of adding another CommonJS module dependency. + */ + +function _min(d0: number, d1: number, d2: number, bx: number, ay: number): number { + return d0 < d1 || d2 < d1 + ? d0 > d2 + ? d2 + 1 + : d0 + 1 + : bx === ay + ? d1 + : d1 + 1; +} + +/** + * Calculates levenshtein distance. + * @param a + * @param b + */ +export default function levenshtein(a: string, b: string): number { + if (a === b) { + return 0; + } + + if (a.length > b.length) { + const tmp = a; + a = b; + b = tmp; + } + + let la = a.length; + let lb = b.length; + + while (la > 0 && (a.charCodeAt(la - 1) === b.charCodeAt(lb - 1))) { + la--; + lb--; + } + + let offset = 0; + + while (offset < la && (a.charCodeAt(offset) === b.charCodeAt(offset))) { + offset++; + } + + la -= offset; + lb -= offset; + + if (la === 0 || lb === 1) { + return lb; + } + + const vector = new Array(la << 1); + + for (let y = 0; y < la; ) { + vector[la + y] = a.charCodeAt(offset + y); + vector[y] = ++y; + } + + let x: number; + let d0: number; + let d1: number; + let d2: number; + let d3: number; + for (x = 0; (x + 3) < lb; ) { + const bx0 = b.charCodeAt(offset + (d0 = x)); + const bx1 = b.charCodeAt(offset + (d1 = x + 1)); + const bx2 = b.charCodeAt(offset + (d2 = x + 2)); + const bx3 = b.charCodeAt(offset + (d3 = x + 3)); + let dd = (x += 4); + for (let y = 0; y < la; ) { + const ay = vector[la + y]; + const dy = vector[y]; + d0 = _min(dy, d0, d1, bx0, ay); + d1 = _min(d0, d1, d2, bx1, ay); + d2 = _min(d1, d2, d3, bx2, ay); + dd = _min(d2, d3, dd, bx3, ay); + vector[y++] = dd; + d3 = d2; + d2 = d1; + d1 = d0; + d0 = dy; + } + } + + let dd: number = 0; + for (; x < lb; ) { + const bx0 = b.charCodeAt(offset + (d0 = x)); + dd = ++x; + for (let y = 0; y < la; y++) { + const dy = vector[y]; + vector[y] = dd = dy < d0 || dd < d0 + ? dy > dd ? dd + 1 : dy + 1 + : bx0 === vector[la + y] + ? d0 + : d0 + 1; + d0 = dy; + } + } + + return dd; +} diff --git a/src/core/util.ts b/src/core/util.ts index e0aeb302..d7392437 100644 --- a/src/core/util.ts +++ b/src/core/util.ts @@ -1,7 +1,9 @@ /** * Grab bag of utility functions used across the code. */ -import {FileSystem} from './file_system'; +import {FileSystem, BFSOneArgCallback, FileSystemConstructor} from './file_system'; +import {ErrorCode, ApiError} from './api_error'; +import levenshtein from './levenshtein'; import * as path from 'path'; export function deprecationMessage(print: boolean, fsName: string, opts: any): void { @@ -163,3 +165,90 @@ export function emptyBuffer(): Buffer { } return emptyBuff = Buffer.alloc(0); } + +/** + * Option validator for a Buffer file system option. + * @hidden + */ +export function bufferValidator(v: object, cb: BFSOneArgCallback): void { + if (Buffer.isBuffer(v)) { + cb(); + } else { + cb(new ApiError(ErrorCode.EINVAL, `option must be a Buffer.`)); + } +} + +/** + * Checks that the given options object is valid for the file system options. + * @hidden + */ +export function checkOptions(fsType: FileSystemConstructor, opts: any, cb: BFSOneArgCallback): void { + const optsInfo = fsType.Options; + const fsName = fsType.Name; + + let pendingValidators = 0; + let callbackCalled = false; + let loopEnded = false; + function validatorCallback(e?: ApiError): void { + if (!callbackCalled) { + if (e) { + callbackCalled = true; + cb(e); + } + pendingValidators--; + if (pendingValidators === 0 && loopEnded) { + cb(); + } + } + } + + // Check for required options. + for (const optName in optsInfo) { + if (optsInfo.hasOwnProperty(optName)) { + const opt = optsInfo[optName]; + const providedValue = opts[optName]; + + if (providedValue === undefined || providedValue === null) { + if (!opt.optional) { + // Required option, not provided. + // Any incorrect options provided? Which ones are close to the provided one? + // (edit distance 5 === close) + const incorrectOptions = Object.keys(opts).filter((o) => !(o in optsInfo)).map((a: string) => { + return {str: a, distance: levenshtein(optName, a)}; + }).filter((o) => o.distance < 5).sort((a, b) => a.distance - b.distance); + // Validators may be synchronous. + if (callbackCalled) { + return; + } + callbackCalled = true; + return cb(new ApiError(ErrorCode.EINVAL, `[${fsName}] Required option '${optName}' not provided.${incorrectOptions.length > 0 ? ` You provided unrecognized option '${incorrectOptions[0].str}'; perhaps you meant to type '${optName}'.` : ''}\nOption description: ${opt.description}`)); + } + // Else: Optional option, not provided. That is OK. + } else { + // Option provided! Check type. + let typeMatches = false; + if (Array.isArray(opt.type)) { + typeMatches = opt.type.indexOf(typeof(providedValue)) !== -1; + } else { + typeMatches = typeof(providedValue) === opt.type; + } + if (!typeMatches) { + // Validators may be synchronous. + if (callbackCalled) { + return; + } + callbackCalled = true; + return cb(new ApiError(ErrorCode.EINVAL, `[${fsName}] Value provided for option ${optName} is not the proper type. Expected ${Array.isArray(opt.type) ? `one of {${opt.type.join(", ")}}` : opt.type}, but received ${typeof(providedValue)}\nOption description: ${opt.description}`)); + } else if (opt.validator) { + pendingValidators++; + opt.validator(providedValue, validatorCallback); + } + // Otherwise: All good! + } + } + } + loopEnded = true; + if (pendingValidators === 0 && !callbackCalled) { + cb(); + } +} diff --git a/src/generic/file_index.ts b/src/generic/file_index.ts index 711b0e97..1dd5de59 100644 --- a/src/generic/file_index.ts +++ b/src/generic/file_index.ts @@ -68,8 +68,8 @@ export class FileIndex { if (this._index.hasOwnProperty(path)) { const dir = this._index[path]; const files = dir.getListing(); - for (let i = 0; i < files.length; i++) { - const item = dir.getItem(files[i]); + for (const file of files) { + const item = dir.getItem(file); if (isFileInode(item)) { cb(item.getData()); } @@ -188,8 +188,8 @@ export class FileIndex { // If I'm a directory, remove myself from the index, and remove my children. if (isDirInode(inode)) { const children = inode.getListing(); - for (let i = 0; i < children.length; i++) { - this.removePath(path + '/' + children[i]); + for (const child of children) { + this.removePath(path + '/' + child); } // Remove the directory from the index, unless it's the root. diff --git a/src/generic/key_value_filesystem.ts b/src/generic/key_value_filesystem.ts index c50230b6..14b9f1e2 100644 --- a/src/generic/key_value_filesystem.ts +++ b/src/generic/key_value_filesystem.ts @@ -177,8 +177,7 @@ export class SimpleSyncRWTransaction implements SyncKeyValueRWTransaction { public abort(): void { // Rollback old values. - for (let i = 0; i < this.modifiedKeys.length; i++) { - const key = this.modifiedKeys[i]; + for (const key of this.modifiedKeys) { const value = this.originalData[key]; if (!value) { // Key didn't exist. diff --git a/src/rollup.config.js b/src/rollup.config.js index 42831833..9da9fd12 100644 --- a/src/rollup.config.js +++ b/src/rollup.config.js @@ -26,6 +26,11 @@ export default { preferBuiltins: true }), sourcemaps(), - buble() + buble({ + transforms: { + // Assumes all `for of` statements are on arrays or array-like items. + dangerousForOf: true + } + }) ] }; diff --git a/src/tslint.json b/src/tslint.json index 082a1174..e02f17a6 100644 --- a/src/tslint.json +++ b/src/tslint.json @@ -35,7 +35,7 @@ "variable-declaration": "onespace" } ], - "prefer-for-of": false, + "prefer-for-of": true, "curly": true, "forin": true, "label-position": true, @@ -72,20 +72,20 @@ "new-parens": true, "one-line": [true, "check-catch", "check-finally", "check-else", "check-open-brace", "check-whitespace"], "semicolon": [true, "always"], - "variable-name": [true, "ban-keywords", "check-format", "allow-leading-underscore"], + "variable-name": [true, "ban-keywords", "check-format", "allow-leading-underscore", "allow-pascal-case"], "whitespace": [true, "check-branch", "check-decl", "check-operator", "check-module", "check-separator", "check-typecast"], "max-line-length": [ false ], "no-string-literal": false, "no-bitwise": false, - "trailing-comma": false, + "trailing-comma": [false], "object-literal-sort-keys": false, - "ordered-imports": false, - "one-variable-per-declaration": false, + "ordered-imports": [false], + "one-variable-per-declaration": [false], "no-shadowed-variable": false, "switch-default": false, - "only-arrow-functions": false, + "only-arrow-functions": [false], "object-literal-shorthand": false, "no-use-before-declare": false, "no-angle-bracket-type-assertion": false, diff --git a/test/rollup.config.js b/test/rollup.config.js index 8eb811c0..1773db18 100644 --- a/test/rollup.config.js +++ b/test/rollup.config.js @@ -28,6 +28,10 @@ export default { sourcemaps({ exclude: '**/*' }), - buble() + buble({ + transforms: { + dangerousForOf: true + } + }) ] }; diff --git a/test/tests/general/check-options-test.ts b/test/tests/general/check-options-test.ts new file mode 100644 index 00000000..e3b974ba --- /dev/null +++ b/test/tests/general/check-options-test.ts @@ -0,0 +1,138 @@ +import assert from '../../harness/wrapped-assert'; +import {checkOptions} from '../../../src/core/util'; +import {FileSystemOptions, FileSystemConstructor} from '../../../src/core/file_system'; +import {ApiError} from '../../../src/core/api_error'; +import Backends from '../../../src/core/backends'; + +declare var __numWaiting: number; + +function numWaitingWrap(cb: any): any { + __numWaiting++; + return function(this: any) { + __numWaiting--; + cb.apply(this, arguments); + } +} + +function getFileSystem(opts: FileSystemOptions): FileSystemConstructor { + return { + Options: opts, + Name: "TestFS", + Create: function() {}, + isAvailable: () => true + }; +} + +function noErrorAssert(e?: ApiError) { + if (e) { + assert(!e, `Received unplanned error: ${e.toString()}`); + } +} + +function errorMessageAssert(expectedMsgs: string[], unexpectedMsgs: string[] = []): (e?: ApiError) => void { + let called = false; + return (e?: ApiError) => { + assert(called === false, `Callback called twice!`); + called = true; + assert(!!e, `Did not receive planned error message.`); + const errorMessage = e.message; + for (const m of expectedMsgs) { + assert(errorMessage.indexOf(m) !== -1, `Error message '${errorMessage}' is missing expected string '${m}'.`); + } + for (const m of unexpectedMsgs) { + assert(errorMessage.indexOf(m) === -1, `Error message '${errorMessage}' unexpectedly contained string '${m}'.`); + } + }; +} + +export default function() { + + const emptyOptionsFS = getFileSystem({}); + checkOptions(emptyOptionsFS, {}, noErrorAssert); + // Tolerates unrecognized options. + checkOptions(emptyOptionsFS, { unrecognized: true }, noErrorAssert); + + const simpleOptionsFS = getFileSystem({ + type: { + type: 'string', + optional: false, + description: "__type_desc__" + }, + size: { + type: "number", + optional: true, + description: "__size_desc__" + }, + multitype: { + type: ["number", "string", "boolean"], + optional: true, + description: "__multitype_desc__" + } + }); + checkOptions(simpleOptionsFS, { type: "cool", size: Number.POSITIVE_INFINITY }, noErrorAssert); + // Doesn't mind if optional type is missing or `null`. + checkOptions(simpleOptionsFS, { type: "cool" }, noErrorAssert); + checkOptions(simpleOptionsFS, { type: "cool", size: null }, noErrorAssert); + // *Does* mind if optional type has incorrect type. + checkOptions(simpleOptionsFS, { type: "cool", size: "cooler" }, errorMessageAssert(['size', 'type', 'number', 'string', '__size_desc__'])); + // Also minds if required type is incorrect type. + checkOptions(simpleOptionsFS, { type: 3 }, errorMessageAssert(['type', 'string', 'number', '__type_desc__'])); + // Minds if required type is missing. Does not suggest recognized type is proper. + checkOptions(simpleOptionsFS, { size: 3 }, errorMessageAssert(['type', '__type_desc__'], ['unrecognized', 'perhaps'])); + // Provides helpful hints with mistyped option. + checkOptions(simpleOptionsFS, { types: "cooled" }, errorMessageAssert(['type', '__type_desc__', 'unrecognized', 'perhaps', 'types'])); + // Supports multiple basic types + checkOptions(simpleOptionsFS, { type: "cool", multitype: true }, noErrorAssert); + checkOptions(simpleOptionsFS, { type: "cool", multitype: "true" }, noErrorAssert); + checkOptions(simpleOptionsFS, { type: "cool", multitype: 1 }, noErrorAssert); + // Proper error message for options with multiple basic types + checkOptions(simpleOptionsFS, { type: "cool", multitype: {} }, errorMessageAssert(['multitype', '__multitype_desc__', 'number', 'string', 'boolean', 'object'])); + + const validatorFS = getFileSystem({ + asyncFail: { + type: 'number', + optional: true, + description: "__asyncFail_desc__", + validator: function(v, cb) { + setTimeout(() => cb(new ApiError(0, '__asyncFail_error__')), 5); + } + }, + asyncPass: { + type: 'number', + optional: false, + description: "__asyncPass_desc__", + validator: function(v, cb) { + setTimeout(() => cb(), 5); + } + }, + syncFail: { + type: 'number', + optional: true, + description: "__syncFail_desc__", + validator: function(v, cb) { cb(new ApiError(0, '__syncFail_error__')); } + }, + syncPass: { + type: 'number', + description: '__syncPass_desc_', + validator: function(v, cb) { cb(); } + } + }); + + // Supports validators. + checkOptions(validatorFS, { asyncPass: 0, syncPass: 0 }, numWaitingWrap(noErrorAssert)); + // Supports validator error messages. + checkOptions(validatorFS, { asyncPass: 0, syncPass: 0, syncFail: 0 }, numWaitingWrap(errorMessageAssert(['__syncFail_error__']))); + checkOptions(validatorFS, { asyncPass: 0, syncPass: 0, asyncFail: 0 }, numWaitingWrap(errorMessageAssert(['__asyncFail_error__']))); + + // Monkey-patched `Create` works with and without options argument. + ( Backends.InMemory.Create)(numWaitingWrap((e: ApiError, rv: any) => { + noErrorAssert(e); + // Make sure rv is a real FS. + rv.readdirSync('/'); + })); + ( Backends.InMemory.Create)({}, numWaitingWrap((e: ApiError, rv: any) => { + noErrorAssert(e); + // Make sure rv is a real FS. + rv.readdirSync('/'); + })); +};