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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 04e9a394f95b65ed94175af20c9d06f09c12e65b
4
- data.tar.gz: 6f71d0fe681e1451c7c91eefc253cfb4d7de83c4
3
+ metadata.gz: 359c5ed702221fc18627406c2b9067f9b415b5b0
4
+ data.tar.gz: 77af3103925d36f9c0a5e7ad8760fe7326541b28
5
5
  SHA512:
6
- metadata.gz: 64815ae11f3e5757035297c2b612056afdf54b17a4c70cb895f2f68ab62b25d3229b0bd8e0188e32055f6939b383b0d1404ed79af558bc146479bfd8e23e6b43
7
- data.tar.gz: e7bf2d1441bf6df976ed0172c1b7f6aced4fb31f5b4e98decd704bbc3d5c5a711300fb49915b79d4ec0c3900bff141e43186e4b52ddfb605c7ef6453a7af1ec5
6
+ metadata.gz: adea5d8dd480ddf7e2d03d10b01b2a433101fefdcd3d14fb4c1f734e348d22bfd24b38e1704e1fa4bc6cbd90c7a7e93f6ff86b8097908efb1a914242ded187b7
7
+ data.tar.gz: 2ce3768d1d2e6220a326725332c01115bbfe85282b9e0575ddb552ed5481d72d341ca2c03018361d22cb59f400fc53b6479fb70e82505e7179bf4e2217351e7c
data/README.md CHANGED
@@ -1,39 +1,264 @@
1
- # Goaltender
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
- Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/goaltender`. To experiment with that code, run `bin/console` for an interactive prompt.
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
- TODO: Delete this and the text above, and describe your gem
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
- ## Installation
108
+ ```ruby
109
+ { miles: 1000.0 }
110
+ ```
8
111
 
9
- Add this line to your application's Gemfile:
112
+ A more complex form would end up with a service class like below:
10
113
 
11
114
  ```ruby
12
- gem 'goaltender'
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
- And then execute:
146
+ With a framework, it would only involve the following:
16
147
 
17
- $ bundle
148
+ ```ruby
149
+ class Forms::Trip < FormParse::Base
18
150
 
19
- Or install it yourself as:
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
- $ gem install goaltender
159
+ def after_init(args)
160
+ @trip = Trip.new(to_hash)
161
+ end
22
162
 
23
- ## Usage
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
- TODO: Write usage instructions here
174
+ end
175
+ ```
26
176
 
27
- ## Development
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
- After checking out the repo, run `bin/setup` to install dependencies. Then, run `bin/console` for an interactive prompt that will allow you to experiment.
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
- To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release` to create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
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
- ## Contributing
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
- 1. Fork it ( https://github.com/[my-github-username]/goaltender/fork )
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.
@@ -1,6 +1,13 @@
1
1
  class Goaltender::Base
2
2
 
3
- include ActiveModel::Model
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
@@ -16,7 +16,7 @@ module Goaltender
16
16
  end
17
17
 
18
18
  def value_parser
19
- @value_parser ||= "FormParse::ValueParser::#{type.to_s.classify}".constantize.new({
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,
@@ -1,3 +1,3 @@
1
1
  module Goaltender
2
- VERSION = "0.0.3"
2
+ VERSION = "0.0.4"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: goaltender
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.3
4
+ version: 0.0.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ryan Francis