formalism 0.2.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/CHANGELOG.md +11 -0
- data/LICENSE.txt +21 -0
- data/README.md +310 -0
- data/lib/formalism.rb +4 -0
- data/lib/formalism/action.rb +30 -0
- data/lib/formalism/form.rb +95 -0
- data/lib/formalism/form/coercion.rb +60 -0
- data/lib/formalism/form/coercion/_base.rb +34 -0
- data/lib/formalism/form/coercion/_numeric.rb +26 -0
- data/lib/formalism/form/coercion/array.rb +30 -0
- data/lib/formalism/form/coercion/boolean.rb +16 -0
- data/lib/formalism/form/coercion/date.rb +20 -0
- data/lib/formalism/form/coercion/float.rb +15 -0
- data/lib/formalism/form/coercion/hash.rb +16 -0
- data/lib/formalism/form/coercion/integer.rb +15 -0
- data/lib/formalism/form/coercion/string.rb +16 -0
- data/lib/formalism/form/coercion/symbol.rb +16 -0
- data/lib/formalism/form/coercion/time.rb +23 -0
- data/lib/formalism/form/fields.rb +153 -0
- data/lib/formalism/form/filling.rb +81 -0
- data/lib/formalism/form/outcome.rb +28 -0
- data/lib/formalism/form/validation_error.rb +15 -0
- data/lib/formalism/version.rb +5 -0
- metadata +238 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 704ad27d3b6f2c48ceb930195acedcc634642e14d76cdf396c8c005ca8af067f
|
4
|
+
data.tar.gz: c38eb79190667e0576bcb0e30bbc5564246ef9d14d6f9d0b4de657857ee4aa33
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: b5838dc86618cae1f6674154ff496e5f2856665daa9b840122430da9425054c3c17c1329fc96091ef2798a1681b78ea4aa526f2c9b7bab2654ca9233b91fb196
|
7
|
+
data.tar.gz: b40a969a78b0f1e820ae9b1fbe1d1cafad933f35155cc2ac5a972475a1d6e8eb5851b6d1d3ba049c2c7e25659edf1a9bd72ccad9715643e979d95f2c5c05c7fe
|
data/CHANGELOG.md
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2020 Alexander Popov
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,310 @@
|
|
1
|
+
# Formalism
|
2
|
+
|
3
|
+
[](https://cirrus-ci.com/github/AlexWayfer/formalism)
|
4
|
+
[](https://codecov.io/gh/AlexWayfer/formalism)
|
5
|
+
[](https://codeclimate.com/github/AlexWayfer/formalism)
|
6
|
+
[](https://depfu.com/repos/github/AlexWayfer/formalism)
|
7
|
+
[](https://inch-ci.org/github/AlexWayfer/formalism)
|
8
|
+
[](https://github.com/AlexWayfer/formalism/blob/master/LICENSE.txt)
|
9
|
+
[](https://rubygems.org/gems/formalism)
|
10
|
+
|
11
|
+
Ruby gem for forms with validations and nesting.
|
12
|
+
|
13
|
+
## Why
|
14
|
+
|
15
|
+
I need for service-like objects.
|
16
|
+
|
17
|
+
I've explored these projects:
|
18
|
+
|
19
|
+
* [Reform](https://github.com/trailblazer/reform)
|
20
|
+
* [Mutations](https://github.com/cypriss/mutations)
|
21
|
+
* [Interactor](https://github.com/collectiveidea/interactor)
|
22
|
+
* [dry-rb](https://github.com/dry-rb)
|
23
|
+
|
24
|
+
But nothing of them supports all features I need for:
|
25
|
+
|
26
|
+
* nesting (into unlimited levels) of themselves;
|
27
|
+
* simple syntax;
|
28
|
+
* custom validations and coercions;
|
29
|
+
* unified output.
|
30
|
+
|
31
|
+
So, I've tried to combine these all into one library and got Formalism.
|
32
|
+
|
33
|
+
### Why here are forms and what about service objects?
|
34
|
+
|
35
|
+
I've discovered that form object, only with validations,
|
36
|
+
are useless without service objects. So, I've combined them:
|
37
|
+
service objects include validations.
|
38
|
+
|
39
|
+
### If these are service objects, why they called forms?
|
40
|
+
|
41
|
+
Because if we're combining them — it's more like forms with logic inside for me
|
42
|
+
than service objects built-in forms. Even in HTML we're writing `<form>`.
|
43
|
+
So, Formalism can accept all data from any-difficult `<form>` and process it,
|
44
|
+
also with nested forms (for example, if you have some request form
|
45
|
+
with contact data and want to pass contacts into something like user form).
|
46
|
+
|
47
|
+
### And if I need for simple service object without validation?
|
48
|
+
|
49
|
+
You can use `Formalism::Action`, a parent of `Formalism::Form`.
|
50
|
+
|
51
|
+
## Installation
|
52
|
+
|
53
|
+
Add this line to your application's Gemfile:
|
54
|
+
|
55
|
+
```ruby
|
56
|
+
gem 'formalism'
|
57
|
+
```
|
58
|
+
|
59
|
+
And then execute:
|
60
|
+
|
61
|
+
```shell
|
62
|
+
bundle install
|
63
|
+
```
|
64
|
+
|
65
|
+
Or install it yourself as:
|
66
|
+
|
67
|
+
```shell
|
68
|
+
gem install formalism
|
69
|
+
```
|
70
|
+
|
71
|
+
## Usage
|
72
|
+
|
73
|
+
### Basic example
|
74
|
+
|
75
|
+
```ruby
|
76
|
+
class FindArtistForm < Formalism::Form
|
77
|
+
field :name
|
78
|
+
|
79
|
+
private
|
80
|
+
|
81
|
+
def validate
|
82
|
+
if name.to_s.empty?
|
83
|
+
errors.add 'Name is not provided'
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def execute
|
88
|
+
Artist.first(fields_and_nested_forms)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
class CreateAlbumForm < Formalism::Form
|
93
|
+
field :name, String
|
94
|
+
fiels :tags, Array, of: String
|
95
|
+
nested :artist, FindArtistForm
|
96
|
+
|
97
|
+
private
|
98
|
+
|
99
|
+
def validate
|
100
|
+
if name.to_s.empty?
|
101
|
+
errors.add 'Name is not provided'
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
def execute
|
106
|
+
Album.create(fields_and_nested_forms)
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
form = CreateAlbumForm.new(
|
111
|
+
name: 'Hits', tags: %w[Indie Rock Hits], artist: { name: 'Alex' }
|
112
|
+
)
|
113
|
+
form.run
|
114
|
+
```
|
115
|
+
|
116
|
+
### Running
|
117
|
+
|
118
|
+
Usually you need to initialize a form and execute `#run` method.
|
119
|
+
Internally, it runs `#valid?` (public) and `#execute` (private) methods.
|
120
|
+
`#valid?` runs `#validate` (private) of a form itself and nested forms.
|
121
|
+
`#run` can be redefined for database transaction, for example.
|
122
|
+
|
123
|
+
Also you can call `.run` with arguments for `#initialize`,
|
124
|
+
it's the alias for `#initialize` + `#run`.
|
125
|
+
|
126
|
+
#### Form outcome
|
127
|
+
|
128
|
+
Any call of `run` returns `Form::Outcome` instance which has `#success?`,
|
129
|
+
`#result` and `#errors` methods. Result is a result of `#execute` method.
|
130
|
+
Be careful: calling `#result` for failed outcome will raise `ValidationError`.
|
131
|
+
|
132
|
+
### Field type
|
133
|
+
|
134
|
+
Field receives type as the second argument.
|
135
|
+
It's not required.
|
136
|
+
It can be a constant, String or Symbol.
|
137
|
+
If specified — there is a coercion to specified type,
|
138
|
+
if not — data remains unchanged.
|
139
|
+
|
140
|
+
Nested forms — their class, as constant.
|
141
|
+
Type or `:initialize` block is required.
|
142
|
+
|
143
|
+
Formalism also supports `Array` type with the optional `:of` option
|
144
|
+
(type of elements).
|
145
|
+
Coercion will be applied to a data itself and to its elements.
|
146
|
+
|
147
|
+
#### Coercion
|
148
|
+
|
149
|
+
There is built-in coercion into some types, if you try to coerce
|
150
|
+
to undefined type — you'll get `Formalism::Form::NoCoercionError`.
|
151
|
+
|
152
|
+
You can define a coercion to some type via definition of such class:
|
153
|
+
|
154
|
+
```ruby
|
155
|
+
# frozen_string_literal: true
|
156
|
+
|
157
|
+
module Formalism
|
158
|
+
class Form < Action
|
159
|
+
class Coercion
|
160
|
+
## Class for coercion to String
|
161
|
+
class String < Base
|
162
|
+
private
|
163
|
+
|
164
|
+
def execute
|
165
|
+
@value&.to_s
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
171
|
+
```
|
172
|
+
|
173
|
+
### Default value
|
174
|
+
|
175
|
+
`field` and `nested` accepts `:default` option.
|
176
|
+
It can be any value, if it's an instance of `Proc` — it'll be executed
|
177
|
+
in the form instance scope.
|
178
|
+
|
179
|
+
### Different keys
|
180
|
+
|
181
|
+
`field` supports `:key` option (Symbol) to receive data by a different key,
|
182
|
+
not as a field name.
|
183
|
+
|
184
|
+
### Custom initialization of nested forms
|
185
|
+
|
186
|
+
By default, nested forms initialized with data by key as their name
|
187
|
+
in parent data. So, if a parent receive `{ foo: 1, bar: { baz: 2 } }`,
|
188
|
+
it's nested form `:bar` will receive `{ baz: 2 }`.
|
189
|
+
|
190
|
+
If you want to prevent initialization at all, or pass custom arguments —
|
191
|
+
you should use `:initialize` option which accepts a proc
|
192
|
+
with a form class argument.
|
193
|
+
|
194
|
+
If you want to just refine incoming data (add or remove) — you should define
|
195
|
+
`#params_for_nested_*` private method, where `*` is a nested form name.
|
196
|
+
You can use `super` inside.
|
197
|
+
|
198
|
+
### Order of filling with data
|
199
|
+
|
200
|
+
Fields and nested forms are filling in order of their definition.
|
201
|
+
But sometimes you want to change this order, for example,
|
202
|
+
if you have a nested forms in ancestors which depends on data in children forms.
|
203
|
+
For such cases you can use `:depends_on` option, which accepts fields
|
204
|
+
and nested forms names as Symbol or Array of symbols. They will be filled
|
205
|
+
(and initialized) before dependent.
|
206
|
+
|
207
|
+
### Merging into final data
|
208
|
+
|
209
|
+
There is `Form#fields_and_nested_forms` as final data
|
210
|
+
(after coercion, defaults, etc). But you may want to not include some fields
|
211
|
+
or nested forms into this data. You can do it via `:merge` option,
|
212
|
+
which can be `true`, `false` or `Proc` (executed in form's instance scope).
|
213
|
+
|
214
|
+
For example:
|
215
|
+
|
216
|
+
```ruby
|
217
|
+
field :bar, merge: true
|
218
|
+
nested :only_valid, nested_form_class, merge: ->(form) { form.valid? }
|
219
|
+
```
|
220
|
+
|
221
|
+
### Runnable
|
222
|
+
|
223
|
+
You can disable `#valid?` and `#run` of forms (including nested ones)
|
224
|
+
by setting `form.runnable = false`.
|
225
|
+
It can be helpful for some cases, for example, with policies (permissions):
|
226
|
+
|
227
|
+
```ruby
|
228
|
+
def initialize_nested_form(name, options)
|
229
|
+
return unless (form = super)
|
230
|
+
|
231
|
+
form.runnable = allowed_to_change?(name)
|
232
|
+
form
|
233
|
+
end
|
234
|
+
```
|
235
|
+
|
236
|
+
### Inheritance
|
237
|
+
|
238
|
+
Any `class ChildForm < ParentForm` will have all fields and nested forms
|
239
|
+
from `ParentForm`.
|
240
|
+
|
241
|
+
#### Removing (inherited) field
|
242
|
+
|
243
|
+
But you're able to remove (usually inherited) fields by:
|
244
|
+
|
245
|
+
```ruby
|
246
|
+
class ChildForm < ParentForm
|
247
|
+
remove_field :field_from_parent
|
248
|
+
end
|
249
|
+
```
|
250
|
+
|
251
|
+
#### Modules
|
252
|
+
|
253
|
+
You can define modules and use them later like this:
|
254
|
+
|
255
|
+
```ruby
|
256
|
+
module CommonFields
|
257
|
+
include Formalism::Form::Fields
|
258
|
+
|
259
|
+
field :base_field
|
260
|
+
nested :base_nested
|
261
|
+
end
|
262
|
+
|
263
|
+
class SomeForm < Formalism::Form
|
264
|
+
include CommonFields
|
265
|
+
|
266
|
+
field :another_field
|
267
|
+
end
|
268
|
+
```
|
269
|
+
|
270
|
+
### Convert to params
|
271
|
+
|
272
|
+
You can convert a Form back to (processed) params, for example, for view render:
|
273
|
+
|
274
|
+
```ruby
|
275
|
+
form = CreateAlbumForm.new(
|
276
|
+
name: 'Hits', tags: %w[Indie Rock Hits], artist: { name: 'Alex' }
|
277
|
+
)
|
278
|
+
|
279
|
+
form.to_params
|
280
|
+
# {
|
281
|
+
# name: 'Hits',
|
282
|
+
# tags: %w[Indie Rock Hits],
|
283
|
+
# artist: { name: 'Alex' }
|
284
|
+
# }
|
285
|
+
```
|
286
|
+
|
287
|
+
### Actions
|
288
|
+
|
289
|
+
For actions without fields, nesting and validation you can use
|
290
|
+
`Formalism::Action` (the parent of `Formalism::Form`).
|
291
|
+
|
292
|
+
## Development
|
293
|
+
|
294
|
+
After checking out the repo, run `bundle install` to install dependencies.
|
295
|
+
|
296
|
+
Then, run `toys rspec` to run the tests.
|
297
|
+
|
298
|
+
To install this gem onto your local machine, run `toys gem install`.
|
299
|
+
|
300
|
+
To release a new version, run `toys gem release %version%`.
|
301
|
+
See how it works [here](https://github.com/AlexWayfer/gem_toys#release).
|
302
|
+
|
303
|
+
## Contributing
|
304
|
+
|
305
|
+
Bug reports and pull requests are welcome on [GitHub](https://github.com/AlexWayfer/formalism).
|
306
|
+
|
307
|
+
## License
|
308
|
+
|
309
|
+
The gem is available as open source under the terms of the
|
310
|
+
[MIT License](https://opensource.org/licenses/MIT).
|
data/lib/formalism.rb
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'gorilla_patch/deep_dup'
|
4
|
+
|
5
|
+
module Formalism
|
6
|
+
## Class for any action
|
7
|
+
class Action
|
8
|
+
class << self
|
9
|
+
def run(*args)
|
10
|
+
new(*args).run
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
attr_reader :params
|
15
|
+
attr_accessor :runnable
|
16
|
+
|
17
|
+
using GorillaPatch::DeepDup
|
18
|
+
|
19
|
+
def initialize(params = {})
|
20
|
+
@runnable = true unless defined? @runnable
|
21
|
+
@params = params.deep_dup || {}
|
22
|
+
end
|
23
|
+
|
24
|
+
def run
|
25
|
+
return unless runnable
|
26
|
+
|
27
|
+
execute
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,95 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'form/fields'
|
4
|
+
require_relative 'form/filling'
|
5
|
+
require_relative 'form/outcome'
|
6
|
+
require_relative 'form/validation_error'
|
7
|
+
|
8
|
+
module Formalism
|
9
|
+
## Class for forms
|
10
|
+
class Form < Action
|
11
|
+
include Form::Fields
|
12
|
+
include Filling
|
13
|
+
|
14
|
+
attr_reader :instance
|
15
|
+
|
16
|
+
def self.inherited(child)
|
17
|
+
super
|
18
|
+
|
19
|
+
child.fields_and_nested_forms.merge!(fields_and_nested_forms)
|
20
|
+
end
|
21
|
+
|
22
|
+
def initialize(params_or_instance = {})
|
23
|
+
if params_or_instance.is_a?(Hash)
|
24
|
+
super
|
25
|
+
else
|
26
|
+
super({})
|
27
|
+
|
28
|
+
@instance = params_or_instance
|
29
|
+
end
|
30
|
+
|
31
|
+
fill_fields_and_nested_forms
|
32
|
+
end
|
33
|
+
|
34
|
+
def valid?
|
35
|
+
return unless runnable
|
36
|
+
|
37
|
+
errors.clear
|
38
|
+
|
39
|
+
nested_forms.each_value(&:valid?)
|
40
|
+
|
41
|
+
validate
|
42
|
+
|
43
|
+
merge_errors_of_nested_forms
|
44
|
+
|
45
|
+
return false if errors.any?
|
46
|
+
|
47
|
+
true
|
48
|
+
end
|
49
|
+
|
50
|
+
def run
|
51
|
+
return unless runnable
|
52
|
+
|
53
|
+
return Outcome.new(errors) unless valid?
|
54
|
+
|
55
|
+
Outcome.new(errors, super)
|
56
|
+
end
|
57
|
+
|
58
|
+
protected
|
59
|
+
|
60
|
+
def errors
|
61
|
+
@errors ||= Set.new
|
62
|
+
end
|
63
|
+
|
64
|
+
private
|
65
|
+
|
66
|
+
def validate; end
|
67
|
+
|
68
|
+
def instance_respond_to?(name)
|
69
|
+
return false unless defined? @instance
|
70
|
+
|
71
|
+
@instance.respond_to?(name)
|
72
|
+
end
|
73
|
+
|
74
|
+
def instance_public_send(name)
|
75
|
+
return false unless defined? @instance
|
76
|
+
|
77
|
+
@instance.public_send(name)
|
78
|
+
end
|
79
|
+
|
80
|
+
def merge_errors_of_nested_forms
|
81
|
+
nested_forms.each do |name, nested_form|
|
82
|
+
should_be_merged = self.class.fields_and_nested_forms[name].fetch(:merge_errors, true)
|
83
|
+
should_be_merged = instance_exec(&should_be_merged) if should_be_merged.is_a?(Proc)
|
84
|
+
|
85
|
+
next unless should_be_merged && nested_form.errors.any?
|
86
|
+
|
87
|
+
merge_errors_of_nested_form name, nested_form
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def merge_errors_of_nested_form(_name, nested_form)
|
92
|
+
errors.merge nested_form.errors
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'gorilla_patch/inflections'
|
4
|
+
|
5
|
+
Dir[
|
6
|
+
File.join(__dir__, 'coercion', '**', '*.rb')
|
7
|
+
]
|
8
|
+
.sort_by! { |file| [File.basename(file).start_with?('_') ? 1 : 2, file] }
|
9
|
+
.each { |file| require file }
|
10
|
+
|
11
|
+
## https://github.com/bbatsov/rubocop/issues/5831
|
12
|
+
module Formalism
|
13
|
+
class Form < Action
|
14
|
+
## Class for coercion (check, initialization)
|
15
|
+
class Coercion
|
16
|
+
def initialize(type, of = nil)
|
17
|
+
@type = type
|
18
|
+
@of = of
|
19
|
+
end
|
20
|
+
|
21
|
+
def check
|
22
|
+
## It's custom error! But cop triggers for single argument anyway.
|
23
|
+
# rubocop:disable Style/RaiseArgs
|
24
|
+
raise NoCoercionError.new(@type) unless exist?
|
25
|
+
# rubocop:enable Style/RaiseArgs
|
26
|
+
|
27
|
+
return unless const_name == 'Array' && @of
|
28
|
+
|
29
|
+
self.class.new(@of).check
|
30
|
+
end
|
31
|
+
|
32
|
+
def result_for(value)
|
33
|
+
coercion_class = exist? ? const_name : 'Base'
|
34
|
+
|
35
|
+
self.class.const_get(coercion_class, false).new(value, @of).result
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
using GorillaPatch::Inflections
|
41
|
+
|
42
|
+
def const_name
|
43
|
+
@type.to_s.camelize
|
44
|
+
end
|
45
|
+
|
46
|
+
def exist?
|
47
|
+
self.class.const_defined?(const_name, false)
|
48
|
+
rescue NameError
|
49
|
+
false
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
## Error for undefined type in coercion
|
54
|
+
class NoCoercionError < ArgumentError
|
55
|
+
def initialize(type)
|
56
|
+
super "Formalism has no coercion to #{type}"
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Formalism
|
4
|
+
class Form < Action
|
5
|
+
class Coercion
|
6
|
+
## Base class for coercion
|
7
|
+
class Base
|
8
|
+
def initialize(value, *)
|
9
|
+
@value = value
|
10
|
+
|
11
|
+
type_name = self.class.name.split('::')[3..-1].join('::')
|
12
|
+
|
13
|
+
@type =
|
14
|
+
if Object.const_defined?(type_name, false)
|
15
|
+
then Object.const_get(type_name, false)
|
16
|
+
else type_name
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def result
|
21
|
+
return @value unless should_be_coreced?
|
22
|
+
|
23
|
+
execute
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def should_be_coreced?
|
29
|
+
@type != 'Base' && !(@type.is_a?(Class) && @value.is_a?(@type))
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Formalism
|
4
|
+
class Form < Action
|
5
|
+
class Coercion
|
6
|
+
## Base class for coercion to Numeric
|
7
|
+
class Numeric < Base
|
8
|
+
class << self
|
9
|
+
private
|
10
|
+
|
11
|
+
def wrap_value_regexp(content)
|
12
|
+
/\A\s*#{content}\s*\z/.freeze
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def execute
|
19
|
+
return unless self.class::VALUE_REGEXP.match? @value.to_s
|
20
|
+
|
21
|
+
@value.public_send(self.class::CONVERSION_METHOD)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Formalism
|
4
|
+
class Form < Action
|
5
|
+
class Coercion
|
6
|
+
## Class for coercion to Array
|
7
|
+
class Array < Base
|
8
|
+
def initialize(value, of = nil)
|
9
|
+
super
|
10
|
+
|
11
|
+
@of = of
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
def should_be_coreced?
|
17
|
+
true
|
18
|
+
end
|
19
|
+
|
20
|
+
def execute
|
21
|
+
result = @value.to_a
|
22
|
+
|
23
|
+
return result unless @of
|
24
|
+
|
25
|
+
result.map! { |element| Coercion.new(@of).result_for(element) }
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Formalism
|
4
|
+
class Form < Action
|
5
|
+
class Coercion
|
6
|
+
## Class for coercion to boolean
|
7
|
+
class Boolean < Base
|
8
|
+
private
|
9
|
+
|
10
|
+
def execute
|
11
|
+
@value && @value.to_s != 'false' ? true : false
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Formalism
|
4
|
+
class Form < Action
|
5
|
+
class Coercion
|
6
|
+
## Class for coercion to Date
|
7
|
+
class Date < Base
|
8
|
+
private
|
9
|
+
|
10
|
+
def execute
|
11
|
+
return if @value.nil?
|
12
|
+
|
13
|
+
::Date.parse(@value)
|
14
|
+
rescue ArgumentError => e
|
15
|
+
raise unless e.message == 'invalid date'
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Formalism
|
4
|
+
class Form < Action
|
5
|
+
class Coercion
|
6
|
+
## Class for coercion to Float
|
7
|
+
class Float < Numeric
|
8
|
+
## https://stackoverflow.com/a/36946626/2630849
|
9
|
+
VALUE_REGEXP = wrap_value_regexp '[-+]?(?:\d+(?:\.\d*)?|\.\d+)'
|
10
|
+
|
11
|
+
CONVERSION_METHOD = :to_f
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Formalism
|
4
|
+
class Form < Action
|
5
|
+
class Coercion
|
6
|
+
## Class for coercion to Integer
|
7
|
+
class Integer < Numeric
|
8
|
+
## https://stackoverflow.com/a/1235990/2630849
|
9
|
+
VALUE_REGEXP = wrap_value_regexp '[-+]?\d+'
|
10
|
+
|
11
|
+
CONVERSION_METHOD = :to_i
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Formalism
|
4
|
+
class Form < Action
|
5
|
+
class Coercion
|
6
|
+
## Class for coercion to Time
|
7
|
+
class Time < Base
|
8
|
+
private
|
9
|
+
|
10
|
+
def execute
|
11
|
+
case @value
|
12
|
+
when ::String
|
13
|
+
::Time.parse @value
|
14
|
+
when ::Integer
|
15
|
+
::Time.at @value
|
16
|
+
end
|
17
|
+
rescue ArgumentError => e
|
18
|
+
raise unless e.message.include? 'out of range'
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,153 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'module_methods'
|
4
|
+
|
5
|
+
require_relative 'coercion'
|
6
|
+
|
7
|
+
module Formalism
|
8
|
+
class Form < Action
|
9
|
+
## Extend some module or class with this module for fields
|
10
|
+
module Fields
|
11
|
+
extend ::ModuleMethods::Extension
|
12
|
+
|
13
|
+
## Module for class methods
|
14
|
+
module ClassMethods
|
15
|
+
def included(something)
|
16
|
+
super
|
17
|
+
|
18
|
+
fields_and_nested_forms.each do |name, options|
|
19
|
+
if options.key?(:form)
|
20
|
+
something.nested name, options[:form], **options
|
21
|
+
else
|
22
|
+
something.field name, options[:type], **options
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def fields_and_nested_forms
|
28
|
+
@fields_and_nested_forms ||= {}
|
29
|
+
end
|
30
|
+
|
31
|
+
def field(name, type = nil, **options)
|
32
|
+
Coercion.new(type, options[:of]).check unless type.nil?
|
33
|
+
|
34
|
+
fields_and_nested_forms[name] = options.merge(type: type)
|
35
|
+
|
36
|
+
define_field_methods(name)
|
37
|
+
end
|
38
|
+
|
39
|
+
def nested(name, form = nil, **options)
|
40
|
+
unless form || options.key?(:initialize)
|
41
|
+
raise ArgumentError, 'Neither form class nor initialize block ' \
|
42
|
+
'is not present'
|
43
|
+
end
|
44
|
+
|
45
|
+
fields_and_nested_forms[name] = options.merge(form: form)
|
46
|
+
|
47
|
+
define_nested_form_methods(name)
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
def remove_field(name)
|
53
|
+
fields_and_nested_forms.delete name
|
54
|
+
|
55
|
+
undef_method name
|
56
|
+
undef_method "#{name}="
|
57
|
+
end
|
58
|
+
|
59
|
+
def define_field_methods(name)
|
60
|
+
module_for_accessors.instance_exec do
|
61
|
+
define_method(name) { fields[name] }
|
62
|
+
|
63
|
+
private
|
64
|
+
|
65
|
+
define_method("#{name}=") do |value|
|
66
|
+
options = self.class.fields_and_nested_forms[name]
|
67
|
+
coerced_value =
|
68
|
+
Coercion.new(*options.values_at(:type, :of)).result_for(value)
|
69
|
+
fields[name] = coerced_value
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def define_nested_form_methods(name)
|
75
|
+
module_for_accessors.instance_exec do
|
76
|
+
define_method("#{name}_form") { nested_forms[name] }
|
77
|
+
|
78
|
+
define_method(name) { nested_forms[name].instance }
|
79
|
+
end
|
80
|
+
|
81
|
+
define_params_for_nested_method name
|
82
|
+
end
|
83
|
+
|
84
|
+
def define_params_for_nested_method(name)
|
85
|
+
params_method_name = "params_for_nested_#{name}"
|
86
|
+
params_method_defined =
|
87
|
+
method_defined?(params_method_name) ||
|
88
|
+
private_method_defined?(params_method_name)
|
89
|
+
|
90
|
+
module_for_accessors.instance_exec do
|
91
|
+
private
|
92
|
+
|
93
|
+
define_method(params_method_name) { @params[name] } unless params_method_defined
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def module_for_accessors
|
98
|
+
if const_defined?(:FieldsAccessors, false)
|
99
|
+
mod = const_get(:FieldsAccessors)
|
100
|
+
else
|
101
|
+
mod = const_set(:FieldsAccessors, Module.new)
|
102
|
+
include mod
|
103
|
+
end
|
104
|
+
mod
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
def fields(for_merge: false)
|
109
|
+
@fields ||= {}
|
110
|
+
|
111
|
+
return @fields unless for_merge
|
112
|
+
|
113
|
+
select_for_merge(:fields)
|
114
|
+
end
|
115
|
+
|
116
|
+
def to_params
|
117
|
+
fields.merge(
|
118
|
+
nested_forms.each_with_object({}) do |(name, nested_form), result|
|
119
|
+
result.merge! nested_form_to_params name, nested_form
|
120
|
+
end
|
121
|
+
)
|
122
|
+
end
|
123
|
+
|
124
|
+
private
|
125
|
+
|
126
|
+
def nested_forms
|
127
|
+
@nested_forms ||= {}
|
128
|
+
end
|
129
|
+
|
130
|
+
def fields_and_nested_forms(for_merge: true)
|
131
|
+
merging_nested_forms =
|
132
|
+
for_merge ? select_for_merge(:nested_forms) : nested_forms
|
133
|
+
|
134
|
+
fields(for_merge: for_merge).merge(
|
135
|
+
merging_nested_forms
|
136
|
+
.map { |name, _nested_form| [name, public_send(name)] }
|
137
|
+
.to_h
|
138
|
+
)
|
139
|
+
end
|
140
|
+
|
141
|
+
def select_for_merge(type)
|
142
|
+
send(type).select do |name, value|
|
143
|
+
merge = self.class.fields_and_nested_forms[name].fetch(:merge, true)
|
144
|
+
merge.is_a?(Proc) ? instance_exec(value, &merge) : merge
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
def nested_form_to_params(name_of_nested_form, nested_form)
|
149
|
+
{ name_of_nested_form => nested_form.to_params }
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Formalism
|
4
|
+
class Form < Action
|
5
|
+
## Module for filling forms with data
|
6
|
+
module Filling
|
7
|
+
private
|
8
|
+
|
9
|
+
def fill_fields_and_nested_forms
|
10
|
+
@filled_fields_and_nested_forms = []
|
11
|
+
|
12
|
+
self.class.fields_and_nested_forms.each_key do |name|
|
13
|
+
fill_field_or_nested_form name
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def fill_field_or_nested_form(name)
|
18
|
+
return if @filled_fields_and_nested_forms.include? name
|
19
|
+
|
20
|
+
options = self.class.fields_and_nested_forms[name]
|
21
|
+
|
22
|
+
fill_depends(*options[:depends_on])
|
23
|
+
|
24
|
+
if options.key?(:form)
|
25
|
+
fill_nested_form name, options
|
26
|
+
else
|
27
|
+
fill_field name, options
|
28
|
+
end
|
29
|
+
|
30
|
+
@filled_fields_and_nested_forms.push name
|
31
|
+
end
|
32
|
+
|
33
|
+
def fill_depends(*depends_on)
|
34
|
+
depends_on.each do |depends_name|
|
35
|
+
next unless self.class.fields_and_nested_forms.key?(depends_name)
|
36
|
+
|
37
|
+
fill_field_or_nested_form depends_name
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def fill_field(name, options)
|
42
|
+
key = options.fetch(:key, name)
|
43
|
+
setter = "#{name}="
|
44
|
+
|
45
|
+
if @params.key?(key)
|
46
|
+
send setter, @params[key]
|
47
|
+
elsif instance_respond_to?(key)
|
48
|
+
send setter, instance_public_send(key)
|
49
|
+
elsif options.key?(:default) && !fields.include?(key)
|
50
|
+
send setter, process_default(options[:default])
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def fill_nested_form(name, options)
|
55
|
+
return unless (form = initialize_nested_form(name, options))
|
56
|
+
|
57
|
+
nested_forms[name] = form
|
58
|
+
end
|
59
|
+
|
60
|
+
def process_default(default)
|
61
|
+
default.is_a?(Proc) ? instance_exec(&default) : default
|
62
|
+
end
|
63
|
+
|
64
|
+
def initialize_nested_form(name, options)
|
65
|
+
args =
|
66
|
+
if @params.key?(name) then [send("params_for_nested_#{name}")]
|
67
|
+
elsif instance_respond_to?(name) then [instance_public_send(name)]
|
68
|
+
elsif options.key?(:default) then [process_default(options[:default])]
|
69
|
+
else []
|
70
|
+
end
|
71
|
+
|
72
|
+
result =
|
73
|
+
instance_exec options[:form], &options.fetch(:initialize, ->(form) { form.new(*args) })
|
74
|
+
result.runnable = false unless runnable
|
75
|
+
result
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
private_constant :Filling
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Formalism
|
4
|
+
## https://github.com/rubocop-hq/rubocop/issues/5831
|
5
|
+
class Form < Action
|
6
|
+
## Private class for results
|
7
|
+
class Outcome
|
8
|
+
attr_reader :errors
|
9
|
+
|
10
|
+
def initialize(errors, result = nil)
|
11
|
+
@errors = errors
|
12
|
+
@result = result
|
13
|
+
end
|
14
|
+
|
15
|
+
def success?
|
16
|
+
@errors.empty?
|
17
|
+
end
|
18
|
+
|
19
|
+
def result
|
20
|
+
raise ValidationError, errors if errors.any?
|
21
|
+
|
22
|
+
@result
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
private_constant :Outcome
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Formalism
|
4
|
+
class Form < Action
|
5
|
+
## When trying to get `#result` from unsuccessful outcome
|
6
|
+
class ValidationError < StandardError
|
7
|
+
attr_reader :errors
|
8
|
+
|
9
|
+
def initialize(errors)
|
10
|
+
@errors = errors
|
11
|
+
super "Outcome has errors: #{errors.to_a}"
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
metadata
ADDED
@@ -0,0 +1,238 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: formalism
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.2.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Alexander Popov
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2020-09-23 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: gorilla_patch
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '4'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '4'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: module_methods
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 0.1.0
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 0.1.0
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: pry-byebug
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '3.9'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '3.9'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: bundler
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '2.0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '2.0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: gem_toys
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: 0.4.0
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: 0.4.0
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: toys
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: 0.11.0
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: 0.11.0
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: codecov
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - "~>"
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: 0.2.1
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - "~>"
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: 0.2.1
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: rspec
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - "~>"
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '3.9'
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - "~>"
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '3.9'
|
125
|
+
- !ruby/object:Gem::Dependency
|
126
|
+
name: simplecov
|
127
|
+
requirement: !ruby/object:Gem::Requirement
|
128
|
+
requirements:
|
129
|
+
- - "~>"
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: 0.19.0
|
132
|
+
type: :development
|
133
|
+
prerelease: false
|
134
|
+
version_requirements: !ruby/object:Gem::Requirement
|
135
|
+
requirements:
|
136
|
+
- - "~>"
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
version: 0.19.0
|
139
|
+
- !ruby/object:Gem::Dependency
|
140
|
+
name: rubocop
|
141
|
+
requirement: !ruby/object:Gem::Requirement
|
142
|
+
requirements:
|
143
|
+
- - "~>"
|
144
|
+
- !ruby/object:Gem::Version
|
145
|
+
version: 0.91.0
|
146
|
+
type: :development
|
147
|
+
prerelease: false
|
148
|
+
version_requirements: !ruby/object:Gem::Requirement
|
149
|
+
requirements:
|
150
|
+
- - "~>"
|
151
|
+
- !ruby/object:Gem::Version
|
152
|
+
version: 0.91.0
|
153
|
+
- !ruby/object:Gem::Dependency
|
154
|
+
name: rubocop-performance
|
155
|
+
requirement: !ruby/object:Gem::Requirement
|
156
|
+
requirements:
|
157
|
+
- - "~>"
|
158
|
+
- !ruby/object:Gem::Version
|
159
|
+
version: '1.0'
|
160
|
+
type: :development
|
161
|
+
prerelease: false
|
162
|
+
version_requirements: !ruby/object:Gem::Requirement
|
163
|
+
requirements:
|
164
|
+
- - "~>"
|
165
|
+
- !ruby/object:Gem::Version
|
166
|
+
version: '1.0'
|
167
|
+
- !ruby/object:Gem::Dependency
|
168
|
+
name: rubocop-rspec
|
169
|
+
requirement: !ruby/object:Gem::Requirement
|
170
|
+
requirements:
|
171
|
+
- - "~>"
|
172
|
+
- !ruby/object:Gem::Version
|
173
|
+
version: '1.43'
|
174
|
+
type: :development
|
175
|
+
prerelease: false
|
176
|
+
version_requirements: !ruby/object:Gem::Requirement
|
177
|
+
requirements:
|
178
|
+
- - "~>"
|
179
|
+
- !ruby/object:Gem::Version
|
180
|
+
version: '1.43'
|
181
|
+
description: 'Simple actions and complex forms with validations, nesting, etc.
|
182
|
+
|
183
|
+
'
|
184
|
+
email: alex.wayfer@gmail.com
|
185
|
+
executables: []
|
186
|
+
extensions: []
|
187
|
+
extra_rdoc_files: []
|
188
|
+
files:
|
189
|
+
- CHANGELOG.md
|
190
|
+
- LICENSE.txt
|
191
|
+
- README.md
|
192
|
+
- lib/formalism.rb
|
193
|
+
- lib/formalism/action.rb
|
194
|
+
- lib/formalism/form.rb
|
195
|
+
- lib/formalism/form/coercion.rb
|
196
|
+
- lib/formalism/form/coercion/_base.rb
|
197
|
+
- lib/formalism/form/coercion/_numeric.rb
|
198
|
+
- lib/formalism/form/coercion/array.rb
|
199
|
+
- lib/formalism/form/coercion/boolean.rb
|
200
|
+
- lib/formalism/form/coercion/date.rb
|
201
|
+
- lib/formalism/form/coercion/float.rb
|
202
|
+
- lib/formalism/form/coercion/hash.rb
|
203
|
+
- lib/formalism/form/coercion/integer.rb
|
204
|
+
- lib/formalism/form/coercion/string.rb
|
205
|
+
- lib/formalism/form/coercion/symbol.rb
|
206
|
+
- lib/formalism/form/coercion/time.rb
|
207
|
+
- lib/formalism/form/fields.rb
|
208
|
+
- lib/formalism/form/filling.rb
|
209
|
+
- lib/formalism/form/outcome.rb
|
210
|
+
- lib/formalism/form/validation_error.rb
|
211
|
+
- lib/formalism/version.rb
|
212
|
+
homepage: https://github.com/AlexWayfer/formalism
|
213
|
+
licenses:
|
214
|
+
- MIT
|
215
|
+
metadata:
|
216
|
+
source_code_uri: https://github.com/AlexWayfer/formalism
|
217
|
+
homepage_uri: https://github.com/AlexWayfer/formalism
|
218
|
+
changelog_uri: https://github.com/AlexWayfer/formalism/blob/master/CHANGELOG.md
|
219
|
+
post_install_message:
|
220
|
+
rdoc_options: []
|
221
|
+
require_paths:
|
222
|
+
- lib
|
223
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
224
|
+
requirements:
|
225
|
+
- - "~>"
|
226
|
+
- !ruby/object:Gem::Version
|
227
|
+
version: '2.5'
|
228
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
229
|
+
requirements:
|
230
|
+
- - ">="
|
231
|
+
- !ruby/object:Gem::Version
|
232
|
+
version: '0'
|
233
|
+
requirements: []
|
234
|
+
rubygems_version: 3.1.2
|
235
|
+
signing_key:
|
236
|
+
specification_version: 4
|
237
|
+
summary: Forms with input data validations and nesting
|
238
|
+
test_files: []
|