Flatten a nested Javascript object any way you like. Designed to be highly customisable Also contains a higly customisable unflattener.
This library is a fully rewritten version of flat.
flat2 has been designed using ES6 classes to make for a more composable, flexible and more maintainable library.
It should now be much easier to test, debug and extend. It is designed to be way more customisable and flexible than the original flat. Please also consider the alternative libs listed at the end of this Readme.
flat2 has been designed to be so flexible that it doesn't even require using objects at all. You can use any kind of data source provided it has a concept of "nesting" and "keys" to be iterated.
It can be as a super-powerful data-transformation library, which can be used on anything from simple objects, to database tables or services, graphs etc.
I originally designed it with the objective to be able to map a React Native stylesheet from the flat structure, using camelCased names to a more natural deeply nested stucture. I needed to separate the value source (styles, a StyleSheet instance) from the source with keys being iterated (Object used to create stylesheet) and then have it unflatten into a new empty object, where each value references the styles by key lookup. This is now possible!
All flattener tests pass except for typed arrays which is rarely used in any case.
I welcome a PR to fix this little issue.
The unflattener is WIP and has not yet been tested. Please help test it and make it an awesome unflattener! It has been designed to be highly customisable as well.
Experimental async support has been added to master branch. Has not yet been tested (WIP). Please help make async flat/unflat blazingly fast and easy to use!
Add async option for Flattener and Unflattener. If set, will call async method/function variant instead.
$ npm install flat2 --save- Unflattener (experimental: WIP)
- Flattener (mostly working)
Creates a flattener, flattens the object and returns the result.
const { flatten } = require('flat2')
const flatObj = flatten({
key1: {
keyA: 'valueI'
},
key2: {
keyB: 'valueII'
},
key3: { a: { b: { c: 2 } } }
})
// {
// 'key1.keyA': 'valueI',
// 'key2.keyB': 'valueII',
// 'key3.a.b.c': 2
// }By default uses . as the delimiter. The key generation can be customised in many ways by passing options.
Instead of using the convenience flatten function, you can also option to instantiate a Flattener instance for reuse, to flatten multiple objects.
The flattener is reset at the start of each flatten operation.
Returns a flattener instance.
const { createFlattener } = require('flat2')
const myFlattener = createFlattener(opts)
// initialize with (optional) target obj
const flattener = createFlattener(opts, obj)Use flattener.flat(obj) to flatten objects
// flatten using initial target
const flatObj1 = flattener.flat()
// reuse flattener and flatten with new target
const flatObj2 = flattener.flat(obj2)You can also import the Flattener class and use new or extend it to create your own custom Flattener
import {
Flattener
} from 'flat2'
class MyFlattener extends Flattener {
// custom overrides/extensions
}
const flattener = new MyFlattener(opts)
/// use it!
const flatObj = flattener.flatten(obj)The following are the supported options, ie. second argument to flatten function (or Flattener constructor)
Use a custom delimiter for (un)flattening your objects, instead of ..
flatten(obj, {
delimiter: ':'
}Use toUpperCase option for flattening your objects and upper case object keys at the same time. This can be handy when working with constants, i.e. API_KEY: 'some key'. This is a built in key transform.
flatten(obj, {
toUpperCase: true
}Use a toLowerCase option for flattening your objects and lower case object keys at the same time. This is a built in key transform.
flatten(obj, {
toLowerCase: true
}Use a built in type of key transformation function:
snakeCase(ie.ale_beta_zeta)camelCase(ie.aleBetaZeta)
flatten(obj, {
keyType: 'snakeCase'
}You can use the following options to further customize how keys are generated
Use a custom function to transform object keys (as opposed to built in transforms such as toLowerCase and toUpperCase). A transform is applied after the key name has been generated.
flatten(obj, {
transformKeyFn: (key) => mySpecialTransform(key)
}Use a custom function to flatten the keyname. By default, the delimiter is inserted between prev and next
Here's an example that uses a colon (:) to both prefix and delimit the keyname
const obj = {
hello: {
world: {
again: 'good morning'
}}}
flatten(obj, { keyNameFn: function(prev, next) {
return prev
? prev + ':' + next // delimit
: ':' + next // prefix
}})
// {
// ':hello:world:again': 'good morning'
// }Use a custom factory function to create the keyname function.
const {
myDefaultKeyNameFn,
createMySpecialKeyNameFn
} = './key-functions'
flatten(obj, {
createKeyNameFn(opts) {
return opts.special ? createMySpecialKeyNameFn(opts) : myDefaultKeyNameFn
}
})Use createFlatKeyFn to pass a factory that generates a function which implements the FlatKey interface (see FlatKeyClass below)
const {
myFlatKey,
} = './key-functions'
flatten(obj, {
createFlatKeyFn(opts) {
return function (opts) {
return {
config(prev, next) {
// ...
}
get name() {
//
}
}
}
}
})Use a custom FlatKey class. Must implement the following interface:
config(key, prev)namegetter
It is used by the default keyNameFn to generate each keyname as follows
return (key, prev) => {
return flatKey.config(key, prev).name
}In case you want to customize the internal flow logic:
To pass a custom createStepper function to override the built-in createStepper factory. You can f.ex extend the Stepper class with your customisations.
function createStepper(object, {
flattener,
prev,
currentDepth
}) {
return new MyStepper(object, {
flattener,
prev,
currentDepth
})
}
flatten(obj, {
createStepper
})To pass a custom createKeyStepper function to override the built-in createKeyStepper factory. You can f.ex extend the KeyStepper class with your customisations.
function createKeyStepper(key, { stepper, flattener }) {
return new MyKeyStepper(key, { stepper, flattener })
}
flatten(obj, {
createKeyStepper
})Pass a transformValue function as option to fine-tune if/how leaf values are transformed. By default the value is returned, but you have access to loads of information to fine-tune if necessary.
function transformValue(value, {
target,
key,
newKey,
prevKey,
ancestorKeys,
lvKeys
}) {
return value
}You can enable safe mode to preserve values such as arrays.
When enabled flat will preserve arrays and their
contents. This is disabled by default.
const flatten = require('flat')
flatten({
this: [
{ contains: 'arrays' },
{ preserving: {
them: 'for you'
}}
]
}, {
safe: true
})
// {
// 'this': [
// { contains: 'arrays' },
// { preserving: {
// them: 'for you'
// }}
// ]
// }Maximum number of nested objects to flatten.
const flatten = require('flat')
flatten({
key1: {
keyA: 'valueI'
},
key2: {
keyB: 'valueII'
},
key3: { a: { b: { c: 2 } } }
}, { maxDepth: 2 })
// {
// 'key1.keyA': 'valueI',
// 'key2.keyB': 'valueII',
// 'key3.a': { b: { c: 2 } }
// }Decide if a value should be flattened any further
const flatten = require('flat')
flatten({
key1: {
keyA: 'valueI'
},
key2: {
keyB: 'valueII'
}
}, { filter: (value) => !value.keyA }) // skip key1
// {
// key1: {
// keyA: 'valueI'
// },
// 'key2.keyB': 'valueII'
// }The Flattener and Stepper classes comes with built-in event handlers to maintain stacks of visited nodes. You can override these event handler to suit your own needs.
onKey(key, depth)onStepsDone(depth)
These events are used to build the stack of ancestorKeys visited while traversing a branch depth first.
onKey(key)
A stepper steps through all keys on one depth level.
The onKey handler of stepper is used to build a stack of visited keys on a particular depth level. It also calls onKey for the flattener to allow it to build up its stack.
You can pass a boolean logging option to enable/disable logging.
Add a logOnly option to limit which "classes" should log. A class is any object that has a name property (note: name will default to constructor.name).
flatten({
HELLO: {
WORLD: {
AGAIN: 'good morning'
}
}
}, {
toLowerCase: true,
logging: true,
logOnly: ['Flattener']
})Currently you can supply a logWhen option that takes an object which can be an instance of either:
FlattenerStepperKeyStepperFlatKey
All of these instances should have access to flattener where you have access to attributes such as:
currentDepthlastAncestorKeyancestorKeyslvKeys(via currentstepperattached)
You can use these attributes to further control when to log.
See TODO.md for our Logger plans.
You can pass your own custom logger in the new logger option.
The custom logger can implement one or more of the following:
log(...args)objLog(...args)
The default Logger methods:
log(...args) {
this.shouldLog && logInfo(this.name, ...args)
}
objLog(...args) {
this.shouldLog && logObj(this.name, ...args)
}Your custom logger can be an instance of your custom logger class that extends Logger (also exported for convenience)
Example:
import {
Logger,
// ...
} from 'flat2`
class MyLogger extends Logger {
constructor(flattener, opts) {
super(opts)
this.flattener = flattener
}
get shouldLog() {
// custom conditions
}
}
const flattener = createFlattener(obj, opts)
const loggingOpts = {
logging: true,
// ... custom opts ?
}
const logger = new MyLogger(flattener, loggingOpts)
flattener.logger = logger
// now ready to use flattener with new logger!We would welcome a PR with more detailed logging constraints, such as current depth level, keys visited etc. and using composition instead of deep inheritance
It might sometimes be useful to have a way to post generate the output values after the fact using the information available at the time of generation for each flat key.
This can f.ex be useful for mapping values (pointers) from the nested to the flat structure.
You can now pass a subscribeValue callback, which can be called after the fact by the flattener.
import {
leaf,
// ... more refs imported
} from 'flat2'
function subscribeValue(newKey, newValue, {
key,
target,
ancestorKeys
})
// dig down cia old ancestor key chain and set newValue on new target
leaf(target, ancestorKeys, newValue, {
// any extra leaf options
})
}The function leaf is also included and exported.
function leaf(obj, path, value, opts = {}) {
// create leaf node in obj at path with value
return obj
}This way you can map the deeply nested leaf values of a target object to point to leaf values in an obj with the flat key structure.
// assuming flatStruct has structure mirroring object of returned by flatten
const styles = createStyleSheet(flatObj)
const nestedStyles = {}
flattener.publishObj(styles, target)Now nestedStyles.card.container will point to styles.cardContainer and so on ;)
Note: You can also publish key/value pair individually using flattener.publish(key, value, target)
See also: Safely accessing deeply nested values
For this use case you can alternatively use unflatten with a custom pathFinder option, but you might be more contrained. The publish/subscribe is more powerful.
The unflattener has yet to be thoroughly tested. Please see the current test suite. I have yet to run the unflattener even once!
Please help make the unflattener work with all the infrastructure options added so far. It will be awesome!
The unflattener be used as follows:
import {
createUnflattener,
unflatten
} from 'flat2'
const unflattener = createUnflattener(opts, nestedObj)
const obj = unflattener.unflat()
// alternative
const obj2 = unflatten(nestedObj)By default, unflatten creates and flattens into an empty object. You can change this by overriding the method createInitialOutputObj
You can also unflatten into existing object by passing in an output option with the output object to use as base.
const modifiedExistingObj = unflatten(nestedObj, {output: existingObj})buildPathdelimitervalueSrclookupValueAtkeysgetKeysskipKeykeysFilterdefaultsmakeLeaf
Function to generate a deep path (list of strings) from a flat key
buildPath(flatKey, opts)
Can f.ex be used to generate a path from a camelCased string using some regular expression "magic" (see default delimiters below).
Delimiter to use for default path builder if no (specialised) buildPath function is provided.
The delimiters currently included are:
default(splits on.)camelCasea complex RegExp taken from regex to split camelCase or titleCase
Use valueSrc to provide a value source to lookup values for the flat keys iterated.
By default, the valueSrc is the target object (where keys are looked up), but it can be any external resource as well.
lookupValueAt(flatKey)
Use lookupValueAt to provide a custom way to lookup the value for a key being iterated. By default, it simply returns the value for the key of the target object.
If provided, defines the exact set of keys to be iterated.
By default the getKeys function is used to determine the keys.
Use getKeys to provide a custom method to get the keys to be iterated.
By default simply uses Object.keys on the valueSrc (which by default is the target object)
Use to skip any key while iterating. A more "dynamic" alternative to filter on the keys list.
Quick way to filter out any keys in the exclude list (string list) provided.
Note: keysFilter will still be run after exclusion.
Use to up-front filter out any keys for iteration that match some pre-defined criteria.
The Unflattener contains a defaults property with the following defaults
{
makeLeaf: leaf,
buildPath(key) {
return key.split(this.delimiter)
},
delimiters: {
default: '.',
camelCase: /([A-Z]+|[A-Z]?[a-z]+)(?=[A-Z]|\b)/
}
}These defaults can be overriden by setting _defaults or by overriding the defaults property (getter).
Function to create a new (deeply nested) leaf node using the path and value
makeLeaf(obj, path, value, opts = {})
Sets nested key/value on obj which is returned.
Arguments:
objis the target (output) object (can be empty or pre-populated)pathis an array of keys or a string with delimitervalueis the value to set for the key in the (deeply nested) pathoptsoptions to use for fine-control
The makeLeaf options (ie. opts) are retrieved using leafOpts property (getter) of Unflattener. By default it passes on all opts of Unflattener and the current flatKey being processed.
The leaf options can be categorized as follows:
- Basic
- Validation and Logging
- Advanced
delimiterdelimiter used by split, usually a string or regular expression (default:.)startDepththe initial depth, (default:0)shallowshallow or expand mode (default:true)keyName(pathKey, opts)function to transform path key (alias:keynamesfor compatibility with originalflat)
validateOnwhether validation of options is turned on (default:false)validator(opts)factory to create a set of validator functionslogger(opts)logger to use
defaultspass in your own custom set of default optionsmakePointer(key, value, opts)custom function to create the pointer (deep path) in the object. Pointer returned must havevalueandsetValue(value)selectAt(acc, key, selectOpts)function to select the next key and value (if leaf into existing object)accValue(value, opts)calculate next accumulator value, ifundefinedset to{}undefined` in order to dig down into objectnextDepth(depth, depthOpts, opts)function to calculate next depth, (default: increments by 1)leafValuefunctionality to set the leaf valueisObj(value)function to test if a value is a valid object (ie. can be iterated with keys)expander(value, opts)function to use in expand mode (default:unflattenitself!)
The leafValue option can be used to control how the leaf value is generated.
It must be an object with:
select(opts)function to select what leaf value method to use based on optionsshallow(value, opts)function to use inshallowmodeexpand(value, opts)function to use inexpandmode
defaults:
select(opts)will selectshallowmode ifshallowoption is set (true)shallow(value, opts)is the identity functionexpand(value, opts)will call the passed-inexpanderfunction on the value to expand it further (only if value is a valid object)
You can use the following options for fine-grained control:
selectKey(acc, key, keyMatchers, opts)function to select the next keykeyQueryMatcher(keys, keyQuery)generic query matcher function to use for key matcherskeyMatcherslist of key matcher functions for special keys such as aRegExpoverwritetrue or false
makePointer(key, value, opts)
Function makePointer is used to calculate and create the pointer used to set the leaf value on. It takes the following options:
keyName(pathKey, opts)function to transform path key (alias:keynamesfor flat for compatibility with originalflat)stopCondition(conditionOpts)function to determine if/when to stop "digging" (default:reachedMaxDepth)whenStopped(acc, opts)what to do when stopped (default: return current accumulator viaidentiyfunction)maxDepthmax depth to dig when using defaultreachedMaxDepthasstopCondition(default:10)selectAt(acc, key, selectOpts)function to select the next key and valueaccValue(value, opts)calculate next accumulator value, ifundefinedset to{}in order to dig further down into objectfetchValue(acc, key, fetchOpts)function to fetch the value with the selected keynextDepth(depth, depthOpts, opts)function to calculate next depth, (default: increments by 1)validateOnwhether validation of options is turned on (default:false)validator(opts)factory to create a set of validator functionslogger(opts)logger to useshallowshallow or expand mode (default:true)leafValuefunctionality to set the leaf valueisExistingObject(obj, opts)used to determine if we are making pointer into existing objectcreatePointer(ref, key, opts)create the pointer to be returned ie. by default an object withvalueandsetValue(value)
The built-in leaf function uses the following default options
{
delimiter: '.',
reachedMaxDepth({ // default stopCondition
maxDepth,
depth
}) {
return depth > maxDepth
},
identity(value) {
return value
},
makePointer(obj, path, opts = {}) {
// ...
}
}There are a host of alternative solutions you might want to consider...