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
@@ -0,0 +1,60 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Fend
|
4
|
+
module Plugins
|
5
|
+
# `collective_params` plugin allows you to specify multiple params at once,
|
6
|
+
# instead of defining each one separately.
|
7
|
+
#
|
8
|
+
# Example:
|
9
|
+
#
|
10
|
+
# plugin :collective_params
|
11
|
+
# plugin :validation_helpers # just to make the example more concise
|
12
|
+
#
|
13
|
+
# validate do |i|
|
14
|
+
# i.params(:name, :email, :address) do |name, email, address|
|
15
|
+
# name.validate_presence
|
16
|
+
#
|
17
|
+
# email.validate_format(EMAIL_REGEX)
|
18
|
+
#
|
19
|
+
# address.params(:city, :street, :zip) do |city, street, zip|
|
20
|
+
# # ...
|
21
|
+
# end
|
22
|
+
# end
|
23
|
+
# end
|
24
|
+
#
|
25
|
+
# Since all params are then available in the same scope, you can add custom
|
26
|
+
# validations more easily:
|
27
|
+
#
|
28
|
+
# validate do |i|
|
29
|
+
# i.params(:started_at, :ended_at) do |started_at, ended_at|
|
30
|
+
# started_at.validate_presence
|
31
|
+
# started_at.validate_type(Time)
|
32
|
+
#
|
33
|
+
# ended_at.validate_presence
|
34
|
+
# ended_at.validate_type(Time)
|
35
|
+
#
|
36
|
+
# if started_at.valid? && ended_at.valid? && started_at > ended_at
|
37
|
+
# started_at.add_error("must happen before ended_at")
|
38
|
+
# end
|
39
|
+
# end
|
40
|
+
# end
|
41
|
+
module CollectiveParams
|
42
|
+
module ParamMethods
|
43
|
+
def params(*names, &block)
|
44
|
+
return if flat? && invalid?
|
45
|
+
|
46
|
+
params = names.each_with_object({}) do |name, result|
|
47
|
+
param = _build_param(self[name])
|
48
|
+
result[name] = param
|
49
|
+
end
|
50
|
+
|
51
|
+
yield(*params.values)
|
52
|
+
|
53
|
+
params.each { |name, param| _nest_errors(name, param.errors) if param.invalid? }
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
register_plugin(:collective_params, CollectiveParams)
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,212 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Fend
|
4
|
+
module Plugins
|
5
|
+
# By default, Fend provides methods for input and output processing.
|
6
|
+
#
|
7
|
+
# class UserValidation < Fend
|
8
|
+
# #...
|
9
|
+
#
|
10
|
+
# def process_input(input)
|
11
|
+
# # symbolize input data keys
|
12
|
+
# symbolized_input = input.each_with_object({}) do |(key, value), result|
|
13
|
+
# new_key = key.is_a?(String) ? key.to_sym : key
|
14
|
+
# result[new_key] = value
|
15
|
+
# end
|
16
|
+
#
|
17
|
+
# # do some additional processing
|
18
|
+
# end
|
19
|
+
#
|
20
|
+
# def process_output(output)
|
21
|
+
# # filter output data
|
22
|
+
# whitelist = [:username, :email, :address]
|
23
|
+
# filtered_output = output.each_with_object({}) do |(key, value), result|
|
24
|
+
# result[key] = value if whitelist.include?(key)
|
25
|
+
# end
|
26
|
+
#
|
27
|
+
# # do some additional processing
|
28
|
+
# end
|
29
|
+
# end
|
30
|
+
#
|
31
|
+
# `data_processing` plugin allows you to define processing steps in more
|
32
|
+
# declarative manner:
|
33
|
+
#
|
34
|
+
# plugin :data_processing
|
35
|
+
#
|
36
|
+
# process(:input) do |input|
|
37
|
+
# # symbolize keys
|
38
|
+
# end
|
39
|
+
#
|
40
|
+
# process(:output) do |output|
|
41
|
+
# # filter
|
42
|
+
# end
|
43
|
+
#
|
44
|
+
# You can define as much processing steps as you need and they will be
|
45
|
+
# executed in order in which they are defined.
|
46
|
+
#
|
47
|
+
# ## Built-in processings
|
48
|
+
#
|
49
|
+
# You can activate built-in processings when loading the plugin:
|
50
|
+
#
|
51
|
+
# # this will:
|
52
|
+
# # symbolize and freeze input data
|
53
|
+
# # stringify output data
|
54
|
+
# plugin :data_processing, input: [:symbolize, :freeze],
|
55
|
+
# output: [:stringify]
|
56
|
+
#
|
57
|
+
# :symbolize
|
58
|
+
# : Symbolizes keys.
|
59
|
+
#
|
60
|
+
# :stringify
|
61
|
+
# : Stringifies keys
|
62
|
+
#
|
63
|
+
# :dup
|
64
|
+
# : Duplicates data
|
65
|
+
#
|
66
|
+
# :freeze
|
67
|
+
# : Freezes data
|
68
|
+
#
|
69
|
+
# All of the above support deeply nested data.
|
70
|
+
#
|
71
|
+
# Built-in processings are executed **before** any
|
72
|
+
# user-defined ones.
|
73
|
+
#
|
74
|
+
# ## Data mutation
|
75
|
+
#
|
76
|
+
# Fend will never mutate the raw input data you provide:
|
77
|
+
#
|
78
|
+
# raw_input = { username: "john", email: "john@example.com" }
|
79
|
+
# UserValidation.call(raw_input)
|
80
|
+
#
|
81
|
+
# However, nothing can stop you from performing destructive operations
|
82
|
+
# (`merge!`, `delete`, etc...) in custom processing steps. If you intend to
|
83
|
+
# mutate input/output data, make sure to use `:dup` processing, in order to
|
84
|
+
# ensure immutability.
|
85
|
+
module DataProcessing
|
86
|
+
BUILT_IN_PROCESSINGS = {
|
87
|
+
symbolize: ->(data) { Process.symbolize_keys(data) },
|
88
|
+
stringify: ->(data) { Process.stringify_keys(data) },
|
89
|
+
dup: ->(data) { Process.duplicate(data) },
|
90
|
+
freeze: ->(data) { Process.frost(data) }
|
91
|
+
}.freeze
|
92
|
+
|
93
|
+
def self.configure(validation, options = {})
|
94
|
+
validation.opts[:data_processing] = {}
|
95
|
+
validation.opts[:data_processing][:input] ||= []
|
96
|
+
validation.opts[:data_processing][:output] ||= []
|
97
|
+
|
98
|
+
return if options.empty?
|
99
|
+
|
100
|
+
options.each do |data_ref, processings|
|
101
|
+
processings.each_with_object(validation.opts[:data_processing][data_ref]) do |name, result|
|
102
|
+
raise Error, "Built-in processing not found: ':#{name}'" unless BUILT_IN_PROCESSINGS.key?(name)
|
103
|
+
|
104
|
+
result << BUILT_IN_PROCESSINGS[name]
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
module ClassMethods
|
110
|
+
def process(data_key, &block)
|
111
|
+
opts[:data_processing][data_key] ||= []
|
112
|
+
opts[:data_processing][data_key] << block
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
module InstanceMethods
|
117
|
+
def process_input(data)
|
118
|
+
super
|
119
|
+
process_data(:input, data)
|
120
|
+
end
|
121
|
+
|
122
|
+
def process_output(data)
|
123
|
+
super
|
124
|
+
process_data(:output, data)
|
125
|
+
end
|
126
|
+
|
127
|
+
private
|
128
|
+
|
129
|
+
def process_data(key, data)
|
130
|
+
result = data
|
131
|
+
|
132
|
+
self.class.opts[:data_processing][key].each do |process_block|
|
133
|
+
result = instance_exec(result, &process_block)
|
134
|
+
end
|
135
|
+
|
136
|
+
result
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
class Process
|
141
|
+
HASH_OR_ARRAY = ->(a) { a.is_a?(Hash) || a.is_a?(Array) }.freeze
|
142
|
+
NESTED_ARRAY = ->(a) { a.is_a?(Array) && a.any? { |member| HASH_OR_ARRAY[member] } }.freeze
|
143
|
+
NESTED_HASH = ->(a) { a.is_a?(Hash) && a.any? { |_, value| HASH_OR_ARRAY[value] } }.freeze
|
144
|
+
|
145
|
+
def self.symbolize_keys(data)
|
146
|
+
return data unless HASH_OR_ARRAY[data]
|
147
|
+
|
148
|
+
transformation = ->(key) { key.is_a?(String) ? key.to_sym : key }
|
149
|
+
|
150
|
+
deep_transform_keys(data, transformation)
|
151
|
+
end
|
152
|
+
|
153
|
+
def self.stringify_keys(data)
|
154
|
+
return data unless HASH_OR_ARRAY[data]
|
155
|
+
|
156
|
+
transformation = ->(key) { key.is_a?(Symbol) ? key.to_s : key }
|
157
|
+
|
158
|
+
deep_transform_keys(data, transformation)
|
159
|
+
end
|
160
|
+
|
161
|
+
def self.duplicate(data, opts = {})
|
162
|
+
return data unless HASH_OR_ARRAY[data]
|
163
|
+
|
164
|
+
case data
|
165
|
+
when NESTED_HASH
|
166
|
+
data.each_with_object({}) { |(key, value), result| result[key] = duplicate(value) }
|
167
|
+
when NESTED_ARRAY
|
168
|
+
data.map(&method(:duplicate))
|
169
|
+
when HASH_OR_ARRAY
|
170
|
+
data.dup
|
171
|
+
else
|
172
|
+
data
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
def self.frost(data)
|
177
|
+
return data unless HASH_OR_ARRAY[data]
|
178
|
+
|
179
|
+
case data
|
180
|
+
when NESTED_HASH
|
181
|
+
data.each_with_object({}) { |(key, value), result| result[key] = frost(value) }.freeze
|
182
|
+
when NESTED_ARRAY
|
183
|
+
data.map(&method(:frost)).freeze
|
184
|
+
when HASH_OR_ARRAY
|
185
|
+
data.dup.freeze
|
186
|
+
else
|
187
|
+
data
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
private
|
192
|
+
|
193
|
+
def self.deep_transform_keys(data, key_proc)
|
194
|
+
case data
|
195
|
+
when Hash
|
196
|
+
data.each_with_object({}) do |(key, value), result|
|
197
|
+
_key = key_proc[key]
|
198
|
+
|
199
|
+
result[_key] = deep_transform_keys(value, key_proc)
|
200
|
+
end
|
201
|
+
when NESTED_ARRAY
|
202
|
+
data.map { |member| deep_transform_keys(member, key_proc) }
|
203
|
+
else
|
204
|
+
data
|
205
|
+
end
|
206
|
+
end
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
210
|
+
register_plugin(:data_processing, DataProcessing)
|
211
|
+
end
|
212
|
+
end
|
@@ -0,0 +1,130 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Fend
|
4
|
+
module Plugins
|
5
|
+
# `dependencies` plugin enables you to register validation dependencies and
|
6
|
+
# later resolve them so they would be available in validation block
|
7
|
+
#
|
8
|
+
# plugin :dependencies
|
9
|
+
#
|
10
|
+
# ## Registering dependencies
|
11
|
+
#
|
12
|
+
# There are two types of dependencies:
|
13
|
+
#
|
14
|
+
# 1. **Inheritable dependencies** - available in current validation class
|
15
|
+
# and in subclasses
|
16
|
+
# 2. **Local dependencies** - available only in current validation class
|
17
|
+
#
|
18
|
+
# ### Inheritable dependencies
|
19
|
+
#
|
20
|
+
# Inheritable dependencies can be registered when plugin is loaded:
|
21
|
+
#
|
22
|
+
# plugin :dependencies, user_class: User
|
23
|
+
#
|
24
|
+
# **Global dependencies** can be registered by loading the plugin directly in
|
25
|
+
# `Fend` class:
|
26
|
+
#
|
27
|
+
# require "address_checker"
|
28
|
+
#
|
29
|
+
# Fend.plugin :dependencies, address_checker: AddressChecker.new
|
30
|
+
#
|
31
|
+
# Now, all `Fend` subclasses will be able to resolve `address_checker`
|
32
|
+
#
|
33
|
+
# ### Local dependencies
|
34
|
+
#
|
35
|
+
# Local dependencies can be registered in `deps` registry, on instance level.
|
36
|
+
# Recommended place to do this is the initializer.
|
37
|
+
#
|
38
|
+
# class UserValidation < Fend
|
39
|
+
# plugin :dependencies
|
40
|
+
#
|
41
|
+
# def initialize(model)
|
42
|
+
# # you can pass a dependency on initialization
|
43
|
+
# deps[:model] = model
|
44
|
+
#
|
45
|
+
# # or
|
46
|
+
#
|
47
|
+
# # hardcode it yourself
|
48
|
+
# deps[:address_checker] = AddressChecker.new
|
49
|
+
# end
|
50
|
+
# end
|
51
|
+
#
|
52
|
+
# user_validation = UserValidation.new(User)
|
53
|
+
#
|
54
|
+
# ## Resolving dependencies
|
55
|
+
#
|
56
|
+
# To resolve dependencies, `:inject` option needs to be provided to the
|
57
|
+
# `validate` method, with a list of keys representing dependency names:
|
58
|
+
#
|
59
|
+
# class UserValidation < Fend
|
60
|
+
# plugin :dependencies, user_model: User
|
61
|
+
#
|
62
|
+
# validate(inject: [:user_model, :address_checker]) do |i, user_model, address_checker|
|
63
|
+
# user_model #=> User
|
64
|
+
# address_checker #=> #<AddressChecker ...>
|
65
|
+
# end
|
66
|
+
#
|
67
|
+
# def initialize(address_checker)
|
68
|
+
# deps[:address_checker] = address_checker
|
69
|
+
# end
|
70
|
+
# end
|
71
|
+
#
|
72
|
+
# ## Overriding inheritable dependencies
|
73
|
+
#
|
74
|
+
# To override inheritable dependency, just load the plugin again in a
|
75
|
+
# subclass, or define local dependency with the same name.
|
76
|
+
#
|
77
|
+
# plugin :dependencies, user_model: SpecialUser
|
78
|
+
#
|
79
|
+
# # or
|
80
|
+
#
|
81
|
+
# def initialize
|
82
|
+
# deps[:user_model] = SpecialUser
|
83
|
+
# end
|
84
|
+
#
|
85
|
+
# ## Example usage
|
86
|
+
#
|
87
|
+
# Here's an example of email uniqueness validation:
|
88
|
+
#
|
89
|
+
# validate(inject: [:user_model]) do |i, user_model|
|
90
|
+
# i.param(:email) do |email|
|
91
|
+
# email.add_error("must be unique") if user_model.exists?(email: email.value)
|
92
|
+
# end
|
93
|
+
# end
|
94
|
+
module Dependencies
|
95
|
+
def self.configure(validation, opts = {})
|
96
|
+
validation.opts[:dependencies] = (validation.opts[:dependencies] || {}).merge(opts)
|
97
|
+
end
|
98
|
+
|
99
|
+
module ClassMethods
|
100
|
+
attr_reader :specified_dependencies
|
101
|
+
|
102
|
+
def validate(opts = {}, &block)
|
103
|
+
if opts.key?(:inject)
|
104
|
+
raise ArgumentError, ":inject option value must be an array" unless opts[:inject].is_a?(Array)
|
105
|
+
|
106
|
+
@specified_dependencies = opts[:inject] unless opts[:inject].nil?
|
107
|
+
end
|
108
|
+
|
109
|
+
super(&block)
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
module InstanceMethods
|
114
|
+
def deps
|
115
|
+
@_deps ||= self.class.opts[:dependencies].dup
|
116
|
+
end
|
117
|
+
|
118
|
+
def validate(&block)
|
119
|
+
super if self.class.specified_dependencies.nil?
|
120
|
+
|
121
|
+
dependencies = deps.values_at(*self.class.specified_dependencies)
|
122
|
+
|
123
|
+
yield(@_input_param, *dependencies) if block_given?
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
register_plugin(:dependencies, Dependencies)
|
129
|
+
end
|
130
|
+
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Fend
|
4
|
+
module Plugins
|
5
|
+
# `external_validation` plugin allows you to delegate param validations to
|
6
|
+
# external classes/objects.
|
7
|
+
#
|
8
|
+
# plugin :external_validation
|
9
|
+
#
|
10
|
+
# External validation must respond to `call` method that takes param value
|
11
|
+
# as an argument and returns error messages either as an array or hash
|
12
|
+
# (nested data).
|
13
|
+
#
|
14
|
+
# class CustomEmailValidator
|
15
|
+
# def initialize
|
16
|
+
# @errors = []
|
17
|
+
# end
|
18
|
+
#
|
19
|
+
# def call(email_value)
|
20
|
+
# @errors << "must be string" unless email_value.is_a?(String)
|
21
|
+
# @errors << "must be unique" unless unique?(email_value)
|
22
|
+
#
|
23
|
+
# @errors
|
24
|
+
# end
|
25
|
+
#
|
26
|
+
# def unique?(value)
|
27
|
+
# UniquenessCheck.call(value)
|
28
|
+
# end
|
29
|
+
# end
|
30
|
+
#
|
31
|
+
# class AddressValidation < Fend
|
32
|
+
# plugin :validation_options
|
33
|
+
# plugin :collective_params
|
34
|
+
#
|
35
|
+
# validate do |i|
|
36
|
+
# i.params(:city, :street) do |city, street|
|
37
|
+
# city.validate(type: String)
|
38
|
+
# street.validate(type: String)
|
39
|
+
# end
|
40
|
+
# end
|
41
|
+
# end
|
42
|
+
#
|
43
|
+
# class UserValidation < Fend
|
44
|
+
# plugin :external_validation
|
45
|
+
# plugin :collective_params
|
46
|
+
#
|
47
|
+
# validate do |i|
|
48
|
+
# i.params(:email, :address) do |email, address|
|
49
|
+
# email.validate_with(CustomEmailValidation.new)
|
50
|
+
#
|
51
|
+
# address.validate_with(AddressValidation)
|
52
|
+
# end
|
53
|
+
# end
|
54
|
+
# end
|
55
|
+
#
|
56
|
+
# `validation_options` plugin supports `external_validation`:
|
57
|
+
#
|
58
|
+
# email.validate(with: CustomEmailValidation.new)
|
59
|
+
#
|
60
|
+
# You are free to combine internal and external validations any way you
|
61
|
+
# like. Using one doesn't mean you can't use the other.
|
62
|
+
|
63
|
+
module ExternalValidation
|
64
|
+
module ParamMethods
|
65
|
+
def validate_with(validation)
|
66
|
+
result = validation.call(value)
|
67
|
+
messages = result.class.ancestors.include?(Fend::Result) ? result.messages : result
|
68
|
+
|
69
|
+
return if messages.is_a?(Hash) && flat? && invalid?
|
70
|
+
|
71
|
+
@errors = if @errors.is_a?(Hash) && messages.is_a?(Hash)
|
72
|
+
_deep_merge_messages(@errors, messages)
|
73
|
+
elsif @errors.is_a?(Array) && messages.is_a?(Array)
|
74
|
+
@errors + messages
|
75
|
+
else
|
76
|
+
messages
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
private
|
81
|
+
|
82
|
+
def _deep_merge_messages(hash, other_hash)
|
83
|
+
hash.merge(other_hash) do |_, old_val, new_val|
|
84
|
+
if old_val.is_a?(Hash) && new_val.is_a?(Hash)
|
85
|
+
deep_merge(old_val, new_val)
|
86
|
+
elsif old_val.is_a?(Array) && new_val.is_a?(Array)
|
87
|
+
old_val + new_val
|
88
|
+
else
|
89
|
+
new_val
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
register_plugin(:external_validation, ExternalValidation)
|
97
|
+
end
|
98
|
+
end
|