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.
@@ -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