ultra-max-sys 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.

Potentially problematic release.


This version of ultra-max-sys might be problematic. Click here for more details.

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