roda 2.2.0 → 2.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (94) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG +34 -0
  3. data/Rakefile +22 -46
  4. data/doc/release_notes/2.3.0.txt +109 -0
  5. data/lib/roda/plugins/assets.rb +2 -1
  6. data/lib/roda/plugins/caching.rb +1 -1
  7. data/lib/roda/plugins/chunked.rb +1 -1
  8. data/lib/roda/plugins/error_email.rb +1 -1
  9. data/lib/roda/plugins/head.rb +6 -0
  10. data/lib/roda/plugins/heartbeat.rb +40 -0
  11. data/lib/roda/plugins/json.rb +23 -3
  12. data/lib/roda/plugins/json_parser.rb +72 -0
  13. data/lib/roda/plugins/mailer.rb +22 -5
  14. data/lib/roda/plugins/named_templates.rb +2 -2
  15. data/lib/roda/plugins/path_rewriter.rb +82 -0
  16. data/lib/roda/plugins/precompile_templates.rb +87 -0
  17. data/lib/roda/plugins/render.rb +111 -43
  18. data/lib/roda/plugins/render_each.rb +1 -1
  19. data/lib/roda/plugins/shared_vars.rb +1 -1
  20. data/lib/roda/plugins/view_options.rb +28 -3
  21. data/lib/roda/version.rb +1 -1
  22. data/spec/composition_spec.rb +3 -3
  23. data/spec/env_spec.rb +1 -1
  24. data/spec/freeze_spec.rb +6 -6
  25. data/spec/integration_spec.rb +16 -15
  26. data/spec/matchers_spec.rb +110 -110
  27. data/spec/opts_spec.rb +8 -8
  28. data/spec/plugin/_erubis_escaping_spec.rb +34 -3
  29. data/spec/plugin/all_verbs_spec.rb +8 -8
  30. data/spec/plugin/assets_spec.rb +164 -150
  31. data/spec/plugin/backtracking_array_spec.rb +18 -18
  32. data/spec/plugin/caching_spec.rb +70 -70
  33. data/spec/plugin/chunked_spec.rb +38 -38
  34. data/spec/plugin/class_level_routing_spec.rb +78 -78
  35. data/spec/plugin/content_for_spec.rb +2 -2
  36. data/spec/plugin/cookies_spec.rb +4 -4
  37. data/spec/plugin/csrf_spec.rb +8 -8
  38. data/spec/plugin/default_headers_spec.rb +6 -6
  39. data/spec/plugin/delay_build_spec.rb +7 -6
  40. data/spec/plugin/delegate_spec.rb +2 -2
  41. data/spec/plugin/delete_empty_headers_spec.rb +2 -2
  42. data/spec/plugin/drop_body_spec.rb +6 -6
  43. data/spec/plugin/empty_root_spec.rb +3 -3
  44. data/spec/plugin/environments_spec.rb +7 -7
  45. data/spec/plugin/error_email_spec.rb +23 -23
  46. data/spec/plugin/error_handler_spec.rb +14 -14
  47. data/spec/plugin/flash_spec.rb +30 -29
  48. data/spec/plugin/h_spec.rb +1 -1
  49. data/spec/plugin/halt_spec.rb +16 -16
  50. data/spec/plugin/hash_matcher_spec.rb +5 -5
  51. data/spec/plugin/head_spec.rb +10 -10
  52. data/spec/plugin/header_matchers_spec.rb +13 -13
  53. data/spec/plugin/heartbeat_spec.rb +74 -0
  54. data/spec/plugin/hooks_spec.rb +20 -20
  55. data/spec/plugin/indifferent_params_spec.rb +1 -1
  56. data/spec/plugin/json_parser_spec.rb +72 -0
  57. data/spec/plugin/json_spec.rb +22 -9
  58. data/spec/plugin/mailer_spec.rb +72 -58
  59. data/spec/plugin/match_affix_spec.rb +2 -2
  60. data/spec/plugin/middleware_spec.rb +7 -7
  61. data/spec/plugin/module_include_spec.rb +4 -4
  62. data/spec/plugin/multi_route_spec.rb +66 -66
  63. data/spec/plugin/multi_run_spec.rb +21 -21
  64. data/spec/plugin/named_templates_spec.rb +6 -6
  65. data/spec/plugin/not_allowed_spec.rb +17 -17
  66. data/spec/plugin/not_found_spec.rb +14 -14
  67. data/spec/plugin/padrino_render_spec.rb +2 -2
  68. data/spec/plugin/param_matchers_spec.rb +6 -6
  69. data/spec/plugin/partials_spec.rb +3 -3
  70. data/spec/plugin/pass_spec.rb +7 -7
  71. data/spec/plugin/path_matchers_spec.rb +6 -6
  72. data/spec/plugin/path_rewriter_spec.rb +37 -0
  73. data/spec/plugin/path_spec.rb +41 -40
  74. data/spec/plugin/per_thread_caching_spec.rb +6 -6
  75. data/spec/plugin/precompile_templates_spec.rb +74 -0
  76. data/spec/plugin/render_each_spec.rb +4 -4
  77. data/spec/plugin/render_spec.rb +179 -76
  78. data/spec/plugin/shared_vars_spec.rb +4 -4
  79. data/spec/plugin/sinatra_helpers_spec.rb +121 -121
  80. data/spec/plugin/slash_path_empty_spec.rb +10 -10
  81. data/spec/plugin/static_spec.rb +4 -4
  82. data/spec/plugin/streaming_spec.rb +11 -11
  83. data/spec/plugin/symbol_matchers_spec.rb +24 -24
  84. data/spec/plugin/symbol_views_spec.rb +3 -3
  85. data/spec/plugin/view_options_spec.rb +10 -10
  86. data/spec/plugin_spec.rb +2 -2
  87. data/spec/redirect_spec.rb +10 -10
  88. data/spec/request_spec.rb +8 -8
  89. data/spec/response_spec.rb +23 -23
  90. data/spec/session_spec.rb +4 -4
  91. data/spec/spec_helper.rb +5 -19
  92. data/spec/version_spec.rb +4 -4
  93. data/spec/views/iv.erb +1 -0
  94. metadata +16 -5
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 9bbbb565f4b5766e8e11f78d322a5446da6358bd
4
- data.tar.gz: 4e7b782d3d4a2292e9461c47053156a18d56bec2
3
+ metadata.gz: 9c94569f43d36e67b81fa0f67d739a24f9b5a594
4
+ data.tar.gz: 8420ceb5dafb0f76e840fdeff028e46f4b0842f9
5
5
  SHA512:
6
- metadata.gz: 864fd659136483c2867b91e6512df778fd9f331c60e30b8166f1146afda0810cb1f08d56b38f2c01f50d7fcd477b9914def3d0eb1479de2533f0db8f94dc3dc3
7
- data.tar.gz: 79dcb04899e305ed875f439d3ebb57cbaf9092bba1614e8fcbbb4b15da55c9b10c74086f3bf24e7b543b9dcaaf9f3042009ab611a9e7b7f0aa0b0077a91a32b0
6
+ metadata.gz: ab68ffe4810164018008821e8a8ae7a66469a58884ff5a25599e0cac03330e393d9b62cf5d8712ceea8e9a870afd431f52a072294c710285a7e69d1a243326b0
7
+ data.tar.gz: ab79ec088d85b8daa3f705fb92bcd9e64be19ae9a5bf131f9414fca72c53a89f00a9c82465ed8db68440bf120a783c1ad4e521e295f15868a4a372463ecf8a06
data/CHANGELOG CHANGED
@@ -1,3 +1,37 @@
1
+ = 2.3.0 (2015-05-13)
2
+
3
+ * Make assets plugin work better with json plugin when r.assets is the last method called in a route block (jeremyevans) (#27)
4
+
5
+ * Support no_mail! method in the mailer plugin, for skipping an email (jeremyevans)
6
+
7
+ * Add precompile_templates plugin, for saving memory when using a forking webserver (jeremyevans)
8
+
9
+ * Document how to allow per-branch HTML escaping of <%= %> in the view_options plugin (jeremyevans)
10
+
11
+ * Add :include_request option to json and json_parser plugins to include request in :serializer/:parser call (janko-m) (#26)
12
+
13
+ * Optimize template cache lookup in render plugin when :cache_key is given (jeremyevans)
14
+
15
+ * Add :engine_opts option to render plugin, for specifying per-template engine options (jeremyevans)
16
+
17
+ * The render plugin and render/view :ext option is now replaced by the :engine option (jeremyevans)
18
+
19
+ * Add path_rewriter plugin, for rewriting paths before routing (jeremyevans)
20
+
21
+ * Add :cache_key option to render/view to explicitly set the template cache key (jeremyevans)
22
+
23
+ * Don't cache templates if :template_block is given to render/view, unless :cache=>true is used (jeremyevans)
24
+
25
+ * Add :cache option to render/view to force caching or not caching the template (jeremyevans)
26
+
27
+ * Avoid rehashing hashes at runtime in plugins (jeremyevans)
28
+
29
+ * Add heartbeat plugin for heartbeat support (jeremyevans)
30
+
31
+ * Support :serializer option in json plugin (janko-m) (#21)
32
+
33
+ * Add json_parser plugin, for parsing request bodies in JSON format (jeremyevans)
34
+
1
35
  = 2.2.0 (2015-04-13)
2
36
 
3
37
  * Add :escaper render plugin option to support custom escaping of <%= %> tags when :escape is used (jeremyevans)
data/Rakefile CHANGED
@@ -66,53 +66,29 @@ end
66
66
 
67
67
  ### Specs
68
68
 
69
- begin
70
- begin
71
- raise LoadError if ENV['RSPEC1']
72
- # RSpec 2
73
- require "rspec/core/rake_task"
74
- spec_class = RSpec::Core::RakeTask
75
- spec_files_meth = :pattern=
76
- rescue LoadError
77
- # RSpec 1
78
- require "spec/rake/spectask"
79
- spec_class = Spec::Rake::SpecTask
80
- spec_files_meth = :spec_files=
81
- end
82
-
83
- spec = lambda do |name, files, d|
84
- lib_dir = File.join(File.dirname(File.expand_path(__FILE__)), 'lib')
85
- ENV['RUBYLIB'] ? (ENV['RUBYLIB'] += ":#{lib_dir}") : (ENV['RUBYLIB'] = lib_dir)
86
-
87
- desc "#{d} with -w, some warnings filtered"
88
- task "#{name}_w" do
89
- ENV['RUBYOPT'] ? (ENV['RUBYOPT'] += " -w") : (ENV['RUBYOPT'] = '-w')
90
- rake = ENV['RAKE'] || "#{FileUtils::RUBY} -S rake"
91
- sh "#{rake} #{name} 2>&1 | egrep -v \"(spec/.*: warning: (possibly )?useless use of == in void context|: warning: instance variable @.* not initialized|: warning: method redefined; discarding old|: warning: previous definition of)|rspec\""
92
- end
93
-
94
- desc d
95
- spec_class.new(name) do |t|
96
- t.send spec_files_meth, files
97
- t.spec_opts = ENV["#{NAME.upcase}_SPEC_OPTS"].split if ENV["#{NAME.upcase}_SPEC_OPTS"]
98
- end
99
- end
100
-
101
- spec_with_cov = lambda do |name, files, d|
102
- spec.call(name, files, d)
103
- desc "#{d} with coverage"
104
- task "#{name}_cov" do
105
- ENV['COVERAGE'] = '1'
106
- Rake::Task[name].invoke
107
- end
108
- end
69
+ spec = proc do |env|
70
+ env.each{|k,v| ENV[k] = v}
71
+ sh "#{FileUtils::RUBY} -rubygems -I lib -e 'ARGV.each{|f| require f}' ./spec/*_spec.rb ./spec/plugin/*_spec.rb"
72
+ env.each{|k,v| ENV.delete(k)}
73
+ end
74
+
75
+ desc "Run specs"
76
+ task "spec" do
77
+ spec.call({})
78
+ end
79
+
80
+ task :default=>:spec
81
+
82
+ desc "Run specs with coverage"
83
+ task "spec_cov" do
84
+ spec.call('COVERAGE'=>'1')
85
+ end
109
86
 
110
- task :default => [:spec]
111
- spec_with_cov.call("spec", Dir["spec/*_spec.rb"] + Dir["spec/plugin/*_spec.rb"], "Run specs")
112
- rescue LoadError
113
- task :default do
114
- puts "Must install rspec to run the default task (which runs specs)"
115
- end
87
+ desc "Run specs with -w, some warnings filtered"
88
+ task "spec_w" do
89
+ ENV['RUBYOPT'] ? (ENV['RUBYOPT'] += " -w") : (ENV['RUBYOPT'] = '-w')
90
+ rake = ENV['RAKE'] || "#{FileUtils::RUBY} -S rake"
91
+ sh %{#{rake} 2>&1 | egrep -v \": warning: instance variable @.* not initialized|: warning: method redefined; discarding old|: warning: previous definition of|: warning: statement not reached"}
116
92
  end
117
93
 
118
94
  ### Other
@@ -0,0 +1,109 @@
1
+ = New Plugins
2
+
3
+ * A json_parser plugin has been added, for parsing request bodies in
4
+ JSON format. This is faster than using a middleware to perform
5
+ the same task. This plugin supports a :parser option to use a
6
+ custom JSON parser, an :include_request option to include the
7
+ request when calling the parser, and a :error_handler option for
8
+ a proc to call with the request if there is an error when parsing.
9
+ Example:
10
+
11
+ plugin :json_parser,
12
+ :parser=>JSON.method(:parse),
13
+ :include_request=>false,
14
+ :error_handler=>proc{|r| r.halt [400, {}, []]}
15
+
16
+ * A path_rewriter plugin has been added, allowing for the rewriting
17
+ of paths before routing. This allows you to rewrite just the
18
+ routing path (the default), or PATH_INFO as well as the routing
19
+ path (if the :path_info option is used). This is useful if you
20
+ want to internally treat one path exactly the same as another
21
+ path.
22
+
23
+ By default, path rewriting is done on prefixes, so any path that
24
+ starts with the prefix will be rewritten. You can pass a
25
+ Regexp when rewriting the path for more complete control.
26
+
27
+ Examples:
28
+
29
+ plugin :path_rewriter
30
+ rewrite_path '/a', '/b'
31
+ # GET /a treated as GET /b
32
+ # GET /a/c treated as GET /b/c
33
+
34
+ rewrite_path /\A\/c\z/, '/d'
35
+ # GET /c treated as GET /d
36
+ # GET /c/e no change
37
+
38
+ * A precompiled_templates plugin has been added, for precompiling
39
+ templates before starting the application. This can save a
40
+ substantial amount of memory if you are using large templates or
41
+ a large number of small templates in conjunction when using
42
+ application preloading with a forking webserver. Example:
43
+
44
+ plugin :precompile_templates
45
+ precompile_templates "views/\*\*/*.erb"
46
+ precompile_templates "views/users/_*.erb", :locals=>[:user]
47
+
48
+ * A heartbeat plugin has been added, for easily handling
49
+ heartbeat/status requests. If a heartbeat/status request comes in,
50
+ it will get a 200 response with a body of "OK". This is designed
51
+ for automated systems that check if the application is functioning.
52
+ The default heartbeat path is /heartbeat, but you can choose a
53
+ different one using the :path option.
54
+
55
+ plugin :heartbeat, :path=>'/heartbeat'
56
+
57
+ = Other New Features
58
+
59
+ * The json plugin now supports a :serializer option to use a custom
60
+ serializer. Additionally, it now supports a :include_request
61
+ option to include the request when calling the serializer.
62
+
63
+ * In the render plugin, the render/view methods now support a
64
+ :cache=>false option to not cache the template. This can be useful
65
+ for large but rarely used templates, or where a new template object
66
+ is created for every render/view call.
67
+
68
+ * In the render plugin, the render/view methods now support a
69
+ :cache_key option to force a specific cache key. Manually setting
70
+ cache keys can result in improved performance, as automatically
71
+ determining the cache key can be a relatively expensive operation.
72
+
73
+ * The render plugin now supports a :engine_opts option, to specify
74
+ per-template engine options. :engine options should be a hash
75
+ keyed by render engine strings, with values being hashes of
76
+ template options.
77
+
78
+ * In the mailer plugin, a no_mail! method is now supported when
79
+ mailing, which will skip the current mail. This makes it easier
80
+ to delay the decision about actually sending the email till it is
81
+ time to send the email, which makes it easier to avoid race
82
+ conditions if you are using a job queue for mailing.
83
+
84
+ = Other Improvements
85
+
86
+ * Roda avoids rehashing hashes at runtime in some places, for a minor
87
+ speedup.
88
+
89
+ * If the :template_block is given to render/view, default to not
90
+ caching the template, since it is likely the template block is
91
+ specific to the request. Allow for the :cache=>true option to be
92
+ used to force the caching of the template.
93
+
94
+ * Roda now returns a 404 response for unmatched GET requests when
95
+ using the assets and json plugins where r.assets is the last
96
+ method called in a route block.
97
+
98
+ = Backwards Compatibility
99
+
100
+ * In the render plugin, the :ext option to the plugin and to the
101
+ render/view methods is now replaced by the :engine option.
102
+ Previously, :engine was used by default if :ext was not given. In
103
+ general, there is no need for two separate options, the engine is
104
+ used as the extension by Tilt.
105
+
106
+ In general, this is a backwards compatible change, except when
107
+ both :ext and :engine were specified differently as plugin options,
108
+ and an inline template is used with render or view without either
109
+ the :ext or :engine options being specified.
@@ -214,7 +214,7 @@ class Roda
214
214
  # :js_opts :: Template options to pass to the render plugin (via :template_opts) when rendering javascript assets
215
215
  # :js_route :: Route under :prefix for javascript assets (default: :js_dir)
216
216
  # :path :: Path to your asset source directory (default: 'assets'). Relative
217
- # paths will be considered relative to the application's :root option.
217
+ # paths will be considered relative to the application's :root option.
218
218
  # :prefix :: Prefix for assets path in your URL/routes (default: 'assets')
219
219
  # :precompiled :: Path to the compiled asset metadata file. If the file exists, will use compiled
220
220
  # mode using the metadata in the file. If the file does not exist, will use
@@ -611,6 +611,7 @@ class Roda
611
611
  scope.render_asset(file, type)
612
612
  end
613
613
  end
614
+ nil
614
615
  end
615
616
  end
616
617
  end
@@ -197,7 +197,7 @@ class Roda
197
197
  # cached for. Also sets the Expires header, useful if you have
198
198
  # HTTP 1.0 clients (Cache-Control is an HTTP 1.1 header).
199
199
  def expires(max_age, opts=OPTS)
200
- cache_control(opts.merge(:max_age=>max_age))
200
+ cache_control(Hash[opts].merge!(:max_age=>max_age))
201
201
  self[EXPIRES] = (Time.now + max_age).httpdate
202
202
  end
203
203
 
@@ -229,7 +229,7 @@ class Roda
229
229
  if opts.empty?
230
230
  opts = template
231
231
  else
232
- opts = opts.merge(template)
232
+ opts = Hash[opts].merge!(template)
233
233
  end
234
234
  end
235
235
 
@@ -92,7 +92,7 @@ END
92
92
  def error_email(e)
93
93
  email_opts = self.class.opts[:error_email].dup
94
94
  headers = email_opts[:default_headers].call(email_opts, e)
95
- headers = headers.merge(email_opts[:headers])
95
+ headers = Hash[headers].merge!(email_opts[:headers])
96
96
  headers = headers.map{|k,v| "#{k}: #{v.gsub(/\r?\n/m, "\r\n ")}"}.sort.join("\r\n")
97
97
  body = email_opts[:body].call(self, e)
98
98
  email_opts[:message] = "#{headers}\r\n\r\n#{body}"
@@ -22,6 +22,12 @@ class Roda
22
22
  #
23
23
  # HEAD requests for +/+, +/a+, and +/b+ will all return 200 status
24
24
  # with an empty body.
25
+ #
26
+ # NOTE: if you have a public facing website it is recommended that
27
+ # you enable this plugin. Search engines and other bots may send a
28
+ # HEAD request prior to crawling a page with a GET request. Without
29
+ # this plugin those HEAD requests will return a 404 status, which
30
+ # may prevent search engine's from crawling your website.
25
31
  module Head
26
32
  EMPTY_ARRAY = [].freeze
27
33
 
@@ -0,0 +1,40 @@
1
+ class Roda
2
+ module RodaPlugins
3
+ # The heartbeat handles heartbeat/status requests. If a request for
4
+ # the heartbeat path comes in, a 200 response with a
5
+ # text/plain Content-Type and a body of "OK" will be returned.
6
+ # The default heartbeat path is "/heartbeat", so to use that:
7
+ #
8
+ # plugin :heartbeat
9
+ #
10
+ # You can also specify a custom heartbeat path:
11
+ #
12
+ # plugin :heartbeat, :path=>'/status'
13
+ module Heartbeat
14
+ OPTS = {}.freeze
15
+ PATH_INFO = 'PATH_INFO'.freeze
16
+ HEARTBEAT_RESPONSE = [200, {'Content-Type'=>'text/plain'}.freeze, ['OK'.freeze].freeze].freeze
17
+
18
+ # Set the heartbeat path to the given path.
19
+ def self.configure(app, opts=OPTS)
20
+ app.opts[:heartbeat_path] = (opts[:path] || app.opts[:heartbeat_path] || "/heartbeat").dup.freeze
21
+ end
22
+
23
+ module InstanceMethods
24
+ # If the request is for a heartbeat path, return the heartbeat response.
25
+ def call
26
+ if env[PATH_INFO] == opts[:heartbeat_path]
27
+ response = HEARTBEAT_RESPONSE.dup
28
+ response[1] = Hash[response[1]]
29
+ response
30
+ else
31
+ super
32
+ end
33
+ end
34
+ end
35
+ end
36
+
37
+ register_plugin(:heartbeat, Heartbeat)
38
+ end
39
+ end
40
+
@@ -32,16 +32,34 @@ class Roda
32
32
  # using the :classes option when loading the plugin:
33
33
  #
34
34
  # plugin :json, :classes=>[Array, Hash, Sequel::Model]
35
+ #
36
+ # By default objects are serialized with +to_json+, but you
37
+ # can pass in a custom serializer, which can be any object
38
+ # that responds to +call(object)+.
39
+ #
40
+ # plugin :json, :serializer=>proc{|o| o.to_json(root: true)}
41
+ #
42
+ # If you need the request information during serialization, such
43
+ # as HTTP headers or query parameters, you can pass in the
44
+ # +:include_request+ option, which will pass in the request
45
+ # object as the second argument when calling the serializer.
46
+ #
47
+ # plugin :json, include_request=>true, serializer=>proc{|o, request| ...}
35
48
  module Json
36
49
  OPTS = {}.freeze
50
+ DEFAULT_SERIALIZER = lambda{|o| o.to_json}
37
51
 
38
- # Set the classes to automatically convert to JSON
52
+ # Set the classes to automatically convert to JSON, and the serializer to use.
39
53
  def self.configure(app, opts=OPTS)
40
54
  classes = opts[:classes] || [Array, Hash]
41
55
  app.opts[:json_result_classes] ||= []
42
56
  app.opts[:json_result_classes] += classes
43
57
  app.opts[:json_result_classes].uniq!
44
58
  app.opts[:json_result_classes].freeze
59
+
60
+ app.opts[:json_result_serializer] = opts[:serializer] || app.opts[:json_result_serializer] || DEFAULT_SERIALIZER
61
+
62
+ app.opts[:json_result_include_request] = opts[:include_request] || app.opts[:json_result_include_request]
45
63
  end
46
64
 
47
65
  module ClassMethods
@@ -71,9 +89,11 @@ class Roda
71
89
  end
72
90
 
73
91
  # Convert the given object to JSON. Uses to_json by default,
74
- # but can be overridden to use a different implementation.
92
+ # but can use a custom serializer passed to the plugin.
75
93
  def convert_to_json(obj)
76
- obj.to_json
94
+ args = [obj]
95
+ args << self if roda_class.opts[:json_result_include_request]
96
+ roda_class.opts[:json_result_serializer].call(*args)
77
97
  end
78
98
  end
79
99
  end
@@ -0,0 +1,72 @@
1
+ require 'json'
2
+
3
+ class Roda
4
+ module RodaPlugins
5
+ # The json_parser plugin parses request bodies in json format
6
+ # if the request's content type specifies json. This is mostly
7
+ # designed for use with JSON API sites.
8
+ #
9
+ # This only parses the request body as JSON if the Content-Type
10
+ # header for the request includes "json".
11
+ module JsonParser
12
+ OPTS = {}.freeze
13
+ JSON_PARAMS_KEY = "roda.json_params".freeze
14
+ INPUT_KEY = "rack.input".freeze
15
+ FORM_HASH_KEY = "rack.request.form_hash".freeze
16
+ FORM_INPUT_KEY = "rack.request.form_input".freeze
17
+ DEFAULT_ERROR_HANDLER = proc{|r| r.halt [400, {}, []]}
18
+ DEFAULT_PARSER = JSON.method(:parse)
19
+
20
+ # Handle options for the json_parser plugin:
21
+ # :error_handler :: A proc to call if an exception is raised when
22
+ # parsing a JSON request body. The proc is called
23
+ # with the request object, and should probably call
24
+ # halt on the request or raise an exception.
25
+ # :parser :: The parser to use for parsing incoming json. Should be
26
+ # an object that responds to +call(str)+ and returns the
27
+ # parsed data. The default is to call JSON.parse.
28
+ # :include_request :: If true, the parser will be called with the request
29
+ # object as the second argument, so the parser needs
30
+ # to respond to +call(str, request)+.
31
+ def self.configure(app, opts=OPTS)
32
+ app.opts[:json_parser_error_handler] = opts[:error_handler] || app.opts[:json_parser_error_handler] || DEFAULT_ERROR_HANDLER
33
+ app.opts[:json_parser_parser] = opts[:parser] || app.opts[:json_parser_parser] || DEFAULT_PARSER
34
+ app.opts[:json_parser_include_request] = opts[:include_request] || app.opts[:json_parser_include_request]
35
+ end
36
+
37
+ module RequestMethods
38
+ # If the Content-Type header in the request includes "json",
39
+ # parse the request body as JSON. Ignore an empty request body.
40
+ def POST
41
+ env = @env
42
+ if post_params = (env[JSON_PARAMS_KEY] || env[FORM_HASH_KEY])
43
+ post_params
44
+ elsif (input = env[INPUT_KEY]) && content_type =~ /json/
45
+ str = input.read
46
+ input.rewind
47
+ return super if str.empty?
48
+ begin
49
+ json_params = env[JSON_PARAMS_KEY] = parse_json(str)
50
+ rescue
51
+ roda_class.opts[:json_parser_error_handler].call(self)
52
+ end
53
+ env[FORM_INPUT_KEY] = input
54
+ json_params
55
+ else
56
+ super
57
+ end
58
+ end
59
+
60
+ private
61
+
62
+ def parse_json(str)
63
+ args = [str]
64
+ args << self if roda_class.opts[:json_parser_include_request]
65
+ roda_class.opts[:json_parser_parser].call(*args)
66
+ end
67
+ end
68
+ end
69
+
70
+ register_plugin(:json_parser, JsonParser)
71
+ end
72
+ end