<commit_before>import {create} from 'most'
import hold from '@most/hold'
import snabbdom from 'snabbdom'
import h from 'snabbdom/h'
import classNameFromVNode from 'snabbdom-selector/lib/classNameFromVNode'
import selectorParser from 'snabbdom-selector/lib/selectorParser'

import {domSelectorParser} from './utils'
import vTreeParser from './vTreeParser'
import {isolateSink, isolateSource} from './isolate'
import makeElementSelector from './select'

function makeVNodeWrapper(rootElement) {
  return function vNodeWrapper(vNode) {
    const {tagName: selectorTagName, id: selectorId} = selectorParser(vNode.sel)
    const vNodeClassName = classNameFromVNode(vNode)
    const {data: vNodeData = {}} = vNode
    const {props: vNodeDataProps = {}} = vNodeData
    const {id: vNodeId = selectorId} = vNodeDataProps

    const isVNodeAndRootElementIdentical =
      vNodeId === rootElement.id &&
      selectorTagName === rootElement.tagName &&
      vNodeClassName === rootElement.className

    if (isVNodeAndRootElementIdentical) {
      return vNode
    }

    const {tagName, id, className} = rootElement
    const elementId = id ? `#${id}` : ``
    const elementClassName = className ?
      `.${className.split(` `).join(`.`)}` :
      ``
    return h(`${tagName}${elementId}${elementClassName}`, {}, [vNode])
  }
}

const domDriverInputGuard =
  view$ => {
    if (!view$ || typeof view$.observe !== `function`) {
      throw new Error(`The DOM driver function expects as input an ` +
        `Observable of virtual DOM elements`)
    }
  }

const defaultOptions = {
  modules: [
    require(`snabbdom/modules/class`),
    require(`snabbdom/modules/props`),
    require(`snabbdom/modules/attributes`),
    require(`snabbdom/modules/style`),
  ],
}

const makeDOMDriver =
  (containerElementSelectors,
    {modules = defaultOptions.modules} = defaultOptions
  ) => {
    const patch = snabbdom.init(modules)
    const rootElement = domSelectorParser(containerElementSelectors)

    const DomDriver =
      view$ => {
        domDriverInputGuard(view$)

        const rootElement$ = hold(
          view$
            .map(vTreeParser)
            .switch()
            .map(makeVNodeWrapper(rootElement))
            .scan(patch, rootElement)
            .skip(1)
            .map(({elm}) => elm)
        )

        rootElement$.drain()

        return {
          namespace: [],
          select: makeElementSelector(rootElement$),
          isolateSink,
          isolateSource,
        }
      }

    return DomDriver
  }

export default makeDOMDriver
<commit_msg>feat(makeDOMDriver): throw error if modules is not an array<commit_after>import {create} from 'most'
import hold from '@most/hold'
import snabbdom from 'snabbdom'
import h from 'snabbdom/h'
import classNameFromVNode from 'snabbdom-selector/lib/classNameFromVNode'
import selectorParser from 'snabbdom-selector/lib/selectorParser'

import {domSelectorParser} from './utils'
import vTreeParser from './vTreeParser'
import {isolateSink, isolateSource} from './isolate'
import makeElementSelector from './select'

function makeVNodeWrapper(rootElement) {
  return function vNodeWrapper(vNode) {
    const {tagName: selectorTagName, id: selectorId} = selectorParser(vNode.sel)
    const vNodeClassName = classNameFromVNode(vNode)
    const {data: vNodeData = {}} = vNode
    const {props: vNodeDataProps = {}} = vNodeData
    const {id: vNodeId = selectorId} = vNodeDataProps

    const isVNodeAndRootElementIdentical =
      vNodeId === rootElement.id &&
      selectorTagName === rootElement.tagName &&
      vNodeClassName === rootElement.className

    if (isVNodeAndRootElementIdentical) {
      return vNode
    }

    const {tagName, id, className} = rootElement
    const elementId = id ? `#${id}` : ``
    const elementClassName = className ?
      `.${className.split(` `).join(`.`)}` :
      ``
    return h(`${tagName}${elementId}${elementClassName}`, {}, [vNode])
  }
}

const domDriverInputGuard =
  view$ => {
    if (!view$ || typeof view$.observe !== `function`) {
      throw new Error(`The DOM driver function expects as input an ` +
        `Observable of virtual DOM elements`)
    }
  }

const defaultOptions = {
  modules: [
    require(`snabbdom/modules/class`),
    require(`snabbdom/modules/props`),
    require(`snabbdom/modules/attributes`),
    require(`snabbdom/modules/style`),
  ],
}

const makeDOMDriver =
  (containerElementSelectors,
    {modules = defaultOptions.modules} = defaultOptions
  ) => {
    const patch = snabbdom.init(modules)
    const rootElement = domSelectorParser(containerElementSelectors)

    const DomDriver =
      view$ => {
        domDriverInputGuard(view$)
        if (!Array.isArray(modules)) {
          throw new Error(`Optional modules option must be ` +
          `an array for snabbdom modules`)
        }

        const rootElement$ = hold(
          view$
            .map(vTreeParser)
            .switch()
            .map(makeVNodeWrapper(rootElement))
            .scan(patch, rootElement)
            .skip(1)
            .map(({elm}) => elm)
        )

        rootElement$.drain()

        return {
          namespace: [],
          select: makeElementSelector(rootElement$),
          isolateSink,
          isolateSource,
        }
      }

    return DomDriver
  }

export default makeDOMDriver
<|endoftext|>


<commit_before>var ngdoc = require('ngdoc.js');
var DOM = require('dom.js').DOM;

describe('ngdoc', function(){
  var Doc = ngdoc.Doc;
  describe('Doc', function(){
    describe('metadata', function(){

      it('should find keywords', function(){
        expect(new Doc('\nHello: World! @ignore.').keywords()).toEqual('hello world');
        expect(new Doc('The `ng:class-odd` and').keywords()).toEqual('and ng:class-odd the');
      });
    });

    describe('parse', function(){
      it('should convert @names into properties', function(){
        var doc = new Doc('\n@name name\n@desc\ndesc\ndesc2\n@dep\n');
        doc.parse();
        expect(doc.name).toEqual('name');
        expect(doc.desc).toEqual('desc\ndesc2');
        expect(doc.dep).toEqual('');
      });

      it('should parse parameters', function(){
        var doc = new Doc(
            '@param {*} a short\n' +
            '@param {Type} b med\n' +
            '@param {Class=} [c=2] long\nline');
        doc.parse();
        expect(doc.param).toEqual([
           {name:'a', description:'short', type:'*', optional:false, 'default':undefined},
           {name:'b', description:'med', type:'Type', optional:false, 'default':undefined},
           {name:'c', description:'long\nline', type:'Class', optional:true, 'default':'2'}
         ]);
      });

      it('should parse return', function(){
        var doc = new Doc('@returns {Type} text *bold*.');
        doc.parse();
        expect(doc.returns).toEqual({
          type: 'Type',
          description: 'text bold.'
        });
      });
    });

  });

  describe('markdown', function(){
    var markdown = ngdoc.markdown;

    it('should replace angular in markdown', function(){
      expect(markdown('<angular/>')).
        toEqual('
<tt><angular/></tt>
');
    });

    it('should not replace anything in
', function(){
      expect(markdown('bah x\n
\nangular.k\n
\n asdf x')).
        toEqual(
            '
bah x
' +
            '

\n' +
            'angular.k\n' +
            '

' +
            '
asdf x
');
    });

    it('should replace text between two

 tags', function() {
      expect(markdown('
x
# One
b
')).
        toMatch('

One
<div');
    });
  });

  describe('trim', function(){
    var trim = ngdoc.trim;
    it('should remove leading/trailing space', function(){
      expect(trim('  \nabc\n  ')).toEqual('abc');
    });

    it('should remove leading space on every line', function(){
      expect(trim('\n 1\n  2\n   3\n')).toEqual('1\n 2\n  3');
    });
  });

  describe('merge', function(){
    it('should merge child with parent', function(){
      var parent = new Doc({name:'angular.service.abc'});
      var methodA = new Doc({name:'methodA', methodOf:'angular.service.abc'});
      var methodB = new Doc({name:'methodB', methodOf:'angular.service.abc'});
      var propA = new Doc({name:'propA', propertyOf:'angular.service.abc'});
      var propB = new Doc({name:'propB', propertyOf:'angular.service.abc'});
      ;var docs = [methodB, methodA, propB, propA, parent]; // keep wrong order;
      ngdoc.merge(docs);
      expect(docs.length).toEqual(1);
      expect(docs[0].name).toEqual('angular.service.abc');
      expect(docs[0].methods).toEqual([methodA, methodB]);
      expect(docs[0].properties).toEqual([propA, propB]);
    });

  });

  ////////////////////////////////////////

  describe('TAG', function(){
    describe('@param', function(){
      it('should parse with no default', function(){
        var doc = new Doc('@param {(number|string)} number Number \n to format.');
        doc.parse();
        expect(doc.param).toEqual([{
          type : '(number|string)',
          name : 'number',
          optional: false,
          'default' : undefined,
          description : 'Number \n to format.' }]);
      });

      it('should parse with default and optional', function(){
        var doc = new Doc('@param {(number|string)=} [fractionSize=2] desc');
        doc.parse();
        expect(doc.param).toEqual([{
          type : '(number|string)',
          name : 'fractionSize',
          optional: true,
          'default' : '2',
          description : 'desc' }]);
      });
    });

    describe('@requires', function() {
      it('should parse more @requires tag into array', function() {
        var doc = new Doc('@requires $service\n@requires $another');
        doc.parse();
        expect(doc.requires).toEqual(['$service', '$another']);
      });
    });

    describe('@property', function() {
      it('should parse @property tags into array', function() {
        var doc = new Doc("@property {type} name1 desc\n@property {type} name2 desc");
        doc.parse();
        expect(doc.properties.length).toEqual(2);
      });

      it('should parse @property with only name', function() {
        var doc = new Doc("@property fake");
        doc.parse();
        expect(doc.properties[0].name).toEqual('fake');
      });

      it('should parse @property with optional type', function() {
        var doc = new Doc("@property {string} name");
        doc.parse();
        expect(doc.properties[0].name).toEqual('name');
        expect(doc.properties[0].type).toEqual('string');
      });

      it('should parse @property with optional description', function() {
        var doc = new Doc("@property name desc rip tion");
        doc.parse();
        expect(doc.properties[0].name).toEqual('name');
        expect(doc.properties[0].description).toEqual('desc rip tion');
      });

      it('should parse @property with type and description both', function() {
        var doc = new Doc("@property {bool} name desc rip tion");
        doc.parse();
        expect(doc.properties[0].name).toEqual('name');
        expect(doc.properties[0].type).toEqual('bool');
        expect(doc.properties[0].description).toEqual('desc rip tion');
      });

    });

    describe('@returns', function() {
      it('should not parse @returns without type', function() {
        var doc = new Doc("@returns lala");
        expect(doc.parse).toThrow();
      });

      it('should parse @returns with type and description', function() {
        var doc = new Doc("@returns {string} descrip tion");
        doc.parse();
        expect(doc.returns).toEqual({type: 'string', description: 'descrip tion'});
      });

      it('should transform description of @returns with markdown', function() {
        var doc = new Doc("@returns {string} descrip *tion*");
        doc.parse();
        expect(doc.returns).toEqual({type: 'string', description: 'descrip tion'});
      });

      it('should support multiline content', function() {
        var doc = new Doc("@returns {string} description\n new line\n another line");
        doc.parse();
        expect(doc.returns).
          toEqual({type: 'string', description: 'description\n new line\n another line'});
      });
    });

    describe('@description', function(){
      it('should support pre blocks', function(){
        var doc = new Doc("@description
abc
");
        doc.parse();
        expect(doc.description).
          toBe('

abc

');
      });

      it('should support multiple pre blocks', function() {
        var doc = new Doc("@description foo \n
abc
\n#bah\nfoo \n
cba
");
        doc.parse();
        expect(doc.description).
          toBe('
foo
' +
               '

abc

' +
               '
bah
\n\n' +
               '
foo
' +
               '

cba

');

      });

      it('should support nested @link annotations with or without description', function() {
        var doc = new Doc("@description " +
            'foo {@link angular.foo}\n\n da {@link angular.foo bar foo bar } \n\n' +
            'dad{@link angular.foo}\n\n' +
            '{@link angular.directive.ng:foo ng:foo}');
        doc.parse();
        expect(doc.description).
          toBe('
foo angular.foo
\n\n' +
               '
da bar foo bar
\n\n' +
               '
dadangular.foo
\n\n' +
               '
ng:foo
');
      });

      it('should increment all headings by two', function() {
        var doc = new Doc('@description # foo\nabc\n## bar \n xyz');
        doc.parse();
        expect(doc.description).
          toBe('
foo
\n\n
abc
\n\n
bar
\n\n
xyz
');
      });
    });

    describe('@example', function(){
      it('should not remove {{}}', function(){
        var doc = new Doc('@example text {{ abc }}');
        doc.parse();
        expect(doc.example).toEqual('text {{ abc }}');
      });
    });

    describe('@deprecated', function() {
      it('should parse @deprecated', function() {
        var doc = new Doc('@deprecated Replaced with foo.');
        doc.parse();
        expect(doc.deprecated).toBe('Replaced with foo.');
      });
    });
  });

  describe('usage', function(){
    var dom;

    beforeEach(function(){
      dom = new DOM();
      this.addMatchers({
        toContain: function(text) {
          this.actual = this.actual.toString();
          return this.actual.indexOf(text) > -1;
        }
      });
    });

    describe('filter', function(){
      it('should format', function(){
        var doc = new Doc({
          ngdoc:'formatter',
          shortName:'myFilter',
          param: [
            {name:'a'},
            {name:'b'}
          ]
        });
        doc.html_usage_filter(dom);
        expect(dom).toContain('myFilter_expression | myFilter:b');
        expect(dom).toContain('angular.filter.myFilter(a, b)');
      });
    });

    describe('validator', function(){
      it('should format', function(){
        var doc = new Doc({
          ngdoc:'validator',
          shortName:'myValidator',
          param: [
            {name:'a'},
            {name:'b'}
          ]
        });
        doc.html_usage_validator(dom);
        expect(dom).toContain('ng:validate="myValidator:b"');
        expect(dom).toContain('angular.validator.myValidator(a, b)');
      });
    });

    describe('formatter', function(){
      it('should format', function(){
        var doc = new Doc({
          ngdoc:'formatter',
          shortName:'myFormatter',
          param: [
            {name:'a'},
          ]
        });
        doc.html_usage_formatter(dom);
        expect(dom).toContain('ng:format="myFormatter:a"');
        expect(dom).toContain('var userInputString = angular.formatter.myFormatter.format(modelValue, a);');
        expect(dom).toContain('var modelValue = angular.formatter.myFormatter.parse(userInputString, a);');
      });
    });
  });

});
<commit_msg>verify that issue# 162 was fixed with doc rewrite<commit_after>var ngdoc = require('ngdoc.js');
var DOM = require('dom.js').DOM;

describe('ngdoc', function(){
  var Doc = ngdoc.Doc;
  describe('Doc', function(){
    describe('metadata', function(){

      it('should find keywords', function(){
        expect(new Doc('\nHello: World! @ignore.').keywords()).toEqual('hello world');
        expect(new Doc('The `ng:class-odd` and').keywords()).toEqual('and ng:class-odd the');
      });
    });

    describe('parse', function(){
      it('should convert @names into properties', function(){
        var doc = new Doc('\n@name name\n@desc\ndesc\ndesc2\n@dep\n');
        doc.parse();
        expect(doc.name).toEqual('name');
        expect(doc.desc).toEqual('desc\ndesc2');
        expect(doc.dep).toEqual('');
      });

      it('should parse parameters', function(){
        var doc = new Doc(
            '@param {*} a short\n' +
            '@param {Type} b med\n' +
            '@param {Class=} [c=2] long\nline');
        doc.parse();
        expect(doc.param).toEqual([
           {name:'a', description:'short', type:'*', optional:false, 'default':undefined},
           {name:'b', description:'med', type:'Type', optional:false, 'default':undefined},
           {name:'c', description:'long\nline', type:'Class', optional:true, 'default':'2'}
         ]);
      });

      it('should parse return', function(){
        var doc = new Doc('@returns {Type} text *bold*.');
        doc.parse();
        expect(doc.returns).toEqual({
          type: 'Type',
          description: 'text bold.'
        });
      });

      it('should not remove extra line breaks', function(){
        var doc = new Doc('@example\nA\n\nB');
        doc.parse();
        expect(doc.example).toEqual('A\n\nB');
      });

    });

  });

  describe('markdown', function(){
    var markdown = ngdoc.markdown;

    it('should replace angular in markdown', function(){
      expect(markdown('<angular/>')).
        toEqual('
<tt><angular/></tt>
');
    });

    it('should not replace anything in
', function(){
      expect(markdown('bah x\n
\nangular.k\n
\n asdf x')).
        toEqual(
            '
bah x
' +
            '

\n' +
            'angular.k\n' +
            '

' +
            '
asdf x
');
    });

    it('should replace text between two

 tags', function() {
      expect(markdown('
x
# One
b
')).
        toMatch('

One
<div');
    });
  });

  describe('trim', function(){
    var trim = ngdoc.trim;
    it('should remove leading/trailing space', function(){
      expect(trim('  \nabc\n  ')).toEqual('abc');
    });

    it('should remove leading space on every line', function(){
      expect(trim('\n 1\n  2\n   3\n')).toEqual('1\n 2\n  3');
    });
  });

  describe('merge', function(){
    it('should merge child with parent', function(){
      var parent = new Doc({name:'angular.service.abc'});
      var methodA = new Doc({name:'methodA', methodOf:'angular.service.abc'});
      var methodB = new Doc({name:'methodB', methodOf:'angular.service.abc'});
      var propA = new Doc({name:'propA', propertyOf:'angular.service.abc'});
      var propB = new Doc({name:'propB', propertyOf:'angular.service.abc'});
      ;var docs = [methodB, methodA, propB, propA, parent]; // keep wrong order;
      ngdoc.merge(docs);
      expect(docs.length).toEqual(1);
      expect(docs[0].name).toEqual('angular.service.abc');
      expect(docs[0].methods).toEqual([methodA, methodB]);
      expect(docs[0].properties).toEqual([propA, propB]);
    });

  });

  ////////////////////////////////////////

  describe('TAG', function(){
    describe('@param', function(){
      it('should parse with no default', function(){
        var doc = new Doc('@param {(number|string)} number Number \n to format.');
        doc.parse();
        expect(doc.param).toEqual([{
          type : '(number|string)',
          name : 'number',
          optional: false,
          'default' : undefined,
          description : 'Number \n to format.' }]);
      });

      it('should parse with default and optional', function(){
        var doc = new Doc('@param {(number|string)=} [fractionSize=2] desc');
        doc.parse();
        expect(doc.param).toEqual([{
          type : '(number|string)',
          name : 'fractionSize',
          optional: true,
          'default' : '2',
          description : 'desc' }]);
      });
    });

    describe('@requires', function() {
      it('should parse more @requires tag into array', function() {
        var doc = new Doc('@requires $service\n@requires $another');
        doc.parse();
        expect(doc.requires).toEqual(['$service', '$another']);
      });
    });

    describe('@property', function() {
      it('should parse @property tags into array', function() {
        var doc = new Doc("@property {type} name1 desc\n@property {type} name2 desc");
        doc.parse();
        expect(doc.properties.length).toEqual(2);
      });

      it('should parse @property with only name', function() {
        var doc = new Doc("@property fake");
        doc.parse();
        expect(doc.properties[0].name).toEqual('fake');
      });

      it('should parse @property with optional type', function() {
        var doc = new Doc("@property {string} name");
        doc.parse();
        expect(doc.properties[0].name).toEqual('name');
        expect(doc.properties[0].type).toEqual('string');
      });

      it('should parse @property with optional description', function() {
        var doc = new Doc("@property name desc rip tion");
        doc.parse();
        expect(doc.properties[0].name).toEqual('name');
        expect(doc.properties[0].description).toEqual('desc rip tion');
      });

      it('should parse @property with type and description both', function() {
        var doc = new Doc("@property {bool} name desc rip tion");
        doc.parse();
        expect(doc.properties[0].name).toEqual('name');
        expect(doc.properties[0].type).toEqual('bool');
        expect(doc.properties[0].description).toEqual('desc rip tion');
      });

    });

    describe('@returns', function() {
      it('should not parse @returns without type', function() {
        var doc = new Doc("@returns lala");
        expect(doc.parse).toThrow();
      });

      it('should parse @returns with type and description', function() {
        var doc = new Doc("@returns {string} descrip tion");
        doc.parse();
        expect(doc.returns).toEqual({type: 'string', description: 'descrip tion'});
      });

      it('should transform description of @returns with markdown', function() {
        var doc = new Doc("@returns {string} descrip *tion*");
        doc.parse();
        expect(doc.returns).toEqual({type: 'string', description: 'descrip tion'});
      });

      it('should support multiline content', function() {
        var doc = new Doc("@returns {string} description\n new line\n another line");
        doc.parse();
        expect(doc.returns).
          toEqual({type: 'string', description: 'description\n new line\n another line'});
      });
    });

    describe('@description', function(){
      it('should support pre blocks', function(){
        var doc = new Doc("@description
abc
");
        doc.parse();
        expect(doc.description).
          toBe('

abc

');
      });

      it('should support multiple pre blocks', function() {
        var doc = new Doc("@description foo \n
abc
\n#bah\nfoo \n
cba
");
        doc.parse();
        expect(doc.description).
          toBe('
foo
' +
               '

abc

' +
               '
bah
\n\n' +
               '
foo
' +
               '

cba

');

      });

      it('should support nested @link annotations with or without description', function() {
        var doc = new Doc("@description " +
            'foo {@link angular.foo}\n\n da {@link angular.foo bar foo bar } \n\n' +
            'dad{@link angular.foo}\n\n' +
            '{@link angular.directive.ng:foo ng:foo}');
        doc.parse();
        expect(doc.description).
          toBe('
foo angular.foo
\n\n' +
               '
da bar foo bar
\n\n' +
               '
dadangular.foo
\n\n' +
               '
ng:foo
');
      });

      it('should increment all headings by two', function() {
        var doc = new Doc('@description # foo\nabc\n## bar \n xyz');
        doc.parse();
        expect(doc.description).
          toBe('
foo
\n\n
abc
\n\n
bar
\n\n
xyz
');
      });
    });

    describe('@example', function(){
      it('should not remove {{}}', function(){
        var doc = new Doc('@example text {{ abc }}');
        doc.parse();
        expect(doc.example).toEqual('text {{ abc }}');
      });
    });

    describe('@deprecated', function() {
      it('should parse @deprecated', function() {
        var doc = new Doc('@deprecated Replaced with foo.');
        doc.parse();
        expect(doc.deprecated).toBe('Replaced with foo.');
      });
    });
  });

  describe('usage', function(){
    var dom;

    beforeEach(function(){
      dom = new DOM();
      this.addMatchers({
        toContain: function(text) {
          this.actual = this.actual.toString();
          return this.actual.indexOf(text) > -1;
        }
      });
    });

    describe('filter', function(){
      it('should format', function(){
        var doc = new Doc({
          ngdoc:'formatter',
          shortName:'myFilter',
          param: [
            {name:'a'},
            {name:'b'}
          ]
        });
        doc.html_usage_filter(dom);
        expect(dom).toContain('myFilter_expression | myFilter:b');
        expect(dom).toContain('angular.filter.myFilter(a, b)');
      });
    });

    describe('validator', function(){
      it('should format', function(){
        var doc = new Doc({
          ngdoc:'validator',
          shortName:'myValidator',
          param: [
            {name:'a'},
            {name:'b'}
          ]
        });
        doc.html_usage_validator(dom);
        expect(dom).toContain('ng:validate="myValidator:b"');
        expect(dom).toContain('angular.validator.myValidator(a, b)');
      });
    });

    describe('formatter', function(){
      it('should format', function(){
        var doc = new Doc({
          ngdoc:'formatter',
          shortName:'myFormatter',
          param: [
            {name:'a'},
          ]
        });
        doc.html_usage_formatter(dom);
        expect(dom).toContain('ng:format="myFormatter:a"');
        expect(dom).toContain('var userInputString = angular.formatter.myFormatter.format(modelValue, a);');
        expect(dom).toContain('var modelValue = angular.formatter.myFormatter.parse(userInputString, a);');
      });
    });
  });

});
<|endoftext|>


<commit_before>/*global _ Backbone */

/*
 * Backbone.Siren
 *
 * Copyright (c) 2013 Kiva Microfunds
 * Licensed under the MIT license.
 * https://github.com/kiva/backbone.siren/blob/master/license.txt
 */

(function(root, factory) {
    'use strict';

    if (typeof define === 'function' && define.amd) {
        // AMD
        define(['underscore', 'backbone', 'backbone-relational'], function(_, Backbone) {
            Backbone.Siren = factory(_, Backbone);
        });
    } else {
        // Browser globals
        root.Backbone.Siren = factory(_, Backbone);
    }

}(this, function (_, Backbone, undefined) {
    'use strict';

    // The store
    var _store = {}
    , store = {
        key: function (model) {
            return model.url() + '-' +  model.cid;
        }

        , add: function (model) {
            _store[this.key(model)] = model;
        }

        , exists: function (model) {
            return !!_store[this.key(model)];
        }

        , all: function () {
            return _store;
        }
    }
    , warn = function (msg) {
        if (Backbone.Siren.settings.showWarnings && console) {
            console.warn(msg);
        }
    };

    /**
     *
     * @static
     * @param name
     * @param lowercaseFirstChar
     * @return {String}
     */
    function toCamelCase(name, lowercaseFirstChar) {
        return ((lowercaseFirstChar ? '' : '-') + name).replace(/(\-[a-z])/g, function(match){return match.toUpperCase().replace('-','');});
    }

    /**
     *
     * @static
     * @param entity
     * @return {Object}
     */
    function getUrl(entity) {
        var link, url;

        if (entity.href) {
            url = entity.href;
        } else if (entity.links) {
            link = entity.links.filter(function (link) {
                return !!(link.rel && link.rel.filter(function (relType) {
                    return relType == 'self';
                }).length);
            })[0];

            if (link) {
                url = link.href;
            }
        } else {
            warn('Missing href or "self" link');
        }

        return url;
    }

    /**
     * Access to the representation's "self" url, or its "href" if there is one.
     *
     * @return {String}
     */
    function url() {
        return getUrl(this._data);
    }

    /**
     *
     * @static
     * @param {Object} sirenObj Can be a siren entity or siren action object
     * @return {Array}
     */
    function getClassNames(sirenObj) {
        return sirenObj['class'];
    }

    /**
     *
     * @static
     * @param {Array} entities
     * @param {Object} filters
     * @return {Array}
     */
    function filter(entities, filters) {
        return _.filter(entities, function (entity) {
            return ModelHasFilterProperties(entity, filters);
        });
    }

    /**
     *
     * @param entity
     * @param filters
     * @return {Boolean}
     */
    function ModelHasFilterProperties(entity, filters) {
        var hasProperties = true;

        if (filters.className) {
            hasProperties = entity.hasClass(filters.className);
        }

        if (filters.rel) {
            hasProperties = hasProperties && (entity.rel() == filters.rel);
        }

        return hasProperties;
    }

    /**
     *
     * @param {Object} obj
     * @param {Object} filtersObj
     * @return {Boolean}
     */
    function ObjectHasFilterProperties(obj, filtersObj) {
        var hasProperties = true;

        if (filtersObj.className) {
            hasProperties = _hasClass(obj, filtersObj.className);
        }

        if (filtersObj.rel) {
            hasProperties = hasRel(obj, filtersObj.rel);
        }

        return hasProperties;
    }

    /**
     *
     * @static
     * @param sirenObj
     * @param className
     * @return {Boolean}
     */
    function _hasClass(sirenObj, className) {
        return _.indexOf(getClassNames(sirenObj), className) > -1;
    }

    /**
     *
     * @param className
     * @return {Boolean}
     */
    function hasClass(className) {
        return _hasClass(this._data, className);
    }

    /**
     *
     * @static
     * @param sirenObj
     * @param relValue
     * @return {Boolean}
     */
    function hasRel(sirenObj, relValue) {
        return getRel(sirenObj) == relValue;
    }

    /**
     * Accesses the "class" property of the Siren Object
     *
     * @return {Array}
     */
    function classes() {
        return getClassNames(this._data);
    }

    /**
     *
     * @static
     * @param sirenObj
     * @param verbose
     * @return {String}
     */
    function getRel(sirenObj, verbose) {
        var _rel = sirenObj.rel;

        if (_rel) {
            _rel = _rel[0];

            if (! verbose) {
                _rel = _rel.slice(_rel.lastIndexOf('/') + 1, _rel.length);
            }
        } else {
            warn('Missing "rel" attribute');
        }

        return _rel;
    }

    /**
     * By default returns characters after the last '/'.  Set to `verbose` to true to return the entire rel string.
     *
     * @param {Boolean} verbose
     * @return {String}
     */
    function rel(verbose) {
        return getRel(this._data, verbose);
    }

    /**
     * Access to the representation's "title"
     *
     * @return {String}
     */
    function title() {
        return this._data.title;
    }

    return {
        store: store

        , settings: {
            showWarnings: true
        }

        , Model: Backbone.Model.extend({

            url: url
            , classes: classes
            , hasClass: hasClass
            , rel: rel
            , title: title

            /**
             * http://backbonejs.org/#Model-parse
             *
             * @param {Object} sirenObj
             */
            , parse: function (sirenObj) {
                var self = this;

                this.resolveEntities()
                    .done(function (args) {
                        _.each(args, function (entity) {
                            var camelCaseRel, model, collection;

                            if (_.indexOf(getClassNames(entity), 'collection') == -1) {
                                // Its a model
                                model = new Backbone.Siren.Model(entity);
                                camelCaseRel = toCamelCase(model.rel());

                                self[camelCaseRel] = model;
                                self._entities.push(camelCaseRel);
                                store.add(model);
                            } else {
                                // Its a collection
                                collection = new Backbone.Siren.Collection(entity);
                                camelCaseRel = toCamelCase(collection.rel());

                                self[camelCaseRel] = collection;
                                self._entities.push(camelCaseRel);
                            }
                        });
                    });

                return sirenObj.properties;
            }

            /**
             * http://backbonejs.org/#Model-toJSON
             *
             * @param {Object} options
             */
            , toJSON: function () {
                var json = _.clone(this.attributes)
                , entities = this.entities();

                _.each(entities, function (entity) {
                    json[entity.rel()] = entity;
                });

                return json;
            }

            /**
             * Filters the entitie's properties and returns only sub-entities
             *
             * @return {Array}
             */

            , entities: function (filters) {
                var self = this
                , entities = _.filter(this, function (val, name) {
                    return _.indexOf(self._entities, name) > -1;
                });

                if (filters) {
                    entities = filter(entities, filters);
                }

                return entities;
            }

            /**
             * Access to the representation's "actions"
             *
             * @return {Array}
             */
            , actions: function (filterObj) {
                var actions = this._data.actions;

                if (filterObj) {
                    actions = _.filter(actions, function (action) {
                        return ObjectHasFilterProperties(action, filterObj)
                    });
                }
                return actions;
            }

            /**
             *
             * @param {Object} options
             * @param {Boolean} options.force forces an ajax request
             * @param {Array} options.range See http://underscorejs.org/#range
             *
             * @return {jQuery.Deferred}
             */
            , resolveEntities: function (filters, options) {
                options = options || {};

                var deferreds = []
                , entities = this._data.entities;

                if (filters) {
                    // @todo Currently only have a filter for "rel"
                    entities = entities.filter(function (el) {
                        var rel = el.rel;

                        rel = rel.slice(rel.lastIndexOf('/') + 1, rel.length);
                        return _.indexOf(rel, filters.rel) > -1;
                    });
                }

                if (options.range) {
                    entities = entities.slice(options.range[0], options.range[1]);
                }

                _.each(entities, function (entity) {
                    var url = getUrl(entity);

                    if ((entity.href || options.force) && url) {
                        deferreds.push($.getJSON(url));
                    } else if (! entity.href) {
                        deferreds.push(entity);
                    }
                });

                return $.when(deferreds);
            }

            /**
             * http://backbonejs.org/#Model-constructor
             *
             * @param {Object} attributes
             * @param {Object} options
             */
            , constructor: function (sirenObj, options) {
                options = options || {};
                options.parse = true; // Force "parse" to be called on instantiation: http://stackoverflow.com/questions/11068989/backbone-js-using-parse-without-calling-fetch/14950519#14950519

                this._data = sirenObj; // Stores the entire siren object in raw json
                this._entities = []; // Stores sub-entity models

                Backbone.Model.call(this, sirenObj, options);
            }

        })

        , Collection: Backbone.Collection.extend({
            url: url
            , classes: classes
            , hasClass: hasClass
            , title: title
            , rel: rel

            /**
             * http://backbonejs.org/#Collection-toJSON
             *
             * @param {Object} options
             */
            , toJSON: function (options) {
                return this.map(function(model){
                    return model.toJSON(options);
                });
            }

            /**
             *
             * @param {Function|Object} arg
             * @return {Array}
             */
            , filter: function (arg) {
                if (typeof arg ==  'function') {
                    return _.filter(this, arg);
                } else {
                    return filter(this, arg);
                }
            }

            /**
             * http://backbonejs.org/#Collection-parse
             *
             * @param {Object} sirenObj
             */
            , parse: function (sirenObj) {
                var models = [];
                _.each(sirenObj.entities, function (entity) {
                    var model = new Backbone.Siren.Model(entity);
                    models.push(model);
                    store.add(model);
                });

                return models;
            }

            /**
             * http://backbonejs.org/#Collection-constructor
             *
             * @param {Object} attributes
             * @param {Object} options
             */
            , constructor: function (sirenObj, options) {
                options = options || {};
                options.parse = true; // Force "parse" to be called on instantiation: http://stackoverflow.com/questions/11068989/backbone-js-using-parse-without-calling-fetch/14950519#14950519

                this._data = sirenObj;

                Backbone.Collection.call(this, sirenObj, options);
            }
        })
    };

}));<commit_msg>filters and options are not needed [yet] remove and add back when there is a use-case<commit_after>/*global _ Backbone */

/*
 * Backbone.Siren
 *
 * Copyright (c) 2013 Kiva Microfunds
 * Licensed under the MIT license.
 * https://github.com/kiva/backbone.siren/blob/master/license.txt
 */

(function(root, factory) {
    'use strict';

    if (typeof define === 'function' && define.amd) {
        // AMD
        define(['underscore', 'backbone', 'backbone-relational'], function(_, Backbone) {
            Backbone.Siren = factory(_, Backbone);
        });
    } else {
        // Browser globals
        root.Backbone.Siren = factory(_, Backbone);
    }

}(this, function (_, Backbone, undefined) {
    'use strict';

    // The store
    var _store = {}
    , store = {
        key: function (model) {
            return model.url() + '-' +  model.cid;
        }

        , add: function (model) {
            _store[this.key(model)] = model;
        }

        , exists: function (model) {
            return !!_store[this.key(model)];
        }

        , all: function () {
            return _store;
        }
    }
    , warn = function (msg) {
        if (Backbone.Siren.settings.showWarnings && console) {
            console.warn(msg);
        }
    };

    /**
     *
     * @static
     * @param name
     * @param lowercaseFirstChar
     * @return {String}
     */
    function toCamelCase(name, lowercaseFirstChar) {
        return ((lowercaseFirstChar ? '' : '-') + name).replace(/(\-[a-z])/g, function(match){return match.toUpperCase().replace('-','');});
    }

    /**
     *
     * @static
     * @param entity
     * @return {Object}
     */
    function getUrl(entity) {
        var link, url;

        if (entity.href) {
            url = entity.href;
        } else if (entity.links) {
            link = entity.links.filter(function (link) {
                return !!(link.rel && link.rel.filter(function (relType) {
                    return relType == 'self';
                }).length);
            })[0];

            if (link) {
                url = link.href;
            }
        } else {
            warn('Missing href or "self" link');
        }

        return url;
    }

    /**
     * Access to the representation's "self" url, or its "href" if there is one.
     *
     * @return {String}
     */
    function url() {
        return getUrl(this._data);
    }

    /**
     *
     * @static
     * @param {Object} sirenObj Can be a siren entity or siren action object
     * @return {Array}
     */
    function getClassNames(sirenObj) {
        return sirenObj['class'];
    }

    /**
     *
     * @static
     * @param {Array} entities
     * @param {Object} filters
     * @return {Array}
     */
    function filter(entities, filters) {
        return _.filter(entities, function (entity) {
            return ModelHasFilterProperties(entity, filters);
        });
    }

    /**
     *
     * @param entity
     * @param filters
     * @return {Boolean}
     */
    function ModelHasFilterProperties(entity, filters) {
        var hasProperties = true;

        if (filters.className) {
            hasProperties = entity.hasClass(filters.className);
        }

        if (filters.rel) {
            hasProperties = hasProperties && (entity.rel() == filters.rel);
        }

        return hasProperties;
    }

    /**
     *
     * @param {Object} obj
     * @param {Object} filtersObj
     * @return {Boolean}
     */
    function ObjectHasFilterProperties(obj, filtersObj) {
        var hasProperties = true;

        if (filtersObj.className) {
            hasProperties = _hasClass(obj, filtersObj.className);
        }

        if (filtersObj.rel) {
            hasProperties = hasRel(obj, filtersObj.rel);
        }

        return hasProperties;
    }

    /**
     *
     * @static
     * @param sirenObj
     * @param className
     * @return {Boolean}
     */
    function _hasClass(sirenObj, className) {
        return _.indexOf(getClassNames(sirenObj), className) > -1;
    }

    /**
     *
     * @param className
     * @return {Boolean}
     */
    function hasClass(className) {
        return _hasClass(this._data, className);
    }

    /**
     *
     * @static
     * @param sirenObj
     * @param relValue
     * @return {Boolean}
     */
    function hasRel(sirenObj, relValue) {
        return getRel(sirenObj) == relValue;
    }

    /**
     * Accesses the "class" property of the Siren Object
     *
     * @return {Array}
     */
    function classes() {
        return getClassNames(this._data);
    }

    /**
     *
     * @static
     * @param sirenObj
     * @param verbose
     * @return {String}
     */
    function getRel(sirenObj, verbose) {
        var _rel = sirenObj.rel;

        if (_rel) {
            _rel = _rel[0];

            if (! verbose) {
                _rel = _rel.slice(_rel.lastIndexOf('/') + 1, _rel.length);
            }
        } else {
            warn('Missing "rel" attribute');
        }

        return _rel;
    }

    /**
     * By default returns characters after the last '/'.  Set to `verbose` to true to return the entire rel string.
     *
     * @param {Boolean} verbose
     * @return {String}
     */
    function rel(verbose) {
        return getRel(this._data, verbose);
    }

    /**
     * Access to the representation's "title"
     *
     * @return {String}
     */
    function title() {
        return this._data.title;
    }

    return {
        store: store

        , settings: {
            showWarnings: true
        }

        , Model: Backbone.Model.extend({

            url: url
            , classes: classes
            , hasClass: hasClass
            , rel: rel
            , title: title

            /**
             * http://backbonejs.org/#Model-parse
             *
             * @param {Object} sirenObj
             */
            , parse: function (sirenObj) {
                var self = this;

                this.resolveEntities()
                    .done(function (args) {
                        _.each(args, function (entity) {
                            var camelCaseRel, model, collection;

                            if (_.indexOf(getClassNames(entity), 'collection') == -1) {
                                // Its a model
                                model = new Backbone.Siren.Model(entity);
                                camelCaseRel = toCamelCase(model.rel());

                                self[camelCaseRel] = model;
                                self._entities.push(camelCaseRel);
                                store.add(model);
                            } else {
                                // Its a collection
                                collection = new Backbone.Siren.Collection(entity);
                                camelCaseRel = toCamelCase(collection.rel());

                                self[camelCaseRel] = collection;
                                self._entities.push(camelCaseRel);
                            }
                        });
                    });

                return sirenObj.properties;
            }

            /**
             * http://backbonejs.org/#Model-toJSON
             *
             * @param {Object} options
             */
            , toJSON: function () {
                var json = _.clone(this.attributes)
                , entities = this.entities();

                _.each(entities, function (entity) {
                    json[entity.rel()] = entity;
                });

                return json;
            }

            /**
             * Filters the entitie's properties and returns only sub-entities
             *
             * @return {Array}
             */

            , entities: function (filters) {
                var self = this
                , entities = _.filter(this, function (val, name) {
                    return _.indexOf(self._entities, name) > -1;
                });

                if (filters) {
                    entities = filter(entities, filters);
                }

                return entities;
            }

            /**
             * Access to the representation's "actions"
             *
             * @return {Array}
             */
            , actions: function (filterObj) {
                var actions = this._data.actions;

                if (filterObj) {
                    actions = _.filter(actions, function (action) {
                        return ObjectHasFilterProperties(action, filterObj)
                    });
                }
                return actions;
            }

            /**
             *
             * @return {jQuery.Deferred}
             */
            , resolveEntities: function () {
                var deferreds = [];

                _.each(this._data.entities, function (entity) {
                    var url = getUrl(entity);

                    if (entity.href && url) {
                        deferreds.push($.getJSON(url));
                    } else if (! entity.href) {
                        deferreds.push(entity);
                    }
                });

                return $.when(deferreds);
            }

            /**
             * http://backbonejs.org/#Model-constructor
             *
             * @param {Object} attributes
             * @param {Object} options
             */
            , constructor: function (sirenObj, options) {
                options = options || {};
                options.parse = true; // Force "parse" to be called on instantiation: http://stackoverflow.com/questions/11068989/backbone-js-using-parse-without-calling-fetch/14950519#14950519

                this._data = sirenObj; // Stores the entire siren object in raw json
                this._entities = []; // Stores sub-entity names

                Backbone.Model.call(this, sirenObj, options);
            }

        })

        , Collection: Backbone.Collection.extend({
            url: url
            , classes: classes
            , hasClass: hasClass
            , title: title
            , rel: rel

            /**
             * http://backbonejs.org/#Collection-toJSON
             *
             * @param {Object} options
             */
            , toJSON: function (options) {
                return this.map(function(model){
                    return model.toJSON(options);
                });
            }

            /**
             *
             * @param {Function|Object} arg
             * @return {Array}
             */
            , filter: function (arg) {
                if (typeof arg ==  'function') {
                    return _.filter(this, arg);
                } else {
                    return filter(this, arg);
                }
            }

            /**
             * http://backbonejs.org/#Collection-parse
             *
             * @param {Object} sirenObj
             */
            , parse: function (sirenObj) {
                var models = [];
                _.each(sirenObj.entities, function (entity) {
                    var model = new Backbone.Siren.Model(entity);
                    models.push(model);
                    store.add(model);
                });

                return models;
            }

            /**
             * http://backbonejs.org/#Collection-constructor
             *
             * @param {Object} attributes
             * @param {Object} options
             */
            , constructor: function (sirenObj, options) {
                options = options || {};
                options.parse = true; // Force "parse" to be called on instantiation: http://stackoverflow.com/questions/11068989/backbone-js-using-parse-without-calling-fetch/14950519#14950519

                this._data = sirenObj;

                Backbone.Collection.call(this, sirenObj, options);
            }
        })
    };

}));<|endoftext|>


<commit_before>Backbone.Siren = (function (_, Backbone, undefined) {
    'use strict';

    // The store-cache
    var _store = {}

    /**
     * Super simple store.  Good enough for now.
     *
     * @type {Object}
     * @private
     */
    , store = {

        /**
         *
         * @param {Backbone.Siren.Model} model
         * @return {Backbone.Siren.Model}
         */
        add: function (model) {
            return _store[model.url()] = model;
        }

        /**
         *
         * @param {Backbone.Siren.Model} model
         * @return {Boolean}
         */
        , exists: function (model) {
            return !!_store[model.url()];
        }

        /**
         *
         * @return {Array}
         */
        , all: function () {
            return _store;
        }
    }

    /**
     *
     * @type {Object}
     * @private
     */
    , warn = function () {
        if (Backbone.Siren.settings.showWarnings && console) {
            console.warn.apply(console, arguments);
        }
    };

    /**
     *
     * @param {Backbone.Siren.Model|Backbone.Siren.Collection} parent
     * @param actionData
     * @constructor
     */
    function Action(actionData, parent) {
        _.extend(this, actionData);
        this.parent = parent;
    }

    Action.prototype = {

        /**
         *
         * @param name
         * @return {*}
         */
        getFieldByName: function (name) {
            return _.find(this.fields, function (field) {
                return field.name == name;
            });
        }

        /**
         *
         * @param {Object} options
         * @return {$.Deferred|undefined}
         */
        , execute: function (options) {
            if (! this.parent) {
                return;
            }

            var defaults = {
                url: this.href
                , actionName: this.name
                , type: this.method
                , contentType: this.type
                , validate: true
                , patch: true       // Force BB to only send those attributes we specify
            };

            options = _.extend(defaults, options);
            return this.parent.save(this.parent.getAllByAction(this.name), options);
        }

    };

    /**
     *
     * @static
     * @param name
     * @return {String}
     */
    function toCamelCase(name) {
        return name.replace(/(\-[a-z])/g, function(match){return match.toUpperCase().replace('-','');});
    }

    /**
     *
     * @static
     * @param entity
     * @return {String}
     */
    function getUrl(entity) {
        var link, url;

        if (entity.href) {
            url = entity.href;
        } else if (entity.links) {
            link = entity.links.filter(function (link) {
                return !!(link.rel && link.rel.filter(function (relType) {
                    return relType == 'self';
                }).length);
            })[0];

            if (link) {
                url = link.href;
            }
        } else {
            warn('Missing href or "self" link');
            url = '';
        }

        return url;
    }

    /**
     * Access to the representation's "self" url, or its "href" if there is one.
     *
     * @return {String}
     */
    function url() {
        return getUrl(this._data);
    }

    /**
     *
     * @static
     * @param {Object} sirenObj Can be a siren entity or siren action object
     * @return {Array}
     */
    function getClassNames(sirenObj) {
        return sirenObj['class'] || [];
    }

    /**
     *
     * @static
     * @param {Array} entities
     * @param {Object} filters
     * @return {Array}
     */
    function filter(entities, filters) {
        return _.filter(entities, function (entity) {
            return hasProperties(entity, filters);
        });
    }

    /**
     *
     * @param {Backbone.Siren.Model|Backbone.Siren.Collection} bbSiren
     * @param {Object} filters
     * @return {Boolean}
     */
    function hasProperties(bbSiren, filters) {
        var _hasProperties = true;

        if (filters.className) {
            _hasProperties = bbSiren.hasClass(filters.className);
        }

        if (filters.rel) {
            _hasProperties = bbSiren.rel() == filters.rel;
        }

        return _hasProperties;
    }

    /**
     *
     * @param {Object} action
     * @param {Object} filters
     * @return {Boolean}
     */
    function actionHasProperties(action, filters) {
        var hasProperties = true;

        if (filters.className) {
            hasProperties = _hasClass(action, filters.className);
        }

        if (filters.name) {
            hasProperties = action.name == filters.name;
        }

        return hasProperties;
    }

    /**
     *
     * @static
     * @param sirenObj
     * @param className
     * @return {Boolean}
     */
    function _hasClass(sirenObj, className) {
        return _.indexOf(getClassNames(sirenObj), className) > -1;
    }

    /**
     *
     * @param className
     * @return {Boolean}
     */
    function hasClass(className) {
        return _hasClass(this._data, className);
    }

    /**
     * Accesses the "class" property of the Siren Object
     *
     * @return {Array}
     */
    function classes() {
        return getClassNames(this._data);
    }

    /**
     *
     * @param {String} rel
     * @return {Array}
     */
    function links(rel) {
        var _links = this._data.links;

        if (rel) {
            _links = _.filter(_links, function (link) {
                return _.indexOf(link.rel, rel) > -1;
            });
        }

        return _links || [];
    }

    /**
     *
     * @param {String} rel
     * @return {Array} An array of jqXhr objects.
     */
    function request(rel) {
        var self = this
        , requests = []
        , links = this.links(rel);

        _.each(links, function (link) {
            requests.push($.getJSON(link.href, function (sirenResponse) {
                self.parseEntity(sirenResponse);
            }));
        });

        return requests;
    }

    /**
     *
     * @static
     * @param sirenObj
     * @return {String}
     */
    function getRel(sirenObj) {
        var rel = sirenObj.rel;

        if (rel) {
            rel = rel[0]; // @todo, arbitrarily grabbing the first rel might be bad but I still don't understand the use case for many rels...
            rel = rel.slice(rel.lastIndexOf('/') + 1, rel.length);
        }

        return rel;
    }

    /**
     *
     * @return {String}
     */
    function rel() {
        return getRel(this._data);
    }

    /**
     *
     * @param filters
     * @return {*|Array}
     */
    function actions(filters) {
        var _actions = this._actions;

        if (filters) {
            _actions = _.filter(_actions, function (action) {
                return actionHasProperties(action, filters);
            });
        }
        return _actions;
    }

    /**
     *
     * @param {String} actionName
     * @return {Object}
     */
    function getAllByAction(actionName) {
        var values
        , action = this.getActionByName(actionName)
        , self = this;

        if (action) {
            values = {};
            _.each(action.fields, function (field) {
                if (self instanceof Backbone.Model) {
                    values[field.name] = self.get(field.name);
                } else {
                    values[field.name] = self.meta(field.name);
                }

            });
        }

        return values;
    }

    /**
     *
     * @param {String} name
     * @return {Object|undefined}
     */
    function getActionByName(name) {
        return _.find(this._actions, function (action) {
            return action.name == name;
        });
    }

    /**
     * Access to the representation's "title"
     *
     * @return {String}
     */
    function title() {
        return this._data.title;
    }

    /**
     *
     * @return {Array}
     */
    function parseActions() {
        var self = this
        , _actions = [];

        _.each(self._data.actions, function (action) {
            var bbSirenAction = new Backbone.Siren.Action(action, self);

            _actions.push(bbSirenAction);

            if (action.name) {
                self[toCamelCase(action.name)] = _.bind(bbSirenAction.execute, bbSirenAction);
            } else {
                warn('Action is missing a name, unable to add top level method', action);
            }
        });

        self._actions = _actions;
        return _actions;
    }

    return {
        settings: {
            showWarnings: true
        }

        , store: store
        , warn: warn
        , Action: Action

        , Model: Backbone.Model.extend({

            url: url
            , classes: classes
            , hasClass: hasClass
            , rel: rel
            , title: title
            , actions: actions
            , links: links
            , getActionByName: getActionByName
            , getAllByAction: getAllByAction
            , parseActions: parseActions
            , request: request

            /**
             *
             * @param {Object} sirenObj
             * @param {Object} options
             */
            , resolveEntities: function (sirenObj, options) {
                var self = this
                , resolvedEntities = [];

                _.each(sirenObj.entities, function(entity) {
                    if ((entity.href && options.autoFetch == 'linked') || options.autoFetch == 'all') {
                        resolvedEntities.push(self.fetchEntity(entity, options));
                    } else {
                        resolvedEntities.push(self.addEntity(entity, options));
                    }
                });

                return $.when(resolvedEntities).done(function () {
                    self.trigger('resolve', self);
                });
            }

            /**
             * http://backbonejs.org/#Model-parse
             *
             * @param {Object} sirenObj
             */
            , parse: function (sirenObj, options) {
                this._data = sirenObj; // Stores the entire siren object in raw json
                this._entities = [];

                this.resolveEntities(sirenObj, options);
                this.parseActions(options);

                return sirenObj.properties;
            }

            /**
             * http://backbonejs.org/#Model-toJSON
             *
             * @param {Object} options
             */
            , toJSON: function () {
                var json = _.clone(this.attributes)
                , entities = this.entities();

                _.each(entities, function (entity) {
                    json[entity.rel()] = entity;
                });

                return json;
            }

            /**
             * Filters the entity's properties and returns only sub-entities
             *
             * @return {Array}
             */
            , entities: function (filters) {
                var self = this
                , entities = _.filter(this, function (val, name) {
                    return _.indexOf(self._entities, name) > -1;
                });

                if (filters) {
                    entities = filter(entities, filters);
                }

                return entities;
            }

            /**
             *
             * @param {Object} entity
             */
            , fetchEntity: function (entity) {
                var self = this;

                return $.getJSON(getUrl(entity), function (resolvedEntity) {
                    self.addEntity(resolvedEntity);
                });
            }

            /**
             *
             * @param {Object} entity
             */
            , parseEntity: function (entity) {
                var bbSiren;

                if (_hasClass(entity, 'collection')) {
                    bbSiren = new Backbone.Siren.Collection(entity);
                } else if (_hasClass(entity, 'error')) {
                    // @todo how should we represent errors?
                    warn('@todo - errors');
                } else {
                    bbSiren = new Backbone.Siren.Model(entity);
                    store.add(bbSiren);
                }

                return bbSiren;
            }

            /**
             *
             * @param {Object} entity
             */
            , addEntity: function (entity) {
                var bbSiren = this.parseEntity(entity)
                , rel = bbSiren.rel();

                this.set(rel, bbSiren);
                this._entities.push(rel);

                return bbSiren;
            }

            /**
             * http://backbonejs.org/#Model-constructor
             *
             * @param {Object} attributes
             * @param {Object} options
             */
            , constructor: function (sirenObj, options) {
                options = options || {};
                options.parse = true; // Force "parse" to be called on instantiation: http://stackoverflow.com/questions/11068989/backbone-js-using-parse-without-calling-fetch/14950519#14950519

                Backbone.Model.call(this, sirenObj, options);
            }

        })

        , Collection: Backbone.Collection.extend({
            url: url
            , classes: classes
            , hasClass: hasClass
            , title: title
            , rel: rel
            , links: links
            , actions: actions
            , getActionByName: getActionByName
            , getAllByAction: getAllByAction
            , parseActions: parseActions
            , request: request

            /**
             * http://backbonejs.org/#Collection-toJSON
             *
             * @param {Object} options
             */
            , toJSON: function (options) {
                return this.map(function(model){
                    return model.toJSON(options);
                });
            }

            /**
             *
             * @param {Function|Object} arg
             * @return {Array}
             */
            , filter: function (arg) {
                if (typeof arg ==  'function') {
                    return _.filter(this, arg);
                } else {
                    return filter(this, arg);
                }
            }

            /**
             * http://backbonejs.org/#Collection-parse
             *
             * @param {Object} sirenObj
             */
            , parse: function (sirenObj, options) {
                this._data = sirenObj; // Store the entire siren object in raw json
                this._meta = sirenObj.properties || {};

                var models = [];
                _.each(sirenObj.entities, function (entity) {
                    var model = new Backbone.Siren.Model(entity);
                    models.push(model);
                    store.add(model);
                });

                this.parseActions(options);

                return models;
            }

            /**
             * Unlike Models, collections don't have attributes.
             * However, there are times we need to store "meta" data about the collection such as
             * the paging "offset".
             *
             * @param {*} prop
             * @param {*} value
             * @return {Object}
             */
            , meta: function (name) {
                return this._meta[name];
            }

            /**
             * http://backbonejs.org/#Collection-constructor
             *
             * @param {Object} attributes
             * @param {Object} options
             */
            , constructor: function (sirenObj, options) {
                options = options || {};
                options.parse = true; // Force "parse" to be called on instantiation: http://stackoverflow.com/questions/11068989/backbone-js-using-parse-without-calling-fetch/14950519#14950519

                Backbone.Collection.call(this, sirenObj, options);
            }
        })
    };
}(_, Backbone));<commit_msg>no need to pass the sirenObject in as a parameter<commit_after>Backbone.Siren = (function (_, Backbone, undefined) {
    'use strict';

    // The store-cache
    var _store = {}

    /**
     * Super simple store.  Good enough for now.
     *
     * @type {Object}
     * @private
     */
    , store = {

        /**
         *
         * @param {Backbone.Siren.Model} model
         * @return {Backbone.Siren.Model}
         */
        add: function (model) {
            return _store[model.url()] = model;
        }

        /**
         *
         * @param {Backbone.Siren.Model} model
         * @return {Boolean}
         */
        , exists: function (model) {
            return !!_store[model.url()];
        }

        /**
         *
         * @return {Array}
         */
        , all: function () {
            return _store;
        }
    }

    /**
     *
     * @type {Object}
     * @private
     */
    , warn = function () {
        if (Backbone.Siren.settings.showWarnings && console) {
            console.warn.apply(console, arguments);
        }
    };

    /**
     *
     * @param {Backbone.Siren.Model|Backbone.Siren.Collection} parent
     * @param actionData
     * @constructor
     */
    function Action(actionData, parent) {
        _.extend(this, actionData);
        this.parent = parent;
    }

    Action.prototype = {

        /**
         *
         * @param name
         * @return {*}
         */
        getFieldByName: function (name) {
            return _.find(this.fields, function (field) {
                return field.name == name;
            });
        }

        /**
         *
         * @param {Object} options
         * @return {$.Deferred|undefined}
         */
        , execute: function (options) {
            if (! this.parent) {
                return;
            }

            var defaults = {
                url: this.href
                , actionName: this.name
                , type: this.method
                , contentType: this.type
                , validate: true
                , patch: true       // Force BB to only send those attributes we specify
            };

            options = _.extend(defaults, options);
            return this.parent.save(this.parent.getAllByAction(this.name), options);
        }

    };

    /**
     *
     * @static
     * @param name
     * @return {String}
     */
    function toCamelCase(name) {
        return name.replace(/(\-[a-z])/g, function(match){return match.toUpperCase().replace('-','');});
    }

    /**
     *
     * @static
     * @param entity
     * @return {String}
     */
    function getUrl(entity) {
        var link, url;

        if (entity.href) {
            url = entity.href;
        } else if (entity.links) {
            link = entity.links.filter(function (link) {
                return !!(link.rel && link.rel.filter(function (relType) {
                    return relType == 'self';
                }).length);
            })[0];

            if (link) {
                url = link.href;
            }
        } else {
            warn('Missing href or "self" link');
            url = '';
        }

        return url;
    }

    /**
     * Access to the representation's "self" url, or its "href" if there is one.
     *
     * @return {String}
     */
    function url() {
        return getUrl(this._data);
    }

    /**
     *
     * @static
     * @param {Object} sirenObj Can be a siren entity or siren action object
     * @return {Array}
     */
    function getClassNames(sirenObj) {
        return sirenObj['class'] || [];
    }

    /**
     *
     * @static
     * @param {Array} entities
     * @param {Object} filters
     * @return {Array}
     */
    function filter(entities, filters) {
        return _.filter(entities, function (entity) {
            return hasProperties(entity, filters);
        });
    }

    /**
     *
     * @param {Backbone.Siren.Model|Backbone.Siren.Collection} bbSiren
     * @param {Object} filters
     * @return {Boolean}
     */
    function hasProperties(bbSiren, filters) {
        var _hasProperties = true;

        if (filters.className) {
            _hasProperties = bbSiren.hasClass(filters.className);
        }

        if (filters.rel) {
            _hasProperties = bbSiren.rel() == filters.rel;
        }

        return _hasProperties;
    }

    /**
     *
     * @param {Object} action
     * @param {Object} filters
     * @return {Boolean}
     */
    function actionHasProperties(action, filters) {
        var hasProperties = true;

        if (filters.className) {
            hasProperties = _hasClass(action, filters.className);
        }

        if (filters.name) {
            hasProperties = action.name == filters.name;
        }

        return hasProperties;
    }

    /**
     *
     * @static
     * @param sirenObj
     * @param className
     * @return {Boolean}
     */
    function _hasClass(sirenObj, className) {
        return _.indexOf(getClassNames(sirenObj), className) > -1;
    }

    /**
     *
     * @param className
     * @return {Boolean}
     */
    function hasClass(className) {
        return _hasClass(this._data, className);
    }

    /**
     * Accesses the "class" property of the Siren Object
     *
     * @return {Array}
     */
    function classes() {
        return getClassNames(this._data);
    }

    /**
     *
     * @param {String} rel
     * @return {Array}
     */
    function links(rel) {
        var _links = this._data.links;

        if (rel) {
            _links = _.filter(_links, function (link) {
                return _.indexOf(link.rel, rel) > -1;
            });
        }

        return _links || [];
    }

    /**
     *
     * @param {String} rel
     * @return {Array} An array of jqXhr objects.
     */
    function request(rel) {
        var self = this
        , requests = []
        , links = this.links(rel);

        _.each(links, function (link) {
            requests.push($.getJSON(link.href, function (sirenResponse) {
                self.parseEntity(sirenResponse);
            }));
        });

        return requests;
    }

    /**
     *
     * @static
     * @param sirenObj
     * @return {String}
     */
    function getRel(sirenObj) {
        var rel = sirenObj.rel;

        if (rel) {
            rel = rel[0]; // @todo, arbitrarily grabbing the first rel might be bad but I still don't understand the use case for many rels...
            rel = rel.slice(rel.lastIndexOf('/') + 1, rel.length);
        }

        return rel;
    }

    /**
     *
     * @return {String}
     */
    function rel() {
        return getRel(this._data);
    }

    /**
     *
     * @param filters
     * @return {*|Array}
     */
    function actions(filters) {
        var _actions = this._actions;

        if (filters) {
            _actions = _.filter(_actions, function (action) {
                return actionHasProperties(action, filters);
            });
        }
        return _actions;
    }

    /**
     *
     * @param {String} actionName
     * @return {Object}
     */
    function getAllByAction(actionName) {
        var values
        , action = this.getActionByName(actionName)
        , self = this;

        if (action) {
            values = {};
            _.each(action.fields, function (field) {
                if (self instanceof Backbone.Model) {
                    values[field.name] = self.get(field.name);
                } else {
                    values[field.name] = self.meta(field.name);
                }

            });
        }

        return values;
    }

    /**
     *
     * @param {String} name
     * @return {Object|undefined}
     */
    function getActionByName(name) {
        return _.find(this._actions, function (action) {
            return action.name == name;
        });
    }

    /**
     * Access to the representation's "title"
     *
     * @return {String}
     */
    function title() {
        return this._data.title;
    }

    /**
     *
     * @return {Array}
     */
    function parseActions() {
        var self = this
        , _actions = [];

        _.each(self._data.actions, function (action) {
            var bbSirenAction = new Backbone.Siren.Action(action, self);

            _actions.push(bbSirenAction);

            if (action.name) {
                self[toCamelCase(action.name)] = _.bind(bbSirenAction.execute, bbSirenAction);
            } else {
                warn('Action is missing a name, unable to add top level method', action);
            }
        });

        self._actions = _actions;
        return _actions;
    }

    return {
        settings: {
            showWarnings: true
        }

        , store: store
        , warn: warn
        , Action: Action

        , Model: Backbone.Model.extend({

            url: url
            , classes: classes
            , hasClass: hasClass
            , rel: rel
            , title: title
            , actions: actions
            , links: links
            , getActionByName: getActionByName
            , getAllByAction: getAllByAction
            , parseActions: parseActions
            , request: request

            /**
             *
             * @param {Object} sirenObj
             * @param {Object} options
             */
            , resolveEntities: function (options) {
                var self = this
                , resolvedEntities = [];

                _.each(this._data.entities, function(entity) {
                    if ((entity.href && options.autoFetch == 'linked') || options.autoFetch == 'all') {
                        resolvedEntities.push(self.fetchEntity(entity, options));
                    } else {
                        resolvedEntities.push(self.setEntity(entity, options));
                    }
                });

                return $.when(resolvedEntities).done(function () {
                    self.trigger('resolve', self);
                });
            }

            /**
             * http://backbonejs.org/#Model-parse
             *
             * @param {Object} sirenObj
             */
            , parse: function (sirenObj, options) {
                this._data = sirenObj; // Stores the entire siren object in raw json
                this._entities = [];

                this.resolveEntities(options);
                this.parseActions(options);

                return sirenObj.properties;
            }

            /**
             * http://backbonejs.org/#Model-toJSON
             *
             * @param {Object} options
             */
            , toJSON: function () {
                var json = _.clone(this.attributes)
                , entities = this.entities();

                _.each(entities, function (entity) {
                    json[entity.rel()] = entity;
                });

                return json;
            }

            /**
             * Filters the entity's properties and returns only sub-entities
             *
             * @return {Array}
             */
            , entities: function (filters) {
                var self = this
                , entities = _.filter(this, function (val, name) {
                    return _.indexOf(self._entities, name) > -1;
                });

                if (filters) {
                    entities = filter(entities, filters);
                }

                return entities;
            }

            /**
             *
             * @param {Object} entity
             */
            , fetchEntity: function (entity) {
                var self = this;

                return $.getJSON(getUrl(entity), function (resolvedEntity) {
                    self.setEntity(resolvedEntity);
                });
            }

            /**
             *
             * @param {Object} entity
             */
            , parseEntity: function (entity) {
                var bbSiren;

                if (_hasClass(entity, 'collection')) {
                    bbSiren = new Backbone.Siren.Collection(entity);
                } else if (_hasClass(entity, 'error')) {
                    // @todo how should we represent errors?
                    warn('@todo - errors');
                } else {
                    bbSiren = new Backbone.Siren.Model(entity);
                    store.add(bbSiren);
                }

                return bbSiren;
            }

            /**
             *
             * @param {Object} entity
             */
            , setEntity: function (entity) {
                var bbSiren = this.parseEntity(entity)
                , rel = bbSiren.rel();

                this.set(rel, bbSiren);
                this._entities.push(rel);

                return bbSiren;
            }

            /**
             * http://backbonejs.org/#Model-constructor
             *
             * @param {Object} attributes
             * @param {Object} options
             */
            , constructor: function (sirenObj, options) {
                options = options || {};
                options.parse = true; // Force "parse" to be called on instantiation: http://stackoverflow.com/questions/11068989/backbone-js-using-parse-without-calling-fetch/14950519#14950519

                Backbone.Model.call(this, sirenObj, options);
            }

        })

        , Collection: Backbone.Collection.extend({
            url: url
            , classes: classes
            , hasClass: hasClass
            , title: title
            , rel: rel
            , links: links
            , actions: actions
            , getActionByName: getActionByName
            , getAllByAction: getAllByAction
            , parseActions: parseActions
            , request: request

            /**
             * http://backbonejs.org/#Collection-toJSON
             *
             * @param {Object} options
             */
            , toJSON: function (options) {
                return this.map(function(model){
                    return model.toJSON(options);
                });
            }

            /**
             *
             * @param {Function|Object} arg
             * @return {Array}
             */
            , filter: function (arg) {
                if (typeof arg ==  'function') {
                    return _.filter(this, arg);
                } else {
                    return filter(this, arg);
                }
            }

            /**
             * http://backbonejs.org/#Collection-parse
             *
             * @param {Object} sirenObj
             */
            , parse: function (sirenObj, options) {
                this._data = sirenObj; // Store the entire siren object in raw json
                this._meta = sirenObj.properties || {};

                var models = [];
                _.each(sirenObj.entities, function (entity) {
                    var model = new Backbone.Siren.Model(entity);
                    models.push(model);
                    store.add(model);
                });

                this.parseActions(options);

                return models;
            }

            /**
             * Unlike Models, collections don't have attributes.
             * However, there are times we need to store "meta" data about the collection such as
             * the paging "offset".
             *
             * @param {*} prop
             * @param {*} value
             * @return {Object}
             */
            , meta: function (name) {
                return this._meta[name];
            }

            /**
             * http://backbonejs.org/#Collection-constructor
             *
             * @param {Object} attributes
             * @param {Object} options
             */
            , constructor: function (sirenObj, options) {
                options = options || {};
                options.parse = true; // Force "parse" to be called on instantiation: http://stackoverflow.com/questions/11068989/backbone-js-using-parse-without-calling-fetch/14950519#14950519

                Backbone.Collection.call(this, sirenObj, options);
            }
        })
    };
}(_, Backbone));<|endoftext|>


<commit_before>'use strict';

import createReactClass from 'create-react-class';
import PropTypes from 'prop-types';
import React from 'react';
import cloneReferencedElement from 'react-clone-referenced-element';
import {
  ScrollView,
  StyleSheet,
  View,
} from 'react-native';
import ScrollableMixin from 'react-native-scrollable-mixin';

type DefaultProps = {
  renderScrollComponent: (props: Object) => ReactElement;
};

let InvertibleScrollView = createReactClass({
  mixins: [ScrollableMixin],

  propTypes: {
    ...ScrollView.propTypes,
    inverted: PropTypes.bool,
    renderScrollComponent: PropTypes.func.isRequired,
  },

  getDefaultProps(): DefaultProps {
    return {
      renderScrollComponent: props => <ScrollView {...props} />,
    };
  },

  getScrollResponder(): ReactComponent {
    return this._scrollComponent.getScrollResponder();
  },

  setNativeProps(props: Object) {
    this._scrollComponent.setNativeProps(props);
  },

  render() {
    var {
      inverted,
      renderScrollComponent,
      ...props
    } = this.props;

    if (inverted) {
      if (this.props.horizontal) {
        props.style = [styles.horizontallyInverted, props.style];
        props.children = this._renderInvertedChildren(props.children, styles.horizontallyInverted);
      } else {
        props.style = [styles.verticallyInverted, props.style];
        props.children = this._renderInvertedChildren(props.children, styles.verticallyInverted);
      }
    }

    return cloneReferencedElement(renderScrollComponent(props), {
      ref: component => { this._scrollComponent = component; },
    });
  },

  _renderInvertedChildren(children, inversionStyle) {
    return React.Children.map(children, child => {
      return child ? <View style={inversionStyle}>{child}</View> : child;
    });
  },
});

let styles = StyleSheet.create({
  verticallyInverted: {
    flex: 1,
    transform: [
      { scaleY: -1 },
    ],
  },
  horizontallyInverted: {
    flex: 1,
    transform: [
      { scaleX: -1 },
    ],
  },
});

module.exports = InvertibleScrollView;
<commit_msg>Fix exporting for web<commit_after>'use strict';

import createReactClass from 'create-react-class';
import PropTypes from 'prop-types';
import React from 'react';
import cloneReferencedElement from 'react-clone-referenced-element';
import {
  ScrollView,
  StyleSheet,
  View,
} from 'react-native';
import ScrollableMixin from 'react-native-scrollable-mixin';

type DefaultProps = {
  renderScrollComponent: (props: Object) => ReactElement;
};

let InvertibleScrollView = createReactClass({
  mixins: [ScrollableMixin],

  propTypes: {
    ...ScrollView.propTypes,
    inverted: PropTypes.bool,
    renderScrollComponent: PropTypes.func.isRequired,
  },

  getDefaultProps(): DefaultProps {
    return {
      renderScrollComponent: props => <ScrollView {...props} />,
    };
  },

  getScrollResponder(): ReactComponent {
    return this._scrollComponent.getScrollResponder();
  },

  setNativeProps(props: Object) {
    this._scrollComponent.setNativeProps(props);
  },

  render() {
    var {
      inverted,
      renderScrollComponent,
      ...props
    } = this.props;

    if (inverted) {
      if (this.props.horizontal) {
        props.style = [styles.horizontallyInverted, props.style];
        props.children = this._renderInvertedChildren(props.children, styles.horizontallyInverted);
      } else {
        props.style = [styles.verticallyInverted, props.style];
        props.children = this._renderInvertedChildren(props.children, styles.verticallyInverted);
      }
    }

    return cloneReferencedElement(renderScrollComponent(props), {
      ref: component => { this._scrollComponent = component; },
    });
  },

  _renderInvertedChildren(children, inversionStyle) {
    return React.Children.map(children, child => {
      return child ? <View style={inversionStyle}>{child}</View> : child;
    });
  },
});

let styles = StyleSheet.create({
  verticallyInverted: {
    flex: 1,
    transform: [
      { scaleY: -1 },
    ],
  },
  horizontallyInverted: {
    flex: 1,
    transform: [
      { scaleX: -1 },
    ],
  },
});

export default InvertibleScrollView;
<|endoftext|>