goaltender 0.0.3 → 0.0.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +246 -21
- data/lib/goaltender/base.rb +8 -1
- data/lib/goaltender/input.rb +1 -1
- data/lib/goaltender/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 359c5ed702221fc18627406c2b9067f9b415b5b0
|
4
|
+
data.tar.gz: 77af3103925d36f9c0a5e7ad8760fe7326541b28
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: adea5d8dd480ddf7e2d03d10b01b2a433101fefdcd3d14fb4c1f734e348d22bfd24b38e1704e1fa4bc6cbd90c7a7e93f6ff86b8097908efb1a914242ded187b7
|
7
|
+
data.tar.gz: 2ce3768d1d2e6220a326725332c01115bbfe85282b9e0575ddb552ed5481d72d341ca2c03018361d22cb59f400fc53b6479fb70e82505e7179bf4e2217351e7c
|
data/README.md
CHANGED
@@ -1,39 +1,264 @@
|
|
1
|
-
|
1
|
+
In my humble opinion, Rails is missing a tool for parsing forms on the backend. Currently the process looks something like this:
|
2
2
|
|
3
|
-
|
3
|
+
```html
|
4
|
+
# new.html.erb
|
5
|
+
<%= form_for @trip do |trip_builder| %>
|
6
|
+
<div class="field">
|
7
|
+
<%= trip_builder.label :miles %>
|
8
|
+
<%= trip_builder.text_field :miles, placeholder: '50 miles' %>
|
9
|
+
</div>
|
10
|
+
<% end %>
|
11
|
+
```
|
12
|
+
|
13
|
+
```ruby
|
14
|
+
# trips_controller
|
15
|
+
|
16
|
+
def create
|
17
|
+
@trip = Trip.new(trip_params)
|
18
|
+
|
19
|
+
if @trip.save
|
20
|
+
redirect_to trips_path, notice: "New trip successfully created."
|
21
|
+
else
|
22
|
+
render "new"
|
23
|
+
end
|
24
|
+
end
|
25
|
+
```
|
26
|
+
|
27
|
+
The problem with this approach is that there is nothing that processes the user inputs before being saved into the database. For example, if the user types "1,000 miles" into the form, Rails would store 0 instead of 1000 into our "miles" column.
|
28
|
+
|
29
|
+
The "Services" concept that many Rails developers favor is on point. Every form should have a corresponding Ruby class whose responsibility is to process the inputs and get the form ready for storage.
|
30
|
+
|
31
|
+
However, this "Services" concept does not have a supporting framework. It is repetitive for every developer, for example, to write code to parse a decimal field that comes into our Ruby controller as a string (as in the example above).
|
32
|
+
|
33
|
+
Here is how it may be done right now:
|
34
|
+
|
35
|
+
```ruby
|
36
|
+
class Forms::Trip
|
37
|
+
|
38
|
+
attr_accessor :trip
|
39
|
+
|
40
|
+
def initialize(args = {})
|
41
|
+
@trip = Trip.new
|
42
|
+
@trip.miles = parse_miles(args[:miles)
|
43
|
+
end
|
44
|
+
|
45
|
+
def parse_miles(miles_input)
|
46
|
+
regex = /(\d|[.])/ # only care about numbers and decimals
|
47
|
+
miles_input.scan(regex).join.try(:to_f)
|
48
|
+
end
|
49
|
+
|
50
|
+
def save
|
51
|
+
@trip.save
|
52
|
+
end
|
53
|
+
|
54
|
+
end
|
55
|
+
```
|
56
|
+
|
57
|
+
```ruby
|
58
|
+
# trips_controller.rb
|
59
|
+
|
60
|
+
def create
|
61
|
+
form = Forms::Trip.new(trip_params)
|
62
|
+
@trip = form.trip
|
63
|
+
|
64
|
+
if form.save
|
65
|
+
redirect_to trips_path, notice: "New trip successfully created."
|
66
|
+
else
|
67
|
+
render "new"
|
68
|
+
end
|
69
|
+
end
|
70
|
+
```
|
71
|
+
|
72
|
+
While this process is not so bad with only one field and one model, it gets more complex with many different fields, types of inputs, and especially with nested attributes.
|
73
|
+
|
74
|
+
A better solution is to have the form service classes inherit from a base "form parser" class that can handle the common parsing needs of the community. For example:
|
75
|
+
|
76
|
+
```ruby
|
77
|
+
class Forms::Trip < FormParse::Base
|
78
|
+
|
79
|
+
input :miles, :float
|
80
|
+
|
81
|
+
def after_init(args)
|
82
|
+
@trip = Trip.new(to_hash)
|
83
|
+
end
|
84
|
+
|
85
|
+
end
|
86
|
+
```
|
87
|
+
|
88
|
+
```ruby
|
89
|
+
# trips_controller.rb
|
90
|
+
# same as above
|
91
|
+
|
92
|
+
def create
|
93
|
+
form = Forms::Trip.new(trip_params)
|
94
|
+
@trip = form.trip
|
95
|
+
|
96
|
+
if form.save
|
97
|
+
redirect_to trips_path, notice: "New trip successfully created."
|
98
|
+
else
|
99
|
+
render "new"
|
100
|
+
end
|
101
|
+
end
|
102
|
+
```
|
103
|
+
|
104
|
+
The FormParse::Base class that Forms::Trip inherits from by default performs the proper regex as seen in the original Forms::Trip service object above. We need only define the input key and the type of input and the base class takes care of the heavy lifting.
|
4
105
|
|
5
|
-
|
106
|
+
The "to_hash" method takes the inputs and converts the parsed values into a hash, which produces the following in this case:
|
6
107
|
|
7
|
-
|
108
|
+
```ruby
|
109
|
+
{ miles: 1000.0 }
|
110
|
+
```
|
8
111
|
|
9
|
-
|
112
|
+
A more complex form would end up with a service class like below:
|
10
113
|
|
11
114
|
```ruby
|
12
|
-
|
115
|
+
class Forms::Trip
|
116
|
+
|
117
|
+
attr_accessor :trip
|
118
|
+
|
119
|
+
def initialize(args = {})
|
120
|
+
@trip = Trip.new
|
121
|
+
@trip.miles = parse_miles(args[:miles)
|
122
|
+
@trip.origin_city = args[:origin_city]
|
123
|
+
@trip.origin_state = args[:origin_state]
|
124
|
+
@trip.departure_datetime = parse_datetime(args[:departure_datetime])
|
125
|
+
@trip.destination_city = args[:destination_city]
|
126
|
+
@trip.destination_state = args[:destination_state]
|
127
|
+
@trip.arrival_datetime = parse_datetime(args[:arrival_datetime])
|
128
|
+
end
|
129
|
+
|
130
|
+
def parse_miles(miles_input)
|
131
|
+
regex = /(\d|[.])/ # only care about numbers and decimals
|
132
|
+
miles_input.scan(regex).join.try(:to_f)
|
133
|
+
end
|
134
|
+
|
135
|
+
def parse_datetime(datetime_input)
|
136
|
+
Date.strptime(datetime, "%m/%d/%Y")
|
137
|
+
end
|
138
|
+
|
139
|
+
def save
|
140
|
+
@trip.save
|
141
|
+
end
|
142
|
+
|
143
|
+
end
|
13
144
|
```
|
14
145
|
|
15
|
-
|
146
|
+
With a framework, it would only involve the following:
|
16
147
|
|
17
|
-
|
148
|
+
```ruby
|
149
|
+
class Forms::Trip < FormParse::Base
|
18
150
|
|
19
|
-
|
151
|
+
input :miles, :float
|
152
|
+
input :origin_city, :string
|
153
|
+
input :origin_state, :string
|
154
|
+
input :departure_datetime, :datetime
|
155
|
+
input :destination_city, :string
|
156
|
+
input :destination_state, :string
|
157
|
+
input :arrival_datetime, :datetime
|
20
158
|
|
21
|
-
|
159
|
+
def after_init(args)
|
160
|
+
@trip = Trip.new(to_hash)
|
161
|
+
end
|
22
162
|
|
23
|
-
|
163
|
+
# to_hash produces:
|
164
|
+
# {
|
165
|
+
# miles: 1000.0,
|
166
|
+
# origin_city: "Chicago",
|
167
|
+
# origin_state: "IL",
|
168
|
+
# departure_datetime: # DateTime object
|
169
|
+
# destination_city: "New York",
|
170
|
+
# destination_state: "NY",
|
171
|
+
# arrival_datetime: # DateTime object
|
172
|
+
# }
|
24
173
|
|
25
|
-
|
174
|
+
end
|
175
|
+
```
|
26
176
|
|
27
|
-
|
177
|
+
Taking it a step further, if our form inputs are not database backed, we can even validate within our new service object like so:
|
28
178
|
|
29
|
-
|
179
|
+
```ruby
|
180
|
+
class FormParse::Base
|
181
|
+
include ActiveModel::Model
|
182
|
+
|
183
|
+
# ... base parsing code
|
184
|
+
end
|
185
|
+
|
186
|
+
class Form::Trip < FormParse::Base
|
187
|
+
|
188
|
+
input :miles, :float
|
189
|
+
input :origin_city, :string
|
190
|
+
input :origin_city, :string
|
191
|
+
input :destination_city, :string
|
192
|
+
input :destination_state, :string
|
193
|
+
|
194
|
+
validates :miles, :origin_city, :origin_state, :destination_city, :destination_state, presence: true
|
195
|
+
|
196
|
+
def after_init(args)
|
197
|
+
@trip = Trip.new(to_hash)
|
198
|
+
end
|
199
|
+
|
200
|
+
def save
|
201
|
+
if valid? && trip.valid?
|
202
|
+
trip.save
|
203
|
+
else
|
204
|
+
return false
|
205
|
+
end
|
206
|
+
end
|
207
|
+
|
208
|
+
end
|
209
|
+
```
|
30
210
|
|
31
|
-
|
211
|
+
Of course, the save method could be abstracted to our base class too, assuming we define which object the Form::Trip class is saving like so:
|
32
212
|
|
33
|
-
|
213
|
+
```ruby
|
214
|
+
class Form::Trip
|
215
|
+
|
216
|
+
def object
|
217
|
+
trip
|
218
|
+
end
|
219
|
+
|
220
|
+
# ... above code here
|
221
|
+
end
|
222
|
+
|
223
|
+
class FormParse::Base
|
224
|
+
|
225
|
+
def save
|
226
|
+
if valid? && object.valid?
|
227
|
+
object.save
|
228
|
+
else
|
229
|
+
return false
|
230
|
+
end
|
231
|
+
end
|
232
|
+
end
|
233
|
+
|
234
|
+
```
|
235
|
+
|
236
|
+
But where we really can see benefits from a "framework" for parsing our Rails forms is when we start to use nested attributes. For example, if we abstract the origin and destination fields to a "Location" model, we could build out a separate service class to handle parsing our "Location" form (even if it is in the context of a parent object like Trip).
|
237
|
+
|
238
|
+
```ruby
|
239
|
+
class Forms::Location < FormParse::Base
|
240
|
+
|
241
|
+
input :city, :string
|
242
|
+
input :state, :string
|
243
|
+
input :departure_datetime, :datetime
|
244
|
+
input :arrival_datetime, :datetime
|
245
|
+
|
246
|
+
validates :city, :state :presence => true
|
247
|
+
|
248
|
+
end
|
249
|
+
|
250
|
+
class Forms::Trip < FormParse::Base
|
251
|
+
|
252
|
+
input :miles, :float
|
253
|
+
input :origin_attributes, :belongs_to, class: 'location'
|
254
|
+
input :destination_attributes, :belongs_to, class: 'location'
|
255
|
+
|
256
|
+
validates :miles, presence: true
|
257
|
+
|
258
|
+
def after_init(args)
|
259
|
+
@trip = Trip.new(to_hash)
|
260
|
+
end
|
261
|
+
end
|
262
|
+
```
|
34
263
|
|
35
|
-
|
36
|
-
2. Create your feature branch (`git checkout -b my-new-feature`)
|
37
|
-
3. Commit your changes (`git commit -am 'Add some feature'`)
|
38
|
-
4. Push to the branch (`git push origin my-new-feature`)
|
39
|
-
5. Create a new Pull Request
|
264
|
+
The FormParse::Base class will recoginze the :belongs_to relationship and utilize the Location service class to parse the part of the params hash that correspond with our destination and origin models. This is great because if there is anywhere that the user can update just the origin or destination of the trip, we could just reuse that Location service object to parse the form.
|
data/lib/goaltender/base.rb
CHANGED
@@ -1,6 +1,13 @@
|
|
1
1
|
class Goaltender::Base
|
2
2
|
|
3
|
-
|
3
|
+
if Rails.version[0].to_i < 4
|
4
|
+
include ActiveModel::Validations
|
5
|
+
include ActiveModel::Conversion
|
6
|
+
extend ActiveModel::Naming
|
7
|
+
else
|
8
|
+
include ActiveModel::Model
|
9
|
+
end
|
10
|
+
|
4
11
|
include Goaltender::BaseModule
|
5
12
|
|
6
13
|
attr_reader :params, :inputs
|
data/lib/goaltender/input.rb
CHANGED
@@ -16,7 +16,7 @@ module Goaltender
|
|
16
16
|
end
|
17
17
|
|
18
18
|
def value_parser
|
19
|
-
@value_parser ||= "
|
19
|
+
@value_parser ||= "Goaltender::ValueParser::#{type.to_s.classify}".constantize.new({
|
20
20
|
input_value: input_value,
|
21
21
|
parse_format: parse_format,
|
22
22
|
form_class: form_class,
|
data/lib/goaltender/version.rb
CHANGED