iknow_params 2.2.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/core_ext/hash/indifferent_access"
4
+
5
+ # Simple wrapper to use IknowParams::Parser to extract and verify content from
6
+ # an arbitrary hash
7
+ class IknowParams::Parser::HashParser
8
+ include IknowParams::Parser
9
+
10
+ attr_reader :params
11
+
12
+ def initialize(view_hash)
13
+ @params = ActiveSupport::HashWithIndifferentAccess.new(view_hash)
14
+ end
15
+
16
+ def parse(&block)
17
+ instance_exec(&block)
18
+ end
19
+ end
@@ -0,0 +1,361 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support'
4
+ require 'active_support/duration'
5
+ require 'active_support/inflector'
6
+ require 'active_support/core_ext/module/delegation'
7
+ require 'tzinfo'
8
+ require 'json-schema'
9
+
10
+ class IknowParams::Serializer
11
+ class LoadError < ArgumentError; end
12
+ class DumpError < ArgumentError; end
13
+
14
+ attr_reader :clazz
15
+
16
+ def initialize(clazz)
17
+ @clazz = clazz
18
+ end
19
+
20
+ def dump(val, json: false)
21
+ matches_type!(val)
22
+ if json && self.class.json_value?
23
+ val
24
+ else
25
+ val.to_s
26
+ end
27
+ end
28
+
29
+ def load(_val)
30
+ raise StandardError.new('unimplemented')
31
+ end
32
+
33
+ def matches_type?(val)
34
+ val.is_a?(clazz)
35
+ end
36
+
37
+ def matches_type!(val, err: DumpError)
38
+ unless matches_type?(val)
39
+ raise err.new("Incorrect type for #{self.class.name}: #{val.inspect}:#{val.class.name}")
40
+ end
41
+ true
42
+ end
43
+
44
+ @registry = {}
45
+ class << self
46
+ delegate :load, :dump, to: :singleton
47
+
48
+ def singleton
49
+ raise ArgumentError.new("Singleton instance not defined for abstract serializer '#{self.name}'")
50
+ end
51
+
52
+ def json_value?
53
+ false
54
+ end
55
+
56
+ def for(name)
57
+ @registry[name.to_s]
58
+ end
59
+
60
+ def for!(name)
61
+ s = self.for(name)
62
+ raise ArgumentError.new("No serializer registered with name: '#{name}'") if s.nil?
63
+ s
64
+ end
65
+
66
+ protected
67
+
68
+ def register_serializer(name, serializer)
69
+ @registry[name] = serializer
70
+ IknowParams::Parser.register_serializer(name, serializer)
71
+ end
72
+
73
+ private
74
+
75
+ def set_singleton!
76
+ instance = self.new
77
+ define_singleton_method(:singleton) { instance }
78
+ IknowParams::Serializer.register_serializer(self.name.demodulize, instance)
79
+ end
80
+
81
+ def json_value!
82
+ define_singleton_method(:json_value?) { true }
83
+ end
84
+ end
85
+
86
+ class String < IknowParams::Serializer
87
+ def initialize
88
+ super(::String)
89
+ end
90
+
91
+ def load(str)
92
+ matches_type!(str, err: LoadError)
93
+ str
94
+ end
95
+
96
+ set_singleton!
97
+ json_value!
98
+ end
99
+
100
+ class Integer < IknowParams::Serializer
101
+ def initialize
102
+ super(::Integer)
103
+ end
104
+
105
+ # JSON only supports floats, so we have to accept a value
106
+ # which may have already been parsed into a Ruby Float or Integer.
107
+ def load(str_or_num)
108
+ raise LoadError.new("Invalid integer: #{str_or_num}") unless [::String, ::Integer].any? { |t| str_or_num.is_a?(t) }
109
+ Integer(str_or_num)
110
+ rescue ArgumentError => e
111
+ raise LoadError.new(e.message)
112
+ end
113
+
114
+ set_singleton!
115
+ json_value!
116
+ end
117
+
118
+ class Float < IknowParams::Serializer
119
+ def initialize
120
+ super(::Float)
121
+ end
122
+
123
+ def load(str)
124
+ Float(str)
125
+ rescue TypeError, ArgumentError => _e
126
+ raise LoadError.new("Invalid type for conversion to Float")
127
+ end
128
+
129
+ set_singleton!
130
+ json_value!
131
+ end
132
+
133
+ class Boolean < IknowParams::Serializer
134
+ def initialize
135
+ super(nil)
136
+ end
137
+
138
+ def load(str)
139
+ str = str.downcase if str.is_a?(::String)
140
+
141
+ if ['false', 'no', 'off', false, '0', 0].include?(str)
142
+ false
143
+ elsif ['true', 'yes', 'on', true, '1', 1].include?(str)
144
+ true
145
+ else
146
+ raise LoadError.new("Invalid boolean: #{str.inspect}")
147
+ end
148
+ end
149
+
150
+ def matches_type?(val)
151
+ [true, false].include?(val)
152
+ end
153
+
154
+ set_singleton!
155
+ json_value!
156
+ end
157
+
158
+ class Numeric < IknowParams::Serializer
159
+ def initialize
160
+ super(::Numeric)
161
+ end
162
+
163
+ def load(str)
164
+ Float(str)
165
+ rescue TypeError, ArgumentError => _e
166
+ raise LoadError.new("Invalid type for conversion to Numeric")
167
+ end
168
+
169
+ set_singleton!
170
+ json_value!
171
+ end
172
+
173
+ # Abstract serializer for ISO8601 dates and times
174
+ class ISO8601 < IknowParams::Serializer
175
+ def load(str)
176
+ clazz.parse(str)
177
+ rescue TypeError, ArgumentError => _e
178
+ raise LoadError.new("Invalid type for conversion to #{clazz}")
179
+ end
180
+
181
+ def dump(val, json: nil)
182
+ matches_type!(val)
183
+ val.iso8601
184
+ end
185
+ end
186
+
187
+ class Date < ISO8601
188
+ def initialize
189
+ super(::Date)
190
+ end
191
+
192
+ set_singleton!
193
+ end
194
+
195
+ class Time < ISO8601
196
+ def initialize
197
+ super(::Time)
198
+ end
199
+
200
+ set_singleton!
201
+ end
202
+
203
+ class Duration < ISO8601
204
+ def initialize
205
+ super(::ActiveSupport::Duration)
206
+ end
207
+
208
+ set_singleton!
209
+ end
210
+
211
+
212
+ class Timezone < IknowParams::Serializer
213
+ def initialize
214
+ super(::TZInfo::Timezone)
215
+ end
216
+
217
+ def load(str)
218
+ TZInfo::Timezone.get(str)
219
+ rescue TZInfo::InvalidTimezoneIdentifier => _e
220
+ raise LoadError.new("Invalid identifier for TZInfo zone: #{str}")
221
+ end
222
+
223
+ def dump(val, json: nil)
224
+ matches_type!(val)
225
+ val.identifier
226
+ end
227
+
228
+ set_singleton!
229
+ end
230
+
231
+ class UUID < String
232
+ def load(str)
233
+ matches_type!(str, err: LoadError)
234
+ super
235
+ end
236
+
237
+ def matches_type?(str)
238
+ super && /[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}/i.match?(str)
239
+ end
240
+
241
+ set_singleton!
242
+ json_value!
243
+ end
244
+
245
+ # Abstract serializer for JSON structures conforming to a specified
246
+ # schema.
247
+ class JsonWithSchema < IknowParams::Serializer
248
+ attr_reader :schema
249
+ def initialize(schema, validate_schema: true)
250
+ @schema = schema
251
+ @validate_schema = validate_schema
252
+ super(nil)
253
+ end
254
+
255
+ def load(structure)
256
+ structure = JSON.parse(structure) if structure.is_a?(::String)
257
+ matches_type!(structure, err: LoadError)
258
+ structure
259
+ rescue JSON::ParserError => ex
260
+ raise LoadError.new("Invalid JSON: #{ex.message}")
261
+ end
262
+
263
+ def dump(val, json: false)
264
+ matches_type!(val)
265
+ if json
266
+ val
267
+ else
268
+ JSON.dump(val)
269
+ end
270
+ end
271
+
272
+ def matches_type?(val)
273
+ JSON::Validator.validate(schema, val, validate_schema: @validate_schema)
274
+ end
275
+
276
+ json_value!
277
+ end
278
+
279
+ # Adds Rails conveniences
280
+ class JsonWithSchema
281
+ class Rails < JsonWithSchema
282
+ def initialize(schema)
283
+ super(schema, validate_schema: !::Rails.env.production?)
284
+ end
285
+
286
+ def load(structure)
287
+ super(convert_strong_parameters(structure))
288
+ end
289
+
290
+ private
291
+
292
+ def convert_strong_parameters(structure)
293
+ case structure
294
+ when ActionController::Parameters
295
+ structure.to_unsafe_h
296
+ when Array
297
+ structure.dup.map { |x| convert_strong_parameters(x) }
298
+ else
299
+ structure
300
+ end
301
+ end
302
+ end
303
+ end
304
+
305
+ ## Abstract serializer for `ActsAsEnum` constants.
306
+ class ActsAsEnum < IknowParams::Serializer
307
+ def load(str)
308
+ constant = clazz.value_of(str)
309
+ if constant.nil?
310
+ raise LoadError.new("Invalid #{clazz.name} member: '#{str}'")
311
+ end
312
+ constant
313
+ end
314
+
315
+ def dump(val, json: nil)
316
+ matches_type!(val)
317
+ val.enum_constant
318
+ end
319
+
320
+ def matches_type?(val)
321
+ return true if super(val)
322
+ dc = clazz.dummy_class
323
+ dc.present? && val.is_a?(dc)
324
+ end
325
+ end
326
+
327
+ ## Abstract serializer for `renum` constants.
328
+ class Renum < IknowParams::Serializer
329
+ def load(str)
330
+ val = clazz.with_name(str)
331
+ if val.nil?
332
+ raise LoadError.new("Invalid enumeration constant: '#{str}'")
333
+ end
334
+ val
335
+ end
336
+
337
+ def dump(val, json: nil)
338
+ matches_type!(val)
339
+ val.name
340
+ end
341
+ end
342
+
343
+ # Abstract serializer for members of a fixed set of lowercase strings,
344
+ # case-normalized on parse.
345
+ class StringEnum < IknowParams::Serializer
346
+ def initialize(*members)
347
+ @member_set = members.map(&:downcase).to_set.freeze
348
+ super(nil)
349
+ end
350
+
351
+ def load(str)
352
+ val = str.to_s.downcase
353
+ matches_type!(val, err: LoadError)
354
+ val
355
+ end
356
+
357
+ def matches_type?(str)
358
+ str.is_a?(::String) && @member_set.include?(str)
359
+ end
360
+ end
361
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module IknowParams
4
+ VERSION = "2.2.1"
5
+ end
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ # This file was generated by the `rspec --init` command. Conventionally, all
4
+ # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
5
+ # The generated `.rspec` file contains `--require spec_helper` which will cause
6
+ # this file to always be loaded, without a need to explicitly require it in any
7
+ # files.
8
+ #
9
+ # Given that it is always loaded, you are encouraged to keep this file as
10
+ # light-weight as possible. Requiring heavyweight dependencies from this file
11
+ # will add to the boot time of your test suite on EVERY test run, even for an
12
+ # individual file that may not need all of that loaded. Instead, consider making
13
+ # a separate helper file that requires the additional dependencies and performs
14
+ # the additional setup, and require it from the spec files that actually need
15
+ # it.
16
+ #
17
+ # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
18
+ RSpec.configure do |config|
19
+ # rspec-expectations config goes here. You can use an alternate
20
+ # assertion/expectation library such as wrong or the stdlib/minitest
21
+ # assertions if you prefer.
22
+ config.expect_with :rspec do |expectations|
23
+ # This option will default to `true` in RSpec 4. It makes the `description`
24
+ # and `failure_message` of custom matchers include text for helper methods
25
+ # defined using `chain`, e.g.:
26
+ # be_bigger_than(2).and_smaller_than(4).description
27
+ # # => "be bigger than 2 and smaller than 4"
28
+ # ...rather than:
29
+ # # => "be bigger than 2"
30
+ expectations.include_chain_clauses_in_custom_matcher_descriptions = true
31
+ end
32
+
33
+ # rspec-mocks config goes here. You can use an alternate test double
34
+ # library (such as bogus or mocha) by changing the `mock_with` option here.
35
+ config.mock_with :rspec do |mocks|
36
+ # Prevents you from mocking or stubbing a method that does not exist on
37
+ # a real object. This is generally recommended, and will default to
38
+ # `true` in RSpec 4.
39
+ mocks.verify_partial_doubles = true
40
+ end
41
+
42
+ # This option will default to `:apply_to_host_groups` in RSpec 4 (and will
43
+ # have no way to turn it off -- the option exists only for backwards
44
+ # compatibility in RSpec 3). It causes shared context metadata to be
45
+ # inherited by the metadata hash of host groups and examples, rather than
46
+ # triggering implicit auto-inclusion in groups with matching metadata.
47
+ config.shared_context_metadata_behavior = :apply_to_host_groups
48
+
49
+ # The settings below are suggested to provide a good initial experience
50
+ # with RSpec, but feel free to customize to your heart's content.
51
+ =begin
52
+ # This allows you to limit a spec run to individual examples or groups
53
+ # you care about by tagging them with `:focus` metadata. When nothing
54
+ # is tagged with `:focus`, all examples get run. RSpec also provides
55
+ # aliases for `it`, `describe`, and `context` that include `:focus`
56
+ # metadata: `fit`, `fdescribe` and `fcontext`, respectively.
57
+ config.filter_run_when_matching :focus
58
+
59
+ # Allows RSpec to persist some state between runs in order to support
60
+ # the `--only-failures` and `--next-failure` CLI options. We recommend
61
+ # you configure your source control system to ignore this file.
62
+ config.example_status_persistence_file_path = "spec/examples.txt"
63
+
64
+ # Limits the available syntax to the non-monkey patched syntax that is
65
+ # recommended. For more details, see:
66
+ # - http://rspec.info/blog/2012/06/rspecs-new-expectation-syntax/
67
+ # - http://www.teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/
68
+ # - http://rspec.info/blog/2014/05/notable-changes-in-rspec-3/#zero-monkey-patching-mode
69
+ config.disable_monkey_patching!
70
+
71
+ # This setting enables warnings. It's recommended, but in some cases may
72
+ # be too noisy due to issues in dependencies.
73
+ config.warnings = true
74
+
75
+ # Many RSpec users commonly either run the entire suite or an individual
76
+ # file, and it's useful to allow more verbose output when running an
77
+ # individual spec file.
78
+ if config.files_to_run.one?
79
+ # Use the documentation formatter for detailed output,
80
+ # unless a formatter has already been configured
81
+ # (e.g. via a command-line flag).
82
+ config.default_formatter = "doc"
83
+ end
84
+
85
+ # Print the 10 slowest examples and example groups at the
86
+ # end of the spec run, to help surface which specs are running
87
+ # particularly slow.
88
+ config.profile_examples = 10
89
+
90
+ # Run specs in random order to surface order dependencies. If you find an
91
+ # order dependency and want to debug it, you can fix the order by providing
92
+ # the seed, which is printed after each run.
93
+ # --seed 1234
94
+ config.order = :random
95
+
96
+ # Seed global randomization in this process using the `--seed` CLI option.
97
+ # Setting this allows you to use `--seed` to deterministically reproduce
98
+ # test failures related to randomization by passing the same `--seed` value
99
+ # as the one that triggered the failure.
100
+ Kernel.srand config.seed
101
+ =end
102
+ end