reform 0.0.1 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGES.md +3 -0
- data/README.md +156 -15
- data/Rakefile +8 -0
- data/lib/reform/form/active_model.rb +28 -0
- data/lib/reform/form/dsl.rb +38 -0
- data/lib/reform/form.rb +132 -0
- data/lib/reform/version.rb +1 -1
- data/lib/reform.rb +4 -0
- data/reform.gemspec +6 -3
- data/test/dsl_test.rb +72 -0
- data/test/reform_test.rb +203 -0
- data/test/test_helper.rb +4 -0
- metadata +63 -3
data/CHANGES.md
ADDED
data/README.md
CHANGED
@@ -1,29 +1,170 @@
|
|
1
1
|
# Reform
|
2
2
|
|
3
|
-
|
3
|
+
Decouple your models from forms. Reform gives you a form object with validations and nested setup of models. It is completely framework-agnostic and doesn't care about your database.
|
4
4
|
|
5
5
|
## Installation
|
6
6
|
|
7
|
-
Add this line to your
|
7
|
+
Add this line to your Gemfile:
|
8
8
|
|
9
|
-
|
9
|
+
```ruby
|
10
|
+
gem 'reform'
|
11
|
+
```
|
10
12
|
|
11
|
-
|
13
|
+
## Defining Forms
|
12
14
|
|
13
|
-
|
15
|
+
Say you need a form for song requests on a radio station. Internally, this would imply associating `songs` table and the `artists` table. You don't wanna reflect that in your web form, do you?
|
14
16
|
|
15
|
-
|
17
|
+
```ruby
|
18
|
+
class SongRequestForm < Reform::Form
|
19
|
+
include DSL
|
16
20
|
|
17
|
-
|
21
|
+
property :title, on: :song
|
22
|
+
property :name, on: :artist
|
18
23
|
|
19
|
-
|
24
|
+
validates :name, :title, presence: true
|
25
|
+
end
|
26
|
+
```
|
20
27
|
|
21
|
-
|
28
|
+
The `::property` method allows defining the fields of the form. Using `:on` delegates this field to a nested object in your form.
|
22
29
|
|
23
|
-
|
30
|
+
__Note__: There is a convenience method `::properties` that allows you to pass an array of fields at one time.
|
24
31
|
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
32
|
+
## Using Forms
|
33
|
+
|
34
|
+
In your controller you'd create a form instance and pass in the models you wanna work on.
|
35
|
+
|
36
|
+
```ruby
|
37
|
+
def new
|
38
|
+
@form = SongRequestForm.new(song: Song.new, artist: Artist.new)
|
39
|
+
end
|
40
|
+
```
|
41
|
+
|
42
|
+
You can also setup the form for editing existing items.
|
43
|
+
|
44
|
+
```ruby
|
45
|
+
def edit
|
46
|
+
@form = SongRequestForm.new(song: Song.find(1), artist: Artist.find(2))
|
47
|
+
end
|
48
|
+
```
|
49
|
+
|
50
|
+
## Rendering Forms
|
51
|
+
|
52
|
+
Your `@form` is now ready to be rendered, either do it yourself or use something like `simple_form`.
|
53
|
+
|
54
|
+
```haml
|
55
|
+
= simple_form_for @form do |f|
|
56
|
+
|
57
|
+
= f.input :name
|
58
|
+
= f.input :title
|
59
|
+
```
|
60
|
+
|
61
|
+
## Validating Forms
|
62
|
+
|
63
|
+
After a form submission, you wanna validate the input.
|
64
|
+
|
65
|
+
```ruby
|
66
|
+
def create
|
67
|
+
@form = SongRequestForm.new(song: Song.new, artist: Artist.new)
|
68
|
+
|
69
|
+
#=> params: {song_request: {title: "Rio", name: "Duran Duran"}}
|
70
|
+
|
71
|
+
if @form.validate(params[:song_request])
|
72
|
+
```
|
73
|
+
|
74
|
+
`Reform` uses the validations you provided in the form - and nothing else.
|
75
|
+
|
76
|
+
|
77
|
+
## Saving Forms
|
78
|
+
|
79
|
+
We provide a bullet-proof way to save your form data: by letting _you_ do it!
|
80
|
+
|
81
|
+
```ruby
|
82
|
+
if @form.validate(params[:song_request])
|
83
|
+
|
84
|
+
@form.save do |data, nested|
|
85
|
+
#=> data: <title: "Rio", name: "Duran Duran">
|
86
|
+
#
|
87
|
+
# nested: {song: {title: "Rio"},
|
88
|
+
# artist: {name: "Duran Duran"}}
|
89
|
+
|
90
|
+
SongRequest.new(nested[:song][:title])
|
91
|
+
end
|
92
|
+
```
|
93
|
+
|
94
|
+
While `data` gives you an object exposing the form property readers, `nested` already reflects the nesting you defined in your form earlier.
|
95
|
+
|
96
|
+
To push the incoming data to the models directly, call `#save` without the block.
|
97
|
+
|
98
|
+
```ruby
|
99
|
+
@form.save #=> populates song and artist with incoming data
|
100
|
+
# by calling @form.song.name= and @form.artist.title=.
|
101
|
+
```
|
102
|
+
|
103
|
+
## ActiveModel - Rails Integration
|
104
|
+
|
105
|
+
Reform offers ActiveModel support to easily make this accessible in Rails based projects. You simply `include Reform::Form::ActiveModel` in your form object and the Rails specific code will be handled for you.
|
106
|
+
|
107
|
+
### Simple Integration
|
108
|
+
#### Form Class
|
109
|
+
|
110
|
+
You have to include a call to `model` to specifiy which is the main object of the form.
|
111
|
+
|
112
|
+
```ruby
|
113
|
+
class UserProfileForm < Reform::Form
|
114
|
+
include DSL
|
115
|
+
include Reform::Form::ActiveModel
|
116
|
+
|
117
|
+
property :email, on: :user
|
118
|
+
properties [:gender, :age], on: :profile
|
119
|
+
|
120
|
+
model :user, on: :user
|
121
|
+
|
122
|
+
validates :email, :gender, presence: true
|
123
|
+
validates :age, numericality: true
|
124
|
+
end
|
125
|
+
```
|
126
|
+
|
127
|
+
#### View Form
|
128
|
+
|
129
|
+
The form becomes __very__ dumb as it knows nothing about the backend assocations or data binding to the database layer. This simply takes input and passes it along to the controller as it should.
|
130
|
+
|
131
|
+
```erb
|
132
|
+
<%= form_for @form do |f| %>
|
133
|
+
<%= f.email_field :email %>
|
134
|
+
<%= f.input :gender %>
|
135
|
+
<%= f.number_field :age %>
|
136
|
+
<%= f.submit %>
|
137
|
+
<% end %>
|
138
|
+
```
|
139
|
+
|
140
|
+
#### Controller
|
141
|
+
|
142
|
+
In the controller you can easily create helpers to build these form objects for you. In the create and update actions Reform allows you total control of what to do with the data being passed via the form. How you interact with the data is entirely up to you.
|
143
|
+
|
144
|
+
```ruby
|
145
|
+
class UsersController < ApplicationController
|
146
|
+
|
147
|
+
def create
|
148
|
+
@form = create_new_form
|
149
|
+
if @form.validate(params[:user])
|
150
|
+
@form.save do |data, map|
|
151
|
+
new_user = User.new(map[:user])
|
152
|
+
new_user.build_user_profile(map[:profile])
|
153
|
+
new_user.save!
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
|
159
|
+
private
|
160
|
+
def create_new_form
|
161
|
+
UserProfileForm.new(user: User.new, profile: UserProfile.new)
|
162
|
+
end
|
163
|
+
end
|
164
|
+
```
|
165
|
+
|
166
|
+
__Note__: this can also be used for the update action as well.
|
167
|
+
|
168
|
+
## Security
|
169
|
+
|
170
|
+
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.
|
data/Rakefile
CHANGED
@@ -0,0 +1,28 @@
|
|
1
|
+
module Reform::Form::ActiveModel
|
2
|
+
def self.included(base)
|
3
|
+
base.class_eval do
|
4
|
+
extend ClassMethods
|
5
|
+
end
|
6
|
+
end
|
7
|
+
|
8
|
+
module ClassMethods
|
9
|
+
def model(*args)
|
10
|
+
@model_options = args # FIXME: make inheritable!
|
11
|
+
main_model = args.last[:on]
|
12
|
+
|
13
|
+
delegate main_model, :to => :model # #song => model.song
|
14
|
+
delegate "persisted?", :to_key, :to_param, :to => main_model # #to_key => song.to_key
|
15
|
+
|
16
|
+
alias_method args.first, main_model # #hit => model.song.
|
17
|
+
end
|
18
|
+
|
19
|
+
def property(name, options={})
|
20
|
+
delegate options[:on], :to => :model
|
21
|
+
super
|
22
|
+
end
|
23
|
+
|
24
|
+
def model_name
|
25
|
+
ActiveModel::Name.new(self, nil, @model_options.first.to_s.camelize)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
class Reform::Form
|
2
|
+
module DSL
|
3
|
+
def self.included(base)
|
4
|
+
base.class_eval do
|
5
|
+
extend ClassMethods
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
module ClassMethods
|
10
|
+
def property(name, *args)
|
11
|
+
representer_class.property(name, *args)
|
12
|
+
end
|
13
|
+
|
14
|
+
def properties(names, *args)
|
15
|
+
names.each do |name|
|
16
|
+
property(name, *args)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
#private
|
21
|
+
def representer_class
|
22
|
+
@representer_class ||= Class.new(Reform::Representer)
|
23
|
+
end
|
24
|
+
|
25
|
+
def model_class
|
26
|
+
rpr = representer_class
|
27
|
+
@model_class ||= Class.new(Reform::Composition) do
|
28
|
+
map_from rpr
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def initialize(models)
|
34
|
+
composition = self.class.model_class.new(models)
|
35
|
+
super(self.class.representer_class, composition)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
data/lib/reform/form.rb
ADDED
@@ -0,0 +1,132 @@
|
|
1
|
+
require 'delegate'
|
2
|
+
|
3
|
+
module Reform
|
4
|
+
class Form < SimpleDelegator
|
5
|
+
# reasons for delegation:
|
6
|
+
# presentation: this object is used in the presentation layer by #form_for.
|
7
|
+
# problem: #form_for uses respond_to?(:email_before_type_cast) which goes to an internal hash in the actual record.
|
8
|
+
# validation: this object also contains the validation rules itself, should be separated.
|
9
|
+
# TODO: figure out #to_key issues.
|
10
|
+
|
11
|
+
def initialize(mapper, composition)
|
12
|
+
@mapper = mapper
|
13
|
+
@model = composition
|
14
|
+
representer = @mapper.new(composition)
|
15
|
+
|
16
|
+
super Fields.new(representer.fields, representer.to_hash) # decorate composition and transform to hash.
|
17
|
+
end
|
18
|
+
|
19
|
+
def validate(params)
|
20
|
+
# here it would be cool to have a validator object containing the validation rules representer-like and then pass it the formed model.
|
21
|
+
update_with(params)
|
22
|
+
|
23
|
+
valid? # this validates on <Fields> using AM::Validations, currently.
|
24
|
+
end
|
25
|
+
|
26
|
+
def save
|
27
|
+
# DISCUSS: we should never hit @mapper here (which writes to the models) when a block is passed.
|
28
|
+
return yield self, to_nested_hash if block_given?
|
29
|
+
|
30
|
+
@mapper.new(model).from_hash(to_hash) # DISCUSS: move to Composition?
|
31
|
+
end
|
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
|
+
# Use representer to return current key-value form hash.
|
41
|
+
def to_hash
|
42
|
+
mapper.new(self).to_hash
|
43
|
+
end
|
44
|
+
|
45
|
+
def to_nested_hash
|
46
|
+
model.nested_hash_for(to_hash) # use composition to compute nested hash.
|
47
|
+
end
|
48
|
+
|
49
|
+
# FIXME: make AM optional.
|
50
|
+
require 'active_model'
|
51
|
+
include ActiveModel::Validations
|
52
|
+
end
|
53
|
+
|
54
|
+
# Keeps values of the form fields. What's in here is to be displayed in the browser!
|
55
|
+
# we need this intermediate object to display both "original values" and new input from the form after submitting.
|
56
|
+
class Fields < OpenStruct
|
57
|
+
def initialize(properties, values={})
|
58
|
+
fields = properties.inject({}) { |hsh, attr| hsh.merge!(attr => nil) }
|
59
|
+
super(fields.merge!(values)) # TODO: stringify value keys!
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
# Keeps composition of models and knows how to transform a plain hash into a nested hash.
|
64
|
+
class Composition
|
65
|
+
class << self
|
66
|
+
def map(options)
|
67
|
+
@attr2obj = {} # {song: ["title", "track"], artist: ["name"]}
|
68
|
+
|
69
|
+
options.each do |mdl, meths|
|
70
|
+
create_accessors(mdl, meths)
|
71
|
+
attr_reader mdl # FIXME: unless already defined!!
|
72
|
+
|
73
|
+
meths.each { |m| @attr2obj[m.to_s] = mdl }
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
# Specific to representable.
|
78
|
+
def map_from(representer)
|
79
|
+
options = {}
|
80
|
+
representer.representable_attrs.each do |cfg|
|
81
|
+
options[cfg.options[:on]] ||= []
|
82
|
+
options[cfg.options[:on]] << cfg.name
|
83
|
+
end
|
84
|
+
|
85
|
+
map options
|
86
|
+
end
|
87
|
+
|
88
|
+
def model_for_property(name)
|
89
|
+
@attr2obj.fetch(name.to_s)
|
90
|
+
end
|
91
|
+
|
92
|
+
private
|
93
|
+
def create_accessors(model, methods)
|
94
|
+
accessors = methods.collect { |m| [m, "#{m}="] }.flatten
|
95
|
+
delegate *accessors, to: "@#{model}"
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
# TODO: make class method?
|
100
|
+
def nested_hash_for(attrs)
|
101
|
+
{}.tap do |hsh|
|
102
|
+
attrs.each do |name, val|
|
103
|
+
obj = self.class.model_for_property(name)
|
104
|
+
hsh[obj] ||= {}
|
105
|
+
hsh[obj][name.to_sym] = val
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
def initialize(models)
|
111
|
+
models.each do |name, obj|
|
112
|
+
instance_variable_set(:"@#{name}", obj)
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
require 'representable/hash'
|
118
|
+
class Representer < Representable::Decorator
|
119
|
+
include Representable::Hash
|
120
|
+
|
121
|
+
def self.properties(names, *args)
|
122
|
+
names.each do |name|
|
123
|
+
property(name, *args)
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
# Returns hash of all property names.
|
128
|
+
def fields
|
129
|
+
representable_attrs.collect { |cfg| cfg.name }
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
data/lib/reform/version.rb
CHANGED
data/lib/reform.rb
CHANGED
data/reform.gemspec
CHANGED
@@ -6,8 +6,8 @@ require 'reform/version'
|
|
6
6
|
Gem::Specification.new do |spec|
|
7
7
|
spec.name = "reform"
|
8
8
|
spec.version = Reform::VERSION
|
9
|
-
spec.authors = ["Nick Sutterer"]
|
10
|
-
spec.email = ["apotonick@gmail.com"]
|
9
|
+
spec.authors = ["Nick Sutterer", "Garrett Heinlen"]
|
10
|
+
spec.email = ["apotonick@gmail.com", "heinleng@gmail.com"]
|
11
11
|
spec.description = %q{Freeing your AR models from form logic.}
|
12
12
|
spec.summary = %q{Freeing your AR models from form logic.}
|
13
13
|
spec.homepage = ""
|
@@ -18,6 +18,9 @@ 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.
|
21
|
+
spec.add_dependency "representable"
|
22
|
+
spec.add_development_dependency "bundler", "~> 1.3"
|
22
23
|
spec.add_development_dependency "rake"
|
24
|
+
spec.add_development_dependency "minitest"
|
25
|
+
spec.add_development_dependency "activemodel"
|
23
26
|
end
|
data/test/dsl_test.rb
ADDED
@@ -0,0 +1,72 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class DslTest < MiniTest::Spec
|
4
|
+
class SongForm < Reform::Form
|
5
|
+
include DSL
|
6
|
+
|
7
|
+
property :title, :on => :song
|
8
|
+
properties [:name, :genre], :on => :artist
|
9
|
+
|
10
|
+
validates :name, :title, :genre, presence: true
|
11
|
+
end
|
12
|
+
|
13
|
+
let (:form) { SongForm.new(:song => OpenStruct.new(:title => "Rio"), :artist => OpenStruct.new()) }
|
14
|
+
|
15
|
+
it "works by creating Representer and Composition for you" do
|
16
|
+
form.validate("title" => "Greyhound", "name" => "Frenzal Rhomb").must_equal false
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
class ActiveModelTest < MiniTest::Spec
|
21
|
+
class HitForm < Reform::Form
|
22
|
+
include DSL
|
23
|
+
include Reform::Form::ActiveModel
|
24
|
+
|
25
|
+
property :title, :on => :song
|
26
|
+
properties [:name, :genre], :on => :artist
|
27
|
+
|
28
|
+
model :hit, :on => :song
|
29
|
+
end
|
30
|
+
|
31
|
+
let (:rio) { OpenStruct.new(:title => "Rio") }
|
32
|
+
let (:duran) { OpenStruct.new }
|
33
|
+
let (:form) { HitForm.new(:song => rio, :artist => duran) }
|
34
|
+
|
35
|
+
it "creates model readers" do
|
36
|
+
form.hit.must_equal rio
|
37
|
+
end
|
38
|
+
|
39
|
+
it "creates composition readers" do
|
40
|
+
form.song.must_equal rio
|
41
|
+
form.artist.must_equal duran
|
42
|
+
end
|
43
|
+
|
44
|
+
it "provides ::model_name" do
|
45
|
+
form.class.model_name.must_equal "Hit"
|
46
|
+
end
|
47
|
+
|
48
|
+
it "provides #persisted?" do
|
49
|
+
HitForm.new(:song => OpenStruct.new.instance_eval { def persisted?; "yo!"; end; self }, :artist => OpenStruct.new).persisted?.must_equal "yo!"
|
50
|
+
end
|
51
|
+
|
52
|
+
it "provides #to_key" do
|
53
|
+
HitForm.new(:song => OpenStruct.new.instance_eval { def to_key; "yo!"; end; self }, :artist => OpenStruct.new).to_key.must_equal "yo!"
|
54
|
+
end
|
55
|
+
|
56
|
+
it "provides #to_param" do
|
57
|
+
HitForm.new(:song => OpenStruct.new.instance_eval { def to_param; "yo!"; end; self }, :artist => OpenStruct.new).to_param.must_equal "yo!"
|
58
|
+
end
|
59
|
+
|
60
|
+
it "works with any order of ::model and ::property" do
|
61
|
+
class AnotherForm < Reform::Form
|
62
|
+
include DSL
|
63
|
+
include Reform::Form::ActiveModel
|
64
|
+
|
65
|
+
model :song, :on => :song
|
66
|
+
property :title, :on => :song
|
67
|
+
end
|
68
|
+
|
69
|
+
|
70
|
+
AnotherForm.new(:song => rio).song.must_equal rio
|
71
|
+
end
|
72
|
+
end
|
data/test/reform_test.rb
ADDED
@@ -0,0 +1,203 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class RepresenterTest < MiniTest::Spec
|
4
|
+
class SongRepresenter < Reform::Representer
|
5
|
+
properties [:title, :year]
|
6
|
+
end
|
7
|
+
|
8
|
+
let (:rpr) { SongRepresenter.new(OpenStruct.new(:title => "Disconnect, Disconnect", :year => 1990)) }
|
9
|
+
|
10
|
+
# TODO: introduce representer_for helper.
|
11
|
+
describe "::properties" do
|
12
|
+
it "accepts array of property names" do
|
13
|
+
rpr.to_hash.must_equal({"title"=>"Disconnect, Disconnect", "year" => 1990} )
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
describe "#fields" do
|
18
|
+
it "returns all properties as strings" do
|
19
|
+
rpr.fields.must_equal(["title", "year"])
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
class FieldsTest < MiniTest::Spec
|
25
|
+
describe "#new" do
|
26
|
+
it "accepts list of properties" do
|
27
|
+
fields = Reform::Fields.new([:name, :title])
|
28
|
+
fields.name.must_equal nil
|
29
|
+
fields.title.must_equal nil
|
30
|
+
end
|
31
|
+
|
32
|
+
it "accepts list of properties and values" do
|
33
|
+
fields = Reform::Fields.new(["name", "title"], "title" => "The Body")
|
34
|
+
fields.name.must_equal nil
|
35
|
+
fields.title.must_equal "The Body"
|
36
|
+
end
|
37
|
+
|
38
|
+
it "processes value syms" do
|
39
|
+
fields = Reform::Fields.new(["name", "title"], :title => "The Body")
|
40
|
+
fields.name.must_equal nil
|
41
|
+
fields.title.must_equal "The Body"
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
class ReformTest < MiniTest::Spec
|
47
|
+
let (:duran) { OpenStruct.new(:name => "Duran Duran") }
|
48
|
+
let (:rio) { OpenStruct.new(:title => "Rio") }
|
49
|
+
|
50
|
+
let (:form) { SongForm.new(SongAndArtistMap, comp) }
|
51
|
+
|
52
|
+
class SongAndArtistMap < Reform::Representer
|
53
|
+
property :name, on: :artist
|
54
|
+
property :title, on: :song
|
55
|
+
end
|
56
|
+
|
57
|
+
class SongForm < Reform::Form
|
58
|
+
end
|
59
|
+
|
60
|
+
describe "Composition" do
|
61
|
+
class SongAndArtist < Reform::Composition
|
62
|
+
map({:artist => [:name], :song => [:title]}) #SongAndArtistMap.representable_attrs
|
63
|
+
end
|
64
|
+
|
65
|
+
let (:comp) { SongAndArtist.new(:artist => @artist=OpenStruct.new, :song => rio) }
|
66
|
+
|
67
|
+
it "delegates to models as defined" do
|
68
|
+
comp.name.must_equal nil
|
69
|
+
comp.title.must_equal "Rio"
|
70
|
+
end
|
71
|
+
|
72
|
+
it "raises when non-mapped property" do
|
73
|
+
assert_raises NoMethodError do
|
74
|
+
comp.raise_an_exception
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
it "creates readers to models" do
|
79
|
+
comp.song.object_id.must_equal rio.object_id
|
80
|
+
comp.artist.object_id.must_equal @artist.object_id
|
81
|
+
end
|
82
|
+
|
83
|
+
describe "::map_from" do
|
84
|
+
it "creates the same mapping" do
|
85
|
+
comp =
|
86
|
+
Class.new(Reform::Composition) do
|
87
|
+
map_from SongAndArtistMap
|
88
|
+
end.
|
89
|
+
new(:artist => duran, :song => rio)
|
90
|
+
|
91
|
+
comp.name.must_equal "Duran Duran"
|
92
|
+
comp.title.must_equal "Rio"
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
describe "#nested_hash_for" do
|
97
|
+
it "returns nested hash" do
|
98
|
+
comp.nested_hash_for(:name => "Jimi Hendrix", :title => "Fire").must_equal({:artist=>{:name=>"Jimi Hendrix"}, :song=>{:title=>"Fire"}})
|
99
|
+
end
|
100
|
+
|
101
|
+
it "works with strings" do
|
102
|
+
comp.nested_hash_for("name" => "Jimi Hendrix", "title" => "Fire").must_equal({:artist=>{:name=>"Jimi Hendrix"}, :song=>{:title=>"Fire"}})
|
103
|
+
end
|
104
|
+
|
105
|
+
it "works with strings in map" do
|
106
|
+
Class.new(Reform::Composition) do
|
107
|
+
map(:artist => ["name"])
|
108
|
+
end.new([nil]).nested_hash_for(:name => "Jimi Hendrix").must_equal({:artist=>{:name=>"Jimi Hendrix"}})
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
describe "(new) form with empty models" do
|
114
|
+
let (:comp) { SongAndArtist.new(:artist => OpenStruct.new, :song => OpenStruct.new) }
|
115
|
+
|
116
|
+
it "returns empty fields" do
|
117
|
+
form.title.must_equal nil
|
118
|
+
form.name.must_equal nil
|
119
|
+
end
|
120
|
+
|
121
|
+
describe "and submitted values" do
|
122
|
+
it "returns filled-out fields" do
|
123
|
+
form.validate("name" => "Duran Duran")
|
124
|
+
|
125
|
+
form.title.must_equal nil
|
126
|
+
form.name.must_equal "Duran Duran"
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
describe "(edit) form with existing models" do
|
132
|
+
let (:comp) { SongAndArtist.new(:artist => duran, :song => rio) }
|
133
|
+
|
134
|
+
it "returns filled-out fields" do
|
135
|
+
form.name.must_equal "Duran Duran"
|
136
|
+
form.title.must_equal "Rio"
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
describe "#validate" do
|
141
|
+
let (:comp) { SongAndArtist.new(:artist => OpenStruct.new, :song => OpenStruct.new) }
|
142
|
+
|
143
|
+
it "ignores unmapped fields in input" do
|
144
|
+
form.validate("name" => "Duran Duran", :genre => "80s")
|
145
|
+
assert_raises NoMethodError do
|
146
|
+
form.genre
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
it "returns true when valid" do
|
151
|
+
form.validate("name" => "Duran Duran").must_equal true
|
152
|
+
end
|
153
|
+
|
154
|
+
# TODO: test errors. test valid.
|
155
|
+
it "returns false when invalid" do
|
156
|
+
class ValidatingForm < Reform::Form
|
157
|
+
validates :name, :presence => true
|
158
|
+
end
|
159
|
+
|
160
|
+
ValidatingForm.new(SongAndArtistMap, comp).validate({}).must_equal false
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
|
165
|
+
describe "#save" do
|
166
|
+
let (:comp) { SongAndArtist.new(:artist => OpenStruct.new, :song => OpenStruct.new) }
|
167
|
+
let (:form) { SongForm.new(SongAndArtistMap, comp) }
|
168
|
+
|
169
|
+
before { form.validate("name" => "Diesel Boy") }
|
170
|
+
|
171
|
+
it "pushes data to models" do
|
172
|
+
form.save
|
173
|
+
|
174
|
+
comp.artist.name.must_equal "Diesel Boy"
|
175
|
+
comp.song.title.must_equal nil
|
176
|
+
end
|
177
|
+
|
178
|
+
describe "#save with block" do
|
179
|
+
it "provides data block argument" do
|
180
|
+
hash = {}
|
181
|
+
|
182
|
+
form.save do |data, map|
|
183
|
+
hash[:name] = data.name
|
184
|
+
hash[:title] = data.title
|
185
|
+
end
|
186
|
+
|
187
|
+
hash.must_equal({:name=>"Diesel Boy", :title=>nil})
|
188
|
+
end
|
189
|
+
|
190
|
+
it "provides nested symbolized hash as second block argument" do
|
191
|
+
hash = {}
|
192
|
+
|
193
|
+
form.save do |data, map|
|
194
|
+
hash = map
|
195
|
+
end
|
196
|
+
|
197
|
+
hash.must_equal({:artist=>{:name=>"Diesel Boy"}})
|
198
|
+
end
|
199
|
+
end
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
# TODO: test errors
|
data/test/test_helper.rb
ADDED
metadata
CHANGED
@@ -1,16 +1,33 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: reform
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0
|
4
|
+
version: 0.1.0
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
8
8
|
- Nick Sutterer
|
9
|
+
- Garrett Heinlen
|
9
10
|
autorequire:
|
10
11
|
bindir: bin
|
11
12
|
cert_chain: []
|
12
|
-
date: 2013-
|
13
|
+
date: 2013-05-09 00:00:00.000000000 Z
|
13
14
|
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: representable
|
17
|
+
requirement: !ruby/object:Gem::Requirement
|
18
|
+
none: false
|
19
|
+
requirements:
|
20
|
+
- - ! '>='
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: '0'
|
23
|
+
type: :runtime
|
24
|
+
prerelease: false
|
25
|
+
version_requirements: !ruby/object:Gem::Requirement
|
26
|
+
none: false
|
27
|
+
requirements:
|
28
|
+
- - ! '>='
|
29
|
+
- !ruby/object:Gem::Version
|
30
|
+
version: '0'
|
14
31
|
- !ruby/object:Gem::Dependency
|
15
32
|
name: bundler
|
16
33
|
requirement: !ruby/object:Gem::Requirement
|
@@ -43,21 +60,61 @@ dependencies:
|
|
43
60
|
- - ! '>='
|
44
61
|
- !ruby/object:Gem::Version
|
45
62
|
version: '0'
|
63
|
+
- !ruby/object:Gem::Dependency
|
64
|
+
name: minitest
|
65
|
+
requirement: !ruby/object:Gem::Requirement
|
66
|
+
none: false
|
67
|
+
requirements:
|
68
|
+
- - ! '>='
|
69
|
+
- !ruby/object:Gem::Version
|
70
|
+
version: '0'
|
71
|
+
type: :development
|
72
|
+
prerelease: false
|
73
|
+
version_requirements: !ruby/object:Gem::Requirement
|
74
|
+
none: false
|
75
|
+
requirements:
|
76
|
+
- - ! '>='
|
77
|
+
- !ruby/object:Gem::Version
|
78
|
+
version: '0'
|
79
|
+
- !ruby/object:Gem::Dependency
|
80
|
+
name: activemodel
|
81
|
+
requirement: !ruby/object:Gem::Requirement
|
82
|
+
none: false
|
83
|
+
requirements:
|
84
|
+
- - ! '>='
|
85
|
+
- !ruby/object:Gem::Version
|
86
|
+
version: '0'
|
87
|
+
type: :development
|
88
|
+
prerelease: false
|
89
|
+
version_requirements: !ruby/object:Gem::Requirement
|
90
|
+
none: false
|
91
|
+
requirements:
|
92
|
+
- - ! '>='
|
93
|
+
- !ruby/object:Gem::Version
|
94
|
+
version: '0'
|
46
95
|
description: Freeing your AR models from form logic.
|
47
96
|
email:
|
48
97
|
- apotonick@gmail.com
|
98
|
+
- heinleng@gmail.com
|
49
99
|
executables: []
|
50
100
|
extensions: []
|
51
101
|
extra_rdoc_files: []
|
52
102
|
files:
|
53
103
|
- .gitignore
|
104
|
+
- CHANGES.md
|
54
105
|
- Gemfile
|
55
106
|
- LICENSE.txt
|
56
107
|
- README.md
|
57
108
|
- Rakefile
|
58
109
|
- lib/reform.rb
|
110
|
+
- lib/reform/form.rb
|
111
|
+
- lib/reform/form/active_model.rb
|
112
|
+
- lib/reform/form/dsl.rb
|
59
113
|
- lib/reform/version.rb
|
60
114
|
- reform.gemspec
|
115
|
+
- test/dsl_test.rb
|
116
|
+
- test/reform_test.rb
|
117
|
+
- test/test_helper.rb
|
61
118
|
homepage: ''
|
62
119
|
licenses:
|
63
120
|
- MIT
|
@@ -83,4 +140,7 @@ rubygems_version: 1.8.25
|
|
83
140
|
signing_key:
|
84
141
|
specification_version: 3
|
85
142
|
summary: Freeing your AR models from form logic.
|
86
|
-
test_files:
|
143
|
+
test_files:
|
144
|
+
- test/dsl_test.rb
|
145
|
+
- test/reform_test.rb
|
146
|
+
- test/test_helper.rb
|