reform 0.1.2 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. data/.gitignore +1 -0
  2. data/.travis.yml +0 -1
  3. data/CHANGES.md +13 -0
  4. data/Gemfile +0 -2
  5. data/README.md +276 -94
  6. data/Rakefile +6 -0
  7. data/TODO.md +10 -1
  8. data/database.sqlite3 +0 -0
  9. data/lib/reform/active_record.rb +2 -0
  10. data/lib/reform/composition.rb +55 -0
  11. data/lib/reform/form/active_model.rb +60 -15
  12. data/lib/reform/form/active_record.rb +3 -3
  13. data/lib/reform/form/composition.rb +69 -0
  14. data/lib/reform/form.rb +183 -80
  15. data/lib/reform/rails.rb +8 -1
  16. data/lib/reform/representer.rb +38 -0
  17. data/lib/reform/version.rb +1 -1
  18. data/lib/reform.rb +5 -2
  19. data/reform.gemspec +3 -2
  20. data/test/active_model_test.rb +83 -9
  21. data/test/coercion_test.rb +26 -0
  22. data/test/composition_test.rb +57 -0
  23. data/test/dummy/Rakefile +7 -0
  24. data/test/dummy/app/controllers/albums_controller.rb +18 -0
  25. data/test/dummy/app/controllers/application_controller.rb +4 -0
  26. data/test/dummy/app/controllers/musician_controller.rb +5 -0
  27. data/test/dummy/app/forms/album_form.rb +18 -0
  28. data/test/dummy/app/helpers/application_helper.rb +2 -0
  29. data/test/dummy/app/models/album.rb +4 -0
  30. data/test/dummy/app/models/song.rb +3 -0
  31. data/test/dummy/app/views/albums/new.html.erb +28 -0
  32. data/test/dummy/app/views/layouts/application.html.erb +14 -0
  33. data/test/dummy/config/application.rb +20 -0
  34. data/test/dummy/config/boot.rb +10 -0
  35. data/test/dummy/config/database.yml +22 -0
  36. data/test/dummy/config/environment.rb +5 -0
  37. data/test/dummy/config/environments/development.rb +16 -0
  38. data/test/dummy/config/environments/production.rb +46 -0
  39. data/test/dummy/config/environments/test.rb +33 -0
  40. data/test/dummy/config/locales/en.yml +5 -0
  41. data/test/dummy/config/routes.rb +4 -0
  42. data/test/dummy/config.ru +4 -0
  43. data/test/dummy/db/test.sqlite3 +0 -0
  44. data/test/dummy/log/production.log +0 -0
  45. data/test/dummy/log/server.log +0 -0
  46. data/test/errors_test.rb +95 -0
  47. data/test/form_composition_test.rb +60 -0
  48. data/test/nested_form_test.rb +129 -0
  49. data/test/rails/integration_test.rb +54 -0
  50. data/test/reform_test.rb +80 -114
  51. data/test/test_helper.rb +14 -1
  52. metadata +86 -11
  53. data/lib/reform/form/dsl.rb +0 -38
  54. data/test/dsl_test.rb +0 -43
@@ -1,12 +1,84 @@
1
1
  require 'test_helper'
2
2
 
3
- class ActiveModelTest < MiniTest::Spec
3
+ class NewActiveModelTest < MiniTest::Spec # TODO: move to test/rails/
4
+ class SongForm < Reform::Form
5
+ include Reform::Form::ActiveModel
6
+
7
+ property :name
8
+ end
9
+
10
+ let (:artist) { Artist.create(:name => "Frank Zappa") }
11
+ let (:form) { SongForm.new(artist) }
12
+
13
+ it { form.persisted?.must_equal true }
14
+ it { form.to_key.must_equal [artist.id] }
15
+ it { form.to_param.must_equal "#{artist.id}" }
16
+ it { form.to_model.must_equal form }
17
+ it { form.id.must_equal artist.id }
18
+
19
+ describe "::model_name" do
20
+ it { form.class.model_name.must_be_kind_of ActiveModel::Name }
21
+ it { form.class.model_name.to_s.must_equal "NewActiveModelTest::Song" }
22
+
23
+ let (:class_with_model) {
24
+ Class.new(Reform::Form) do
25
+ include Reform::Form::ActiveModel
26
+
27
+ model :album
28
+ end
29
+ }
30
+
31
+ it { class_with_model.model_name.must_be_kind_of ActiveModel::Name }
32
+ it { class_with_model.model_name.to_s.must_equal "Album" }
33
+ end
34
+ end
35
+
36
+ class FormBuilderCompatTest < MiniTest::Spec
37
+ let (:form_class) {
38
+ Class.new(Reform::Form) do
39
+ include Reform::Form::ActiveModel::FormBuilderMethods
40
+
41
+ property :artist do
42
+ property :name
43
+ validates :name, :presence => true
44
+ end
45
+
46
+ collection :songs do
47
+ property :title
48
+ validates :title, :presence => true
49
+ end
50
+ end
51
+ }
52
+ let (:form) { form_class.new(OpenStruct.new(:artist => Artist.new, :songs => [OpenStruct.new])) }
53
+ # TODO: test when keys are missing!
54
+
55
+ it "respects _attributes params hash" do
56
+ form.validate("artist_attributes" => {"name" => "Blink 182"},
57
+ "songs_attributes" => {"0" => {"title" => "Damnit"}})
58
+
59
+ form.artist.name.must_equal "Blink 182"
60
+ form.songs.first.title.must_equal "Damnit"
61
+ end
62
+
63
+ it "defines _attributes= setter so Rails' FB works properly" do
64
+ form.must_respond_to("artist_attributes=")
65
+ form.must_respond_to("songs_attributes=")
66
+ end
67
+
68
+ it "returns flat errors hash" do
69
+ form.validate("artist_attributes" => {"name" => ""},
70
+ "songs_attributes" => {"0" => {"title" => ""}})
71
+ form.errors.messages.must_equal(:"artist.name" => ["can't be blank"], :"songs.title" => ["can't be blank"])
72
+ end
73
+ end
74
+
75
+ class ActiveModelWithCompositionTest < MiniTest::Spec
4
76
  class HitForm < Reform::Form
5
- include DSL
77
+ include Composition
6
78
  include Reform::Form::ActiveModel
7
79
 
8
- property :title, :on => :song
9
- properties [:name, :genre], :on => :artist
80
+ property :title, :on => :song
81
+ properties [:name, :genre], :on => :artist # we need to check both ::property and ::properties here!
10
82
 
11
83
  model :hit, :on => :song
12
84
  end
@@ -21,11 +93,11 @@ class ActiveModelTest < MiniTest::Spec
21
93
  end
22
94
 
23
95
  it "doesn't delegate when :on missing" do
24
- class HitForm < Reform::Form
25
- include DSL
96
+ class SongOnlyForm < Reform::Form
97
+ include Composition
26
98
  include Reform::Form::ActiveModel
27
99
 
28
- property :title, :on => :song
100
+ property :title, :on => :song
29
101
 
30
102
  model :song
31
103
  end.new(:song => rio, :artist => duran).song.must_equal rio
@@ -34,6 +106,7 @@ class ActiveModelTest < MiniTest::Spec
34
106
 
35
107
 
36
108
  it "creates composition readers" do
109
+ skip "we don't want those anymore since they don't represent the form internal state!"
37
110
  form.song.must_equal rio
38
111
  form.artist.must_equal duran
39
112
  end
@@ -55,12 +128,13 @@ class ActiveModelTest < MiniTest::Spec
55
128
  end
56
129
 
57
130
  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!"
131
+ form = HitForm.new(:song => OpenStruct.new, :artist => OpenStruct.new)
132
+ form.to_model.must_equal form
59
133
  end
60
134
 
61
135
  it "works with any order of ::model and ::property" do
62
136
  class AnotherForm < Reform::Form
63
- include DSL
137
+ include Composition
64
138
  include Reform::Form::ActiveModel
65
139
 
66
140
  model :song, :on => :song
@@ -0,0 +1,26 @@
1
+ require "test_helper"
2
+ require "reform/form/coercion"
3
+
4
+ class CoercionTest < MiniTest::Spec
5
+ it "allows coercion" do
6
+ form = Class.new(Reform::Form) do
7
+ include Reform::Form::Coercion
8
+
9
+ property :written_at, :type => DateTime
10
+ end.new(OpenStruct.new(:written_at => "31/03/1981"))
11
+
12
+ form.written_at.must_be_kind_of DateTime
13
+ form.written_at.must_equal DateTime.parse("Tue, 31 Mar 1981 00:00:00 +0000")
14
+ end
15
+
16
+ it "allows coercion in validate" do
17
+ form = Class.new(Reform::Form) do
18
+ include Reform::Form::Coercion
19
+
20
+ property :id, :type => Integer
21
+ end.new(OpenStruct.new())
22
+
23
+ form.validate("id" => "1")
24
+ form.to_hash.must_equal("id" => 1)
25
+ end
26
+ end
@@ -0,0 +1,57 @@
1
+ class CompositionTest < ReformSpec
2
+ class SongAndArtist < Reform::Composition
3
+ map({:artist => [:name], :song => [:title]}) #SongAndArtistMap.representable_attrs
4
+ end
5
+
6
+ let (:comp) { SongAndArtist.new(:artist => @artist=OpenStruct.new, :song => rio) }
7
+
8
+ it "delegates to models as defined" do
9
+ comp.name.must_equal nil
10
+ comp.title.must_equal "Rio"
11
+ end
12
+
13
+ it "raises when non-mapped property" do
14
+ assert_raises NoMethodError do
15
+ comp.raise_an_exception
16
+ end
17
+ end
18
+
19
+ it "creates readers to models" do
20
+ comp.song.object_id.must_equal rio.object_id
21
+ comp.artist.object_id.must_equal @artist.object_id
22
+ end
23
+
24
+ describe "::map_from" do
25
+ it "creates the same mapping" do
26
+ comp =
27
+ Class.new(Reform::Composition) do
28
+ map_from(
29
+ Class.new(Reform::Representer) do
30
+ property :name, :on => :artist
31
+ property :title, :on => :song
32
+ end
33
+ )
34
+ end.
35
+ new(:artist => duran, :song => rio)
36
+
37
+ comp.name.must_equal "Duran Duran"
38
+ comp.title.must_equal "Rio"
39
+ end
40
+ end
41
+
42
+ describe "#nested_hash_for" do
43
+ it "returns nested hash" do
44
+ comp.nested_hash_for(:name => "Jimi Hendrix", :title => "Fire").must_equal({:artist=>{:name=>"Jimi Hendrix"}, :song=>{:title=>"Fire"}})
45
+ end
46
+
47
+ it "works with strings" do
48
+ comp.nested_hash_for("name" => "Jimi Hendrix", "title" => "Fire").must_equal({:artist=>{:name=>"Jimi Hendrix"}, :song=>{:title=>"Fire"}})
49
+ end
50
+
51
+ it "works with strings in map" do
52
+ Class.new(Reform::Composition) do
53
+ map(:artist => ["name"])
54
+ end.new([nil]).nested_hash_for(:name => "Jimi Hendrix").must_equal({:artist=>{:name=>"Jimi Hendrix"}})
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,7 @@
1
+ # Add your own tasks in files placed in lib/tasks ending in .rake,
2
+ # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
3
+
4
+ require File.expand_path('../config/application', __FILE__)
5
+ require 'rake'
6
+
7
+ Rails::Application.load_tasks
@@ -0,0 +1,18 @@
1
+ class AlbumsController < ActionController::Base
2
+ def new
3
+ album = Album.new(:songs => [Song.new, Song.new])
4
+ @form = AlbumForm.new(album)
5
+ end
6
+
7
+ def create
8
+ album = Album.new(songs: [Song.new, Song.new])
9
+ @form = AlbumForm.new(album)
10
+
11
+ if @form.validate(params["album"])
12
+ @form.save
13
+ redirect_to album_path(album)
14
+ else
15
+ render :new
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,4 @@
1
+ class ApplicationController < ActionController::Base
2
+ protect_from_forgery
3
+ layout 'application'
4
+ end
@@ -0,0 +1,5 @@
1
+ class MusicianController < ActionController::Base
2
+ def index
3
+ render :text => Artist.find(:all)
4
+ end
5
+ end
@@ -0,0 +1,18 @@
1
+ class AlbumForm < Reform::Form # FIXME: sub forms don't inherit FBM.
2
+
3
+ model :album
4
+
5
+ property :title
6
+
7
+ collection :songs do
8
+ property :title
9
+ validates :title, presence: true
10
+ end
11
+
12
+ validates :title, presence: true
13
+
14
+ def save
15
+ super
16
+ model.save
17
+ end
18
+ end
@@ -0,0 +1,2 @@
1
+ module ApplicationHelper
2
+ end
@@ -0,0 +1,4 @@
1
+ class Album < ActiveRecord::Base
2
+ has_many :songs
3
+ has_and_belongs_to_many :artists
4
+ end
@@ -0,0 +1,3 @@
1
+ class Song < ActiveRecord::Base
2
+ belongs_to :album
3
+ end
@@ -0,0 +1,28 @@
1
+ New Album
2
+
3
+ <%= form_for @form do |f| %>
4
+ <% if @form.errors.any? %>
5
+ <div>
6
+ <h3><%= pluralize(@form.errors.count, "error") %> prohibited this form from being saved:</h3>
7
+
8
+ <ul>
9
+ <% @form.errors.full_messages.each do |msg| %>
10
+ <li><%= msg %></li>
11
+ <% end %>
12
+ </ul>
13
+ </div>
14
+ <% end %>
15
+
16
+ <%= f.label :title, "Album Title" %>
17
+ <%= f.text_field :title %>
18
+
19
+ <%= f.fields_for :songs do |song| %>
20
+ <div>
21
+ <%= song.label :title %>
22
+ <%= song.text_field :title %>
23
+ </div>
24
+ <% end %>
25
+
26
+ <%= f.submit %>
27
+ <% end %>
28
+
@@ -0,0 +1,14 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>Dummy</title>
5
+ <%= stylesheet_link_tag :all %>
6
+ <%= javascript_include_tag :defaults %>
7
+ <%= csrf_meta_tag %>
8
+ </head>
9
+ <body>
10
+
11
+ <%= yield %>
12
+
13
+ </body>
14
+ </html>
@@ -0,0 +1,20 @@
1
+ require File.expand_path('../boot', __FILE__)
2
+
3
+ require "active_model/railtie"
4
+ require "action_controller/railtie"
5
+ require "action_view/railtie"
6
+
7
+ Bundler.require
8
+
9
+ module Dummy
10
+ class Application < Rails::Application
11
+ config.encoding = "utf-8"
12
+
13
+ # Configure sensitive parameters which will be filtered from the log file.
14
+ config.filter_parameters += [:password]
15
+
16
+ config.cache_store = :memory_store
17
+ end
18
+ end
19
+
20
+ require "reform/rails"
@@ -0,0 +1,10 @@
1
+ require 'rubygems'
2
+ gemfile = File.expand_path('../../../../Gemfile', __FILE__)
3
+
4
+ if File.exist?(gemfile)
5
+ ENV['BUNDLE_GEMFILE'] = gemfile
6
+ require 'bundler'
7
+ Bundler.setup
8
+ end
9
+
10
+ $:.unshift File.expand_path('../../../../lib', __FILE__)
@@ -0,0 +1,22 @@
1
+ # SQLite version 3.x
2
+ # gem install sqlite3-ruby (not necessary on OS X Leopard)
3
+ development:
4
+ adapter: sqlite3
5
+ database: db/development.sqlite3
6
+ pool: 5
7
+ timeout: 5000
8
+
9
+ # Warning: The database defined as "test" will be erased and
10
+ # re-generated from your development database when you run "rake".
11
+ # Do not set this db to the same as development or production.
12
+ test:
13
+ adapter: sqlite3
14
+ database: db/test.sqlite3
15
+ pool: 5
16
+ timeout: 5000
17
+
18
+ production:
19
+ adapter: sqlite3
20
+ database: db/production.sqlite3
21
+ pool: 5
22
+ timeout: 5000
@@ -0,0 +1,5 @@
1
+ # Load the rails application
2
+ require File.expand_path('../application', __FILE__)
3
+
4
+ # Initialize the rails application
5
+ Dummy::Application.initialize!
@@ -0,0 +1,16 @@
1
+ Dummy::Application.configure do
2
+ # Settings specified here will take precedence over those in config/environment.rb
3
+
4
+ # In the development environment your application's code is reloaded on
5
+ # every request. This slows down response time but is perfect for development
6
+ # since you don't have to restart the webserver when you make code changes.
7
+ config.cache_classes = false
8
+
9
+ # Log error messages when you accidentally call methods on nil.
10
+ config.whiny_nils = true
11
+
12
+ # Show full error reports and disable caching
13
+ config.consider_all_requests_local = true
14
+ config.action_controller.perform_caching = false
15
+ config.secret_key_base = 123
16
+ end
@@ -0,0 +1,46 @@
1
+ Dummy::Application.configure do
2
+ # Settings specified here will take precedence over those in config/environment.rb
3
+
4
+ # The production environment is meant for finished, "live" apps.
5
+ # Code is not reloaded between requests
6
+ config.cache_classes = true
7
+
8
+ # Full error reports are disabled and caching is turned on
9
+ config.consider_all_requests_local = false
10
+ config.action_controller.perform_caching = true
11
+
12
+ # Specifies the header that your server uses for sending files
13
+ config.action_dispatch.x_sendfile_header = "X-Sendfile"
14
+
15
+ # For nginx:
16
+ # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect'
17
+
18
+ # If you have no front-end server that supports something like X-Sendfile,
19
+ # just comment this out and Rails will serve the files
20
+
21
+ # See everything in the log (default is :info)
22
+ # config.log_level = :debug
23
+
24
+ # Use a different logger for distributed setups
25
+ # config.logger = SyslogLogger.new
26
+
27
+ # Use a different cache store in production
28
+ # config.cache_store = :mem_cache_store
29
+
30
+ # Disable Rails's static asset server
31
+ # In production, Apache or nginx will already do this
32
+ config.serve_static_assets = false
33
+
34
+ # Enable serving of images, stylesheets, and javascripts from an asset server
35
+ # config.action_controller.asset_host = "http://assets.example.com"
36
+
37
+ # Disable delivery errors, bad email addresses will be ignored
38
+ # config.action_mailer.raise_delivery_errors = false
39
+
40
+ # Enable threaded mode
41
+ # config.threadsafe!
42
+
43
+ # Enable locale fallbacks for I18n (makes lookups for any locale fall back to
44
+ # the I18n.default_locale when a translation can not be found)
45
+ config.i18n.fallbacks = true
46
+ end
@@ -0,0 +1,33 @@
1
+ Dummy::Application.configure do
2
+ # Settings specified here will take precedence over those in config/environment.rb
3
+
4
+ # The test environment is used exclusively to run your application's
5
+ # test suite. You never need to work with it otherwise. Remember that
6
+ # your test database is "scratch space" for the test suite and is wiped
7
+ # and recreated between test runs. Don't rely on the data there!
8
+ config.cache_classes = true
9
+
10
+ # Log error messages when you accidentally call methods on nil.
11
+ config.whiny_nils = true
12
+
13
+ # Show full error reports and disable caching
14
+ config.consider_all_requests_local = true
15
+ config.action_controller.perform_caching = false
16
+
17
+ # Raise exceptions instead of rendering exception templates
18
+ config.action_dispatch.show_exceptions = false
19
+
20
+ # Disable request forgery protection in test environment
21
+ config.action_controller.allow_forgery_protection = false
22
+ config.secret_key_base = "yo"
23
+
24
+ # Tell Action Mailer not to deliver emails to the real world.
25
+ # The :test delivery method accumulates sent emails in the
26
+ # ActionMailer::Base.deliveries array.
27
+ #config.action_mailer.delivery_method = :test
28
+
29
+ # Use SQL instead of Active Record's schema dumper when creating the test database.
30
+ # This is necessary if your schema can't be completely dumped by the schema dumper,
31
+ # like if you have constraints or database-specific column types
32
+ # config.active_record.schema_format = :sql
33
+ end
@@ -0,0 +1,5 @@
1
+ # Sample localization file for English. Add more files in this directory for other locales.
2
+ # See http://github.com/svenfuchs/rails-i18n/tree/master/rails%2Flocale for starting points.
3
+
4
+ en:
5
+ hello: "Hello world"
@@ -0,0 +1,4 @@
1
+ Dummy::Application.routes.draw do
2
+ get ':controller(/:action(/:id(.:format)))'
3
+ resources :albums
4
+ end
@@ -0,0 +1,4 @@
1
+ # This file is used by Rack-based servers to start the application.
2
+
3
+ require ::File.expand_path('../config/environment', __FILE__)
4
+ run Dummy::Application
File without changes
File without changes
File without changes
@@ -0,0 +1,95 @@
1
+ require 'test_helper'
2
+
3
+ class ErrorsTest < MiniTest::Spec
4
+ class AlbumForm < Reform::Form
5
+ property :title
6
+
7
+ property :hit do
8
+ property :title
9
+ validates :title, :presence => true
10
+ end
11
+
12
+ collection :songs do
13
+ property :title
14
+ validates :title, :presence => true
15
+ end
16
+
17
+ validates :title, :presence => true
18
+ end
19
+
20
+ let (:album) do
21
+ OpenStruct.new(
22
+ :title => "Blackhawks Over Los Angeles",
23
+ :hit => song,
24
+ :songs => songs # TODO: document this requirement
25
+ )
26
+ end
27
+ let (:song) { OpenStruct.new(:title => "Downtown") }
28
+ let (:songs) { [song=OpenStruct.new(:title => "Calling"), song] }
29
+ let (:form) { AlbumForm.new(album) }
30
+
31
+
32
+ describe "incorrect #validate" do
33
+ before { form.validate(
34
+ "hit" =>{"title" => ""},
35
+ "title" => "",
36
+ "songs" => [{"title" => ""}, {"title" => ""}]) } # FIXME: what happens if item is missing?
37
+
38
+ it do
39
+ form.errors.messages.must_equal({
40
+ :title => ["can't be blank"],
41
+ :"hit.title"=>["can't be blank"],
42
+ :"songs.title"=>["can't be blank"]})
43
+ end
44
+
45
+ it do
46
+ #form.errors.must_equal({:title => ["can't be blank"]})
47
+ # TODO: this should only contain local errors?
48
+ end
49
+
50
+ # nested forms keep their own Errors:
51
+ it { form.hit.errors.messages.must_equal({:title=>["can't be blank"]}) }
52
+ it { form.songs[0].errors.messages.must_equal({:title=>["can't be blank"]}) }
53
+
54
+ it do
55
+ form.errors.messages.must_equal({
56
+ :title => ["can't be blank"],
57
+ :"hit.title" => ["can't be blank"],
58
+ :"songs.title"=> ["can't be blank"]})
59
+ end # TODO: add another invalid item.
60
+ end
61
+
62
+ describe "#validate with main form invalid" do
63
+ before { @result = form.validate("title"=>"") }
64
+
65
+ it { @result.must_equal false }
66
+ it { form.errors.messages.must_equal({:title=>["can't be blank"]}) }
67
+ end
68
+
69
+ describe "#validate with middle nested form invalid" do
70
+ before { @result = form.validate("hit"=>{"title" => ""}) }
71
+
72
+ it { @result.must_equal false }
73
+ it { form.errors.messages.must_equal({:"hit.title"=>["can't be blank"]}) }
74
+ end
75
+
76
+ describe "#validate with last nested form invalid" do
77
+ before { @result = form.validate("songs"=>[{"title" => ""}]) }
78
+
79
+ it { @result.must_equal false }
80
+ it { form.errors.messages.must_equal({:"songs.title"=>["can't be blank"]}) }
81
+ end
82
+
83
+ describe "correct #validate" do
84
+ before { @result = form.validate(
85
+ "hit" => {"title" => "Sacrifice"},
86
+ "title" => "Second Heat",
87
+ "songs" => [{"title"=>"Heart Of A Lion"}]
88
+ ) }
89
+
90
+ it { @result.must_equal true }
91
+ it { form.hit.title.must_equal "Sacrifice" }
92
+ it { form.title.must_equal "Second Heat" }
93
+ it { form.songs.first.title.must_equal "Heart Of A Lion" }
94
+ end
95
+ end
@@ -0,0 +1,60 @@
1
+ require 'test_helper'
2
+
3
+ class FormCompositionTest < MiniTest::Spec
4
+ class SongForm < Reform::Form
5
+ include Composition
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 => song, :artist => artist) }
14
+ let (:song) { OpenStruct.new(:title => "Rio") }
15
+ let (:artist) { OpenStruct.new(:name => "Duran Duran") }
16
+
17
+
18
+ # delegation form -> composition works
19
+ it { form.title.must_equal "Rio" }
20
+ it { form.name.must_equal "Duran Duran" }
21
+ # delegation form -> composed models (e.g. when saving this can be handy)
22
+ it { form.song.must_equal song }
23
+ it { form.artist.must_equal artist }
24
+
25
+
26
+ it "creates Composition for you" do
27
+ form.validate("title" => "Greyhound", "name" => "Frenzal Rhomb").must_equal false
28
+ end
29
+
30
+ describe "#save" do
31
+ it "provides data block argument" do
32
+ hash = {}
33
+
34
+ form.save do |data, map|
35
+ hash[:name] = data.name
36
+ hash[:title] = data.title
37
+ end
38
+
39
+ hash.must_equal({:name=>"Duran Duran", :title=>"Rio"})
40
+ end
41
+
42
+ it "provides nested symbolized hash as second block argument" do
43
+ hash = {}
44
+
45
+ form.save do |data, map|
46
+ hash = map
47
+ end
48
+
49
+ hash.must_equal({:song=>{:title=>"Rio"}, :artist=>{:name=>"Duran Duran"}})
50
+ end
51
+
52
+ it "pushes data to models when no block passed" do
53
+ form.validate("title" => "Greyhound", "name" => "Frenzal Rhomb")
54
+ form.save
55
+
56
+ artist.name.must_equal "Frenzal Rhomb"
57
+ song.title.must_equal "Greyhound"
58
+ end
59
+ end
60
+ end