storage.js

import Symbol from 'es6-symbol';
import isNil from 'is-nil';
import isString from 'is-string';
import isFunction from 'is-function';
import forEach from 'for-each';
import startsWith from 'starts-with';
import Module from './module';
import path from './utils/path';
import { requires, assert } from './utils/assertions';

const INVALID_CALLBACK = 'Invalid callback';
const INVALID_MODULE = 'Invalid module';
const INVALID_NAMESPACE = 'Invalid namespace';
const INVALID_MODULE_NAME = 'Invalid module name';
const INVALID_MODULE_PATH = 'Invalid module path';
const NAMESPACE_NOT_FOUND = 'Namespace was not found';
const MODULE_NOT_FOUND = 'Module with path was not found';

const REGISTRY_FIELDS = {
    size: Symbol('size')
};

const FIELDS = {
    separator: Symbol('separator'),
    panic: Symbol('panic'),
    namespaces: Symbol('namespaces'),
    size: Symbol('size')
};

/**
 * Represents a modules storage.
 */
class Storage {

    /**
     * Creates a new instance of Storage.
     * @param {string} [separator] - Namespace separator.
     * @param {boolean} [panic] - Indicates whether it needs to throw an error when module not found.
     */
    constructor(separator = '/', panic = true) {
        this[FIELDS.separator] = separator;
        this[FIELDS.panic] = panic;
        this[FIELDS.namespaces] = {};
        this[FIELDS.size] = 0;
    }

    /**
     * Returns size of a storage or namespace.
     * If namespace was given, count of items inside this namespace will be returned.
     * @param {string} [namespace=undefined] Namespace name
     * @returns {number} Size of a storage/namespace.
     */
    size(namespace) {
        if (isString(namespace)) {
            const registry = this[FIELDS.namespaces][namespace];

            if (isNil(registry)) {
                return 0;
            }

            return registry[REGISTRY_FIELDS.size];
        }

        return this[FIELDS.size];
    }

    /**
     * Returns an array of registered namespaces.
     * @param {string} [parent=undefined] - Parent namespace.
     * @returns {Array} An array of registered namespaces.
     */
    namespaces(parent) {
        const namespaces = [];

        forEach(this[FIELDS.namespaces], (_, key) => {
            if (parent) {
                if (key !== parent && startsWith(key, parent)) {
                    namespaces.push(key);
                }
            } else {
                namespaces.push(key);
            }
        });

        return namespaces;
    }

    /**
     * Adds a module to a storage.
     * @param {Module} module - Target module to add.
     * @throws {Error} If a module with a same path already exists.
     * @returns {Storage} Returns current instance of Storage.
     */
    addItem(module) {
        requires(module, 'module');
        assert(module instanceof Module, INVALID_MODULE);

        const namespace = module.getNamespace();

        assert(isString(namespace), INVALID_NAMESPACE);

        const name = module.getName();

        assert(isString(name) && name.trim() !== '', INVALID_MODULE_NAME);
        assert(
            path.isValidName(this[FIELDS.separator], name),
            `${INVALID_MODULE_PATH} Module can not contain namespace separators`
        );

        let registry = this[FIELDS.namespaces][namespace];

        if (!registry) {
            registry = {};
            registry[REGISTRY_FIELDS.size] = 0;
            this[FIELDS.namespaces][namespace] = registry;
        }

        assert(
            !registry.hasOwnProperty(name),
            `${name} is already registered`
        );

        registry[name] = module;
        registry[REGISTRY_FIELDS.size] += 1;
        this[FIELDS.size] += 1;

        return this;
    }

    /**
     * Finds a module by a given path.
     * @param {string} fullPath - Module full path.
     * @return {Module} Found module.
     * @throws {Error} If panic="true" and module not found.
     */
    getItem(fullPath) {
        requires(fullPath, 'path');
        assert(isString(fullPath), INVALID_MODULE_PATH);

        const parts = path.split(this[FIELDS.separator], fullPath);
        const namespace = this[FIELDS.namespaces][parts.namespace];
        const panic = this[FIELDS.panic];

        if (!namespace) {
            assert(!panic, `${MODULE_NOT_FOUND}: ${fullPath}!`);

            return null;
        }

        const module = namespace[parts.name];

        if (!module) {
            assert(!panic, `${MODULE_NOT_FOUND}: ${fullPath}!`);

            return null;
        }

        return module;
    }

    /**
     * Clears a storage.
     * If namespace name is passed - clears a namespace by a given name.
     * @param {string} [namespace=null] - Namespace name to clear.
     * @returns {Storage} Returns current instance of Storage.
     */
    clear(namespace) {
        assert(
            isNil(namespace) || isString(namespace),
            INVALID_NAMESPACE
        );

        if (!isString(namespace)) {
            this[FIELDS.namespaces] = {};
            this[FIELDS.size] = 0;

            return this;
        }

        const registry = this[FIELDS.namespaces][namespace];

        if (!isNil(registry)) {
            this[FIELDS.size] -= registry[REGISTRY_FIELDS.size];

            this[FIELDS.namespaces][namespace] = null;
        }

        return this;
    }

    /**
     * Determines whether a module exists by a given path.
     * @param {string} path - Module full path.
     * @return {boolean} Value that determines whether a module exists by a given path.
     */
    contains(fullPath) {
        requires(fullPath, 'path');
        assert(isString(fullPath), INVALID_MODULE_PATH);

        const parts = path.split(this[FIELDS.separator], fullPath);
        const namespace = this[FIELDS.namespaces][parts.namespace];

        if (!namespace) {
            return false;
        }

        const module = namespace[parts.name];

        if (!module) {
            return false;
        }

        return true;
    }

    /**
     * Iterates over modules in a given namespace.
     * @param {string} namespace - Namespace name.
     * @returns {Storage} Returns current instance on Storage.
     * @throws {Error} If panic="true" and namespace not found.
     */
    forEachIn(namespace, callback) {
        requires(callback, 'callback');
        assert(isString(namespace), INVALID_NAMESPACE);
        assert(isFunction(callback), INVALID_CALLBACK);

        const registry = this[FIELDS.namespaces][namespace];
        const panic = this[FIELDS.panic];

        if (!registry) {
            assert(!panic, `${NAMESPACE_NOT_FOUND}: ${namespace}!`);
            return this;
        }

        forEach(registry, (value, name) => {
            callback(value, path.join(this[FIELDS.separator], namespace, name));
        });

        return this;
    }
}

export default Storage;