'use strict'
const fs = require('fs')
const path = require('path')
const { Logger } = require('./logger')
const LOGGER = new Logger('utils/utils')
const { Seq, Map, List } = require('immutable')
const utils = {}
utils.DB_DIR = 'db'
utils.OUTS_DIR = 'compileOutputs'
utils.FLATS_DIR = 'sourcesFlattened'
utils.SOURCES_DIR = 'contracts'
utils.COMPILES_DIR = 'compiles'
utils.LINKS_DIR = 'links'
utils.DEPLOYS_DIR = 'deploys'
utils.LIB_PATTERN = /__(([a-zA-Z0-9])+\/*)+\.sol:[a-zA-Z0-9]+_+/g
utils.DEMO_SRC_PATH = 'contracts'
utils.getEndpointURL = () => {
const { getConfig } = require('./config.js')
const config = getConfig()
return config['ETH_URL']
}
utils.getNetwork = () => {
const Eth = require('ethjs')
return new Eth(new Eth.HttpProvider(utils.getEndpointURL()))
}
/**
* Deep version of fromJS https://stackoverflow.com/a/40663730
*/
utils.fromJS = (js) => {
return typeof js !== 'object' || js === null ? js :
Array.isArray(js) ?
Seq(js).map(utils.fromJS).toList() :
Seq(js).map(utils.fromJS).toOrderedMap()
}
utils.toJS = (imm) => {
return (List.isList(imm)) ? imm.map((val) => {return utils.toJS(val)}).toJS() :
(Map.isMap(imm)) ? imm.map((val) => {return utils.toJS(val)}).toJS() :
imm
}
utils.equal = (a,b) => {
if (Map.isMap(a) && Map.isMap(b)) { return utils.immEqual(a,b) }
if (List.isList(a) && List.isList(b)) { return utils.immEqual(a,b) }
return utils.deepEqual(a,b)
}
/**
* Determines if two newline delimited texts are equal by line.
*
* @method textsEqual
* @memberof module:utils
* @param a {String} first text to compare
* @param b {String} first text to compare
* @return first line number where the two texts differ, or -1 if they are identical
*/
utils.textsEqual = (a,b) => {
const aLines = a.split('\n')
const bLines = b.split('\n')
for (let i = 0; i < aLines.length; i += 1) {
if ( aLines[i] !== bLines[i] ) { return i }
}
return -1
}
utils.stringsEqual = (a,b) => {
for (let i = 0; i < a.length; i += 1) {
if (a[i] !== b[i]) { return i }
}
return -1
}
/*
* Deep JS object equality testing from https://stackoverflow.com/a/10316616
*/
utils.deepEqual = (a,b) => {
if (a instanceof Array && b instanceof Array)
return utils.arraysEqual(a,b)
if (Object.getPrototypeOf(a)===Object.prototype &&
Object.getPrototypeOf(b)===Object.prototype) {
return utils.objectsEqual(a,b)
}
if (a instanceof Map && b instanceof Map) {
return utils.mapsEqual(a,b)
}
if (a instanceof Set && b instanceof Set) {
throw new Error('equality by hashing not implemented.')
}
if ((a instanceof ArrayBuffer || ArrayBuffer.isView(a)) &&
(b instanceof ArrayBuffer || ArrayBuffer.isView(b))) {
return utils.typedArraysEqual(a,b)
}
return a==b // see note[1] -- IMPORTANT
}
utils.arraysEqual = (a,b) => {
if (a.length!=b.length) { return false }
for (let i=0; i < a.length; i++) {
if (!utils.deepEqual(a[i],b[i]))
return false
return true
}
}
utils.objectsEqual = (a,b) => {
const aKeys = Object.getOwnPropertyNames(a)
const bKeys = Object.getOwnPropertyNames(b)
if (aKeys.length != bKeys.length) { return false }
aKeys.sort()
bKeys.sort()
for (let i=0; i < aKeys.length; i++) {
if (aKeys[i]!=bKeys[i]) { // keys must be strings
return false
}
return utils.deepEqual(aKeys.map(k=>a[k]), aKeys.map(k=>b[k]))
}
}
utils.mapsEqual = (a,b) => {
if (a.size != b.size) { return false }
const aPairs = Array.from(a)
const bPairs = Array.from(b)
aPairs.sort((x,y) => x[0]<y[0])
bPairs.sort((x,y) => x[0]<y[0])
for (let i=0; i<a.length; i++) {
if (!utils.deepEqual(aPairs[i][0],bPairs[i][0]) ||
!utils.deepEqual(aPairs[i][1],bPairs[i][1])) {
return false
}
return true
}
}
utils.typedArraysEqual = (_a, _b) => {
const a = new Uint8Array(_a)
const b = new Uint8Array(_b)
if (a.length != b.length) { return false }
for (let i=0; i < a.length; i++) {
if (a[i]!=b[i]) { return false }
return true
}
}
/**
* @param _a {Immutable} first list or map to compare.
* @param _b {Immutable} second list or map to compare.
* @return true if the two objects have equal value (JSON strings) false otherwise.
*/
utils.immEqual = (_a, _b) => {
return JSON.stringify(_a.toJS()) === JSON.stringify(_b.toJS())
}
/**
* @return true if we are in a browser (tests for present of `window` and `window.document`
*/
utils.isBrowser = () => {
return (typeof window != 'undefined' && window.document)
}
utils.ensureDir = (dirName) => {
if (!fs.existsSync(dirName)) { fs.mkdirSync(dirName, { recursive: true } ) }
}
/**
* Remove a file (not a directory) if it exists
*/
utils.rimRafFileSync = (fn) => {
if (fs.existsSync(fn)) { fs.unlinkSync(fn) }
}
/**
* Traverse directories collecting files to perform a callback function on
* @param startDirs a list of paths to start searching in
* @param skipFilt a function that returns true for files that need to be skipped
* @param cb a callback that accepts the source text of a file plus its full path
* @param dcb a callback for every directory that is encountered
*/
utils.traverseDirs = (startDirs, skipFilt, cb, dcb) => {
const queue = startDirs
while (queue.length > 0) {
const f = queue.pop()
const shortList = path.basename(f).split('.')
if (skipFilt(shortList, f)) { continue }
if (!fs.existsSync(f)) {
LOGGER.debug(`Skipping traversal directory ${f} which does not exist.`)
continue
}
if (fs.lstatSync(f).isDirectory()) {
fs.readdirSync(f).forEach((f2) => queue.push(path.join(f,f2)))
if (dcb) { dcb(f) }
} else {
const source = fs.readFileSync(f).toString()
cb(source, f)
}
}
}
/**
* Traverse directories recursively building an Immutable Map with hierarchical keys
* as directory paths and values as file contents (leaf nodes)
* @param f the full path to a filename (possibly a directory) to start traversal
* @param skipFilt a function that returns true for files that need to be skipped
*/
utils.buildFromDirs = (f, skipFilt) => {
const shortList = path.basename(f).split('.')
if (skipFilt(shortList)) { return null }
if (fs.lstatSync(f).isDirectory()) {
return new Map(List(fs.readdirSync(f)).map((f2) => {
const builtValues = utils.buildFromDirs(path.join(f,f2), skipFilt)
const baseKey = path.basename(f2)
const key = (baseKey.endsWith('.json')) ? baseKey.split('.')[0] : baseKey
return builtValues ? [key, builtValues] : null
}))
} else {
return utils.fromJS(JSON.parse(fs.readFileSync(f)))
}
}
/**
* @return true if the given object is an ethjs object, otherwise false
*/
utils.isNetwork = (_network) => {
return (_network && _network.net_version)
}
utils.thenPrint = (promise) => {
promise.then((value) => {console.log(JSON.stringify(value))})
}
/**
* Chain and return a (possibly asynchronous) call after the outputter,
* also possibly asynchronous.
* @param outputCallResult the result of calling outputter method, will have a `then`
* property if it's thenable / asynchronous.
* @param callback method, possibly asynchronous, which accepts as input the
* return value of the outputter method call (`outputCallResult`)
*/
utils.awaitOutputter = (outputCallResult, afterOutput) => {
if (outputCallResult.then) {
return outputCallResult.then((val) => {
return afterOutput(val) })
} else {
return afterOutput(outputCallResult)
}
}
/**
* Chain and return a (possibly asynchronous) call after the inputter,
* also possibly asynchronous
* @param inputCallResult the result of calling the inputter method on some args
* will have a `then` property if it's thenable / asynchronous
* @param callback method, possibly asynchronous, which accepts as input the
* return value of the inputter method call (`inputCallResult`)
*/
utils.awaitInputter = (inputCallResult, afterInput) => {
if (inputCallResult.then) {
return inputCallResult.then((val) => {
return afterInput(val) })
} else {
return afterInput(inputCallResult)
}
}
module.exports = utils