libflitter/canon/CanonicalUnit.js

/**
 * @module libflitter/canon/CanonicalUnit
 */

const path = require('path')
const rra = require('recursive-readdir-async')
const Unit = require('../Unit')

const priv = {
    // Formats a file path to canonical name.
    format_file_name(name) {
        const base_dir = process.platform === 'win32' ? this.directory.replace(/\\/g, '/') : this.directory
        name = name.replace(base_dir, '')

        if ( this.suffix ) {
            const suffix_regexp = new RegExp(`${this.suffix}`, 'g')
            name = name.replace(suffix_regexp, '')
        }

        name = name.replace(/\//g, ':').substring(1)

        return name
    }
}

/**
 * Base class for canonical units that load resources from the filesystem
 * and name resources based on the file's location within that system.
 * @extends module:libflitter/Unit~Unit
 */
class CanonicalUnit extends Unit {
    /**
     * Defines the services required by this unit.
     * The 'canon' service is provided by default.
     * @returns {Array<string>}
     */
    static get services() {
        return [...super.services, 'canon']
    }

    /**
     * Get the name of the service provided by this unit.
     * @returns {string}
     */
    static get name() {
        return this.prototype.canonical_item+'s'
    }

    /**
     * Instantiate the unit.
     * @param {string} base_directory - the base search directory
     */
    constructor(base_directory) {
        super()

        /**
         * The root directory for this canonical resource's files.
         * @type {Promise<void> | Promise<string>}
         */
        this.directory = path.resolve(base_directory)

        /**
         * The file extension of the canonical item files.
         * @type {string} - '.js'
         */
        this.suffix = '.js'

        /**
         * The canonical name of the item.
         * @type {string} = 'item'
         */
        this.canonical_item = 'item'

        /**
         * Mapping of canonical names to instances for this item.
         * @type {object}
         */
        this.canonical_items = {}
    }

    /**
     * Initializes the unit. Recursively iterates over the base directory and finds
     * all valid files. Loads the instances from those files and initializes them.
     * @param {module:libflitter/app/FlitterApp~FlitterApp} app - the Flitter app
     * @returns {Promise<void>}
     */
    async go(app) {
        const files = await rra.list(this.directory)
        for ( let key in files ) {
            if ( !files.hasOwnProperty(key) ) continue
            if ( files[key].fullname && files[key].fullname.endsWith(this.suffix) ) {
                const name = priv.format_file_name.bind(this)(files[key].fullname)
                const instance = require(files[key].fullname)

                if ( app.di().__is_injectable(instance) ) {
                    app.di().inject(instance)
                }

                this.canonical_items[name] = await this.init_canonical_file({
                    app, name, instance
                })
            }
        }

        this.canon.register_resource(this.canonical_item, this.get.bind(this))
    }

    /**
     * Prepare a single canonical item and return the value that should be given by the resolver.
     * @param {object} info
     * @param {module:libflitter/app/FlitterApp} info.app
     * @param {string} info.name - the unqualified canonical name
     * @param {*} info.instance - the exports from the file
     * @returns {Promise<*>}
     */
    async init_canonical_file({app, name, instance}) {
        return instance
    }

    /**
     * Get the directories provided by this unit.
     * @returns {object}
     */
    directories() {
        return {
            [this.canonical_item+'s']: this.directory,
        }
    }

    /**
     * Resolve an unqualified canonical name to a registered canonical item.
     * @param {string} name
     * @returns {object}
     */
    get(name) {
        const name_parts = name.split('.')
        let descending_value = this.canonical_items
        name_parts.forEach(part => {
            descending_value = descending_value[part]
        })

        return descending_value
    }
}

module.exports = exports = CanonicalUnit