formant 0.1.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +9 -0
- data/.travis.yml +13 -0
- data/CHANGELOG.md +14 -0
- data/CODE_OF_CONDUCT.md +13 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +329 -0
- data/Rakefile +9 -0
- data/bin/console +14 -0
- data/bin/setup +7 -0
- data/formant.gemspec +33 -0
- data/lib/formant.rb +186 -0
- data/lib/formant/version.rb +3 -0
- metadata +209 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 0ef99a58902f6fdda0ef94e433385faa28d6d45d
|
4
|
+
data.tar.gz: 90212569c9a2edc2f82f6f1723fad875a8d36e3c
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: f1a28c65af280f5b1b98c0dcfd4a931dca84efd2d827756c163913a1f066af2b5ec609ebc4a45af80efc75d383dc9f98250fdcf660b5fa513e517411a99ad003
|
7
|
+
data.tar.gz: ced2edd67ec3e4334be521f228e5445f8d0e8d7a0f09248171a4aef492d494b7ee2572f47436d85a0da33ea91ad19b3600cac4f9358883d3197a554904237fa7
|
data/.gitignore
ADDED
data/.travis.yml
ADDED
data/CHANGELOG.md
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
# Change Log
|
2
|
+
All notable changes to this project will be documented in this file.
|
3
|
+
This project adheres to [Semantic Versioning](http://semver.org/).
|
4
|
+
|
5
|
+
## 0.1.1 2015-08-17
|
6
|
+
[changed] Make phony_rails gem a runtime dependency.
|
7
|
+
[changed] Internal test changes, switch to use activeSupport::TestCase.
|
8
|
+
[changed] Set random US timezone in test helper to help flush out timezone bugs.
|
9
|
+
|
10
|
+
## 0.1.1 2015-08-17
|
11
|
+
[changed] Updated dependencies in gemspec.
|
12
|
+
|
13
|
+
## 0.1.0 2015-08-17
|
14
|
+
Initial release.
|
data/CODE_OF_CONDUCT.md
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
# Contributor Code of Conduct
|
2
|
+
|
3
|
+
As contributors and maintainers of this project, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities.
|
4
|
+
|
5
|
+
We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, ethnicity, age, or religion.
|
6
|
+
|
7
|
+
Examples of unacceptable behavior by participants include the use of sexual language or imagery, derogatory comments or personal attacks, trolling, public or private harassment, insults, or other unprofessional conduct.
|
8
|
+
|
9
|
+
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct. Project maintainers who do not follow the Code of Conduct may be removed from the project team.
|
10
|
+
|
11
|
+
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by opening an issue or contacting one or more of the project maintainers.
|
12
|
+
|
13
|
+
This Code of Conduct is adapted from the [Contributor Covenant](http://contributor-covenant.org), version 1.0.0, available at [http://contributor-covenant.org/version/1/0/0/](http://contributor-covenant.org/version/1/0/0/)
|
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2015 Anthony Garcia
|
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,329 @@
|
|
1
|
+
# Formant
|
2
|
+
|
3
|
+
[![Build Status](https://travis-ci.org/polypressure/formant.svg?branch=master)](https://travis-ci.org/polypressure/formant)
|
4
|
+
[![Test Coverage](https://codeclimate.com/github/polypressure/formant/badges/coverage.svg)](https://codeclimate.com/github/polypressure/formant/coverage)
|
5
|
+
[![Code Climate](https://codeclimate.com/github/polypressure/formant/badges/gpa.svg)](https://codeclimate.com/github/polypressure/formant)
|
6
|
+
[![security](https://hakiri.io/github/polypressure/formant/master.svg)](https://hakiri.io/github/polypressure/formant/master)
|
7
|
+
[![Dependency Status](https://gemnasium.com/polypressure/formant.svg)](https://gemnasium.com/polypressure/formant)
|
8
|
+
|
9
|
+
|
10
|
+
|
11
|
+
Formant is a tiny library that provides a simplified, minimalistic form object implementation for Rails applications. A form object is a simple, (mostly) plain-old Ruby object, separate from your ActiveRecord models, that lets you collect and validate input. A bit more specifically, Formant helps you to keep any input parsing, normalization, validation, and formatting logic related to form processing out of your ActiveRecord models, ensuring that they stay lean and focused on persistence.
|
12
|
+
|
13
|
+
Form objects also simplify the collection of input from complicated forms. With form objects, when collecting input that involves multiple ActiveRecord models, you can avoid having to use something like `accepts_nested_attributes_for`. Instead, you define a single form object containing all the necessary fields spanning multiple models. You use this single form object in place of the multiple ActiveRecord models within your view, form, and controller. You can then parse, normalize, and validate that input in one place, and pass/distribute the form field values to whatever number of model objects are required by your business logic.
|
14
|
+
|
15
|
+
With Formant, you can declaratively specify any special parsing and normalization logic to apply to the form field values upon input (i.e. right before validation). Some built-in examples of parsing/normalizing input include:
|
16
|
+
|
17
|
+
* Stripping leading and trailing whitespace, and squishing internal whitespace.
|
18
|
+
* Parsing date/time strings using the current timezone.
|
19
|
+
* Normalizing phone numbers into a consistent internal format, e.g. "+13125551212" for storage in the database.
|
20
|
+
* Normalizing and parsing currency strings into a BigDecimal.
|
21
|
+
|
22
|
+
Once parsed and normalized, Formant then applies any validation rules that you've specified, using the standard validation macros provided by ActiveRecord. These can be invoked as usual, i.e. with the `valid?`, `invalid?`, or `validate` methods.
|
23
|
+
|
24
|
+
Formant also lets you specify special formatting rules on fields upon output (typically, when redisplaying forms). Some built-in examples of formatting output include:
|
25
|
+
|
26
|
+
* Rendering date/time values using localized string formats.
|
27
|
+
* Displaying phone numbers in a standard format, e.g. "(312) 555-1212"
|
28
|
+
* Formatting a BigDecimal value as a currency/price string.
|
29
|
+
* Formatting a number delimited with commas and decimal points.
|
30
|
+
|
31
|
+
|
32
|
+
|
33
|
+
|
34
|
+
## Installation
|
35
|
+
|
36
|
+
Add this line to your application's Gemfile:
|
37
|
+
|
38
|
+
```ruby
|
39
|
+
gem 'formant'
|
40
|
+
```
|
41
|
+
|
42
|
+
And then execute:
|
43
|
+
|
44
|
+
$ bundle
|
45
|
+
|
46
|
+
Or install it yourself as:
|
47
|
+
|
48
|
+
$ gem install formant
|
49
|
+
|
50
|
+
|
51
|
+
### Configuration
|
52
|
+
|
53
|
+
Add the following to your `config/application.rb`:
|
54
|
+
|
55
|
+
```ruby
|
56
|
+
module MyApplication
|
57
|
+
class Application < Rails::Application
|
58
|
+
...
|
59
|
+
config.autoload_paths << Rails.root.join('forms')
|
60
|
+
end
|
61
|
+
end
|
62
|
+
```
|
63
|
+
|
64
|
+
You can create a locale file containing date/time formats named `time_formats.en.yml`. In a standard Rails application, this file would go in the `config/locales` directory, and would look something like this:
|
65
|
+
|
66
|
+
```ruby
|
67
|
+
en:
|
68
|
+
time:
|
69
|
+
formats:
|
70
|
+
day_date_time: '%a, %b %e, %l:%M %p'
|
71
|
+
time_day_date: '%l:%M %p - %a %b %e'
|
72
|
+
fullday_date_time: '%A, %B %e, %l:%M %p'
|
73
|
+
|
74
|
+
```
|
75
|
+
|
76
|
+
## Usage
|
77
|
+
|
78
|
+
Define your form as a subclass of `Formant::FormObject`, specify the fields using `attr_accessor`, then specify parsing, validation, and formatting rules. Your form objects should go in the app/forms directory:
|
79
|
+
|
80
|
+
```ruby
|
81
|
+
class AppointmentForm < FormObject
|
82
|
+
|
83
|
+
attr_accessor(
|
84
|
+
:starts_at,
|
85
|
+
:first_name, :last_name,
|
86
|
+
:phone, :email,
|
87
|
+
:monthly_revenue,
|
88
|
+
:some_big_number
|
89
|
+
)
|
90
|
+
|
91
|
+
#
|
92
|
+
# Rules for any special parsing/transformation upon input…
|
93
|
+
#
|
94
|
+
|
95
|
+
# This parses datetime strings using the current Time.zone
|
96
|
+
# into a ActiveSupport::TimeWithZone object:
|
97
|
+
parse :starts_at, as: :datetime
|
98
|
+
|
99
|
+
# This normalizes phone numbers into a consistent internal format
|
100
|
+
# (with no dashes, dots, or other separators, and a leading "+1").
|
101
|
+
# For example, if the user passes in a phone number in the format
|
102
|
+
# "312-555-1212" or "(312)555.1212", it will be normalized
|
103
|
+
# into the format "+13125551212":
|
104
|
+
parse :phone, as: :phone_number
|
105
|
+
|
106
|
+
# This normalizes and parses a string price (possibly containing
|
107
|
+
# currency symbols, commas, and decimal points) into a BigDecimal
|
108
|
+
# value:
|
109
|
+
parse :monthly_revenue, as: :currency
|
110
|
+
|
111
|
+
|
112
|
+
# This strips leading and trailing whitespace from the field values:
|
113
|
+
parse :last_name, to: :strip_whitespace
|
114
|
+
parse :email, to: :strip_whitespace
|
115
|
+
# This also collapses multiple consecutive internal spaces into a single space:
|
116
|
+
parse :first_name, to: :strip_whitespace, squish: true
|
117
|
+
|
118
|
+
|
119
|
+
#
|
120
|
+
# Rules for any special formatting/transformation upon output
|
121
|
+
# or redisplay of the form. These will be triggered by invoking
|
122
|
+
# the FormObject#reformatted! method…
|
123
|
+
#
|
124
|
+
|
125
|
+
# This reformats the datetime in the :starts_at field using the
|
126
|
+
# format specified by the :day_date_time key, which can be defined
|
127
|
+
# in config/locales/time_formats.en.yml file:
|
128
|
+
reformat :starts_at, as: :datetime, format: :day_date_time
|
129
|
+
|
130
|
+
# This reformats the phone number in the :phone field into a
|
131
|
+
# standard format for the 'US'. For a phone number normalized
|
132
|
+
# to "+13125551212", the reformatted number is "(312) 555-1212":
|
133
|
+
reformat :phone, as: :phone_number, country_code: 'US'
|
134
|
+
|
135
|
+
# This reformats the BigDecimal in the :monthly_revenue field
|
136
|
+
# into a string, eg. "$10,252.32". You can pass it options as
|
137
|
+
# defined in http://api.rubyonrails.org/classes/ActionView/Helpers/NumberHelper.html#method-i-number_to_currency
|
138
|
+
reformat :monthly_revenue, as: :currency
|
139
|
+
|
140
|
+
# This reformats the number in the :some_big_number field into
|
141
|
+
# string delimited with commas and decimal points, e.g.
|
142
|
+
# "25,123.08". You can pass it options as # defined in
|
143
|
+
# http://api.rubyonrails.org/classes/ActionView/Helpers/NumberHelper.html#method-i-number_with_delimiter
|
144
|
+
reformat :some_big_number, as: :number_with_delimiter
|
145
|
+
|
146
|
+
#
|
147
|
+
# Validation rules, as usual:
|
148
|
+
#
|
149
|
+
validates :starts_at, presence: true
|
150
|
+
validate :in_future
|
151
|
+
|
152
|
+
validates :first_name, presence: true
|
153
|
+
validates :last_name, presence: true
|
154
|
+
|
155
|
+
validates_plausible_phone :phone, presence: true
|
156
|
+
validates :email, presence: true, email: true
|
157
|
+
|
158
|
+
def in_future
|
159
|
+
errors.add(:starts_at, "must be in the future") if starts_at && starts_at.past?
|
160
|
+
end
|
161
|
+
|
162
|
+
end
|
163
|
+
```
|
164
|
+
|
165
|
+
In your controller, instantiate a FormObject by passing it the request params, then validate the input params by calling either `valid?` or `invalid?`. If validation succeeds, you can then pass the form field values to any models or business-logic objects. If validation fails, you can redisplay the form as usual, passing the form object (rather than the model object) to the view—just as you would with an ActiveRecord model:
|
166
|
+
|
167
|
+
```ruby
|
168
|
+
class AppointmentsController < ApplicationController
|
169
|
+
|
170
|
+
def create
|
171
|
+
appointment_form = AppointmentForm.new(appointment_params)
|
172
|
+
|
173
|
+
#
|
174
|
+
# Ideally, this should be in a separate business-logic object,
|
175
|
+
# we're showing this logic inline within the controller for
|
176
|
+
# the sake of simplicity:
|
177
|
+
#
|
178
|
+
if appointment_form.invalid?
|
179
|
+
@appointment_form = appointment_form.reformatted!
|
180
|
+
render :new and return
|
181
|
+
end
|
182
|
+
|
183
|
+
client = Client.new(
|
184
|
+
first_name: appointment_form.first_name,
|
185
|
+
last_name: appointment_form.last_name,
|
186
|
+
phone: appointment_form.phone,
|
187
|
+
email: appointment_form.email
|
188
|
+
)
|
189
|
+
|
190
|
+
appointment = Appointment.new(
|
191
|
+
starts_at: appointment_form.starts_at
|
192
|
+
)
|
193
|
+
|
194
|
+
client.save!
|
195
|
+
appointment.save!
|
196
|
+
|
197
|
+
redirect_to appointments_path, notice: "Your appointment has been booked."
|
198
|
+
end
|
199
|
+
|
200
|
+
end
|
201
|
+
```
|
202
|
+
|
203
|
+
Your form looks just as it normally would, but rather than using your ActiveRecord models directly, you use the form object instead. Here's an example written in the Slim templating language:
|
204
|
+
|
205
|
+
```ruby
|
206
|
+
h4 Make an appointment
|
207
|
+
|
208
|
+
= form_for(@appointment_form, url: appointments_url) do |f|
|
209
|
+
= render "shared/validation_errors", errors: @appointment_form.errors
|
210
|
+
|
211
|
+
.row.collapse
|
212
|
+
.small-2.columns
|
213
|
+
= f.text_field :starts_at, placeholder: 'Date/Time', required: true
|
214
|
+
.small-2.columns
|
215
|
+
= f.text_field :first_name, placeholder: 'First Name', required: true
|
216
|
+
.small-2.columns
|
217
|
+
= f.text_field :last_name, placeholder: 'Last Name', required: true
|
218
|
+
.small-2.columns
|
219
|
+
= f.text_field :phone, placeholder: 'Phone', required: true
|
220
|
+
.small-2.columns
|
221
|
+
= f.text_field :email, placeholder: 'Email', required: true, type: 'email'
|
222
|
+
|
223
|
+
.row
|
224
|
+
.small-12.columns
|
225
|
+
= f.button 'Submit'
|
226
|
+
```
|
227
|
+
You can see that you can access the fields and the validation errors as you usually would with an ActiveRecord model.
|
228
|
+
|
229
|
+
### Other stuff
|
230
|
+
|
231
|
+
You can get a params hash of all the form object's attribute by calling `to_params`.
|
232
|
+
|
233
|
+
You can register callbacks to be invoked before and after validation:
|
234
|
+
|
235
|
+
```ruby
|
236
|
+
class MyForm < Formant::FormObject
|
237
|
+
...
|
238
|
+
|
239
|
+
before_validation :do_stuff_before_validation
|
240
|
+
after_validation :do_stuff_after_validation
|
241
|
+
|
242
|
+
...
|
243
|
+
|
244
|
+
def do_stuff_before_validation
|
245
|
+
# Some pre-validation logic.
|
246
|
+
end
|
247
|
+
|
248
|
+
def do_stuff_after_validation
|
249
|
+
# Some post validation logic.
|
250
|
+
end
|
251
|
+
|
252
|
+
end
|
253
|
+
|
254
|
+
```
|
255
|
+
|
256
|
+
## Additional parsing and reformatting rules
|
257
|
+
|
258
|
+
Parsing and formatting rules can be added trivially. Here is an example for how you would add rules for converting a blank string into a nil:
|
259
|
+
|
260
|
+
```ruby
|
261
|
+
#
|
262
|
+
#
|
263
|
+
module BlankAsNil
|
264
|
+
|
265
|
+
#
|
266
|
+
# All parse rule methods must have a name beginning with
|
267
|
+
# the prefix "parse_" and ending with the parse rule type.
|
268
|
+
# In this example, the parse rule type is "slug".
|
269
|
+
#
|
270
|
+
# The method signature should always be as follows: the first
|
271
|
+
# argument is field_value, in which Formant passes the unparsed
|
272
|
+
# value of the form field/attribute. The second argument
|
273
|
+
# contains a hash of any options required by your parse rule
|
274
|
+
# logic.
|
275
|
+
#
|
276
|
+
# Your parse method should return the parsed value of course.
|
277
|
+
# Formant takes care of assigning it to the field attribute.
|
278
|
+
#
|
279
|
+
def parse_blank_into_nil(field_value, options={})
|
280
|
+
field_value.nil? || (field_value.is_a?(String) && field_value !~ /\S/) ? nil : field_value
|
281
|
+
end
|
282
|
+
|
283
|
+
|
284
|
+
#
|
285
|
+
# For a reformatting rule, it's pretty much the same thing,
|
286
|
+
# except you replace the "parse_" prefix in the method name
|
287
|
+
# with "format_".
|
288
|
+
#
|
289
|
+
# This is an example for formatting numbers with a delimiter,
|
290
|
+
# assuming Formant didn't already provide this:
|
291
|
+
#
|
292
|
+
def format_number_with_delimiter(field_value, options={})
|
293
|
+
number_with_delimiter(field_value, options)
|
294
|
+
end
|
295
|
+
|
296
|
+
end
|
297
|
+
|
298
|
+
#
|
299
|
+
# You can then specify that the parse rule can be used as normal
|
300
|
+
# with the parse macro/directive:
|
301
|
+
#
|
302
|
+
class MyForm < Formant::FormObject
|
303
|
+
include MoreParsingAndFormattingRules
|
304
|
+
|
305
|
+
attr_accessor :title, :some_number
|
306
|
+
|
307
|
+
parse :title, as: :blank_into_nil
|
308
|
+
format :some_number, as: :number_with_delimiter
|
309
|
+
end
|
310
|
+
|
311
|
+
|
312
|
+
|
313
|
+
```
|
314
|
+
|
315
|
+
|
316
|
+
|
317
|
+
## Development
|
318
|
+
|
319
|
+
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.
|
320
|
+
|
321
|
+
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).
|
322
|
+
|
323
|
+
## Contributing
|
324
|
+
|
325
|
+
1. Fork it ( https://github.com/polypressure/formant/fork )
|
326
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
327
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
328
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
329
|
+
5. Create a new Pull Request
|
data/Rakefile
ADDED
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "formant"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "irb"
|
14
|
+
IRB.start
|
data/bin/setup
ADDED
data/formant.gemspec
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'formant/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "formant"
|
8
|
+
spec.version = Formant::VERSION
|
9
|
+
spec.authors = ["Anthony Garcia"]
|
10
|
+
spec.email = ["polypressure@outlook.com"]
|
11
|
+
|
12
|
+
spec.summary = "Minimalist form object implementation."
|
13
|
+
spec.homepage = "https://github.com/polypressure/formant"
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
17
|
+
spec.bindir = "exe"
|
18
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_runtime_dependency 'phony_rails', '~> 0.12.9'
|
22
|
+
|
23
|
+
spec.add_development_dependency "bundler", "~> 1.9"
|
24
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
25
|
+
spec.add_development_dependency "activemodel", "~>4.0"
|
26
|
+
spec.add_development_dependency "activesupport", "~>4.0"
|
27
|
+
spec.add_development_dependency "actionview", "~>4.0"
|
28
|
+
spec.add_development_dependency "minitest", "~> 5.5"
|
29
|
+
spec.add_development_dependency 'm', '~> 1.3', '>= 1.3.1'
|
30
|
+
spec.add_development_dependency 'minitest-reporters', '~> 1.0', '>= 1.0.19'
|
31
|
+
spec.add_development_dependency "codeclimate-test-reporter", '~> 0.4.7'
|
32
|
+
|
33
|
+
end
|
data/lib/formant.rb
ADDED
@@ -0,0 +1,186 @@
|
|
1
|
+
require "formant/version"
|
2
|
+
|
3
|
+
require 'active_model'
|
4
|
+
require 'active_support/all'
|
5
|
+
require 'action_view'
|
6
|
+
require 'phony_rails'
|
7
|
+
|
8
|
+
|
9
|
+
module Formant
|
10
|
+
include ActionView::Helpers::NumberHelper
|
11
|
+
|
12
|
+
#
|
13
|
+
# Base class for Form objects.
|
14
|
+
#
|
15
|
+
# See https://www.reinteractive.net/posts/158-form-objects-in-rails
|
16
|
+
# (among others) for more info on form objects for more details.
|
17
|
+
#
|
18
|
+
# This is a simplified implementation of form objects that focuses
|
19
|
+
# on collecting input in a PORO, specifying any special parsing
|
20
|
+
# and transformation on fields upon input (i.e., before validation),
|
21
|
+
# and special formatting/transformation on fields for display/output
|
22
|
+
# and redisplay of forms.
|
23
|
+
#
|
24
|
+
class FormObject
|
25
|
+
|
26
|
+
include ActiveModel::Model
|
27
|
+
include ActiveModel::Validations::Callbacks
|
28
|
+
|
29
|
+
before_validation :parse_fields!
|
30
|
+
|
31
|
+
class << self
|
32
|
+
attr_accessor :parse_fields, :format_fields
|
33
|
+
end
|
34
|
+
|
35
|
+
#
|
36
|
+
# Directive to add a parse rule for the specified attribute.
|
37
|
+
# Parse rules let you apply any special parsing/transformation
|
38
|
+
# logic on the form's attributes upon input. The parsing rules
|
39
|
+
# are applied automatically prior to validation.
|
40
|
+
#
|
41
|
+
# Usage example:
|
42
|
+
#
|
43
|
+
# class MyForm < FormObject
|
44
|
+
# ...
|
45
|
+
# parse :appointment_time, as: :datetime
|
46
|
+
# parse :phone, as: :phone_number
|
47
|
+
# parse :price, as: :currency
|
48
|
+
# parse :email, to: :strip_whitespace
|
49
|
+
# ...
|
50
|
+
# end
|
51
|
+
#
|
52
|
+
# The above example specifies that the appointment_time attribute
|
53
|
+
# should be parsed with the parse_datetime method (which converts
|
54
|
+
# the datetime string into an ActiveSupport::TimeWithZone object),
|
55
|
+
# and that the # phone attribute should be parsed with the
|
56
|
+
# parse_phone_number method (which normalizes the phone number into
|
57
|
+
# a standard format).
|
58
|
+
#
|
59
|
+
def self.parse(field_name, options={})
|
60
|
+
self.parse_fields ||= []
|
61
|
+
parse_type = options[:as] || options[:to]
|
62
|
+
raise "no parse type provided" if parse_type.blank?
|
63
|
+
self.parse_fields << [ field_name, "parse_#{parse_type}", options ]
|
64
|
+
end
|
65
|
+
|
66
|
+
#
|
67
|
+
# Directive to add a reformat rule for the specified attribute.
|
68
|
+
# These let you apply any special formatting/normalization
|
69
|
+
# logic on the form's attributes upon output. Typically you want
|
70
|
+
# to do this when you have to redisplay a form.
|
71
|
+
#
|
72
|
+
# The format rules are triggered when the FormObject#reformatted!
|
73
|
+
# method is invoked, which modifies the attribute in place.
|
74
|
+
#
|
75
|
+
# Usage example:
|
76
|
+
#
|
77
|
+
# class MyForm < FormObject
|
78
|
+
# ...
|
79
|
+
# reformat :appointment_time, as: :datetime, format: :day_date_time
|
80
|
+
# reformat :phone, as: :phone_number, country_code: 'US'
|
81
|
+
# ...
|
82
|
+
# end
|
83
|
+
#
|
84
|
+
# The above example specifies that the appointment_time attribute
|
85
|
+
# should be reformatted with the format_datetime method (using the format
|
86
|
+
# specified in the locale file with the :day_date_time key), and that
|
87
|
+
# the phone attribute should be parsed with the parse_phone_number
|
88
|
+
# method, and a fixed country_code of 'US'
|
89
|
+
#
|
90
|
+
def self.reformat(field_name, options={})
|
91
|
+
self.format_fields ||= []
|
92
|
+
self.format_fields << [ field_name, "format_#{options[:as]}", options ]
|
93
|
+
end
|
94
|
+
|
95
|
+
#
|
96
|
+
# Triger any formatting rules specified with the reformat directive.
|
97
|
+
# The attributes are reformatted and mutated in place.
|
98
|
+
#
|
99
|
+
# Returns an instance of the form object.
|
100
|
+
#
|
101
|
+
def reformatted!
|
102
|
+
self.class.format_fields.each do |field_name, format_method, options|
|
103
|
+
formatted_value = send(format_method, get_field(field_name), options)
|
104
|
+
set_field(field_name, formatted_value)
|
105
|
+
end
|
106
|
+
self
|
107
|
+
end
|
108
|
+
|
109
|
+
#
|
110
|
+
# Return all the attributes as a params hash.
|
111
|
+
#
|
112
|
+
def to_params
|
113
|
+
attrs = Hash.new
|
114
|
+
instance_variables.each do |ivar|
|
115
|
+
name = ivar[1..-1]
|
116
|
+
attrs[name.to_sym] = instance_variable_get(ivar) if respond_to? "#{name}="
|
117
|
+
end
|
118
|
+
attrs
|
119
|
+
end
|
120
|
+
|
121
|
+
|
122
|
+
private
|
123
|
+
|
124
|
+
def parse_datetime(field_value, options={})
|
125
|
+
Time.zone.parse(field_value || '')
|
126
|
+
end
|
127
|
+
|
128
|
+
def parse_phone_number(field_value, options={})
|
129
|
+
cc = options[:country_code] || 'US'
|
130
|
+
PhonyRails.normalize_number(field_value, country_code: cc)
|
131
|
+
end
|
132
|
+
|
133
|
+
def parse_currency(field_value, options={})
|
134
|
+
if field_value.is_a?(String)
|
135
|
+
field_value.gsub(/[^0-9\.]+/, '').to_d
|
136
|
+
elsif field_value.is_a?(Numeric)
|
137
|
+
field_value.to_d
|
138
|
+
else
|
139
|
+
field_value
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
def parse_strip_whitespace(field_value, options={})
|
144
|
+
return field_value unless field_value.is_a?(String)
|
145
|
+
options[:squish] ? field_value.squish : field_value.strip if field_value
|
146
|
+
end
|
147
|
+
|
148
|
+
def format_datetime(field_value, options={})
|
149
|
+
I18n.l(field_value, options).squish if field_value
|
150
|
+
end
|
151
|
+
|
152
|
+
def format_phone_number(field_value, options={})
|
153
|
+
field_value.phony_formatted(options) if field_value
|
154
|
+
end
|
155
|
+
|
156
|
+
def format_currency(field_value, options={})
|
157
|
+
ActionView::Base.new.number_to_currency(field_value, options) if field_value
|
158
|
+
end
|
159
|
+
|
160
|
+
def format_number_with_delimiter(field_value, options={})
|
161
|
+
ActionView::Base.new.number_with_delimiter(field_value, options)
|
162
|
+
end
|
163
|
+
|
164
|
+
def parse_fields!
|
165
|
+
self.class.parse_fields.each do |field_name, parse_method, options|
|
166
|
+
parsed_value = send(parse_method, get_field(field_name), options)
|
167
|
+
set_field(field_name, parsed_value)
|
168
|
+
end
|
169
|
+
true
|
170
|
+
end
|
171
|
+
|
172
|
+
def get_field(field_name)
|
173
|
+
instance_variable_get(as_sym(field_name))
|
174
|
+
end
|
175
|
+
|
176
|
+
def set_field(field_name, value)
|
177
|
+
instance_variable_set(as_sym(field_name), value)
|
178
|
+
end
|
179
|
+
|
180
|
+
def as_sym(field_name)
|
181
|
+
field = "@#{field_name}".to_sym
|
182
|
+
end
|
183
|
+
|
184
|
+
end
|
185
|
+
|
186
|
+
end
|
metadata
ADDED
@@ -0,0 +1,209 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: formant
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.2
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Anthony Garcia
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2015-08-18 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: phony_rails
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 0.12.9
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 0.12.9
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: bundler
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '1.9'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '1.9'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rake
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '10.0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '10.0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: activemodel
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '4.0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '4.0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: activesupport
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '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: '4.0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: actionview
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '4.0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '4.0'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: minitest
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - "~>"
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '5.5'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - "~>"
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '5.5'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: m
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - "~>"
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '1.3'
|
118
|
+
- - ">="
|
119
|
+
- !ruby/object:Gem::Version
|
120
|
+
version: 1.3.1
|
121
|
+
type: :development
|
122
|
+
prerelease: false
|
123
|
+
version_requirements: !ruby/object:Gem::Requirement
|
124
|
+
requirements:
|
125
|
+
- - "~>"
|
126
|
+
- !ruby/object:Gem::Version
|
127
|
+
version: '1.3'
|
128
|
+
- - ">="
|
129
|
+
- !ruby/object:Gem::Version
|
130
|
+
version: 1.3.1
|
131
|
+
- !ruby/object:Gem::Dependency
|
132
|
+
name: minitest-reporters
|
133
|
+
requirement: !ruby/object:Gem::Requirement
|
134
|
+
requirements:
|
135
|
+
- - "~>"
|
136
|
+
- !ruby/object:Gem::Version
|
137
|
+
version: '1.0'
|
138
|
+
- - ">="
|
139
|
+
- !ruby/object:Gem::Version
|
140
|
+
version: 1.0.19
|
141
|
+
type: :development
|
142
|
+
prerelease: false
|
143
|
+
version_requirements: !ruby/object:Gem::Requirement
|
144
|
+
requirements:
|
145
|
+
- - "~>"
|
146
|
+
- !ruby/object:Gem::Version
|
147
|
+
version: '1.0'
|
148
|
+
- - ">="
|
149
|
+
- !ruby/object:Gem::Version
|
150
|
+
version: 1.0.19
|
151
|
+
- !ruby/object:Gem::Dependency
|
152
|
+
name: codeclimate-test-reporter
|
153
|
+
requirement: !ruby/object:Gem::Requirement
|
154
|
+
requirements:
|
155
|
+
- - "~>"
|
156
|
+
- !ruby/object:Gem::Version
|
157
|
+
version: 0.4.7
|
158
|
+
type: :development
|
159
|
+
prerelease: false
|
160
|
+
version_requirements: !ruby/object:Gem::Requirement
|
161
|
+
requirements:
|
162
|
+
- - "~>"
|
163
|
+
- !ruby/object:Gem::Version
|
164
|
+
version: 0.4.7
|
165
|
+
description:
|
166
|
+
email:
|
167
|
+
- polypressure@outlook.com
|
168
|
+
executables: []
|
169
|
+
extensions: []
|
170
|
+
extra_rdoc_files: []
|
171
|
+
files:
|
172
|
+
- ".gitignore"
|
173
|
+
- ".travis.yml"
|
174
|
+
- CHANGELOG.md
|
175
|
+
- CODE_OF_CONDUCT.md
|
176
|
+
- Gemfile
|
177
|
+
- LICENSE.txt
|
178
|
+
- README.md
|
179
|
+
- Rakefile
|
180
|
+
- bin/console
|
181
|
+
- bin/setup
|
182
|
+
- formant.gemspec
|
183
|
+
- lib/formant.rb
|
184
|
+
- lib/formant/version.rb
|
185
|
+
homepage: https://github.com/polypressure/formant
|
186
|
+
licenses:
|
187
|
+
- MIT
|
188
|
+
metadata: {}
|
189
|
+
post_install_message:
|
190
|
+
rdoc_options: []
|
191
|
+
require_paths:
|
192
|
+
- lib
|
193
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
194
|
+
requirements:
|
195
|
+
- - ">="
|
196
|
+
- !ruby/object:Gem::Version
|
197
|
+
version: '0'
|
198
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
199
|
+
requirements:
|
200
|
+
- - ">="
|
201
|
+
- !ruby/object:Gem::Version
|
202
|
+
version: '0'
|
203
|
+
requirements: []
|
204
|
+
rubyforge_project:
|
205
|
+
rubygems_version: 2.4.8
|
206
|
+
signing_key:
|
207
|
+
specification_version: 4
|
208
|
+
summary: Minimalist form object implementation.
|
209
|
+
test_files: []
|