import {
always,
applyTo,
both,
compose,
cond,
converge,
curry,
difference,
filter,
keys,
identity,
is,
isEmpty,
map,
merge,
omit,
pathSatisfies,
pick,
pipe,
reduce,
test as regTest,
T,
when
} from 'ramda'
import {remover, keeper, impliedRemove} from './prune'
import {onlySpecTransforms, isPlainObj, applyNonTransformProps} from './util'
import alwaysEvolve from './alwaysEvolve'
import evolveSpec from './evolveSpec'
import mapSpec from './mapSpec'
/**
* Applies transform functions in a given spec to a given object, BUT only the
* trasnforms that DON'T match key names on the input object are applied.
*
* @func
* @sig {k: v} -> {k: v} -> {k: v}
* @param {Object} spec An object which contains transform functions (only those which do not correspond to keys on the input object will be applied to the input object)
* @param {Object} input An object which will be passed through the re-shaping transform functions defined by the spec
* @returns {Object} The input object with the spec transforms applied to it
* (only those that do not correspond to key names on the input object)
*/
export const applyWholeObjectTransforms = curry((spec, value) =>
compose(
mapSpec,
applyTo(spec),
pick,
difference(keys(filter(is(Function), spec))),
keys
)(value)
)
const baseShaper = curry((evolver, spec, value) =>
pipe(
applyNonTransformProps(spec),
evolver(onlySpecTransforms(spec)),
converge(merge, [
identity,
converge(applyTo, [
when(isEmpty, always(value)),
applyWholeObjectTransforms(spec)
])
])
)(value)
)
/**
* This function allows one to blend an object with a clone that is taken
* through prop transformations. Unlike object mappers/transformers, not every
* value in the spec need be a function. Non-function values are simply added to
* the result. If a given value in the spec IS a function however, it will be
* used to transform OR to create a prop. If the prop specified on the spec
* exists on the value (object) being mapped, a prop-level transform will be
* perform (just like [Ramda's evolve()](http://ramdajs.com/docs/#evolve)).
* But if a prop in the spec doesn't exist in the object and if that prop is a
* transformer function, then the transformer will be used to create that prop
* on the resulting object BUT the entire object will be passed into the
* transform function (just like [Ramda's applySpec()](http://ramdajs.com/docs/#applySpec), rather than just a single prop.
*
* @func
* @sig {k: v} -> {k: v} -> {k: v}
* @param {Object} spec An object whose values are either transform functions or pass-through values to be added to the return object
* @param {Object} input An object which will be passed through the re-shaping transform functions defined by the spec
* @returns {Object} The input object with the spec transforms applied to it
*/
export const shapeLoosely = baseShaper(evolveSpec)
/**
* Applies a shaping spec to an input object, but will NOT pass through any props unless they are named in the spec.
* In other words, you're providing it template for creating a brand new object from some raw input.
*
* @func
* @sig {k: v} -> {k: v} -> {k: v}
* @param {Object} spec An object whose values are either transform functions or pass-through values to be added to the return object
* @param {Object} input An object which will be passed through the re-shaping transform functions defined by the spec
* @returns {Object} The input object with the spec transforms applied to it
*/
export const shapeStrictly = curry((spec, input) => compose(pick(keys(spec)), shapeLoosely(spec))(input))
const pruneOutput = curry(
(spec, input) =>
when(
isPlainObj,
cond([
[is(Function), identity],
[both(isPlainObj, pathSatisfies(regTest(/remove/i), ['shapeyMode'])), remover],
[both(isPlainObj, pathSatisfies(regTest(/keep/i), ['shapeyMode'])), keeper],
[both(isPlainObj, pathSatisfies(regTest(/strict/i), ['shapeyMode'])),
sp => pipe(impliedRemove(sp), applyNonTransformProps(sp))],
[isPlainObj, applyNonTransformProps],
[T, always]
])(spec)
)(input)
)
const applyTransforms = curry(
(spec, input) => cond([
[is(Function), identity],
[both(isPlainObj, pathSatisfies(regTest(/prop/i), ['shapeyTransforms'])),
pipe(onlySpecTransforms, alwaysEvolve)],
[both(isPlainObj, pathSatisfies(regTest(/whole/i), ['shapeyTransforms'])),
pipe(onlySpecTransforms, mapSpec)],
[isPlainObj, pipe(onlySpecTransforms, shapeLoosely)],
[T, always]
])(spec)(input)
)
/**
* A single function that selects and applies one of the available re-shaping functions in the shapey library,
* depending on what you've set the `shapeyMode` prop to in the `spec` you provide as the first argument to this function.
*
* Think of it like a case statement in a Redux reducer, however since you most likely _don't_ want to sacrifice
* the meanining associated with the "type" property to internals of shapey, a prop called "shapeyMode" is used instead.
*
* If for some reason you have some prop on the input (the object you're transforming) already named "shapeyMode", um . . . don't.
*
* Available modes (case & space in-sensitive):
* "strict" - uses `shapeStrictly`, where _only_ the props named in your spec are included in the output
* "keep" - uses `keepAndShape`, where all the props you name in your spec are kept.
* "remove" - uses `removeAndShape`, where all the props you name in your spec are removed.
*
* In addition to controlling the mode for Shapey, you can control how the transforms are applied.
* This is controlled via the reserved prop in your spec called "shapeyTransforms", and the available options for it are:
* "prop" - All transforms are applied at the prop-level, regardless if they exist on the input object
* "whole" - All transforms are given the _entire_ input object as input
* (regardless if a prop matching the name of the transform exists on the input object)
*
* @func
* @sig {k: v} -> {k: v} -> {k: v}
* @param {Object} spec An object whose values are either transform functions or pass-through values to be added to the return object
* @param {Object} input An object which will be passed through the re-shaping transform functions defined by the spec
* @returns {Object} The input object with the spec transforms applied to it
*/
const makeShaper = curry((spec, input) => pipe(
applyTransforms(spec),
pruneOutput(spec),
when(isPlainObj, omit(['shapeyTransforms', 'shapeyMode']))
)(input))
/**
* Applies a list of functions (in sequence) to a single input, passing
* the transformed output as the input value to the next function in the chain.
* If a spec object is included in the transforms, the shaper that corresponds
* to the "shapeyMode" prop is invoked (otherwise `shapeLoosely()` is used).
* This creates a transform function for the pipeline.
*
* @func
* @sig [a -> b, b -> c, c -> d, ...] -> * -> *
* @param {Function[]} transforms A list of transform functions
* @param {*} input The input value to pass through the enhancer pipeline
* @returns {*} The output of the original input passed through the chain of transform functions
*/
export const shapeline = curry((transforms, input) =>
reduce((inputObj, fn) => fn(inputObj), input, map(makeShaper, transforms || []))
)
export default makeShaper