/**
* @module libflitter/utility/UtilityUnit
*/
const Unit = require('../Unit')
const path = require('path')
const Output = require('./services/Output.service')
/**
* The utility unit contains various utility functions and tools that
* are made available to almost all other units. Currently, a function
* for restricting console.log calls by logging level is the one method
* included. Also provides the path to the root folder of the app.
*
* @extends module:libflitter/Unit~Unit
*/
class UtilityUnit extends Unit {
/**
* Defines the services required by this unit.
* @returns {Array<string>}
*/
static get services() {
return [...super.services, 'configs']
}
/**
* Array of additional service names provided by this unit.
* @returns {Array<string>}
*/
static get provides() {
return ['output']
}
/**
* Gets the name of the service provided by this unit: 'utility'
* @returns {string} - 'utility'
*/
static get name() {
return 'utility'
}
/**
* Instantiate the unit.
* @param {string} [app_root = './app'] - path to the 'app' folder
*/
constructor(app_root = './app'){
super()
/**
* Fully-qualified path to the 'app' folder.
* @type {Promise<void> | Promise<string>}
*/
this.directory = path.resolve(app_root)
/**
* Fully-qualified path to the 'app/services' folder.
* @type {Promise<void> | Promise<string>}
*/
this.services_dir = path.resolve(app_root, 'services')
}
/**
* Loads the unit. Binds the application root and global logging function to the appropriate contexts.
* @param {module:libflitter/app/FlitterApp~FlitterApp} app - the Flitter app
* @param {module:libflitter/Context~Context} context - the unit's context
* @returns {Promise<void>}
*/
async go(app){
super.go(app)
const output = new Output()
output.level = this.configs.get('server.logging.level')
output.timestamp = this.configs.get('server.logging.include_timestamp')
// Replace the output service instantiated by FlitterApp
app.output = output
this.output = output
app.di().container.register_singleton(Output.name, output)
}
/**
* Get the fully-qualified path to the root folder of the Flitter application.
* @returns {string}
*/
root(){
if ( require && require.main && require.main.filename ) return require.main.filename
else if ( process && process.mainModule && process.mainModule.filename ) return process.mainModule.filename
else return process.cwd()
}
/**
* Returns true if the application is running on Windows.
* @returns {boolean}
*/
is_windows() {
return process.platform === 'win32'
}
/**
* Returns true if the application is running on Linux.
* @returns {boolean}
*/
is_linux() {
return process.platform === 'linux'
}
/**
* Returns true if the application is running on macOS.
* @returns {boolean}
*/
is_mac() {
return process.platform === 'darwin'
}
/**
* Get the directives provided by this unit.
* @returns {Object}
*/
directives(){
return {
// ServerDirective
}
}
/**
* Get the directories managed by this unit. Includes 'root' for the application root.
* @returns {{root: string}}
*/
directories() {
return {
root: this.root()
}
}
/**
* Get the unit's migrations.
* @returns {string}
*/
migrations(){
return path.resolve(__dirname, 'migrations')
}
/**
* Make a deep copy of some item. If item is primitive, return it.
* It item is an array, create a new array with deep copies of each array item.
* If item is an object, create a new object with key-values such that the keys are the
* same, but the values are deep copies of the keys.
*
* Assumes items can eventually be resolved to primitive types, and that they
* contain no circular structure.
* @param item
* @returns {Object}
*/
deep_copy(item){
if ( Array.isArray(item) ){
const ret = []
item.forEach(e => {
ret.push(this.deep_copy(e))
})
return ret
} else if ( typeof item === 'object' ){
const ret = {}
Object.keys(item).forEach(key => {
ret[key] = this.deep_copy(item[key])
})
return ret
} else return item
}
/* Useful functions */
/**
* Deep merges object 2 into object 1, recursively.
* This mutates object 1.
* @param obj1
* @param obj2
* @returns {*}
*/
deep_merge(obj1, obj2) {
for ( let key in obj2 ){
if ( !Object.keys(obj1).includes(key) ) obj1[key] = obj2[key]
else if ( typeof obj1[key] !== 'object' ) obj1[key] = obj2[key]
else if ( typeof obj2[key] !== 'object' ) obj1[key] = obj2[key]
else obj1[key] = this.deep_merge(obj1[key], obj2[key])
}
return obj1
}
/**
* Grabs an environment variable by name and tries to infer its type.
* @param {string} name
* @param default_value
* @return {string|null|boolean|number}
*/
env(name, default_value){
const val = process.env[name]
if ( !val ) return typeof default_value !== "undefined" ? default_value : null
else return this.infer(val)
}
/**
* Attempt to infer the variable type of a string's data.
* @param {string} val
* @return {boolean|null|*|number|undefined}
*/
infer(val){
if ( !val ) return null
else if ( val.toLowerCase() === 'true' ) return true
else if ( val.toLowerCase() === 'false' ) return false
else if ( !isNaN(val) ) return +val
else if ( this.is_json(val) ) return JSON.parse(val)
else if ( val.toLowerCase() === 'null' ) return null
else if ( val.toLowerCase() === 'undefined' ) return undefined
else return val
}
/**
* Checks if a string is valid JSON.
* @param string
* @return {boolean} - true if the string is valid JSON
*/
is_json(string){
try {
JSON.parse(string)
return true
}
catch (e) {
return false
}
}
}
module.exports = UtilityUnit