brainstem-js 0.2.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.travis.yml +3 -6
- data/Gemfile.lock +1 -1
- data/README.md +3 -1
- data/Rakefile +2 -0
- data/lib/brainstem/js/version.rb +1 -1
- data/spec/brainstem-collection-spec.coffee +25 -0
- data/spec/{brianstem-expectation-spec.coffee → brainstem-expectation-spec.coffee} +132 -17
- data/spec/brainstem-model-spec.coffee +67 -27
- data/spec/brainstem-sync-spec.coffee +29 -6
- data/spec/brainstem-utils-spec.coffee +11 -1
- data/spec/helpers/builders.coffee +2 -2
- data/spec/helpers/models/post.coffee +1 -1
- data/spec/helpers/models/project.coffee +1 -1
- data/spec/helpers/models/task.coffee +2 -2
- data/spec/helpers/models/time-entry.coffee +1 -1
- data/spec/helpers/models/user.coffee +1 -1
- data/spec/helpers/spec-helper.coffee +21 -0
- data/spec/loaders/abstract-loader-shared-behavior.coffee +604 -0
- data/spec/loaders/abstract-loader-spec.coffee +3 -0
- data/spec/loaders/collection-loader-spec.coffee +146 -0
- data/spec/loaders/model-loader-spec.coffee +99 -0
- data/spec/storage-manager-spec.coffee +242 -56
- data/vendor/assets/javascripts/brainstem/brainstem-collection.coffee +16 -6
- data/vendor/assets/javascripts/brainstem/brainstem-expectation.coffee +70 -20
- data/vendor/assets/javascripts/brainstem/brainstem-model.coffee +13 -13
- data/vendor/assets/javascripts/brainstem/brainstem-sync.coffee +8 -3
- data/vendor/assets/javascripts/brainstem/loaders/abstract-loader.coffee +289 -0
- data/vendor/assets/javascripts/brainstem/loaders/collection-loader.coffee +68 -0
- data/vendor/assets/javascripts/brainstem/loaders/model-loader.coffee +35 -0
- data/vendor/assets/javascripts/brainstem/loading-mixin.coffee +1 -7
- data/vendor/assets/javascripts/brainstem/storage-manager.coffee +79 -196
- data/vendor/assets/javascripts/brainstem/utils.coffee +18 -4
- metadata +17 -6
    
        data/.travis.yml
    CHANGED
    
    | @@ -1,12 +1,9 @@ | |
| 1 1 | 
             
            language: ruby
         | 
| 2 | 
            -
            rvm: | 
| 2 | 
            +
            rvm:
         | 
| 3 | 
            +
              - 1.9.3
         | 
| 4 | 
            +
              - 2.0.0
         | 
| 3 5 |  | 
| 4 6 | 
             
            before_install: gem install bundler
         | 
| 5 7 |  | 
| 6 | 
            -
            notifications:
         | 
| 7 | 
            -
              hipchat: 09a2719b3284ea0d0d247355fa5542@Mavenlink Dev
         | 
| 8 | 
            -
             | 
| 9 8 | 
             
            git:
         | 
| 10 9 | 
             
              depth: 1
         | 
| 11 | 
            -
             | 
| 12 | 
            -
            source_key: LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlFcEFJQkFBS0NBUUVBOHBpRjdldDZKM2FmWCt0WDlWRG5HZEt4dDdZZVJVVUh3K29JQVJDekorejBvS3MxCnNwaEUxZlBSQksyUDFMUVV5cngyUThzalgxdnA1NUt6U3lnVWExMFRlUG90VnVWN3hZbVFqTG5FM2FueTJBNXIKMFhXZG4wS2k2cUt5TkVwd2ZJa3JYam8xSmY4SHJ0ajZ5Yi9VTFcySnZxbTBGTTZRNmU1TCtmUGdQN0JqeS9WSApUWHF4UWVRaTVaSVppa2JoeGxoK3RQNVZaM1JMRTF3bEhRdXpESUFZemRFdnFXeEFxYkhvNktyMjJSZzRhaGNPCnNEZ1FGK0drR0h2bjBKTkpmc05Cd3d2YUQ2Y3M5N2pnY045b1VrK1phOFNlbFZhazRjWVhRUk5WaTdPMVdkQnkKSE1xaVNPVjFZdEhSWWdwSFpPaHdYSUdxK3ZhUXVFR21SRzdBY1FJREFRQUJBb0lCQVFDOXBiQ2xjdjFXbG13MwpEd0wrK3RUL0llL2VmeGVnN1RzSjFBMlh6NWRPc2ZYM0dJRHM4ZzUxOTVuQi8zQytSbDB1dEMvOEJYVE1ta3o3CnhIbzNXY2pFdWNsOFBJOXZMQTBiT3RSdXZ0Y0F0bGZxd1ROV1ZvejNNSit0cjZ5Q0ptTlRaK1FvVUhhMkVtM08KS1QrOHNpTEx0S01IRXlGOVZwS0EzZEkxUDRwaUhodlFBdkgzQkdRR3piNW9UczV6RXJnRmZNbEZKWFc0S2NxcApYc1NnWDBjd3d3Nit3WDFLblRGY3lNak9QbTB2MWZSSTdHMGdtOG02VVJrakdVeXhpRXhkTVNEdm5yVEt4ajlvCitIK1NucU14RW5uczJRR1lnSEtnV1ZaSFFBUU9UenBNSVdvNndHODNMMDlyOUo3S1dzZHRwNStKeDNZWWE2eTMKU0tod3NQdGxBb0dCQVAvZTJ2SitCVmpkRXVOSjJibWo4YnlGK2lJd292Vm9FYW4xUExkZ3JJbTNQWkFrLytyMgpiSFV0d0xDVjBSR1l5Z1ZqaW42YmEzUitNM2ptanErM3Z1N0FsVlJNazdvcFVXcjUwanRzbDhhT1k4WkIxTlhuClRlRW5keEswTnBWaWs1VlFhNzBqcmh3NGtmekxhUk9xdUV1SGt6Qk95MXhJeStyUkNMNDMzT1ZmQW9HQkFQSzMKOHNXYm5YRzR0WjdHMU5QVjZ0YXJ5bEt4bFluRnhseG4rUmZueTFHbG1NejU4YW1Jb1F2SnJuSHlSR29xQ2dNOQo4eWJFMmVoVjJFM20wKzUwampDeFNrMjhxbkNybDdnbTdNcjhsVzdGQnZLSUw2NDFsM1pDT3pLSnBKWFdGN1BaCmdYVGplTmduRzliQkljMldBNHpzaGVDK05CRWxidmFkekRrT0hkd3ZBb0dBWkRlZ1lCdzE4ZkZkQllNV2NSeWkKZ1Nta3FDR09vam9wdVB6aDFCMWNWdkJiZjRyT1pmUXcxTkNmeVVwVXdlU3JNK01pQ3FiTE5xeDdjcDR6UXVYZwpOZGxlWTg4K2lVckhwZlBGZ1JydWM0bXYwS1pXTzVYR0xpcnIrM3AwYXB4YW04QU5BdDdud2d2eU9pWmR1S05FClhlanpJSmVzRlRBNkZuWGJTODNMaWxjQ2dZRUExai9kd3VUOGM3ZnlTZmVGUW9DZnpXTFRNMitpYW56ei9mbWgKZmFLVWJMdmFSNFdSOW02dWlmTTdVMFhoY2owdG5YTC93WWNlT3VJY0Q1ZmtGNmMzSkhBN0FLZTdZNzEwTFkvZQprY2VvT0tFZTR0T29Fd1VuYjdKREF2ZFJHeHBpemRUL1d5aTRNVVZFWTZzVHBaLzMvbHVDU2NKYnY0N2xoamdBClg1VEFjdTBDZ1lCRFhnNUF2VHVXTE5pRk91RW5RekhrRmp0N0lwRVVUN2lpZDByMHBJempMVmc1c08zSXRqTkQKNjlvWitIYUlCSVpMWkNQc2lpS2dmQkFIS0xXY3NEQTRnelY2ckpUWDdmdXYvcHZUTk9tdmJlcG5OK0JGd05xdApVU2c5UmxFZ0lFWEE3Q041aVFvb2l2SmFiT3VEREl0NUhiTS9Gd1M0V3QvQVdnQUZkYmpab3c9PQotLS0tLUVORCBSU0EgUFJJVkFURSBLRVktLS0tLQo=
         | 
    
        data/Gemfile.lock
    CHANGED
    
    
    
        data/README.md
    CHANGED
    
    | @@ -1,5 +1,7 @@ | |
| 1 1 | 
             
            # Brainstem.js
         | 
| 2 2 |  | 
| 3 | 
            +
            [](https://travis-ci.org/mavenlink/brainstem-js)
         | 
| 4 | 
            +
             | 
| 3 5 | 
             
            [Brainstem](https://github.com/mavenlink/brainstem) is designed to power rich APIs in Rails. The Brainstem gem provides a presenter library that handles converting ActiveRecord objects into structured JSON and a set of API abstractions that allow users to request sorts, filters, and association loads, allowing for simpler implementations, fewer requests, and smaller responses.
         | 
| 4 6 |  | 
| 5 7 | 
             
            The Brainstem.js library is a companion library for Backbone.js that makes integration with Brainstem APIs a breeze.  Brainstem.js adds an identity map and relational models to Backbone.
         | 
| @@ -23,7 +25,7 @@ What follows is an overview. | |
| 23 25 |  | 
| 24 26 | 
             
            ### StorageManager
         | 
| 25 27 |  | 
| 26 | 
            -
            The ` | 
| 28 | 
            +
            The `Brainstem.StorageManager` is in charge of loading data over the API, as well as returning already cached data.  We recommend setting one up in a singleton App class.
         | 
| 27 29 |  | 
| 28 30 | 
             
            	class Application
         | 
| 29 31 | 
             
            	  constructor: ->
         | 
    
        data/Rakefile
    CHANGED
    
    | @@ -9,6 +9,7 @@ task :build do | |
| 9 9 | 
             
              sh "rakep build"
         | 
| 10 10 | 
             
            end
         | 
| 11 11 |  | 
| 12 | 
            +
            desc 'Run jasmine specs with phantomjs'
         | 
| 12 13 | 
             
            task :spec => :build do
         | 
| 13 14 | 
             
              begin
         | 
| 14 15 | 
             
                exec *%w(phantomjs build/headless.js build/headless.html)
         | 
| @@ -18,6 +19,7 @@ task :spec => :build do | |
| 18 19 | 
             
              end
         | 
| 19 20 | 
             
            end
         | 
| 20 21 |  | 
| 22 | 
            +
            desc 'Start WEBrick server to run jasmine specs in browser'
         | 
| 21 23 | 
             
            task :server => :clean do
         | 
| 22 24 | 
             
              exec "rakep server"
         | 
| 23 25 | 
             
            end
         | 
    
        data/lib/brainstem/js/version.rb
    CHANGED
    
    
| @@ -53,6 +53,31 @@ describe 'Brainstem.Collection', -> | |
| 53 53 | 
             
                  expect(collection.lastFetchOptions.page).toEqual 3
         | 
| 54 54 | 
             
                  expect(collection.length).toEqual 5
         | 
| 55 55 |  | 
| 56 | 
            +
                it "fetches based on the last limit and offset if they were the pagination options used", ->
         | 
| 57 | 
            +
                  respondWith server, "/api/time_entries?limit=2&offset=0", resultsFrom: "time_entries", data: { time_entries: [buildTimeEntry(), buildTimeEntry()] }
         | 
| 58 | 
            +
                  respondWith server, "/api/time_entries?limit=2&offset=2", resultsFrom: "time_entries", data: { time_entries: [buildTimeEntry(), buildTimeEntry()] }
         | 
| 59 | 
            +
                  respondWith server, "/api/time_entries?limit=2&offset=4", resultsFrom: "time_entries", data: { time_entries: [buildTimeEntry()] }
         | 
| 60 | 
            +
                  collection = base.data.loadCollection "time_entries", limit: 2, offset: 0
         | 
| 61 | 
            +
                  expect(collection.length).toEqual 0
         | 
| 62 | 
            +
                  server.respond()
         | 
| 63 | 
            +
                  expect(collection.length).toEqual 2
         | 
| 64 | 
            +
                  expect(collection.lastFetchOptions.offset).toEqual 0
         | 
| 65 | 
            +
             | 
| 66 | 
            +
                  spy = jasmine.createSpy()
         | 
| 67 | 
            +
                  collection.loadNextPage success: spy
         | 
| 68 | 
            +
                  server.respond()
         | 
| 69 | 
            +
                  expect(spy).toHaveBeenCalledWith(collection, true)
         | 
| 70 | 
            +
                  expect(collection.lastFetchOptions.offset).toEqual 2
         | 
| 71 | 
            +
                  expect(collection.length).toEqual 4
         | 
| 72 | 
            +
             | 
| 73 | 
            +
                  spy = jasmine.createSpy()
         | 
| 74 | 
            +
                  collection.loadNextPage success: spy
         | 
| 75 | 
            +
                  expect(collection.length).toEqual 4
         | 
| 76 | 
            +
                  server.respond()
         | 
| 77 | 
            +
                  expect(spy).toHaveBeenCalledWith(collection, false)
         | 
| 78 | 
            +
                  expect(collection.lastFetchOptions.offset).toEqual 4
         | 
| 79 | 
            +
                  expect(collection.length).toEqual 5
         | 
| 80 | 
            +
             | 
| 56 81 | 
             
              describe "reload", ->
         | 
| 57 82 | 
             
                it "reloads the collection with the original params", ->
         | 
| 58 83 | 
             
                  respondWith server, "/api/posts?include=replies&parents_only=true&per_page=5&page=1", resultsFrom: "posts", data: { posts: [buildPost(message: "old post", reply_ids: [])] }
         | 
| @@ -63,6 +63,32 @@ describe 'Brainstem Expectations', -> | |
| 63 63 | 
             
                  expect(collection.get(1).get("tasks").models).toEqual [task1]
         | 
| 64 64 | 
             
                  expect(collection.get(2).get("tasks").models).toEqual []
         | 
| 65 65 |  | 
| 66 | 
            +
                describe 'recursive loading', ->
         | 
| 67 | 
            +
                  context 'recursive option is false', ->
         | 
| 68 | 
            +
                    it "should not try to recursively load includes in an expectation", ->
         | 
| 69 | 
            +
                      expectation = manager.stub "projects", include: '*', response: (stub) ->
         | 
| 70 | 
            +
                        stub.results = [project1, project2]
         | 
| 71 | 
            +
                        stub.associated.projects = [project1, project2]
         | 
| 72 | 
            +
                        stub.associated.tasks = [task1]
         | 
| 73 | 
            +
             | 
| 74 | 
            +
                      spy = spyOn(Brainstem.AbstractLoader.prototype, '_loadAdditionalIncludes')
         | 
| 75 | 
            +
                      collection = manager.loadCollection "projects", include: ["tasks" : ["time_entries"]]
         | 
| 76 | 
            +
                      expectation.respond()
         | 
| 77 | 
            +
                      expect(spy).not.toHaveBeenCalled()
         | 
| 78 | 
            +
             | 
| 79 | 
            +
                  context 'recursive option is true', ->
         | 
| 80 | 
            +
                    it "should recursively load includes in an expectation", ->
         | 
| 81 | 
            +
                      expectation = manager.stub "projects", include: '*', response: (stub) ->
         | 
| 82 | 
            +
                        stub.results = [project1, project2]
         | 
| 83 | 
            +
                        stub.associated.projects = [project1, project2]
         | 
| 84 | 
            +
                        stub.associated.tasks = [task1]
         | 
| 85 | 
            +
                        stub.recursive = true
         | 
| 86 | 
            +
             | 
| 87 | 
            +
                      spy = spyOn(Brainstem.AbstractLoader.prototype, '_loadAdditionalIncludes')
         | 
| 88 | 
            +
                      collection = manager.loadCollection "projects", include: ["tasks" : ["time_entries"]]
         | 
| 89 | 
            +
                      expectation.respond()
         | 
| 90 | 
            +
                      expect(spy).toHaveBeenCalled()
         | 
| 91 | 
            +
             | 
| 66 92 | 
             
                describe "triggering errors", ->
         | 
| 67 93 | 
             
                  it "triggers errors when asked to do so", ->
         | 
| 68 94 | 
             
                    errorSpy = jasmine.createSpy()
         | 
| @@ -80,8 +106,7 @@ describe 'Brainstem Expectations', -> | |
| 80 106 |  | 
| 81 107 | 
             
                    expectation.respond()
         | 
| 82 108 | 
             
                    expect(errorSpy).toHaveBeenCalled()
         | 
| 83 | 
            -
                    expect(errorSpy.mostRecentCall.args[0] | 
| 84 | 
            -
                    expect(errorSpy.mostRecentCall.args[1]).toEqual resp
         | 
| 109 | 
            +
                    expect(errorSpy.mostRecentCall.args[0]).toEqual resp
         | 
| 85 110 |  | 
| 86 111 | 
             
                  it "does not trigger errors when asked not to", ->
         | 
| 87 112 | 
             
                    errorSpy = jasmine.createSpy()
         | 
| @@ -92,6 +117,10 @@ describe 'Brainstem Expectations', -> | |
| 92 117 | 
             
                    expectation.respond()
         | 
| 93 118 | 
             
                    expect(errorSpy).not.toHaveBeenCalled()
         | 
| 94 119 |  | 
| 120 | 
            +
                it "should work without specifying results", ->
         | 
| 121 | 
            +
                  manager.stubImmediate "projects"
         | 
| 122 | 
            +
                  expect(-> manager.loadCollection("projects")).not.toThrow()
         | 
| 123 | 
            +
             | 
| 95 124 | 
             
              describe "responding immediately", ->
         | 
| 96 125 | 
             
                it "uses stubImmediate", ->
         | 
| 97 126 | 
             
                  expectation = manager.stubImmediate "projects", include: ["tasks"], response: (stub) ->
         | 
| @@ -133,6 +162,7 @@ describe 'Brainstem Expectations', -> | |
| 133 162 | 
             
                it "should allow wildcard params", ->
         | 
| 134 163 | 
             
                  manager.stubImmediate "projects", include: '*', response: (stub) ->
         | 
| 135 164 | 
             
                    stub.results = [project1, project2]
         | 
| 165 | 
            +
                    stub.associated.tasks = [task1]
         | 
| 136 166 | 
             
                  expect(manager.loadCollection("projects", include: ["tasks"]).models).toEqual [project1, project2]
         | 
| 137 167 | 
             
                  expect(manager.loadCollection("projects", include: ["users"]).models).toEqual [project1, project2]
         | 
| 138 168 | 
             
                  expect(manager.loadCollection("projects").models).toEqual [project1, project2]
         | 
| @@ -181,29 +211,114 @@ describe 'Brainstem Expectations', -> | |
| 181 211 | 
             
              describe "optionsMatch", ->
         | 
| 182 212 | 
             
                it "should ignore wrapping arrays", ->
         | 
| 183 213 | 
             
                  expectation = new Brainstem.Expectation("projects", { include: "workspaces" }, manager)
         | 
| 184 | 
            -
                   | 
| 185 | 
            -
             | 
| 214 | 
            +
                  loader = new Brainstem.CollectionLoader(storageManager: manager)
         | 
| 215 | 
            +
             | 
| 216 | 
            +
                  loader.setup(name: "projects", include: "workspaces")
         | 
| 217 | 
            +
                  expect(expectation.loaderOptionsMatch(loader)).toBe true
         | 
| 218 | 
            +
             | 
| 219 | 
            +
                  loader.setup(name: "projects", include: ["workspaces"])
         | 
| 220 | 
            +
                  expect(expectation.loaderOptionsMatch(loader)).toBe true
         | 
| 186 221 |  | 
| 187 222 | 
             
                it "should treat * as an any match", ->
         | 
| 188 223 | 
             
                  expectation = new Brainstem.Expectation("projects", { include: "*" }, manager)
         | 
| 189 | 
            -
             | 
| 190 | 
            -
                   | 
| 191 | 
            -
                   | 
| 224 | 
            +
             | 
| 225 | 
            +
                  loader = new Brainstem.CollectionLoader(storageManager: manager)
         | 
| 226 | 
            +
                  loader.setup(name: "projects", include: "workspaces")
         | 
| 227 | 
            +
                  expect(expectation.loaderOptionsMatch(loader)).toBe true
         | 
| 228 | 
            +
             | 
| 229 | 
            +
                  loader = new Brainstem.CollectionLoader(storageManager: manager)
         | 
| 230 | 
            +
                  loader.setup(name: "projects", include: ["anything"])
         | 
| 231 | 
            +
                  expect(expectation.loaderOptionsMatch(loader)).toBe true
         | 
| 232 | 
            +
             | 
| 233 | 
            +
                  loader = new Brainstem.CollectionLoader(storageManager: manager)
         | 
| 234 | 
            +
                  loader.setup(name: "projects", {})
         | 
| 235 | 
            +
                  expect(expectation.loaderOptionsMatch(loader)).toBe true
         | 
| 192 236 |  | 
| 193 237 | 
             
                it "should treat strings and numbers the same when appropriate", ->
         | 
| 194 238 | 
             
                  expectation = new Brainstem.Expectation("projects", { only: "1" }, manager)
         | 
| 195 | 
            -
             | 
| 196 | 
            -
                   | 
| 239 | 
            +
             | 
| 240 | 
            +
                  loader = new Brainstem.CollectionLoader(storageManager: manager)
         | 
| 241 | 
            +
                  loader.setup(name: "projects", only: 1)
         | 
| 242 | 
            +
                  expect(expectation.loaderOptionsMatch(loader)).toBe true
         | 
| 243 | 
            +
             | 
| 244 | 
            +
                  loader = new Brainstem.CollectionLoader(storageManager: manager)
         | 
| 245 | 
            +
                  loader.setup(name: "projects", only: "1")
         | 
| 246 | 
            +
                  expect(expectation.loaderOptionsMatch(loader)).toBe true
         | 
| 197 247 |  | 
| 198 248 | 
             
                it "should treat null, empty array, and empty object the same", ->
         | 
| 199 249 | 
             
                  expectation = new Brainstem.Expectation("projects", { filters: {} }, manager)
         | 
| 200 | 
            -
             | 
| 201 | 
            -
                   | 
| 202 | 
            -
                   | 
| 203 | 
            -
                  expect(expectation. | 
| 250 | 
            +
             | 
| 251 | 
            +
                  loader = new Brainstem.CollectionLoader(storageManager: manager)
         | 
| 252 | 
            +
                  loader.setup(name: "projects", filters: null)
         | 
| 253 | 
            +
                  expect(expectation.loaderOptionsMatch(loader)).toBe true
         | 
| 254 | 
            +
             | 
| 255 | 
            +
                  loader = new Brainstem.CollectionLoader(storageManager: manager)
         | 
| 256 | 
            +
                  loader.setup(name: "projects", filters: {})
         | 
| 257 | 
            +
                  expect(expectation.loaderOptionsMatch(loader)).toBe true
         | 
| 258 | 
            +
             | 
| 259 | 
            +
                  loader = new Brainstem.CollectionLoader(storageManager: manager)
         | 
| 260 | 
            +
                  loader.setup(name: "projects", {})
         | 
| 261 | 
            +
                  expect(expectation.loaderOptionsMatch(loader)).toBe true
         | 
| 262 | 
            +
             | 
| 263 | 
            +
                  loader = new Brainstem.CollectionLoader(storageManager: manager)
         | 
| 264 | 
            +
                  loader.setup(name: "projects", filters: { foo: "bar" })
         | 
| 265 | 
            +
                  expect(expectation.loaderOptionsMatch(loader)).toBe false
         | 
| 204 266 |  | 
| 205 267 | 
             
                  expectation = new Brainstem.Expectation("projects", {}, manager)
         | 
| 206 | 
            -
             | 
| 207 | 
            -
                   | 
| 208 | 
            -
                   | 
| 209 | 
            -
                  expect(expectation. | 
| 268 | 
            +
             | 
| 269 | 
            +
                  loader = new Brainstem.CollectionLoader(storageManager: manager)
         | 
| 270 | 
            +
                  loader.setup(name: "projects", filters: null)
         | 
| 271 | 
            +
                  expect(expectation.loaderOptionsMatch(loader)).toBe true
         | 
| 272 | 
            +
             | 
| 273 | 
            +
                  loader = new Brainstem.CollectionLoader(storageManager: manager)
         | 
| 274 | 
            +
                  loader.setup(name: "projects", filters: {})
         | 
| 275 | 
            +
                  expect(expectation.loaderOptionsMatch(loader)).toBe true
         | 
| 276 | 
            +
             | 
| 277 | 
            +
                  loader = new Brainstem.CollectionLoader(storageManager: manager)
         | 
| 278 | 
            +
                  loader.setup(name: "projects", {})
         | 
| 279 | 
            +
                  expect(expectation.loaderOptionsMatch(loader)).toBe true
         | 
| 280 | 
            +
             | 
| 281 | 
            +
                  loader = new Brainstem.CollectionLoader(storageManager: manager)
         | 
| 282 | 
            +
                  loader.setup(name: "projects", filters: { foo: "bar" })
         | 
| 283 | 
            +
                  expect(expectation.loaderOptionsMatch(loader)).toBe false
         | 
| 284 | 
            +
             | 
| 285 | 
            +
              describe 'stubbing models', ->
         | 
| 286 | 
            +
                context 'a model that matches the load is already in the storage manager', ->
         | 
| 287 | 
            +
                  it 'updates that model', ->
         | 
| 288 | 
            +
                    project = buildAndCacheProject()
         | 
| 289 | 
            +
             | 
| 290 | 
            +
                    expectation = manager.stubModel 'project', project.id, response: (stub) ->
         | 
| 291 | 
            +
                      stub.result = buildProject(id: project.id, title: 'foobar')
         | 
| 292 | 
            +
             | 
| 293 | 
            +
                    loaderSpy = jasmine.createSpy('loader').andCallFake (model) ->
         | 
| 294 | 
            +
                      expect(model).toEqual project
         | 
| 295 | 
            +
                      expect(model.get('title')).toEqual 'foobar'
         | 
| 296 | 
            +
             | 
| 297 | 
            +
                    loader = manager.loadModel 'project', project.id
         | 
| 298 | 
            +
                    loader.done(loaderSpy)
         | 
| 299 | 
            +
             | 
| 300 | 
            +
                    expectation.respond()
         | 
| 301 | 
            +
                    expect(loaderSpy).toHaveBeenCalled()
         | 
| 302 | 
            +
                    expect(manager.storage('projects').length).toEqual 1
         | 
| 303 | 
            +
             | 
| 304 | 
            +
                context 'a model is not already in the storage manager', ->
         | 
| 305 | 
            +
                  it 'adds the model from the loader to the storageManager', ->
         | 
| 306 | 
            +
                    project = buildProject()
         | 
| 307 | 
            +
                    stubbedProject = buildProject(id: project.id, title: 'foobar')
         | 
| 308 | 
            +
             | 
| 309 | 
            +
                    expectation = manager.stubModel 'project', project.id, response: (stub) ->
         | 
| 310 | 
            +
                      stub.result = stubbedProject
         | 
| 311 | 
            +
             | 
| 312 | 
            +
                    loader = manager.loadModel 'project', project.id
         | 
| 313 | 
            +
             | 
| 314 | 
            +
                    loaderSpy = jasmine.createSpy('loader').andCallFake (model) ->
         | 
| 315 | 
            +
                      expect(model).toEqual loader.getModel()
         | 
| 316 | 
            +
                      expect(model.attributes).toEqual stubbedProject.attributes
         | 
| 317 | 
            +
                      expect(manager.storage('projects').get(project.id)).toEqual loader.getModel()
         | 
| 318 | 
            +
                      expect(model.get('title')).toEqual 'foobar'
         | 
| 319 | 
            +
             | 
| 320 | 
            +
                    loader.done(loaderSpy)
         | 
| 321 | 
            +
             | 
| 322 | 
            +
                    expectation.respond()
         | 
| 323 | 
            +
                    expect(loaderSpy).toHaveBeenCalled()
         | 
| 324 | 
            +
                    expect(manager.storage('projects').length).toEqual 1
         | 
| @@ -28,6 +28,51 @@ describe 'Brainstem.Model', -> | |
| 28 28 | 
             
                  expect(base.data.storage('users').get(5).attributes).toEqual(response.users[5])
         | 
| 29 29 | 
             
                  expect(base.data.storage('users').get(6).attributes).toEqual(response.users[6])
         | 
| 30 30 |  | 
| 31 | 
            +
                describe 'adding new models to the storage manager', ->
         | 
| 32 | 
            +
                  context 'there is an ID on the model already', ->
         | 
| 33 | 
            +
                    # usually happens when fetching an existing model and not using StorageManager#loadModel
         | 
| 34 | 
            +
                    # new App.Models.Task(id: 5).fetch()
         | 
| 35 | 
            +
             | 
| 36 | 
            +
                    beforeEach ->
         | 
| 37 | 
            +
                      model.set('id', 1)
         | 
| 38 | 
            +
             | 
| 39 | 
            +
                    context 'model ID matches response ID', ->
         | 
| 40 | 
            +
                      it 'should add the parsing model to the storage manager', ->
         | 
| 41 | 
            +
                        response.tasks[1].id = 1
         | 
| 42 | 
            +
                        expect(base.data.storage('tasks').get(1)).toBeUndefined()
         | 
| 43 | 
            +
             | 
| 44 | 
            +
                        model.parse(response)
         | 
| 45 | 
            +
                        expect(base.data.storage('tasks').get(1)).not.toBeUndefined()
         | 
| 46 | 
            +
                        expect(base.data.storage('tasks').get(1)).toEqual model
         | 
| 47 | 
            +
                        expect(base.data.storage('tasks').get(1).attributes).toEqual response.tasks[1]
         | 
| 48 | 
            +
             | 
| 49 | 
            +
                    context 'model ID does not match response ID', ->
         | 
| 50 | 
            +
                      # this only happens when an association has the same brainstemKey as the parent record
         | 
| 51 | 
            +
                      # we want to add a new model to the storage manager and not worry about ourself
         | 
| 52 | 
            +
             | 
| 53 | 
            +
                      it 'should not add the parsing model to the storage manager', ->
         | 
| 54 | 
            +
                        response.tasks[1].id = 2345
         | 
| 55 | 
            +
                        expect(base.data.storage('tasks').get(1)).toBeUndefined()
         | 
| 56 | 
            +
             | 
| 57 | 
            +
                        model.parse(response)
         | 
| 58 | 
            +
                        expect(base.data.storage('tasks').get(1)).toBeUndefined()
         | 
| 59 | 
            +
                        expect(base.data.storage('tasks').get(2345)).not.toEqual model
         | 
| 60 | 
            +
             | 
| 61 | 
            +
                  context 'there is not an ID on the model instance already', ->
         | 
| 62 | 
            +
                    # usually happens when creating a new model:
         | 
| 63 | 
            +
                    # new App.Models.Task(title: 'test').save()
         | 
| 64 | 
            +
             | 
| 65 | 
            +
                    beforeEach ->
         | 
| 66 | 
            +
                      expect(model.id).toBeUndefined()
         | 
| 67 | 
            +
             | 
| 68 | 
            +
                    it 'should add the parsing model to the storage manager', ->
         | 
| 69 | 
            +
                      response.tasks[1].title = 'Hello'
         | 
| 70 | 
            +
                      expect(base.data.storage('tasks').get(1)).toBeUndefined()
         | 
| 71 | 
            +
             | 
| 72 | 
            +
                      model.parse(response)
         | 
| 73 | 
            +
                      expect(base.data.storage('tasks').get(1)).toEqual(model)
         | 
| 74 | 
            +
                      expect(base.data.storage('tasks').get(1).get('title')).toEqual('Hello')
         | 
| 75 | 
            +
             | 
| 31 76 | 
             
                it 'should work with an empty response', ->
         | 
| 32 77 | 
             
                  expect( -> model.parse(tasks: {}, results: [], count: 0)).not.toThrow()
         | 
| 33 78 |  | 
| @@ -74,33 +119,6 @@ describe 'Brainstem.Model', -> | |
| 74 119 | 
             
                    expect(base.data.storage('users').get(5).get('created_at')).toEqual(1361820357000)
         | 
| 75 120 | 
             
                    expect(base.data.storage('users').get(6).get('created_at')).toEqual(1359573957000)
         | 
| 76 121 |  | 
| 77 | 
            -
              describe 'setLoaded', ->
         | 
| 78 | 
            -
                it "should set the values of @loaded", ->
         | 
| 79 | 
            -
                  model.setLoaded true
         | 
| 80 | 
            -
                  expect(model.loaded).toEqual(true)
         | 
| 81 | 
            -
                  model.setLoaded false
         | 
| 82 | 
            -
                  expect(model.loaded).toEqual(false)
         | 
| 83 | 
            -
             | 
| 84 | 
            -
                it "triggers 'loaded' when becoming true", ->
         | 
| 85 | 
            -
                  spy = jasmine.createSpy()
         | 
| 86 | 
            -
                  model.bind "loaded", spy
         | 
| 87 | 
            -
                  model.setLoaded false
         | 
| 88 | 
            -
                  expect(spy).not.toHaveBeenCalled()
         | 
| 89 | 
            -
                  model.setLoaded true
         | 
| 90 | 
            -
                  expect(spy).toHaveBeenCalled()
         | 
| 91 | 
            -
             | 
| 92 | 
            -
                it "doesn't trigger loaded if trigger: false is provided", ->
         | 
| 93 | 
            -
                  spy = jasmine.createSpy()
         | 
| 94 | 
            -
                  model.bind "loaded", spy
         | 
| 95 | 
            -
                  model.setLoaded true, trigger: false
         | 
| 96 | 
            -
                  expect(spy).not.toHaveBeenCalled()
         | 
| 97 | 
            -
             | 
| 98 | 
            -
                it "returns self", ->
         | 
| 99 | 
            -
                  spy = jasmine.createSpy()
         | 
| 100 | 
            -
                  model.bind "loaded", spy
         | 
| 101 | 
            -
                  model.setLoaded true
         | 
| 102 | 
            -
                  expect(spy).toHaveBeenCalledWith(model)
         | 
| 103 | 
            -
             | 
| 104 122 | 
             
              describe 'associations', ->
         | 
| 105 123 | 
             
                describe 'associationDetails', ->
         | 
| 106 124 |  | 
| @@ -181,6 +199,28 @@ describe 'Brainstem.Model', -> | |
| 181 199 | 
             
                      expect(project.associationsAreLoaded(["time_entries"])).toBeTruthy()
         | 
| 182 200 | 
             
                      expect(project.associationsAreLoaded(["tasks"])).toBeFalsy()
         | 
| 183 201 |  | 
| 202 | 
            +
                  describe "when supplying associations that do not exist", ->
         | 
| 203 | 
            +
                    class TestClass extends Brainstem.Model
         | 
| 204 | 
            +
                      @associations:
         | 
| 205 | 
            +
                        user: "users"
         | 
| 206 | 
            +
             | 
| 207 | 
            +
                    testClass = null
         | 
| 208 | 
            +
             | 
| 209 | 
            +
                    beforeEach ->
         | 
| 210 | 
            +
                      testClass = new TestClass()
         | 
| 211 | 
            +
             | 
| 212 | 
            +
                    it "returns true when supplying only an association that does not exist", ->
         | 
| 213 | 
            +
                      expect(testClass.associationsAreLoaded(['foobar'])).toBe true
         | 
| 214 | 
            +
                      expect(testClass.associationsAreLoaded(['user'])).toBe false
         | 
| 215 | 
            +
             | 
| 216 | 
            +
                    it "returns false when supplying both a real association that is not loaded and an association that does not exist", ->
         | 
| 217 | 
            +
                      expect(testClass.associationsAreLoaded(['user', 'foobar'])).toBe false
         | 
| 218 | 
            +
             | 
| 219 | 
            +
                    it "returns true when supplying a real association that is loaded and an association that does not exist", ->
         | 
| 220 | 
            +
                      testClass.set('user_id', buildAndCacheUser().id)
         | 
| 221 | 
            +
                      expect(testClass.associationsAreLoaded(['user'])).toBe true
         | 
| 222 | 
            +
                      expect(testClass.associationsAreLoaded(['user', 'foobar'])).toBe true
         | 
| 223 | 
            +
             | 
| 184 224 | 
             
                describe "get", ->
         | 
| 185 225 | 
             
                  it "should delegate to Backbone.Model#get for anything that is not an association", ->
         | 
| 186 226 | 
             
                    timeEntry = new App.Models.TimeEntry(id: 5, project_id: 10, task_id: 2, title: "foo")
         | 
| @@ -1,22 +1,45 @@ | |
| 1 1 | 
             
            describe "Brainstem.Sync", ->
         | 
| 2 | 
            -
               | 
| 3 | 
            -
                ajaxSpy = null
         | 
| 2 | 
            +
              ajaxSpy = null
         | 
| 4 3 |  | 
| 5 | 
            -
             | 
| 6 | 
            -
             | 
| 4 | 
            +
              beforeEach ->
         | 
| 5 | 
            +
                ajaxSpy = spyOn($, 'ajax')
         | 
| 7 6 |  | 
| 7 | 
            +
              describe "updating models", ->
         | 
| 8 8 | 
             
                it "should use toServerJSON instead of toJSON", ->
         | 
| 9 9 | 
             
                  modelSpy = spyOn(Brainstem.Model.prototype, 'toServerJSON')
         | 
| 10 10 | 
             
                  model = buildTimeEntry()
         | 
| 11 11 | 
             
                  model.save()
         | 
| 12 12 | 
             
                  expect(modelSpy).toHaveBeenCalled()
         | 
| 13 13 |  | 
| 14 | 
            -
                it "should pass options. | 
| 14 | 
            +
                it "should pass options.include through the JSON", ->
         | 
| 15 15 | 
             
                  model = buildTimeEntry()
         | 
| 16 16 | 
             
                  model.save({}, include: 'creator')
         | 
| 17 17 | 
             
                  expect(ajaxSpy.mostRecentCall.args[0].data).toMatch(/"include":"creator"/)
         | 
| 18 18 |  | 
| 19 | 
            +
                it "should accept an array for options.include", ->
         | 
| 20 | 
            +
                  model = buildTimeEntry()
         | 
| 21 | 
            +
                  model.save({}, include: ['creator', 'story'])
         | 
| 22 | 
            +
                  expect(ajaxSpy.mostRecentCall.args[0].data).toMatch(/"include":"creator,story"/)
         | 
| 23 | 
            +
             | 
| 19 24 | 
             
                it "should setup param roots when models have a paramRoot set", ->
         | 
| 20 25 | 
             
                  model = buildTimeEntry()
         | 
| 21 26 | 
             
                  model.save({})
         | 
| 22 | 
            -
                  expect(ajaxSpy.mostRecentCall.args[0].data).toMatch(/"time_entry":{/)
         | 
| 27 | 
            +
                  expect(ajaxSpy.mostRecentCall.args[0].data).toMatch(/"time_entry":{/)
         | 
| 28 | 
            +
             | 
| 29 | 
            +
              describe 'error handler', ->
         | 
| 30 | 
            +
                it 'wraps the error handler in an errorInterceptor', ->
         | 
| 31 | 
            +
                  model = buildTimeEntry()
         | 
| 32 | 
            +
                  base.data.errorInterceptor = jasmine.createSpy('errorInterceptor')
         | 
| 33 | 
            +
             | 
| 34 | 
            +
                  model.save({})
         | 
| 35 | 
            +
                  ajaxSpy.mostRecentCall.args[0].error()
         | 
| 36 | 
            +
                  expect(base.data.errorInterceptor).toHaveBeenCalled()
         | 
| 37 | 
            +
             | 
| 38 | 
            +
                it 'only wraps the error handler if base.data.errorInterceptor is defined', ->
         | 
| 39 | 
            +
                  delete base.data.errorInterceptor
         | 
| 40 | 
            +
                  model = buildTimeEntry()
         | 
| 41 | 
            +
                  errorSpy = jasmine.createSpy('error spy')
         | 
| 42 | 
            +
             | 
| 43 | 
            +
                  model.save({}, error: errorSpy)
         | 
| 44 | 
            +
                  ajaxSpy.mostRecentCall.args[0].error()
         | 
| 45 | 
            +
                  expect(errorSpy).toHaveBeenCalled()
         |