Skip to content

Commit

Permalink
Merge pull request #9 from JaapRood/feature/defaults
Browse files Browse the repository at this point in the history
Add defaults as option to type.factory
  • Loading branch information
JaapRood authored May 4, 2018
2 parents 4ac5d61 + b6b6be0 commit ce34730
Show file tree
Hide file tree
Showing 8 changed files with 102 additions and 32 deletions.
10 changes: 8 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,7 @@ Returns a new instance (`Immutable.Map`) of the state type using the type's defa
- `attributes` - `object` or any `Immutable.Iterable` of key-value pairs with which the type defaults will be overridden and amended. Nested `object`s and `array`s are converted to `Immutable.Map`and `Immutable.List` respectively, while any `Immutable.Iterable`s will be left untouched.
- `options` - `object` with the following keys
- `parse` - `function` as described by `state.parse` that is to be used instead of `state.parse` to transform the passed in `attributes`.
- `defaults` - plain `object` or `Immutable.Iterable` of default key-value pairs that are used as the base of the instance, instead of those defined for the `State`.

```js
const User = Vry.State.create('user', {
Expand Down Expand Up @@ -249,6 +250,8 @@ A place to implement custom parsing behaviour. This method gets called by `state
Must return a `Immutable.Iterable`. Any `Immutable.Seq`s returned are converted into `Immutable.Map` and `Immutable.List`. By default this method is a no-op, simply returning the attributes passed in.

- `attributes` - (required) `Immutable.Map` of attributes. Any plain `object`s or `array`s are represented as `Immutable.Seq`s (`Keyed` and `Indexed` respectively), making it easy to deal with nested collections with a uniform API and giving you the opportunity to convert them to something else like a `Set`.
- `options` - `object` with the following keys
- `defaults` - plain `object` or `Immutable.Iterable` of default key-value pairs that are used as the base of the instance, instead of those defined for the `State`.

```js
const User = Vry.State.create('user', {
Expand All @@ -257,7 +260,7 @@ const User = Vry.State.create('user', {
activated: false
})

User.parse = (attributes) => {
User.parse = (attributes, options) => {
// Make sure names start with a capital
const name = attributes.get('name')

Expand Down Expand Up @@ -428,7 +431,9 @@ Returns a new instance (`Immutable.Map`) of the model using the model's defaults

- `attributes` - `object` or any `Immutable.Iterable` of key-value pairs with which the type defaults will be overridden and amended. The model's schema is used to handle the creation of nested types. Nested `object`s and `array`s are converted to `Immutable.Map`and `Immutable.List` respectively, while any `Immutable.Iterable`s will be left untouched.
- `options` - `object` with the following keys
- `parse` - `function` as described by `state.parse` that is to be used instead of `state.parse` to transform the passed in `attributes`.
- `parse` - `function` as described by `state.parse` that is to be used instead of `model.parse` to transform the passed in `attributes`.
- `defaults` - plain `object` or `Immutable.Iterable` of default key-value pairs that are used as the base of the instance, instead of those defined for the model. Nested values are propagated to nested models.
- `schema` - schema definition that describes any nested models, to be used instead of the schema defined for the model. See the documentation for [`Schema`](#schema).

```js

Expand Down Expand Up @@ -527,6 +532,7 @@ By default it uses the `Schema` of the model to defer the parsing of other neste
- `attributes` - (required) `Immutable.Map` of attributes. Any plain `object`s or `array`s are represented as `Immutable.Seq`s (`Keyed` and `Indexed` respectively), making it easy to deal with nested collections with a uniform API and giving you the opportunity to convert them to something else like a `Set`.
- `options` - `object` with values for the following keys
- `schema` - schema definition that describes any nested models, to be used instead of the schema defined for the model. See the documentation for [`Schema`](#schema).
- `defaults` - plain `object` or `Immutable.Iterable` of default key-value pairs that are used as the base of the instance, instead of those defined for the `Model`. Nested values are propagated to nested models.

```js
const User = Vry.Model.create({
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "vry",
"version": "2.1.0",
"version": "2.1.1",
"description": "Define `Model` and `Collection` like types to manage your `Immutable` data structures in a functional way",
"main": "lib/index.js",
"scripts": {
Expand Down Expand Up @@ -33,6 +33,7 @@
"lodash.isstring": "^4.0.1",
"lodash.isundefined": "^3.0.1",
"lodash.keys": "^4.0.7",
"lodash.omit": "^4.5.0",
"lodash.uniqueid": "^3.0.0",
"shortid": "^2.2.4",
"warning": "^3.0.0"
Expand Down
5 changes: 4 additions & 1 deletion src/factory.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
const Invariant = require('invariant')
const Immutable = require('immutable')
const ShortId = require('shortid')
const _assign = require('lodash.assign')
const _isPlainObject = require('lodash.isplainobject')
const _isFunction = require('lodash.isfunction')
const _isArray = require('lodash.isarray')
Expand Down Expand Up @@ -35,11 +36,13 @@ internals.factory = function(rawEntity={}, options={}) {
, 'Raw entity, when passed, must be a plain object or Immutable Iterable')
Invariant(_isPlainObject(options), 'options, when passed, must be a plain object')
Invariant(!options.parse || _isFunction(options.parse), 'The `parse` prop of the options, when passed, must be a function')
Invariant(!options.defaults || exports.isDefaults(options.defaults), 'The `defaults` prop of the options, when passed, must be plain object or Immutable Iterable')

var parse = options.parse || this.parse || ((attrs) => attrs)
var defaults = Immutable.Map(options.defaults || this.defaults() || {})

// merge with with defaults and cast any nested native selections to Seqs
var entity = this.defaults().merge(Immutable.Map(rawEntity)).map((value, key) => {
var entity = defaults.merge(Immutable.Map(rawEntity)).map((value, key) => {
if (Immutable.Iterable.isIterable(value)) {
return value
}
Expand Down
20 changes: 15 additions & 5 deletions src/model.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,12 +81,15 @@ exports.create = (spec) => {

internals.parse = function(attrs, options={}) {
const schema = options.schema || this.schema()
const modelDefaults = Immutable.Map(options.defaults || this.defaults())

return attrs.map((modelValue, modelProp) => {
const definition = Schema.getDefinition(schema, modelProp)
const defaults = modelDefaults.get(modelProp)

// if no type was defined for this prop there is nothing for us to do
if (!modelValue || !definition) return modelValue;


if (Schema.isType(definition)) {
let type = definition
Expand All @@ -99,22 +102,29 @@ internals.parse = function(attrs, options={}) {
// if the value is already and instance of what we're trying to make it
// there is nothing for us to do
return modelValue
}

return type.factory(modelValue)
}

return type.factory(modelValue, { defaults })
} else if (Schema.isSchema(definition)) {
let nestedSchema = definition

if (nestedSchema.getItemSchema) { // could be an Iterable schema
let itemSchema = nestedSchema.getItemSchema()

return nestedSchema.factory(this.parse(modelValue, { schema: itemSchema }))
// I'm not sure why we did this parsing recursively like this, rather than letting the type itself
// deal with parsing itself. I'll leave it for while, in case we just didn't cover the use-case
// with a test properly.
// ---------------------
//
// return nestedSchema.factory(this.parse(modelValue, { schema: itemSchema }), { defaults })

return nestedSchema.factory(modelValue, { defaults })

} else if ( // support plain objects and arrays as they'll automatically get cast properly
Immutable.Iterable.isIndexed(modelValue) && _isArray(nestedSchema) ||
Immutable.Iterable.isKeyed(modelValue) && _isPlainObject(nestedSchema)
) {
return this.parse(modelValue, { schema: nestedSchema })
return this.parse(modelValue, { schema: nestedSchema, defaults })
}
}

Expand Down
11 changes: 6 additions & 5 deletions src/schema.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,24 @@ const _every = require('lodash.every')
const _isArray = require('lodash.isarray')
const _isPlainObject = require('lodash.isplainobject')
const _isFunction = require('lodash.isfunction')
const _omit = require('lodash.omit')

const internals = {}

internals.idenity = (val) => val
internals.identity = (val) => val

internals.IterableSchema = function(iterable, itemSchema) {
Invariant(_isFunction(iterable), 'Iterable constructor required to create IterableSchema')
Invariant(exports.isType(itemSchema) || exports.isSchema(itemSchema), 'Item schema or type required to create IterableSchema')

_assign(this, {
getItemSchema: () => itemSchema,
factory: (val, ...otherArgs) => {
const factory = itemSchema.factory || internals.idenity
return iterable(val).map((item) => factory(item, ...otherArgs))
factory: (val, options, ...otherArgs) => {
const factory = itemSchema.factory || internals.identity
return iterable(val).map((item) => factory(item, _omit(options, 'defaults'), ...otherArgs))
},
serialize: (val, ...otherArgs) => {
const serialize = itemSchema.serialize || internals.idenity
const serialize = itemSchema.serialize || internals.identity
return val.map((item) => serialize(item, ...otherArgs)).toJS()
},
mergeDeep: (current, next) => {
Expand Down
36 changes: 30 additions & 6 deletions test/model.js
Original file line number Diff line number Diff line change
Expand Up @@ -75,12 +75,14 @@ Test('Model.create', function(t) {
})

Test('model.factory - parse', function(t) {
t.plan(1 + 10 + 2 + 1)
t.plan(3 + 1 + 1 + 10 + 2 + 1)

const OtherModel = Model.create({
typeName: 'woo'
})

const defaultA = 'a-default';
const defaultB = 'b-default';
const inputA = 'a';
const outputA = 'A';
const outputB = 'B';
Expand All @@ -89,15 +91,20 @@ Test('model.factory - parse', function(t) {

const schema = {
a: {
factory: (value) => {
factory: (value, options) => {
t.equal(inputA, value, 'factory is called with the untransformed value');

t.ok(_.isPlainObject(options), 'factory is called with options')
t.equal(options.defaults, defaultA, 'default value for prop is forwarded to factory as defaults option')

return outputA;
}
},
nested: {
b: {
factory: () => outputB
factory: (value, options) => {
t.equal(options.defaults, defaultB, 'nested schema factory is called with the nested default value')
return outputB
}
}
},
nestedArray: [{
Expand All @@ -109,8 +116,17 @@ Test('model.factory - parse', function(t) {
instanceOf() { return true }
},

iterableSchema: {
getItemSchema: () => ({factory: (val) => val }),
factory(val, options) {
t.deepEqual(options.defaults, [defaultA], 'default value for iterable prop is forwarded to its factory')

return Immutable.List(val)
}
},

nestedList: Schema.listOf({
factory: (val) => val
factory: (val, options) => val
}),

nestedSet: Schema.setOf({
Expand All @@ -128,6 +144,14 @@ Test('model.factory - parse', function(t) {
})

t.doesNotThrow(() => {
const defaults = {
a: defaultA,
nested: {
b: defaultB
},
nestedList: [defaultA],
iterableSchema: [defaultA]
}
const attrs = {
nonDefined: 'prop',
a: inputA,
Expand All @@ -145,7 +169,7 @@ Test('model.factory - parse', function(t) {
nestedOrderedSet: [outputC, outputB, outputA]
}

const instance = TestModel.factory(attrs)
const instance = TestModel.factory(attrs, { defaults })

t.equal(instance.get('a'), outputA, 'value returned by factory of schema is used as value')
t.equal(instance.get('nonDefined'), 'prop', 'parser ignores any attributes not defined in schema')
Expand Down
21 changes: 12 additions & 9 deletions test/schema.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,15 @@ Test('Schema.isType', function(t) {
})

Test('Schema.listOf', function(t) {
t.plan(11)
t.plan(13)

const factoryOptions = { optionsFor: 'factory' }
const factoryOptions = { optionsFor: 'factory', defaults: 'should-not-be-forwarded' }
const serializeOptions = { optionsFor: 'serialize' }

const itemType = {
factory: (val, options) => {
t.deepEqual(factoryOptions, options, 'options are forwarded to the item schema factory')
t.deepEqual(_.omit(factoryOptions, 'defaults'), options, 'options are forwarded to the item schema factory')
t.notOk(options.defaults, 'defaults option is not forwarded to the item schema factory')
return val + 'modified'
},
serialize: (val, options) => {
Expand Down Expand Up @@ -53,14 +54,15 @@ Test('Schema.listOf', function(t) {
})

Test('Schema.setOf', function(t) {
t.plan(11)
t.plan(13)

const factoryOptions = { optionsFor: 'factory' }
const factoryOptions = { optionsFor: 'factory', defaults: 'should-not-be-forwarded' }
const serializeOptions = { optionsFor: 'serialize' }

const itemType = {
factory: (val, options) => {
t.deepEqual(factoryOptions, options, 'options are forwarded to the item schema factory')
t.deepEqual(_.omit(factoryOptions, 'defaults'), options, 'options are forwarded to the item schema factory')
t.notOk(options.defaults, 'defaults option is not forwarded to the item schema factory')
return val + 'modified'
},
serialize: (val, options) => {
Expand Down Expand Up @@ -91,14 +93,15 @@ Test('Schema.setOf', function(t) {
})

Test('Schema.orderedSetOf', function(t) {
t.plan(11)
t.plan(13)

const factoryOptions = { optionsFor: 'factory' }
const factoryOptions = { optionsFor: 'factory', defaults: 'should-not-be-forwarded' }
const serializeOptions = { optionsFor: 'serialize' }

const itemType = {
factory: (val, options) => {
t.deepEqual(factoryOptions, options, 'options are forwarded to the item schema factory')
t.deepEqual(_.omit(factoryOptions, 'defaults'), options, 'options are forwarded to the item schema factory')
t.notOk(options.defaults, 'defaults option is not forwarded to the item schema factory')
return val + 'modified'
},
serialize: (val, options) => {
Expand Down
28 changes: 25 additions & 3 deletions test/state.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ Test('state.defaults', function(t) {
})

Test('state.factory', function(t) {
t.plan(6 + 3 + 2 + 1);
t.plan(6 + 3 + 2 + 1 + 3);


t.doesNotThrow(function() {
Expand Down Expand Up @@ -105,10 +105,23 @@ Test('state.factory', function(t) {
var state = State.create('test-state', { a: 1, b: 3});
state.factory.call(null)
}, 'can be called without context')

t.doesNotThrow(function() {
var state = State.create('test-state', { a: 1, b: 2})
var defaultsOption = { b: 3, c: {}, d: 5 }

var instance = state.factory({}, { defaults: defaultsOption })

t.deepEqual(
_.omit(instance.toJSON(), ['__typeName', '__cid']),
defaultsOption
, 'returned instance uses defaults passed as an option')
t.notOk(instance.has('a'), 'when defaults option is passed, state level defaults are not used')
}, 'accepts an object of defaults as an option')
});

Test('state.factory - parse', function(t) {
t.plan(10);
t.plan(12);



Expand All @@ -130,9 +143,18 @@ Test('state.factory - parse', function(t) {
f: Immutable.OrderedSet()
};

var testDefaults = {
a: 4
}

var instance = state.factory(rawInstance, {
parse: function(attrs) {
defaults: testDefaults,

parse: function(attrs, options) {
t.ok(Immutable.Map.isMap(attrs), 'parse function is called with attributes as a Map');
t.ok(_.isPlainObject(options), 'parse function is called with options');
t.deepEqual(options.defaults, testDefaults, 'options contains the defaults option passed to the factory');


return attrs.map(function(value, key) {
if (key === 'a') {
Expand Down

0 comments on commit ce34730

Please sign in to comment.