From 07a649ab6de21a839466fd3bf491422a5e7074f0 Mon Sep 17 00:00:00 2001 From: Konstantin Haase Date: Thu, 24 Mar 2011 08:43:39 +0100 Subject: [PATCH 01/14] initial commit --- sinatra-contrib/Gemfile | 2 ++ sinatra-contrib/README.md | 1 + sinatra-contrib/sinatra-contrib.gemspec | 17 +++++++++++++++++ 3 files changed, 20 insertions(+) create mode 100644 sinatra-contrib/Gemfile create mode 100644 sinatra-contrib/README.md create mode 100644 sinatra-contrib/sinatra-contrib.gemspec diff --git a/sinatra-contrib/Gemfile b/sinatra-contrib/Gemfile new file mode 100644 index 00000000..7f5eae31 --- /dev/null +++ b/sinatra-contrib/Gemfile @@ -0,0 +1,2 @@ +source "http://rubygems.org" unless ENV['QUICK'] +gemspec diff --git a/sinatra-contrib/README.md b/sinatra-contrib/README.md new file mode 100644 index 00000000..3b56babf --- /dev/null +++ b/sinatra-contrib/README.md @@ -0,0 +1 @@ +TODO: More imports/rewrites, documentation. \ No newline at end of file diff --git a/sinatra-contrib/sinatra-contrib.gemspec b/sinatra-contrib/sinatra-contrib.gemspec new file mode 100644 index 00000000..1ffad82e --- /dev/null +++ b/sinatra-contrib/sinatra-contrib.gemspec @@ -0,0 +1,17 @@ +Gem::Specification.new do |s| + s.name = "sinatra-contrib" + s.version = "1.2.0" + s.description = "Collection of useful Sinatra extensions" + s.authors = ["Konstantin Haase"] + s.email = "konstantin.mailinglists@googlemail.com" + s.files = Dir["**/*.{rb,md}"] << "LICENSE" + s.has_rdoc = 'yard' + s.homepage = "http://github.com/rkh/#{s.name}" + s.require_paths = ["lib"] + s.summary = s.description + + s.add_dependency "sinatra", "~> 1.2.0" + s.add_dependency "backports", ">= 2.0" + + s.add_development_dependency "rspec", "~> 2.3" +end From f5e015894d3b6338f02f817af17b2e13675a2d43 Mon Sep 17 00:00:00 2001 From: Konstantin Haase Date: Thu, 24 Mar 2011 08:45:29 +0100 Subject: [PATCH 02/14] import sinatra-decompile --- sinatra-contrib/lib/sinatra/decompile.rb | 61 ++++++++++++++++++++++++ sinatra-contrib/spec/decompile_spec.rb | 42 ++++++++++++++++ sinatra-contrib/spec/spec_helper.rb | 11 +++++ 3 files changed, 114 insertions(+) create mode 100644 sinatra-contrib/lib/sinatra/decompile.rb create mode 100644 sinatra-contrib/spec/decompile_spec.rb create mode 100644 sinatra-contrib/spec/spec_helper.rb diff --git a/sinatra-contrib/lib/sinatra/decompile.rb b/sinatra-contrib/lib/sinatra/decompile.rb new file mode 100644 index 00000000..93c93c9c --- /dev/null +++ b/sinatra-contrib/lib/sinatra/decompile.rb @@ -0,0 +1,61 @@ +require 'sinatra/base' +require 'backports' + +module Sinatra + ## + # Can be used as extension or stand-alone: + # + # Sinatra::Decompile.decompile(...) + module Decompile + extend self + + ## + # Regenerates a string pattern for a given route + # + # Example: + # + # class Sinatra::Application + # routes.each do |verb, list| + # puts "#{verb}:" + # list.each do |data| + # puts "\t" << decompile(data) + # end + # end + # end + # + # Will return the internal Regexp if unable to reconstruct the pattern, + # which likely indicates that a Regexp was used in the first place. + # + # You can also use this to check whether you could actually use a string + # pattern instead of your regexp: + # + # decompile /^/foo$/ # => '/foo' + def decompile(pattern, keys = nil, *) + # Everything in here is basically just the reverse of + # Sinatra::Base#compile + pattern, keys = pattern if pattern.respond_to? :to_ary + keys, str = keys.try(:dup), pattern.inspect + return pattern unless str.start_with? '/' and str.end_with? '/' + str.gsub! /^\/\^?|\$?\/$/, '' + return pattern if str =~ /^[\.\+]/ + str.gsub! /\([^\(]*\)/ do |part| + case part + when '(.*?)' + return pattern if keys.shift != 'splat' + '*' + when '([^\/?#]+)' + return pattern if keys.empty? + ":" << keys.shift + else + return pattern + end + end + str.gsub /(.)([\.\+\(\)\/])/ do + return pattern if $1 != "\\" + $2 + end + end + end + + register Decompile +end diff --git a/sinatra-contrib/spec/decompile_spec.rb b/sinatra-contrib/spec/decompile_spec.rb new file mode 100644 index 00000000..84a54ba7 --- /dev/null +++ b/sinatra-contrib/spec/decompile_spec.rb @@ -0,0 +1,42 @@ +require 'backports' +require_relative 'spec_helper' + +RSpec::Matchers.define :decompile do |path| + match do |app| + @compiled, @keys = app.send :compile, path + @decompiled = app.decompile(@compiled, @keys) + @decompiled.should == path + end + + failure_message_for_should do |app| + values = [app, @compiled, @keys, path, @decompiled].map(&:inspect) + "expected %s to decompile %s with %s to %s, but was %s" % values + end +end + +describe Sinatra::Decompile do + subject { Sinatra::Application } + it { should decompile("") } + it { should decompile("/") } + it { should decompile("/?") } + it { should decompile("/foo") } + it { should decompile("/:name") } + it { should decompile("/:name?") } + it { should decompile("/:foo/:bar") } + it { should decompile("/page/:id/edit") } + it { should decompile("/hello/*") } + it { should decompile("/*/foo/*") } + it { should decompile("*") } + it { should decompile(":name.:format") } + it { should decompile(/./) } + it { should decompile(/f(oo)/) } + it { should decompile(/ba+r/) } + + it 'just returns strings' do + subject.decompile('/foo').should == '/foo' + end + + it 'just decompile simple regexps without keys' do + subject.decompile(%r{/foo}).should == '/foo' + end +end diff --git a/sinatra-contrib/spec/spec_helper.rb b/sinatra-contrib/spec/spec_helper.rb new file mode 100644 index 00000000..13e9ca10 --- /dev/null +++ b/sinatra-contrib/spec/spec_helper.rb @@ -0,0 +1,11 @@ +require 'forwardable' + +module TestHelpers +end + +require 'sinatra/contrib' + +RSpec.configure do |config| + config.expect_with :rspec, :stdlib + config.include Sinatra::TestHelpers +end From 0aa2cc73083a4f09e164b2802484eb57f2ea0abd Mon Sep 17 00:00:00 2001 From: Konstantin Haase Date: Thu, 24 Mar 2011 08:45:47 +0100 Subject: [PATCH 03/14] rewrite sinatra-test-helpers --- sinatra-contrib/lib/sinatra/test_helpers.rb | 79 +++++++++++++++++++++ sinatra-contrib/spec/spec_helper.rb | 5 -- 2 files changed, 79 insertions(+), 5 deletions(-) create mode 100644 sinatra-contrib/lib/sinatra/test_helpers.rb diff --git a/sinatra-contrib/lib/sinatra/test_helpers.rb b/sinatra-contrib/lib/sinatra/test_helpers.rb new file mode 100644 index 00000000..c7454ca0 --- /dev/null +++ b/sinatra-contrib/lib/sinatra/test_helpers.rb @@ -0,0 +1,79 @@ +require 'sinatra/base' +require 'rack/test' +require 'rack' +require 'forwardable' + +module Sinatra + Base.set :environment, :test + + module TestHelpers + class Session < Rack::Test::Session + def global_env + @global_env ||= {} + end + + private + + def default_env + super.merge global_env + end + end + + include Rack::Test::Methods + extend Forwardable + + def_delegators :last_response, :body, :headers, :status, :errors + def_delegators :app, :configure, :set, :enable, :disable, :use, :helpers, :register + def_delegators :current_session, :env_for + + def mock_app(base = Sinatra::Base, &block) + @app = Sinatra.new(base, &block) + end + + def app=(base) + @app = base + end + + alias set_app app= + + def app + @app ||= Class.new Sinatra::Base + Rack::Lint.new @app + end + + unless method_defined? :options + def options(uri, params = {}, env = {}, &block) + env = env_for(uri, env.merge(:method => "OPTIONS", :params => params)) + current_session.send(:process_request, uri, env, &block) + end + end + + unless method_defined? :patch + def patch(uri, params = {}, env = {}, &block) + env = env_for(uri, env.merge(:method => "PATCH", :params => params)) + current_session.send(:process_request, uri, env, &block) + end + end + + def last_request? + last_request + true + rescue Rack::Test::Error + false + end + + def session + return {} unless last_request? + raise Rack::Test:Error, "session not enabled for app" unless last_env["rack.session"] or app.session? + last_request.session + end + + def last_env + last_request.env + end + + def build_rack_test_session(name) # :nodoc: + Session.new rack_mock_session(name) + end + end +end diff --git a/sinatra-contrib/spec/spec_helper.rb b/sinatra-contrib/spec/spec_helper.rb index 13e9ca10..cfd75b6d 100644 --- a/sinatra-contrib/spec/spec_helper.rb +++ b/sinatra-contrib/spec/spec_helper.rb @@ -1,8 +1,3 @@ -require 'forwardable' - -module TestHelpers -end - require 'sinatra/contrib' RSpec.configure do |config| From fd19d70134495d36ad43cd839b84b7b11e1e7c0d Mon Sep 17 00:00:00 2001 From: Konstantin Haase Date: Thu, 24 Mar 2011 08:46:25 +0100 Subject: [PATCH 04/14] infrastructure --- sinatra-contrib/lib/sinatra/contrib.rb | 27 +++++++++++ sinatra-contrib/lib/sinatra/contrib/setup.rb | 46 +++++++++++++++++++ .../lib/sinatra/contrib/version.rb | 44 ++++++++++++++++++ 3 files changed, 117 insertions(+) create mode 100644 sinatra-contrib/lib/sinatra/contrib.rb create mode 100644 sinatra-contrib/lib/sinatra/contrib/setup.rb create mode 100644 sinatra-contrib/lib/sinatra/contrib/version.rb diff --git a/sinatra-contrib/lib/sinatra/contrib.rb b/sinatra-contrib/lib/sinatra/contrib.rb new file mode 100644 index 00000000..7f33e5b3 --- /dev/null +++ b/sinatra-contrib/lib/sinatra/contrib.rb @@ -0,0 +1,27 @@ +require 'sinatra/contrib/setup' + +module Sinatra + module Contrib + ## + # Common middleware that doesn't bring run time overhead if not used + # or breaks if external dependencies are missing. Will extend + # Sinatra::Application by default. + module Common + register :ConfigFile + register :Decompile + register :Namespace + helpers :LinkHeader + end + + ## + # Other extensions you don't want to be loaded unless needed. + module Custom + end + + ## + # Stuff that aren't Sinatra extensions, technically. + autoload :TestHelpers + end + + register Sinatra::Contrib::Common +end diff --git a/sinatra-contrib/lib/sinatra/contrib/setup.rb b/sinatra-contrib/lib/sinatra/contrib/setup.rb new file mode 100644 index 00000000..eebafaa2 --- /dev/null +++ b/sinatra-contrib/lib/sinatra/contrib/setup.rb @@ -0,0 +1,46 @@ +require 'sinatra/base' +require 'sinatra/contrib/version' +require 'backports' + +module Sinatra + module Contrib + module Loader + def extensions + @extensions ||= {:helpers => [], :register => []} + end + + def register(name, path = nil) + autoload name, path, :register + end + + def helpers(name, path = nil) + autoload name, path, :helpers + end + + def autoload(name, path = nil, method = nil) + path ||= "sinatra/#{name.to_s.underscore}" + extensions[method] << name if method + Sinatra.autoload(name, path) + end + + def registered(base) + @extensions.each do |meth, list| + base.send(meth, *list.map { |n| Sinatra.const_get n }) + end + end + end + + module Common + extend Loader + end + + module Custom + extend Loader + end + + extend Loader + def self.registered(base) + base.register Common, Custom + end + end +end diff --git a/sinatra-contrib/lib/sinatra/contrib/version.rb b/sinatra-contrib/lib/sinatra/contrib/version.rb new file mode 100644 index 00000000..1f32d8a3 --- /dev/null +++ b/sinatra-contrib/lib/sinatra/contrib/version.rb @@ -0,0 +1,44 @@ +module Sinatra + module Contrib + def self.version + VERSION + end + + module VERSION + extend Comparable + + MAJOR = 1 + MINOR = 2 + TINY = 0 + SIGNATURE = [MAJOR, MINOR, TINY] + STRING = SIGNATURE.join '.' + + def self.major; MAJOR end + def self.minor; MINOR end + def self.tiny; TINY end + def self.to_s; STRING end + + def self.hash + STRING.hash + end + + def self.<=>(other) + other = other.split('.').map { |i| i.to_i } if other.respond_to? :split + SIGNATURE <=> Array(other) + end + + def self.inspect + STRING.inspect + end + + def self.respond_to?(meth, *) + meth.to_s !~ /^__|^to_str$/ and STRING.respond_to? meth unless super + end + + def self.method_missing(meth, *args, &block) + return super unless STRING.respond_to?(meth) + STRING.send(meth, *args, &block) + end + end + end +end From 074aedc187e20a6bb00a4c996121fa851d29b394 Mon Sep 17 00:00:00 2001 From: Konstantin Haase Date: Thu, 24 Mar 2011 08:46:55 +0100 Subject: [PATCH 05/14] new extensions: sinatra-link-helper --- sinatra-contrib/lib/sinatra/link_header.rb | 84 +++++++++++++++++ sinatra-contrib/spec/link_header_spec.rb | 100 +++++++++++++++++++++ 2 files changed, 184 insertions(+) create mode 100644 sinatra-contrib/lib/sinatra/link_header.rb create mode 100644 sinatra-contrib/spec/link_header_spec.rb diff --git a/sinatra-contrib/lib/sinatra/link_header.rb b/sinatra-contrib/lib/sinatra/link_header.rb new file mode 100644 index 00000000..c7fc1903 --- /dev/null +++ b/sinatra-contrib/lib/sinatra/link_header.rb @@ -0,0 +1,84 @@ +require 'sinatra/base' + +module Sinatra + ## + # Helper methods for generating Link HTTP headers and HTML tags. + module LinkHeader + ## + # Set Link HTTP header and returns HTML tags for telling the browser to + # prefetch given resources (only supported by Opera and Firefox at the + # moment). + def prefetch(*urls) + link(:prefetch, *urls) + end + + ## + # Sets Link HTTP header and returns HTML tags for using stylesheets. + def stylesheet(*urls) + urls << {} unless urls.last.respond_to? :to_hash + urls.last[:type] ||= mime_type(:css) + link(:stylesheet, *urls) + end + + ## + # Sets Link HTTP header and returns corresponding HTML tags. + # + # Example: + # + # # Sets header: + # # Link: ; rel="next" + # # Returns String: + # # '' + # link '/foo', :rel => :next + # + # # Multiple URLs + # link :stylesheet, '/a.css', '/b.css' + def link(*urls) + opts = urls.last.respond_to?(:to_hash) ? urls.pop : {} + opts[:rel] = urls.shift unless urls.first.respond_to? :to_str + options = opts.map { |k, v| " #{k}=#{v.to_s.inspect}" } + html_pattern = "" + http_pattern = ["<%s>", *options].join ";" + link = (response["Link"] ||= "") + + urls.map do |url| + link << "\n" unless link.empty? + link << (http_pattern % url) + html_pattern % url + end.join "\n" + end + + ## + # Takes the current value of th Link header(s) and generates HTML tags + # from it. + # + # Example: + # + # get '/' do + # # You can of course use fancy helpers like #link, #stylesheet + # # or #prefetch + # response["Link"] = '; rel="next"' + # haml :some_page + # end + # + # __END__ + # + # @@ layout + # %head= link_headers + # %body= yield + def link_headers + yield if block_given? + return "" unless response.include? "Link" + response["Link"].lines.map do |line| + url, *opts = line.split(';').map(&:strip) + "" + end.join "\n" + end + + def self.registered(base) + puts "WARNING: #{self} is a helpers module, not an extension." + end + end + + helpers LinkHeader +end diff --git a/sinatra-contrib/spec/link_header_spec.rb b/sinatra-contrib/spec/link_header_spec.rb new file mode 100644 index 00000000..cf8d30b0 --- /dev/null +++ b/sinatra-contrib/spec/link_header_spec.rb @@ -0,0 +1,100 @@ +require 'backports' +require_relative 'spec_helper' + +describe Sinatra::LinkHeader do + before do + mock_app do + helpers Sinatra::LinkHeader + before('/') { link 'something', :rel => 'from-filter', :foo => :bar } + + get '/' do + link :something, 'booyah' + end + + get '/style' do + stylesheet '/style.css' + end + + get '/prefetch' do + prefetch '/foo' + end + + get '/link_headers' do + response['Link'] = " ;bar=\"baz\"" + stylesheet '/style.css' + prefetch '/foo' + link_headers + end + end + end + + describe :link do + it "sets link headers" do + get '/' + headers['Link'].lines.should include('; rel="something"') + end + + it "returns link html tags" do + get '/' + body.should == '' + end + + it "takes an options hash" do + get '/' + elements = ["", "foo=\"bar\"", "rel=\"from-filter\""] + headers['Link'].lines.first.strip.split('; ').sort.should == elements + end + end + + describe :stylesheet do + it 'sets link headers' do + get '/style' + headers['Link'].should match(%r{^;}) + end + + it 'sets type to text/css' do + get '/style' + headers['Link'].should include('type="text/css"') + end + + it 'sets rel to stylesheet' do + get '/style' + headers['Link'].should include('rel="stylesheet"') + end + + it 'returns html tag' do + get '/style' + body.should match(%r{^;}) + end + + it 'sets rel to prefetch' do + get '/prefetch' + headers['Link'].should include('rel="prefetch"') + end + + it 'returns html tag' do + get '/prefetch' + body.should == '' + end + end + + describe :link_headers do + it 'generates html for all link headers' do + get '/link_headers' + body.should include('') + body.should include('') + end + end +end From aee11b812fed2348c43c2f61492a7cfaffc26649 Mon Sep 17 00:00:00 2001 From: Konstantin Haase Date: Thu, 24 Mar 2011 08:47:56 +0100 Subject: [PATCH 06/14] start reimplementation of sinatra-namespace --- sinatra-contrib/lib/sinatra/namespace.rb | 100 +++++ sinatra-contrib/spec/namespace_spec.rb | 484 +++++++++++++++++++++++ 2 files changed, 584 insertions(+) create mode 100644 sinatra-contrib/lib/sinatra/namespace.rb create mode 100644 sinatra-contrib/spec/namespace_spec.rb diff --git a/sinatra-contrib/lib/sinatra/namespace.rb b/sinatra-contrib/lib/sinatra/namespace.rb new file mode 100644 index 00000000..3bbd0368 --- /dev/null +++ b/sinatra-contrib/lib/sinatra/namespace.rb @@ -0,0 +1,100 @@ +require 'sinatra/base' +require 'sinatra/decompile' + +module Sinatra + module Namespace + def self.new(base, pattern, conditions = {}, &block) + Module.new do + extend NamespacedMethods + @base, @extensions = base, [] + @pattern, @conditions = compile(pattern, conditions) + namespace = self + before { extend namespace } + class_eval(&block) + end + end + + module SharedMethods + def namespace(pattern, conditions = {}, &block) + Sinatra::Namespace.new(self, pattern, conditions, &block) + end + end + + module NamespacedMethods + include SharedMethods + include Sinatra::Decompile + attr_reader :base + + def self.prefixed(*names) + names.each { |n| define_method(n) { |*a, &b| prefixed(n, *a, &b) }} + end + + prefixed :before, :after, :delete, :get, :head, :options, :patch, :post, :put + + def helpers(*extensions, &block) + class_eval(&block) if block_given? + include(*extensions) if extensions.any? + end + + def register(*extensions, &block) + extensions << Module.new(&block) if block_given? + @extensions += extensions + extensions.each do |extension| + extend extension + extension.registered(self) if extension.respond_to?(:registered) + end + end + + private + + def app + base.respond_to?(:base) ? base.base : base + end + + def compile(pattern, conditions, default_pattern = nil) + if pattern.respond_to? :to_hash + conditions = conditions.merge pattern.to_hash + pattern = nil + end + base_pattern, base_conditions = @pattern, @conditions + pattern ||= default_pattern + base_pattern ||= base.pattern if base.respond_to? :pattern + base_conditions ||= base.conditions if base.respond_to? :conditions + [ prefixed_path(base_pattern, pattern), + (base_conditions || {}).merge(conditions) ] + end + + def prefixed_path(a, b) + return a || b || // unless a and b + a, b = decompile(a), decompile(b) unless a.class == b.class + a, b = regexpify(a), regexpify(b) unless a.class == b.class + path = a.class.new "#{a}#{b}" + path = /^#{path}$/ if path.is_a? Regexp and base == app + path + end + + def regexpify(pattern) + pattern = Sinatra::Base.send(:compile, pattern).first.inspect + pattern.gsub! /^\/(\^|\\A)?|(\$|\\Z)?\/$/, '' + Regexp.new pattern + end + + def prefixed(method, pattern = nil, conditions = {}, &block) + default = '*' if method == :before or method == :after + pattern, conditions = compile pattern, conditions, default + base.send(method, pattern, conditions, &block) + end + end + + module BaseMethods + include SharedMethods + end + + def self.extend_object(base) + base.extend BaseMethods + end + end + + register Sinatra::Namespace + Delegator.delegate :namespace +end diff --git a/sinatra-contrib/spec/namespace_spec.rb b/sinatra-contrib/spec/namespace_spec.rb new file mode 100644 index 00000000..1e12d147 --- /dev/null +++ b/sinatra-contrib/spec/namespace_spec.rb @@ -0,0 +1,484 @@ +require 'backports' +require_relative 'spec_helper' + +describe Sinatra::Namespace do + verbs = [:get, :head, :post, :put, :delete, :options] + verbs << :patch if Sinatra::VERSION >= '1.3' + + def mock_app(&block) + super do + register Sinatra::Namespace + class_eval(&block) + end + end + + def namespace(*args, &block) + mock_app { namespace(*args, &block) } + end + + verbs.each do |verb| + describe "HTTP #{verb.to_s.upcase}" do + describe 'pattern generation' do + it "should add routes including prefix to the base app" do + namespace("/foo") { send(verb, "/bar") { "baz" }} + send(verb, "/foo/bar").should be_ok + body.should == "baz" unless verb == :head + send(verb, "/foo/baz").should_not be_ok + end + + it "should allows adding routes with no path" do + namespace("/foo") { send(verb) { "bar" } } + send(verb, "/foo").should be_ok + body.should == "bar" unless verb == :head + end + + it "allows unsing regular expressions" do + namespace("/foo") { send(verb, /\/\d\d/) { "bar" }} + send(verb, "/foo/12").should be_ok + body.should == "bar" unless verb == :head + send(verb, "/foo/123").should_not be_ok + end + + it "allows using regular expressions for the prefix" do + namespace(/\/\d\d/) { send(verb, /\/\d\d/) { "foo" }} + send(verb, "/23/12").should be_ok + body.should == "foo" unless verb == :head + send(verb, "/123/12").should_not be_ok + end + + it "sets params correctly from namespace" do + namespace("/:foo") { send(verb, "/bar") { params[:foo] }} + send(verb, "/foo/bar").should be_ok + body.should == "foo" unless verb == :head + send(verb, "/foo/baz").should_not be_ok + send(verb, "/fox/bar").should be_ok + body.should == "fox" unless verb == :head + end + + it "sets params correctly from route" do + namespace("/foo") { send(verb, "/:bar") { params[:bar] }} + send(verb, "/foo/bar").should be_ok + body.should == "bar" unless verb == :head + send(verb, "/foo/baz").should be_ok + body.should == "baz" unless verb == :head + end + + it "allows splats to be combined from namespace and route" do + namespace("/*") { send(verb, "/*") { params[:splat].join " - " }} + send(verb, '/foo/bar').should be_ok + body.should == "foo - bar" unless verb == :head + end + + it "sets params correctly from namespace if simple regexp is used for route" do + namespace("/:foo") { send(verb, %r{/bar}) { params[:foo] }} + send(verb, "/foo/bar").should be_ok + body.should == "foo" unless verb == :head + send(verb, "/foo/baz").should_not be_ok + send(verb, "/fox/bar").should be_ok + body.should == "fox" unless verb == :head + end + + it "sets params correctly from route if simple regexp is used for namespace" do + namespace(%r{/foo}) { send(verb, "/:bar") { params[:bar] }} + send(verb, "/foo/bar").should be_ok + body.should == "bar" unless verb == :head + send(verb, "/foo/baz").should be_ok + body.should == "baz" unless verb == :head + end + + it 'allows defining routes without a pattern' do + namespace(%r{/foo}) { send(verb) { 'bar' } } + send(verb, '/foo').should be_ok + body.should == 'bar' unless verb == :head + end + end + + describe 'conditions' do + it 'allows using conditions for namespaces' do + mock_app do + namespace(:host_name => 'example.com') { send(verb) { 'yes' }} + send(verb, '/') { 'no' } + end + send(verb, '/', {}, 'HTTP_HOST' => 'example.com') + last_response.should be_ok + body.should == 'yes' unless verb == :head + send(verb, '/', {}, 'HTTP_HOST' => 'example.org') + last_response.should be_ok + body.should == 'no' unless verb == :head + end + + it 'allows using conditions for before filters' do + namespace '/foo' do + before(:host_name => 'example.com') { @yes = "yes" } + send(verb) { @yes || "no" } + end + send(verb, '/foo', {}, 'HTTP_HOST' => 'example.com') + last_response.should be_ok + body.should == 'yes' unless verb == :head + send(verb, '/foo', {}, 'HTTP_HOST' => 'example.org') + last_response.should be_ok + body.should == 'no' unless verb == :head + end + + it 'allows using conditions for after filters' do + ran = false + namespace '/foo' do + before(:host_name => 'example.com') { ran = true } + send(verb) { "ok" } + end + send(verb, '/foo', {}, 'HTTP_HOST' => 'example.org') + ran.should be_false + send(verb, '/foo', {}, 'HTTP_HOST' => 'example.com') + ran.should be_true + end + + it 'allows using conditions for routes' do + namespace '/foo' do + send(verb, :host_name => 'example.com') { "ok" } + end + send(verb, '/foo', {}, 'HTTP_HOST' => 'example.com').should be_ok + send(verb, '/foo', {}, 'HTTP_HOST' => 'example.org').should_not be_ok + end + + it 'allows using conditions for before filters and the namespace' do + ran = false + namespace '/', :provides => :txt do + before(:host_name => 'example.com') { ran = true } + send(verb) { "ok" } + end + send(verb, '/', {}, 'HTTP_HOST' => 'example.org', 'HTTP_ACCEPT' => 'text/plain') + ran.should be_false + send(verb, '/', {}, 'HTTP_HOST' => 'example.com', 'HTTP_ACCEPT' => 'text/html') + ran.should be_false + send(verb, '/', {}, 'HTTP_HOST' => 'example.com', 'HTTP_ACCEPT' => 'text/plain') + ran.should be_true + end + + it 'allows using conditions for routes and the namespace' do + namespace '/foo', :host_name => 'example.com' do + send(verb, :provides => :txt) { "ok" } + end + send(verb, '/foo', {}, 'HTTP_HOST' => 'example.com', 'HTTP_ACCEPT' => 'text/plain').should be_ok + send(verb, '/foo', {}, 'HTTP_HOST' => 'example.com', 'HTTP_ACCEPT' => 'text/html').should_not be_ok + send(verb, '/foo', {}, 'HTTP_HOST' => 'example.org', 'HTTP_ACCEPT' => 'text/plain').should_not be_ok + end + + it 'allows combining conditions with a prefix for namespaces' do + namespace '/', :host_name => 'example.com' do + send(verb) { "ok" } + end + send(verb, '/', {}, 'HTTP_HOST' => 'example.com').should be_ok + send(verb, '/', {}, 'HTTP_HOST' => 'example.org').should_not be_ok + end + + it 'allows combining conditions with a prefix for before filters' do + ran = false + namespace :provides => :txt do + before('/foo', :host_name => 'example.com') { ran = true } + send(verb, '/*') { "ok" } + end + send(verb, '/foo', {}, 'HTTP_HOST' => 'example.org', 'HTTP_ACCEPT' => 'text/plain') + ran.should be_false + send(verb, '/foo', {}, 'HTTP_HOST' => 'example.com', 'HTTP_ACCEPT' => 'text/html') + ran.should be_false + send(verb, '/bar', {}, 'HTTP_HOST' => 'example.com', 'HTTP_ACCEPT' => 'text/plain') + ran.should be_false + send(verb, '/foo', {}, 'HTTP_HOST' => 'example.com', 'HTTP_ACCEPT' => 'text/plain') + ran.should be_true + end + + it 'allows combining conditions with a prefix for after filters' do + ran = false + namespace :provides => :txt do + after('/foo', :host_name => 'example.com') { ran = true } + send(verb, '/*') { "ok" } + end + send(verb, '/foo', {}, 'HTTP_HOST' => 'example.org', 'HTTP_ACCEPT' => 'text/plain') + ran.should be_false + send(verb, '/foo', {}, 'HTTP_HOST' => 'example.com', 'HTTP_ACCEPT' => 'text/html') + ran.should be_false + send(verb, '/bar', {}, 'HTTP_HOST' => 'example.com', 'HTTP_ACCEPT' => 'text/plain') + ran.should be_false + send(verb, '/foo', {}, 'HTTP_HOST' => 'example.com', 'HTTP_ACCEPT' => 'text/plain') + ran.should be_true + end + + it 'allows combining conditions with a prefix for routes' do + namespace :host_name => 'example.com' do + send(verb, '/foo', :provides => :txt) { "ok" } + end + send(verb, '/foo', {}, 'HTTP_HOST' => 'example.com', 'HTTP_ACCEPT' => 'text/plain').should be_ok + send(verb, '/foo', {}, 'HTTP_HOST' => 'example.com', 'HTTP_ACCEPT' => 'text/html').should_not be_ok + send(verb, '/foo', {}, 'HTTP_HOST' => 'example.org', 'HTTP_ACCEPT' => 'text/plain').should_not be_ok + end + + it 'allows combining conditions with a prefix for filters and the namespace' do + ran = false + namespace '/f', :provides => :txt do + before('oo', :host_name => 'example.com') { ran = true } + send(verb, '/*') { "ok" } + end + send(verb, '/foo', {}, 'HTTP_HOST' => 'example.org', 'HTTP_ACCEPT' => 'text/plain') + ran.should be_false + send(verb, '/foo', {}, 'HTTP_HOST' => 'example.com', 'HTTP_ACCEPT' => 'text/html') + ran.should be_false + send(verb, '/far', {}, 'HTTP_HOST' => 'example.com', 'HTTP_ACCEPT' => 'text/plain') + ran.should be_false + send(verb, '/foo', {}, 'HTTP_HOST' => 'example.com', 'HTTP_ACCEPT' => 'text/plain') + ran.should be_true + end + + it 'allows combining conditions with a prefix for routes and the namespace' do + namespace '/f', :host_name => 'example.com' do + send(verb, 'oo', :provides => :txt) { "ok" } + end + send(verb, '/foo', {}, 'HTTP_HOST' => 'example.com', 'HTTP_ACCEPT' => 'text/plain').should be_ok + send(verb, '/foo', {}, 'HTTP_HOST' => 'example.com', 'HTTP_ACCEPT' => 'text/html').should_not be_ok + send(verb, '/foo', {}, 'HTTP_HOST' => 'example.org', 'HTTP_ACCEPT' => 'text/plain').should_not be_ok + end + end + + describe 'filters' do + it 'should trigger before filters for namespaces' do + ran = false + namespace('/foo') { before { ran = true }} + send(verb, '/foo') + ran.should be_true + end + + it 'should trigger after filters for namespaces' do + ran = false + namespace('/foo') { after { ran = true }} + send(verb, '/foo') + ran.should be_true + end + + it 'should not trigger before filter for different namespaces' do + ran = false + namespace('/foo') { before { ran = true }} + send(verb, '/fox') + ran.should be_false + end + + it 'should not trigger after filter for different namespaces' do + ran = false + namespace('/foo') { after { ran = true }} + send(verb, '/fox') + ran.should be_false + end + end + + describe 'helpers' do + it "allows defining helpers with the helpers method" do + namespace '/foo' do + helpers do + def magic + 42 + end + end + + send verb, '/bar' do + magic.to_s + end + end + + send(verb, '/foo/bar').should be_ok + body.should == '42' unless verb == :head + end + + it "allows defining helpers without the helpers method" do + namespace '/foo' do + def magic + 42 + end + + send verb, '/bar' do + magic.to_s + end + end + + send(verb, '/foo/bar').should be_ok + body.should == '42' unless verb == :head + end + + it "allows using helper mixins with the helpers method" do + mixin = Module.new do + def magic + 42 + end + end + + namespace '/foo' do + helpers mixin + send verb, '/bar' do + magic.to_s + end + end + + send(verb, '/foo/bar').should be_ok + body.should == '42' unless verb == :head + end + + it "makes helpers defined inside a namespace not available to routes outside that namespace" do + mock_app do + namespace '/foo' do + def magic + 42 + end + + send verb, '/bar' do + magic.to_s + end + end + + send verb, '/' do + magic.to_s + end + end + + proc { send verb, '/' }.should raise_error(NameError) + end + + it "makes helper mixins used inside a namespace not available to routes outside that namespace" do + mixin = Module.new do + def magic + 42 + end + end + + mock_app do + namespace '/foo' do + helpers mixin + send verb, '/bar' do + magic.to_s + end + end + + send verb, '/' do + magic.to_s + end + end + + proc { send verb, '/' }.should raise_error(NameError) + end + + it "allows accessing helpers defined outside the namespace" do + mock_app do + helpers do + def magic + 42 + end + end + + namespace '/foo' do + send verb, '/bar' do + magic.to_s + end + end + end + + send(verb, '/foo/bar').should be_ok + body.should == '42' unless verb == :head + end + + it "allows calling super in helpers overwritten inside a namespace" do + mock_app do + helpers do + def magic + 42 + end + end + + namespace '/foo' do + def magic + super - 19 + end + + send verb, '/bar' do + magic.to_s + end + end + end + + send(verb, '/foo/bar').should be_ok + body.should == '23' unless verb == :head + end + end + + describe 'nesting' do + it 'routes to nested namespaces' do + namespace '/foo' do + namespace '/bar' do + send(verb, '/baz') { 'OKAY!!11!'} + end + end + + send(verb, '/foo/bar/baz').should be_ok + body.should == 'OKAY!!11!' unless verb == :head + end + + it 'exposes helpers to nested namespaces' do + namespace '/foo' do + helpers do + def magic + 42 + end + end + + namespace '/bar' do + send verb, '/baz' do + magic.to_s + end + end + end + + send(verb, '/foo/bar/baz').should be_ok + body.should == '42' unless verb == :head + end + + it 'does not use helpers of nested namespaces outside that namespace' do + namespace '/foo' do + namespace '/bar' do + def magic + 42 + end + + send verb, '/baz' do + magic.to_s + end + end + + send verb do + magic.to_s + end + end + + proc { send verb, '/foo' }.should raise_error(NameError) + end + + it 'sets params correctly' do + namespace('/:a') { namespace('/:b') { send(verb) { params[:a] }}} + send(verb, '/foo/bar').should be_ok + body.should == 'foo' unless verb == :head + end + end + + describe 'error handlers' do + it "should allow custom error handlers with not found" + it "should allow custom error handlers with error" + end + + describe 'templates' do + it "allows to define nested templates" + it "allows to define nested layouts" + it "allows setting a different views directory" + end + + describe 'extensions' do + it 'allows registering extensions for a namespace only' + it 'triggers route_added hook' + it 'prevents changing app global settings' + end + end + end +end From 99029a0b411972365e37e94e69154fc0731f6b6e Mon Sep 17 00:00:00 2001 From: Konstantin Haase Date: Thu, 24 Mar 2011 08:57:36 +0100 Subject: [PATCH 07/14] implement namespace local error handling --- sinatra-contrib/lib/sinatra/namespace.rb | 21 ++++++++++++++++++++ sinatra-contrib/spec/namespace_spec.rb | 25 ++++++++++++++++++++++-- 2 files changed, 44 insertions(+), 2 deletions(-) diff --git a/sinatra-contrib/lib/sinatra/namespace.rb b/sinatra-contrib/lib/sinatra/namespace.rb index 3bbd0368..3e846196 100644 --- a/sinatra-contrib/lib/sinatra/namespace.rb +++ b/sinatra-contrib/lib/sinatra/namespace.rb @@ -8,8 +8,17 @@ module Sinatra extend NamespacedMethods @base, @extensions = base, [] @pattern, @conditions = compile(pattern, conditions) + namespace = self before { extend namespace } + define_method(:error_block!) do |*keys| + if block = keys.inject(nil) { |b,k| b ||= namespace.errors[k] } + instance_eval(&block) + else + super(*keys) + end + end + class_eval(&block) end end @@ -45,6 +54,18 @@ module Sinatra end end + def errors + @errors ||= {} + end + + def not_found(&block) + error(404, &block) + end + + def error(codes = Exception, &block) + [*codes].each { |c| errors[c] = block } + end + private def app diff --git a/sinatra-contrib/spec/namespace_spec.rb b/sinatra-contrib/spec/namespace_spec.rb index 1e12d147..537d3ed6 100644 --- a/sinatra-contrib/spec/namespace_spec.rb +++ b/sinatra-contrib/spec/namespace_spec.rb @@ -464,8 +464,29 @@ describe Sinatra::Namespace do end describe 'error handlers' do - it "should allow custom error handlers with not found" - it "should allow custom error handlers with error" + it "should allow custom error handlers with not found" do + namespace('/de') do + not_found { 'nicht gefunden' } + end + send(verb, '/foo').status.should == 404 + last_response.body.should_not == 'nicht gefunden' unless verb == :head + get('/en/foo').status.should == 404 + last_response.body.should_not == 'nicht gefunden' unless verb == :head + get('/de/foo').status.should == 404 + last_response.body.should == 'nicht gefunden' unless verb == :head + end + + it "should allow custom error handlers with error" do + namespace('/de') do + error(404) { 'nicht gefunden' } + end + send(verb, '/foo').status.should == 404 + last_response.body.should_not == 'nicht gefunden' unless verb == :head + get('/en/foo').status.should == 404 + last_response.body.should_not == 'nicht gefunden' unless verb == :head + get('/de/foo').status.should == 404 + last_response.body.should == 'nicht gefunden' unless verb == :head + end end describe 'templates' do From ec248a8b5aa1a85a08e8007f69facb5337c1c6d5 Mon Sep 17 00:00:00 2001 From: Konstantin Haase Date: Thu, 24 Mar 2011 10:25:00 +0100 Subject: [PATCH 08/14] add reimplemenation of sinatra-config-file --- sinatra-contrib/lib/sinatra/config_file.rb | 40 ++++++++++++++++ .../spec/config_file/key_value.yml | 6 +++ .../spec/config_file/missing_env.yml | 4 ++ .../spec/config_file/with_envs.yml | 7 +++ .../spec/config_file/with_nested_envs.yml | 11 +++++ sinatra-contrib/spec/config_file_spec.rb | 47 +++++++++++++++++++ 6 files changed, 115 insertions(+) create mode 100644 sinatra-contrib/lib/sinatra/config_file.rb create mode 100644 sinatra-contrib/spec/config_file/key_value.yml create mode 100644 sinatra-contrib/spec/config_file/missing_env.yml create mode 100644 sinatra-contrib/spec/config_file/with_envs.yml create mode 100644 sinatra-contrib/spec/config_file/with_nested_envs.yml create mode 100644 sinatra-contrib/spec/config_file_spec.rb diff --git a/sinatra-contrib/lib/sinatra/config_file.rb b/sinatra-contrib/lib/sinatra/config_file.rb new file mode 100644 index 00000000..b73e2ea0 --- /dev/null +++ b/sinatra-contrib/lib/sinatra/config_file.rb @@ -0,0 +1,40 @@ +require 'sinatra/base' +require 'yaml' + +module Sinatra + module ConfigFile + def self.registered(base) + base.set :environments, %w[test production development] + end + + def config_file(*paths) + Dir.chdir(root || '.') do + paths.each do |pattern| + Dir.glob(pattern) do |file| + $stderr.puts "loading config file '#{file}'" if logging? + yaml = config_for_env(YAML.load_file(file)) || {} + yaml.each_pair do |key, value| + for_env = config_for_env(value) + set key, for_env unless value and for_env.nil? and respond_to? key + end + end + end + end + end + + private + + def config_for_env(hash) + if hash.respond_to? :keys and hash.keys.all? { |k| environments.include? k.to_s } + hash = hash[environment.to_s] || hash[environment.to_sym] + end + + if hash.respond_to? :to_hash + indifferent_hash = Hash.new {|hash,key| hash[key.to_s] if Symbol === key } + indifferent_hash.merge hash.to_hash + else + hash + end + end + end +end diff --git a/sinatra-contrib/spec/config_file/key_value.yml b/sinatra-contrib/spec/config_file/key_value.yml new file mode 100644 index 00000000..7cfd314c --- /dev/null +++ b/sinatra-contrib/spec/config_file/key_value.yml @@ -0,0 +1,6 @@ +--- +foo: bar +something: 42 +nested: + a: 1 + b: 2 diff --git a/sinatra-contrib/spec/config_file/missing_env.yml b/sinatra-contrib/spec/config_file/missing_env.yml new file mode 100644 index 00000000..88145668 --- /dev/null +++ b/sinatra-contrib/spec/config_file/missing_env.yml @@ -0,0 +1,4 @@ +--- +foo: + production: 10 + development: 20 \ No newline at end of file diff --git a/sinatra-contrib/spec/config_file/with_envs.yml b/sinatra-contrib/spec/config_file/with_envs.yml new file mode 100644 index 00000000..61934d63 --- /dev/null +++ b/sinatra-contrib/spec/config_file/with_envs.yml @@ -0,0 +1,7 @@ +--- +development: + foo: development +production: + foo: production +test: + foo: test diff --git a/sinatra-contrib/spec/config_file/with_nested_envs.yml b/sinatra-contrib/spec/config_file/with_nested_envs.yml new file mode 100644 index 00000000..22e759d8 --- /dev/null +++ b/sinatra-contrib/spec/config_file/with_nested_envs.yml @@ -0,0 +1,11 @@ +--- +database: + production: + adapter: postgresql + database: foo_production + development: + adapter: sqlite + database: db/development.db + test: + adapter: sqlite + database: db/test.db \ No newline at end of file diff --git a/sinatra-contrib/spec/config_file_spec.rb b/sinatra-contrib/spec/config_file_spec.rb new file mode 100644 index 00000000..30da04d9 --- /dev/null +++ b/sinatra-contrib/spec/config_file_spec.rb @@ -0,0 +1,47 @@ +require 'backports' +require_relative 'spec_helper' + +describe Sinatra::ConfigFile do + attr_accessor :settings + def config_file(*args, &block) + test = self + mock_app do + test.settings = settings + register Sinatra::ConfigFile + set :root, File.expand_path('../config_file', __FILE__) + instance_eval(&block) if block + config_file(*args) + end + end + + it 'should set options from a simple config_file' do + config_file 'key_value.yml' + settings.foo.should == 'bar' + settings.something.should == 42 + end + + it 'should create indifferent hashes' do + config_file 'key_value.yml' + settings.nested['a'].should == 1 + settings.nested[:a].should == 1 + end + + it 'should recognize env specific settings per file' do + config_file 'with_envs.yml' + settings.foo.should == 'test' + end + + it 'should recognize env specific settings per setting' do + config_file 'with_nested_envs.yml' + settings.database[:adapter].should == 'sqlite' + end + + it 'should not set present values to nil if the current env is missing' do + # first let's check the test is actually working properly + config_file('missing_env.yml') { set :foo => 42, :environment => :production } + settings.foo.should == 10 + # now test it + config_file('missing_env.yml') { set :foo => 42, :environment => :test } + settings.foo.should == 42 + end +end From e6f15a288827e4484f366778dad31f22639af3c8 Mon Sep 17 00:00:00 2001 From: Konstantin Haase Date: Sat, 26 Mar 2011 00:02:03 +0100 Subject: [PATCH 09/14] add sinatra-respond-with --- sinatra-contrib/lib/sinatra/contrib.rb | 1 + sinatra-contrib/lib/sinatra/respond_with.rb | 154 +++++++++++ sinatra-contrib/sinatra-contrib.gemspec | 2 +- sinatra-contrib/spec/respond_with/bar.erb | 1 + .../spec/respond_with/bar.json.erb | 1 + .../spec/respond_with/foo.html.erb | 1 + .../spec/respond_with/not_html.sass | 2 + sinatra-contrib/spec/respond_with_spec.rb | 245 ++++++++++++++++++ 8 files changed, 406 insertions(+), 1 deletion(-) create mode 100644 sinatra-contrib/lib/sinatra/respond_with.rb create mode 100644 sinatra-contrib/spec/respond_with/bar.erb create mode 100644 sinatra-contrib/spec/respond_with/bar.json.erb create mode 100644 sinatra-contrib/spec/respond_with/foo.html.erb create mode 100644 sinatra-contrib/spec/respond_with/not_html.sass create mode 100644 sinatra-contrib/spec/respond_with_spec.rb diff --git a/sinatra-contrib/lib/sinatra/contrib.rb b/sinatra-contrib/lib/sinatra/contrib.rb index 7f33e5b3..197ecef9 100644 --- a/sinatra-contrib/lib/sinatra/contrib.rb +++ b/sinatra-contrib/lib/sinatra/contrib.rb @@ -10,6 +10,7 @@ module Sinatra register :ConfigFile register :Decompile register :Namespace + register :RespondWith helpers :LinkHeader end diff --git a/sinatra-contrib/lib/sinatra/respond_with.rb b/sinatra-contrib/lib/sinatra/respond_with.rb new file mode 100644 index 00000000..42f39064 --- /dev/null +++ b/sinatra-contrib/lib/sinatra/respond_with.rb @@ -0,0 +1,154 @@ +require 'sinatra/base' +require 'json' unless String.method_defined? :to_json + +module Sinatra + module RespondWith + class Format + def initialize(app) + @app, @map, @generic, @default = app, {}, {}, nil + end + + def on(type, &block) + @app.settings.mime_types(type).each do |mime| + case mime + when '*/*' then @default = block + when /^([^\/]+)\/\*$/ then @generic[$1] = block + else @map[mime] = block + end + end + end + + def finish + yield self if block_given? + mime_type = @app.content_type || + @app.request.preferred_type(@map.keys) || + @app.request.preferred_type || + 'text/html' + type = mime_type.split(/\s*;\s*/, 2).first + handlers = [@map[type], @generic[type[/^[^\/]+/]], @default].compact + handlers.each do |block| + if result = block.call(type) + @app.content_type mime_type + @app.halt result + end + end + @app.halt 406 + end + + def method_missing(meth, *args, &block) + return super if args.any? or block.nil? or not @app.mime_type(meth) + on(meth, &block) + end + end + + module Helpers + def respond_with(template, object = nil, &block) + object, template = template, nil unless Symbol === template + format = Format.new(self) + format.on "*/*" do |type| + exts = settings.ext_map[type] + exts << :xml if type.end_with? '+xml' + if template + args = template_cache.fetch(type, template) { template_for(template, exts) } + if args.any? + locals = { :object => object } + locals.merge! object.to_hash if object.respond_to? :to_hash + args << { :locals => locals } + halt send(*args) + end + end + if object + exts.each do |ext| + next unless meth = "to_#{ext}" and object.respond_to? meth + halt(*object.send(meth)) + end + end + false + end + format.finish(&block) + end + + def respond_to(&block) + Format.new(self).finish(&block) + end + + private + + def template_for(name, exts) + # in production this is cached, so don't worry to much about runtime + possible = [] + settings.template_engines[:all].each do |engine| + exts.each { |ext| possible << [engine, "#{name}.#{ext}"] } + end + exts.each do |ext| + settings.template_engines[ext].each { |e| possible << [e, name] } + end + possible.each do |engine, template| + find_template(settings.views, template, Tilt[engine]) do |file| + next unless File.exist? file + return settings.rendering_method(engine) << template.to_sym + end + end + [] # nil or false would not be cached + end + end + + attr_accessor :ext_map + + def remap_extensions + ext_map.clear + Rack::Mime::MIME_TYPES.each { |e,t| ext_map[t] << e[1..-1].to_sym } + ext_map['text/javascript'] << 'js' + ext_map['text/xml'] << 'xml' + end + + def mime_type(*) + result = super + remap_extensions + result + end + + def respond_to(*formats, &block) + if formats.any? + @respond_to ||= [] + @respond_to.concat formats + elsif @respond_to.nil? and superclass.respond_to? :respond_to + superclass.respond_to + else + @respond_to + end + end + + def rendering_method(engine) + return [engine] if Sinatra::Templates.method_defined? engine + return [:mab] if engine.to_sym == :markaby + [:render, :engine] + end + + private + + def compile!(verb, path, block, options = {}) + options[:provides] ||= respond_to if respond_to + super + end + + ENGINES = { + :css => [:less, :sass, :scss], + :xml => [:builder, :nokogiri], + :js => [:coffee], + :html => [:erb, :erubis, :haml, :slim, :liquid, :radius, :mab, :markdown, + :textile, :rdoc], + :all => Sinatra::Templates.instance_methods.map(&:to_sym) + [:mab] - + [:find_template, :markaby] + } + + ENGINES.default = [] + + def self.registered(base) + base.ext_map = Hash.new { |h,k| h[k] = [] } + base.set :template_engines, ENGINES.dup + base.remap_extensions + base.helpers Helpers + end + end +end diff --git a/sinatra-contrib/sinatra-contrib.gemspec b/sinatra-contrib/sinatra-contrib.gemspec index 1ffad82e..c40e54d5 100644 --- a/sinatra-contrib/sinatra-contrib.gemspec +++ b/sinatra-contrib/sinatra-contrib.gemspec @@ -10,7 +10,7 @@ Gem::Specification.new do |s| s.require_paths = ["lib"] s.summary = s.description - s.add_dependency "sinatra", "~> 1.2.0" + s.add_dependency "sinatra", "~> 1.2.2" s.add_dependency "backports", ">= 2.0" s.add_development_dependency "rspec", "~> 2.3" diff --git a/sinatra-contrib/spec/respond_with/bar.erb b/sinatra-contrib/spec/respond_with/bar.erb new file mode 100644 index 00000000..b4978795 --- /dev/null +++ b/sinatra-contrib/spec/respond_with/bar.erb @@ -0,0 +1 @@ +Girl! I wanna take you to a ... bar! \ No newline at end of file diff --git a/sinatra-contrib/spec/respond_with/bar.json.erb b/sinatra-contrib/spec/respond_with/bar.json.erb new file mode 100644 index 00000000..37f123d0 --- /dev/null +++ b/sinatra-contrib/spec/respond_with/bar.json.erb @@ -0,0 +1 @@ +json! \ No newline at end of file diff --git a/sinatra-contrib/spec/respond_with/foo.html.erb b/sinatra-contrib/spec/respond_with/foo.html.erb new file mode 100644 index 00000000..534038b0 --- /dev/null +++ b/sinatra-contrib/spec/respond_with/foo.html.erb @@ -0,0 +1 @@ +Hello <%= name %>! \ No newline at end of file diff --git a/sinatra-contrib/spec/respond_with/not_html.sass b/sinatra-contrib/spec/respond_with/not_html.sass new file mode 100644 index 00000000..50f8864f --- /dev/null +++ b/sinatra-contrib/spec/respond_with/not_html.sass @@ -0,0 +1,2 @@ +body + color: red diff --git a/sinatra-contrib/spec/respond_with_spec.rb b/sinatra-contrib/spec/respond_with_spec.rb new file mode 100644 index 00000000..026c508f --- /dev/null +++ b/sinatra-contrib/spec/respond_with_spec.rb @@ -0,0 +1,245 @@ +require 'backports' +require_relative 'spec_helper' + +describe Sinatra::RespondWith do + def provides(*args) + @provides = args + end + + def respond_app(&block) + types = @provides + mock_app do + set :app_file, __FILE__ + set :views, root + '/respond_with' + register Sinatra::RespondWith + respond_to(*types) if types + class_eval(&block) + end + end + + def respond_to(*args, &block) + respond_app { get('/') { respond_to(*args, &block) } } + end + + def respond_with(*args, &block) + respond_app { get('/') { respond_with(*args, &block) } } + end + + def req(*types) + p = types.shift if types.first.is_a? String and types.first.start_with? '/' + accept = types.map { |t| Sinatra::Base.mime_type(t).to_s }.join ',' + get (p || '/'), {}, 'HTTP_ACCEPT' => accept + end + + describe "Helpers#respond_to" do + it 'allows defining handlers by file extensions' do + respond_to do |format| + format.html { "html!" } + format.json { "json!" } + end + + req(:html).body.should == "html!" + req(:json).body.should == "json!" + end + + it 'respects quality' do + respond_to do |format| + format.html { "html!" } + format.json { "json!" } + end + + req("text/html;q=0.7, application/json;q=0.3").body.should == "html!" + req("text/html;q=0.3, application/json;q=0.7").body.should == "json!" + end + + it 'allows using mime types' do + respond_to do |format| + format.on('text/html') { "html!" } + format.json { "json!" } + end + + req(:html).body.should == "html!" + end + + it 'allows using wildcards in format matchers' do + respond_to do |format| + format.on('text/*') { "text!" } + format.json { "json!" } + end + + req(:html).body.should == "text!" + end + + it 'allows using catch all wildcards in format matchers' do + respond_to do |format| + format.on('*/*') { "anything!" } + format.json { "json!" } + end + + req(:html).body.should == "anything!" + end + + it 'prefers concret over generic' do + respond_to do |format| + format.on('text/*') { "text!" } + format.on('*/*') { "anything!" } + format.json { "json!" } + end + + req(:json).body.should == "json!" + req(:html).body.should == "text!" + end + + it 'does not set up default handlers' do + respond_to + req.should_not be_ok + status.should == 406 + end + end + + describe "Helpers#respond_with" do + describe "matching" do + it 'allows defining handlers by file extensions' do + respond_with(:ignore) do |format| + format.html { "html!" } + format.json { "json!" } + end + + req(:html).body.should == "html!" + req(:json).body.should == "json!" + end + + it 'respects quality' do + respond_with(:ignore) do |format| + format.html { "html!" } + format.json { "json!" } + end + + req("text/html;q=0.7, application/json;q=0.3").body.should == "html!" + req("text/html;q=0.3, application/json;q=0.7").body.should == "json!" + end + + it 'allows using mime types' do + respond_with(:ignore) do |format| + format.on('text/html') { "html!" } + format.json { "json!" } + end + + req(:html).body.should == "html!" + end + + it 'allows using wildcards in format matchers' do + respond_with(:ignore) do |format| + format.on('text/*') { "text!" } + format.json { "json!" } + end + + req(:html).body.should == "text!" + end + + it 'allows using catch all wildcards in format matchers' do + respond_with(:ignore) do |format| + format.on('*/*') { "anything!" } + format.json { "json!" } + end + + req(:html).body.should == "anything!" + end + + it 'prefers concret over generic' do + respond_with(:ignore) do |format| + format.on('text/*') { "text!" } + format.on('*/*') { "anything!" } + format.json { "json!" } + end + + req(:json).body.should == "json!" + req(:html).body.should == "text!" + end + end + + describe "default behavior" do + it 'converts objects to json out of the box' do + respond_with 'a' => 'b' + req(:json).body.should == {'a' => 'b'}.to_json + end + + it 'handles multiple routes correctly' do + respond_app do + get('/') { respond_with 'a' => 'b' } + get('/:name') { respond_with 'a' => params[:name] } + end + req('/', :json).body.should == {'a' => 'b'}.to_json + req('/b', :json).body.should == {'a' => 'b'}.to_json + req('/c', :json).body.should == {'a' => 'c'}.to_json + end + + it "calls to_EXT if available" do + respond_with Struct.new(:to_pdf).new("hello") + req(:pdf).body.should == "hello" + end + + it 'results in a 406 if format cannot be produced' do + respond_with({}) + req(:html).should_not be_ok + status.should == 406 + end + end + + describe 'templates' do + it 'looks for templates with name.target.engine' do + respond_with :foo, :name => 'World' + req(:html).should be_ok + body.should == "Hello World!" + end + + it 'looks for templates with name.engine for specific engines' do + respond_with :bar + req(:html).should be_ok + body.should == "Girl! I wanna take you to a ... bar!" + end + + it 'does not use name.engine for engines producing other formats' do + respond_with :not_html + req(:html).should_not be_ok + status.should == 406 + body.should be_empty + end + + it 'falls back to to_EXT if no template is found' do + respond_with :foo, :name => 'World' + req(:json).should be_ok + body.should == {:name => 'World'}.to_json + end + + it 'favors templates over to_EXT' do + respond_with :bar, :name => 'World' + req(:json).should be_ok + body.should == 'json!' + end + end + + describe 'customizing' do + it 'allows customizing' do + respond_with(:foo, :name => 'World') { |f| f.html { 'html!' }} + req(:html).should be_ok + body.should == "html!" + end + + it 'falls back to default behavior if none matches' do + respond_with(:foo, :name => 'World') { |f| f.json { 'json!' }} + req(:html).should be_ok + body.should == "Hello World!" + end + + it 'favors generic rule over default behavior' do + respond_with(:foo, :name => 'World') { |f| f.on('*/*') { 'generic!' }} + req(:html).should be_ok + body.should == "generic!" + end + end + end + + describe :respond_to do + end +end From 18e379f1cf853344907a7dcbe3499acfbe7adb45 Mon Sep 17 00:00:00 2001 From: Konstantin Haase Date: Sat, 26 Mar 2011 00:13:26 +0100 Subject: [PATCH 10/14] add reimplementation of sinatra-extension --- sinatra-contrib/lib/sinatra/contrib.rb | 1 + sinatra-contrib/lib/sinatra/extension.rb | 50 +++++++++++++++++++++ sinatra-contrib/lib/sinatra/test_helpers.rb | 9 +++- sinatra-contrib/spec/config_file_spec.rb | 3 -- sinatra-contrib/spec/extension_spec.rb | 33 ++++++++++++++ 5 files changed, 92 insertions(+), 4 deletions(-) create mode 100644 sinatra-contrib/lib/sinatra/extension.rb create mode 100644 sinatra-contrib/spec/extension_spec.rb diff --git a/sinatra-contrib/lib/sinatra/contrib.rb b/sinatra-contrib/lib/sinatra/contrib.rb index 197ecef9..5325d54e 100644 --- a/sinatra-contrib/lib/sinatra/contrib.rb +++ b/sinatra-contrib/lib/sinatra/contrib.rb @@ -21,6 +21,7 @@ module Sinatra ## # Stuff that aren't Sinatra extensions, technically. + autoload :Extension autoload :TestHelpers end diff --git a/sinatra-contrib/lib/sinatra/extension.rb b/sinatra-contrib/lib/sinatra/extension.rb new file mode 100644 index 00000000..6ab50307 --- /dev/null +++ b/sinatra-contrib/lib/sinatra/extension.rb @@ -0,0 +1,50 @@ +require 'sinatra/base' +require 'backports/basic_object' unless defined? BasicObject + +module Sinatra + module Extension + def self.new(&block) + ext = Module.new.extend(self) + ext.class_eval(&block) + ext + end + + def settings + self + end + + def configure(*args, &block) + record(:configure, *args) { |c| c.instance_exec(c, &block) } + end + + def registered(base = nil, &block) + base ? replay(base) : record(:class_eval, &block) + end + + private + + def record(method, *args, &block) + recorded_methods << [method, args, block] + end + + def replay(object) + recorded_methods.each { |m, a, b| object.send(m, *a, &b) } + end + + def recorded_methods + @recorded_methods ||= [] + end + + def method_missing(method, *args, &block) + return super unless Sinatra::Base.respond_to? method + record(method, *args, &block) + DontCall.new(method) + end + + class DontCall < BasicObject + def initialize(method) @method = method end + def method_missing(*) fail "not supposed to use result of #@method!" end + def inspect; "#<#{self.class}: #{@method}>" end + end + end +end diff --git a/sinatra-contrib/lib/sinatra/test_helpers.rb b/sinatra-contrib/lib/sinatra/test_helpers.rb index c7454ca0..16316a3f 100644 --- a/sinatra-contrib/lib/sinatra/test_helpers.rb +++ b/sinatra-contrib/lib/sinatra/test_helpers.rb @@ -21,13 +21,20 @@ module Sinatra include Rack::Test::Methods extend Forwardable + attr_accessor :settings def_delegators :last_response, :body, :headers, :status, :errors def_delegators :app, :configure, :set, :enable, :disable, :use, :helpers, :register def_delegators :current_session, :env_for def mock_app(base = Sinatra::Base, &block) - @app = Sinatra.new(base, &block) + inner = nil + @app = Sinatra.new(base) do + inner = self + class_eval(&block) + end + @settings = inner + app end def app=(base) diff --git a/sinatra-contrib/spec/config_file_spec.rb b/sinatra-contrib/spec/config_file_spec.rb index 30da04d9..02f09876 100644 --- a/sinatra-contrib/spec/config_file_spec.rb +++ b/sinatra-contrib/spec/config_file_spec.rb @@ -2,11 +2,8 @@ require 'backports' require_relative 'spec_helper' describe Sinatra::ConfigFile do - attr_accessor :settings def config_file(*args, &block) - test = self mock_app do - test.settings = settings register Sinatra::ConfigFile set :root, File.expand_path('../config_file', __FILE__) instance_eval(&block) if block diff --git a/sinatra-contrib/spec/extension_spec.rb b/sinatra-contrib/spec/extension_spec.rb new file mode 100644 index 00000000..01883054 --- /dev/null +++ b/sinatra-contrib/spec/extension_spec.rb @@ -0,0 +1,33 @@ +require 'backports' +require_relative 'spec_helper' + +describe Sinatra::Extension do + module ExampleExtension + extend Sinatra::Extension + + set :foo, :bar + settings.set :bar, :blah + + configure :test, :production do + set :reload_stuff, false + end + + configure :development do + set :reload_stuff, true + end + + get '/' do + "from extension, yay" + end + end + + before { mock_app { register ExampleExtension }} + + it('allows using set') { settings.foo.should == :bar } + it('implements configure') { settings.reload_stuff.should be_false } + + it 'allows defing routes' do + get('/').should be_ok + body.should == "from extension, yay" + end +end From 503250fd6784930450a4fea20cb163e53bc46b51 Mon Sep 17 00:00:00 2001 From: Konstantin Haase Date: Sat, 26 Mar 2011 00:16:40 +0100 Subject: [PATCH 11/14] add tests for Sinatra::RespondWith#respond_to --- sinatra-contrib/spec/respond_with_spec.rb | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/sinatra-contrib/spec/respond_with_spec.rb b/sinatra-contrib/spec/respond_with_spec.rb index 026c508f..3b7ee951 100644 --- a/sinatra-contrib/spec/respond_with_spec.rb +++ b/sinatra-contrib/spec/respond_with_spec.rb @@ -241,5 +241,26 @@ describe Sinatra::RespondWith do end describe :respond_to do + it 'acts as global provides condition' do + respond_app do + respond_to :json, :html + get('/a') { 'ok' } + get('/b') { 'ok' } + end + + req('/b', :xml).should_not be_ok + req('/b', :html).should be_ok + end + + it 'still allows provides' do + respond_app do + respond_to :json, :html + get('/a') { 'ok' } + get('/b', :provides => :json) { 'ok' } + end + + req('/b', :html).should_not be_ok + req('/b', :json).should be_ok + end end end From 31cdac9d0ed4feac1c77ca61bf178528e9ea2515 Mon Sep 17 00:00:00 2001 From: Konstantin Haase Date: Sat, 26 Mar 2011 00:22:19 +0100 Subject: [PATCH 12/14] make respond-with play nice with namespace --- sinatra-contrib/lib/sinatra/namespace.rb | 5 +++++ sinatra-contrib/lib/sinatra/respond_with.rb | 2 +- sinatra-contrib/spec/respond_with_spec.rb | 14 ++++++++++++++ 3 files changed, 20 insertions(+), 1 deletion(-) diff --git a/sinatra-contrib/lib/sinatra/namespace.rb b/sinatra-contrib/lib/sinatra/namespace.rb index 3e846196..8bca9253 100644 --- a/sinatra-contrib/lib/sinatra/namespace.rb +++ b/sinatra-contrib/lib/sinatra/namespace.rb @@ -66,6 +66,11 @@ module Sinatra [*codes].each { |c| errors[c] = block } end + def respond_to(*args) + return @conditions[:provides] || base.respond_to if args.empty? + @conditions[:provides] = args + end + private def app diff --git a/sinatra-contrib/lib/sinatra/respond_with.rb b/sinatra-contrib/lib/sinatra/respond_with.rb index 42f39064..e5a6d122 100644 --- a/sinatra-contrib/lib/sinatra/respond_with.rb +++ b/sinatra-contrib/lib/sinatra/respond_with.rb @@ -108,7 +108,7 @@ module Sinatra result end - def respond_to(*formats, &block) + def respond_to(*formats) if formats.any? @respond_to ||= [] @respond_to.concat formats diff --git a/sinatra-contrib/spec/respond_with_spec.rb b/sinatra-contrib/spec/respond_with_spec.rb index 3b7ee951..e00f060a 100644 --- a/sinatra-contrib/spec/respond_with_spec.rb +++ b/sinatra-contrib/spec/respond_with_spec.rb @@ -262,5 +262,19 @@ describe Sinatra::RespondWith do req('/b', :html).should_not be_ok req('/b', :json).should be_ok end + + it 'plays well with namespaces' do + respond_app do + register Sinatra::Namespace + namespace '/a' do + respond_to :json + get { 'json' } + end + get('/b') { 'anything' } + end + + req('/a', :html).should_not be_ok + req('/b', :html).should be_ok + end end end From fb488b9867f9854c0bb412a35eff80b05af3d186 Mon Sep 17 00:00:00 2001 From: Konstantin Haase Date: Sat, 26 Mar 2011 00:39:16 +0100 Subject: [PATCH 13/14] trigger route_added hooks for namespace-only extensions --- sinatra-contrib/lib/sinatra/namespace.rb | 13 ++++++- sinatra-contrib/spec/namespace_spec.rb | 46 ++++++++++++++++++++++-- 2 files changed, 55 insertions(+), 4 deletions(-) diff --git a/sinatra-contrib/lib/sinatra/namespace.rb b/sinatra-contrib/lib/sinatra/namespace.rb index 8bca9253..c3f46773 100644 --- a/sinatra-contrib/lib/sinatra/namespace.rb +++ b/sinatra-contrib/lib/sinatra/namespace.rb @@ -54,6 +54,10 @@ module Sinatra end end + def invoke_hook(name, *args) + @extensions.each { |e| e.send(name, *args) if e.respond_to?(name) } + end + def errors @errors ||= {} end @@ -108,7 +112,14 @@ module Sinatra def prefixed(method, pattern = nil, conditions = {}, &block) default = '*' if method == :before or method == :after pattern, conditions = compile pattern, conditions, default - base.send(method, pattern, conditions, &block) + result = base.send(method, pattern, conditions, &block) + invoke_hook :route_added, method.to_s.upcase, pattern, block + result + end + + def method_missing(meth, *args, &block) + return super if args.any? or block or not base.respond_to? meth + base.send meth end end diff --git a/sinatra-contrib/spec/namespace_spec.rb b/sinatra-contrib/spec/namespace_spec.rb index 537d3ed6..919f6765 100644 --- a/sinatra-contrib/spec/namespace_spec.rb +++ b/sinatra-contrib/spec/namespace_spec.rb @@ -496,9 +496,49 @@ describe Sinatra::Namespace do end describe 'extensions' do - it 'allows registering extensions for a namespace only' - it 'triggers route_added hook' - it 'prevents changing app global settings' + it 'allows read access to settings' do + value = nil + mock_app do + set :foo, 42 + namespace '/foo' do + value = foo + end + end + value.should == 42 + end + + it 'allows registering extensions for a namespace only' do + a = b = nil + extension = Module.new { define_method(:views) { "CUSTOM!!!" } } + mock_app do + namespace '/' do + register extension + a = views + end + b = views + end + a.should == 'CUSTOM!!!' + b.should_not == 'CUSTOM!!!' + end + + it 'triggers route_added hook' do + route = nil + extension = Module.new + extension.singleton_class.class_eval do + define_method(:route_added) { |*r| route = r } + end + mock_app do + namespace '/f' do + register extension + get('oo') { } + end + get('/bar') { } + end + route[1].should == '/foo' + end + + it 'prevents changing app global settings' do + end end end end From 676d217a3be507937240bd2eba41425af7c3755a Mon Sep 17 00:00:00 2001 From: Konstantin Haase Date: Sat, 26 Mar 2011 00:40:06 +0100 Subject: [PATCH 14/14] add test for raising and error when trying to change app global settings from a namespace --- sinatra-contrib/spec/namespace_spec.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/sinatra-contrib/spec/namespace_spec.rb b/sinatra-contrib/spec/namespace_spec.rb index 919f6765..2cfb9c18 100644 --- a/sinatra-contrib/spec/namespace_spec.rb +++ b/sinatra-contrib/spec/namespace_spec.rb @@ -538,6 +538,7 @@ describe Sinatra::Namespace do end it 'prevents changing app global settings' do + proc { namespace('/') { set :foo, :bar }}.should raise_error end end end