Comparing version 0.2.0 to 0.3.0
{ | ||
"name": "rosie", | ||
"version": "0.2.0", | ||
"version": "0.3.0", | ||
"main": "src/rosie.js", | ||
@@ -21,2 +21,2 @@ "ignore": [ | ||
} | ||
} | ||
} |
@@ -1,11 +0,31 @@ | ||
{ "name": "rosie" | ||
, "version": "0.2.0" | ||
, "description": "factory for building JavaScript objects, mostly useful for setting up test data. Inspired by factory_girl" | ||
, "keywords": ["factory", "rosie", "test"] | ||
, "author": "Brandon Keepers <brandon@opensoul.org>" | ||
, "contributors": [] | ||
, "dependencies" : {} | ||
, "engines": { "node": "*" } | ||
, "repository": { "type": "git", "url": "git://github.com/bkeepers/rosie.git" } | ||
, "main": "src/rosie.js" | ||
} | ||
{ | ||
"name": "rosie", | ||
"version": "0.3.0", | ||
"description": "factory for building JavaScript objects, mostly useful for setting up test data. Inspired by factory_girl", | ||
"keywords": [ | ||
"factory", | ||
"rosie", | ||
"test" | ||
], | ||
"author": "Brandon Keepers <brandon@opensoul.org>", | ||
"contributors": [], | ||
"dependencies": {}, | ||
"devDependencies": { | ||
"karma": "^0.12.1", | ||
"karma-chrome-launcher": "^0.1.2", | ||
"karma-jasmine": "^0.1.5", | ||
"karma-phantomjs-launcher": "^0.1.2" | ||
}, | ||
"engines": { | ||
"node": "*" | ||
}, | ||
"repository": { | ||
"type": "git", | ||
"url": "git://github.com/bkeepers/rosie.git" | ||
}, | ||
"main": "src/rosie.js", | ||
"scripts": { | ||
"test": "./node_modules/karma/bin/karma start --singleRun true", | ||
"watch": "./node_modules/karma/bin/karma start" | ||
} | ||
} |
@@ -16,7 +16,10 @@ # Rosie | ||
.attr('random_seed', function() { return Math.random(); }) | ||
.attr('players', function() { | ||
return [ | ||
Factory.attributes('player'), | ||
Factory.attributes('player') | ||
]; | ||
// Default to two players. If players were given, fill in | ||
// whatever attributes might be missing. | ||
.attr('players', ['players'], function(players) { | ||
if (!players) { players = [{}, {}]; } | ||
return players.map(function(data) { | ||
return Factory.attributes('player', data); | ||
}); | ||
}); | ||
@@ -26,4 +29,10 @@ | ||
.sequence('id') | ||
.sequence('name', function(i) { return 'player' + i; }); | ||
.sequence('name', function(i) { return 'player' + i; }) | ||
// Define `position` to depend on `id`. | ||
.attr('position', ['id'], function(id) { | ||
var positions = ['pitcher', '1st base', '2nd base', '3rd base']; | ||
return positions[id % positions.length]; | ||
}); | ||
Factory.define('disabled-player').extend('player').attr('state', 'disabled') | ||
@@ -51,11 +60,33 @@ | ||
Factory.attributes('game') // return just the attributes | ||
You can also define a callback function to be run after building an object: | ||
Factory.define('coach').after(function(coach, options) { if (options.buildPlayer) { Factory.build('player', {coach_id: coach.id}; } }) | ||
Factory.define('coach') | ||
.option('buildPlayer', false) | ||
.sequence('id') | ||
.attr('players', ['id', 'buildPlayer'], function(id, buildPlayer) { | ||
if (buildPlayer) { | ||
return [Factory.build('player', {coach_id: id})]; | ||
} | ||
}) | ||
.after(function(coach, options) { | ||
if (options.buildPlayer) { | ||
console.log('built player:', coach.players[0]); | ||
} | ||
}); | ||
Factory.build('coach', {}, {buildPlayer: true}); | ||
## Contributing | ||
0. Fork it | ||
0. Create your feature branch (`git checkout -b my-new-feature`) | ||
0. Install the test dependencies (`script/bootstrap` - requires NodeJS and npm) | ||
0. Make your changes and make sure the tests pass (`npm test`) | ||
0. Commit your changes (`git commit -am 'Added some feature'`) | ||
0. Push to the branch (`git push origin my-new-feature`) | ||
0. Create new Pull Request | ||
## Credits | ||
Thanks to [Daniel Morrison](http://twitter.com/danielmorrison/status/58883772040486912) for the name and [Jon Hoyt](http://twitter.com/jonmagic) for inspiration and brainstorming the idea. |
@@ -7,3 +7,3 @@ describe('Factory', function() { | ||
describe('build', function() { | ||
describe('with a constructor', function() { | ||
describe('with a normal constructor', function() { | ||
var Thing = function(attrs) { | ||
@@ -31,6 +31,42 @@ for(var attr in attrs) { | ||
it('should run callbacks', function() { | ||
expect(Factory.build('thing').afterCalled).toBe(true); | ||
expect(Factory.build('thing').afterCalled).toBe(true); | ||
}); | ||
}); | ||
describe('with a constructor with a .create() function', function() { | ||
var afterArgs; | ||
var createArgs; | ||
var built; | ||
var created; | ||
// i.e. an Ember class | ||
var Thing = { | ||
create: function() { | ||
createArgs = [].slice.call(arguments); | ||
created = {}; | ||
return created; | ||
} | ||
}; | ||
beforeEach(function() { | ||
createArgs = afterArgs = null; | ||
Factory.define('thing', Thing).attr('name', 'Thing 1').after(function() { | ||
afterArgs = [].slice.call(arguments); | ||
}); | ||
built = Factory.build('thing'); | ||
}); | ||
it('should run callbacks', function() { | ||
expect(afterArgs).toEqual([created, /* options = */undefined]); | ||
}); | ||
it('should call .create on the class with the attributes', function() { | ||
expect(createArgs).toEqual([{name: 'Thing 1'}]); | ||
}); | ||
it('should return the value returned by create', function() { | ||
expect(created).toBe(built); | ||
}); | ||
}); | ||
describe('without a constructor', function() { | ||
@@ -48,2 +84,7 @@ beforeEach(function() { | ||
}); | ||
it('throws error if the factory is not defined', function() { | ||
expect(function(){Factory.build('nothing')}) | ||
.toThrow('The "nothing" factory is not defined.'); | ||
}); | ||
}); | ||
@@ -147,2 +188,53 @@ }); | ||
}); | ||
it('should allow depending on other attributes', function() { | ||
factory | ||
.attr('fullName', ['firstName', 'lastName'], function(first, last) { | ||
return first + ' ' + last; | ||
}) | ||
.attr('firstName', 'Default') | ||
.attr('lastName', 'Name'); | ||
expect(factory.attributes()) | ||
.toEqual({ | ||
firstName: 'Default', | ||
lastName: 'Name', | ||
fullName: 'Default Name' | ||
}); | ||
expect(factory.attributes({ firstName: 'Michael', lastName: 'Bluth' })) | ||
.toEqual({ | ||
fullName: 'Michael Bluth', | ||
firstName: 'Michael', | ||
lastName: 'Bluth' | ||
}); | ||
expect(factory.attributes({ fullName: 'Buster Bluth' })) | ||
.toEqual({ | ||
fullName: 'Buster Bluth', | ||
firstName: 'Default', | ||
lastName: 'Name' | ||
}); | ||
}); | ||
it('throws when building when a dependency cycle is unbroken', function() { | ||
factory | ||
.option('rate', 0.0275) | ||
.attr('fees', ['total', 'rate'], function(total, rate){ return total * rate; }) | ||
.attr('total', ['fees', 'rate'], function(fees, rate){ return fees / rate; }); | ||
expect(function(){ factory.build(); }).toThrow('detected a dependency cycle: fees -> total -> fees'); | ||
}); | ||
it('always calls dynamic attributes when they depend on themselves', function() { | ||
factory.attr('person', ['person'], function(person) { | ||
if (!person) { person = {}; } | ||
if (!person.name) { person.name = 'Bob'; } | ||
return person; | ||
}); | ||
expect(factory.attributes({ person: { age: 55 }})).toEqual({ | ||
person: { name: 'Bob', age: 55 } | ||
}); | ||
}); | ||
}); | ||
@@ -193,3 +285,40 @@ | ||
}); | ||
describe('option', function() { | ||
beforeEach(function() { | ||
factory.option('useCapsLock', false); | ||
}); | ||
it('should return the factory', function() { | ||
expect(factory.option('rate')).toBe(factory); | ||
}); | ||
it('should not create attributes in the build result', function() { | ||
expect(factory.attributes().useCapsLock).toBeUndefined(); | ||
}); | ||
it('throws when no default or value is given', function() { | ||
factory.option('someOptionWithoutAValue'); | ||
expect(function(){ factory.attributes(); }).toThrow('option `someOptionWithoutAValue` has no default value and none was provided'); | ||
}); | ||
it('should be usable by attributes', function() { | ||
var useCapsLockValues = []; | ||
factory.attr('name', ['useCapsLock'], function(useCapsLock) { | ||
useCapsLockValues.push(useCapsLock); | ||
var name = 'Madeline'; | ||
if (useCapsLock) { | ||
return name.toUpperCase(); | ||
} else { | ||
return name; | ||
} | ||
}); | ||
// use default values | ||
expect(factory.attributes().name).toEqual('Madeline'); | ||
// override default values | ||
expect(factory.attributes({}, { useCapsLock: true }).name).toEqual('MADELINE'); | ||
expect(useCapsLockValues).toEqual([false, true]); | ||
}); | ||
}); | ||
}); | ||
}); |
369
src/rosie.js
@@ -0,4 +1,13 @@ | ||
/** | ||
* Creates a new factory with attributes, options, etc. to be used to build | ||
* objects. Generally you should use `Factory.define()` instead of this | ||
* constructor. | ||
* | ||
* @param {?Function} constructor | ||
* @class | ||
*/ | ||
var Factory = function(constructor) { | ||
this.construct = constructor; | ||
this.attrs = {}; | ||
this.opts = {}; | ||
this.sequences = {}; | ||
@@ -9,18 +18,122 @@ this.callbacks = []; | ||
Factory.prototype = { | ||
attr: function(attr, value) { | ||
var callback = typeof value == 'function' ? value : function() { return value; }; | ||
this.attrs[attr] = callback; | ||
/** | ||
* Define an attribute on this factory. Attributes can optionally define a | ||
* default value, either as a value (e.g. a string or number) or as a builder | ||
* function. For example: | ||
* | ||
* // no default value for age | ||
* Factory.define('Person').attr('age') | ||
* | ||
* // static default value for age | ||
* Factory.define('Person').attr('age', 18) | ||
* | ||
* // dynamic default value for age | ||
* Factory.define('Person').attr('age', function() { | ||
* return Math.random() * 100; | ||
* }) | ||
* | ||
* Attributes with dynamic default values can depend on options or other | ||
* attributes: | ||
* | ||
* Factory.define('Person').attr('age', ['name'], function(name) { | ||
* return name === 'Brian' ? 30 : 18; | ||
* }); | ||
* | ||
* By default if the consumer of your factory provides a value for an | ||
* attribute your builder function will not be called. You can override this | ||
* behavior by declaring that your attribute depends on itself: | ||
* | ||
* Factory.define('Person').attr('spouse', ['spouse'], function(spouse) { | ||
* return Factory.build('Person', spouse); | ||
* }); | ||
* | ||
* As in the example above, this can be a useful way to fill in | ||
* partially-specified child objects. | ||
* | ||
* @param {string} attr | ||
* @param {?Array.<string>} dependencies | ||
* @param {*} value | ||
* @return {Factory} | ||
*/ | ||
attr: function(attr, dependencies, value) { | ||
var builder; | ||
if (arguments.length === 2) { | ||
value = dependencies; | ||
dependencies = null; | ||
} | ||
builder = typeof value === 'function' ? value : function() { return value; }; | ||
this.attrs[attr] = { dependencies: dependencies || [], builder: builder }; | ||
return this; | ||
}, | ||
sequence: function(attr, callback) { | ||
var factory = this; | ||
callback = callback || function(i) { return i; }; | ||
this.attrs[attr] = function() { | ||
factory.sequences[attr] = factory.sequences[attr] || 0; | ||
return callback(++factory.sequences[attr]); | ||
}; | ||
/** | ||
* Define an option for this factory. Options are values that may inform | ||
* dynamic attribute behavior but are not included in objects built by the | ||
* factory. Like attributes, options may have dependencies. Unlike | ||
* attributes, options may only depend on other options. | ||
* | ||
* Factory.define('Person') | ||
* .option('includeRelationships', false) | ||
* .attr( | ||
* 'spouse', | ||
* ['spouse', 'includeRelationships'], | ||
* function(spouse, includeRelationships) { | ||
* return includeRelationships ? | ||
* Factory.build('Person', spouse) : | ||
* null; | ||
* }); | ||
* | ||
* Factory.build('Person', null, { includeRelationships: true }); | ||
* | ||
* Options may have either static or dynamic default values, just like | ||
* attributes. Options without default values must have a value specified | ||
* when building. | ||
* | ||
* @param {string} attr | ||
* @param {?Array.<string>} dependencies | ||
* @param {?*} value | ||
* @return {Factory} | ||
*/ | ||
option: function(opt, dependencies, value) { | ||
var builder; | ||
if (arguments.length === 2) { | ||
value = dependencies; | ||
dependencies = null; | ||
} | ||
if (arguments.length > 1) { | ||
builder = typeof value === 'function' ? value : function() { return value; }; | ||
} | ||
this.opts[opt] = { dependencies: dependencies || [], builder: builder }; | ||
return this; | ||
}, | ||
/** | ||
* Defines an attribute that, by default, simply has an auto-incrementing | ||
* numeric value starting at 1. You can provide your own builder function | ||
* that accepts the number of the sequence and returns whatever value you'd | ||
* like it to be. | ||
* | ||
* Factory.define('Person').sequence('id'); | ||
* | ||
* @param {string} attr | ||
* @param {?function(number): *} builder | ||
* @return {Factory} | ||
*/ | ||
sequence: function(attr, builder) { | ||
builder = builder || function(i) { return i; }; | ||
return this.attr(attr, function() { | ||
this.sequences[attr] = this.sequences[attr] || 0; | ||
return builder(++this.sequences[attr]); | ||
}); | ||
}, | ||
/** | ||
* Sets a post-processor callback that will receive built objects and the | ||
* options for the build just before they are returned from the #build | ||
* function. | ||
* | ||
* @param {function(object, ?object)} callback | ||
* @return {Factory} | ||
*/ | ||
after: function(callback) { | ||
@@ -31,17 +144,169 @@ this.callbacks.push(callback); | ||
attributes: function(attrs) { | ||
attrs = attrs || {}; | ||
for(var attr in this.attrs) { | ||
if(!attrs.hasOwnProperty(attr)) { | ||
attrs[attr] = this.attrs[attr](); | ||
/** | ||
* Sets the constructor for this factory to be another factory. This can be | ||
* used to create more specific sub-types of factories. | ||
* | ||
* @param {Factory} | ||
* @return {Factory} | ||
*/ | ||
inherits: function(parentFactory) { | ||
this.construct = function(attributes, options) { | ||
return Factory.build(parentFactory, attributes, options); | ||
}; | ||
return this; | ||
}, | ||
/** | ||
* Builds a plain object containing values for each of the declared | ||
* attributes. The result of this is the same as the result when using #build | ||
* when there is no constructor registered. | ||
* | ||
* @param {?object} attributes | ||
* @param {?object} options | ||
* @return {object} | ||
*/ | ||
attributes: function(attributes, options) { | ||
attributes = attributes || {}; | ||
options = this.options(options); | ||
for (var attr in this.attrs) { | ||
this._attrValue(attr, attributes, options, [attr]); | ||
} | ||
return attributes; | ||
}, | ||
/** | ||
* Generates a value for the given named attribute and adds the result to the | ||
* given attributes list. | ||
* | ||
* @private | ||
* @param {string} attr | ||
* @param {object} attributes | ||
* @param {object} options | ||
* @param {Array.<string>} stack | ||
* @return {*} | ||
*/ | ||
_attrValue: function(attr, attributes, options, stack) { | ||
if (!this._alwaysCallBuilder(attr) && attributes.hasOwnProperty(attr)) { | ||
return attributes[attr]; | ||
} | ||
var value = this._buildWithDependencies(this.attrs[attr], function(dep) { | ||
if (options.hasOwnProperty(dep)) { | ||
return options[dep]; | ||
} else if (dep === attr) { | ||
return attributes[dep]; | ||
} else if (stack.indexOf(dep) >= 0) { | ||
throw new Error('detected a dependency cycle: '+stack.concat([dep]).join(' -> ')); | ||
} else { | ||
return this._attrValue(dep, attributes, options, stack.concat([dep])); | ||
} | ||
}); | ||
attributes[attr] = value; | ||
return value; | ||
}, | ||
/** | ||
* Determines whether the given named attribute has listed itself as a | ||
* dependency. | ||
* | ||
* @private | ||
* @param {string} attr | ||
* @return {boolean} | ||
*/ | ||
_alwaysCallBuilder: function(attr) { | ||
var attrMeta = this.attrs[attr]; | ||
return attrMeta.dependencies.indexOf(attr) >= 0; | ||
}, | ||
/** | ||
* Generates values for all the registered options using the values given. | ||
* | ||
* @private | ||
* @param {object} options | ||
* @return {object} | ||
*/ | ||
options: function(options) { | ||
options = options || {}; | ||
for (var opt in this.opts) { | ||
options[opt] = this._optionValue(opt, options); | ||
} | ||
return attrs; | ||
return options; | ||
}, | ||
build: function(attrs) { | ||
var result = this.attributes(attrs); | ||
return this.construct ? new this.construct(result) : result; | ||
/** | ||
* Generates a value for the given named option and adds the result to the | ||
* given options list. | ||
* | ||
* @private | ||
* @param {string} | ||
* @param {object} options | ||
* @return {*} | ||
*/ | ||
_optionValue: function(opt, options) { | ||
if (options.hasOwnProperty(opt)) { | ||
return options[opt]; | ||
} | ||
var optMeta = this.opts[opt]; | ||
if (!optMeta.builder) { | ||
throw new Error('option `'+opt+'` has no default value and none was provided'); | ||
} | ||
return this._buildWithDependencies(optMeta, function(dep) { | ||
return this._optionValue(dep, options); | ||
}); | ||
}, | ||
/** | ||
* Calls the builder function with its dependencies as determined by the | ||
* given dependency resolver. | ||
* | ||
* @private | ||
* @param {{builder: function(...[*]): *, dependencies: Array.<string>}} meta | ||
* @param {function(string): *} getDep | ||
* @return {*} | ||
*/ | ||
_buildWithDependencies: function(meta, getDep) { | ||
var deps = meta.dependencies; | ||
var self = this; | ||
var args = deps.map(function(){ return getDep.apply(self, arguments); }); | ||
return meta.builder.apply(this, args); | ||
}, | ||
/** | ||
* Builds objects by getting values for all attributes and optionally passing | ||
* the result to a constructor function. | ||
* | ||
* @param {object} attributes | ||
* @param {object} options | ||
* @return {*} | ||
*/ | ||
build: function(attributes, options) { | ||
var result = this.attributes(attributes, options); | ||
var retval = null; | ||
if (this.construct) { | ||
if (typeof this.construct.create === 'function') { | ||
retval = this.construct.create(result); | ||
} else { | ||
retval = new this.construct(result); | ||
} | ||
} else { | ||
retval = result; | ||
} | ||
for (var i = 0; i < this.callbacks.length; i++) { | ||
this.callbacks[i](retval, options); | ||
} | ||
return retval; | ||
}, | ||
/** | ||
* Extends a given factory by copying over its attributes, options, | ||
* callbacks, and constructor. This can be useful when you want to make | ||
* different types which all share certain attributes. | ||
* | ||
* @param {string} name The factory to extend. | ||
* @return {Factory} | ||
*/ | ||
extend: function(name) { | ||
@@ -51,11 +316,14 @@ var factory = Factory.factories[name]; | ||
if (this.construct === undefined) { this.construct = factory.construct; } | ||
for(var attr in factory.attrs) { | ||
if(factory.attrs.hasOwnProperty(attr)) { | ||
for (var attr in factory.attrs) { | ||
if (factory.attrs.hasOwnProperty(attr)) { | ||
this.attrs[attr] = factory.attrs[attr]; | ||
} | ||
} | ||
for (var opt in factory.opts) { | ||
if (factory.opts.hasOwnProperty(opt)) { | ||
this.opts[opt] = factory.opts[opt]; | ||
} | ||
} | ||
// Copy the parent's callbacks | ||
for(var i = 0; i < factory.callbacks.length; i++) { | ||
this.callbacks.push(factory.callbacks[i]); | ||
} | ||
this.callbacks = factory.callbacks.slice(); | ||
return this; | ||
@@ -67,2 +335,10 @@ } | ||
/** | ||
* Defines a factory by name and constructor function. Call #attr and #option | ||
* on the result to define the properties of this factory. | ||
* | ||
* @param {string} name | ||
* @param {?function(object): *} constructor | ||
* @return {Factory} | ||
*/ | ||
Factory.define = function(name, constructor) { | ||
@@ -74,14 +350,29 @@ var factory = new Factory(constructor); | ||
Factory.build = function(name, attrs, options) { | ||
var obj = this.factories[name].build(attrs); | ||
for(var i = 0; i < this.factories[name].callbacks.length; i++) { | ||
this.factories[name].callbacks[i](obj, options); | ||
} | ||
return obj; | ||
/** | ||
* Locates a factory by name and calls #build on it. | ||
* | ||
* @param {string} name | ||
* @param {object} attributes | ||
* @param {object} options | ||
* @return {*} | ||
*/ | ||
Factory.build = function(name, attributes, options) { | ||
if (!this.factories[name]) | ||
throw new Error('The "'+name+'" factory is not defined.'); | ||
return this.factories[name].build(attributes, options); | ||
}; | ||
Factory.buildList = function(name, size, attrs, options) { | ||
/** | ||
* Builds a collection of objects using the named factory. | ||
* | ||
* @param {string} name | ||
* @param {number} size | ||
* @param {object} attributes | ||
* @param {object} options | ||
* @return {Array.<*>} | ||
*/ | ||
Factory.buildList = function(name, size, attributes, options) { | ||
var objs = []; | ||
for(var i = 0; i < size; i++) { | ||
objs.push(Factory.build(name, attrs, options)); | ||
for (var i = 0; i < size; i++) { | ||
objs.push(Factory.build(name, attributes, options)); | ||
} | ||
@@ -91,4 +382,12 @@ return objs; | ||
Factory.attributes = function(name, attrs) { | ||
return this.factories[name].attributes(attrs); | ||
/** | ||
* Locates a factory by name and calls #attributes on it. | ||
* | ||
* @param {string} name | ||
* @param {object} attributes | ||
* @param {object} options | ||
* @return {object} | ||
*/ | ||
Factory.attributes = function(name, attributes, options) { | ||
return this.factories[name].attributes(attributes, options); | ||
}; | ||
@@ -95,0 +394,0 @@ |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
New author
Supply chain riskA new npm collaborator published a version of the package for the first time. New collaborators are usually benign additions to a project, but do indicate a change to the security surface area of a package.
Found 1 instance in 1 package
Copyleft License
License(Experimental) Copyleft license information was found.
Found 1 instance in 1 package
Mixed license
License(Experimental) Package contains multiple licenses.
Found 1 instance in 1 package
Non-permissive License
License(Experimental) A license not known to be considered permissive was found.
Found 1 instance in 1 package
201680
0
100
5040
90
4
14
1