roda-cj 0.9.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (51) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG +13 -0
  3. data/MIT-LICENSE +20 -0
  4. data/README.rdoc +715 -0
  5. data/Rakefile +124 -0
  6. data/lib/roda/plugins/all_verbs.rb +48 -0
  7. data/lib/roda/plugins/default_headers.rb +50 -0
  8. data/lib/roda/plugins/error_handler.rb +69 -0
  9. data/lib/roda/plugins/flash.rb +108 -0
  10. data/lib/roda/plugins/h.rb +24 -0
  11. data/lib/roda/plugins/halt.rb +79 -0
  12. data/lib/roda/plugins/header_matchers.rb +57 -0
  13. data/lib/roda/plugins/hooks.rb +106 -0
  14. data/lib/roda/plugins/indifferent_params.rb +47 -0
  15. data/lib/roda/plugins/middleware.rb +88 -0
  16. data/lib/roda/plugins/multi_route.rb +77 -0
  17. data/lib/roda/plugins/not_found.rb +62 -0
  18. data/lib/roda/plugins/pass.rb +34 -0
  19. data/lib/roda/plugins/render.rb +217 -0
  20. data/lib/roda/plugins/streaming.rb +165 -0
  21. data/lib/roda/version.rb +3 -0
  22. data/lib/roda.rb +610 -0
  23. data/spec/composition_spec.rb +19 -0
  24. data/spec/env_spec.rb +11 -0
  25. data/spec/integration_spec.rb +63 -0
  26. data/spec/matchers_spec.rb +683 -0
  27. data/spec/module_spec.rb +29 -0
  28. data/spec/opts_spec.rb +42 -0
  29. data/spec/plugin/all_verbs_spec.rb +29 -0
  30. data/spec/plugin/default_headers_spec.rb +63 -0
  31. data/spec/plugin/error_handler_spec.rb +67 -0
  32. data/spec/plugin/flash_spec.rb +123 -0
  33. data/spec/plugin/h_spec.rb +13 -0
  34. data/spec/plugin/halt_spec.rb +62 -0
  35. data/spec/plugin/header_matchers_spec.rb +61 -0
  36. data/spec/plugin/hooks_spec.rb +97 -0
  37. data/spec/plugin/indifferent_params_spec.rb +13 -0
  38. data/spec/plugin/middleware_spec.rb +52 -0
  39. data/spec/plugin/multi_route_spec.rb +98 -0
  40. data/spec/plugin/not_found_spec.rb +99 -0
  41. data/spec/plugin/pass_spec.rb +23 -0
  42. data/spec/plugin/render_spec.rb +148 -0
  43. data/spec/plugin/streaming_spec.rb +52 -0
  44. data/spec/plugin_spec.rb +61 -0
  45. data/spec/redirect_spec.rb +24 -0
  46. data/spec/request_spec.rb +55 -0
  47. data/spec/response_spec.rb +131 -0
  48. data/spec/session_spec.rb +35 -0
  49. data/spec/spec_helper.rb +89 -0
  50. data/spec/version_spec.rb +8 -0
  51. metadata +136 -0
data/Rakefile ADDED
@@ -0,0 +1,124 @@
1
+ require "rake"
2
+ require "rake/clean"
3
+
4
+ NAME = 'roda'
5
+ VERS = lambda do
6
+ require File.expand_path("../lib/roda/version.rb", __FILE__)
7
+ Roda::RodaVersion
8
+ end
9
+ CLEAN.include ["#{NAME}-*.gem", "rdoc", "coverage", "www/public/*.html", "www/public/rdoc"]
10
+
11
+ # Gem Packaging and Release
12
+
13
+ desc "Packages #{NAME}"
14
+ task :package=>[:clean] do |p|
15
+ sh %{gem build #{NAME}.gemspec}
16
+ end
17
+
18
+ ### RDoc
19
+
20
+ RDOC_DEFAULT_OPTS = ["--line-numbers", "--inline-source", '--title', 'Roda: Routing tree web framework']
21
+
22
+ begin
23
+ gem 'hanna-nouveau'
24
+ RDOC_DEFAULT_OPTS.concat(['-f', 'hanna'])
25
+ rescue Gem::LoadError
26
+ end
27
+
28
+ rdoc_task_class = begin
29
+ require "rdoc/task"
30
+ RDoc::Task
31
+ rescue LoadError
32
+ require "rake/rdoctask"
33
+ Rake::RDocTask
34
+ end
35
+
36
+ RDOC_OPTS = RDOC_DEFAULT_OPTS + ['--main', 'README.rdoc']
37
+ RDOC_FILES = %w"README.rdoc CHANGELOG MIT-LICENSE lib/**/*.rb"
38
+
39
+ rdoc_task_class.new do |rdoc|
40
+ rdoc.rdoc_dir = "rdoc"
41
+ rdoc.options += RDOC_OPTS
42
+ rdoc.rdoc_files.add RDOC_FILES
43
+ end
44
+
45
+ rdoc_task_class.new(:website_rdoc) do |rdoc|
46
+ rdoc.rdoc_dir = "www/public/rdoc"
47
+ rdoc.options += RDOC_OPTS
48
+ rdoc.rdoc_files.add RDOC_FILES
49
+ end
50
+
51
+ ### Website
52
+
53
+ desc "Make local version of website"
54
+ task :website_base do
55
+ sh %{#{FileUtils::RUBY} www/make_www.rb}
56
+ end
57
+
58
+ desc "Make local version of website, with rdoc"
59
+ task :website => [:website_base, :website_rdoc]
60
+
61
+ desc "Make local version of website"
62
+ task :serve => :website do
63
+ sh %{#{FileUtils::RUBY} -C www -S rackup}
64
+ end
65
+
66
+
67
+ ### Specs
68
+
69
+ begin
70
+ begin
71
+ # RSpec 1
72
+ require "spec/rake/spectask"
73
+ spec_class = Spec::Rake::SpecTask
74
+ spec_files_meth = :spec_files=
75
+ rescue LoadError
76
+ # RSpec 2
77
+ require "rspec/core/rake_task"
78
+ spec_class = RSpec::Core::RakeTask
79
+ spec_files_meth = :pattern=
80
+ end
81
+
82
+ spec = lambda do |name, files, d|
83
+ lib_dir = File.join(File.dirname(File.expand_path(__FILE__)), 'lib')
84
+ ENV['RUBYLIB'] ? (ENV['RUBYLIB'] += ":#{lib_dir}") : (ENV['RUBYLIB'] = lib_dir)
85
+ desc d
86
+ spec_class.new(name) do |t|
87
+ t.send spec_files_meth, files
88
+ t.spec_opts = ENV["#{NAME.upcase}_SPEC_OPTS"].split if ENV["#{NAME.upcase}_SPEC_OPTS"]
89
+ end
90
+ end
91
+
92
+ spec_with_cov = lambda do |name, files, d|
93
+ spec.call(name, files, d)
94
+ desc "#{d} with coverage"
95
+ task "#{name}_cov" do
96
+ ENV['COVERAGE'] = '1'
97
+ Rake::Task[name].invoke
98
+ end
99
+ end
100
+
101
+ task :default => [:spec]
102
+ spec_with_cov.call("spec", Dir["spec/*_spec.rb"] + Dir["spec/plugin/*_spec.rb"], "Run specs")
103
+ rescue LoadError
104
+ task :default do
105
+ puts "Must install rspec to run the default task (which runs specs)"
106
+ end
107
+ end
108
+
109
+ ### Other
110
+
111
+ desc "Print #{NAME} version"
112
+ task :version do
113
+ puts VERS.call
114
+ end
115
+
116
+ desc "Start an IRB shell using the extension"
117
+ task :irb do
118
+ require 'rbconfig'
119
+ ruby = ENV['RUBY'] || File.join(RbConfig::CONFIG['bindir'], RbConfig::CONFIG['ruby_install_name'])
120
+ irb = ENV['IRB'] || File.join(RbConfig::CONFIG['bindir'], File.basename(ruby).sub('ruby', 'irb'))
121
+ sh %{#{irb} -I lib -r #{NAME}}
122
+ end
123
+
124
+
@@ -0,0 +1,48 @@
1
+ class Roda
2
+ module RodaPlugins
3
+ # The all_verbs plugin adds methods for http verbs other than
4
+ # get and post. The following verbs are added, assuming
5
+ # rack handles them: delete, head, options, link, patch, put,
6
+ # trace, unlink.
7
+ #
8
+ # These methods operate just like Roda's default get and post
9
+ # methods other that the http verb used, so using them without
10
+ # any arguments just checks for the request method, while
11
+ # using them with any arguments also checks that the arguments
12
+ # match the full path.
13
+ #
14
+ # Example:
15
+ #
16
+ # plugin :all_verbs
17
+ #
18
+ # route do |r|
19
+ # r.delete
20
+ # # Handle DELETE
21
+ # end
22
+ # r.put do
23
+ # # Handle PUT
24
+ # end
25
+ # r.patch do
26
+ # # Handle PATCH
27
+ # end
28
+ # end
29
+ #
30
+ # The verb methods are defined via metaprogramming, so there
31
+ # isn't documentation for the individual methods created.
32
+ module AllVerbs
33
+ module RequestMethods
34
+ %w'delete head options link patch put trace unlink'.each do |t|
35
+ if ::Rack::Request.method_defined?("#{t}?")
36
+ class_eval(<<-END, __FILE__, __LINE__+1)
37
+ def #{t}(*args, &block)
38
+ is_or_on(*args, &block) if #{t}?
39
+ end
40
+ END
41
+ end
42
+ end
43
+ end
44
+ end
45
+
46
+ register_plugin(:all_verbs, AllVerbs)
47
+ end
48
+ end
@@ -0,0 +1,50 @@
1
+ class Roda
2
+ module RodaPlugins
3
+ # The default_headers plugin accepts a hash of headers,
4
+ # and overrides the default_headers method in the
5
+ # response class to be a copy of the headers.
6
+ #
7
+ # Note that when using this module, you should not
8
+ # attempt to mutate of the values set in the default
9
+ # headers hash.
10
+ #
11
+ # Example:
12
+ #
13
+ # plugin :default_headers, 'Content-Type'=>'text/csv'
14
+ #
15
+ # You can also modify the default headers later:
16
+ #
17
+ # plugin :default_headers
18
+ # default_headers['Foo'] = 'bar'
19
+ # default_headers.merge!('Bar'=>'baz')
20
+ module DefaultHeaders
21
+ # Merge the given headers into the existing default headers, if any.
22
+ def self.configure(app, headers={})
23
+ app.instance_eval do
24
+ @default_headers ||= {}
25
+ @default_headers.merge!(headers)
26
+ end
27
+ end
28
+
29
+ module ClassMethods
30
+ # The default response headers to use for the current class.
31
+ attr_reader :default_headers
32
+
33
+ # Copy the default headers into the subclass when inheriting.
34
+ def inherited(subclass)
35
+ super
36
+ subclass.instance_variable_set(:@default_headers, default_headers.dup)
37
+ end
38
+ end
39
+
40
+ module ResponseMethods
41
+ # Get the default headers from the related roda class.
42
+ def default_headers
43
+ self.class.roda_class.default_headers.dup
44
+ end
45
+ end
46
+ end
47
+
48
+ register_plugin(:default_headers, DefaultHeaders)
49
+ end
50
+ end
@@ -0,0 +1,69 @@
1
+ class Roda
2
+ module RodaPlugins
3
+ # The error_handler plugin adds an error handler to the routing,
4
+ # so that if routing the request raises an error, a nice error
5
+ # message page can be returned to the user.
6
+ #
7
+ # You can provide the error handler as a block to the plugin:
8
+ #
9
+ # plugin :error_handler do |e|
10
+ # "Oh No!"
11
+ # end
12
+ #
13
+ # Or later via the +error+ class method:
14
+ #
15
+ # plugin :error_handler
16
+ #
17
+ # error do |e|
18
+ # "Oh No!"
19
+ # end
20
+ #
21
+ # In both cases, the exception instance is passed into the block,
22
+ # and the block can return the request body via a string.
23
+ #
24
+ # If an exception is raised, the response status will be set to 500
25
+ # before executing the error handler. The error handler can change
26
+ # the response status if necessary.
27
+ module ErrorHandler
28
+ # If a block is given, automatically call the +error+ method on
29
+ # the Roda class with it.
30
+ def self.configure(app, &block)
31
+ if block
32
+ app.error(&block)
33
+ end
34
+ end
35
+
36
+ module ClassMethods
37
+ # Install the given block as the error handler, so that if routing
38
+ # the request raises an exception, the block will be called with
39
+ # the exception in the scope of the Roda instance.
40
+ def error(&block)
41
+ define_method(:handle_error, &block)
42
+ private :handle_error
43
+ end
44
+ end
45
+
46
+ module InstanceMethods
47
+ private
48
+
49
+ # If an error occurs, set the response status to 500 and call
50
+ # the error handler.
51
+ def _route
52
+ super
53
+ rescue => e
54
+ response.status = 500
55
+ super{handle_error(e)}
56
+ end
57
+
58
+ # By default, have the error handler reraise the error, so using
59
+ # the plugin without installing an error handler doesn't change
60
+ # behavior.
61
+ def handle_error(e)
62
+ raise e
63
+ end
64
+ end
65
+ end
66
+
67
+ register_plugin(:error_handler, ErrorHandler)
68
+ end
69
+ end
@@ -0,0 +1,108 @@
1
+ require 'delegate'
2
+
3
+ class Roda
4
+ module RodaPlugins
5
+ # The flash plugin adds a +flash+ instance method to Roda,
6
+ # for typical web application flash handling, where values
7
+ # set in the current flash hash are available in the next
8
+ # request.
9
+ #
10
+ # With the example below, if a POST request is submitted,
11
+ # it will redirect and the resulting GET request will
12
+ # return 'b'.
13
+ #
14
+ # plugin :flash
15
+ #
16
+ # route do |r|
17
+ # r.is '' do
18
+ # r.get do
19
+ # flash['a']
20
+ # end
21
+ #
22
+ # r.post do
23
+ # flash['a'] = 'b'
24
+ # r.redirect('')
25
+ # end
26
+ # end
27
+ # end
28
+ module Flash
29
+ # Simple flash hash, where assiging to the hash updates the flash
30
+ # used in the following request.
31
+ class FlashHash < DelegateClass(Hash)
32
+ # The flash hash for the next request. This
33
+ # is what gets written to by #[]=.
34
+ attr_reader :next
35
+
36
+ # The flash hash for the current request
37
+ alias now __getobj__
38
+
39
+ # Setup the next hash when initializing, and handle treat nil
40
+ # as a new empty hash.
41
+ def initialize(hash={})
42
+ super(hash||{})
43
+ @next = {}
44
+ end
45
+
46
+ # Update the next hash with the given key and value.
47
+ def []=(k, v)
48
+ @next[k] = v
49
+ end
50
+
51
+ # Remove given key from the next hash, or clear the next hash if
52
+ # no argument is given.
53
+ def discard(key=(no_arg=true))
54
+ if no_arg
55
+ @next.clear
56
+ else
57
+ @next.delete(key)
58
+ end
59
+ end
60
+
61
+ # Copy the entry with the given key from the current hash to the
62
+ # next hash, or copy all entries from the current hash to the
63
+ # next hash if no argument is given.
64
+ def keep(key=(no_arg=true))
65
+ if no_arg
66
+ @next.merge!(self)
67
+ else
68
+ self[key] = self[key]
69
+ end
70
+ end
71
+
72
+ # Replace the current hash with the next hash and clear the next hash.
73
+ def sweep
74
+ replace(@next)
75
+ @next.clear
76
+ self
77
+ end
78
+ end
79
+
80
+ module InstanceMethods
81
+ # The internal session key used to store the flash.
82
+ KEY = :_flash
83
+
84
+ # Access the flash hash for the current request, loading
85
+ # it from the session if it is not already loaded.
86
+ def flash
87
+ @_flash ||= FlashHash.new(session[KEY])
88
+ end
89
+
90
+ private
91
+
92
+ # If the routing doesn't raise an error, rotate the flash
93
+ # hash in the session so the next request has access to it.
94
+ def _route
95
+ res = super
96
+
97
+ if f = @_flash
98
+ session[KEY] = f.next
99
+ end
100
+
101
+ res
102
+ end
103
+ end
104
+ end
105
+
106
+ register_plugin(:flash, Flash)
107
+ end
108
+ end
@@ -0,0 +1,24 @@
1
+ class Roda
2
+ module RodaPlugins
3
+ # The h plugin adds an +h+ instance method that will HTML
4
+ # escape the input and return it.
5
+ #
6
+ # The following example will return "&lt;foo&gt;" as the body.
7
+ #
8
+ # plugin :h
9
+ #
10
+ # route do |r|
11
+ # h('<foo>')
12
+ # end
13
+ module H
14
+ module InstanceMethods
15
+ # HTML escape the input and return the escaped version.
16
+ def h(s)
17
+ ::Rack::Utils.escape_html(s.to_s)
18
+ end
19
+ end
20
+ end
21
+
22
+ register_plugin(:h, H)
23
+ end
24
+ end
@@ -0,0 +1,79 @@
1
+ class Roda
2
+ module RodaPlugins
3
+ # The halt plugin augments the standard request +halt+ method to handle more than
4
+ # just rack response arrays.
5
+ #
6
+ # After loading the halt plugin:
7
+ #
8
+ # plugin :halt
9
+ #
10
+ # You can call halt with no arguments to immediately stop processing:
11
+ #
12
+ # route do |r|
13
+ # r.halt
14
+ # end
15
+ #
16
+ # You can call the halt method with an integer to set the response status and return:
17
+ #
18
+ # route do |r|
19
+ # r.halt(403)
20
+ # end
21
+ #
22
+ # Or set the response body and return:
23
+ #
24
+ # route do |r|
25
+ # r.halt("body')
26
+ # end
27
+ #
28
+ # Or set both:
29
+ #
30
+ # route do |r|
31
+ # r.halt(403, "body')
32
+ # end
33
+ #
34
+ # Or set response status, headers, and body:
35
+ #
36
+ # route do |r|
37
+ # r.halt(403, {'Content-Type'=>'text/csv'}, "body')
38
+ # end
39
+ #
40
+ # Note that there is a difference between provide status, headers, and body as separate
41
+ # arguments and providing them as a rack response array. With a rack response array,
42
+ # the values are used directly, while with 3 arguments, the headers given are merged into
43
+ # the existing headers and the given body is written to the existing response body.
44
+ module Halt
45
+ module RequestMethods
46
+ # Expand default halt method to handle status codes, headers, and bodies. See Halt.
47
+ def halt(*res)
48
+ case res.length
49
+ when 0 # do nothing
50
+ when 1
51
+ case v = res[0]
52
+ when Integer
53
+ response.status = v
54
+ when String
55
+ response.write v
56
+ when Array
57
+ super
58
+ else
59
+ raise Roda::RodaError, "singular argument to #halt must be Integer, String, or Array"
60
+ end
61
+ when 2
62
+ response.status = res[0]
63
+ response.write res[1]
64
+ when 3
65
+ response.status = res[0]
66
+ response.headers.merge!(res[1])
67
+ response.write res[2]
68
+ else
69
+ raise Roda::RodaError, "too many arguments given to #halt (accepts 0-3, received #{res.length})"
70
+ end
71
+
72
+ _halt response.finish
73
+ end
74
+ end
75
+ end
76
+
77
+ register_plugin(:halt, Halt)
78
+ end
79
+ end
@@ -0,0 +1,57 @@
1
+ class Roda
2
+ module RodaPlugins
3
+ # The header_matchers plugin adds hash matchers for matching on less-common
4
+ # HTTP headers.
5
+ #
6
+ # plugin :header_matchers
7
+ #
8
+ # It adds a +:header+ matcher for matching on arbitrary headers, which matches
9
+ # if the header is present:
10
+ #
11
+ # route do |r|
12
+ # r.on :header=>'X-App-Token' do
13
+ # end
14
+ # end
15
+ #
16
+ # It adds a +:host+ matcher for matching by the host of the request:
17
+ #
18
+ # route do |r|
19
+ # r.on :host=>'foo.example.com' do
20
+ # end
21
+ # end
22
+ #
23
+ # It adds an +:accept+ matcher for matching based on the Accept header:
24
+ #
25
+ # route do |r|
26
+ # r.on :accept=>'text/csv' do
27
+ # end
28
+ # end
29
+ #
30
+ # Note that the accept matcher is very simple and cannot handle wildcards,
31
+ # priorities, or anything but a simple comma separated list of mime types.
32
+ module HeaderMatchers
33
+ module RequestMethods
34
+ private
35
+
36
+ # Match if the given mimetype is one of the accepted mimetypes.
37
+ def match_accept(mimetype)
38
+ if env["HTTP_ACCEPT"].to_s.split(',').any?{|s| s.strip == mimetype}
39
+ response["Content-Type"] = mimetype
40
+ end
41
+ end
42
+
43
+ # Match if the given uppercase key is present inside the environment.
44
+ def match_header(key)
45
+ env[key.upcase.tr("-","_")]
46
+ end
47
+
48
+ # Match if the host of the request is the same as the hostname.
49
+ def match_host(hostname)
50
+ hostname === host
51
+ end
52
+ end
53
+ end
54
+
55
+ register_plugin(:header_matchers, HeaderMatchers)
56
+ end
57
+ end
@@ -0,0 +1,106 @@
1
+ class Roda
2
+ module RodaPlugins
3
+ # The hooks plugin adds before and after hooks to the request cycle.
4
+ #
5
+ # plugin :hooks
6
+ #
7
+ # before do
8
+ # request.redirect('/login') unless logged_in?
9
+ # @time = Time.now
10
+ # end
11
+ #
12
+ # after do |res|
13
+ # logger.notice("Took #{Time.now - @time} seconds")
14
+ # end
15
+ #
16
+ # Note that in general, before hooks are not needed, since you can just
17
+ # run code at the top of the route block:
18
+ #
19
+ # route do |r|
20
+ # r.redirect('/login') unless logged_in?
21
+ # # ...
22
+ # end
23
+ #
24
+ # Note that the after hook is called with the rack response array
25
+ # of status, headers, and body. If it wants to change the response,
26
+ # it must mutate this argument, calling <tt>response.status=</tt> inside
27
+ # an after block will not affect the returned status.
28
+ #
29
+ # However, this code makes it easier to write after hooks, as well as
30
+ # handle cases where before hooks are added after the route block.
31
+ module Hooks
32
+ def self.configure(app)
33
+ @app.instance_exec do
34
+ @after ||= nil
35
+ @before ||= nil
36
+ end
37
+ end
38
+
39
+ module ClassMethods
40
+ # Add an after hook. If there is already an after hook defined,
41
+ # use a proc that instance_execs the existing after proc and
42
+ # then instance_execs the given after proc, so that the given
43
+ # after proc always executes after the previous one.
44
+ def after(&block)
45
+ if block
46
+ @after = if b = @after
47
+ @after = proc do |res|
48
+ instance_exec(res, &b)
49
+ instance_exec(res, &block)
50
+ end
51
+ else
52
+ block
53
+ end
54
+ end
55
+ @after
56
+ end
57
+
58
+ # Add a before hook. If there is already a before hook defined,
59
+ # use a proc that instance_execs the give before proc and
60
+ # then instance_execs the existing before proc, so that the given
61
+ # before proc always executes before the previous one.
62
+ def before(&block)
63
+ if block
64
+ @before = if b = @before
65
+ @before = proc do
66
+ instance_exec(&block)
67
+ instance_exec(&b)
68
+ end
69
+ else
70
+ block
71
+ end
72
+ end
73
+ @before
74
+ end
75
+
76
+ # Copy the before and after hooks into the subclasses
77
+ # when inheriting
78
+ def inherited(subclass)
79
+ super
80
+ subclass.instance_variable_set(:@before, @before)
81
+ subclass.instance_variable_set(:@after, @after)
82
+ end
83
+ end
84
+
85
+ module InstanceMethods
86
+ private
87
+
88
+ # Before routing, execute the before hooks, and
89
+ # execute the after hooks before returning.
90
+ def _route(*, &block)
91
+ if b = self.class.before
92
+ instance_exec(&b)
93
+ end
94
+
95
+ res = super
96
+ ensure
97
+ if b = self.class.after
98
+ instance_exec(res, &b)
99
+ end
100
+ end
101
+ end
102
+ end
103
+
104
+ register_plugin(:hooks, Hooks)
105
+ end
106
+ end