diff --git a/sinatra-contrib/lib/sinatra/contrib.rb b/sinatra-contrib/lib/sinatra/contrib.rb index 2af57b51..a0424928 100644 --- a/sinatra-contrib/lib/sinatra/contrib.rb +++ b/sinatra-contrib/lib/sinatra/contrib.rb @@ -7,6 +7,7 @@ module Sinatra # or breaks if external dependencies are missing. Will extend # Sinatra::Application by default. module Common + register :Reloader register :ConfigFile register :Namespace register :RespondWith diff --git a/sinatra-contrib/lib/sinatra/reloader.rb b/sinatra-contrib/lib/sinatra/reloader.rb new file mode 100644 index 00000000..b0f0412a --- /dev/null +++ b/sinatra-contrib/lib/sinatra/reloader.rb @@ -0,0 +1,145 @@ +require 'pathname' + +module Sinatra + module Reloader + class Route + attr_accessor :app, :source_location, :verb, :signature + + def initialize(attrs={}) + self.app = attrs[:app] + self.source_location = attrs[:source_location] + self.verb = attrs[:verb] + self.signature = attrs[:signature] + end + end + + class Watcher + @path_watcher_map ||= Hash.new { |hash, key| hash[key] = new(key) } + + def self.watcher_for(path) + @path_watcher_map[Pathname.new(path).expand_path.to_s] + end + + def self.watch_file(path) + watcher_for(path) + end + + def self.watch_route(route) + watcher_for(route.source_location).routes << route + end + + def self.watch_inline_templates(path, app) + watcher_for(path).inline_templates(app) + end + + def self.ignore(path) + watcher_for(path).ignore + end + + def self.watchers + @path_watcher_map.values + end + + def self.updated + watchers.find_all(&:updated?) + end + + attr_reader :path, :routes, :mtime + attr_writer :app + + def initialize(path) + @path, @routes = path, [] + update + end + + def updated? + !ignored? && mtime != File.mtime(path) + end + + def update + @mtime = File.mtime(path) + end + + def inline_templates(app) + @inline_templates = true + @app = app + end + + def inline_templates? + !!@inline_templates + end + + def ignore + @ignore = true + end + + def ignored? + !!@ignore + end + + def app + @app || (routes.first.app unless routes.empty?) || Sinatra::Application + end + end + + def self.registered(klass) + klass.extend BaseMethods + klass.extend ExtensionMethods + klass.enable :reload_templates + klass.before { Reloader.perform } + end + + def self.perform + Watcher.updated.each do |watcher| + if watcher.inline_templates? + watcher.app.set(:inline_templates, watcher.path) + end + watcher.routes.each do |route| + watcher.app.deactivate_route(route.verb, route.signature) + end + $LOADED_FEATURES.delete(watcher.path) + require watcher.path + watcher.update + end + end + + module BaseMethods + def route(verb, path, options={}, &block) + source_location = block.respond_to?(:source_location) ? + block.source_location.first : caller_files.first + super.tap do |signature| + Watcher.watch_route Route.new( + :app => self, + :source_location => source_location, + :verb => verb, + :signature => signature + ) + end + end + + def iniline_templates=(file=nil) + file = (file.nil? || file == true) ? + (caller_files.first || File.expand_path($0)) : file + Watcher.watch_inline_templates(file, self) + super + end + end + + module ExtensionMethods + def deactivate_route(verb, signature) + (routes[verb] ||= []).delete(signature) + end + + def also_reload(glob) + Dir[glob].each { |path| Watcher.watch_file(path) } + end + + def dont_reload(glob) + Dir[glob].each { |path| Watcher.ignore(path) } + end + end + end + + register Reloader + Delegator.delegate :also_reload, :dont_reload +end diff --git a/sinatra-contrib/spec/reloader/app.rb.erb b/sinatra-contrib/spec/reloader/app.rb.erb new file mode 100644 index 00000000..b6d827a9 --- /dev/null +++ b/sinatra-contrib/spec/reloader/app.rb.erb @@ -0,0 +1,19 @@ +class App < Sinatra::Base + register Sinatra::Reloader +<% unless inline_templates.nil? %> + enable :inline_templates +<% end %> + +<% routes.each do |route| %> + <%= route %> +<% end %> +end + +<% unless inline_templates.nil? %> +__END__ + +<% inline_templates.each_pair do |name, content| %> +@@<%= name %> +<%= content %> +<% end %> +<% end %> diff --git a/sinatra-contrib/spec/reloader_spec.rb b/sinatra-contrib/spec/reloader_spec.rb new file mode 100644 index 00000000..86b86969 --- /dev/null +++ b/sinatra-contrib/spec/reloader_spec.rb @@ -0,0 +1,76 @@ +require 'backports' +require_relative 'spec_helper' +require 'fileutils' + +describe Sinatra::Reloader do + def tmp_dir + File.expand_path('../../tmp', __FILE__) + end + + def app_file_path + File.join(tmp_dir, 'app.rb') + end + + def write_app_file(options={}) + options[:routes] ||= ['get("/foo") { erb :foo }'] + options[:inline_templates] ||= nil + + File.open(app_file_path, 'w') do |f| + template_path = File.expand_path('../reloader/app.rb.erb', __FILE__) + template = Tilt.new(template_path, nil, :trim => '<>') + f.write template.render(Object.new, options) + end + end + + def update_app_file(options={}) + original_mtime = File.mtime(app_file_path) + begin + write_app_file(options) + sleep 0.1 + end until original_mtime != File.mtime(app_file_path) + end + + before(:each) do + FileUtils.rm_rf(tmp_dir) + FileUtils.mkdir_p(tmp_dir) + write_app_file( + :routes => ['get("/foo") { erb :foo }'], + :inline_templates => { :foo => 'foo' } + ) + $LOADED_FEATURES.delete app_file_path + require app_file_path + self.app = App + end + + after(:all) { FileUtils.rm_rf(tmp_dir) } + + it "doesn't mess up the application" do + get('/foo').body.should == 'foo' + end + + it "knows when a route has been modified" do + update_app_file(:routes => ['get("/foo") { "bar" }']) + get('/foo').body.should == 'bar' + end + + it "knows when a route has been added" do + update_app_file( + :routes => ['get("/foo") { "foo" }', 'get("/bar") { "bar" }'] + ) + get('/foo').body.should == 'foo' + get('/bar').body.should == 'bar' + end + + it "knows when a route has been removed" do + update_app_file(:routes => ['get("/bar") { "bar" }']) + get('/foo').status.should == 404 + end + + it "reloads inline templates" do + update_app_file( + :routes => ['get("/foo") { erb :foo }'], + :inline_templates => { :foo => 'bar' } + ) + get('/foo').body.should == 'bar' + end +end