├── .gitignore ├── Isolate ├── README.md ├── Rakefile ├── config.ru ├── lib ├── init.rb ├── isolate-3.1.0.pre.3 │ ├── .autotest │ ├── .rvmrc │ ├── CHANGELOG.rdoc │ ├── Isolate │ ├── Manifest.txt │ ├── README.rdoc │ ├── Rakefile │ ├── lib │ │ ├── hoe │ │ │ └── isolate.rb │ │ ├── isolate.rb │ │ └── isolate │ │ │ ├── completely.rb │ │ │ ├── entry.rb │ │ │ ├── events.rb │ │ │ ├── installer.rb │ │ │ ├── now.rb │ │ │ ├── rake.rb │ │ │ └── sandbox.rb │ └── test │ │ ├── fixtures │ │ ├── blort-0.0.gem │ │ ├── isolate.rb │ │ ├── override.rb │ │ ├── override.rb.local │ │ └── with-hoe │ │ │ └── specifications │ │ │ ├── hoe-2.3.3.gemspec │ │ │ ├── rake-0.8.7.gemspec │ │ │ └── rubyforge-1.0.4.gemspec │ │ ├── isolate │ │ └── test.rb │ │ ├── test_isolate.rb │ │ ├── test_isolate_entry.rb │ │ ├── test_isolate_events.rb │ │ └── test_isolate_sandbox.rb └── server.rb ├── pom.xml ├── public ├── albums.json ├── images │ ├── add.png │ ├── control_end.png │ ├── control_end_hover.png │ ├── control_pause.png │ ├── control_pause_hover.png │ ├── control_play.png │ ├── control_play_hover.png │ ├── control_start.png │ ├── control_start_hover.png │ ├── control_stop.png │ ├── control_stop_hover.png │ └── remove.png ├── index.html ├── js │ ├── Tunes.js │ └── vendor │ │ ├── angular-mocks.js │ │ ├── angular.js │ │ ├── backbone.js │ │ ├── jquery-1.5.1.min.js │ │ ├── modernizr-1.6.min.js │ │ └── underscore.js ├── music │ ├── blue.mp3 │ ├── jazz.mp3 │ ├── minimalish.mp3 │ └── slower.mp3 └── style │ ├── fancypants.css │ └── screen.css ├── server.js └── test-js ├── SpecRunner.html ├── lib ├── angular-mocks.js └── jasmine │ ├── MIT.LICENSE │ ├── jasmine-html.js │ ├── jasmine.css │ ├── jasmine.js │ ├── jasmine_favicon.png │ └── version.txt └── spec └── TunesSpec.js /.gitignore: -------------------------------------------------------------------------------- 1 | tmp/ 2 | -------------------------------------------------------------------------------- /Isolate: -------------------------------------------------------------------------------- 1 | 2 | gem "json_pure", "~> 1.5.3" 3 | gem "sinatra", "~> 1.2.3" 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Backbone Tunes: A backbone.js demo 2 | # Angular Tunes: An AngularJS demo 3 | 4 | - Displays a list of available albums and their tracks 5 | - Allows queueing albums for playback 6 | - Plays the queue, one track at a time 7 | 8 | Live Demo: 9 | 10 | 11 | ## Rewrite notes 12 | 13 | Have you seen any of the awesome [Peepcode screencasts](http://peepcode.com/)? They are well done. 14 | When I saw that they started [a series on Backbone.js](http://peepcode.com/products/backbone-ii), I 15 | went ahead and bought the available episodes and watched them. 16 | 17 | And then it happened... I realized that while the screencast was very well done, and the app was 18 | quite nice, the code could use several improvements. The biggest one being - this app could be much 19 | simpler if it was written in [AngularJS](http://angularjs.org). So I sat down and rewrote most of 20 | the code in 2-3 hours. 21 | 22 | I expected the rewrite to be simpler, but **I didn't expect to delete about 2/3 of the codebase** 23 | (see [diff]). 24 | 25 | I didn't spend too much time thinking about how the backbone code could be improved, so if you can 26 | write the app with backbone in a significantly better way, I'd be very happy to look over your fork! 27 | 28 | Anyway, as I was rewriting the app I came to these conclusions about the Backbone app and Backbone 29 | itself: 30 | 31 | - **There is just way too much wiring going on.** 32 | 33 | I think that this is an artifact of backbone being agnostic of templating and very heavily focused 34 | on the model. This means that the burden of keeping the view updated with using the right 35 | templates updated in the right places at the right time is up to the developer. 36 | 37 | If you look at the diff, you'll notice that I deleted all of the views and replaced them with 38 | nothing. This stuff is not necessary in Angular. Angular looks at the template and figures out 39 | how the view should be updated with what kind of dom manipulation, when, and with what data. 40 | 41 | - **Template fragmentation.** 42 | 43 | Templates in the original backbone app were split into several tiny fragments. Because of this a 44 | web designer would have a hard time figuring out how the templates end up being composed in the 45 | final view. To make matters even worse, the template fragments in `index.html` were not complete, 46 | since before combining all templates into the final view each of the fragment is wrapped into a 47 | dom element with some css class and this information is in the javascript code. 48 | 49 | Now you might think that this is not a big deal for some reason, but this kind of fragmentation 50 | results in creating a messy DOM tree with some non-obvious errors. For example in the original 51 | backbone app, `div` elements are being nested in the `ul` container, instead of proper use of 52 | `li`s. This kind of stuff is hard to spot with fragmented templates, unless you inspect the DOM 53 | in the browser. 54 | 55 | - **Models are unnecessary complicated.** 56 | 57 | Backbone is all about rich models and that's great, but I think that the original code was 58 | overdoing it. I'm not sure if that's because of backbone's deficiencies or because the code was 59 | not designed well. For instance, for the playlist, in the angular version I'm simply using a plain 60 | old javascript array, instead of a fancy object. Simple is better IMO because less code means 61 | less code to test and maintain! 62 | 63 | - **Dependencies, dependencies, dependencies.** 64 | 65 | Even a trivial app like this one, requires including underscore and jquery libraries to get stuff 66 | done. jQuery (minified) alone is much bigger (31KB) than backbone and the app code combined. 67 | AngularJS is just 25KB (min+gzip) and doesn't *require* jQuery or underscore. 68 | 69 | 70 | If you feel that any of these points are wrong, feel free to fork the code, improve the backbone 71 | version and prove your argument with a diff. 72 | 73 | 74 | ### Features and Fixes 75 | 76 | The original app contains several bugs and missing features that I fixed/added: 77 | 78 | - next/previous buttons don't throw exceptions when pressed while playlist is empty 79 | - pressing play when playlist is empty doesn't do anything 80 | - when playlist is emptied, play button doesn't start to play the last played song 81 | - when one song finishes playing, the next one in the playlist starts 82 | - the DOM contains fewer elements and I fixed issues like nesting `div`s inside of `ul`s 83 | - conditional css and Modernizr were removed since they are not being utilized by the app 84 | 85 | 86 | ## USAGE 87 | 88 | ### Run: 89 | 90 | rake server 91 | or 92 | 93 | node server.js 94 | 95 | Visit the webserver at: 96 | 97 | http://localhost:9292 98 | 99 | 100 | ### Test: 101 | 102 | open `test-js/SpecRunner.html` in your browser (using the file:// protocol) 103 | 104 | 105 | ## License 106 | 107 | All my code as well as AngularJS are licensed under the [MIT license]. The license of the original 108 | code from PeepCode is unknown, but I got their OK to fork and publish it: http://goo.gl/UB36U 109 | 110 | [diff]: https://github.com/angular/peepcode-tunes/commit/87dfa695d9981b1fc439c6cf4ed32f77970faf8f 111 | [MIT license]: https://github.com/angular/angular.js/blob/master/LICENSE 112 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require './lib/init' 2 | 3 | desc "Run the server" 4 | task :server do 5 | system "rackup config.ru" 6 | end -------------------------------------------------------------------------------- /config.ru: -------------------------------------------------------------------------------- 1 | 2 | require './lib/server' 3 | 4 | run Sinatra::Application 5 | -------------------------------------------------------------------------------- /lib/init.rb: -------------------------------------------------------------------------------- 1 | Dir['./lib/isolate*/lib'].each do |dir| 2 | $: << dir 3 | end 4 | 5 | require "rubygems" 6 | require "isolate/now" 7 | 8 | require "sinatra" 9 | -------------------------------------------------------------------------------- /lib/isolate-3.1.0.pre.3/.autotest: -------------------------------------------------------------------------------- 1 | require "autotest/restart" 2 | 3 | Autotest.add_hook :initialize do |at| 4 | at.testlib = "minitest/autorun" 5 | end 6 | -------------------------------------------------------------------------------- /lib/isolate-3.1.0.pre.3/.rvmrc: -------------------------------------------------------------------------------- 1 | rvm_gemset_create_on_use_flag=1 2 | rvm ruby-1.9.2@isolate 3 | -------------------------------------------------------------------------------- /lib/isolate-3.1.0.pre.3/CHANGELOG.rdoc: -------------------------------------------------------------------------------- 1 | === 3.0.0 / 2010-10-19 2 | 3 | * Remove deprecated Isolate.{gems,instance} methods. 4 | * Update RDoc location in the README. 5 | * Make isolated Hoe projects use :system => false by default. 6 | * Teach block file detection regex about Windows. 7 | 8 | === 2.1.2 / 2010-07-23 9 | 10 | * Teach `isolate:sh` how to work on Windows. 11 | 12 | === 2.1.1 / 2010-07-08 13 | 14 | * Fix $LOAD_PATH filtering bug when system is false. 15 | 16 | === 2.1.0 / 2010-07-01 17 | 18 | * Pass self to event hooks. Speculative coding FTL. 19 | * Load ~/.isolate/user.rb if it exists. 20 | 21 | === 2.0.2 / 2010-05-25 22 | 23 | * Provide reasonable stale output for missing deps. 24 | 25 | === 2.0.1 / 2010-05-24 26 | 27 | * Fine, fine. Re-enables support for Ruby 1.8.6. 28 | * Make deprecated Isolate.gems more compatible with 1.x. [Eric Wong] 29 | 30 | === 2.0.0 / 2010-05-10 31 | 32 | * Rewrite README and RDoc. 33 | * Deprecate `Isolate.gems` and `Isolate.instance`. 34 | * Add Isolate::Event hooks for better extension/integration. 35 | * Add ISOLATED env var when Isolate is activated. 36 | * Teach the Hoe plugin to recognize Isolate files. 37 | * Add `env` as an alias for `environment`. 38 | * Significantly refactor API and internals. 39 | * Add .local files for overriding global and gem args and options. 40 | * Segregate isolated gems by Ruby engine and version. 41 | * Make sure it's possible to install a local .gem file. 42 | * Make additive ENV changes idempotent. [Eric Wong] 43 | * Isolate is a module now, not a class. 44 | * Use tmp/isolate as the default, replacing tmp/gems. 45 | * Allow options changes in Isolate files or blocks. 46 | * Make entries additive. Multiple calls to Isolate#gem is fine. 47 | * Lock down required Ruby and RubyGems versions (1.8.7+, 1.3.6+). 48 | 49 | === 1.10.2 / 2010-04-24 50 | 51 | Add 'isolate/now' convenience require in prep. for 2.x. 52 | 53 | === 1.10.1 / 2010-04-23 54 | 55 | * Fix cleanup, it wasn't. 56 | 57 | === 1.10.0 / 2010-03-15 58 | 59 | * Speculative now! shortcut. 60 | 61 | === 1.9.3 / 2010-02-24 62 | 63 | * Passing :file => true to Isolate.gems will try +Isolate+ 64 | and config/isolate.rb. 65 | 66 | === 1.9.2 / 2010-02-17 67 | 68 | * Make it easier to break out the manifest to a separate file. 69 | 70 | === 1.9.1 / 2010-01-18 71 | 72 | * Append to sources on install, don't just replace 'em. 73 | 74 | === 1.9.0 / 2010-01-18 75 | 76 | * Allow isolation to be disabled. 77 | 78 | === 1.8.2 / 2010-01-13 79 | 80 | * Don't include Isolate as a dependency in .gems. 81 | * Tweak the README structure for clarity. 82 | 83 | === 1.8.1 / 2010-01-13 84 | 85 | * Allow isolate:dotgems to take an env. 86 | 87 | === 1.8.0 / 2010-01-05 88 | 89 | * Make build args forgive non-Array input. 90 | * OMG, heaven forbid I use math. 91 | * Remove deprecated Isolate.activate method. 92 | * Remove passthrough, that's what conditionals are for. 93 | 94 | === 1.7.1 / 2009-12-08 95 | 96 | * Move to 1-phase activation. Deprecate Isolate.activate. 97 | 98 | === 1.7.0 / 2009-12-07 99 | 100 | * Activate gems even in passthrough mode. 101 | * hoe/isolate cleans up by default. 102 | * Isolate#activate cleans up. Isolate.activate is a simple front-end. 103 | 104 | === 1.6.1 / 2009-10-04 105 | 106 | * Simplify subshells. 107 | * Squash warning. 108 | 109 | === 1.6.0 / 2009-10-03 110 | 111 | * Add Rake helpers. 112 | * Expose the bin path to subshells. 113 | 114 | === 1.5.1 / 2009-10-02 115 | 116 | * Fix passthrough for explicitly false conditions. 117 | 118 | === 1.5.0 / 2009-10-01 119 | 120 | * Implemented passthrough. 121 | 122 | === 1.4.0 / 2009-09-30 123 | 124 | * Added automatic cleanup. 125 | * Minor code refactoring. 126 | 127 | === 1.3.0 / 2009-09-23 128 | 129 | * Add support for Gem build args. See the README for details. 130 | 131 | === 1.2.1 / 2009-09-22 132 | 133 | * I am a moron. Made the Hoe plugin work again. 134 | * Be consistent about accessors vs ivars. [Review by Scott W] 135 | 136 | === 1.2.0 / 2009-09-22 137 | 138 | * Added a Hoe plugin. 139 | 140 | === 1.1.0 / 2009-09-22 141 | 142 | * Breaking change: Install by default. 143 | 144 | === 1.0.2 / 2009-09-21 145 | 146 | * Fix some README typos. 147 | * Add Entry#matches? to encapsulate activation and installation decisions. 148 | * Remove block form of Isolate#enable, it's unused. 149 | * Properly instance_eval Isolate#environment blocks. 150 | * Reset stubs properly after tests (Random test order FTFW). 151 | 152 | === 1.0.1 / 2009-09-21 153 | 154 | * Doco updates. [Review by Evan] 155 | * Don't modify Entry#options on install. [Review by Evan] 156 | 157 | === 1.0.0 / 2009-09-21 158 | 159 | * Birthday! 160 | -------------------------------------------------------------------------------- /lib/isolate-3.1.0.pre.3/Isolate: -------------------------------------------------------------------------------- 1 | env :development do 2 | gem "hoe-doofus", "1.0.0" 3 | gem "hoe-git", "1.3.0" 4 | gem "minitest", "1.7.2" 5 | end 6 | -------------------------------------------------------------------------------- /lib/isolate-3.1.0.pre.3/Manifest.txt: -------------------------------------------------------------------------------- 1 | .autotest 2 | .rvmrc 3 | CHANGELOG.rdoc 4 | Isolate 5 | Manifest.txt 6 | README.rdoc 7 | Rakefile 8 | lib/hoe/isolate.rb 9 | lib/isolate.rb 10 | lib/isolate/completely.rb 11 | lib/isolate/entry.rb 12 | lib/isolate/events.rb 13 | lib/isolate/installer.rb 14 | lib/isolate/now.rb 15 | lib/isolate/rake.rb 16 | lib/isolate/sandbox.rb 17 | test/fixtures/blort-0.0.gem 18 | test/fixtures/isolate.rb 19 | test/fixtures/override.rb 20 | test/fixtures/override.rb.local 21 | test/fixtures/with-hoe/specifications/hoe-2.3.3.gemspec 22 | test/fixtures/with-hoe/specifications/rake-0.8.7.gemspec 23 | test/fixtures/with-hoe/specifications/rubyforge-1.0.4.gemspec 24 | test/isolate/test.rb 25 | test/test_isolate.rb 26 | test/test_isolate_entry.rb 27 | test/test_isolate_events.rb 28 | test/test_isolate_sandbox.rb 29 | -------------------------------------------------------------------------------- /lib/isolate-3.1.0.pre.3/README.rdoc: -------------------------------------------------------------------------------- 1 | = Isolate 2 | 3 | * http://github.com/jbarnette/isolate 4 | 5 | == Description 6 | 7 | Isolate is a very simple RubyGems sandbox. It provides a way to 8 | express and automatically install your project's Gem dependencies. 9 | 10 | == Wha? 11 | 12 | When Isolate runs, it uses GEM_HOME, GEM_PATH, and a few other tricks 13 | to separate your code from the system's RubyGems configuration, 14 | leaving it free to run in blissful solitude. 15 | 16 | Isolate is very, very, very stupid simple. For a much more 17 | full-featured Gem bundler, check out Yehuda Katz and Carl Lerche's 18 | Bundler[http://github.com/carlhuda/bundler]: It does a lot of fancy 19 | AOT dependency resolution, supports non-gem (including git) resources, 20 | and is probably a better fit for you. 21 | 22 | == Requirements 23 | 24 | RubyGems 1.3.6 or better, Ruby 1.8.6 or better. 25 | 26 | == Getting Started 27 | 28 | === Rails 2 29 | 30 | In config/preinitializer.rb: 31 | 32 | require "rubygems" 33 | require "isolate/now" 34 | 35 | In Isolate: 36 | 37 | gem "rails", "2.3.5" 38 | gem "aasm", "2.0.0" 39 | 40 | env :development, :test do 41 | gem "sqlite3-ruby", "1.2.5" 42 | end 43 | 44 | env :production do 45 | gem "memcached", "0.19.2" 46 | end 47 | 48 | Try running rake environment. Before anything else happens, 49 | Isolate will make sure you have copies of every gem you need (extend 50 | the example above to cover all your dependencies). If they're already 51 | installed on your system Isolate will use them, otherwise a private 52 | copy will be installed under tmp/isolate. 53 | 54 | === Rails 3 55 | 56 | In config/boot.rb: 57 | 58 | require "rubygems" 59 | require "isolate/now" 60 | 61 | Construct your Isolate file as above. Be sure to remove any 62 | references to Bundler.setup and Bundler.require from 63 | config/boot.rb and config/application.rb. 64 | 65 | === Sinatra, Rack, and Anything Else 66 | 67 | There's nothing special about Rails, it's just an easy first 68 | example. You can use Isolate with any library or framework by simply 69 | putting an Isolate file in the root of your project and 70 | requiring isolate/now as early as possible in the startup 71 | process. 72 | 73 | When you're starting up, Isolate tries to determine its environment by 74 | looking at the ISOLATE_ENV, RACK_ENV, and 75 | RAILS_ENV env vars. If none are set, it defaults to 76 | development. 77 | 78 | === Library Development 79 | 80 | If you're using Hoe[http://blog.zenspider.com/hoe] to manage your 81 | library, you can use Isolate's Hoe plugin to automatically install 82 | your lib's development, runtime, and test dependencies without 83 | polluting your system RubyGems, and run your tests/specs in total 84 | isolation. 85 | 86 | Assuming you have a recent Hoe and isolate's installed, it's as simple 87 | as putting: 88 | 89 | Hoe.plugin :isolate 90 | 91 | before the Hoe.spec call in your Rakefile. 92 | 93 | If you're not using Hoe, you can use an Isolate.now! block at 94 | the top of your Rakefile. See the RDoc for details. 95 | 96 | == Rake 97 | 98 | Isolate provides a few useful Rake tasks. If you're requiring 99 | isolate/now, you'll get them automatically when you run 100 | Rake. If not, you can include them by requiring isolate/rake. 101 | 102 | === isolate:env 103 | 104 | This task shows you the current Isolate settings and gems. 105 | 106 | $ rake isolate:env 107 | 108 | path: tmp/isolate/ruby-1.8 109 | env: development 110 | files: Isolate 111 | 112 | cleanup? true 113 | enabled? true 114 | install? true 115 | multiruby? true 116 | system? true 117 | verbose? true 118 | 119 | [all environments] 120 | gem rails, = 2.3.5 121 | gem aasm, = 2.0.0 122 | 123 | [development, test] 124 | gem sqlite3-ruby, = 1.2.5 125 | 126 | [production] 127 | gem memcached, = 0.19.2 128 | 129 | === isolate:sh 130 | 131 | This task allows you to run a subshell or a command in the isolated 132 | environment, making any command-line tools available on your 133 | PATH. 134 | 135 | # run a single command in an isolated subshell 136 | $ rake isolate:sh['gem list'] 137 | 138 | # run a new isolated subshell 139 | $ rake isolate:sh 140 | 141 | === isolate:stale 142 | 143 | This task lists gems that have a more recent released version than the 144 | one you're using. 145 | 146 | $ rake isolate:stale 147 | aasm (2.0.0 < 2.1.5) 148 | 149 | == Further Reading 150 | 151 | require "isolate/now" is sugar for Isolate.now!, 152 | which creates, configures, and activates a singleton version of 153 | Isolate's sandbox. Isolate.now! takes a few useful options, 154 | and lets you define an entire environment inline without using an 155 | external file. 156 | 157 | For detailed information on Isolate.now! and the rest of the 158 | public API, please see the RDoc. 159 | 160 | == Not Gonna Happen 161 | 162 | * Dependency resolution. If this ever becomes a serious problem for 163 | us, we'll likely push hard to solve it in RubyGems proper. If you 164 | run into these sorts of problems a lot, use Bundler. Or less 165 | libraries. 166 | 167 | * Autorequire. Unlike config.gems or other solutions, Isolate 168 | expects you to be a good little Rubyist and manually 169 | require the libraries you use. 170 | 171 | * Support for Git or other SCMs. You're welcome to write an extension 172 | that supports 'em, but Isolate itself is focused on packaged, 173 | released gems. 174 | 175 | == Installation 176 | 177 | $ gem install isolate 178 | 179 | == Meta 180 | 181 | RDoc:: http://rubydoc.info/gems/isolate/frames 182 | Bugs:: http://github.com/jbarnette/isolate/issues 183 | IRC:: #isolate on Freenode 184 | Mailing List:: isolate@librelist.com 185 | 186 | == License 187 | 188 | Copyright 2009-2010 John Barnette, et al. (code@jbarnette.com) 189 | 190 | Permission is hereby granted, free of charge, to any person obtaining 191 | a copy of this software and associated documentation files (the 192 | 'Software'), to deal in the Software without restriction, including 193 | without limitation the rights to use, copy, modify, merge, publish, 194 | distribute, sublicense, and/or sell copies of the Software, and to 195 | permit persons to whom the Software is furnished to do so, subject to 196 | the following conditions: 197 | 198 | The above copyright notice and this permission notice shall be 199 | included in all copies or substantial portions of the Software. 200 | 201 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 202 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 203 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 204 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 205 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 206 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 207 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 208 | -------------------------------------------------------------------------------- /lib/isolate-3.1.0.pre.3/Rakefile: -------------------------------------------------------------------------------- 1 | $:.unshift "./lib" 2 | require "isolate/now" 3 | 4 | require "hoe" 5 | 6 | Hoe.plugins.delete :rubyforge 7 | Hoe.plugin :doofus, :git, :isolate 8 | 9 | Hoe.spec "isolate" do 10 | developer "John Barnette", "code@jbarnette.com" 11 | developer "Ryan Davis", "ryand-ruby@zenspider.com" 12 | 13 | require_ruby_version ">= 1.8.6" 14 | require_rubygems_version ">= 1.3.6" 15 | 16 | self.extra_rdoc_files = Dir["*.rdoc"] 17 | self.history_file = "CHANGELOG.rdoc" 18 | self.readme_file = "README.rdoc" 19 | self.testlib = :minitest 20 | end 21 | -------------------------------------------------------------------------------- /lib/isolate-3.1.0.pre.3/lib/hoe/isolate.rb: -------------------------------------------------------------------------------- 1 | require "rubygems" 2 | require "isolate" 3 | 4 | class Hoe # :nodoc: 5 | 6 | # This module is a Hoe plugin. You can set its attributes in your 7 | # Rakefile's Hoe spec, like this: 8 | # 9 | # Hoe.plugin :isolate 10 | # 11 | # Hoe.spec "myproj" do 12 | # self.isolate_dir = "tmp/isolated" 13 | # end 14 | # 15 | # NOTE! The Isolate plugin is a little bit special: It messes with 16 | # the plugin ordering to make sure that it comes before everything 17 | # else. 18 | 19 | module Isolate 20 | 21 | # Where should Isolate, um, isolate? [default: "tmp/isolate"] 22 | # FIX: consider removing this and allowing +isolate_options+ instead. 23 | 24 | attr_accessor :isolate_dir 25 | 26 | def initialize_isolate 27 | # Tee hee! Move ourselves to the front to beat out :test. 28 | Hoe.plugins.unshift Hoe.plugins.delete(:isolate) 29 | 30 | self.isolate_dir ||= "tmp/isolate" 31 | @sandbox = ::Isolate::Sandbox.new 32 | 33 | @sandbox.entries.each do |entry| 34 | dep = [entry.name, *entry.requirement.as_list] 35 | 36 | if entry.environments.include? "development" 37 | extra_dev_deps << dep 38 | elsif entry.environments.empty? 39 | extra_deps << dep 40 | end 41 | end 42 | end 43 | 44 | def define_isolate_tasks # HACK 45 | 46 | # reset, now that they've had a chance to change it 47 | @sandbox.options :path => isolate_dir, :system => false 48 | 49 | # allows traditional extra{_dev}_deps calls to override 50 | (self.extra_deps + self.extra_dev_deps).each do |name, version| 51 | @sandbox.gem name, *Array(version) 52 | end 53 | 54 | @sandbox.activate 55 | end 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /lib/isolate-3.1.0.pre.3/lib/isolate.rb: -------------------------------------------------------------------------------- 1 | require "isolate/sandbox" 2 | 3 | # Restricts +GEM_PATH+ and +GEM_HOME+ and provides a DSL for 4 | # expressing your code's runtime Gem dependencies. See README.rdoc for 5 | # rationale, limitations, and examples. 6 | 7 | module Isolate 8 | 9 | # Duh. 10 | 11 | VERSION = "3.1.0.pre.3" 12 | 13 | # Disable Isolate. If a block is provided, isolation will be 14 | # disabled for the scope of the block. 15 | 16 | def self.disable &block 17 | sandbox.disable(&block) 18 | end 19 | 20 | # What environment should be isolated? Consults environment 21 | # variables ISOLATE_ENV, RAILS_ENV, and 22 | # RACK_ENV. Defaults to development"/tt> if none are 23 | # set. 24 | 25 | def self.env 26 | ENV["ISOLATE_ENV"] || ENV["RAILS_ENV"] || ENV["RACK_ENV"] || "development" 27 | end 28 | 29 | @@sandbox = nil 30 | 31 | # A singleton instance of Isolate::Sandbox. 32 | 33 | def self.sandbox 34 | @@sandbox 35 | end 36 | 37 | # Declare an isolated RubyGems environment, installed in +path+. Any 38 | # block given will be instance_evaled, see 39 | # Isolate::Sandbox#gem and Isolate::Sandbox#environment for the sort 40 | # of stuff you can do. 41 | # 42 | # Valid options: 43 | # 44 | # :cleanup:: Should obsolete gems be removed? Default is +true+. 45 | # 46 | # :file:: Specify an Isolate file to +instance_eval+. Default is 47 | # Isolate or config/isolate.rb, whichever 48 | # is found first. Passing false disables file 49 | # loading. 50 | # 51 | # :install:: Should missing gems be installed? Default is +true+. 52 | # 53 | # :multiruby:: Should Isolate assume that multiple Ruby versions 54 | # will be used simultaneously? If so, gems will be 55 | # segregated by Ruby version. Default is +true+. 56 | # 57 | # :path:: Where should isolated gems be kept? Default is 58 | # tmp/isolate", and a Ruby version specifier suffix 59 | # will be added if :multiruby is +true+. 60 | # 61 | # :system:: Should system gems be allowed to satisfy dependencies? 62 | # Default is +true+. 63 | # 64 | # :verbose:: Should Isolate be chatty during installs and nukes? 65 | # Default is +true+. 66 | 67 | def self.now! options = {}, &block 68 | @@sandbox = Isolate::Sandbox.new options, &block 69 | @@sandbox.activate 70 | end 71 | 72 | # Poke RubyGems, since we've probably monkeyed with a bunch of paths 73 | # and suchlike. Deprecated and scheduled for removal in v4.0.0. 74 | 75 | def self.refresh # :nodoc: 76 | $stderr.puts "Deprecated, removal in v4.0.0. Use Gem.refresh." 77 | Gem.refresh 78 | end 79 | end 80 | -------------------------------------------------------------------------------- /lib/isolate-3.1.0.pre.3/lib/isolate/completely.rb: -------------------------------------------------------------------------------- 1 | require "isolate" 2 | require "isolate/rake" if defined?(Rake) 3 | 4 | Isolate.now! :system => false 5 | -------------------------------------------------------------------------------- /lib/isolate-3.1.0.pre.3/lib/isolate/entry.rb: -------------------------------------------------------------------------------- 1 | require "isolate/events" 2 | require "isolate/installer" 3 | require "rubygems" 4 | require "rubygems/command" 5 | require "rubygems/dependency_installer" 6 | require "rubygems/requirement" 7 | require "rubygems/version" 8 | 9 | module Isolate 10 | 11 | # An isolated Gem, with requirement, environment restrictions, and 12 | # installation options. Generally intended for internal use. This 13 | # class exposes lifecycle events for extension, see Isolate::Events 14 | # for more information. 15 | 16 | class Entry 17 | include Events 18 | 19 | # Which environments does this entry care about? Generally an 20 | # Array of Strings. An empty array means "all", not "none". 21 | 22 | attr_reader :environments 23 | 24 | # What's the name of this entry? Generally the name of a gem. 25 | 26 | attr_reader :name 27 | 28 | # Extra information or hints for installation. See +initialize+ 29 | # for well-known keys. 30 | 31 | attr_reader :options 32 | 33 | # What version of this entry is required? Expressed as a 34 | # Gem::Requirement, which see. 35 | 36 | attr_reader :requirement 37 | 38 | # Create a new entry. Takes +sandbox+ (currently an instance of 39 | # Isolate::Sandbox), +name+ (as above), and any number of optional 40 | # version requirements (generally strings). Options can be passed 41 | # as a trailing hash. Well-known keys: 42 | # 43 | # :args:: Command-line build arguments. Passed to the gem at 44 | # installation time. 45 | # 46 | # :source:: An alternative RubyGems source for this gem. 47 | 48 | def initialize sandbox, name, *requirements 49 | @environments = [] 50 | @file = nil 51 | @name = name 52 | @options = {} 53 | @requirement = Gem::Requirement.default 54 | @sandbox = sandbox 55 | 56 | if /\.gem$/ =~ @name && File.file?(@name) 57 | @file = File.expand_path @name 58 | 59 | @name = File.basename(@file, ".gem"). 60 | gsub(/-#{Gem::Version::VERSION_PATTERN}$/, "") 61 | end 62 | 63 | update(*requirements) 64 | end 65 | 66 | # Activate this entry. Fires :activating and 67 | # :activated. 68 | 69 | def activate 70 | fire :activating, :activated do 71 | Gem.activate name, *requirement.as_list 72 | end 73 | end 74 | 75 | # Install this entry in the sandbox. Fires :installing 76 | # and :installed. 77 | 78 | def install 79 | old = Gem.sources.dup 80 | 81 | begin 82 | fire :installing, :installed do 83 | installer = Isolate::Installer.new @sandbox 84 | 85 | Gem.sources += Array(options[:source]) if options[:source] 86 | Gem::Command.build_args = Array(options[:args]) if options[:args] 87 | 88 | installer.install @file || name, requirement 89 | end 90 | ensure 91 | Gem.sources = old 92 | Gem::Command.build_args = nil 93 | end 94 | end 95 | 96 | # Is this entry interested in +environment+? 97 | 98 | def matches? environment 99 | environments.empty? || environments.include?(environment) 100 | end 101 | 102 | # Is this entry satisfied by +spec+ (generally a 103 | # Gem::Specification)? 104 | 105 | def matches_spec? spec 106 | name == spec.name and requirement.satisfied_by? spec.version 107 | end 108 | 109 | # The Gem::Specification for this entry. 110 | 111 | def specification 112 | Gem.source_index.find_name(name, requirement).first 113 | end 114 | 115 | # Updates this entry's environments, options, and 116 | # requirement. Environments and options are merged, requirement is 117 | # replaced. Fires :updating and :updated. 118 | 119 | def update *reqs 120 | fire :updating, :updated do 121 | @environments |= @sandbox.environments 122 | @options.merge! reqs.pop if Hash === reqs.last 123 | @requirement = Gem::Requirement.new reqs unless reqs.empty? 124 | end 125 | 126 | self 127 | end 128 | end 129 | end 130 | -------------------------------------------------------------------------------- /lib/isolate-3.1.0.pre.3/lib/isolate/events.rb: -------------------------------------------------------------------------------- 1 | module Isolate 2 | 3 | # A simple way to watch and extend the Isolate lifecycle. 4 | # 5 | # Isolate::Events.watch Isolate::Sandbox, :initialized do |sandbox| 6 | # puts "A sandbox just got initialized: #{sandbox}" 7 | # end 8 | # 9 | # Read the source for Isolate::Sandbox and Isolate::Entry to see 10 | # what sort of events are fired. 11 | 12 | module Events 13 | 14 | # Watch for an event called +name+ from an instance of 15 | # +klass+. +block+ will be called when the event occurs. Block 16 | # args vary by event, but usually an instance of the relevant 17 | # class is passed. 18 | 19 | def self.watch klass, name, &block 20 | watchers[[klass, name]] << block 21 | end 22 | 23 | def self.fire klass, name, *args #:nodoc: 24 | watchers[[klass, name]].each do |block| 25 | block[*args] 26 | end 27 | end 28 | 29 | def self.watchers #:nodoc: 30 | @watchers ||= Hash.new { |h, k| h[k] = [] } 31 | end 32 | 33 | def fire name, after = nil, *args, &block #:nodoc: 34 | Isolate::Events.fire self.class, name, self, *args 35 | 36 | if after && block_given? 37 | yield self 38 | Isolate::Events.fire self.class, after, *args 39 | end 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /lib/isolate-3.1.0.pre.3/lib/isolate/installer.rb: -------------------------------------------------------------------------------- 1 | require "rubygems/dependency_installer" 2 | 3 | module Isolate 4 | class Installer < Gem::DependencyInstaller 5 | def initialize sandbox 6 | super :development => false, 7 | :generate_rdoc => false, 8 | :generate_ri => false, 9 | :install_dir => sandbox.path 10 | 11 | # reset super's use of sandbox.path exclusively 12 | @source_index = Gem.source_index 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/isolate-3.1.0.pre.3/lib/isolate/now.rb: -------------------------------------------------------------------------------- 1 | require "isolate" 2 | require "isolate/rake" if defined?(Rake) 3 | 4 | Isolate.now! 5 | -------------------------------------------------------------------------------- /lib/isolate-3.1.0.pre.3/lib/isolate/rake.rb: -------------------------------------------------------------------------------- 1 | namespace :isolate do 2 | desc "Show current isolated environment." 3 | task :env do 4 | require "pathname" 5 | 6 | sandbox = Isolate.sandbox 7 | here = Pathname Dir.pwd 8 | path = Pathname(sandbox.path).relative_path_from here 9 | files = sandbox.files.map { |f| Pathname(f) } 10 | 11 | puts 12 | puts " path: #{path}" 13 | puts " env: #{Isolate.env}" 14 | 15 | files.map! { |f| f.absolute? ? f.relative_path_from(here) : f } 16 | puts " files: #{files.join ', '}" 17 | puts 18 | 19 | %w(cleanup? enabled? install? multiruby? system? verbose?).each do |flag| 20 | printf "%10s %s\n", flag, sandbox.send(flag) 21 | end 22 | 23 | grouped = Hash.new { |h, k| h[k] = [] } 24 | sandbox.entries.each { |e| grouped[e.environments] << e } 25 | 26 | puts 27 | 28 | grouped.keys.sort.each do |envs| 29 | title = "all environments" if envs.empty? 30 | title ||= envs.join ", " 31 | 32 | puts "[#{title}]" 33 | 34 | grouped[envs].each do |e| 35 | gem = "gem #{e.name}, #{e.requirement}" 36 | gem << ", #{e.options.inspect}" unless e.options.empty? 37 | puts gem 38 | end 39 | 40 | puts 41 | end 42 | end 43 | 44 | desc "Run an isolated command or subshell." 45 | task :sh, [:command] do |t, args| 46 | exec args.command || ENV["SHELL"] || ENV["COMSPEC"] 47 | end 48 | 49 | desc "Which isolated gems have updates available?" 50 | task :stale do 51 | require "rubygems/source_index" 52 | require "rubygems/spec_fetcher" 53 | 54 | index = Gem::SourceIndex.new 55 | outdated = [] 56 | 57 | Isolate.sandbox.entries.each do |entry| 58 | if entry.specification 59 | index.add_spec entry.specification 60 | else 61 | outdated << entry 62 | end 63 | end 64 | 65 | index.outdated.each do |name| 66 | outdated << Isolate.sandbox.entries.find { |e| e.name == name } 67 | end 68 | 69 | outdated.sort_by { |e| e.name }.each do |entry| 70 | local = entry.specification ? entry.specification.version : "0" 71 | dep = Gem::Dependency.new entry.name, ">= #{local}" 72 | remotes = Gem::SpecFetcher.fetcher.fetch dep 73 | remote = remotes.last.first.version 74 | 75 | puts "#{entry.name} (#{local} < #{remote})" 76 | end 77 | end 78 | end 79 | -------------------------------------------------------------------------------- /lib/isolate-3.1.0.pre.3/lib/isolate/sandbox.rb: -------------------------------------------------------------------------------- 1 | require "fileutils" 2 | require "isolate/entry" 3 | require "isolate/events" 4 | require "rbconfig" 5 | require "rubygems/defaults" 6 | require "rubygems/uninstaller" 7 | 8 | module Isolate 9 | 10 | # An isolated environment. This class exposes lifecycle events for 11 | # extension, see Isolate::Events for more information. 12 | 13 | class Sandbox 14 | include Events 15 | 16 | attr_reader :entries # :nodoc: 17 | attr_reader :environments # :nodoc: 18 | attr_reader :files # :nodoc: 19 | 20 | # Create a new Isolate::Sandbox instance. See Isolate.now! for the 21 | # most common use of the API. You probably don't want to use this 22 | # constructor directly. Fires :initializing and 23 | # :initialized. 24 | 25 | def initialize options = {}, &block 26 | @enabled = false 27 | @entries = [] 28 | @environments = [] 29 | @files = [] 30 | @options = options 31 | 32 | fire :initializing 33 | 34 | user = File.expand_path "~/.isolate/user.rb" 35 | load user if File.exist? user 36 | 37 | file, local = nil 38 | 39 | unless FalseClass === options[:file] 40 | file = options[:file] || Dir["{Isolate,config/isolate.rb}"].first 41 | local = "#{file}.local" if file 42 | end 43 | 44 | load file if file 45 | 46 | if block_given? 47 | /\@(.+?):\d+/ =~ block.to_s 48 | files << ($1 || "inline block") 49 | instance_eval(&block) 50 | end 51 | 52 | load local if local && File.exist?(local) 53 | fire :initialized 54 | end 55 | 56 | # Activate this set of isolated entries, respecting an optional 57 | # +environment+. Points RubyGems to a separate repository, messes 58 | # with paths, auto-installs gems (if necessary), activates 59 | # everything, and removes any superfluous gem (again, if 60 | # necessary). If +environment+ isn't specified, +ISOLATE_ENV+, 61 | # +RAILS_ENV+, and +RACK_ENV+ are checked before falling back to 62 | # "development". Fires :activating and 63 | # :activated. 64 | 65 | def activate environment = nil 66 | enable unless enabled? 67 | fire :activating 68 | 69 | env = (environment || Isolate.env).to_s 70 | 71 | install env if install? 72 | 73 | entries.each do |e| 74 | e.activate if e.matches? env 75 | end 76 | 77 | cleanup if cleanup? 78 | fire :activated 79 | 80 | self 81 | end 82 | 83 | def cleanup # :nodoc: 84 | fire :cleaning 85 | 86 | installed = index.gems.values.sort 87 | legit = legitimize! 88 | extra = installed - legit 89 | 90 | unless extra.empty? 91 | padding = Math.log10(extra.size).to_i + 1 92 | format = "[%0#{padding}d/%s] Nuking %s." 93 | 94 | extra.each_with_index do |e, i| 95 | log format % [i + 1, extra.size, e.full_name] 96 | 97 | Gem::DefaultUserInteraction.use_ui Gem::SilentUI.new do 98 | Gem::Uninstaller.new(e.name, 99 | :version => e.version, 100 | :ignore => true, 101 | :executables => true, 102 | :install_dir => path).uninstall 103 | end 104 | end 105 | end 106 | 107 | fire :cleaned 108 | end 109 | 110 | def cleanup? 111 | install? and @options.fetch(:cleanup, true) 112 | end 113 | 114 | def disable &block 115 | return self if not enabled? 116 | fire :disabling 117 | 118 | ENV.replace @old_env 119 | 120 | @enabled = false 121 | 122 | Gem.clear_paths 123 | Gem.refresh 124 | 125 | fire :disabled 126 | 127 | begin; return yield ensure enable end if block_given? 128 | 129 | self 130 | end 131 | 132 | def enable # :nodoc: 133 | return self if enabled? 134 | fire :enabling 135 | 136 | @old_env = ENV.to_hash 137 | 138 | unless system? 139 | 140 | # HACK: Gotta keep isolate explicitly in the LOAD_PATH in 141 | # subshells, and the only way I can think of to do that is by 142 | # abusing RUBYOPT. 143 | 144 | lib = File.expand_path "../..", __FILE__ 145 | dirname = Regexp.escape lib 146 | 147 | unless ENV["RUBYOPT"] =~ /\s+-I\s*#{lib}\b/ 148 | ENV["RUBYOPT"] = "#{ENV['RUBYOPT']} -I#{lib}" 149 | end 150 | end 151 | 152 | bin = File.join path, "bin" 153 | 154 | unless ENV["PATH"].split(File::PATH_SEPARATOR).include? bin 155 | ENV["PATH"] = [bin, ENV["PATH"]].join File::PATH_SEPARATOR 156 | end 157 | 158 | paths = (ENV["GEM_PATH"] || "").split File::PATH_SEPARATOR 159 | paths.push Gem.dir 160 | 161 | paths.clear unless system? 162 | paths.push path 163 | 164 | Gem.clear_paths 165 | 166 | ENV["GEM_HOME"] = path 167 | ENV["GEM_PATH"] = paths.uniq.join File::PATH_SEPARATOR 168 | ENV["ISOLATED"] = path 169 | 170 | Gem.refresh 171 | 172 | @enabled = true 173 | fire :enabled 174 | 175 | self 176 | end 177 | 178 | def enabled? 179 | @enabled 180 | end 181 | 182 | # Restricts +gem+ calls inside +block+ to a set of +environments+. 183 | 184 | def environment *environments, &block 185 | old = @environments 186 | @environments = @environments.dup.concat environments.map { |e| e.to_s } 187 | 188 | instance_eval(&block) 189 | ensure 190 | @environments = old 191 | end 192 | 193 | alias_method :env, :environment 194 | 195 | # Express a gem dependency. Works pretty much like RubyGems' +gem+ 196 | # method, but respects +environment+ and doesn't activate 'til 197 | # later. 198 | 199 | def gem name, *requirements 200 | entry = entries.find { |e| e.name == name } 201 | return entry.update(*requirements) if entry 202 | 203 | entries << entry = Entry.new(self, name, *requirements) 204 | entry 205 | end 206 | 207 | # A source index representing only isolated gems. 208 | 209 | def index 210 | @index ||= Gem::SourceIndex.from_gems_in File.join(path, "specifications") 211 | end 212 | 213 | def install environment # :nodoc: 214 | fire :installing 215 | 216 | installable = entries.select do |e| 217 | !Gem.available?(e.name, *e.requirement.as_list) && 218 | e.matches?(environment) 219 | end 220 | 221 | unless installable.empty? 222 | padding = Math.log10(installable.size).to_i + 1 223 | format = "[%0#{padding}d/%s] Isolating %s (%s)." 224 | 225 | FileUtils.mkdir_p path 226 | 227 | installable.each_with_index do |entry, i| 228 | log format % [i + 1, installable.size, entry.name, entry.requirement] 229 | entry.install 230 | 231 | index.refresh! 232 | Gem.source_index.refresh! 233 | end 234 | end 235 | 236 | fire :installed 237 | 238 | self 239 | end 240 | 241 | def install? # :nodoc: 242 | @options.fetch :install, true 243 | end 244 | 245 | def load file # :nodoc: 246 | files << file 247 | instance_eval IO.read(file), file, 1 248 | end 249 | 250 | def log s # :nodoc: 251 | $stderr.puts s if verbose? 252 | end 253 | 254 | 255 | def multiruby? 256 | @options.fetch :multiruby, true 257 | end 258 | 259 | def options options = nil 260 | @options.merge! options if options 261 | @options 262 | end 263 | 264 | def path 265 | base = @options.fetch :path, "tmp/isolate" 266 | 267 | unless @options.key?(:multiruby) && @options[:multiruby] == false 268 | suffix = "#{Gem.ruby_engine}-#{RbConfig::CONFIG['ruby_version']}" 269 | base = File.join(base, suffix) unless base =~ /#{suffix}/ 270 | end 271 | 272 | File.expand_path base 273 | end 274 | 275 | def system? 276 | @options.fetch :system, true 277 | end 278 | 279 | def verbose? 280 | @options.fetch :verbose, true 281 | end 282 | 283 | private 284 | 285 | # Returns a list of Gem::Specification instances that 1. exist in 286 | # the isolated gem path, and 2. are allowed to be there. Used in 287 | # cleanup. It's only an external method 'cause recursion is 288 | # easier. 289 | 290 | def legitimize! deps = entries 291 | specs = [] 292 | 293 | deps.flatten.each do |dep| 294 | spec = index.find_name(dep.name, dep.requirement).last 295 | 296 | if spec 297 | specs.concat legitimize!(spec.runtime_dependencies) 298 | specs << spec 299 | end 300 | end 301 | 302 | specs.uniq 303 | end 304 | end 305 | end 306 | -------------------------------------------------------------------------------- /lib/isolate-3.1.0.pre.3/test/fixtures/blort-0.0.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular/peepcode-tunes/cc3cd4a44b48e34afe9f34d60810be95716fd589/lib/isolate-3.1.0.pre.3/test/fixtures/blort-0.0.gem -------------------------------------------------------------------------------- /lib/isolate-3.1.0.pre.3/test/fixtures/isolate.rb: -------------------------------------------------------------------------------- 1 | gem "hoe" 2 | -------------------------------------------------------------------------------- /lib/isolate-3.1.0.pre.3/test/fixtures/override.rb: -------------------------------------------------------------------------------- 1 | gem "monkey", :args => "--threaten" 2 | -------------------------------------------------------------------------------- /lib/isolate-3.1.0.pre.3/test/fixtures/override.rb.local: -------------------------------------------------------------------------------- 1 | environment :bar do 2 | gem "monkey", :args => "--asplode" 3 | end 4 | -------------------------------------------------------------------------------- /lib/isolate-3.1.0.pre.3/test/fixtures/with-hoe/specifications/hoe-2.3.3.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | 3 | Gem::Specification.new do |s| 4 | s.name = %q{hoe} 5 | s.version = "2.3.3" 6 | 7 | s.required_rubygems_version = Gem::Requirement.new(">= 1.3.1") if s.respond_to? :required_rubygems_version= 8 | s.authors = ["Ryan Davis"] 9 | s.date = %q{2009-08-07} 10 | s.default_executable = %q{sow} 11 | s.description = %q{Hoe is a rake/rubygems helper for project Rakefiles. It helps generate 12 | rubygems and includes a dynamic plug-in system allowing for easy 13 | extensibility. Hoe ships with plug-ins for all your usual project 14 | tasks including rdoc generation, testing, packaging, and deployment. 15 | 16 | Plug-ins Provided: 17 | 18 | * Hoe::Clean 19 | * Hoe::Debug 20 | * Hoe::Deps 21 | * Hoe::Flay 22 | * Hoe::Flog 23 | * Hoe::Inline 24 | * Hoe::Package 25 | * Hoe::Publish 26 | * Hoe::RCov 27 | * Hoe::Signing 28 | * Hoe::Test 29 | 30 | See class rdoc for help. Hint: ri Hoe} 31 | s.email = ["ryand-ruby@zenspider.com"] 32 | s.executables = ["sow"] 33 | s.extra_rdoc_files = ["History.txt", "Manifest.txt", "README.txt"] 34 | s.files = ["History.txt", "Manifest.txt", "README.txt", "Rakefile", "bin/sow", "lib/hoe.rb", "lib/hoe/clean.rb", "lib/hoe/debug.rb", "lib/hoe/deps.rb", "lib/hoe/flay.rb", "lib/hoe/flog.rb", "lib/hoe/inline.rb", "lib/hoe/package.rb", "lib/hoe/publish.rb", "lib/hoe/rake.rb", "lib/hoe/rcov.rb", "lib/hoe/signing.rb", "lib/hoe/test.rb", "template/.autotest.erb", "template/History.txt.erb", "template/Manifest.txt.erb", "template/README.txt.erb", "template/Rakefile.erb", "template/bin/file_name.erb", "template/lib/file_name.rb.erb", "template/test/test_file_name.rb.erb", "test/test_hoe.rb"] 35 | s.homepage = %q{http://rubyforge.org/projects/seattlerb/} 36 | s.rdoc_options = ["--main", "README.txt"] 37 | s.require_paths = ["lib"] 38 | s.rubyforge_project = %q{seattlerb} 39 | s.rubygems_version = %q{1.3.5} 40 | s.summary = %q{Hoe is a rake/rubygems helper for project Rakefiles} 41 | s.test_files = ["test/test_hoe.rb"] 42 | 43 | if s.respond_to? :specification_version then 44 | current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION 45 | s.specification_version = 3 46 | 47 | if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then 48 | s.add_runtime_dependency(%q, [">= 1.0.4"]) 49 | s.add_runtime_dependency(%q, [">= 0.8.7"]) 50 | else 51 | s.add_dependency(%q, [">= 1.0.4"]) 52 | s.add_dependency(%q, [">= 0.8.7"]) 53 | end 54 | else 55 | s.add_dependency(%q, [">= 1.0.4"]) 56 | s.add_dependency(%q, [">= 0.8.7"]) 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /lib/isolate-3.1.0.pre.3/test/fixtures/with-hoe/specifications/rake-0.8.7.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | 3 | Gem::Specification.new do |s| 4 | s.name = %q{rake} 5 | s.version = "0.8.7" 6 | 7 | s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= 8 | s.authors = ["Jim Weirich"] 9 | s.date = %q{2009-05-14} 10 | s.default_executable = %q{rake} 11 | s.description = %q{Rake is a Make-like program implemented in Ruby. Tasks and dependencies are specified in standard Ruby syntax.} 12 | s.email = %q{jim@weirichhouse.org} 13 | s.executables = ["rake"] 14 | s.extra_rdoc_files = ["README", "MIT-LICENSE", "TODO", "CHANGES", "doc/command_line_usage.rdoc", "doc/glossary.rdoc", "doc/proto_rake.rdoc", "doc/rakefile.rdoc", "doc/rational.rdoc", "doc/release_notes/rake-0.4.14.rdoc", "doc/release_notes/rake-0.4.15.rdoc", "doc/release_notes/rake-0.5.0.rdoc", "doc/release_notes/rake-0.5.3.rdoc", "doc/release_notes/rake-0.5.4.rdoc", "doc/release_notes/rake-0.6.0.rdoc", "doc/release_notes/rake-0.7.0.rdoc", "doc/release_notes/rake-0.7.1.rdoc", "doc/release_notes/rake-0.7.2.rdoc", "doc/release_notes/rake-0.7.3.rdoc", "doc/release_notes/rake-0.8.0.rdoc", "doc/release_notes/rake-0.8.2.rdoc", "doc/release_notes/rake-0.8.3.rdoc", "doc/release_notes/rake-0.8.4.rdoc", "doc/release_notes/rake-0.8.5.rdoc", "doc/release_notes/rake-0.8.6.rdoc", "doc/release_notes/rake-0.8.7.rdoc"] 15 | s.files = ["install.rb", "CHANGES", "MIT-LICENSE", "Rakefile", "README", "TODO", "bin/rake", "lib/rake/alt_system.rb", "lib/rake/classic_namespace.rb", "lib/rake/clean.rb", "lib/rake/contrib/compositepublisher.rb", "lib/rake/contrib/ftptools.rb", "lib/rake/contrib/publisher.rb", "lib/rake/contrib/rubyforgepublisher.rb", "lib/rake/contrib/sshpublisher.rb", "lib/rake/contrib/sys.rb", "lib/rake/gempackagetask.rb", "lib/rake/loaders/makefile.rb", "lib/rake/packagetask.rb", "lib/rake/rake_test_loader.rb", "lib/rake/rdoctask.rb", "lib/rake/ruby182_test_unit_fix.rb", "lib/rake/runtest.rb", "lib/rake/tasklib.rb", "lib/rake/testtask.rb", "lib/rake/win32.rb", "lib/rake.rb", "test/capture_stdout.rb", "test/check_expansion.rb", "test/check_no_expansion.rb", "test/contrib/test_sys.rb", "test/data/rakelib/test1.rb", "test/data/rbext/rakefile.rb", "test/filecreation.rb", "test/functional.rb", "test/in_environment.rb", "test/rake_test_setup.rb", "test/reqfile.rb", "test/reqfile2.rb", "test/session_functional.rb", "test/shellcommand.rb", "test/test_application.rb", "test/test_clean.rb", "test/test_definitions.rb", "test/test_earlytime.rb", "test/test_extension.rb", "test/test_file_creation_task.rb", "test/test_file_task.rb", "test/test_filelist.rb", "test/test_fileutils.rb", "test/test_ftp.rb", "test/test_invocation_chain.rb", "test/test_makefile_loader.rb", "test/test_multitask.rb", "test/test_namespace.rb", "test/test_package_task.rb", "test/test_pathmap.rb", "test/test_pseudo_status.rb", "test/test_rake.rb", "test/test_rdoc_task.rb", "test/test_require.rb", "test/test_rules.rb", "test/test_task_arguments.rb", "test/test_task_manager.rb", "test/test_tasklib.rb", "test/test_tasks.rb", "test/test_test_task.rb", "test/test_top_level_functions.rb", "test/test_win32.rb", "test/data/imports/deps.mf", "test/data/sample.mf", "test/data/chains/Rakefile", "test/data/default/Rakefile", "test/data/dryrun/Rakefile", "test/data/file_creation_task/Rakefile", "test/data/imports/Rakefile", "test/data/multidesc/Rakefile", "test/data/namespace/Rakefile", "test/data/statusreturn/Rakefile", "test/data/unittest/Rakefile", "test/data/unittest/subdir", "doc/command_line_usage.rdoc", "doc/example", "doc/example/a.c", "doc/example/b.c", "doc/example/main.c", "doc/example/Rakefile1", "doc/example/Rakefile2", "doc/glossary.rdoc", "doc/jamis.rb", "doc/proto_rake.rdoc", "doc/rake.1.gz", "doc/rakefile.rdoc", "doc/rational.rdoc", "doc/release_notes", "doc/release_notes/rake-0.4.14.rdoc", "doc/release_notes/rake-0.4.15.rdoc", "doc/release_notes/rake-0.5.0.rdoc", "doc/release_notes/rake-0.5.3.rdoc", "doc/release_notes/rake-0.5.4.rdoc", "doc/release_notes/rake-0.6.0.rdoc", "doc/release_notes/rake-0.7.0.rdoc", "doc/release_notes/rake-0.7.1.rdoc", "doc/release_notes/rake-0.7.2.rdoc", "doc/release_notes/rake-0.7.3.rdoc", "doc/release_notes/rake-0.8.0.rdoc", "doc/release_notes/rake-0.8.2.rdoc", "doc/release_notes/rake-0.8.3.rdoc", "doc/release_notes/rake-0.8.4.rdoc", "doc/release_notes/rake-0.8.5.rdoc", "doc/release_notes/rake-0.8.6.rdoc", "doc/release_notes/rake-0.8.7.rdoc"] 16 | s.homepage = %q{http://rake.rubyforge.org} 17 | s.rdoc_options = ["--line-numbers", "--main", "README", "--title", "Rake -- Ruby Make"] 18 | s.require_paths = ["lib"] 19 | s.rubyforge_project = %q{rake} 20 | s.rubygems_version = %q{1.3.3} 21 | s.summary = %q{Ruby based make-like utility.} 22 | 23 | if s.respond_to? :specification_version then 24 | current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION 25 | s.specification_version = 2 26 | 27 | if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then 28 | else 29 | end 30 | else 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /lib/isolate-3.1.0.pre.3/test/fixtures/with-hoe/specifications/rubyforge-1.0.4.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | 3 | Gem::Specification.new do |s| 4 | s.name = %q{rubyforge} 5 | s.version = "1.0.4" 6 | 7 | s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= 8 | s.authors = ["Ryan Davis", "Eric Hodel", "Ara T Howard"] 9 | s.date = %q{2009-07-21} 10 | s.default_executable = %q{rubyforge} 11 | s.description = %q{A script which automates a limited set of rubyforge operations. 12 | 13 | * Run 'rubyforge help' for complete usage. 14 | * Setup: For first time users AND upgrades to 0.4.0: 15 | * rubyforge setup (deletes your username and password, so run sparingly!) 16 | * edit ~/.rubyforge/user-config.yml 17 | * rubyforge config 18 | * For all rubyforge upgrades, run 'rubyforge config' to ensure you have latest. 19 | * Don't forget to login! logging in will store a cookie in your 20 | .rubyforge directory which expires after a time. always run the 21 | login command before any operation that requires authentication, 22 | such as uploading a package.} 23 | s.email = ["ryand-ruby@zenspider.com", "drbrain@segment7.net", "ara.t.howard@gmail.com"] 24 | s.executables = ["rubyforge"] 25 | s.extra_rdoc_files = ["History.txt", "Manifest.txt", "README.txt"] 26 | s.files = ["History.txt", "Manifest.txt", "README.txt", "Rakefile", "bin/rubyforge", "lib/rubyforge.rb", "lib/rubyforge/client.rb", "lib/rubyforge/cookie_manager.rb", "test/test_rubyforge.rb", "test/test_rubyforge_client.rb", "test/test_rubyforge_cookie_manager.rb"] 27 | s.homepage = %q{http://codeforpeople.rubyforge.org/rubyforge/} 28 | s.rdoc_options = ["--main", "README.txt"] 29 | s.require_paths = ["lib"] 30 | s.rubyforge_project = %q{codeforpeople} 31 | s.rubygems_version = %q{1.3.5} 32 | s.summary = %q{A script which automates a limited set of rubyforge operations} 33 | s.test_files = ["test/test_rubyforge.rb", "test/test_rubyforge_client.rb", "test/test_rubyforge_cookie_manager.rb"] 34 | 35 | if s.respond_to? :specification_version then 36 | current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION 37 | s.specification_version = 3 38 | 39 | if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then 40 | else 41 | end 42 | else 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /lib/isolate-3.1.0.pre.3/test/isolate/test.rb: -------------------------------------------------------------------------------- 1 | require "isolate" 2 | require "minitest/autorun" 3 | 4 | module Isolate 5 | class Test < MiniTest::Unit::TestCase 6 | def setup 7 | Gem.refresh 8 | 9 | @env = ENV.to_hash 10 | @lp = $LOAD_PATH.dup 11 | @lf = $LOADED_FEATURES.dup 12 | end 13 | 14 | def teardown 15 | Gem::DependencyInstaller.reset_value 16 | Gem::Uninstaller.reset_value 17 | 18 | ENV.replace @env 19 | $LOAD_PATH.replace @lp 20 | $LOADED_FEATURES.replace @lf 21 | 22 | FileUtils.rm_rf "tmp/isolate" 23 | end 24 | end 25 | end 26 | 27 | module BrutalStub 28 | @@value = [] 29 | def value; @@value end 30 | def reset_value; value.clear end 31 | end 32 | 33 | class Gem::DependencyInstaller 34 | extend BrutalStub 35 | 36 | alias old_install install 37 | def install name, requirement 38 | self.class.value << [name, requirement] 39 | end 40 | end 41 | 42 | class Gem::Uninstaller 43 | extend BrutalStub 44 | 45 | attr_reader :gem, :version, :gem_home 46 | alias old_uninstall uninstall 47 | 48 | def uninstall 49 | self.class.value << [self.gem, 50 | self.version.to_s, 51 | self.gem_home.sub(Dir.pwd + "/", '')] 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /lib/isolate-3.1.0.pre.3/test/test_isolate.rb: -------------------------------------------------------------------------------- 1 | require "isolate/test" 2 | 3 | require "isolate" 4 | 5 | class TestIsolate < Isolate::Test 6 | WITH_HOE = "test/fixtures/with-hoe" 7 | 8 | def teardown 9 | Isolate.sandbox.disable if Isolate.sandbox 10 | super 11 | end 12 | 13 | def test_self_env 14 | assert_equal "development", Isolate.env 15 | 16 | ENV["RAILS_ENV"] = "foo" 17 | 18 | assert_equal "foo", Isolate.env 19 | 20 | ENV["RAILS_ENV"] = nil 21 | ENV["RACK_ENV"] = "bar" 22 | 23 | assert_equal "bar", Isolate.env 24 | 25 | ENV["RACK_ENV"] = nil 26 | ENV["ISOLATE_ENV"] = "baz" 27 | 28 | assert_equal "baz", Isolate.env 29 | end 30 | 31 | def test_self_now! 32 | assert_nil Isolate.sandbox 33 | 34 | Isolate.now! :path => WITH_HOE, :multiruby => false, :system => false do 35 | gem "hoe" 36 | end 37 | 38 | refute_nil Isolate.sandbox 39 | assert_equal File.expand_path(WITH_HOE), Isolate.sandbox.path 40 | assert_equal "hoe", Isolate.sandbox.entries.first.name 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /lib/isolate-3.1.0.pre.3/test/test_isolate_entry.rb: -------------------------------------------------------------------------------- 1 | require "isolate/entry" 2 | require "isolate/test" 3 | 4 | class TestIsolateEntry < Isolate::Test 5 | def setup 6 | @sandbox = Object.new 7 | def @sandbox.environments; @e ||= [] end 8 | def @sandbox.path; "tmp/gems" end 9 | 10 | super 11 | end 12 | 13 | def test_initialize 14 | @sandbox.environments.concat %w(foo bar) 15 | 16 | entry = e "baz", "> 1.0", "< 2.0", :quux => :corge 17 | 18 | assert_equal %w(foo bar), entry.environments 19 | assert_equal "baz", entry.name 20 | assert_equal Gem::Requirement.new("> 1.0", "< 2.0"), entry.requirement 21 | assert_equal :corge, entry.options[:quux] 22 | 23 | entry = e "plugh" 24 | 25 | assert_equal Gem::Requirement.default, entry.requirement 26 | assert_equal({}, entry.options) 27 | end 28 | 29 | def test_install_file 30 | file = "test/fixtures/blort-0.0.gem" 31 | entry = e file 32 | entry.install 33 | 34 | assert_equal File.expand_path(file), 35 | Gem::DependencyInstaller.value.first.first 36 | end 37 | 38 | def test_matches? 39 | @sandbox.environments << "test" 40 | entry = e "hi" 41 | 42 | assert entry.matches?("test") 43 | assert !entry.matches?("double secret production") 44 | 45 | entry.environments.clear 46 | assert entry.matches?("double secret production") 47 | end 48 | 49 | def test_matches_spec? 50 | entry = e "hi", "1.1" 51 | 52 | assert entry.matches_spec?(spec("hi", "1.1")) 53 | assert !entry.matches_spec?(spec("bye", "1.1")) 54 | assert !entry.matches_spec?(spec("hi", "1.2")) 55 | end 56 | 57 | def test_update 58 | entry = e "hi", "1.1" 59 | 60 | assert_equal [], entry.environments 61 | 62 | @sandbox.environments.concat %w(corge corge plugh) 63 | entry.update 64 | 65 | assert_equal %w(corge plugh), entry.environments 66 | 67 | entry.update :foo => :bar 68 | entry.update :bar => :baz 69 | 70 | assert_equal({ :foo => :bar, :bar => :baz }, entry.options) 71 | 72 | entry.update :args => "--first" 73 | entry.update :args => "--second" 74 | assert_equal "--second", entry.options[:args] 75 | end 76 | 77 | def e *args 78 | Isolate::Entry.new @sandbox, *args 79 | end 80 | 81 | Spec = Struct.new :name, :version 82 | 83 | def spec name, version 84 | Spec.new name, Gem::Version.new(version) 85 | end 86 | end 87 | -------------------------------------------------------------------------------- /lib/isolate-3.1.0.pre.3/test/test_isolate_events.rb: -------------------------------------------------------------------------------- 1 | require "isolate/events" 2 | require "isolate/test" 3 | 4 | class TestIsolateEvents < Isolate::Test 5 | include Isolate::Events 6 | 7 | def setup 8 | Isolate::Events.watchers.clear 9 | super 10 | end 11 | 12 | def test_self_watch 13 | b = lambda {} 14 | Isolate::Events.watch String, :foo, &b 15 | assert_equal [b], Isolate::Events.watchers[[String, :foo]] 16 | end 17 | 18 | def test_fire 19 | count = 0 20 | 21 | Isolate::Events.watch self.class, :increment do 22 | count += 1 23 | end 24 | 25 | fire :increment 26 | assert_equal 1, count 27 | end 28 | 29 | def test_fire_block 30 | count = 0 31 | 32 | [:increment, :incremented].each do |name| 33 | Isolate::Events.watch self.class, name do 34 | count += 1 35 | end 36 | end 37 | 38 | fire :increment, :incremented do |x| 39 | assert_same self, x 40 | end 41 | 42 | assert_equal 2, count 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /lib/isolate-3.1.0.pre.3/test/test_isolate_sandbox.rb: -------------------------------------------------------------------------------- 1 | require "isolate/test" 2 | 3 | class TestIsolateSandbox < Isolate::Test 4 | WITH_HOE = "test/fixtures/with-hoe" 5 | 6 | def setup 7 | @sandbox = sandbox 8 | super 9 | end 10 | 11 | def test_activate 12 | @sandbox = sandbox :path => WITH_HOE 13 | 14 | refute_loaded_spec "hoe" 15 | @sandbox.gem "hoe" 16 | 17 | result = @sandbox.activate 18 | assert_equal @sandbox, result 19 | assert_loaded_spec "hoe" 20 | end 21 | 22 | def test_activate_environment_explicit 23 | @sandbox = sandbox :path => WITH_HOE 24 | 25 | @sandbox.gem "rubyforge" 26 | @sandbox.environment(:borg) { gem "hoe" } 27 | @sandbox.activate :borg 28 | 29 | assert_loaded_spec "hoe" 30 | assert_loaded_spec "rubyforge" 31 | end 32 | 33 | def test_activate_environment_implicit 34 | s = sandbox :path => WITH_HOE 35 | 36 | s.gem "rubyforge" 37 | s.environment(:borg) { gem "hoe" } 38 | 39 | s.activate 40 | refute_loaded_spec "hoe" 41 | assert_loaded_spec "rubyforge" 42 | end 43 | 44 | def test_activate_install 45 | s = sandbox :path => WITH_HOE, :install => true 46 | s.gem "foo" 47 | 48 | # rescuing because activate, well, actually tries to activate 49 | begin; s.activate; rescue Gem::LoadError; end 50 | 51 | assert_equal ["foo", Gem::Requirement.default], 52 | Gem::DependencyInstaller.value.shift 53 | end 54 | 55 | # TODO: cleanup with 2 versions of same gem, 1 activated 56 | # TODO: install with 1 older version, 1 new gem to be installed 57 | 58 | def test_cleanup 59 | s = sandbox :path => WITH_HOE, :install => true, :cleanup => true 60 | s.activate # no gems on purpose 61 | 62 | expected = [["hoe", "2.3.3", WITH_HOE], 63 | ["rake", "0.8.7", WITH_HOE], 64 | ["rubyforge", "1.0.4", WITH_HOE]] 65 | 66 | assert_equal expected, Gem::Uninstaller.value 67 | end 68 | 69 | def test_disable 70 | home, path, bin = ENV.values_at "GEM_HOME", "GEM_PATH", "PATH" 71 | load_path = $LOAD_PATH.dup 72 | 73 | @sandbox.enable 74 | 75 | refute_equal home, ENV["GEM_HOME"] 76 | refute_equal path, ENV["GEM_PATH"] 77 | refute_equal bin, ENV["PATH"] 78 | 79 | refute_equal load_path, $LOAD_PATH 80 | 81 | result = @sandbox.disable 82 | assert_same @sandbox, result 83 | 84 | assert_equal home, ENV["GEM_HOME"] 85 | assert_equal path, ENV["GEM_PATH"] 86 | assert_equal bin, ENV["PATH"] 87 | assert_equal load_path, $LOAD_PATH 88 | end 89 | 90 | def test_enable 91 | refute_empty Gem.find_files("hoe.rb"), 92 | "There's a hoe.rb somewhere in the current env." 93 | 94 | assert_same @sandbox, @sandbox.enable 95 | 96 | assert_equal @sandbox.path, ENV["GEM_PATH"] 97 | assert_equal @sandbox.path, ENV["GEM_HOME"] 98 | assert ENV["PATH"].include?(File.join(@sandbox.path, "bin")), "in path" 99 | 100 | assert_equal [], Gem.find_files("hoe.rb"), 101 | "Can't find hoe.rb now, 'cause we're activated!" 102 | 103 | assert_empty Gem.loaded_specs 104 | assert_equal [@sandbox.path], Gem.path 105 | end 106 | 107 | def test_enable_idempotent_path_env 108 | bin = File.join @sandbox.path, "bin" 109 | path = ENV["PATH"] = [bin, ENV["PATH"]].join(File::PATH_SEPARATOR) 110 | 111 | @sandbox.enable 112 | assert_equal path, ENV["PATH"] 113 | end 114 | 115 | def test_idempotent_rubyopt_env 116 | @sandbox.enable 117 | rubyopt = ENV["RUBYOPT"] 118 | @sandbox.disable 119 | 120 | refute_equal rubyopt, ENV["RUBYOPT"] 121 | 122 | ENV["RUBYOPT"] = rubyopt 123 | @sandbox.enable 124 | assert_equal rubyopt, ENV["RUBYOPT"] 125 | end 126 | 127 | def test_environment 128 | @sandbox.gem "none" 129 | 130 | @sandbox.environment "test", "ci" do 131 | gem "test-ci" 132 | 133 | environment "production" do 134 | gem "test-ci-production" 135 | end 136 | end 137 | 138 | none, test_ci, test_ci_production = @sandbox.entries 139 | 140 | assert_equal [], none.environments 141 | assert_equal %w(test ci), test_ci.environments 142 | assert_equal %w(test ci production), test_ci_production.environments 143 | end 144 | 145 | def test_gem 146 | g = @sandbox.gem "foo" 147 | assert_includes @sandbox.entries, g 148 | 149 | assert_equal "foo", g.name 150 | assert_equal Gem::Requirement.create(">= 0"), g.requirement 151 | end 152 | 153 | def test_gem_multi_calls 154 | g = @sandbox.gem "foo" 155 | g2 = @sandbox.gem "foo", :foo => :bar 156 | 157 | @sandbox.gem "foo", :bar => :baz 158 | 159 | assert_same g, g2 160 | assert_equal :bar, g.options[:foo] 161 | assert_equal :baz, g.options[:bar] 162 | 163 | @sandbox.gem "foo", "> 1.7" 164 | assert_equal Gem::Requirement.new("> 1.7"), g.requirement 165 | 166 | @sandbox.environment :corge do 167 | gem "foo" 168 | end 169 | 170 | @sandbox.environment :plurgh do 171 | gem "foo" 172 | end 173 | 174 | assert_equal %w(corge plurgh), g.environments 175 | end 176 | 177 | def test_gem_multi_requirements 178 | g = @sandbox.gem "foo", "= 1.0", "< 2.0" 179 | assert_equal Gem::Requirement.create(["= 1.0", "< 2.0"]), g.requirement 180 | end 181 | 182 | def test_gem_options 183 | g = @sandbox.gem "foo", :source => "somewhere" 184 | assert_equal "somewhere", g.options[:source] 185 | end 186 | 187 | def test_initialize_defaults 188 | s = Isolate::Sandbox.new 189 | 190 | assert_equal [], s.entries 191 | assert_equal [], s.environments 192 | assert_match(/tmp\/isolate/, s.path) 193 | 194 | assert s.cleanup? 195 | assert s.install? 196 | assert s.system? 197 | assert s.verbose? 198 | assert s.multiruby? 199 | end 200 | 201 | def test_initialize_override_defaults 202 | s = Isolate::Sandbox.new :path => "x", :cleanup => false, 203 | :install => false, :system => false, 204 | :verbose => false, :multiruby => false 205 | 206 | assert_equal File.expand_path("x"), s.path 207 | 208 | refute s.cleanup? 209 | refute s.install? 210 | refute s.system? 211 | refute s.verbose? 212 | refute s.multiruby? 213 | end 214 | 215 | # First the specifically requested file, then the block (if given), 216 | # THEN the local override file (if it exists). 217 | 218 | def test_initialize_file_and_block 219 | s = sandbox :file => "test/fixtures/override.rb" do 220 | environment :foo do 221 | gem "monkey", "2.0", :args => "--panic" 222 | end 223 | end 224 | 225 | monkey = s.entries.first 226 | 227 | assert_equal %w(foo bar), monkey.environments 228 | assert_equal "--asplode", monkey.options[:args] 229 | assert_equal Gem::Requirement.new("2.0"), monkey.requirement 230 | end 231 | 232 | def test_options 233 | @sandbox.options :hello => :monkey 234 | assert_equal :monkey, @sandbox.options[:hello] 235 | end 236 | 237 | def test_path 238 | s = sandbox :multiruby => false do 239 | options :path => "tmp/foo" 240 | end 241 | 242 | assert_equal File.expand_path("tmp/foo"), s.path 243 | 244 | v = [Gem.ruby_engine, RbConfig::CONFIG["ruby_version"]].join "-" 245 | s = sandbox :multiruby => true 246 | p = File.expand_path("tmp/isolate/#{v}") 247 | 248 | assert_equal p, s.path 249 | 250 | s = sandbox :path => "tmp/isolate/#{v}", :multiruby => false 251 | assert_equal p, s.path 252 | end 253 | 254 | def assert_loaded_spec name 255 | assert Gem.loaded_specs[name], 256 | "#{name} is a loaded gemspec, and it shouldn't be!" 257 | end 258 | 259 | def refute_loaded_spec name 260 | refute Gem.loaded_specs[name], 261 | "#{name} is NOT a loaded gemspec, and it should be!" 262 | end 263 | 264 | def sandbox *args, &block 265 | opts = { 266 | :install => false, 267 | :system => false, 268 | :verbose => false, 269 | :multiruby => false 270 | } 271 | 272 | opts.merge! args.pop if Hash === args.last 273 | Isolate::Sandbox.new opts, &block 274 | end 275 | end 276 | -------------------------------------------------------------------------------- /lib/server.rb: -------------------------------------------------------------------------------- 1 | require "./lib/init" 2 | 3 | disable :logging 4 | set :root, File.dirname(__FILE__) + "/../" 5 | 6 | get "/" do 7 | File.readlines("public/index.html") 8 | end 9 | 10 | get "/albums" do 11 | content_type "application/json" 12 | send_file "public/albums.json" 13 | end 14 | 15 | get "/favicon.ico" do 16 | "" 17 | end 18 | 19 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 3 | 4.0.0 4 | org.angularjs.apps 5 | music-player 6 | 1.0-SNAPSHOT 7 | pom 8 | Peepcode's Backbone Music Player 9 | 10 | 11 | UTF-8 12 | 13 | 14 | 15 | public 16 | test-js 17 | 18 | 19 | -------------------------------------------------------------------------------- /public/albums.json: -------------------------------------------------------------------------------- 1 | [{ 2 | "title": "Bound - Zen Bound Ingame Music", 3 | "artist": "Ghost Monkey", 4 | "tracks": [{ 5 | "title": "Care", 6 | "url": "music/blue.mp3" 7 | }, 8 | { 9 | "title": "Rope and Wood", 10 | "url": "music/jazz.mp3" 11 | }, 12 | { 13 | "title": "Problem Solvent", 14 | "url": "music/minimalish.mp3" 15 | }, 16 | { 17 | "title": "Unpaint My Skin", 18 | "url": "music/slower.mp3" 19 | }, 20 | { 21 | "title": "Nostalgia", 22 | "url": "music/blue.mp3" 23 | }, 24 | { 25 | "title": "Interludum", 26 | "url": "music/jazz.mp3" 27 | }, 28 | { 29 | "title": "Grind", 30 | "url": "music/minimalish.mp3" 31 | }, 32 | { 33 | "title": "Diagrams", 34 | "url": "music/slower.mp3" 35 | }, 36 | { 37 | "title": "Hare", 38 | "url": "music/blue.mp3" 39 | }, 40 | { 41 | "title": "Carefree", 42 | "url": "music/jazz.mp3" 43 | }, 44 | { 45 | "title": "Tunnel At The End Of Light", 46 | "url": "music/minimalish.mp3" 47 | }] 48 | }, 49 | { 50 | "title": "Where the Earth Meets the Sky", 51 | "artist": "Tom Heasley", 52 | "tracks": [{ 53 | "title": "Ground Zero", 54 | "url": "music/blue.mp3" 55 | }, 56 | { 57 | "title": "Western Sky", 58 | "url": "music/jazz.mp3" 59 | }, 60 | { 61 | "title": "Monterey Bay", 62 | "url": "music/minimalish.mp3" 63 | }, 64 | { 65 | "title": "Where the Earth Meets the Sky", 66 | "url": "music/slower.mp3" 67 | }] 68 | }] 69 | -------------------------------------------------------------------------------- /public/images/add.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular/peepcode-tunes/cc3cd4a44b48e34afe9f34d60810be95716fd589/public/images/add.png -------------------------------------------------------------------------------- /public/images/control_end.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular/peepcode-tunes/cc3cd4a44b48e34afe9f34d60810be95716fd589/public/images/control_end.png -------------------------------------------------------------------------------- /public/images/control_end_hover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular/peepcode-tunes/cc3cd4a44b48e34afe9f34d60810be95716fd589/public/images/control_end_hover.png -------------------------------------------------------------------------------- /public/images/control_pause.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular/peepcode-tunes/cc3cd4a44b48e34afe9f34d60810be95716fd589/public/images/control_pause.png -------------------------------------------------------------------------------- /public/images/control_pause_hover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular/peepcode-tunes/cc3cd4a44b48e34afe9f34d60810be95716fd589/public/images/control_pause_hover.png -------------------------------------------------------------------------------- /public/images/control_play.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular/peepcode-tunes/cc3cd4a44b48e34afe9f34d60810be95716fd589/public/images/control_play.png -------------------------------------------------------------------------------- /public/images/control_play_hover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular/peepcode-tunes/cc3cd4a44b48e34afe9f34d60810be95716fd589/public/images/control_play_hover.png -------------------------------------------------------------------------------- /public/images/control_start.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular/peepcode-tunes/cc3cd4a44b48e34afe9f34d60810be95716fd589/public/images/control_start.png -------------------------------------------------------------------------------- /public/images/control_start_hover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular/peepcode-tunes/cc3cd4a44b48e34afe9f34d60810be95716fd589/public/images/control_start_hover.png -------------------------------------------------------------------------------- /public/images/control_stop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular/peepcode-tunes/cc3cd4a44b48e34afe9f34d60810be95716fd589/public/images/control_stop.png -------------------------------------------------------------------------------- /public/images/control_stop_hover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular/peepcode-tunes/cc3cd4a44b48e34afe9f34d60810be95716fd589/public/images/control_stop_hover.png -------------------------------------------------------------------------------- /public/images/remove.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular/peepcode-tunes/cc3cd4a44b48e34afe9f34d60810be95716fd589/public/images/remove.png -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Angular Tunes 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |
21 |
22 |

Playlist

23 | 29 |
    30 |
  • 32 | 33 | {{album.title}} 34 | {{album.artist}} 35 |
      36 |
    1. {{track.title}}
    2. 39 |
    40 |
  • 41 |
42 |
43 |
44 |

Music Library

45 |
    46 |
  • 47 | 48 | {{album.title}} 49 | {{album.artist}} 50 |
  • 51 |
52 |
53 |
54 | 55 | 56 | -------------------------------------------------------------------------------- /public/js/Tunes.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | (function(window) { 4 | 5 | var tunesApp = angular.module('tunesApp', []); 6 | 7 | // // app with fake backend (uncoment if you want to use it) 8 | // var tunesAppFake = angular.module('tunesAppFake', ['tunesApp', 'ngMockE2E']); 9 | // tunesAppFake.run(function($httpBackend) { 10 | // $httpBackend.whenGET(/templates\/.*/).passThrough(); 11 | // $httpBackend.whenGET('albums.json').respond([ 12 | // { 13 | // "title": "test album", 14 | // "artist": "test artist", 15 | // "tracks": [ 16 | // { 17 | // "title": "test track 1", 18 | // "url": "music/blue.mp3" 19 | // }, 20 | // { 21 | // "title": "test track 2", 22 | // "url": "music/jazz.mp3" 23 | // } 24 | // ] 25 | // } 26 | // ]); 27 | // }); 28 | 29 | window.TunesCtrl = function($scope, $http, player) { 30 | $scope.player = player; 31 | $http.get('albums.json').success(function(data) { 32 | $scope.albums = data; 33 | }); 34 | }; 35 | 36 | 37 | tunesApp.factory('player', function(audio, $rootScope) { 38 | var player, 39 | playlist = [], 40 | paused = false, 41 | current = { 42 | album: 0, 43 | track: 0 44 | }; 45 | 46 | player = { 47 | playlist: playlist, 48 | 49 | current: current, 50 | 51 | playing: false, 52 | 53 | play: function(track, album) { 54 | if (!playlist.length) return; 55 | 56 | if (angular.isDefined(track)) current.track = track; 57 | if (angular.isDefined(album)) current.album = album; 58 | 59 | if (!paused) audio.src = playlist[current.album].tracks[current.track].url; 60 | audio.play(); 61 | player.playing = true; 62 | paused = false; 63 | }, 64 | 65 | pause: function() { 66 | if (player.playing) { 67 | audio.pause(); 68 | player.playing = false; 69 | paused = true; 70 | } 71 | }, 72 | 73 | reset: function() { 74 | player.pause(); 75 | current.album = 0; 76 | current.track = 0; 77 | }, 78 | 79 | next: function() { 80 | if (!playlist.length) return; 81 | paused = false; 82 | if (playlist[current.album].tracks.length > (current.track + 1)) { 83 | current.track++; 84 | } else { 85 | current.track = 0; 86 | current.album = (current.album + 1) % playlist.length; 87 | } 88 | if (player.playing) player.play(); 89 | }, 90 | 91 | previous: function() { 92 | if (!playlist.length) return; 93 | paused = false; 94 | if (current.track > 0) { 95 | current.track--; 96 | } else { 97 | current.album = (current.album - 1 + playlist.length) % playlist.length; 98 | current.track = playlist[current.album].tracks.length - 1; 99 | } 100 | if (player.playing) player.play(); 101 | } 102 | }; 103 | 104 | playlist.add = function(album) { 105 | if (playlist.indexOf(album) != -1) return; 106 | playlist.push(album); 107 | }; 108 | 109 | playlist.remove = function(album) { 110 | var index = playlist.indexOf(album); 111 | if (index == current.album) player.reset(); 112 | playlist.splice(index, 1); 113 | }; 114 | 115 | audio.addEventListener('ended', function() { 116 | $rootScope.$apply(player.next); 117 | }, false); 118 | 119 | return player; 120 | }); 121 | 122 | 123 | // extract the audio for making the player easier to test 124 | tunesApp.factory('audio', function($document) { 125 | var audio = $document[0].createElement('audio'); 126 | return audio; 127 | }); 128 | 129 | })(window); -------------------------------------------------------------------------------- /public/js/vendor/backbone.js: -------------------------------------------------------------------------------- 1 | // Backbone.js 0.5.1 2 | // (c) 2010 Jeremy Ashkenas, DocumentCloud Inc. 3 | // Backbone may be freely distributed under the MIT license. 4 | // For all details and documentation: 5 | // http://documentcloud.github.com/backbone 6 | 7 | (function(){ 8 | 9 | // Initial Setup 10 | // ------------- 11 | 12 | // Save a reference to the global object. 13 | var root = this; 14 | 15 | // Save the previous value of the `Backbone` variable. 16 | var previousBackbone = root.Backbone; 17 | 18 | // The top-level namespace. All public Backbone classes and modules will 19 | // be attached to this. Exported for both CommonJS and the browser. 20 | var Backbone; 21 | if (typeof exports !== 'undefined') { 22 | Backbone = exports; 23 | } else { 24 | Backbone = root.Backbone = {}; 25 | } 26 | 27 | // Current version of the library. Keep in sync with `package.json`. 28 | Backbone.VERSION = '0.5.1'; 29 | 30 | // Require Underscore, if we're on the server, and it's not already present. 31 | var _ = root._; 32 | if (!_ && (typeof require !== 'undefined')) _ = require('underscore')._; 33 | 34 | // For Backbone's purposes, jQuery or Zepto owns the `$` variable. 35 | var $ = root.jQuery || root.Zepto; 36 | 37 | // Runs Backbone.js in *noConflict* mode, returning the `Backbone` variable 38 | // to its previous owner. Returns a reference to this Backbone object. 39 | Backbone.noConflict = function() { 40 | root.Backbone = previousBackbone; 41 | return this; 42 | }; 43 | 44 | // Turn on `emulateHTTP` to use support legacy HTTP servers. Setting this option will 45 | // fake `"PUT"` and `"DELETE"` requests via the `_method` parameter and set a 46 | // `X-Http-Method-Override` header. 47 | Backbone.emulateHTTP = false; 48 | 49 | // Turn on `emulateJSON` to support legacy servers that can't deal with direct 50 | // `application/json` requests ... will encode the body as 51 | // `application/x-www-form-urlencoded` instead and will send the model in a 52 | // form param named `model`. 53 | Backbone.emulateJSON = false; 54 | 55 | // Backbone.Events 56 | // ----------------- 57 | 58 | // A module that can be mixed in to *any object* in order to provide it with 59 | // custom events. You may `bind` or `unbind` a callback function to an event; 60 | // `trigger`-ing an event fires all callbacks in succession. 61 | // 62 | // var object = {}; 63 | // _.extend(object, Backbone.Events); 64 | // object.bind('expand', function(){ alert('expanded'); }); 65 | // object.trigger('expand'); 66 | // 67 | Backbone.Events = { 68 | 69 | // Bind an event, specified by a string name, `ev`, to a `callback` function. 70 | // Passing `"all"` will bind the callback to all events fired. 71 | bind : function(ev, callback) { 72 | var calls = this._callbacks || (this._callbacks = {}); 73 | var list = calls[ev] || (calls[ev] = []); 74 | list.push(callback); 75 | return this; 76 | }, 77 | 78 | // Remove one or many callbacks. If `callback` is null, removes all 79 | // callbacks for the event. If `ev` is null, removes all bound callbacks 80 | // for all events. 81 | unbind : function(ev, callback) { 82 | var calls; 83 | if (!ev) { 84 | this._callbacks = {}; 85 | } else if (calls = this._callbacks) { 86 | if (!callback) { 87 | calls[ev] = []; 88 | } else { 89 | var list = calls[ev]; 90 | if (!list) return this; 91 | for (var i = 0, l = list.length; i < l; i++) { 92 | if (callback === list[i]) { 93 | list[i] = null; 94 | break; 95 | } 96 | } 97 | } 98 | } 99 | return this; 100 | }, 101 | 102 | // Trigger an event, firing all bound callbacks. Callbacks are passed the 103 | // same arguments as `trigger` is, apart from the event name. 104 | // Listening for `"all"` passes the true event name as the first argument. 105 | trigger : function(eventName) { 106 | var list, calls, ev, callback, args; 107 | var both = 2; 108 | if (!(calls = this._callbacks)) return this; 109 | while (both--) { 110 | ev = both ? eventName : 'all'; 111 | if (list = calls[ev]) { 112 | for (var i = 0, l = list.length; i < l; i++) { 113 | if (!(callback = list[i])) { 114 | list.splice(i, 1); i--; l--; 115 | } else { 116 | args = both ? Array.prototype.slice.call(arguments, 1) : arguments; 117 | callback.apply(this, args); 118 | } 119 | } 120 | } 121 | } 122 | return this; 123 | } 124 | 125 | }; 126 | 127 | // Backbone.Model 128 | // -------------- 129 | 130 | // Create a new model, with defined attributes. A client id (`cid`) 131 | // is automatically generated and assigned for you. 132 | Backbone.Model = function(attributes, options) { 133 | var defaults; 134 | attributes || (attributes = {}); 135 | if (defaults = this.defaults) { 136 | if (_.isFunction(defaults)) defaults = defaults(); 137 | attributes = _.extend({}, defaults, attributes); 138 | } 139 | this.attributes = {}; 140 | this._escapedAttributes = {}; 141 | this.cid = _.uniqueId('c'); 142 | this.set(attributes, {silent : true}); 143 | this._changed = false; 144 | this._previousAttributes = _.clone(this.attributes); 145 | if (options && options.collection) this.collection = options.collection; 146 | this.initialize(attributes, options); 147 | }; 148 | 149 | // Attach all inheritable methods to the Model prototype. 150 | _.extend(Backbone.Model.prototype, Backbone.Events, { 151 | 152 | // A snapshot of the model's previous attributes, taken immediately 153 | // after the last `"change"` event was fired. 154 | _previousAttributes : null, 155 | 156 | // Has the item been changed since the last `"change"` event? 157 | _changed : false, 158 | 159 | // The default name for the JSON `id` attribute is `"id"`. MongoDB and 160 | // CouchDB users may want to set this to `"_id"`. 161 | idAttribute : 'id', 162 | 163 | // Initialize is an empty function by default. Override it with your own 164 | // initialization logic. 165 | initialize : function(){}, 166 | 167 | // Return a copy of the model's `attributes` object. 168 | toJSON : function() { 169 | return _.clone(this.attributes); 170 | }, 171 | 172 | // Get the value of an attribute. 173 | get : function(attr) { 174 | return this.attributes[attr]; 175 | }, 176 | 177 | // Get the HTML-escaped value of an attribute. 178 | escape : function(attr) { 179 | var html; 180 | if (html = this._escapedAttributes[attr]) return html; 181 | var val = this.attributes[attr]; 182 | return this._escapedAttributes[attr] = escapeHTML(val == null ? '' : '' + val); 183 | }, 184 | 185 | // Returns `true` if the attribute contains a value that is not null 186 | // or undefined. 187 | has : function(attr) { 188 | return this.attributes[attr] != null; 189 | }, 190 | 191 | // Set a hash of model attributes on the object, firing `"change"` unless you 192 | // choose to silence it. 193 | set : function(attrs, options) { 194 | 195 | // Extract attributes and options. 196 | options || (options = {}); 197 | if (!attrs) return this; 198 | if (attrs.attributes) attrs = attrs.attributes; 199 | var now = this.attributes, escaped = this._escapedAttributes; 200 | 201 | // Run validation. 202 | if (!options.silent && this.validate && !this._performValidation(attrs, options)) return false; 203 | 204 | // Check for changes of `id`. 205 | if (this.idAttribute in attrs) this.id = attrs[this.idAttribute]; 206 | 207 | // We're about to start triggering change events. 208 | var alreadyChanging = this._changing; 209 | this._changing = true; 210 | 211 | // Update attributes. 212 | for (var attr in attrs) { 213 | var val = attrs[attr]; 214 | if (!_.isEqual(now[attr], val)) { 215 | now[attr] = val; 216 | delete escaped[attr]; 217 | this._changed = true; 218 | if (!options.silent) this.trigger('change:' + attr, this, val, options); 219 | } 220 | } 221 | 222 | // Fire the `"change"` event, if the model has been changed. 223 | if (!alreadyChanging && !options.silent && this._changed) this.change(options); 224 | this._changing = false; 225 | return this; 226 | }, 227 | 228 | // Remove an attribute from the model, firing `"change"` unless you choose 229 | // to silence it. `unset` is a noop if the attribute doesn't exist. 230 | unset : function(attr, options) { 231 | if (!(attr in this.attributes)) return this; 232 | options || (options = {}); 233 | var value = this.attributes[attr]; 234 | 235 | // Run validation. 236 | var validObj = {}; 237 | validObj[attr] = void 0; 238 | if (!options.silent && this.validate && !this._performValidation(validObj, options)) return false; 239 | 240 | // Remove the attribute. 241 | delete this.attributes[attr]; 242 | delete this._escapedAttributes[attr]; 243 | if (attr == this.idAttribute) delete this.id; 244 | this._changed = true; 245 | if (!options.silent) { 246 | this.trigger('change:' + attr, this, void 0, options); 247 | this.change(options); 248 | } 249 | return this; 250 | }, 251 | 252 | // Clear all attributes on the model, firing `"change"` unless you choose 253 | // to silence it. 254 | clear : function(options) { 255 | options || (options = {}); 256 | var attr; 257 | var old = this.attributes; 258 | 259 | // Run validation. 260 | var validObj = {}; 261 | for (attr in old) validObj[attr] = void 0; 262 | if (!options.silent && this.validate && !this._performValidation(validObj, options)) return false; 263 | 264 | this.attributes = {}; 265 | this._escapedAttributes = {}; 266 | this._changed = true; 267 | if (!options.silent) { 268 | for (attr in old) { 269 | this.trigger('change:' + attr, this, void 0, options); 270 | } 271 | this.change(options); 272 | } 273 | return this; 274 | }, 275 | 276 | // Fetch the model from the server. If the server's representation of the 277 | // model differs from its current attributes, they will be overriden, 278 | // triggering a `"change"` event. 279 | fetch : function(options) { 280 | options || (options = {}); 281 | var model = this; 282 | var success = options.success; 283 | options.success = function(resp, status, xhr) { 284 | if (!model.set(model.parse(resp, xhr), options)) return false; 285 | if (success) success(model, resp); 286 | }; 287 | options.error = wrapError(options.error, model, options); 288 | return (this.sync || Backbone.sync).call(this, 'read', this, options); 289 | }, 290 | 291 | // Set a hash of model attributes, and sync the model to the server. 292 | // If the server returns an attributes hash that differs, the model's 293 | // state will be `set` again. 294 | save : function(attrs, options) { 295 | options || (options = {}); 296 | if (attrs && !this.set(attrs, options)) return false; 297 | var model = this; 298 | var success = options.success; 299 | options.success = function(resp, status, xhr) { 300 | if (!model.set(model.parse(resp, xhr), options)) return false; 301 | if (success) success(model, resp, xhr); 302 | }; 303 | options.error = wrapError(options.error, model, options); 304 | var method = this.isNew() ? 'create' : 'update'; 305 | return (this.sync || Backbone.sync).call(this, method, this, options); 306 | }, 307 | 308 | // Destroy this model on the server if it was already persisted. Upon success, the model is removed 309 | // from its collection, if it has one. 310 | destroy : function(options) { 311 | options || (options = {}); 312 | if (this.isNew()) return this.trigger('destroy', this, this.collection, options); 313 | var model = this; 314 | var success = options.success; 315 | options.success = function(resp) { 316 | model.trigger('destroy', model, model.collection, options); 317 | if (success) success(model, resp); 318 | }; 319 | options.error = wrapError(options.error, model, options); 320 | return (this.sync || Backbone.sync).call(this, 'delete', this, options); 321 | }, 322 | 323 | // Default URL for the model's representation on the server -- if you're 324 | // using Backbone's restful methods, override this to change the endpoint 325 | // that will be called. 326 | url : function() { 327 | var base = getUrl(this.collection) || this.urlRoot || urlError(); 328 | if (this.isNew()) return base; 329 | return base + (base.charAt(base.length - 1) == '/' ? '' : '/') + encodeURIComponent(this.id); 330 | }, 331 | 332 | // **parse** converts a response into the hash of attributes to be `set` on 333 | // the model. The default implementation is just to pass the response along. 334 | parse : function(resp, xhr) { 335 | return resp; 336 | }, 337 | 338 | // Create a new model with identical attributes to this one. 339 | clone : function() { 340 | return new this.constructor(this); 341 | }, 342 | 343 | // A model is new if it has never been saved to the server, and lacks an id. 344 | isNew : function() { 345 | return this.id == null; 346 | }, 347 | 348 | // Call this method to manually fire a `change` event for this model. 349 | // Calling this will cause all objects observing the model to update. 350 | change : function(options) { 351 | this.trigger('change', this, options); 352 | this._previousAttributes = _.clone(this.attributes); 353 | this._changed = false; 354 | }, 355 | 356 | // Determine if the model has changed since the last `"change"` event. 357 | // If you specify an attribute name, determine if that attribute has changed. 358 | hasChanged : function(attr) { 359 | if (attr) return this._previousAttributes[attr] != this.attributes[attr]; 360 | return this._changed; 361 | }, 362 | 363 | // Return an object containing all the attributes that have changed, or false 364 | // if there are no changed attributes. Useful for determining what parts of a 365 | // view need to be updated and/or what attributes need to be persisted to 366 | // the server. 367 | changedAttributes : function(now) { 368 | now || (now = this.attributes); 369 | var old = this._previousAttributes; 370 | var changed = false; 371 | for (var attr in now) { 372 | if (!_.isEqual(old[attr], now[attr])) { 373 | changed = changed || {}; 374 | changed[attr] = now[attr]; 375 | } 376 | } 377 | return changed; 378 | }, 379 | 380 | // Get the previous value of an attribute, recorded at the time the last 381 | // `"change"` event was fired. 382 | previous : function(attr) { 383 | if (!attr || !this._previousAttributes) return null; 384 | return this._previousAttributes[attr]; 385 | }, 386 | 387 | // Get all of the attributes of the model at the time of the previous 388 | // `"change"` event. 389 | previousAttributes : function() { 390 | return _.clone(this._previousAttributes); 391 | }, 392 | 393 | // Run validation against a set of incoming attributes, returning `true` 394 | // if all is well. If a specific `error` callback has been passed, 395 | // call that instead of firing the general `"error"` event. 396 | _performValidation : function(attrs, options) { 397 | var error = this.validate(attrs); 398 | if (error) { 399 | if (options.error) { 400 | options.error(this, error, options); 401 | } else { 402 | this.trigger('error', this, error, options); 403 | } 404 | return false; 405 | } 406 | return true; 407 | } 408 | 409 | }); 410 | 411 | // Backbone.Collection 412 | // ------------------- 413 | 414 | // Provides a standard collection class for our sets of models, ordered 415 | // or unordered. If a `comparator` is specified, the Collection will maintain 416 | // its models in sort order, as they're added and removed. 417 | Backbone.Collection = function(models, options) { 418 | options || (options = {}); 419 | if (options.comparator) this.comparator = options.comparator; 420 | _.bindAll(this, '_onModelEvent', '_removeReference'); 421 | this._reset(); 422 | if (models) this.reset(models, {silent: true}); 423 | this.initialize.apply(this, arguments); 424 | }; 425 | 426 | // Define the Collection's inheritable methods. 427 | _.extend(Backbone.Collection.prototype, Backbone.Events, { 428 | 429 | // The default model for a collection is just a **Backbone.Model**. 430 | // This should be overridden in most cases. 431 | model : Backbone.Model, 432 | 433 | // Initialize is an empty function by default. Override it with your own 434 | // initialization logic. 435 | initialize : function(){}, 436 | 437 | // The JSON representation of a Collection is an array of the 438 | // models' attributes. 439 | toJSON : function() { 440 | return this.map(function(model){ return model.toJSON(); }); 441 | }, 442 | 443 | // Add a model, or list of models to the set. Pass **silent** to avoid 444 | // firing the `added` event for every new model. 445 | add : function(models, options) { 446 | if (_.isArray(models)) { 447 | for (var i = 0, l = models.length; i < l; i++) { 448 | this._add(models[i], options); 449 | } 450 | } else { 451 | this._add(models, options); 452 | } 453 | return this; 454 | }, 455 | 456 | // Remove a model, or a list of models from the set. Pass silent to avoid 457 | // firing the `removed` event for every model removed. 458 | remove : function(models, options) { 459 | if (_.isArray(models)) { 460 | for (var i = 0, l = models.length; i < l; i++) { 461 | this._remove(models[i], options); 462 | } 463 | } else { 464 | this._remove(models, options); 465 | } 466 | return this; 467 | }, 468 | 469 | // Get a model from the set by id. 470 | get : function(id) { 471 | if (id == null) return null; 472 | return this._byId[id.id != null ? id.id : id]; 473 | }, 474 | 475 | // Get a model from the set by client id. 476 | getByCid : function(cid) { 477 | return cid && this._byCid[cid.cid || cid]; 478 | }, 479 | 480 | // Get the model at the given index. 481 | at: function(index) { 482 | return this.models[index]; 483 | }, 484 | 485 | // Force the collection to re-sort itself. You don't need to call this under normal 486 | // circumstances, as the set will maintain sort order as each item is added. 487 | sort : function(options) { 488 | options || (options = {}); 489 | if (!this.comparator) throw new Error('Cannot sort a set without a comparator'); 490 | this.models = this.sortBy(this.comparator); 491 | if (!options.silent) this.trigger('reset', this, options); 492 | return this; 493 | }, 494 | 495 | // Pluck an attribute from each model in the collection. 496 | pluck : function(attr) { 497 | return _.map(this.models, function(model){ return model.get(attr); }); 498 | }, 499 | 500 | // When you have more items than you want to add or remove individually, 501 | // you can reset the entire set with a new list of models, without firing 502 | // any `added` or `removed` events. Fires `reset` when finished. 503 | reset : function(models, options) { 504 | models || (models = []); 505 | options || (options = {}); 506 | this.each(this._removeReference); 507 | this._reset(); 508 | this.add(models, {silent: true}); 509 | if (!options.silent) this.trigger('reset', this, options); 510 | return this; 511 | }, 512 | 513 | // Fetch the default set of models for this collection, resetting the 514 | // collection when they arrive. If `add: true` is passed, appends the 515 | // models to the collection instead of resetting. 516 | fetch : function(options) { 517 | options || (options = {}); 518 | var collection = this; 519 | var success = options.success; 520 | options.success = function(resp, status, xhr) { 521 | collection[options.add ? 'add' : 'reset'](collection.parse(resp, xhr), options); 522 | if (success) success(collection, resp); 523 | }; 524 | options.error = wrapError(options.error, collection, options); 525 | return (this.sync || Backbone.sync).call(this, 'read', this, options); 526 | }, 527 | 528 | // Create a new instance of a model in this collection. After the model 529 | // has been created on the server, it will be added to the collection. 530 | // Returns the model, or 'false' if validation on a new model fails. 531 | create : function(model, options) { 532 | var coll = this; 533 | options || (options = {}); 534 | model = this._prepareModel(model, options); 535 | if (!model) return false; 536 | var success = options.success; 537 | options.success = function(nextModel, resp, xhr) { 538 | coll.add(nextModel, options); 539 | if (success) success(nextModel, resp, xhr); 540 | }; 541 | model.save(null, options); 542 | return model; 543 | }, 544 | 545 | // **parse** converts a response into a list of models to be added to the 546 | // collection. The default implementation is just to pass it through. 547 | parse : function(resp, xhr) { 548 | return resp; 549 | }, 550 | 551 | // Proxy to _'s chain. Can't be proxied the same way the rest of the 552 | // underscore methods are proxied because it relies on the underscore 553 | // constructor. 554 | chain: function () { 555 | return _(this.models).chain(); 556 | }, 557 | 558 | // Reset all internal state. Called when the collection is reset. 559 | _reset : function(options) { 560 | this.length = 0; 561 | this.models = []; 562 | this._byId = {}; 563 | this._byCid = {}; 564 | }, 565 | 566 | // Prepare a model to be added to this collection 567 | _prepareModel: function(model, options) { 568 | if (!(model instanceof Backbone.Model)) { 569 | var attrs = model; 570 | model = new this.model(attrs, {collection: this}); 571 | if (model.validate && !model._performValidation(attrs, options)) model = false; 572 | } else if (!model.collection) { 573 | model.collection = this; 574 | } 575 | return model; 576 | }, 577 | 578 | // Internal implementation of adding a single model to the set, updating 579 | // hash indexes for `id` and `cid` lookups. 580 | // Returns the model, or 'false' if validation on a new model fails. 581 | _add : function(model, options) { 582 | options || (options = {}); 583 | model = this._prepareModel(model, options); 584 | if (!model) return false; 585 | var already = this.getByCid(model) || this.get(model); 586 | if (already) throw new Error(["Can't add the same model to a set twice", already.id]); 587 | this._byId[model.id] = model; 588 | this._byCid[model.cid] = model; 589 | var index = options.at != null ? options.at : 590 | this.comparator ? this.sortedIndex(model, this.comparator) : 591 | this.length; 592 | this.models.splice(index, 0, model); 593 | model.bind('all', this._onModelEvent); 594 | this.length++; 595 | if (!options.silent) model.trigger('add', model, this, options); 596 | return model; 597 | }, 598 | 599 | // Internal implementation of removing a single model from the set, updating 600 | // hash indexes for `id` and `cid` lookups. 601 | _remove : function(model, options) { 602 | options || (options = {}); 603 | model = this.getByCid(model) || this.get(model); 604 | if (!model) return null; 605 | delete this._byId[model.id]; 606 | delete this._byCid[model.cid]; 607 | this.models.splice(this.indexOf(model), 1); 608 | this.length--; 609 | if (!options.silent) model.trigger('remove', model, this, options); 610 | this._removeReference(model); 611 | return model; 612 | }, 613 | 614 | // Internal method to remove a model's ties to a collection. 615 | _removeReference : function(model) { 616 | if (this == model.collection) { 617 | delete model.collection; 618 | } 619 | model.unbind('all', this._onModelEvent); 620 | }, 621 | 622 | // Internal method called every time a model in the set fires an event. 623 | // Sets need to update their indexes when models change ids. All other 624 | // events simply proxy through. "add" and "remove" events that originate 625 | // in other collections are ignored. 626 | _onModelEvent : function(ev, model, collection, options) { 627 | if ((ev == 'add' || ev == 'remove') && collection != this) return; 628 | if (ev == 'destroy') { 629 | this._remove(model, options); 630 | } 631 | if (model && ev === 'change:' + model.idAttribute) { 632 | delete this._byId[model.previous(model.idAttribute)]; 633 | this._byId[model.id] = model; 634 | } 635 | this.trigger.apply(this, arguments); 636 | } 637 | 638 | }); 639 | 640 | // Underscore methods that we want to implement on the Collection. 641 | var methods = ['forEach', 'each', 'map', 'reduce', 'reduceRight', 'find', 'detect', 642 | 'filter', 'select', 'reject', 'every', 'all', 'some', 'any', 'include', 643 | 'contains', 'invoke', 'max', 'min', 'sortBy', 'sortedIndex', 'toArray', 'size', 644 | 'first', 'rest', 'last', 'without', 'indexOf', 'lastIndexOf', 'isEmpty']; 645 | 646 | // Mix in each Underscore method as a proxy to `Collection#models`. 647 | _.each(methods, function(method) { 648 | Backbone.Collection.prototype[method] = function() { 649 | return _[method].apply(_, [this.models].concat(_.toArray(arguments))); 650 | }; 651 | }); 652 | 653 | // Backbone.Router 654 | // ------------------- 655 | 656 | // Routers map faux-URLs to actions, and fire events when routes are 657 | // matched. Creating a new one sets its `routes` hash, if not set statically. 658 | Backbone.Router = function(options) { 659 | options || (options = {}); 660 | if (options.routes) this.routes = options.routes; 661 | this._bindRoutes(); 662 | this.initialize.apply(this, arguments); 663 | }; 664 | 665 | // Cached regular expressions for matching named param parts and splatted 666 | // parts of route strings. 667 | var namedParam = /:([\w\d]+)/g; 668 | var splatParam = /\*([\w\d]+)/g; 669 | var escapeRegExp = /[-[\]{}()+?.,\\^$|#\s]/g; 670 | 671 | // Set up all inheritable **Backbone.Router** properties and methods. 672 | _.extend(Backbone.Router.prototype, Backbone.Events, { 673 | 674 | // Initialize is an empty function by default. Override it with your own 675 | // initialization logic. 676 | initialize : function(){}, 677 | 678 | // Manually bind a single named route to a callback. For example: 679 | // 680 | // this.route('search/:query/p:num', 'search', function(query, num) { 681 | // ... 682 | // }); 683 | // 684 | route : function(route, name, callback) { 685 | Backbone.history || (Backbone.history = new Backbone.History); 686 | if (!_.isRegExp(route)) route = this._routeToRegExp(route); 687 | Backbone.history.route(route, _.bind(function(fragment) { 688 | var args = this._extractParameters(route, fragment); 689 | callback.apply(this, args); 690 | this.trigger.apply(this, ['route:' + name].concat(args)); 691 | }, this)); 692 | }, 693 | 694 | // Simple proxy to `Backbone.history` to save a fragment into the history. 695 | navigate : function(fragment, triggerRoute) { 696 | Backbone.history.navigate(fragment, triggerRoute); 697 | }, 698 | 699 | // Bind all defined routes to `Backbone.history`. We have to reverse the 700 | // order of the routes here to support behavior where the most general 701 | // routes can be defined at the bottom of the route map. 702 | _bindRoutes : function() { 703 | if (!this.routes) return; 704 | var routes = []; 705 | for (var route in this.routes) { 706 | routes.unshift([route, this.routes[route]]); 707 | } 708 | for (var i = 0, l = routes.length; i < l; i++) { 709 | this.route(routes[i][0], routes[i][1], this[routes[i][1]]); 710 | } 711 | }, 712 | 713 | // Convert a route string into a regular expression, suitable for matching 714 | // against the current location hash. 715 | _routeToRegExp : function(route) { 716 | route = route.replace(escapeRegExp, "\\$&") 717 | .replace(namedParam, "([^\/]*)") 718 | .replace(splatParam, "(.*?)"); 719 | return new RegExp('^' + route + '$'); 720 | }, 721 | 722 | // Given a route, and a URL fragment that it matches, return the array of 723 | // extracted parameters. 724 | _extractParameters : function(route, fragment) { 725 | return route.exec(fragment).slice(1); 726 | } 727 | 728 | }); 729 | 730 | // Backbone.History 731 | // ---------------- 732 | 733 | // Handles cross-browser history management, based on URL fragments. If the 734 | // browser does not support `onhashchange`, falls back to polling. 735 | Backbone.History = function() { 736 | this.handlers = []; 737 | _.bindAll(this, 'checkUrl'); 738 | }; 739 | 740 | // Cached regex for cleaning hashes. 741 | var hashStrip = /^#*/; 742 | 743 | // Cached regex for detecting MSIE. 744 | var isExplorer = /msie [\w.]+/; 745 | 746 | // Has the history handling already been started? 747 | var historyStarted = false; 748 | 749 | // Set up all inheritable **Backbone.History** properties and methods. 750 | _.extend(Backbone.History.prototype, { 751 | 752 | // The default interval to poll for hash changes, if necessary, is 753 | // twenty times a second. 754 | interval: 50, 755 | 756 | // Get the cross-browser normalized URL fragment, either from the URL, 757 | // the hash, or the override. 758 | getFragment : function(fragment, forcePushState) { 759 | if (fragment == null) { 760 | if (this._hasPushState || forcePushState) { 761 | fragment = window.location.pathname; 762 | var search = window.location.search; 763 | if (search) fragment += search; 764 | if (fragment.indexOf(this.options.root) == 0) fragment = fragment.substr(this.options.root.length); 765 | } else { 766 | fragment = window.location.hash; 767 | } 768 | } 769 | return fragment.replace(hashStrip, ''); 770 | }, 771 | 772 | // Start the hash change handling, returning `true` if the current URL matches 773 | // an existing route, and `false` otherwise. 774 | start : function(options) { 775 | 776 | // Figure out the initial configuration. Do we need an iframe? 777 | // Is pushState desired ... is it available? 778 | if (historyStarted) throw new Error("Backbone.history has already been started"); 779 | this.options = _.extend({}, {root: '/'}, this.options, options); 780 | this._wantsPushState = !!this.options.pushState; 781 | this._hasPushState = !!(this.options.pushState && window.history && window.history.pushState); 782 | var fragment = this.getFragment(); 783 | var docMode = document.documentMode; 784 | var oldIE = (isExplorer.exec(navigator.userAgent.toLowerCase()) && (!docMode || docMode <= 7)); 785 | if (oldIE) { 786 | this.iframe = $('