reform 0.1.1 → 0.1.2
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +1 -0
- data/.travis.yml +3 -2
- data/CHANGES.md +5 -0
- data/Gemfile +3 -0
- data/README.md +37 -1
- data/TODO.md +3 -1
- data/gemfiles/Gemfile.rails-3.0 +7 -0
- data/lib/reform/form.rb +22 -11
- data/lib/reform/form/active_model.rb +10 -7
- data/lib/reform/form/coercion.rb +18 -0
- data/lib/reform/version.rb +1 -1
- data/reform.gemspec +2 -1
- data/test/active_model_test.rb +20 -2
- data/test/dsl_test.rb +25 -0
- data/test/reform_test.rb +10 -5
- data/test/test_helper.rb +2 -4
- metadata +22 -4
data/.gitignore
CHANGED
data/.travis.yml
CHANGED
data/CHANGES.md
CHANGED
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -100,6 +100,26 @@ To push the incoming data to the models directly, call `#save` without the block
|
|
100
100
|
# by calling @form.song.name= and @form.artist.title=.
|
101
101
|
```
|
102
102
|
|
103
|
+
## Coercion
|
104
|
+
|
105
|
+
Often you want incoming form data to be converted to a type, like timestamps. Reform uses [virtus](https://github.com/solnic/virtus) for coercion, the DSL is seamlessly integrated into Reform with the `:type` option.
|
106
|
+
|
107
|
+
Be sure to add `virtus` to your Gemfile.
|
108
|
+
|
109
|
+
```ruby
|
110
|
+
require 'reform/form/coercion'
|
111
|
+
|
112
|
+
class SongRequestForm < Reform::Form
|
113
|
+
include DSL
|
114
|
+
include Reform::Form::Coercion
|
115
|
+
|
116
|
+
property :written_at, on: :song, type: DateTime
|
117
|
+
end
|
118
|
+
|
119
|
+
@form.save do |data, nested|
|
120
|
+
data.written_at #=> <DateTime XXX>
|
121
|
+
```
|
122
|
+
|
103
123
|
## Rails Integration
|
104
124
|
|
105
125
|
[A sample Rails app using Reform.](https://github.com/gogogarrett/reform_example)
|
@@ -121,7 +141,7 @@ class UserProfileForm < Reform::Form
|
|
121
141
|
property :email, on: :user
|
122
142
|
properties [:gender, :age], on: :profile
|
123
143
|
|
124
|
-
model :user
|
144
|
+
model :user
|
125
145
|
|
126
146
|
validates :email, :gender, presence: true
|
127
147
|
validates :age, numericality: true
|
@@ -129,6 +149,12 @@ class UserProfileForm < Reform::Form
|
|
129
149
|
end
|
130
150
|
```
|
131
151
|
|
152
|
+
Basically, `model :user` tells Reform to use the `:user` object in the composition as the form main object while using `"user"` as the form name (needed for URL computation). If you want to change the form name let Reform know.
|
153
|
+
|
154
|
+
```ruby
|
155
|
+
model :singer, :on => :user # form name is "singer" whereas main object is `:user` in composition.
|
156
|
+
```
|
157
|
+
|
132
158
|
|
133
159
|
#### View Form
|
134
160
|
|
@@ -190,3 +216,13 @@ end
|
|
190
216
|
## Security
|
191
217
|
|
192
218
|
By explicitely defining the form layout using `::property` there is no more need for protecting from unwanted input. `strong_parameter` or `attr_accessible` become obsolete. Reform will simply ignore undefined incoming parameters.
|
219
|
+
|
220
|
+
## Maintainers
|
221
|
+
|
222
|
+
[Nick Sutterer](https://github.com/apotonick)
|
223
|
+
|
224
|
+
[Garrett Heinlen](https://github.com/gogogarrett)
|
225
|
+
|
226
|
+
### Attributions!!!
|
227
|
+
|
228
|
+
Great thanks to [Blake Education](https://github.com/blake-education) for giving us the freedom and time to develop this project while working on their project.
|
data/TODO.md
CHANGED
@@ -1,2 +1,4 @@
|
|
1
1
|
* `validates :title, :presence => true`
|
2
|
-
with @model.title == "Little Green Car" and validate({}) the form is still valid (as we "have" a valid title). is that what we want?
|
2
|
+
with @model.title == "Little Green Car" and validate({}) the form is still valid (as we "have" a valid title). is that what we want?
|
3
|
+
|
4
|
+
* document Form#to_hash and Form#to_nested_hash (e.g. with OpenStruct composition to make it a very simple form)
|
data/lib/reform/form.rb
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
require 'delegate'
|
2
|
+
require 'ostruct'
|
2
3
|
|
3
4
|
module Reform
|
4
5
|
class Form < SimpleDelegator
|
@@ -8,12 +9,11 @@ module Reform
|
|
8
9
|
# validation: this object also contains the validation rules itself, should be separated.
|
9
10
|
# TODO: figure out #to_key issues.
|
10
11
|
|
11
|
-
def initialize(
|
12
|
-
@mapper =
|
12
|
+
def initialize(mapper_class, composition)
|
13
|
+
@mapper = mapper_class
|
13
14
|
@model = composition
|
14
|
-
representer = @mapper.new(composition)
|
15
15
|
|
16
|
-
super
|
16
|
+
super(setup_fields(mapper_class, composition)) # delegate all methods to Fields instance.
|
17
17
|
end
|
18
18
|
|
19
19
|
def validate(params)
|
@@ -30,13 +30,6 @@ module Reform
|
|
30
30
|
@mapper.new(model).from_hash(to_hash) # DISCUSS: move to Composition?
|
31
31
|
end
|
32
32
|
|
33
|
-
private
|
34
|
-
attr_accessor :mapper, :model
|
35
|
-
|
36
|
-
def update_with(params)
|
37
|
-
mapper.new(self).from_hash(params) # sets form properties found in params on self.
|
38
|
-
end
|
39
|
-
|
40
33
|
# Use representer to return current key-value form hash.
|
41
34
|
def to_hash
|
42
35
|
mapper.new(self).to_hash
|
@@ -46,6 +39,24 @@ module Reform
|
|
46
39
|
model.nested_hash_for(to_hash) # use composition to compute nested hash.
|
47
40
|
end
|
48
41
|
|
42
|
+
private
|
43
|
+
attr_accessor :mapper, :model
|
44
|
+
|
45
|
+
def setup_fields(mapper_class, composition)
|
46
|
+
# decorate composition and transform to hash.
|
47
|
+
representer = mapper_class.new(composition)
|
48
|
+
|
49
|
+
create_fields(representer.fields, representer.to_hash)
|
50
|
+
end
|
51
|
+
|
52
|
+
def create_fields(field_names, fields)
|
53
|
+
Fields.new(field_names, fields)
|
54
|
+
end
|
55
|
+
|
56
|
+
def update_with(params)
|
57
|
+
mapper.new(self).from_hash(params) # sets form properties found in params on self.
|
58
|
+
end
|
59
|
+
|
49
60
|
# FIXME: make AM optional.
|
50
61
|
require 'active_model'
|
51
62
|
include ActiveModel::Validations
|
@@ -6,14 +6,14 @@ module Reform::Form::ActiveModel
|
|
6
6
|
end
|
7
7
|
|
8
8
|
module ClassMethods
|
9
|
-
def model(
|
10
|
-
@model_options
|
11
|
-
|
9
|
+
def model(main_model, options={})
|
10
|
+
@model_options = [main_model, options] # FIXME: make inheritable!
|
11
|
+
composition_model = options[:on] || main_model
|
12
12
|
|
13
|
-
delegate
|
14
|
-
delegate :persisted?, :to_key, :to_param, :to =>
|
13
|
+
delegate composition_model, :to => :model # #song => model.song
|
14
|
+
delegate :persisted?, :to_key, :to_param, :to_model, :to => composition_model # #to_key => song.to_key
|
15
15
|
|
16
|
-
alias_method
|
16
|
+
alias_method main_model, composition_model # #hit => model.song.
|
17
17
|
end
|
18
18
|
|
19
19
|
def property(name, options={})
|
@@ -22,7 +22,10 @@ module Reform::Form::ActiveModel
|
|
22
22
|
end
|
23
23
|
|
24
24
|
def model_name
|
25
|
-
|
25
|
+
name = @model_options.first.to_s.camelize
|
26
|
+
|
27
|
+
return ::ActiveModel::Name.new(OpenStruct.new(:name => name)) if ::ActiveModel::VERSION::MAJOR == 3 and ::ActiveModel::VERSION::MINOR == 0
|
28
|
+
::ActiveModel::Name.new(self, nil, name)
|
26
29
|
end
|
27
30
|
end
|
28
31
|
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'representable/decorator/coercion'
|
2
|
+
|
3
|
+
class Reform::Form
|
4
|
+
module Coercion
|
5
|
+
def self.included(base)
|
6
|
+
base.extend(ClassMethods)
|
7
|
+
end
|
8
|
+
|
9
|
+
module ClassMethods
|
10
|
+
def representer_class
|
11
|
+
super.class_eval do
|
12
|
+
include Representable::Decorator::Coercion unless self < Representable::Decorator::Coercion # DISCUSS: include it once. why do we have to check this?
|
13
|
+
self
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
data/lib/reform/version.rb
CHANGED
data/reform.gemspec
CHANGED
@@ -18,11 +18,12 @@ Gem::Specification.new do |spec|
|
|
18
18
|
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
19
|
spec.require_paths = ["lib"]
|
20
20
|
|
21
|
-
spec.add_dependency "representable"
|
21
|
+
spec.add_dependency "representable", ">= 1.5.3"
|
22
22
|
spec.add_dependency "activemodel"
|
23
23
|
spec.add_development_dependency "bundler", "~> 1.3"
|
24
24
|
spec.add_development_dependency "rake"
|
25
25
|
spec.add_development_dependency "minitest"
|
26
26
|
spec.add_development_dependency "activerecord"
|
27
27
|
spec.add_development_dependency "sqlite3"
|
28
|
+
spec.add_development_dependency "virtus"
|
28
29
|
end
|
data/test/active_model_test.rb
CHANGED
@@ -15,10 +15,24 @@ class ActiveModelTest < MiniTest::Spec
|
|
15
15
|
let (:duran) { OpenStruct.new }
|
16
16
|
let (:form) { HitForm.new(:song => rio, :artist => duran) }
|
17
17
|
|
18
|
-
|
19
|
-
|
18
|
+
describe "main form reader #hit" do
|
19
|
+
it "delegates to :on model" do
|
20
|
+
form.hit.must_equal rio
|
21
|
+
end
|
22
|
+
|
23
|
+
it "doesn't delegate when :on missing" do
|
24
|
+
class HitForm < Reform::Form
|
25
|
+
include DSL
|
26
|
+
include Reform::Form::ActiveModel
|
27
|
+
|
28
|
+
property :title, :on => :song
|
29
|
+
|
30
|
+
model :song
|
31
|
+
end.new(:song => rio, :artist => duran).song.must_equal rio
|
32
|
+
end
|
20
33
|
end
|
21
34
|
|
35
|
+
|
22
36
|
it "creates composition readers" do
|
23
37
|
form.song.must_equal rio
|
24
38
|
form.artist.must_equal duran
|
@@ -40,6 +54,10 @@ class ActiveModelTest < MiniTest::Spec
|
|
40
54
|
HitForm.new(:song => OpenStruct.new.instance_eval { def to_param; "yo!"; end; self }, :artist => OpenStruct.new).to_param.must_equal "yo!"
|
41
55
|
end
|
42
56
|
|
57
|
+
it "provides #to_model" do
|
58
|
+
HitForm.new(:song => OpenStruct.new.instance_eval { def to_model; "yo!"; end; self }, :artist => OpenStruct.new).to_model.must_equal "yo!"
|
59
|
+
end
|
60
|
+
|
43
61
|
it "works with any order of ::model and ::property" do
|
44
62
|
class AnotherForm < Reform::Form
|
45
63
|
include DSL
|
data/test/dsl_test.rb
CHANGED
@@ -15,4 +15,29 @@ class DslTest < MiniTest::Spec
|
|
15
15
|
it "works by creating Representer and Composition for you" do
|
16
16
|
form.validate("title" => "Greyhound", "name" => "Frenzal Rhomb").must_equal false
|
17
17
|
end
|
18
|
+
|
19
|
+
require 'reform/form/coercion'
|
20
|
+
it "allows coercion" do
|
21
|
+
form = Class.new(Reform::Form) do
|
22
|
+
include Reform::Form::DSL
|
23
|
+
include Reform::Form::Coercion
|
24
|
+
|
25
|
+
property :written_at, :type => DateTime, :on => :song
|
26
|
+
end.new(:song => OpenStruct.new(:written_at => "31/03/1981"))
|
27
|
+
|
28
|
+
form.written_at.must_be_kind_of DateTime
|
29
|
+
form.written_at.must_equal DateTime.parse("Tue, 31 Mar 1981 00:00:00 +0000")
|
30
|
+
end
|
31
|
+
|
32
|
+
it "allows coercion in validate" do
|
33
|
+
form = Class.new(Reform::Form) do
|
34
|
+
include Reform::Form::DSL
|
35
|
+
include Reform::Form::Coercion
|
36
|
+
|
37
|
+
property :id, :type => Integer, :on => :song
|
38
|
+
end.new(:song => OpenStruct.new())
|
39
|
+
|
40
|
+
form.validate("id" => "1")
|
41
|
+
form.to_hash.must_equal("id" => 1)
|
42
|
+
end
|
18
43
|
end
|
data/test/reform_test.rb
CHANGED
@@ -45,6 +45,7 @@ class FieldsTest < MiniTest::Spec
|
|
45
45
|
end
|
46
46
|
|
47
47
|
it "processes value syms" do
|
48
|
+
skip "we don't need to test this as representer.to_hash always returns strings"
|
48
49
|
fields = Reform::Fields.new(["name", "title"], :title => "The Body")
|
49
50
|
fields.name.must_equal nil
|
50
51
|
fields.title.must_equal "The Body"
|
@@ -53,6 +54,12 @@ class FieldsTest < MiniTest::Spec
|
|
53
54
|
end
|
54
55
|
|
55
56
|
class ReformTest < MiniTest::Spec
|
57
|
+
def errors_for(form)
|
58
|
+
errors = form.errors
|
59
|
+
errors = errors.messages unless ::ActiveModel::VERSION::MAJOR == 3 and ::ActiveModel::VERSION::MINOR == 0
|
60
|
+
errors
|
61
|
+
end
|
62
|
+
|
56
63
|
let (:duran) { OpenStruct.new(:name => "Duran Duran") }
|
57
64
|
let (:rio) { OpenStruct.new(:title => "Rio") }
|
58
65
|
|
@@ -174,7 +181,7 @@ class ReformTest < MiniTest::Spec
|
|
174
181
|
|
175
182
|
it "populates errors" do
|
176
183
|
form.validate({})
|
177
|
-
form.
|
184
|
+
errors_for(form).must_equal({:name=>["can't be blank"], :title=>["can't be blank"]})
|
178
185
|
end
|
179
186
|
end
|
180
187
|
|
@@ -189,7 +196,7 @@ class ReformTest < MiniTest::Spec
|
|
189
196
|
end.new(SongAndArtistMap, comp)
|
190
197
|
|
191
198
|
form.validate({}).must_equal false
|
192
|
-
form.
|
199
|
+
errors_for(form).must_equal({:name=>["Please give me a name"]})
|
193
200
|
end
|
194
201
|
end
|
195
202
|
|
@@ -212,7 +219,7 @@ class ReformTest < MiniTest::Spec
|
|
212
219
|
it "is invalid and shows error when taken" do
|
213
220
|
form = ActiveRecordForm.new(SongAndArtistMap, comp)
|
214
221
|
form.validate({"name" => "Racer X"}).must_equal false
|
215
|
-
form.
|
222
|
+
errors_for(form).must_equal({:name=>["has already been taken"], :title => ["can't be blank"]})
|
216
223
|
end
|
217
224
|
|
218
225
|
require 'reform/rails'
|
@@ -264,5 +271,3 @@ class ReformTest < MiniTest::Spec
|
|
264
271
|
end
|
265
272
|
end
|
266
273
|
end
|
267
|
-
|
268
|
-
# TODO: test errors
|
data/test/test_helper.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: reform
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.2
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -10,7 +10,7 @@ authors:
|
|
10
10
|
autorequire:
|
11
11
|
bindir: bin
|
12
12
|
cert_chain: []
|
13
|
-
date: 2013-
|
13
|
+
date: 2013-06-14 00:00:00.000000000 Z
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|
16
16
|
name: representable
|
@@ -19,7 +19,7 @@ dependencies:
|
|
19
19
|
requirements:
|
20
20
|
- - ! '>='
|
21
21
|
- !ruby/object:Gem::Version
|
22
|
-
version:
|
22
|
+
version: 1.5.3
|
23
23
|
type: :runtime
|
24
24
|
prerelease: false
|
25
25
|
version_requirements: !ruby/object:Gem::Requirement
|
@@ -27,7 +27,7 @@ dependencies:
|
|
27
27
|
requirements:
|
28
28
|
- - ! '>='
|
29
29
|
- !ruby/object:Gem::Version
|
30
|
-
version:
|
30
|
+
version: 1.5.3
|
31
31
|
- !ruby/object:Gem::Dependency
|
32
32
|
name: activemodel
|
33
33
|
requirement: !ruby/object:Gem::Requirement
|
@@ -124,6 +124,22 @@ dependencies:
|
|
124
124
|
- - ! '>='
|
125
125
|
- !ruby/object:Gem::Version
|
126
126
|
version: '0'
|
127
|
+
- !ruby/object:Gem::Dependency
|
128
|
+
name: virtus
|
129
|
+
requirement: !ruby/object:Gem::Requirement
|
130
|
+
none: false
|
131
|
+
requirements:
|
132
|
+
- - ! '>='
|
133
|
+
- !ruby/object:Gem::Version
|
134
|
+
version: '0'
|
135
|
+
type: :development
|
136
|
+
prerelease: false
|
137
|
+
version_requirements: !ruby/object:Gem::Requirement
|
138
|
+
none: false
|
139
|
+
requirements:
|
140
|
+
- - ! '>='
|
141
|
+
- !ruby/object:Gem::Version
|
142
|
+
version: '0'
|
127
143
|
description: Freeing your AR models from form logic.
|
128
144
|
email:
|
129
145
|
- apotonick@gmail.com
|
@@ -141,10 +157,12 @@ files:
|
|
141
157
|
- Rakefile
|
142
158
|
- TODO.md
|
143
159
|
- database.sqlite3
|
160
|
+
- gemfiles/Gemfile.rails-3.0
|
144
161
|
- lib/reform.rb
|
145
162
|
- lib/reform/form.rb
|
146
163
|
- lib/reform/form/active_model.rb
|
147
164
|
- lib/reform/form/active_record.rb
|
165
|
+
- lib/reform/form/coercion.rb
|
148
166
|
- lib/reform/form/dsl.rb
|
149
167
|
- lib/reform/rails.rb
|
150
168
|
- lib/reform/version.rb
|