roda 2.2.0 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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