ultra-lite-hub 0.0.1

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 (162) hide show
  1. checksums.yaml +7 -0
  2. data/roda-3.105.0/MIT-LICENSE +21 -0
  3. data/roda-3.105.0/lib/roda/cache.rb +42 -0
  4. data/roda-3.105.0/lib/roda/plugins/Integer_matcher_max.rb +55 -0
  5. data/roda-3.105.0/lib/roda/plugins/_after_hook.rb +11 -0
  6. data/roda-3.105.0/lib/roda/plugins/_base64.rb +34 -0
  7. data/roda-3.105.0/lib/roda/plugins/_before_hook.rb +41 -0
  8. data/roda-3.105.0/lib/roda/plugins/_optimized_matching.rb +227 -0
  9. data/roda-3.105.0/lib/roda/plugins/_symbol_class_matchers.rb +108 -0
  10. data/roda-3.105.0/lib/roda/plugins/_symbol_regexp_matchers.rb +24 -0
  11. data/roda-3.105.0/lib/roda/plugins/additional_render_engines.rb +61 -0
  12. data/roda-3.105.0/lib/roda/plugins/additional_view_directories.rb +66 -0
  13. data/roda-3.105.0/lib/roda/plugins/all_verbs.rb +50 -0
  14. data/roda-3.105.0/lib/roda/plugins/assets.rb +902 -0
  15. data/roda-3.105.0/lib/roda/plugins/assets_preloading.rb +89 -0
  16. data/roda-3.105.0/lib/roda/plugins/assume_ssl.rb +28 -0
  17. data/roda-3.105.0/lib/roda/plugins/autoload_hash_branches.rb +79 -0
  18. data/roda-3.105.0/lib/roda/plugins/autoload_named_routes.rb +65 -0
  19. data/roda-3.105.0/lib/roda/plugins/backtracking_array.rb +88 -0
  20. data/roda-3.105.0/lib/roda/plugins/bearer_token.rb +28 -0
  21. data/roda-3.105.0/lib/roda/plugins/branch_locals.rb +74 -0
  22. data/roda-3.105.0/lib/roda/plugins/break.rb +43 -0
  23. data/roda-3.105.0/lib/roda/plugins/caching.rb +206 -0
  24. data/roda-3.105.0/lib/roda/plugins/capture_erb.rb +94 -0
  25. data/roda-3.105.0/lib/roda/plugins/chunked.rb +331 -0
  26. data/roda-3.105.0/lib/roda/plugins/class_level_routing.rb +111 -0
  27. data/roda-3.105.0/lib/roda/plugins/class_matchers.rb +155 -0
  28. data/roda-3.105.0/lib/roda/plugins/common_logger.rb +80 -0
  29. data/roda-3.105.0/lib/roda/plugins/conditional_sessions.rb +67 -0
  30. data/roda-3.105.0/lib/roda/plugins/content_for.rb +103 -0
  31. data/roda-3.105.0/lib/roda/plugins/content_security_policy.rb +329 -0
  32. data/roda-3.105.0/lib/roda/plugins/cookie_flags.rb +157 -0
  33. data/roda-3.105.0/lib/roda/plugins/cookies.rb +51 -0
  34. data/roda-3.105.0/lib/roda/plugins/csrf.rb +81 -0
  35. data/roda-3.105.0/lib/roda/plugins/custom_block_results.rb +85 -0
  36. data/roda-3.105.0/lib/roda/plugins/custom_matchers.rb +89 -0
  37. data/roda-3.105.0/lib/roda/plugins/default_headers.rb +64 -0
  38. data/roda-3.105.0/lib/roda/plugins/default_status.rb +35 -0
  39. data/roda-3.105.0/lib/roda/plugins/delay_build.rb +18 -0
  40. data/roda-3.105.0/lib/roda/plugins/delegate.rb +77 -0
  41. data/roda-3.105.0/lib/roda/plugins/delete_empty_headers.rb +36 -0
  42. data/roda-3.105.0/lib/roda/plugins/direct_call.rb +38 -0
  43. data/roda-3.105.0/lib/roda/plugins/disallow_file_uploads.rb +38 -0
  44. data/roda-3.105.0/lib/roda/plugins/drop_body.rb +43 -0
  45. data/roda-3.105.0/lib/roda/plugins/each_part.rb +87 -0
  46. data/roda-3.105.0/lib/roda/plugins/early_hints.rb +25 -0
  47. data/roda-3.105.0/lib/roda/plugins/empty_root.rb +49 -0
  48. data/roda-3.105.0/lib/roda/plugins/environments.rb +79 -0
  49. data/roda-3.105.0/lib/roda/plugins/erb_h.rb +43 -0
  50. data/roda-3.105.0/lib/roda/plugins/error_email.rb +152 -0
  51. data/roda-3.105.0/lib/roda/plugins/error_handler.rb +133 -0
  52. data/roda-3.105.0/lib/roda/plugins/error_mail.rb +149 -0
  53. data/roda-3.105.0/lib/roda/plugins/exception_page.rb +445 -0
  54. data/roda-3.105.0/lib/roda/plugins/filter_common_logger.rb +46 -0
  55. data/roda-3.105.0/lib/roda/plugins/flash.rb +117 -0
  56. data/roda-3.105.0/lib/roda/plugins/h.rb +59 -0
  57. data/roda-3.105.0/lib/roda/plugins/halt.rb +108 -0
  58. data/roda-3.105.0/lib/roda/plugins/hash_branch_view_subdir.rb +76 -0
  59. data/roda-3.105.0/lib/roda/plugins/hash_branches.rb +145 -0
  60. data/roda-3.105.0/lib/roda/plugins/hash_matcher.rb +39 -0
  61. data/roda-3.105.0/lib/roda/plugins/hash_paths.rb +128 -0
  62. data/roda-3.105.0/lib/roda/plugins/hash_public.rb +125 -0
  63. data/roda-3.105.0/lib/roda/plugins/hash_public_cache.rb +91 -0
  64. data/roda-3.105.0/lib/roda/plugins/hash_routes.rb +304 -0
  65. data/roda-3.105.0/lib/roda/plugins/head.rb +99 -0
  66. data/roda-3.105.0/lib/roda/plugins/header_matchers.rb +88 -0
  67. data/roda-3.105.0/lib/roda/plugins/heartbeat.rb +41 -0
  68. data/roda-3.105.0/lib/roda/plugins/hmac_paths.rb +413 -0
  69. data/roda-3.105.0/lib/roda/plugins/hooks.rb +92 -0
  70. data/roda-3.105.0/lib/roda/plugins/host_authorization.rb +155 -0
  71. data/roda-3.105.0/lib/roda/plugins/host_routing.rb +190 -0
  72. data/roda-3.105.0/lib/roda/plugins/hsts.rb +35 -0
  73. data/roda-3.105.0/lib/roda/plugins/indifferent_params.rb +124 -0
  74. data/roda-3.105.0/lib/roda/plugins/inject_erb.rb +33 -0
  75. data/roda-3.105.0/lib/roda/plugins/invalid_request_body.rb +107 -0
  76. data/roda-3.105.0/lib/roda/plugins/ip_from_header.rb +34 -0
  77. data/roda-3.105.0/lib/roda/plugins/json.rb +119 -0
  78. data/roda-3.105.0/lib/roda/plugins/json_parser.rb +107 -0
  79. data/roda-3.105.0/lib/roda/plugins/link_to.rb +83 -0
  80. data/roda-3.105.0/lib/roda/plugins/mail_processor.rb +629 -0
  81. data/roda-3.105.0/lib/roda/plugins/mailer.rb +283 -0
  82. data/roda-3.105.0/lib/roda/plugins/map_matcher.rb +48 -0
  83. data/roda-3.105.0/lib/roda/plugins/match_affix.rb +64 -0
  84. data/roda-3.105.0/lib/roda/plugins/match_hook.rb +34 -0
  85. data/roda-3.105.0/lib/roda/plugins/match_hook_args.rb +93 -0
  86. data/roda-3.105.0/lib/roda/plugins/middleware.rb +225 -0
  87. data/roda-3.105.0/lib/roda/plugins/middleware_stack.rb +101 -0
  88. data/roda-3.105.0/lib/roda/plugins/module_include.rb +96 -0
  89. data/roda-3.105.0/lib/roda/plugins/multi_public.rb +95 -0
  90. data/roda-3.105.0/lib/roda/plugins/multi_route.rb +156 -0
  91. data/roda-3.105.0/lib/roda/plugins/multi_run.rb +144 -0
  92. data/roda-3.105.0/lib/roda/plugins/multi_view.rb +83 -0
  93. data/roda-3.105.0/lib/roda/plugins/multibyte_string_matcher.rb +57 -0
  94. data/roda-3.105.0/lib/roda/plugins/named_routes.rb +167 -0
  95. data/roda-3.105.0/lib/roda/plugins/named_templates.rb +102 -0
  96. data/roda-3.105.0/lib/roda/plugins/not_allowed.rb +149 -0
  97. data/roda-3.105.0/lib/roda/plugins/not_found.rb +55 -0
  98. data/roda-3.105.0/lib/roda/plugins/optimized_segment_matchers.rb +53 -0
  99. data/roda-3.105.0/lib/roda/plugins/optimized_string_matchers.rb +50 -0
  100. data/roda-3.105.0/lib/roda/plugins/padrino_render.rb +42 -0
  101. data/roda-3.105.0/lib/roda/plugins/param_matchers.rb +83 -0
  102. data/roda-3.105.0/lib/roda/plugins/params_capturing.rb +116 -0
  103. data/roda-3.105.0/lib/roda/plugins/part.rb +76 -0
  104. data/roda-3.105.0/lib/roda/plugins/partials.rb +74 -0
  105. data/roda-3.105.0/lib/roda/plugins/pass.rb +47 -0
  106. data/roda-3.105.0/lib/roda/plugins/path.rb +255 -0
  107. data/roda-3.105.0/lib/roda/plugins/path_matchers.rb +54 -0
  108. data/roda-3.105.0/lib/roda/plugins/path_rewriter.rb +111 -0
  109. data/roda-3.105.0/lib/roda/plugins/permissions_policy.rb +336 -0
  110. data/roda-3.105.0/lib/roda/plugins/placeholder_string_matchers.rb +48 -0
  111. data/roda-3.105.0/lib/roda/plugins/plain_hash_response_headers.rb +32 -0
  112. data/roda-3.105.0/lib/roda/plugins/precompile_templates.rb +156 -0
  113. data/roda-3.105.0/lib/roda/plugins/public.rb +182 -0
  114. data/roda-3.105.0/lib/roda/plugins/r.rb +35 -0
  115. data/roda-3.105.0/lib/roda/plugins/recheck_precompiled_assets.rb +107 -0
  116. data/roda-3.105.0/lib/roda/plugins/redirect_http_to_https.rb +99 -0
  117. data/roda-3.105.0/lib/roda/plugins/redirect_path.rb +59 -0
  118. data/roda-3.105.0/lib/roda/plugins/relative_path.rb +74 -0
  119. data/roda-3.105.0/lib/roda/plugins/render.rb +998 -0
  120. data/roda-3.105.0/lib/roda/plugins/render_coverage.rb +105 -0
  121. data/roda-3.105.0/lib/roda/plugins/render_each.rb +224 -0
  122. data/roda-3.105.0/lib/roda/plugins/render_locals.rb +102 -0
  123. data/roda-3.105.0/lib/roda/plugins/request_aref.rb +77 -0
  124. data/roda-3.105.0/lib/roda/plugins/request_headers.rb +81 -0
  125. data/roda-3.105.0/lib/roda/plugins/response_attachment.rb +94 -0
  126. data/roda-3.105.0/lib/roda/plugins/response_content_type.rb +79 -0
  127. data/roda-3.105.0/lib/roda/plugins/response_request.rb +29 -0
  128. data/roda-3.105.0/lib/roda/plugins/route_block_args.rb +50 -0
  129. data/roda-3.105.0/lib/roda/plugins/route_csrf.rb +388 -0
  130. data/roda-3.105.0/lib/roda/plugins/run_append_slash.rb +51 -0
  131. data/roda-3.105.0/lib/roda/plugins/run_handler.rb +51 -0
  132. data/roda-3.105.0/lib/roda/plugins/run_require_slash.rb +46 -0
  133. data/roda-3.105.0/lib/roda/plugins/sec_fetch_site_csrf.rb +131 -0
  134. data/roda-3.105.0/lib/roda/plugins/send_file.rb +127 -0
  135. data/roda-3.105.0/lib/roda/plugins/sessions.rb +537 -0
  136. data/roda-3.105.0/lib/roda/plugins/shared_vars.rb +86 -0
  137. data/roda-3.105.0/lib/roda/plugins/sinatra_helpers.rb +443 -0
  138. data/roda-3.105.0/lib/roda/plugins/slash_path_empty.rb +26 -0
  139. data/roda-3.105.0/lib/roda/plugins/static.rb +42 -0
  140. data/roda-3.105.0/lib/roda/plugins/static_routing.rb +89 -0
  141. data/roda-3.105.0/lib/roda/plugins/status_303.rb +34 -0
  142. data/roda-3.105.0/lib/roda/plugins/status_handler.rb +86 -0
  143. data/roda-3.105.0/lib/roda/plugins/streaming.rb +170 -0
  144. data/roda-3.105.0/lib/roda/plugins/strip_path_prefix.rb +33 -0
  145. data/roda-3.105.0/lib/roda/plugins/symbol_matchers.rb +188 -0
  146. data/roda-3.105.0/lib/roda/plugins/symbol_status.rb +32 -0
  147. data/roda-3.105.0/lib/roda/plugins/symbol_views.rb +37 -0
  148. data/roda-3.105.0/lib/roda/plugins/timestamp_public.rb +78 -0
  149. data/roda-3.105.0/lib/roda/plugins/type_routing.rb +215 -0
  150. data/roda-3.105.0/lib/roda/plugins/typecast_params.rb +1183 -0
  151. data/roda-3.105.0/lib/roda/plugins/typecast_params_sized_integers.rb +108 -0
  152. data/roda-3.105.0/lib/roda/plugins/unescape_path.rb +41 -0
  153. data/roda-3.105.0/lib/roda/plugins/view_options.rb +201 -0
  154. data/roda-3.105.0/lib/roda/plugins/view_subdir_leading_slash.rb +49 -0
  155. data/roda-3.105.0/lib/roda/plugins.rb +68 -0
  156. data/roda-3.105.0/lib/roda/request.rb +722 -0
  157. data/roda-3.105.0/lib/roda/response.rb +221 -0
  158. data/roda-3.105.0/lib/roda/session_middleware.rb +183 -0
  159. data/roda-3.105.0/lib/roda/version.rb +18 -0
  160. data/roda-3.105.0/lib/roda.rb +586 -0
  161. data/ultra-lite-hub.gemspec +12 -0
  162. metadata +201 -0
@@ -0,0 +1,1183 @@
1
+ # frozen-string-literal: true
2
+
3
+ require 'date'
4
+ require 'time'
5
+
6
+ class Roda
7
+ module RodaPlugins
8
+ # The typecast_params plugin allows for type conversion of submitted parameters.
9
+ # Submitted parameters should be considered untrusted input, and in standard use
10
+ # with browsers, parameters are submitted as strings (or a hash/array containing
11
+ # strings). In most cases it makes sense to explicitly convert the parameter to the
12
+ # desired type. While this can be done via manual conversion:
13
+ #
14
+ # val = request.params['key'].to_i
15
+ # val = nil unless val > 0
16
+ #
17
+ # the typecast_params plugin adds a friendlier interface:
18
+ #
19
+ # val = typecast_params.pos_int('key')
20
+ #
21
+ # As +typecast_params+ is a fairly long method name, and may be a method you call
22
+ # frequently, you may want to consider aliasing it to something more terse in your
23
+ # application, such as +tp+.
24
+ #
25
+ # typecast_params offers support for default values:
26
+ #
27
+ # val = typecast_params.pos_int('key', 1)
28
+ #
29
+ # The default value is only used if no value has been submitted for the parameter,
30
+ # or if the conversion of the value results in +nil+. Handling defaults for parameter
31
+ # conversion manually is more difficult, since the parameter may not be present at all,
32
+ # or it may be present but an empty string because the user did not enter a value on
33
+ # the related form. Use of typecast_params for the conversion handles both cases.
34
+ #
35
+ # In many cases, parameters should be required, and if they aren't submitted, that
36
+ # should be considered an error. typecast_params handles this with ! methods:
37
+ #
38
+ # val = typecast_params.pos_int!('key')
39
+ #
40
+ # These ! methods raise an error instead of returning +nil+, and do not allow defaults.
41
+ #
42
+ # The errors raised by this plugin use a specific exception class,
43
+ # +Roda::RodaPlugins::TypecastParams::Error+. This allows you to handle
44
+ # this specific exception class globally and return an appropriate 4xx
45
+ # response to the client. You can use the Error#param_name and Error#reason
46
+ # methods to get more information about the error.
47
+ #
48
+ # To make it easy to handle cases where many parameters need the same conversion
49
+ # done, you can pass an array of keys to a conversion method, and it will return an array
50
+ # of converted values:
51
+ #
52
+ # val1, val2 = typecast_params.pos_int(['key1', 'key2'])
53
+ #
54
+ # This is equivalent to:
55
+ #
56
+ # val1 = typecast_params.pos_int('key1')
57
+ # val2 = typecast_params.pos_int('key2')
58
+ #
59
+ # The ! methods also support arrays, ensuring that all parameters have a value:
60
+ #
61
+ # val1, val2 = typecast_params.pos_int!(['key1', 'key2'])
62
+ #
63
+ # For handling of array parameters, where all entries in the array use the
64
+ # same conversion, there is an +array+ method which takes the type as the first argument
65
+ # and the keys to convert as the second argument:
66
+ #
67
+ # vals = typecast_params.array(:pos_int, 'keys')
68
+ #
69
+ # If you want to ensure that all entries in the array are converted successfully and that
70
+ # there is a value for the array itself, you can use +array!+:
71
+ #
72
+ # vals = typecast_params.array!(:pos_int, 'keys')
73
+ #
74
+ # This will raise an exception if any of the values in the array for parameter +keys+ cannot
75
+ # be converted to integer.
76
+ #
77
+ # Both +array+ and +array!+ support default values which are used if no value is present
78
+ # for the parameter:
79
+ #
80
+ # vals1 = typecast_params.array(:pos_int, 'keys1', [])
81
+ # vals2 = typecast_params.array!(:pos_int, 'keys2', [])
82
+ #
83
+ # You can also pass an array of keys to +array+ or +array!+, if you would like to perform
84
+ # the same conversion on multiple arrays:
85
+ #
86
+ # foo_ids, bar_ids = typecast_params.array!(:pos_int, ['foo_ids', 'bar_ids'])
87
+ #
88
+ # The previous examples have shown use of the +pos_int+ method, which uses +to_i+ to convert the
89
+ # value to an integer, but returns +nil+ if the resulting integer is not positive. Unless you need
90
+ # to handle negative numbers, it is recommended to use +pos_int+ instead of +int+ as +int+ will
91
+ # convert invalid values to 0 (since that is how <tt>String#to_i</tt> works).
92
+ #
93
+ # There are many built in methods for type conversion:
94
+ #
95
+ # any :: Returns the value as is without conversion
96
+ # str :: Raises if value is not already a string
97
+ # nonempty_str :: Raises if value is not already a string, and converts
98
+ # the empty string or string containing only whitespace to +nil+
99
+ # bool :: Converts entry to boolean if in one of the recognized formats:
100
+ # nil :: nil, ''
101
+ # true :: true, 1, '1', 't', 'true', 'yes', 'y', 'on' # case insensitive
102
+ # false :: false, 0, '0', 'f', 'false', 'no', 'n', 'off' # case insensitive
103
+ # If not in one of those formats, raises an error.
104
+ # int :: Converts value to integer using +to_i+ (note that invalid input strings will be
105
+ # returned as 0)
106
+ # pos_int :: Converts value using +to_i+, but non-positive values are converted to +nil+
107
+ # Integer :: Converts value to integer using <tt>Kernel::Integer</tt>, with base 10 for
108
+ # string inputs, and a check that the output value is equal to the input
109
+ # value for numeric inputs.
110
+ # float :: Converts value to float using +to_f+ (note that invalid input strings will be
111
+ # returned as 0.0)
112
+ # Float :: Converts value to float using <tt>Kernel::Float(value)</tt>
113
+ # Hash :: Raises if value is not already a hash
114
+ # date :: Converts value to Date using <tt>Date.parse(value)</tt>
115
+ # time :: Converts value to Time using <tt>Time.parse(value)</tt>
116
+ # datetime :: Converts value to DateTime using <tt>DateTime.parse(value)</tt>
117
+ # file :: Raises if value is not already a hash with a :tempfile key whose value
118
+ # responds to +read+ (this is the format rack uses for uploaded files).
119
+ #
120
+ # All of these methods also support ! methods (e.g. +pos_int!+), and all of them can be
121
+ # used in the +array+ and +array!+ methods to support arrays of values.
122
+ #
123
+ # Since parameter hashes can be nested, the <tt>[]</tt> method can be used to access nested
124
+ # hashes:
125
+ #
126
+ # # params: {'key'=>{'sub_key'=>'1'}}
127
+ # typecast_params['key'].pos_int!('sub_key') # => 1
128
+ #
129
+ # This works to an arbitrary depth:
130
+ #
131
+ # # params: {'key'=>{'sub_key'=>{'sub_sub_key'=>'1'}}}
132
+ # typecast_params['key']['sub_key'].pos_int!('sub_sub_key') # => 1
133
+ #
134
+ # And also works with arrays at any depth, if those arrays contain hashes:
135
+ #
136
+ # # params: {'key'=>[{'sub_key'=>{'sub_sub_key'=>'1'}}]}
137
+ # typecast_params['key'][0]['sub_key'].pos_int!('sub_sub_key') # => 1
138
+ #
139
+ # # params: {'key'=>[{'sub_key'=>['1']}]}
140
+ # typecast_params['key'][0].array!(:pos_int, 'sub_key') # => [1]
141
+ #
142
+ # To allow easier access to nested data, there is a +dig+ method:
143
+ #
144
+ # typecast_params.dig(:pos_int, 'key', 'sub_key')
145
+ # typecast_params.dig(:pos_int, 'key', 0, 'sub_key', 'sub_sub_key')
146
+ #
147
+ # +dig+ will return +nil+ if any access while looking up the nested value returns +nil+.
148
+ # There is also a +dig!+ method, which will raise an Error if +dig+ would return +nil+:
149
+ #
150
+ # typecast_params.dig!(:pos_int, 'key', 'sub_key')
151
+ # typecast_params.dig!(:pos_int, 'key', 0, 'sub_key', 'sub_sub_key')
152
+ #
153
+ # Note that none of these conversion methods modify +request.params+. They purely do the
154
+ # conversion and return the converted value. However, in some cases it is useful to do all
155
+ # the conversion up front, and then pass a hash of converted parameters to an internal
156
+ # method that expects to receive values in specific types. The +convert!+ method does
157
+ # this, and there is also a +convert_each!+ method
158
+ # designed for converting multiple values using the same block:
159
+ #
160
+ # converted_params = typecast_params.convert! do |tp|
161
+ # tp.int('page')
162
+ # tp.pos_int!('artist_id')
163
+ # tp.array!(:pos_int, 'album_ids')
164
+ # tp.convert!('sales') do |stp|
165
+ # stp.pos_int!(['num_sold', 'num_shipped'])
166
+ # end
167
+ # tp.convert!('members') do |mtp|
168
+ # mtp.convert_each! do |stp|
169
+ # stp.str!(['first_name', 'last_name'])
170
+ # end
171
+ # end
172
+ # end
173
+ #
174
+ # # converted_params:
175
+ # # {
176
+ # # 'page' => 1,
177
+ # # 'artist_id' => 2,
178
+ # # 'album_ids' => [3, 4],
179
+ # # 'sales' => {
180
+ # # 'num_sold' => 5,
181
+ # # 'num_shipped' => 6
182
+ # # },
183
+ # # 'members' => [
184
+ # # {'first_name' => 'Foo', 'last_name' => 'Bar'},
185
+ # # {'first_name' => 'Baz', 'last_name' => 'Quux'}
186
+ # # ]
187
+ # # }
188
+ #
189
+ # +convert!+ and +convert_each!+ only return values you explicitly specify for conversion
190
+ # inside the passed block.
191
+ #
192
+ # You can specify the +:symbolize+ option to +convert!+ or +convert_each!+, which will
193
+ # symbolize the resulting hash keys:
194
+ #
195
+ # converted_params = typecast_params.convert!(symbolize: true) do |tp|
196
+ # tp.int('page')
197
+ # tp.pos_int!('artist_id')
198
+ # tp.array!(:pos_int, 'album_ids')
199
+ # tp.convert!('sales') do |stp|
200
+ # stp.pos_int!(['num_sold', 'num_shipped'])
201
+ # end
202
+ # tp.convert!('members') do |mtp|
203
+ # mtp.convert_each! do |stp|
204
+ # stp.str!(['first_name', 'last_name'])
205
+ # end
206
+ # end
207
+ # end
208
+ #
209
+ # # converted_params:
210
+ # # {
211
+ # # :page => 1,
212
+ # # :artist_id => 2,
213
+ # # :album_ids => [3, 4],
214
+ # # :sales => {
215
+ # # :num_sold => 5,
216
+ # # :num_shipped => 6
217
+ # # },
218
+ # # :members => [
219
+ # # {:first_name => 'Foo', :last_name => 'Bar'},
220
+ # # {:first_name => 'Baz', :last_name => 'Quux'}
221
+ # # ]
222
+ # # }
223
+ #
224
+ # Using the +:symbolize+ option makes it simpler to transition from untrusted external
225
+ # data (string keys), to semitrusted data that can be used internally (trusted in the sense that
226
+ # the expected types are used, not that you trust the values).
227
+ #
228
+ # Note that if there are multiple conversion errors raised inside a +convert!+ or +convert_each!+
229
+ # block, they are recorded and a single TypecastParams::Error instance is raised after
230
+ # processing the block. TypecastParams::Error#param_names can be called on the exception to
231
+ # get an array of all parameter names with conversion issues, and TypecastParams::Error#all_errors
232
+ # can be used to get an array of all Error instances.
233
+ #
234
+ # Because of how +convert!+ and +convert_each!+ work, you should avoid calling
235
+ # TypecastParams::Params#[] inside the block you pass to these methods, because if the #[]
236
+ # call fails, it will skip the reminder of the block.
237
+ #
238
+ # Be aware that when you use +convert!+ and +convert_each!+, the conversion methods called
239
+ # inside the block may return nil if there is a error raised, and nested calls to
240
+ # +convert!+ and +convert_each!+ may not return values.
241
+ #
242
+ # When loading the typecast_params plugin, a subclass of +TypecastParams::Params+ is created
243
+ # specific to the Roda application. You can add support for custom types by passing a block
244
+ # when loading the typecast_params plugin. This block is executed in the context of the
245
+ # subclass, and calling +handle_type+ in the block can be used to add conversion methods.
246
+ # +handle_type+ accepts a type name, an options hash, and the block used to convert the type.
247
+ # Supported options are:
248
+ # +:invalid_value_message+ :: The message to use for type conversions that result in a nil value
249
+ # (a space and the parameter name is appended to this).
250
+ # +:max_input_bytesize+ :: The maximum bytesize of string input.
251
+ #
252
+ # You can override the invalid value message of an existing type using the
253
+ # +invalid_value_message+ method. You can also override the max input bytesize of an existing
254
+ # type using the +max_input_bytesize+ method.
255
+ #
256
+ # plugin :typecast_params do
257
+ # handle_type(:album, max_input_bytesize: 100,
258
+ # invalid_value_message: "invalid album id in parameter") do |value|
259
+ # if id = convert_pos_int(val)
260
+ # Album[id]
261
+ # end
262
+ # end
263
+ # max_input_bytesize(:date, 256)
264
+ # invalid_value_message(:pos_int, "value must be greater than 0 for parameter")
265
+ # end
266
+ #
267
+ # By default, the typecast_params conversion procs are passed the parameter value directly
268
+ # from +request.params+ without modification. In some cases, it may be beneficial to
269
+ # strip leading and trailing whitespace from parameter string values before processing, which
270
+ # you can do by passing the <tt>strip: :all</tt> option when loading the plugin.
271
+ #
272
+ # By default, the typecasting methods for some types check whether the bytesize of input
273
+ # strings is over the maximum expected values, and raise an error in such cases. The input
274
+ # bytesize is checked prior to any type conversion. If you would like to skip this check
275
+ # and allow any bytesize when doing type conversion for param string values, you can do so by
276
+ # passing the # <tt>:skip_bytesize_checking</tt> option when loading the plugin. By default,
277
+ # there is an 100 byte limit on integer input, an 1000 byte input on float input, and a 128
278
+ # byte limit on date/time input.
279
+ #
280
+ # By default, the typecasting methods check whether input strings have null bytes, and raise
281
+ # an error in such cases. This check for null bytes occurs prior to any type conversion.
282
+ # If you would like to skip this check and allow null bytes in param string values,
283
+ # you can do so by passing the <tt>:allow_null_bytes</tt> option when loading the plugin.
284
+ #
285
+ # You can use the :date_parse_input_handler option to specify custom handling of date
286
+ # parsing input. Modern versions of Ruby and the date gem internally raise if the input to
287
+ # date parsing methods is too large to prevent denial of service. If you are using an
288
+ # older version of Ruby, you can use this option to enforce the same check:
289
+ #
290
+ # plugin :typecast_params, date_parse_input_handler: proc {|string|
291
+ # raise ArgumentError, "too big" if string.bytesize > 128
292
+ # string
293
+ # }
294
+ #
295
+ # You can also use this option to modify the input, such as truncating it to the first
296
+ # 128 bytes:
297
+ #
298
+ # plugin :typecast_params, date_parse_input_handler: proc {|string|
299
+ # string.b[0, 128]
300
+ # }
301
+ #
302
+ # The +date_parse_input_handler+ is only called if the value is under the max input
303
+ # bytesize, so you may need to call +max_input_bytesize+ for the +:date+, +:time+, and
304
+ # +:datetime+ methods to override the max input bytesize if you want to use this option
305
+ # for input strings over 128 bytes.
306
+ #
307
+ # By design, typecast_params only deals with string keys, it is not possible to use
308
+ # symbol keys as arguments to the conversion methods and have them converted.
309
+ module TypecastParams
310
+ # Sentinal value for whether to raise exception during #process
311
+ CHECK_NIL = Object.new.freeze
312
+
313
+ # Exception class for errors that are caused by misuse of the API by the programmer.
314
+ # These are different from +Error+ which are raised because the submitted parameters
315
+ # do not match what is expected. Should probably be treated as a 5xx error.
316
+ class ProgrammerError < RodaError; end
317
+
318
+ # Exception class for errors that are due to the submitted parameters not matching
319
+ # what is expected. Should probably be treated as a 4xx error.
320
+ class Error < RodaError
321
+ # Set the keys in the given exception. If the exception is not already an
322
+ # instance of the class, create a new instance to wrap it.
323
+ def self.create(keys, reason, e)
324
+ if e.is_a?(self)
325
+ e.keys ||= keys
326
+ e.reason ||= reason
327
+ e
328
+ else
329
+ backtrace = e.backtrace
330
+ e = new("#{e.class}: #{e.message}")
331
+ e.keys = keys
332
+ e.reason = reason
333
+ e.set_backtrace(backtrace) if backtrace
334
+ e
335
+ end
336
+ end
337
+
338
+ # The keys used to access the parameter that caused the error. This is an array
339
+ # that can be splatted to +dig+ to get the value of the parameter causing the error.
340
+ attr_accessor :keys
341
+
342
+ # An array of all other errors that were raised with this error. If the error
343
+ # was not raised inside Params#convert! or Params#convert_each!, this will just be
344
+ # an array containing the current receiver.
345
+ #
346
+ # This allows you to use Params#convert! to process a form input, and if any
347
+ # conversion errors occur inside the block, it can provide an array of all parameter
348
+ # names and reasons for parameters with problems.
349
+ attr_writer :all_errors
350
+
351
+ def all_errors
352
+ @all_errors ||= [self]
353
+ end
354
+
355
+ # The reason behind this error. If this error was caused by a conversion method,
356
+ # this will be the conversion method symbol. If this error was caused
357
+ # because a value was missing, then it will be +:missing+. If this error was
358
+ # caused because a value was not the correct type, then it will be +:invalid_type+.
359
+ attr_accessor :reason
360
+
361
+ # The likely parameter name where the contents were not expected. This is
362
+ # designed for cases where the parameter was submitted with the typical
363
+ # application/x-www-form-urlencoded or multipart/form-data content types,
364
+ # and assumes the typical rack parsing of these content types into
365
+ # parameters. # If the parameters were submitted via JSON, #keys should be
366
+ # used directly.
367
+ #
368
+ # Example:
369
+ #
370
+ # # keys: ['page']
371
+ # param_name => 'page'
372
+ #
373
+ # # keys: ['artist', 'name']
374
+ # param_name => 'artist[name]'
375
+ #
376
+ # # keys: ['album', 'artist', 'name']
377
+ # param_name => 'album[artist][name]'
378
+ def param_name
379
+ if keys.length > 1
380
+ first, *rest = keys
381
+ v = first.dup
382
+ rest.each do |param|
383
+ v << "["
384
+ v << param unless param.is_a?(Integer)
385
+ v << "]"
386
+ end
387
+ v
388
+ else
389
+ keys.first
390
+ end
391
+ end
392
+
393
+ # An array of all parameter names for parameters where the context were not
394
+ # expected. If Params#convert! was not used, this will be an array containing
395
+ # #param_name. If Params#convert! was used and multiple exceptions were
396
+ # captured inside the convert! block, this will contain the parameter names
397
+ # related to all captured exceptions.
398
+ def param_names
399
+ all_errors.map(&:param_name)
400
+ end
401
+ end
402
+
403
+ module AllowNullByte
404
+ private
405
+
406
+ # Allow ASCII NUL bytes ("\0") in parameter string values.
407
+ def check_null_byte(v)
408
+ end
409
+ end
410
+
411
+ module StringStripper
412
+ private
413
+
414
+ # Strip any resulting input string.
415
+ def param_value(key)
416
+ v = super
417
+
418
+ if v.is_a?(String)
419
+ v = v.strip
420
+ end
421
+
422
+ v
423
+ end
424
+ end
425
+
426
+ module DateParseInputHandler
427
+ # Pass input string to date parsing through handle_date_parse_input.
428
+ def _string_parse!(klass, v)
429
+ v = handle_date_parse_input(v)
430
+ super
431
+ end
432
+ end
433
+
434
+ module SkipBytesizeChecking
435
+ private
436
+
437
+ # Do not check max input bytesize
438
+ def check_allowed_bytesize(v, max)
439
+ end
440
+ end
441
+
442
+ # Class handling conversion of submitted parameters to desired types.
443
+ class Params
444
+ # Handle conversions for the given type using the given block.
445
+ # For a type named +foo+, this will create the following methods:
446
+ #
447
+ # * foo(key, default=nil)
448
+ # * foo!(key)
449
+ # * convert_foo(value) # private
450
+ # * _convert_array_foo(value) # private
451
+ # * _invalid_value_message_for_foo # private
452
+ # * _max_input_bytesize_for_foo # private
453
+ #
454
+ # This method is used to define all type conversions, even the built
455
+ # in ones. It can be called in subclasses to setup subclass-specific
456
+ # types.
457
+ def self.handle_type(type, opts=OPTS, &block)
458
+ convert_meth = :"convert_#{type}"
459
+ define_method(convert_meth, &block)
460
+
461
+ convert_array_meth = :"_convert_array_#{type}"
462
+ define_method(convert_array_meth) do |v|
463
+ raise Error, "expected array but received #{v.inspect}" unless v.is_a?(Array)
464
+ v.map! do |val|
465
+ check_allowed_bytesize(val, _max_input_bytesize_for(type))
466
+ check_null_byte(val)
467
+ send(convert_meth, val)
468
+ end
469
+ end
470
+
471
+ private convert_meth, convert_array_meth
472
+
473
+ invalid_value_message(type, opts[:invalid_value_message])
474
+ max_input_bytesize(type, opts[:max_input_bytesize])
475
+
476
+ define_method(type) do |key, default=nil|
477
+ process_arg(convert_meth, key, default, type) if require_hash!
478
+ end
479
+
480
+ define_method(:"#{type}!") do |key|
481
+ send(type, key, CHECK_NIL)
482
+ end
483
+ end
484
+
485
+ # Set the invalid message for the given type.
486
+ def self.invalid_value_message(type, message)
487
+ invalid_value_message_meth = :"_invalid_value_message_for_#{type}"
488
+ define_method(invalid_value_message_meth){message}
489
+ private invalid_value_message_meth
490
+ alias_method invalid_value_message_meth, invalid_value_message_meth
491
+ end
492
+
493
+ # Set the maximum input bytesize for the given type.
494
+ def self.max_input_bytesize(type, bytesize)
495
+ max_input_bytesize_meth = :"_max_input_bytesize_for_#{type}"
496
+ define_method(max_input_bytesize_meth){bytesize}
497
+ private max_input_bytesize_meth
498
+ alias_method max_input_bytesize_meth, max_input_bytesize_meth
499
+ end
500
+
501
+ # Create a new instance with the given object and nesting level.
502
+ # +obj+ should be an array or hash, and +nesting+ should be an
503
+ # array. Designed for internal use, should not be called by
504
+ # external code.
505
+ def self.nest(obj, nesting)
506
+ v = allocate
507
+ v.instance_variable_set(:@nesting, nesting)
508
+ v.send(:initialize, obj)
509
+ v
510
+ end
511
+
512
+ handle_type(:any) do |v|
513
+ v
514
+ end
515
+
516
+ handle_type(:str) do |v|
517
+ raise Error, "expected string but received #{v.inspect}" unless v.is_a?(::String)
518
+ v
519
+ end
520
+
521
+ handle_type(:nonempty_str, :invalid_value_message=>"empty string provided for parameter") do |v|
522
+ if (v = convert_str(v)) && !v.strip.empty?
523
+ v
524
+ end
525
+ end
526
+
527
+ handle_type(:bool, :invalid_value_message=>"empty string provided for parameter") do |v|
528
+ case v
529
+ when ''
530
+ nil
531
+ when false, 0, /\A(?:0|f(?:alse)?|no?|off)\z/i
532
+ false
533
+ when true, 1, /\A(?:1|t(?:rue)?|y(?:es)?|on)\z/i
534
+ true
535
+ else
536
+ raise Error, "expected bool but received #{v.inspect}"
537
+ end
538
+ end
539
+
540
+ handle_type(:int, :max_input_bytesize=>100, :invalid_value_message=>"empty string provided for parameter") do |v|
541
+ string_or_numeric!(v) && v.to_i
542
+ end
543
+ alias base_convert_int convert_int
544
+
545
+ handle_type(:pos_int, :max_input_bytesize=>100, :invalid_value_message=>"empty string, non-integer, or non-positive integer provided for parameter") do |v|
546
+ if (v = base_convert_int(v)) && v > 0
547
+ v
548
+ end
549
+ end
550
+
551
+ handle_type(:Integer, :max_input_bytesize=>100, :invalid_value_message=>"empty string provided for parameter") do |v|
552
+ if string_or_numeric!(v)
553
+ case v
554
+ when String
555
+ ::Kernel::Integer(v, 10)
556
+ when Integer
557
+ v
558
+ else
559
+ i = ::Kernel::Integer(v)
560
+ raise Error, "numeric value passed to Integer contains non-Integer part: #{v.inspect}" unless i == v
561
+ i
562
+ end
563
+ end
564
+ end
565
+ alias base_convert_Integer convert_Integer
566
+
567
+ handle_type(:float, :max_input_bytesize=>1000, :invalid_value_message=>"empty string provided for parameter") do |v|
568
+ string_or_numeric!(v) && v.to_f
569
+ end
570
+
571
+ handle_type(:Float, :max_input_bytesize=>1000, :invalid_value_message=>"empty string provided for parameter") do |v|
572
+ string_or_numeric!(v) && ::Kernel::Float(v)
573
+ end
574
+
575
+ handle_type(:Hash) do |v|
576
+ raise Error, "expected hash but received #{v.inspect}" unless v.is_a?(::Hash)
577
+ v
578
+ end
579
+
580
+ handle_type(:date, :max_input_bytesize=>128) do |v|
581
+ parse!(::Date, v)
582
+ end
583
+
584
+ handle_type(:time, :max_input_bytesize=>128) do |v|
585
+ parse!(::Time, v)
586
+ end
587
+
588
+ handle_type(:datetime, :max_input_bytesize=>128) do |v|
589
+ parse!(::DateTime, v)
590
+ end
591
+
592
+ handle_type(:file) do |v|
593
+ raise Error, "expected hash with :tempfile entry" unless v.is_a?(::Hash) && v.has_key?(:tempfile) && v[:tempfile].respond_to?(:read)
594
+ v
595
+ end
596
+
597
+ # Set the object used for converting. Conversion methods will convert members of
598
+ # the passed object.
599
+ def initialize(obj)
600
+ case @obj = obj
601
+ when Hash, Array
602
+ # nothing
603
+ else
604
+ if @nesting
605
+ handle_error(nil, (@obj.nil? ? :missing : :invalid_type), "value of #{param_name(nil)} parameter not an array or hash: #{obj.inspect}", true)
606
+ else
607
+ handle_error(nil, :invalid_type, "parameters given not an array or hash: #{obj.inspect}", true)
608
+ end
609
+ end
610
+ end
611
+
612
+ # If key is a String Return whether the key is present in the object,
613
+ def present?(key)
614
+ case key
615
+ when String
616
+ !any(key).nil?
617
+ when Array
618
+ key.all? do |k|
619
+ raise ProgrammerError, "non-String element in array argument passed to present?: #{k.inspect}" unless k.is_a?(String)
620
+ !any(k).nil?
621
+ end
622
+ else
623
+ raise ProgrammerError, "unexpected argument passed to present?: #{key.inspect}"
624
+ end
625
+ end
626
+
627
+ # Return a new Params instance for the given +key+. The value of +key+ should be an array
628
+ # if +key+ is an integer, or hash otherwise.
629
+ def [](key)
630
+ @subs ||= {}
631
+ if sub = @subs[key]
632
+ return sub
633
+ end
634
+
635
+ if @obj.is_a?(Array)
636
+ unless key.is_a?(Integer)
637
+ handle_error(key, :invalid_type, "invalid use of non-integer key for accessing array: #{key.inspect}", true)
638
+ end
639
+ else
640
+ if key.is_a?(Integer)
641
+ handle_error(key, :invalid_type, "invalid use of integer key for accessing hash: #{key}", true)
642
+ end
643
+ end
644
+
645
+ v = @obj[key]
646
+ v = yield if v.nil? && defined?(yield)
647
+
648
+ begin
649
+ sub = self.class.nest(v, Array(@nesting) + [key])
650
+ rescue => e
651
+ handle_error(key, :invalid_type, e, true)
652
+ end
653
+
654
+ @subs[key] = sub
655
+ sub.sub_capture(@capture, @symbolize, @skip_missing)
656
+ sub
657
+ end
658
+
659
+ # Return the nested value for key. If there is no nested_value for +key+,
660
+ # calls the block to return the value, or returns nil if there is no block given.
661
+ def fetch(key)
662
+ send(:[], key){return(yield if defined?(yield))}
663
+ end
664
+
665
+ # Captures conversions inside the given block, and returns a hash of all conversions,
666
+ # including conversions of subkeys. +keys+ should be an array of subkeys to access,
667
+ # or nil to convert the current object. If +keys+ is given as a hash, it is used as
668
+ # the options hash. Options:
669
+ #
670
+ # :raise :: If set to false, do not raise errors for missing keys
671
+ # :skip_missing :: If set to true, does not store values if the key is not
672
+ # present in the params.
673
+ # :symbolize :: Convert any string keys in the resulting hash and for any
674
+ # conversions below
675
+ def convert!(keys=nil, opts=OPTS)
676
+ if keys.is_a?(Hash)
677
+ opts = keys
678
+ keys = nil
679
+ end
680
+
681
+ _capture!(:nested_params, opts) do
682
+ if sub = subkey(Array(keys).dup, opts.fetch(:raise, true))
683
+ yield sub
684
+ end
685
+ end
686
+ end
687
+
688
+ # Runs conversions similar to convert! for each key specified by the :keys option. If :keys option is not given
689
+ # and the object is an array, runs conversions for all entries in the array. If the :keys
690
+ # option is not given and the object is a Hash with string keys '0', '1', ..., 'N' (with
691
+ # no skipped keys), runs conversions for all entries in the hash. If :keys option is a Proc
692
+ # or a Method, calls the proc/method with the current object, which should return an
693
+ # array of keys to use.
694
+ # Supports options given to #convert!, and this additional option:
695
+ #
696
+ # :keys :: The keys to extract from the object. If a proc or method,
697
+ # calls the value with the current object, which should return the array of keys
698
+ # to use.
699
+ def convert_each!(opts=OPTS, &block)
700
+ np = !@capture
701
+
702
+ _capture!(nil, opts) do
703
+ case keys = opts[:keys]
704
+ when nil
705
+ keys = (0...@obj.length)
706
+
707
+ valid = if @obj.is_a?(Array)
708
+ true
709
+ else
710
+ keys = keys.map(&:to_s)
711
+ keys.all?{|k| @obj.has_key?(k)}
712
+ end
713
+
714
+ unless valid
715
+ handle_error(nil, :invalid_type, "convert_each! called on object not an array or hash with keys '0'..'N'")
716
+ next
717
+ end
718
+ when Array
719
+ # nothing to do
720
+ when Proc, Method
721
+ keys = keys.call(@obj)
722
+ else
723
+ raise ProgrammerError, "unsupported convert_each! :keys option: #{keys.inspect}"
724
+ end
725
+
726
+ keys.map do |i|
727
+ begin
728
+ if v = subkey([i], opts.fetch(:raise, true))
729
+ yield v
730
+ v.nested_params if np
731
+ end
732
+ rescue => e
733
+ handle_error(i, :invalid_type, e)
734
+ end
735
+ end
736
+ end
737
+ end
738
+
739
+ # Convert values nested under the current obj. Traverses the current object using +nest+, then converts
740
+ # +key+ on that object using +type+:
741
+ #
742
+ # tp.dig(:pos_int, 'foo') # tp.pos_int('foo')
743
+ # tp.dig(:pos_int, 'foo', 'bar') # tp['foo'].pos_int('bar')
744
+ # tp.dig(:pos_int, 'foo', 'bar', 'baz') # tp['foo']['bar'].pos_int('baz')
745
+ #
746
+ # Returns nil if any of the values are not present or not the expected type. If the nest path results
747
+ # in an object that is not an array or hash, then raises an Error.
748
+ #
749
+ # You can use +dig+ to get access to nested arrays by using <tt>:array</tt> or <tt>:array!</tt> as
750
+ # the first argument and providing the type in the second argument:
751
+ #
752
+ # tp.dig(:array, :pos_int, 'foo', 'bar', 'baz') # tp['foo']['bar'].array(:pos_int, 'baz')
753
+ def dig(type, *nest, key)
754
+ _dig(false, type, nest, key)
755
+ end
756
+
757
+ # Similar to +dig+, but raises an Error instead of returning +nil+ if no value is found.
758
+ def dig!(type, *nest, key)
759
+ _dig(true, type, nest, key)
760
+ end
761
+
762
+ # Convert the value of +key+ to an array of values of the given +type+. If +default+ is
763
+ # given, any +nil+ values in the array are replaced with +default+. If +key+ is an array
764
+ # then this returns an array of arrays, one for each respective value of +key+. If there is
765
+ # no value for +key+, nil is returned instead of an array.
766
+ def array(type, key, default=nil)
767
+ meth = :"_convert_array_#{type}"
768
+ raise ProgrammerError, "no typecast_params type registered for #{type.inspect}" unless respond_to?(meth, true)
769
+ process_arg(meth, key, default, type) if require_hash!
770
+ end
771
+
772
+ # Call +array+ with the +type+, +key+, and +default+, but if the return value is nil or any value in
773
+ # the returned array is +nil+, raise an Error.
774
+ def array!(type, key, default=nil)
775
+ v = array(type, key, default)
776
+
777
+ if key.is_a?(Array)
778
+ key.zip(v).each do |k, arr|
779
+ check_array!(k, arr)
780
+ end
781
+ else
782
+ check_array!(key, v)
783
+ end
784
+
785
+ v
786
+ end
787
+
788
+ protected
789
+
790
+ # Recursively descendent into all known subkeys and get the converted params from each.
791
+ def nested_params
792
+ return @nested_params if @nested_params
793
+
794
+ params = @params
795
+
796
+ if @subs
797
+ @subs.each do |key, v|
798
+ if key.is_a?(String) && symbolize?
799
+ key = key.to_sym
800
+ end
801
+ params[key] = v.nested_params
802
+ end
803
+ end
804
+
805
+ params
806
+ end
807
+
808
+ # Recursive method to get subkeys.
809
+ def subkey(keys, do_raise)
810
+ unless key = keys.shift
811
+ return self
812
+ end
813
+
814
+ reason = :invalid_type
815
+
816
+ case key
817
+ when String
818
+ unless @obj.is_a?(Hash)
819
+ raise Error, "parameter #{param_name(nil)} is not a hash" if do_raise
820
+ return
821
+ end
822
+ present = !@obj[key].nil?
823
+ when Integer
824
+ unless @obj.is_a?(Array)
825
+ raise Error, "parameter #{param_name(nil)} is not an array" if do_raise
826
+ return
827
+ end
828
+ present = key < @obj.length
829
+ else
830
+ raise ProgrammerError, "invalid argument used to traverse parameters: #{key.inspect}"
831
+ end
832
+
833
+ unless present
834
+ reason = :missing
835
+ raise Error, "parameter #{param_name(key)} is not present" if do_raise
836
+ return
837
+ end
838
+
839
+ self[key].subkey(keys, do_raise)
840
+ rescue => e
841
+ handle_error(key, reason, e)
842
+ end
843
+
844
+ # Inherit given capturing and symbolize setting from parent object.
845
+ def sub_capture(capture, symbolize, skip_missing)
846
+ if @capture = capture
847
+ @symbolize = symbolize
848
+ @skip_missing = skip_missing
849
+ @params = @obj.class.new
850
+ end
851
+ end
852
+
853
+ private
854
+
855
+ # Whether to symbolize keys when capturing. Note that the method
856
+ # is renamed to +symbolize?+.
857
+ attr_reader :symbolize
858
+ alias symbolize? symbolize
859
+ undef symbolize
860
+
861
+ # Internals of convert! and convert_each!.
862
+ def _capture!(ret, opts)
863
+ previous_symbolize = @symbolize
864
+ previous_skip_missing = @skip_missing
865
+
866
+ unless cap = @capture
867
+ @params = @obj.class.new
868
+ @subs.clear if @subs
869
+ capturing_started = true
870
+ cap = @capture = []
871
+ end
872
+
873
+ if opts.has_key?(:symbolize)
874
+ @symbolize = !!opts[:symbolize]
875
+ end
876
+ if opts.has_key?(:skip_missing)
877
+ @skip_missing = !!opts[:skip_missing]
878
+ end
879
+
880
+ begin
881
+ v = yield
882
+ rescue Error => e
883
+ cap << e unless cap.last == e
884
+ end
885
+
886
+ if capturing_started
887
+ unless cap.empty?
888
+ e = cap[0]
889
+ e.all_errors = cap
890
+ raise e
891
+ end
892
+
893
+ if ret == :nested_params
894
+ nested_params
895
+ else
896
+ v
897
+ end
898
+ end
899
+ ensure
900
+ @nested_params = nil
901
+
902
+ if capturing_started
903
+ # Unset capturing if capturing was already started.
904
+ @capture = nil
905
+ else
906
+ # If capturing was not already started, update cached nested params
907
+ # before resetting symbolize setting.
908
+ @nested_params = nested_params
909
+ end
910
+
911
+ @symbolize = previous_symbolize
912
+ @skip_missing = previous_skip_missing
913
+ end
914
+
915
+ # Raise an error if the array given does contains nil values.
916
+ def check_array!(key, arr)
917
+ if arr
918
+ if arr.any?{|val| val.nil?}
919
+ handle_error(key, :invalid_type, "invalid value in array parameter #{param_name(key)}")
920
+ end
921
+ else
922
+ handle_error(key, :missing, "missing parameter for #{param_name(key)}")
923
+ end
924
+ end
925
+
926
+ # Internals of dig/dig!
927
+ def _dig(force, type, nest, key)
928
+ if type == :array || type == :array!
929
+ conv_type = nest.shift
930
+ unless conv_type.is_a?(Symbol)
931
+ raise ProgrammerError, "incorrect subtype given when using #{type} as argument for dig/dig!: #{conv_type.inspect}"
932
+ end
933
+ meth = type
934
+ type = conv_type
935
+ args = [meth, type]
936
+ else
937
+ meth = type
938
+ args = [type]
939
+ end
940
+
941
+ unless respond_to?("_convert_array_#{type}", true)
942
+ raise ProgrammerError, "no typecast_params type registered for #{meth.inspect}"
943
+ end
944
+
945
+ if v = subkey(nest, force)
946
+ v.send(*args, key, (CHECK_NIL if force))
947
+ end
948
+ end
949
+
950
+ # Format a reasonable parameter name value, for use in exception messages.
951
+ def param_name(key)
952
+ first, *rest = keys(key)
953
+ if first
954
+ v = first.dup
955
+ rest.each do |param|
956
+ v << "[#{param}]"
957
+ end
958
+ v
959
+ end
960
+ end
961
+
962
+ # If +key+ is not +nil+, add it to the given nesting. Otherwise, just return the given nesting.
963
+ # Designed for use in setting the +keys+ values in raised exceptions.
964
+ def keys(key)
965
+ Array(@nesting) + Array(key)
966
+ end
967
+
968
+ # Handle any conversion errors. By default, reraises Error instances with the keys set,
969
+ # converts ::ArgumentError instances to Error instances, and reraises other exceptions.
970
+ def handle_error(key, reason, e, do_raise=false)
971
+ case e
972
+ when String
973
+ handle_error(key, reason, Error.new(e), do_raise)
974
+ when Error, ArgumentError
975
+ if @capture && (le = @capture.last) && le == e
976
+ raise e if do_raise
977
+ return
978
+ end
979
+
980
+ e = Error.create(keys(key), reason, e)
981
+
982
+ if @capture
983
+ @capture << e
984
+ raise e if do_raise
985
+ nil
986
+ else
987
+ raise e
988
+ end
989
+ else
990
+ raise e
991
+ end
992
+ end
993
+
994
+ # Issue an error unless the current object is a hash. Used to ensure we don't try to access
995
+ # entries if the current object is an array.
996
+ def require_hash!
997
+ @obj.is_a?(Hash) || handle_error(nil, :invalid_type, "expected hash object in #{param_name(nil)} but received array object")
998
+ end
999
+
1000
+ # If +key+ is not an array, convert the value at the given +key+ using the +meth+ method and +default+
1001
+ # value. If +key+ is an array, return an array with the conversion done for each respective member of +key+.
1002
+ def process_arg(meth, key, default, type)
1003
+ case key
1004
+ when String
1005
+ v = process(meth, key, default, type)
1006
+
1007
+ if @capture
1008
+ key = key.to_sym if symbolize?
1009
+ if !@skip_missing || @obj.has_key?(key)
1010
+ @params[key] = v
1011
+ end
1012
+ end
1013
+
1014
+ v
1015
+ when Array
1016
+ key.map do |k|
1017
+ raise ProgrammerError, "non-String element in array argument passed to typecast_params: #{k.inspect}" unless k.is_a?(String)
1018
+ process_arg(meth, k, default, type)
1019
+ end
1020
+ else
1021
+ raise ProgrammerError, "Unsupported argument for typecast_params conversion method: #{key.inspect}"
1022
+ end
1023
+ end
1024
+
1025
+ # The invalid message to use if the given type conversion fails, which may be nil to use the default.
1026
+ def _invalid_value_message_for(type)
1027
+ send(:"_invalid_value_message_for_#{type}")
1028
+ end
1029
+
1030
+ # The maximum input bytesize for the given type, which may be nil.
1031
+ def _max_input_bytesize_for(type)
1032
+ send(:"_max_input_bytesize_for_#{type}")
1033
+ end
1034
+
1035
+ # Raise an Error if the value is a string with bytesize over max (if max is given)
1036
+ def check_allowed_bytesize(v, max)
1037
+ if max && v.is_a?(String) && v.bytesize > max
1038
+ handle_error(nil, :too_long, "string parameter is too long for type", true)
1039
+ end
1040
+ end
1041
+
1042
+ # Raise an Error if the value is a string containing a null byte.
1043
+ def check_null_byte(v)
1044
+ if v.is_a?(String) && v.index("\0")
1045
+ handle_error(nil, :null_byte, "string parameter contains null byte", true)
1046
+ end
1047
+ end
1048
+
1049
+ # Get the value of +key+ for the object, and convert it to the expected type using +meth+.
1050
+ # If the value either before or after conversion is nil, return the +default+ value.
1051
+ def process(meth, key, default, type)
1052
+ orig_v = v = param_value(key)
1053
+
1054
+ if v.nil?
1055
+ if default == CHECK_NIL
1056
+ handle_error(key, :missing, "missing parameter for #{param_name(key)}")
1057
+ end
1058
+ else
1059
+ check_allowed_bytesize(v, _max_input_bytesize_for(type))
1060
+ check_null_byte(v)
1061
+ v = send(meth, v)
1062
+ end
1063
+
1064
+ if v.nil?
1065
+ if !orig_v.nil? && default == CHECK_NIL
1066
+ invalid_value_message = _invalid_value_message_for(type)
1067
+ invalid_value_message ||= "invalid parameter value for"
1068
+ handle_error(key, :invalid_value, "#{invalid_value_message} #{param_name(key)}")
1069
+ end
1070
+
1071
+ default
1072
+ else
1073
+ v
1074
+ end
1075
+ rescue => e
1076
+ handle_error(key, meth.to_s.sub(/\A_?convert_/, '').to_sym, e)
1077
+ end
1078
+
1079
+ # Get the value for the given key in the object.
1080
+ def param_value(key)
1081
+ @obj[key]
1082
+ end
1083
+
1084
+ # Helper for conversion methods where '' should be considered nil,
1085
+ # and only String or Numeric values should be converted.
1086
+ def string_or_numeric!(v)
1087
+ case v
1088
+ when ''
1089
+ nil
1090
+ when String, Numeric
1091
+ true
1092
+ else
1093
+ raise Error, "unexpected value received: #{v.inspect}"
1094
+ end
1095
+ end
1096
+
1097
+ # Helper for conversion methods where '' should be considered nil,
1098
+ # and only String values should be converted by calling +parse+ on
1099
+ # the given +klass+.
1100
+ def parse!(klass, v)
1101
+ case v
1102
+ when ''
1103
+ nil
1104
+ when String
1105
+ _string_parse!(klass, v)
1106
+ else
1107
+ raise Error, "unexpected value received: #{v.inspect}"
1108
+ end
1109
+ end
1110
+
1111
+ # Handle parsing for string values passed to parse!.
1112
+ def _string_parse!(klass, v)
1113
+ klass.parse(v)
1114
+ end
1115
+ end
1116
+
1117
+ # Set application-specific Params subclass unless one has been set,
1118
+ # and if a block is passed, eval it in the context of the subclass.
1119
+ # Respect the <tt>strip: :all</tt> to strip all parameter strings
1120
+ # before processing them.
1121
+ def self.configure(app, opts=OPTS, &block)
1122
+ app.const_set(:TypecastParams, Class.new(RodaPlugins::TypecastParams::Params)) unless app.const_defined?(:TypecastParams)
1123
+ app::TypecastParams.class_eval(&block) if block
1124
+ if opts[:strip] == :all
1125
+ app::TypecastParams.send(:include, StringStripper)
1126
+ end
1127
+ if opts[:allow_null_bytes]
1128
+ app::TypecastParams.send(:include, AllowNullByte)
1129
+ end
1130
+ if opts[:skip_bytesize_checking]
1131
+ app::TypecastParams.send(:include, SkipBytesizeChecking)
1132
+ end
1133
+ if opts[:date_parse_input_handler]
1134
+ app::TypecastParams.class_eval do
1135
+ include DateParseInputHandler
1136
+ define_method(:handle_date_parse_input, &opts[:date_parse_input_handler])
1137
+ private :handle_date_parse_input
1138
+ alias handle_date_parse_input handle_date_parse_input
1139
+ end
1140
+ end
1141
+ end
1142
+
1143
+ module ClassMethods
1144
+ # Freeze the Params subclass when freezing the class.
1145
+ def freeze
1146
+ self::TypecastParams.freeze
1147
+ super
1148
+ end
1149
+
1150
+ # Assign the application subclass a subclass of the current Params subclass.
1151
+ def inherited(subclass)
1152
+ super
1153
+ subclass.const_set(:TypecastParams, Class.new(self::TypecastParams))
1154
+ end
1155
+ end
1156
+
1157
+ module InstanceMethods
1158
+ # Return and cache the instance of the TypecastParams class wrapping access
1159
+ # to the request's params (merging query string params and body params).
1160
+ # Type conversion methods will be called on the result of this method.
1161
+ def typecast_params
1162
+ @_typecast_params ||= self.class::TypecastParams.new(@_request.params)
1163
+ end
1164
+
1165
+ # Return and cache the instance of the TypecastParams class wrapping
1166
+ # access to parameters in the request's query string.
1167
+ # Type conversion methods will be called on the result of this method.
1168
+ def typecast_query_params
1169
+ @_typecast_query_params ||= self.class::TypecastParams.new(@_request.GET)
1170
+ end
1171
+
1172
+ # Return and cache the instance of the TypecastParams class wrapping
1173
+ # access to parameters in the request's body.
1174
+ # Type conversion methods will be called on the result of this method.
1175
+ def typecast_body_params
1176
+ @_typecast_body_params ||= self.class::TypecastParams.new(@_request.POST)
1177
+ end
1178
+ end
1179
+ end
1180
+
1181
+ register_plugin(:typecast_params, TypecastParams)
1182
+ end
1183
+ end