extensions/fixtures/fixtures_loader.js

'use strict'

/**
 * @module extensions/fixtures/FixturesLoader
 */

const fs = require('fs')
const path = require('path')
const glob = require('glob')
const chalk = require('chalk')
const yaml = require('js-yaml')
const _ = require('lodash')

/**
 * Fixtures loader extension.
 *
 * @class
 */
class FixturesLoader {
    /**
     * @param {string} [fixturesDir='fixtures'] - The name of the fixtures directory relative to feature
     */
    constructor({ fixturesDir } = { fixturesDir: 'fixtures' }) {
        this.fixturesDir = fixturesDir
        this.featureUri = undefined
    }

    /**
     * Configures the loader
     *
     * @param {string} [fixturesDir='fixtures'] - The name of the fixtures directory relative to feature
     */
    configure({ fixturesDir } = { fixturesDir: 'fixtures' }) {
        this.fixturesDir = fixturesDir
    }

    /**
     * Sets feature uri, used to resolve fixtures files.
     * When trying to load a fixture file the path will be comprised of:
     * - feature uri
     * - fixturesDir
     * - fixture name
     *
     * @param {string} featureUri - Feature uri
     */
    setFeatureUri(featureUri) {
        this.featureUri = featureUri
    }

    /**
     * Loads content from file.
     *
     * @param {string} file - File path
     * @return {Promise.<string>} File content
     */
    loadText(file) {
        return new Promise((resolve, reject) => {
            fs.readFile(file, (err, data) => {
                if (err) return reject(err)
                resolve(data.toString('utf8'))
            })
        })
    }

    /**
     * Loads content from yaml file.
     *
     * @param {string} file - File path
     * @return {Promise.<Object|Array>} Parsed yaml data
     */
    loadYaml(file) {
        return this.loadText(file).then(content => {
            try {
                const data = yaml.safeLoad(content)
                if (data === undefined) {
                    return Promise.reject(
                        new Error(
                            `Fixture file is invalid, yaml parsing resulted in undefined data for file: ${file}`
                        )
                    )
                }

                return data
            } catch (err) {
                return Promise.reject(
                    new Error(`Unable to parse yaml fixture file: ${file}.\nerror: ${err.message}`)
                )
            }
        })
    }

    /**
     * Loads content from json file.
     *
     * @param {string} file - File path
     * @return {Promise.<Object>} Json data
     */
    loadJson(file) {
        return this.loadText(file).then(content => {
            try {
                const data = JSON.parse(content)

                return data
            } catch (err) {
                return Promise.reject(
                    new Error(`Unable to parse json fixture file: ${file}.\nerror: ${err.message}`)
                )
            }
        })
    }

    /**
     * Loads content from javascript module.
     *
     * @param {string} file - File path
     * @return {Promise.<*>} Data generated from the module
     */
    loadModule(file) {
        try {
            const relativePath = path.relative(__dirname, file)
            const mod = require(relativePath)

            if (!_.isFunction(mod)) {
                return Promise.reject(
                    new Error(
                        [
                            `javascript fixture file should export default function.\n`,
                            `Make sure you declared 'module.exports = <function>' in ${file}`
                        ].join('')
                    )
                )
            }

            return Promise.resolve(mod())
        } catch (err) {
            return Promise.reject(
                new Error(
                    `An error occurred while loading fixture file: ${file}\nerror: ${err.message}`
                )
            )
        }
    }

    /**
     * Tries to load a fixture from current feature directory.
     * Will search for the following file extensions:
     * - yaml
     * - yml
     * - js
     * - json
     * - txt
     *
     * @param {string} fixture - Fixture file name without extension
     * @return {Promise.<Object|string>} Fixture content
     */
    load(fixture) {
        if (this.featureUri === undefined)
            return Promise.reject(
                new Error(`Cannot load fixture: ${fixture}, no feature uri defined`)
            )

        const featureDir = path.dirname(this.featureUri)
        const pattern = `${featureDir}/${this.fixturesDir}/${fixture}.@(yaml|yml|js|json|txt)`

        return new Promise((resolve, reject) => {
            glob(pattern, (err, files) => {
                const fixturesCount = files.length

                if (fixturesCount === 0)
                    return reject(new Error(`No fixture found for: ${fixture} (${pattern})`))
                if (fixturesCount > 1) {
                    return reject(
                        new Error(
                            [
                                `Found ${fixturesCount} matching fixture files, `,
                                `you should have only one matching '${fixture}', matches:\n  `,
                                `- ${files.join('\n  - ')}`
                            ].join('')
                        )
                    )
                }

                const fixtureFile = files[0]
                const ext = path.extname(fixtureFile).substr(1)

                switch (ext) {
                    case 'yml':
                    case 'yaml':
                        return resolve(this.loadYaml(fixtureFile))

                    case 'js':
                        return resolve(this.loadModule(fixtureFile))

                    case 'json':
                        return resolve(this.loadJson(fixtureFile))

                    default:
                        return resolve(this.loadText(fixtureFile))
                }
            })
        })
    }

    /**
     * Resets fixtures loader.
     */
    reset() {
        this.featureUri = undefined
    }
}

/**
 * Create a new isolated fixtures loader
 * @return {FixturesLoader}
 */
module.exports = function(...args) {
    return new FixturesLoader(...args)
}

/**
 * fixtures loader extension.
 * @type {FixturesLoader}
 */
module.exports.Fixture = FixturesLoader