declare var require; import * as R from 'ramda'; // let $ = require('jquery'); import { Component, InjectionToken } from '@angular/core'; import { ValidatorFn, FormBuilder, FormControl, FormArray, FormGroup, AbstractControl } from '@angular/forms'; // import { Maybe } from 'sanctuary'; // let CryptoJS = require('crypto-js'); import { Path, NestedArr, NestedObj, Obj, Pred, Type, ObjectMapper, Prop, StringLike, Fn } from './types'; // require('materialize-css/dist/js/materialize.min'); // let YAML = require('yamljs'); // convert an array to an object based on a lambda // make an object from an array with mapper function export let arr2obj = (fn: (s: StringLike) => T) => (arr: StringLike[]): Obj => R.pipe( // Ramda issue... doesn't know what will go in so somehow decides on the last option, functor, while I need the first option, list // <(list: ArrayLike) => [string, T][]> // <[string, T][]> // R.map((k: StringLike): [string, T] => [k.toString(), fn(k)]), (list: StringLike[]): [string, T][] => list.map((k: StringLike): [string, T] => [k.toString(), fn(k)]), R.fromPairs )(arr); // make a Map from an array with mapper function export function arr2map(arr: Array, fn: (v: T) => U): Map { return arr.reduce((map, k) => map.set(k, fn(k)), new Map()); } // execute a function and return the input unchanged, to allow chaining export function doReturn(fn: (v: T) => void): (v: T) => T { return (v) => { fn(v); return v; }; } // return the hash or get parts of a Location as an object function urlBit(url: Obj, part: string): Obj { // : Location let par_str = ( url[part]).substring(1); return fromQuery(par_str); } // convert a GET query string (part after `?`) to an object export function fromQuery(str: string): Obj { let params = decodeURIComponent(str).split('&'); return R.fromPairs(params.map((s: string) => <[string, string]> s.split('='))); } // convert an object to a GET query string (part after `?`) // export let toQuery = jQuery.param; export function toQuery(obj: {}): string { let enc = R.map(encodeURIComponent)(obj); return R.toPairs(enc).map(R.join('=')).join('&'); } // let range = (n) => Array(n).fill().map((x,i)=>i); // let spawn_n = (fn, n = 5, interval = 1000) => range(n).forEach((x) => setTimeout(fn, x * interval)); // 'cast' a function such that in case it receives a bad (non-conformant) // value for input, it will return a default value of its output type // intercepts bad input values for a function so as to return a default output value // ... this might hurt when switching to like Immutable.js though. // export function typed(from: Type[], to: Type, fn: Fn): Fn { //T: (...) => to return function() { for (let i = 0; i < from.length; i++) { let frm = from[i]; let v = arguments[i]; if(frm && (R.isNil(v) || v.constructor != frm)) return (new to).valueOf(); } return callFn(fn, this, arguments); }; } // wrapper for setter methods, return if not all parameters are defined // export function combine(fn: Fn, allow_undef: {[key: string]: boolean} = {}): (v: T) => void { return function() { // let names = /([^\(]+)(?=\))'/.exec(fn.toString()).split(',').slice(1); let names = fn.toString().split('(')[1].split(')')[0].split(/[,\s]+/); for (let i = 0; i < arguments.length; i++) { let v = arguments[i]; let name = names[i] .replace(/_\d+$/, '') // fixes SweetJS suffixing all names with like _123. this will however break functions already named .*_\d+, e.g. foo_123 // do not minify the code while uing this function, it will break -- functions wrapped in combine will no longer trigger. if(typeof v === 'undefined' && !allow_undef[name]) return; // || R.isNil(v) } callFn(fn, this, arguments); //return }; } // simpler guard, just a try-catch wrapper with default value // export function fallback(def: T, fn: Fn): Fn { //fn: (...) => T return function() { try { return callFn(fn, this, arguments); } catch(e) { console.warn('fallback error', e.stack); return def; } }; } // just log errors. only useful in contexts with silent crash. // export function tryLog(fn: Fn): Fn { return function() { try { return callFn(fn, this, arguments); } catch(e) { console.warn('tryLog error:', e); return undefined; } }; } // create a Component, decorating the class with the provided metadata // export let FooComp = ng2comp({ component: {}, parameters: [], decorators: {}, class: class FooComp {} }) export function ng2comp>(o: { component?: Component, parameters?: Array | never /*?*/>, decorators?: Obj, class: TComp }): TComp { let { component = {}, parameters = [], decorators = {}, class: cls } = o; R.keys(decorators).forEach((k: string) => { (Reflect).decorate([decorators[k]], cls.prototype, k); }); return Object.assign(cls, { annotations: [new Component(component)], parameters: parameters.map(x => x instanceof InjectionToken ? x : [x]), }); }; // find the index of an item within a Set (indicating in what order the item was added). export function findIndexSet(x: any, set: Set): number { return Array.from(set).findIndex(y => y == x); } // editVals from elins; map values of an object using a mapper // only keep properties in original object export let editValsOriginal: ObjectMapper = R.curry((fnObj: Obj, obj: Obj) => R.mapObjIndexed((v: T, k: string) => { let fn = fnObj[k]; return fn ? fn(v) : v })(obj)); // export let editVals = (fnObj) => (obj) => R.reduce((acc, fn, k) => _.update(k, fn(acc[k]))(acc), obj)(fnObj); // ^ no k in FP // keep all original properties, map even over keys not in the original object export let editValsBoth: ObjectMapper = R.curry((fnObj: Obj, obj: Obj) => R.keys(fnObj).reduce((acc, k: string) => R.assoc(k, fnObj[k])(acc), obj)); // only keep properties in mapper object, map even over keys not in the original object export let editValsLambda: ObjectMapper = R.curry((fnObj: Obj, obj: Obj) => R.mapObjIndexed((fn: Function, k: string) => { let v = obj[k]; return fn ? fn(v) : v })(fnObj)); // split an object into its keys and values: `let [keys, vals] = splitObj(obj);` export function splitObj(obj: {}): [string[], any[]] { return [R.keys(obj), R.values(obj)]; } // http://www.2ality.com/2012/04/eval-variables.html // evaluate an expression within a context (of the component) export let evalExpr = (context: {}, vars: {} = {}) => (expr: string) => { let varArr = [context, vars]; let propObj = Object.assign({}, ...[...varArr, ...varArr.map(x => Object.getPrototypeOf(x))] .map(x => arr2obj(k => x[k.toString()])(Object.getOwnPropertyNames(x)))); let [keys, vals] = splitObj(propObj); let fn = Function.apply(context, keys.concat(`return ${expr}`)); return fn.apply(context, vals); } // print a complex object for debugging -- regular log sucks cuz it elides values, JSON.stringify errors on cyclical structures. export function print(k: string, v: {}): void { let cname = (v: any) => v ? v.constructor.name : null; console.log(k, cname(v), R.map(cname)(v)); }; // transform a value while the predicate holds true export function transformWhile(predicate: Pred, transformer: (v: T) => T, v: T): T { while(predicate(v)) { v = transformer(v); } return v; } // intended to allow sub-classing to create custom errors export class ExtendableError extends Error { constructor(message: string) { super(message); this.name = this.constructor.name; this.message = message; if (typeof Error['captureStackTrace'] === 'function') { Error['captureStackTrace'](this, this.constructor); } else { this.stack = (new Error(message)).stack; } } } const isObject = R.both(R.is(Object), R.complement(R.is(Array))); // extract any iterables wrapped in functions from a json structure as a collection of tuples export function extractIterables(v: any, path: Prop[] = [], iters: Array<[Path, any[]]> = []): Array<[Path, any[]]> { if(R.is(Array)(v)) { v.map((x: any, i: number) => extractIterables(x, path.concat(i), iters)); } else if(isObject(v)) { R.mapObjIndexed((x: any, k: string) => extractIterables(x, path.concat(k), iters))(v); } else if(R.is(Function)(v)) { iters.push([path, v()]); } return iters; } // parameterize a JSON structure with variables (slots to be iterated over) into a function export function parameterizeStructure(val: any, iterableColl: Array<[Path, any[]]>): Function { // go over the paths so as to unset each 'gap' let slotted = iterableColl.reduce((v, [path, iterable]) => R.assoc(path, undefined)(v), val); // make reducer iterating over paths to inject a value into each from a parameter return function() { let args = arguments; let paramColl = iterableColl.map(([path, iter], idx) => [path, args[idx]]); return paramColl.reduce((v, [path, param]) => R.assoc(path, param)(v), slotted); } } // // encody a message by base64 // export let encode = window.btoa; // // decody a message by base64 // export let decode = window.atob; export const MAX_ARRAY = 2**32-1; // recursive structure of `ValidatorFn`; note arrays values won't translate to a list here. export type ValidatorStruct = ValidatorFn | { [k: string]: ValidatorStruct }; // generate a form based on a value to edit (including arrays) and a `ValidatorStruct`. // Matches up validator/control numbers by array sizes in the value. // TODO: add `AsyncValidator`s, structure-level (as opposed to item-level) validators // potentially add { sync, async } and { item, structure } wrappers in ValidatorStruct. export function editCtrl(fb: FormBuilder, val: any, vldtr: ValidatorStruct): AbstractControl { return isObject(val) ? fb.group(R.mapObjIndexed((v: any, k: string) => editCtrl(fb, v, (<{ [k: string]: ValidatorStruct }>vldtr)[k]))(val)) : R.is(Array)(val) ? fb.array(val.map((v: any, i: number) => editCtrl(fb, v, vldtr))) : fb.control(val, vldtr); } // ^ still UNUSED, but needed to load in existing values to edit! type _mapNestedFn = (v: T, path: Path) => U; /** * map the contents of a nested yet otherwise homogeneous array (e.g. [1, [[2], 3]]) */ export let mapNestedArr = (f: _mapNestedFn, v: NestedArr, path: Prop[] = []): NestedArr => v.map((x: T|NestedArr, i: number) => mapNestedArr_(f, x, path.concat(i))); let mapNestedArr_ = (f: _mapNestedFn, v: NestedArr|T, path: Prop[] = []): NestedArr|U => R.is(Array)(v) ? (> v).map((x: T|NestedArr, i: number) => mapNestedArr_(f, x, path.concat(i))) : f(v, path); /** * map the contents of a nested yet otherwise homogeneous object (e.g. { a: 1, z: { y: { b: 2 }, c: 3 } }) */ export let mapNestedObj = (f: _mapNestedFn, v: NestedObj, path: Prop[] = []): NestedObj => R.mapObjIndexed((x: T|NestedObj, k: string) => mapNestedObj_(f, x, path.concat(k)))(v); let mapNestedObj_ = (f: _mapNestedFn, v: NestedObj|T, path: Prop[] = []): NestedObj|U => R.is(Object)(v) ? R.mapObjIndexed((x: T|NestedObj, k: string) => mapNestedObj_(f, x, path.concat(k)))(> v) : f(v, path); // export let mapNestedBoth = ... export let falsy = R.either(R.isEmpty, R.not); export let truthy = R.complement(falsy); // differs from JS's 'truthy': []/{} -> false. // look up the property corresponding to a string in a lookup object export let lookup = R.flip(R.prop); // lookup with fallback to key export let lookupOr = R.curry((o: Object, k: string) => lookup(o, k) || k); // // bypass Angular security (aimed at user-supplied data) for safe hardcoded values // const safe = R.objOf('changingThisBreaksApplicationSecurity'); export let callFn = (fn: Fn, thisArg: any, args: IArguments): T => fn.apply(thisArg, Array.prototype.slice.call(args)); // convert an object to a list of string tokens export let tokenize: (obj: Object) => string[] = R.pipe((o: {}) => R.values(o), R.reduce((arr, val) => { let mapper = (v: any) => R.is(String, v) ? v : R.is(Array, v) ? R.chain(mapper)(v) : R.is(Object, v) ? tokenize(v) : // _.isPlainObject []; return arr.concat(mapper(val)); }, [])); // check if a search string matches an object, based on any string tokens contained in both // potential performance improvement: pre-save tokenizations per object export let searchMatches = (search: string) => (obj: Object): boolean => { let tokens = tokenize(obj); return (search || '').split(/[\x00-\x2F]+/) .every((q: string) => tokens.some((token: string) => new RegExp(q, 'i').test(token))); }; // filter a collection by search matches to its string values export let filterNames = (arr: any[], search: string = '') => arr.filter(searchMatches(search)); // coerces an action label string into a string literal, enabling typechecking (TS2.0 tagged union types), and ensuring uniqueness let typeCache: { [label: string]: boolean } = {}; export function type(label: T | ''): T { if (typeCache[label]) { throw new Error(`Action type "${label}" is not unique"`); } typeCache[label] = true; return label; } export let json = JSON.parse; // convert a class instance to a pojo export let pojofy = R.pipe(JSON.stringify, JSON.parse); // debug logging for FP (composition chains) export let trace = R.curry((tag: string, x: any) => { console.log(tag, x); return x; }); // create a url with query params from an object export function makeUrl(url: string, pars = {}): string { return R.keys(pars).length ? `${url}?${toQuery(pars)}` : url; } // convert query/hash params to an object export let fromParams: (str: string) => Obj = R.pipe( R.defaultTo(''), R.split('&'), R.map( R.pipe( R.split('='), // maybe it's different in variable size array and fixed size array to type inferring (parts: string[]):[any, any] => [parts[0], parts[1]] ) ), R.fromPairs, R.map(decodeURIComponent), ); export const firstUpper = (s: string): string => R.toUpper( R.head(s)) + R.tail(s);