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/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/lib/sinatra/contrib.rb b/sinatra-contrib/lib/sinatra/contrib.rb
new file mode 100644
index 00000000..5325d54e
--- /dev/null
+++ b/sinatra-contrib/lib/sinatra/contrib.rb
@@ -0,0 +1,29 @@
+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
+ register :RespondWith
+ 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 :Extension
+ 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
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/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/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/lib/sinatra/namespace.rb b/sinatra-contrib/lib/sinatra/namespace.rb
new file mode 100644
index 00000000..c3f46773
--- /dev/null
+++ b/sinatra-contrib/lib/sinatra/namespace.rb
@@ -0,0 +1,137 @@
+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 }
+ 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
+
+ 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
+
+ def invoke_hook(name, *args)
+ @extensions.each { |e| e.send(name, *args) if e.respond_to?(name) }
+ 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
+
+ def respond_to(*args)
+ return @conditions[:provides] || base.respond_to if args.empty?
+ @conditions[:provides] = args
+ 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
+ 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
+
+ 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/lib/sinatra/respond_with.rb b/sinatra-contrib/lib/sinatra/respond_with.rb
new file mode 100644
index 00000000..e5a6d122
--- /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)
+ 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/lib/sinatra/test_helpers.rb b/sinatra-contrib/lib/sinatra/test_helpers.rb
new file mode 100644
index 00000000..16316a3f
--- /dev/null
+++ b/sinatra-contrib/lib/sinatra/test_helpers.rb
@@ -0,0 +1,86 @@
+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
+ 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)
+ inner = nil
+ @app = Sinatra.new(base) do
+ inner = self
+ class_eval(&block)
+ end
+ @settings = inner
+ app
+ 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/sinatra-contrib.gemspec b/sinatra-contrib/sinatra-contrib.gemspec
new file mode 100644
index 00000000..c40e54d5
--- /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.2"
+ s.add_dependency "backports", ">= 2.0"
+
+ s.add_development_dependency "rspec", "~> 2.3"
+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..02f09876
--- /dev/null
+++ b/sinatra-contrib/spec/config_file_spec.rb
@@ -0,0 +1,44 @@
+require 'backports'
+require_relative 'spec_helper'
+
+describe Sinatra::ConfigFile do
+ def config_file(*args, &block)
+ mock_app do
+ 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
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/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
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
diff --git a/sinatra-contrib/spec/namespace_spec.rb b/sinatra-contrib/spec/namespace_spec.rb
new file mode 100644
index 00000000..2cfb9c18
--- /dev/null
+++ b/sinatra-contrib/spec/namespace_spec.rb
@@ -0,0 +1,546 @@
+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" 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
+ 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 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
+ proc { namespace('/') { set :foo, :bar }}.should raise_error
+ end
+ end
+ end
+ end
+end
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..e00f060a
--- /dev/null
+++ b/sinatra-contrib/spec/respond_with_spec.rb
@@ -0,0 +1,280 @@
+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
+ 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
+
+ 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
diff --git a/sinatra-contrib/spec/spec_helper.rb b/sinatra-contrib/spec/spec_helper.rb
new file mode 100644
index 00000000..cfd75b6d
--- /dev/null
+++ b/sinatra-contrib/spec/spec_helper.rb
@@ -0,0 +1,6 @@
+require 'sinatra/contrib'
+
+RSpec.configure do |config|
+ config.expect_with :rspec, :stdlib
+ config.include Sinatra::TestHelpers
+end