fend 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +516 -0
- data/fend.gemspec +21 -0
- data/lib/fend.rb +296 -0
- data/lib/fend/plugins/coercions.rb +442 -0
- data/lib/fend/plugins/collective_params.rb +60 -0
- data/lib/fend/plugins/data_processing.rb +212 -0
- data/lib/fend/plugins/dependencies.rb +130 -0
- data/lib/fend/plugins/external_validation.rb +98 -0
- data/lib/fend/plugins/full_messages.rb +67 -0
- data/lib/fend/plugins/validation_helpers.rb +246 -0
- data/lib/fend/plugins/validation_options.rb +116 -0
- data/lib/fend/plugins/value_helpers.rb +148 -0
- data/lib/fend/version.rb +13 -0
- metadata +86 -0
data/fend.gemspec
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
require File.expand_path("../lib/fend/version", __FILE__)
|
2
|
+
|
3
|
+
Gem::Specification.new do |gem|
|
4
|
+
gem.name = "fend"
|
5
|
+
gem.version = Fend.version
|
6
|
+
gem.authors = ["Aleksandar Radunovic"]
|
7
|
+
gem.email = ["aleksandar@radunovic.io"]
|
8
|
+
|
9
|
+
gem.summary = "Small and extensible data validation toolkit"
|
10
|
+
gem.description = gem.summary
|
11
|
+
gem.homepage = "https://fend.radunovic.io"
|
12
|
+
gem.license = "MIT"
|
13
|
+
|
14
|
+
gem.files = Dir["README.md", "LICENSE.txt", "lib/**/*.rb", "fend.gemspec"]
|
15
|
+
gem.require_path = "lib"
|
16
|
+
|
17
|
+
gem.required_ruby_version = ">= 2.0"
|
18
|
+
|
19
|
+
gem.add_development_dependency "rake"
|
20
|
+
gem.add_development_dependency "rspec"
|
21
|
+
end
|
data/lib/fend.rb
ADDED
@@ -0,0 +1,296 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Fend
|
4
|
+
# Generic error class
|
5
|
+
class Error < StandardError; end
|
6
|
+
|
7
|
+
# Core class that represents validation param. Class methods are added
|
8
|
+
# by Fend::Plugins::Core::ParamClassMethods module.
|
9
|
+
# Instance methods are added by Fend::Plugins::Core::ParamMethods module.
|
10
|
+
class Param
|
11
|
+
@fend_class = ::Fend
|
12
|
+
end
|
13
|
+
|
14
|
+
# Core class that represents validation result.
|
15
|
+
# Class methods are added by Fend::Plugins::Core::ResultClassMethods.
|
16
|
+
# Instance methods are added by Fend::Plugins::Core::ResultMethods.
|
17
|
+
class Result
|
18
|
+
@fend_class = ::Fend
|
19
|
+
end
|
20
|
+
|
21
|
+
@opts = {}
|
22
|
+
@validation_block = nil
|
23
|
+
|
24
|
+
# Module in which all Fend plugins should be defined.
|
25
|
+
module Plugins
|
26
|
+
@plugins = {}
|
27
|
+
|
28
|
+
# Use plugin if already loaded. If not, load and return it.
|
29
|
+
def self.load_plugin(name)
|
30
|
+
unless plugin = @plugins[name]
|
31
|
+
require "fend/plugins/#{name}"
|
32
|
+
|
33
|
+
raise Error, "plugin #{name} did not register itself correctly in Fend::Plugins" unless plugin = @plugins[name]
|
34
|
+
end
|
35
|
+
plugin
|
36
|
+
end
|
37
|
+
|
38
|
+
# Register plugin so that it can loaded.
|
39
|
+
def self.register_plugin(name, mod)
|
40
|
+
@plugins[name] = mod
|
41
|
+
end
|
42
|
+
|
43
|
+
# Core plugin. Provides core functionality.
|
44
|
+
module Core
|
45
|
+
module ClassMethods
|
46
|
+
attr_reader :opts
|
47
|
+
|
48
|
+
attr_reader :validation_block
|
49
|
+
|
50
|
+
def inherited(subclass)
|
51
|
+
subclass.instance_variable_set(:@opts, opts.dup)
|
52
|
+
subclass.opts.each do |key, value|
|
53
|
+
if (value.is_a?(Array) || value.is_a?(Hash)) && !value.frozen?
|
54
|
+
subclass.opts[key] = value.dup
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
param_class = Class.new(self::Param)
|
59
|
+
param_class.fend_class = subclass
|
60
|
+
subclass.const_set(:Param, param_class)
|
61
|
+
|
62
|
+
result_class = Class.new(self::Result)
|
63
|
+
result_class.fend_class = subclass
|
64
|
+
subclass.const_set(:Result, result_class)
|
65
|
+
end
|
66
|
+
|
67
|
+
def plugin(plugin, *args, &block)
|
68
|
+
plugin = Plugins.load_plugin(plugin) if plugin.is_a?(Symbol)
|
69
|
+
plugin.load_dependencies(self, *args, &block) if plugin.respond_to?(:load_dependencies)
|
70
|
+
|
71
|
+
include(plugin::InstanceMethods) if defined?(plugin::InstanceMethods)
|
72
|
+
extend(plugin::ClassMethods) if defined?(plugin::ClassMethods)
|
73
|
+
|
74
|
+
self::Param.send(:include, plugin::ParamMethods) if defined?(plugin::ParamMethods)
|
75
|
+
self::Param.extend(plugin::ParamClassMethods) if defined?(plugin::ParamClassMethods)
|
76
|
+
|
77
|
+
self::Result.send(:include, plugin::ResultMethods) if defined?(plugin::ResultMethods)
|
78
|
+
self::Result.extend(plugin::ResultClassMethods) if defined?(plugin::ResultClassMethods)
|
79
|
+
|
80
|
+
plugin.configure(self, *args, &block) if plugin.respond_to?(:configure)
|
81
|
+
|
82
|
+
plugin
|
83
|
+
end
|
84
|
+
|
85
|
+
# Store validation block for later execution:
|
86
|
+
#
|
87
|
+
# validate do |i|
|
88
|
+
# i.param(:foo) do |foo|
|
89
|
+
# # foo validation logic
|
90
|
+
# end
|
91
|
+
# end
|
92
|
+
def validate(&block)
|
93
|
+
@validation_block = block
|
94
|
+
end
|
95
|
+
|
96
|
+
def call(input)
|
97
|
+
new.call(input)
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
module InstanceMethods
|
102
|
+
# Trigger data validation and return Result
|
103
|
+
def call(raw_data)
|
104
|
+
set_data(raw_data)
|
105
|
+
validate(&validation_block)
|
106
|
+
|
107
|
+
result(input: @_input_data, output: @_output_data, errors: @_input_param.errors)
|
108
|
+
end
|
109
|
+
|
110
|
+
# Set:
|
111
|
+
# * raw input data
|
112
|
+
# * validation input data
|
113
|
+
# * result output data
|
114
|
+
# * input param
|
115
|
+
def set_data(raw_data)
|
116
|
+
@_raw_data = raw_data
|
117
|
+
@_input_data = process_input(raw_data) || raw_data
|
118
|
+
@_output_data = process_output(@_input_data) || @_input_data
|
119
|
+
@_input_param = param_class.new(@_input_data)
|
120
|
+
end
|
121
|
+
|
122
|
+
# Returns validation block set on class level
|
123
|
+
def validation_block
|
124
|
+
self.class.validation_block
|
125
|
+
end
|
126
|
+
|
127
|
+
# Get validation param class
|
128
|
+
def param_class
|
129
|
+
self.class::Param
|
130
|
+
end
|
131
|
+
|
132
|
+
# Get validation result class
|
133
|
+
def result_class
|
134
|
+
self.class::Result
|
135
|
+
end
|
136
|
+
|
137
|
+
# Process input data
|
138
|
+
def process_input(input); end
|
139
|
+
|
140
|
+
# Process output data
|
141
|
+
def process_output(output); end
|
142
|
+
|
143
|
+
# Start validation
|
144
|
+
def validate(&block)
|
145
|
+
yield(@_input_param) if block_given?
|
146
|
+
end
|
147
|
+
|
148
|
+
# Instantiate and return result
|
149
|
+
def result(args)
|
150
|
+
result_class.new(args)
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
module ParamClassMethods
|
155
|
+
# References Fend class under which the param class is namespaced
|
156
|
+
attr_accessor :fend_class
|
157
|
+
end
|
158
|
+
|
159
|
+
module ParamMethods
|
160
|
+
# Get param value
|
161
|
+
attr_reader :value
|
162
|
+
|
163
|
+
# Get param validation errors
|
164
|
+
attr_reader :errors
|
165
|
+
|
166
|
+
def initialize(value)
|
167
|
+
@value = value
|
168
|
+
@errors = []
|
169
|
+
end
|
170
|
+
|
171
|
+
# Fetch nested value
|
172
|
+
def [](name)
|
173
|
+
@value.fetch(name, nil) if @value.respond_to?(:fetch)
|
174
|
+
end
|
175
|
+
|
176
|
+
# Define child param and execute validation block
|
177
|
+
def param(name, &block)
|
178
|
+
return if flat? && invalid?
|
179
|
+
|
180
|
+
value = self[name]
|
181
|
+
param = _build_param(value)
|
182
|
+
|
183
|
+
yield(param)
|
184
|
+
|
185
|
+
_nest_errors(name, param.errors) if param.invalid?
|
186
|
+
end
|
187
|
+
|
188
|
+
# Define array member param and execute validation block
|
189
|
+
def each(&block)
|
190
|
+
return if (flat? && invalid?) || !@value.is_a?(Array)
|
191
|
+
|
192
|
+
@value.each_with_index do |value, index|
|
193
|
+
param = _build_param(value)
|
194
|
+
|
195
|
+
yield(param, index)
|
196
|
+
|
197
|
+
_nest_errors(index, param.errors) if param.invalid?
|
198
|
+
end
|
199
|
+
end
|
200
|
+
|
201
|
+
# Returns true if param is valid (no errors)
|
202
|
+
def valid?
|
203
|
+
errors.empty?
|
204
|
+
end
|
205
|
+
|
206
|
+
# Returns true if param is invalid/errors are present
|
207
|
+
def invalid?
|
208
|
+
!valid?
|
209
|
+
end
|
210
|
+
|
211
|
+
# Append param error message
|
212
|
+
def add_error(message)
|
213
|
+
@errors << message
|
214
|
+
end
|
215
|
+
|
216
|
+
def inspect
|
217
|
+
"#{fend_class.inspect}::Param #{super}"
|
218
|
+
end
|
219
|
+
|
220
|
+
def to_s
|
221
|
+
"#{fend_class.inspect}::Param"
|
222
|
+
end
|
223
|
+
|
224
|
+
# Return Fend class under which Param class is namespaced
|
225
|
+
def fend_class
|
226
|
+
self.class::fend_class
|
227
|
+
end
|
228
|
+
|
229
|
+
private
|
230
|
+
|
231
|
+
def flat?
|
232
|
+
errors.is_a?(Array)
|
233
|
+
end
|
234
|
+
|
235
|
+
def _nest_errors(name, messages)
|
236
|
+
@errors = {} unless @errors.is_a?(Hash)
|
237
|
+
@errors[name] = messages
|
238
|
+
end
|
239
|
+
|
240
|
+
def _build_param(*args)
|
241
|
+
self.class.new(*args)
|
242
|
+
end
|
243
|
+
end
|
244
|
+
|
245
|
+
module ResultClassMethods
|
246
|
+
attr_accessor :fend_class
|
247
|
+
end
|
248
|
+
|
249
|
+
module ResultMethods
|
250
|
+
# Get raw input data
|
251
|
+
attr_reader :input
|
252
|
+
|
253
|
+
# Get output data
|
254
|
+
attr_reader :output
|
255
|
+
|
256
|
+
def initialize(args = {})
|
257
|
+
@input = args.fetch(:input)
|
258
|
+
@output = args.fetch(:output)
|
259
|
+
@errors = args.fetch(:errors)
|
260
|
+
end
|
261
|
+
|
262
|
+
# Get error messages
|
263
|
+
def messages
|
264
|
+
return {} if success?
|
265
|
+
|
266
|
+
@errors
|
267
|
+
end
|
268
|
+
|
269
|
+
# Check if if validation failed
|
270
|
+
def failure?
|
271
|
+
!success?
|
272
|
+
end
|
273
|
+
|
274
|
+
# Check if if validation succeeded
|
275
|
+
def success?
|
276
|
+
@errors.empty?
|
277
|
+
end
|
278
|
+
|
279
|
+
def fend_class
|
280
|
+
self.class.fend_class
|
281
|
+
end
|
282
|
+
|
283
|
+
def inspect
|
284
|
+
"#{fend_class.inspect}::Result"
|
285
|
+
end
|
286
|
+
|
287
|
+
def to_s
|
288
|
+
"#{fend_class.inspect}::Result"
|
289
|
+
end
|
290
|
+
end
|
291
|
+
end
|
292
|
+
end
|
293
|
+
|
294
|
+
extend Fend::Plugins::Core::ClassMethods
|
295
|
+
plugin Fend::Plugins::Core
|
296
|
+
end
|
@@ -0,0 +1,442 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "bigdecimal"
|
4
|
+
require "bigdecimal/util"
|
5
|
+
|
6
|
+
require "date"
|
7
|
+
require "time"
|
8
|
+
|
9
|
+
class Fend
|
10
|
+
module Plugins
|
11
|
+
# `coercions` plugin provides a way to coerce validaiton input.
|
12
|
+
# First, the plugin needs to be loaded
|
13
|
+
#
|
14
|
+
# plugin :coercions
|
15
|
+
#
|
16
|
+
# Because of Fend's dynamic nature, coercion is separated from validation.
|
17
|
+
# As such, coercion needs to be done before the actual validation. In order
|
18
|
+
# to make this work, type schema must be passed to `coerce` method.
|
19
|
+
#
|
20
|
+
# coerce username: :string, age: :integer, admin: :boolean
|
21
|
+
#
|
22
|
+
# As you can see, type schema is just a hash containing param names and
|
23
|
+
# types to which the values need to be converted. Here are some examples:
|
24
|
+
#
|
25
|
+
# # coerce username value to string
|
26
|
+
# coerce(username: :string)
|
27
|
+
#
|
28
|
+
# # coerce address value to hash
|
29
|
+
# coerce(address: :hash)
|
30
|
+
#
|
31
|
+
# # coerce address value to hash
|
32
|
+
# # coerce address[:city] value to string
|
33
|
+
# # coerce address[:street] value to string
|
34
|
+
# coerce(address: { city: :string, street: :string })
|
35
|
+
#
|
36
|
+
# # coerce tags to an array
|
37
|
+
# coerce(tags: :array)
|
38
|
+
#
|
39
|
+
# # coerce tags to an array of strings
|
40
|
+
# coerce(tags: [:string])
|
41
|
+
#
|
42
|
+
# # coerce tags to an array of hashes, each containing `id` and `name` of the tag
|
43
|
+
# coerce(tags: [{ id: :integer, name: :string }])
|
44
|
+
#
|
45
|
+
# Coerced data will also serve as result output:
|
46
|
+
#
|
47
|
+
# result = UserValidation.call(username: 1234, age: "18", admin: 0)
|
48
|
+
# result.output #=> { username: "1234", age: 18, admin: false }
|
49
|
+
#
|
50
|
+
# ## Built-in coercions
|
51
|
+
#
|
52
|
+
# General rules:
|
53
|
+
#
|
54
|
+
# * If input value **cannot** be coerced to specified type, it is returned
|
55
|
+
# unmodified.
|
56
|
+
#
|
57
|
+
# * `nil` is returned if input value is an empty string, except for `:hash`
|
58
|
+
# and `:array` coercions.
|
59
|
+
#
|
60
|
+
# :any
|
61
|
+
# : Returns input
|
62
|
+
#
|
63
|
+
# :string
|
64
|
+
# : Returns `input.to_s` if input is `Numeric` or `Symbol`
|
65
|
+
#
|
66
|
+
# :symbol
|
67
|
+
# : Returns `input.to_sym` if `input.respond_to?(:to_sym)`
|
68
|
+
#
|
69
|
+
# :integer
|
70
|
+
# : Uses `Kernel.Integer(input)`
|
71
|
+
#
|
72
|
+
# :float
|
73
|
+
# : Uses `Kernel.Float(input)`
|
74
|
+
#
|
75
|
+
# :decimal
|
76
|
+
# : Uses `Kernel.Float(input).to_d`
|
77
|
+
#
|
78
|
+
# :date
|
79
|
+
# : Uses `Date.parse(input)`
|
80
|
+
#
|
81
|
+
# :date_time
|
82
|
+
# : Uses `DateTime.parse(input)`
|
83
|
+
#
|
84
|
+
# :time
|
85
|
+
# : Uses `Time.parse(input)`
|
86
|
+
#
|
87
|
+
# :boolean
|
88
|
+
# : Returns `true` if input is one of:
|
89
|
+
# `1, "1", "t", "true", :true "y","yes", "on"` (case insensitive)
|
90
|
+
#
|
91
|
+
# : Returns `false` if input is one of:
|
92
|
+
# `0, "0", "f", "false", :false, "n", "no", "off"` (case insensitive)
|
93
|
+
#
|
94
|
+
# :array
|
95
|
+
# : Returns `[]` if input is an empty string.
|
96
|
+
#
|
97
|
+
# : Returns input if input is an array
|
98
|
+
#
|
99
|
+
# :hash
|
100
|
+
# : Returns `{}` if input is an empty string.
|
101
|
+
#
|
102
|
+
# : Returns input if input is a hash
|
103
|
+
#
|
104
|
+
# ## Strict coercions
|
105
|
+
#
|
106
|
+
# Adding `strict_` prefix to type name will cause error to be raised
|
107
|
+
# when input is incoercible:
|
108
|
+
#
|
109
|
+
# coerce username: :strict_string
|
110
|
+
#
|
111
|
+
# UserValidation.call(username: Hash.new)
|
112
|
+
# #=> Fend::Plugins::Coercions::CoercionError: cannot coerce {} to string
|
113
|
+
#
|
114
|
+
# Custom error message can be defined by setting `:strict_error_message`
|
115
|
+
# option when loading the plugin:
|
116
|
+
#
|
117
|
+
# plugin :coercions, strict_error_message: "Incoercible input encountered"
|
118
|
+
#
|
119
|
+
# # or
|
120
|
+
#
|
121
|
+
# plugin :coercions, strict_error_message: ->(value, type) { "#{value.inspect} cannot become #{type}" }
|
122
|
+
#
|
123
|
+
# ## Defining custom coercions and overriding built-in ones
|
124
|
+
#
|
125
|
+
# You can define your own coercion method or override the built-in one by
|
126
|
+
# passing a block and using `coerce_to` method, when loading the plugin:
|
127
|
+
#
|
128
|
+
# plugin :coercions do
|
129
|
+
# # add new
|
130
|
+
# coerce_to(:positive_integer) do |input|
|
131
|
+
# Kernel.Integer(input).abs
|
132
|
+
# end
|
133
|
+
#
|
134
|
+
# # override existing
|
135
|
+
# coerce_to(:integer) do |input|
|
136
|
+
# # ...
|
137
|
+
# end
|
138
|
+
# end
|
139
|
+
#
|
140
|
+
# ### Handling incoercible input
|
141
|
+
#
|
142
|
+
# If input value cannot be coerced, either `ArgumentError` or `TypeError`
|
143
|
+
# should be raised.
|
144
|
+
#
|
145
|
+
# class PostValidation < Fend
|
146
|
+
# plugin :coercions do
|
147
|
+
# coerce_to(:user) do |input|
|
148
|
+
# raise ArgumentError unless input.is_a?(Integer)
|
149
|
+
#
|
150
|
+
# User.find(input)
|
151
|
+
# end
|
152
|
+
# end
|
153
|
+
#
|
154
|
+
# # ...
|
155
|
+
#
|
156
|
+
# end
|
157
|
+
#
|
158
|
+
# `ArgumentError` and `TypeError` are rescued on a higher level and
|
159
|
+
# input is returned as is.
|
160
|
+
#
|
161
|
+
# `coerce(modified_by: :user)`
|
162
|
+
#
|
163
|
+
# result = PostValidation.call(modified_by: "invalid_id")
|
164
|
+
#
|
165
|
+
# result.input #=> { modified_by: "invalid_id" }
|
166
|
+
# result.output #=> { modified_by: "invalid_id" }
|
167
|
+
#
|
168
|
+
# If **strict** coercion is specified, errors are re-raised as `CoercionError`.
|
169
|
+
#
|
170
|
+
# `coerce(modified_by: :strict_user)`
|
171
|
+
#
|
172
|
+
# result = PostValidation.call(modified_by: "invalid_id")
|
173
|
+
# #=> Fend::Plugins::Coercions::CoercionError: cannot coerce invalid_id to user
|
174
|
+
#
|
175
|
+
# ### Handling empty strings
|
176
|
+
#
|
177
|
+
# In order to check if input is an empty string, you can take advantange of
|
178
|
+
# `empty_string?` helper method. It takes input as an argument:
|
179
|
+
#
|
180
|
+
# coerce_to(:user) do |input|
|
181
|
+
# return if empty_string?(input)
|
182
|
+
#
|
183
|
+
# # ...
|
184
|
+
# end
|
185
|
+
module Coercions
|
186
|
+
class CoercionError < Error; end
|
187
|
+
|
188
|
+
def self.configure(validation, opts = {}, &block)
|
189
|
+
validation.const_set(:Coerce, Class.new(Fend::Plugins::Coercions::Coerce)) unless validation.const_defined?(:Coerce)
|
190
|
+
validation::Coerce.class_eval(&block) if block_given?
|
191
|
+
validation.opts[:coercions_strict_error_message] = opts.fetch(:strict_error_message, validation.opts[:coercions_strict_error_message])
|
192
|
+
validation::Coerce.fend_class = validation
|
193
|
+
|
194
|
+
validation.const_set(:Coercer, Coercer) unless validation.const_defined?(:Coercer)
|
195
|
+
end
|
196
|
+
|
197
|
+
|
198
|
+
module ClassMethods
|
199
|
+
attr_accessor :type_schema
|
200
|
+
|
201
|
+
def inherited(subclass)
|
202
|
+
super
|
203
|
+
coerce_class = Class.new(self::Coerce)
|
204
|
+
coerce_class.fend_class = subclass
|
205
|
+
subclass.const_set(:Coerce, coerce_class)
|
206
|
+
end
|
207
|
+
|
208
|
+
def coerce(type_schema_hash)
|
209
|
+
@type_schema = type_schema_hash
|
210
|
+
end
|
211
|
+
end
|
212
|
+
|
213
|
+
|
214
|
+
module InstanceMethods
|
215
|
+
def type_schema
|
216
|
+
schema = self.class.type_schema
|
217
|
+
|
218
|
+
return {} if schema.nil?
|
219
|
+
|
220
|
+
raise Error, "type schema must be hash" unless schema.is_a?(Hash)
|
221
|
+
|
222
|
+
schema
|
223
|
+
end
|
224
|
+
|
225
|
+
def process_input(data)
|
226
|
+
data = super || data
|
227
|
+
coerce(data)
|
228
|
+
end
|
229
|
+
|
230
|
+
private
|
231
|
+
|
232
|
+
def coerce(data)
|
233
|
+
coercer.call(data, type_schema)
|
234
|
+
end
|
235
|
+
|
236
|
+
def coercer
|
237
|
+
@_coercer ||= Coercer.new(self.class::Coerce.new)
|
238
|
+
end
|
239
|
+
end
|
240
|
+
|
241
|
+
class Coercer
|
242
|
+
attr_reader :coerce
|
243
|
+
|
244
|
+
def initialize(coerce)
|
245
|
+
@coerce = coerce
|
246
|
+
end
|
247
|
+
|
248
|
+
def call(data, schema)
|
249
|
+
data.each_with_object({}) do |(name, value), result|
|
250
|
+
type = schema[name]
|
251
|
+
|
252
|
+
result[name] = coerce_value(type, value)
|
253
|
+
end
|
254
|
+
end
|
255
|
+
|
256
|
+
private
|
257
|
+
|
258
|
+
def coerce_value(type, value)
|
259
|
+
case type
|
260
|
+
when NilClass then value
|
261
|
+
when Hash then process_hash(value, type)
|
262
|
+
when Array then process_array(value, type.first)
|
263
|
+
else
|
264
|
+
coerce.to(type, value)
|
265
|
+
end
|
266
|
+
end
|
267
|
+
|
268
|
+
def process_hash(input, schema)
|
269
|
+
coerced_value = coerce_value(:hash, input)
|
270
|
+
|
271
|
+
return coerced_value unless coerced_value.is_a?(Hash)
|
272
|
+
|
273
|
+
call(coerced_value, schema)
|
274
|
+
end
|
275
|
+
|
276
|
+
def process_array(input, member_schema)
|
277
|
+
coerced_value = coerce_value(:array, input)
|
278
|
+
|
279
|
+
return coerced_value unless coerced_value.is_a?(Array)
|
280
|
+
|
281
|
+
coerced_value.each_with_object([]) do |member, result|
|
282
|
+
value = member
|
283
|
+
type = member_schema.is_a?(Array) ? member_schema.first : member_schema
|
284
|
+
|
285
|
+
coerced_member_value = coerce_value(type, value)
|
286
|
+
|
287
|
+
next if coerced_member_value.nil?
|
288
|
+
|
289
|
+
result << coerced_member_value
|
290
|
+
end
|
291
|
+
end
|
292
|
+
end
|
293
|
+
|
294
|
+
class Coerce
|
295
|
+
STRICT_PREFIX = "strict_".freeze
|
296
|
+
|
297
|
+
@fend_class = Fend
|
298
|
+
|
299
|
+
class << self
|
300
|
+
attr_accessor :fend_class
|
301
|
+
end
|
302
|
+
|
303
|
+
def self.coerce_to(type, &block)
|
304
|
+
method_name = "to_#{type}"
|
305
|
+
|
306
|
+
define_method(method_name, &block)
|
307
|
+
|
308
|
+
private method_name
|
309
|
+
end
|
310
|
+
|
311
|
+
def self.to(type, value)
|
312
|
+
new.to(type, value)
|
313
|
+
end
|
314
|
+
|
315
|
+
def to(type, value, opts = {})
|
316
|
+
type = type.to_s.sub(STRICT_PREFIX, "") if is_strict = type.to_s.start_with?(STRICT_PREFIX)
|
317
|
+
|
318
|
+
begin
|
319
|
+
method("to_#{type}").call(value)
|
320
|
+
rescue ArgumentError, TypeError
|
321
|
+
is_strict ? raise_error(value, type) : value
|
322
|
+
end
|
323
|
+
end
|
324
|
+
|
325
|
+
private
|
326
|
+
|
327
|
+
def to_any(input)
|
328
|
+
return if empty_string?(input)
|
329
|
+
|
330
|
+
input
|
331
|
+
end
|
332
|
+
|
333
|
+
def to_string(input)
|
334
|
+
return if empty_string?(input) || input.nil?
|
335
|
+
|
336
|
+
case input
|
337
|
+
when String then input
|
338
|
+
when Numeric, Symbol then input.to_s
|
339
|
+
else
|
340
|
+
raise ArgumentError
|
341
|
+
end
|
342
|
+
end
|
343
|
+
|
344
|
+
def to_symbol(input)
|
345
|
+
return if empty_string?(input) || input.nil?
|
346
|
+
|
347
|
+
return input.to_sym if input.respond_to?(:to_sym)
|
348
|
+
|
349
|
+
raise ArgumentError
|
350
|
+
end
|
351
|
+
|
352
|
+
def to_integer(input)
|
353
|
+
return if empty_string?(input)
|
354
|
+
|
355
|
+
::Kernel.Integer(input)
|
356
|
+
end
|
357
|
+
|
358
|
+
def to_float(input)
|
359
|
+
return if empty_string?(input)
|
360
|
+
|
361
|
+
::Kernel.Float(input)
|
362
|
+
end
|
363
|
+
|
364
|
+
def to_decimal(input)
|
365
|
+
return if empty_string?(input)
|
366
|
+
|
367
|
+
to_float(input).to_d
|
368
|
+
end
|
369
|
+
|
370
|
+
def to_date(input)
|
371
|
+
return if empty_string?(input)
|
372
|
+
|
373
|
+
raise ArgumentError unless input.respond_to?(:to_str)
|
374
|
+
|
375
|
+
::Date.parse(input)
|
376
|
+
end
|
377
|
+
|
378
|
+
def to_date_time(input)
|
379
|
+
return if empty_string?(input)
|
380
|
+
|
381
|
+
raise ArgumentError unless input.respond_to?(:to_str)
|
382
|
+
|
383
|
+
::DateTime.parse(input)
|
384
|
+
end
|
385
|
+
|
386
|
+
def to_time(input)
|
387
|
+
return if empty_string?(input)
|
388
|
+
|
389
|
+
raise ArgumentError unless input.respond_to?(:to_str)
|
390
|
+
|
391
|
+
::Time.parse(input)
|
392
|
+
end
|
393
|
+
|
394
|
+
def to_boolean(input)
|
395
|
+
return if empty_string?(input)
|
396
|
+
|
397
|
+
case input
|
398
|
+
when true, 1, /\A(?:1|t(?:rue)?|y(?:es)?|on)\z/i then true
|
399
|
+
when false, 0, /\A(?:0|f(?:alse)?|no?|off)\z/i then false
|
400
|
+
else
|
401
|
+
raise ArgumentError
|
402
|
+
end
|
403
|
+
end
|
404
|
+
|
405
|
+
def to_array(input)
|
406
|
+
return [] if empty_string?(input)
|
407
|
+
return input if input.is_a?(Array)
|
408
|
+
|
409
|
+
raise ArgumentError
|
410
|
+
end
|
411
|
+
|
412
|
+
def to_hash(input)
|
413
|
+
return {} if empty_string?(input)
|
414
|
+
return input if input.is_a?(Hash)
|
415
|
+
|
416
|
+
raise ArgumentError
|
417
|
+
end
|
418
|
+
|
419
|
+
private
|
420
|
+
|
421
|
+
def raise_error(input, type)
|
422
|
+
message = fend_class.opts[:coercions_strict_error_message] || "cannot coerce #{input.inspect} to #{type}"
|
423
|
+
message = message.is_a?(String) ? message : message.call(input, type)
|
424
|
+
|
425
|
+
raise CoercionError, message
|
426
|
+
end
|
427
|
+
|
428
|
+
def empty_string?(input)
|
429
|
+
return false unless input.is_a?(String) || input.is_a?(Symbol)
|
430
|
+
|
431
|
+
!(/\A[[:space:]]*\z/.match(input).nil?)
|
432
|
+
end
|
433
|
+
|
434
|
+
def fend_class
|
435
|
+
self.class.fend_class
|
436
|
+
end
|
437
|
+
end
|
438
|
+
end
|
439
|
+
|
440
|
+
register_plugin(:coercions, Coercions)
|
441
|
+
end
|
442
|
+
end
|