roda 2.28.0 → 2.29.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 (66) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG +46 -0
  3. data/README.rdoc +25 -7
  4. data/doc/release_notes/2.29.0.txt +156 -0
  5. data/lib/roda.rb +25 -3
  6. data/lib/roda/plugins/_erubis_escaping.rb +2 -0
  7. data/lib/roda/plugins/_symbol_regexp_matchers.rb +22 -0
  8. data/lib/roda/plugins/assets.rb +3 -2
  9. data/lib/roda/plugins/branch_locals.rb +74 -0
  10. data/lib/roda/plugins/caching.rb +15 -7
  11. data/lib/roda/plugins/chunked.rb +10 -7
  12. data/lib/roda/plugins/content_for.rb +4 -1
  13. data/lib/roda/plugins/drop_body.rb +3 -2
  14. data/lib/roda/plugins/error_email.rb +3 -2
  15. data/lib/roda/plugins/error_mail.rb +3 -2
  16. data/lib/roda/plugins/head.rb +2 -1
  17. data/lib/roda/plugins/header_matchers.rb +3 -0
  18. data/lib/roda/plugins/heartbeat.rb +3 -2
  19. data/lib/roda/plugins/json.rb +5 -3
  20. data/lib/roda/plugins/json_parser.rb +3 -2
  21. data/lib/roda/plugins/mailer.rb +3 -3
  22. data/lib/roda/plugins/match_affix.rb +6 -0
  23. data/lib/roda/plugins/multi_route.rb +3 -1
  24. data/lib/roda/plugins/padrino_render.rb +3 -2
  25. data/lib/roda/plugins/params_capturing.rb +3 -3
  26. data/lib/roda/plugins/partials.rb +3 -3
  27. data/lib/roda/plugins/path.rb +4 -2
  28. data/lib/roda/plugins/path_rewriter.rb +2 -2
  29. data/lib/roda/plugins/per_thread_caching.rb +2 -0
  30. data/lib/roda/plugins/placeholder_string_matchers.rb +42 -0
  31. data/lib/roda/plugins/precompile_templates.rb +3 -2
  32. data/lib/roda/plugins/render.rb +86 -37
  33. data/lib/roda/plugins/render_each.rb +2 -1
  34. data/lib/roda/plugins/render_locals.rb +102 -0
  35. data/lib/roda/plugins/run_append_slash.rb +2 -1
  36. data/lib/roda/plugins/run_handler.rb +2 -1
  37. data/lib/roda/plugins/sinatra_helpers.rb +4 -4
  38. data/lib/roda/plugins/static_path_info.rb +2 -0
  39. data/lib/roda/plugins/static_routing.rb +1 -1
  40. data/lib/roda/plugins/streaming.rb +9 -4
  41. data/lib/roda/plugins/symbol_matchers.rb +23 -20
  42. data/lib/roda/plugins/view_options.rb +63 -28
  43. data/lib/roda/plugins/view_subdirs.rb +1 -0
  44. data/lib/roda/plugins/websockets.rb +2 -0
  45. data/lib/roda/version.rb +1 -1
  46. data/spec/composition_spec.rb +2 -2
  47. data/spec/matchers_spec.rb +6 -5
  48. data/spec/plugin/_erubis_escaping_spec.rb +5 -5
  49. data/spec/plugin/backtracking_array_spec.rb +0 -2
  50. data/spec/plugin/branch_locals_spec.rb +88 -0
  51. data/spec/plugin/content_for_spec.rb +8 -2
  52. data/spec/plugin/halt_spec.rb +8 -0
  53. data/spec/plugin/header_matchers_spec.rb +20 -5
  54. data/spec/plugin/multi_route_spec.rb +1 -1
  55. data/spec/plugin/named_templates_spec.rb +2 -2
  56. data/spec/plugin/params_capturing_spec.rb +1 -1
  57. data/spec/plugin/per_thread_caching_spec.rb +1 -1
  58. data/spec/plugin/placeholder_string_matchers_spec.rb +159 -0
  59. data/spec/plugin/render_locals_spec.rb +114 -0
  60. data/spec/plugin/render_spec.rb +83 -8
  61. data/spec/plugin/streaming_spec.rb +104 -4
  62. data/spec/plugin/symbol_matchers_spec.rb +1 -1
  63. data/spec/plugin/view_options_spec.rb +83 -7
  64. data/spec/plugin/websockets_spec.rb +7 -8
  65. data/spec/spec_helper.rb +22 -2
  66. metadata +11 -2
@@ -22,11 +22,12 @@ class Roda
22
22
  # # GET /a/ => App gets "/" as PATH_INFO
23
23
  module RunAppendSlash
24
24
  OPTS = {}.freeze
25
+ RodaPlugins.deprecate_constant(self, :OPTS)
25
26
 
26
27
  # Set plugin specific options. Options:
27
28
  # :use_redirects :: Whether to issue 302 redirects when appending the
28
29
  # trailing slash.
29
- def self.configure(app, opts=OPTS)
30
+ def self.configure(app, opts=RodaPlugins::OPTS)
30
31
  app.opts[:run_append_slash_redirect] = !!opts[:use_redirects]
31
32
  end
32
33
 
@@ -26,6 +26,7 @@ class Roda
26
26
  # end
27
27
  module RunHandler
28
28
  OPTS = {}.freeze
29
+ RodaPlugins.deprecate_constant(self, :OPTS)
29
30
 
30
31
  module RequestMethods
31
32
  # If a block is given, yield the rack response array to it. The response can
@@ -34,7 +35,7 @@ class Roda
34
35
  # If the <tt>:not_found=>:pass</tt> option is given, and the rack response
35
36
  # returned by the app is a 404 response, do not return the response, continue
36
37
  # routing normally.
37
- def run(app, opts=OPTS)
38
+ def run(app, opts=RodaPlugins::OPTS)
38
39
  res = catch(:halt){super(app)}
39
40
  yield res if block_given?
40
41
  throw(:halt, res) unless opts[:not_found] == :pass && res[0] == 404
@@ -212,7 +212,7 @@ class Roda
212
212
  # OTHER DEALINGS IN THE SOFTWARE.
213
213
  module SinatraHelpers
214
214
  OPTS = {}.freeze
215
-
215
+ RodaPlugins.deprecate_constant(self, :OPTS)
216
216
  CONTENT_TYPE = "Content-Type".freeze
217
217
  RodaPlugins.deprecate_constant(self, :CONTENT_TYPE)
218
218
  CONTENT_DISPOSITION = "Content-Disposition".freeze
@@ -248,7 +248,7 @@ class Roda
248
248
  # Add delegate methods to the route block scope
249
249
  # calling request or response methods, unless the
250
250
  # :delegate option is false.
251
- def self.configure(app, opts=OPTS)
251
+ def self.configure(app, opts=RodaPlugins::OPTS)
252
252
  app.send(:include, DelegateMethods) unless opts[:delegate] == false
253
253
  end
254
254
 
@@ -339,7 +339,7 @@ class Roda
339
339
  end
340
340
 
341
341
  # Use the contents of the file at +path+ as the response body. See plugin documentation for options.
342
- def send_file(path, opts = OPTS)
342
+ def send_file(path, opts = RodaPlugins::OPTS)
343
343
  res = response
344
344
  headers = res.headers
345
345
  if opts[:type] || !headers["Content-Type"]
@@ -441,7 +441,7 @@ class Roda
441
441
 
442
442
  # Set the Content-Type of the response body given a media type or file
443
443
  # extension. See plugin documentation for options.
444
- def content_type(type = (return @headers["Content-Type"]; nil), opts = OPTS)
444
+ def content_type(type = (return @headers["Content-Type"]; nil), opts = RodaPlugins::OPTS)
445
445
  unless (mime_type = mime_type(type) || opts[:default])
446
446
  raise RodaError, "Unknown media type: #{type}"
447
447
  end
@@ -1,5 +1,7 @@
1
1
  class Roda
2
2
  module RodaPlugins
3
+ warn 'The static_path_info plugin is deprecated and will be removed in Roda 3. It has been a no-op since Roda 2, and can just be removed from the application.'
4
+
3
5
  module StaticPathInfo
4
6
  end
5
7
 
@@ -47,7 +47,7 @@ class Roda
47
47
  # method call takes precedence over the static_route method call for /foo.
48
48
  # As shown above, you can use Roda's routing tree methods inside the
49
49
  # static_route block to have shared behavior for different request methods,
50
- # while still having handling the request methods differently.
50
+ # while still handling the request methods differently.
51
51
  #
52
52
  # Note that if you want to use the static_routing plugin and the hooks
53
53
  # plugin at the same time, you should load the hooks plugin first.
@@ -73,6 +73,7 @@ class Roda
73
73
  # OTHER DEALINGS IN THE SOFTWARE.
74
74
  module Streaming
75
75
  OPTS = {}.freeze
76
+ RodaPlugins.deprecate_constant(self, :OPTS)
76
77
 
77
78
  # Class of the response body in case you use #stream.
78
79
  #
@@ -109,7 +110,7 @@ class Roda
109
110
  end
110
111
 
111
112
  # Handle streaming options, see Streaming for details.
112
- def initialize(opts=OPTS, &back)
113
+ def initialize(opts=RodaPlugins::OPTS, &back)
113
114
  @scheduler = opts[:scheduler] || Scheduler.new(self)
114
115
  @back = back.to_proc
115
116
  @keep_open = opts[:keep_open]
@@ -117,7 +118,7 @@ class Roda
117
118
  @closed = false
118
119
 
119
120
  if opts[:callback]
120
- callback(&opts[:callback])
121
+ @callbacks << opts[:callback]
121
122
  end
122
123
  end
123
124
 
@@ -134,6 +135,7 @@ class Roda
134
135
 
135
136
  # Add the given block as a callback to call when the block closes.
136
137
  def callback(&block)
138
+ RodaPlugins.warn 'Stream#callback in the streaming plugin is deprecated and will be removed in Roda 3. Specify callback at initialization using the stream method :callback option.'
137
139
  return yield if closed?
138
140
  @callbacks << block
139
141
  end
@@ -172,8 +174,11 @@ class Roda
172
174
  # Immediately return a streaming response using the current response
173
175
  # status and headers, calling the block to get the streaming response.
174
176
  # See Streaming for details.
175
- def stream(opts=OPTS, &block)
176
- opts = opts.merge(:scheduler=>EventMachine) if !opts.has_key?(:scheduler) && env['async.callback']
177
+ def stream(opts=RodaPlugins::OPTS, &block)
178
+ if !opts.has_key?(:scheduler) && env['async.callback']
179
+ RodaPlugins.warn 'The automatic support for EventMachine in the streaming plugin is deprecated and will be removed in Roda 3.'
180
+ opts = opts.merge(:scheduler=>EventMachine)
181
+ end
177
182
 
178
183
  if opts[:loop]
179
184
  block = proc do |out|
@@ -25,8 +25,8 @@ class Roda
25
25
  # :rest :: <tt>/(.*)/</tt>, all remaining characters, if any
26
26
  # :w :: <tt>/(\w+)/</tt>, a alphanumeric segment
27
27
  #
28
- # If placeholder string matchers are supported, this feature also applies to
29
- # embedded colons in strings, so the following:
28
+ # If the placeholder_string_matchers plugin is loaded, this feature also applies to
29
+ # placeholders in strings, so the following:
30
30
  #
31
31
  # r.on "users/:username" do
32
32
  # # ...
@@ -35,33 +35,36 @@ class Roda
35
35
  # Would match +/users/foobar123+, but not +/users/foo+, +/users/FooBar123+,
36
36
  # or +/users/foobar_123+.
37
37
  #
38
- # If placeholder string matchers are supported, it also adds the following
39
- # symbol matchers:
40
- #
41
- # :format :: <tt>/(?:\.(\w+))?/</tt>, an optional format/extension
42
- # :opt :: <tt>/(?:\/([^\/]+))?</tt>, an optional segment
43
- # :optd :: <tt>/(?:\/(\d+))?</tt>, an optional decimal segment
44
- #
45
- # These are only added when placeholder string matchers are supported,
46
- # because they only make sense when used inside of a string, due to how
47
- # segment matching works. Example:
48
- #
49
- # r.is "album:opt" do |id| end
50
- # # matches /album (yielding nil) and /album/foo (yielding "foo")
51
- # # does not match /album/ or /album/foo/bar
52
- #
53
38
  # If using this plugin with the params_capturing plugin, this plugin should
54
39
  # be loaded first.
55
40
  module SymbolMatchers
41
+ def self.load_dependencies(app)
42
+ app.plugin :_symbol_regexp_matchers
43
+ end
44
+
56
45
  def self.configure(app)
57
46
  app.symbol_matcher(:d, /(\d+)/)
58
47
  app.symbol_matcher(:w, /(\w+)/)
59
48
  app.symbol_matcher(:rest, /(.*)/)
60
49
 
61
50
  if !app.opts[:verbatim_string_matcher]
62
- app.symbol_matcher(:format, /(?:\.(\w+))?/)
63
- app.symbol_matcher(:opt, /(?:\/([^\/]+))?/)
64
- app.symbol_matcher(:optd, /(?:\/(\d+))?/)
51
+ # RODA3: Remove
52
+ app::RodaRequest.class_eval do
53
+ def match_symbol_format
54
+ Roda::RodaPlugins.warn('Implicit use of the :format symbol matcher is deprecated and will be removed in Roda 3. If you want to use the :format symbol matcher, add the following code to your Roda class: symbol_matcher(:format, /(?:\.(\w+))?/)')
55
+ /(?:\.(\w+))?/
56
+ end
57
+
58
+ def match_symbol_opt
59
+ Roda::RodaPlugins.warn('Implicit use of the :opt symbol matcher is deprecated and will be removed in Roda 3. If you want to use the :opt symbol matcher, add the following code to your Roda class: symbol_matcher(:opt, /(?:\/([^\/]+))?/)')
60
+ /(?:\/([^\/]+))?/
61
+ end
62
+
63
+ def match_symbol_optd
64
+ Roda::RodaPlugins.warn('Implicit use of the :optd symbol matcher is deprecated and will be removed in Roda 3. If you want to use the :optd symbol matcher, add the following code to your Roda class: symbol_matcher(:optd, /(?:\/(\d+))?/)')
65
+ /(?:\/(\d+))?/
66
+ end
67
+ end
65
68
  end
66
69
  end
67
70
 
@@ -4,7 +4,7 @@
4
4
  class Roda
5
5
  module RodaPlugins
6
6
  # The view_options plugin allows you to override view and layout
7
- # options and locals for specific branches and routes.
7
+ # options for specific branches and routes.
8
8
  #
9
9
  # plugin :render
10
10
  # plugin :view_options
@@ -12,15 +12,14 @@ class Roda
12
12
  # route do |r|
13
13
  # r.on "users" do
14
14
  # set_layout_options :template=>'users_layout'
15
- # set_layout_locals :title=>'Users'
16
15
  # set_view_options :engine=>'haml'
17
- # set_view_locals :footer=>'(c) Roda'
18
16
  #
19
17
  # # ...
20
18
  # end
21
19
  # end
22
20
  #
23
- # The options and locals you specify have higher precedence than
21
+ # The options you specify via the set_view_options and
22
+ # set_layout_options methods have higher precedence than
24
23
  # the render plugin options, but lower precedence than options
25
24
  # you directly pass to the view/render methods.
26
25
  #
@@ -60,22 +59,22 @@ class Roda
60
59
  #
61
60
  # If you have an existing Roda application that doesn't use
62
61
  # automatic HTML escaping for <tt><%= %></tt> tags via the
63
- # :render plugin's :escape option, but you want to switch to
62
+ # :render plugin's :escape=>:erubi option, but you want to switch to
64
63
  # using the :escape option, you can now do so without making
65
64
  # all changes at once. With set_view_options, you can now
66
65
  # specify escaping or not on a per branch basis in the routing
67
66
  # tree:
68
67
  #
69
- # plugin :render, :escape=>true
68
+ # plugin :render, :escape=>:erubi
70
69
  # plugin :view_options
71
70
  #
72
71
  # route do |r|
73
72
  # # Don't escape <%= %> by default
74
- # set_view_options :template_opts=>{:engine_class=>nil}
73
+ # set_view_options :template_opts=>{:escape=>false}
75
74
  #
76
75
  # r.on "users" do
77
76
  # # Escape <%= %> in this branch
78
- # set_view_options :template_opts=>{:engine_class=>render_opts[:template_opts][:engine_class]}
77
+ # set_view_options :template_opts=>{:escape=>true}
79
78
  # end
80
79
  # end
81
80
  module ViewOptions
@@ -85,27 +84,7 @@ class Roda
85
84
  app.plugin :render
86
85
  end
87
86
 
88
- # The following methods are created via metaprogramming:
89
- # set_layout_locals :: Set locals to use in the layout
90
- # set_layout_options :: Set options to use when rendering the layout
91
- # set_view_locals :: Set locals to use in the view
92
- # set_view_options :: Set options to use when rendering the view
93
87
  module InstanceMethods
94
- %w'layout view'.each do |type|
95
- %w'locals options'.each do |var|
96
- v = "_#{type}_#{var}"
97
- module_eval(<<-END, __FILE__, __LINE__+1)
98
- def set#{v}(opts)
99
- if @#{v}
100
- @#{v} = Hash[@#{v}].merge!(opts)
101
- else
102
- @#{v} = opts
103
- end
104
- end
105
- END
106
- end
107
- end
108
-
109
88
  # Append a view subdirectory to use. If there hasn't already
110
89
  # been a view subdirectory set, this just sets it to the argument.
111
90
  # If there has already been a view subdirectory set, this sets
@@ -125,8 +104,62 @@ class Roda
125
104
  @_view_subdir = v
126
105
  end
127
106
 
107
+ # Set branch/route options to use when rendering the layout
108
+ def set_layout_options(opts)
109
+ if options = @_layout_options
110
+ @_layout_options = Hash[options].merge!(opts)
111
+ else
112
+ @_layout_options = opts
113
+ end
114
+ end
115
+
116
+ # Set branch/route options to use when rendering the view
117
+ def set_view_options(opts)
118
+ if options = @_view_options
119
+ @_view_options = Hash[options].merge!(opts)
120
+ else
121
+ @_view_options = opts
122
+ end
123
+ end
124
+
125
+ # RODA3: Remove
126
+ def set_layout_locals(opts)
127
+ RodaPlugins.warn "The set_layout_locals method in the view_options plugin is deprecated and will be removed in Roda 3. This feature has been moved to the branch_locals plugin."
128
+ if locals = @_layout_locals
129
+ @_layout_locals = Hash[locals].merge!(opts)
130
+ else
131
+ @_layout_locals = opts
132
+ end
133
+ end
134
+
135
+ # RODA3: Remove
136
+ def set_view_locals(opts)
137
+ RodaPlugins.warn "The set_view_locals method in the view_options plugin is deprecated and will be removed in Roda 3. This feature has been moved to the branch_locals plugin."
138
+ if locals = @_view_locals
139
+ @_view_locals = Hash[locals].merge!(opts)
140
+ else
141
+ @_view_locals = opts
142
+ end
143
+ end
144
+
128
145
  private
129
146
 
147
+ def render_locals
148
+ locals = super
149
+ if @_view_locals
150
+ locals = Hash[locals].merge!(@_view_locals)
151
+ end
152
+ locals
153
+ end
154
+
155
+ def layout_locals
156
+ locals = super
157
+ if @_view_locals
158
+ locals = Hash[locals].merge!(@_layout_locals)
159
+ end
160
+ locals
161
+ end
162
+
130
163
  # If view options or locals have been set and this
131
164
  # template isn't a layout template, merge the options
132
165
  # and locals into the returned hash.
@@ -138,6 +171,7 @@ class Roda
138
171
  t_opts.merge!(v_opts)
139
172
  end
140
173
 
174
+ # RODA3: Remove
141
175
  if v_locals = @_view_locals
142
176
  t_opts[:locals] = if t_locals = t_opts[:locals]
143
177
  Hash[v_locals].merge!(t_locals)
@@ -159,6 +193,7 @@ class Roda
159
193
  opts.merge!(l_opts)
160
194
  end
161
195
 
196
+ # RODA3: Remove
162
197
  if l_locals = @_layout_locals
163
198
  opts[:locals] = if o_locals = opts[:locals]
164
199
  Hash[o_locals].merge!(l_locals)
@@ -3,4 +3,5 @@
3
3
  # make attempts to load the view_subdirs plugin load the
4
4
  # view_options plugin instead.
5
5
  require 'roda/plugins/view_options'
6
+ Roda::RodaPlugins.warn "The view_subdirs plugin is a deprecated alias for the view_options plugin that will be removed in Roda 3. Use the view_options plugin instead."
6
7
  Roda::RodaPlugins.register_plugin(:view_subdirs, Roda::RodaPlugins::ViewOptions)
@@ -4,6 +4,8 @@ require 'faye/websocket'
4
4
 
5
5
  class Roda
6
6
  module RodaPlugins
7
+ warn "The websockets plugin is deprecated and will be removed in Roda 3. Consider maintaining the plugin as a separate gem if you would like to keep using it."
8
+
7
9
  # The websocket plugin adds integration support for websockets.
8
10
  # Currently, only 'faye-websocket' is supported, so eventmachine
9
11
  # is required for websockets. See the
data/lib/roda/version.rb CHANGED
@@ -4,7 +4,7 @@ class Roda
4
4
  RodaMajorVersion = 2
5
5
 
6
6
  # The minor version of Roda, updated for new feature releases of Roda.
7
- RodaMinorVersion = 28
7
+ RodaMinorVersion = 29
8
8
 
9
9
  # The patch version of Roda, updated only for bug fixes from the last
10
10
  # feature release.
@@ -19,13 +19,13 @@ describe "r.run" do
19
19
 
20
20
  it "modifies SCRIPT_NAME/PATH_INFO when calling run" do
21
21
  a = app{|r| "#{r.script_name}|#{r.path_info}"}
22
- app(:static_path_info){|r| r.on("a"){r.run a}}
22
+ app{|r| r.on("a"){r.run a}}
23
23
  body("/a/b").must_equal "/a|/b"
24
24
  end
25
25
 
26
26
  it "restores SCRIPT_NAME/PATH_INFO before returning from run" do
27
27
  a = app{|r| "#{r.script_name}|#{r.path_info}"}
28
- app(:static_path_info){|r| s = catch(:halt){r.on("a"){r.run a}}; "#{s[2].join}%#{r.script_name}|#{r.path_info}"}
28
+ app{|r| s = catch(:halt){r.on("a"){r.run a}}; "#{s[2].join}%#{r.script_name}|#{r.path_info}"}
29
29
  body("/a/b").must_equal "/a|/b%|/a/b"
30
30
  end
31
31
  end
@@ -158,7 +158,7 @@ describe "r.is" do
158
158
  end
159
159
 
160
160
  describe "matchers" do
161
- it "should handle string with embedded param" do
161
+ deprecated "should handle string with embedded param" do
162
162
  app do |r|
163
163
  r.on "posts/:id" do |id|
164
164
  id
@@ -185,6 +185,7 @@ describe "matchers" do
185
185
  '2'
186
186
  end
187
187
  end
188
+ # RODA3: remove option
188
189
  app.opts[:verbatim_string_matcher] = true
189
190
 
190
191
  status('/post/123').must_equal 404
@@ -194,7 +195,7 @@ describe "matchers" do
194
195
  body('/responses-:id').must_equal '2'
195
196
  end
196
197
 
197
- it "should handle multiple params in single string" do
198
+ deprecated "should handle multiple params in single string" do
198
199
  app do |r|
199
200
  r.on "u/:uid/posts/:id" do |uid, id|
200
201
  uid + id
@@ -206,7 +207,7 @@ describe "matchers" do
206
207
  status("/u/jdoe/pots/123").must_equal 404
207
208
  end
208
209
 
209
- it "should escape regexp metacharaters in string" do
210
+ deprecated "should escape regexp metacharaters in string" do
210
211
  app do |r|
211
212
  r.on "u/:uid/posts?/:id" do |uid, id|
212
213
  uid + id
@@ -218,7 +219,7 @@ describe "matchers" do
218
219
  status("/u/jdoe/post/123").must_equal 404
219
220
  end
220
221
 
221
- it "should handle colons by themselves" do
222
+ deprecated "should handle colons by themselves" do
222
223
  app do |r|
223
224
  r.on "u/:/:uid/posts/::id" do |uid, id|
224
225
  uid + id
@@ -394,7 +395,7 @@ describe "r.on" do
394
395
  status.must_equal 404
395
396
  end
396
397
 
397
- it "executes on arbitrary object" do
398
+ deprecated "executes on arbitrary object" do
398
399
  app do |r|
399
400
  r.on Object.new do
400
401
  "+1"