forminate 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 98167c9f1b4bb61b66458c8166c6c1406dc29426
4
+ data.tar.gz: 5e6e5c08e0467967649dba74a28677b07353ef06
5
+ SHA512:
6
+ metadata.gz: 9d82544a8edb6a3a87d70d7ae76313942a73ce0242a618dcd55937dee533cfe1d3870001ca9614b4db8bf8f4f138f9aba167f55438ac1d8470d0b9b59622a74d
7
+ data.tar.gz: e156a1daa75023040fa9a04db1df13464d08a4afaba43718a9be20cbb7c35f0179e5e57bdb0d262b670a890a853d629be6890b5504591500fbfaef52cfad7c61
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/.travis.yml ADDED
@@ -0,0 +1,6 @@
1
+ language: ruby
2
+ rvm:
3
+ - 1.9.3
4
+ - 2.0.0
5
+ - jruby-19mode
6
+ - rbx-19mode
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in forminate.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Mo Lawson
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,32 @@
1
+ # Forminate
2
+
3
+ [![Build Status](https://travis-ci.org/molawson/forminate.png?branch=master)](https://travis-ci.org/molawson/forminate)
4
+ [![Code Climate](https://codeclimate.com/github/molawson/forminate.png)](https://codeclimate.com/github/molawson/forminate)
5
+
6
+ TODO: Write a gem description
7
+
8
+ ## Installation
9
+
10
+ Add this line to your application's Gemfile:
11
+
12
+ gem 'forminate'
13
+
14
+ And then execute:
15
+
16
+ $ bundle
17
+
18
+ Or install it yourself as:
19
+
20
+ $ gem install forminate
21
+
22
+ ## Usage
23
+
24
+ TODO: Write usage instructions here
25
+
26
+ ## Contributing
27
+
28
+ 1. Fork it
29
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
30
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
31
+ 4. Push to the branch (`git push origin my-new-feature`)
32
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,5 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rspec/core/rake_task'
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+ task :default => :spec
data/forminate.gemspec ADDED
@@ -0,0 +1,28 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'forminate/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "forminate"
8
+ spec.version = Forminate::VERSION
9
+ spec.authors = ["Mo Lawson"]
10
+ spec.email = ["moklawson@gmail.com"]
11
+ spec.description = "Form objects for Rails applications"
12
+ spec.summary = "Create form objects from multiple Active Record and/or ActiveAttr models."
13
+ spec.homepage = "https://github.com/molawson/forminate"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files`.split($/)
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_dependency "active_attr", "~> 0.8"
22
+ spec.add_dependency "activesupport", ">= 3.0.2", "< 4.1"
23
+ spec.add_dependency "client_side_validations", "~> 3.2"
24
+
25
+ spec.add_development_dependency "bundler", "~> 1.3"
26
+ spec.add_development_dependency "rake"
27
+ spec.add_development_dependency "rspec", "~> 2.11"
28
+ end
@@ -0,0 +1,124 @@
1
+ module ActiveModel
2
+ class Validator
3
+ # To make use of validators present on associated object, we need to be
4
+ # able to specify the original object that a given validator came from.
5
+ attr_accessor :target_object
6
+ end
7
+ end
8
+
9
+ module Forminate
10
+ # This module makes Forminate work with bcardarella's client_side_validations
11
+ # gem (https://github.com/bcardarella/client_side_validations).
12
+ #
13
+ # The majority of the methods contained here are overrides or alternative
14
+ # versions of methods in the client_side_validations gem. For the most part,
15
+ # they follow the shape and ideas of the original implementation, usually adding
16
+ # the ability to send messages to an associated object rather than the Forminate
17
+ # object itself.
18
+ module ClientSideValidations
19
+ extend ActiveSupport::Concern
20
+
21
+ # Where the magic happens. This overrides the client_side_validations gem's
22
+ # method of the same name, using Forminate's #association_validators rather
23
+ # than ActiveModel's _validators method. It still calls super so that it
24
+ # includes validations placed directly on the Forminate object.
25
+ def client_side_validation_hash(force = nil)
26
+ hash = super
27
+ assoc_hash = association_validators.reduce({}) do |assoc_hash, (attr, validators)|
28
+ unless [nil, :block].include?(attr)
29
+
30
+ validator_hash = validators.reduce(Hash.new { |h,k| h[k] = []}) do |kind_hash, validator|
31
+ model = validator.target_object
32
+ model_pattern = /^#{model.class.name.underscore}_/
33
+ target_attr = attr.to_s.sub(model_pattern, '').to_sym
34
+
35
+ if force.is_a? Hash
36
+ relevant_force = force.select { |k, v| k.to_s.match model_pattern }
37
+ assoc_force = relevant_force.reduce({}) do |assoc_force, (key, value)|
38
+ key = key.to_s.sub(model_pattern, '').to_sym
39
+ assoc_force.merge({ key => value })
40
+ end
41
+ else
42
+ assoc_force = force
43
+ end
44
+
45
+ if _can_use_for_client_side_validation?(model, target_attr, validator, assoc_force)
46
+ if client_side_hash = validator.client_side_hash(model, target_attr, extract_force_option(target_attr, assoc_force))
47
+ kind_hash[validator.kind] << client_side_hash.except(:on, :if, :unless)
48
+ end
49
+ end
50
+
51
+ kind_hash
52
+ end
53
+
54
+ if validator_hash.present?
55
+ assoc_hash.merge!(attr => validator_hash)
56
+ else
57
+ assoc_hash
58
+ end
59
+ else
60
+ assoc_hash
61
+ end
62
+ end
63
+ hash.merge assoc_hash
64
+ end
65
+
66
+ private
67
+
68
+ # Constructs the associated validators for use with client_side_validations.
69
+ # This is meant to mimic ActiveModel's #_validators method that the
70
+ # client_side_validations gem relies on for construction the client side
71
+ # validation hash.
72
+ def association_validators
73
+ associations.reduce({}) do |assoc_validators, (name, object)|
74
+ if should_validate_assoc?(name) && object.respond_to?(:_validators)
75
+ object._validators.each do |attr, validators|
76
+ new_validators = validators.reduce([]) do |new_validators, validator|
77
+ new_validator = validator.dup
78
+ new_validator.target_object = object
79
+ new_validators << new_validator
80
+ end
81
+ assoc_validators["#{name}_#{attr}".to_sym] = new_validators
82
+ end
83
+ end
84
+ assoc_validators
85
+ end
86
+ end
87
+
88
+ # Alternative version of the client_side_validation gem's
89
+ # #can_use_for_client_side_validation? method, allowing the target 'model'
90
+ # to be passed in as an argument.
91
+ def _can_use_for_client_side_validation?(model, attr, validator, force)
92
+ if validator_turned_off?(attr, validator, force)
93
+ result = false
94
+ else
95
+ result = ((model.respond_to?(:new_record?) && validator.options[:on] == (model.new_record? ? :create : :update)) || validator.options[:on].nil?)
96
+ result = result && validator.kind != :block
97
+
98
+ if validator.options[:if] || validator.options[:unless]
99
+ if result = can_force_validator?(attr, validator, force)
100
+ if validator.options[:if]
101
+ result = result && _run_conditional(model, validator.options[:if])
102
+ end
103
+ if validator.options[:unless]
104
+ result = result && !_run_conditional(model, validator.options[:unless])
105
+ end
106
+ end
107
+ end
108
+ end
109
+
110
+ result
111
+ end
112
+
113
+ # Alternative version of the client_side_validation gem's #run_conditional
114
+ # method, allowing the target 'model' to be passed in as an argument.
115
+ def _run_conditional(model, method_name_or_proc)
116
+ case method_name_or_proc
117
+ when Proc
118
+ method_name_or_proc.call(model)
119
+ else
120
+ model.send(method_name_or_proc)
121
+ end
122
+ end
123
+ end
124
+ end
@@ -0,0 +1,3 @@
1
+ module Forminate
2
+ VERSION = "0.1.0"
3
+ end
data/lib/forminate.rb ADDED
@@ -0,0 +1,196 @@
1
+ require "forminate/version"
2
+
3
+ require 'active_support/concern'
4
+ require 'active_attr'
5
+
6
+ module Forminate
7
+ extend ActiveSupport::Concern
8
+ include ActiveAttr::Model
9
+
10
+ included do
11
+ validate do
12
+ associations.each do |name, object|
13
+ if should_validate_assoc?(name) && object.respond_to?(:invalid?) && object.invalid?
14
+ object.errors.each do |field, messages|
15
+ errors["#{name}_#{field}".to_sym] = messages
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
21
+
22
+ module ClassMethods
23
+ def attributes_for(name, options = {})
24
+ define_attributes name
25
+ define_association name, options
26
+ end
27
+
28
+ def association_names
29
+ @association_names ||= []
30
+ end
31
+
32
+ def association_validations
33
+ @association_validations ||= {}
34
+ end
35
+
36
+ private
37
+
38
+ def define_attributes(association_name)
39
+ attributes = association_name.to_s.classify.constantize.attribute_names
40
+ attributes.each { |attr| define_attribute(attr, association_name) }
41
+ end
42
+
43
+ def define_attribute(attr, assoc)
44
+ ActiveAttr::AttributeDefinition.new("#{assoc}_#{attr}").tap do |attribute_definition|
45
+ attribute_name = attribute_definition.name.to_s
46
+ attributes[attribute_name] = attribute_definition
47
+ end
48
+ define_attribute_reader(attr, assoc)
49
+ define_attribute_writer(attr, assoc)
50
+ end
51
+
52
+ def define_attribute_reader(attr, assoc)
53
+ define_method("#{assoc}_#{attr}") do
54
+ send(assoc.to_sym).send(attr.to_sym)
55
+ end
56
+ end
57
+
58
+ def define_attribute_writer(attr, assoc)
59
+ define_method("#{assoc}_#{attr}=") do |value|
60
+ send(assoc.to_sym).send("#{attr}=".to_sym, value)
61
+ end
62
+ end
63
+
64
+ def define_association(name, options = {})
65
+ association_names << name
66
+ should_validate = if options.has_key?(:validate)
67
+ options[:validate]
68
+ else
69
+ true
70
+ end
71
+ association_validations[name] = should_validate
72
+ send(:attr_accessor, name)
73
+ end
74
+ end
75
+
76
+ def initialize(attributes = {})
77
+ build_associations(attributes)
78
+ super
79
+ end
80
+
81
+ def persisted?
82
+ false
83
+ end
84
+
85
+ def association_names
86
+ self.class.association_names
87
+ end
88
+
89
+ def associations
90
+ Hash[association_names.map { |name| [name, send(name)] }]
91
+ end
92
+
93
+ def save
94
+ return false unless valid?
95
+
96
+ before_save
97
+ if defined? ActiveRecord
98
+ ActiveRecord::Base.transaction { persist_associations }
99
+ else
100
+ persist_associations
101
+ end
102
+ self
103
+ end
104
+
105
+ def before_save
106
+ # hook method
107
+ end
108
+
109
+ def method_missing(name, *args, &block)
110
+ assoc, assoc_method_name = association_for_method(name)
111
+ if assoc && assoc.respond_to?(assoc_method_name)
112
+ assoc.send(assoc_method_name, *args)
113
+ else
114
+ super
115
+ end
116
+ end
117
+
118
+ def respond_to_missing?(name, include_private = false)
119
+ assoc, assoc_method_name = association_for_method(name)
120
+ if assoc && assoc.respond_to?(assoc_method_name)
121
+ true
122
+ else
123
+ super
124
+ end
125
+ end
126
+
127
+ private
128
+
129
+ def build_associations(attributes)
130
+ association_names.each do |association_name|
131
+ association = build_association(association_name, attributes)
132
+ instance_variable_set("@#{association_name}".to_sym, association)
133
+ end
134
+ end
135
+
136
+ def build_association(name, attributes)
137
+ association_attributes = attributes_for_association(name, attributes)
138
+ klass = name.to_s.classify.constantize
139
+
140
+ if klass.respond_to? :primary_key
141
+ primary_key = attributes["#{name}_#{klass.primary_key}".to_sym]
142
+ end
143
+
144
+ if primary_key
145
+ object = klass.find primary_key
146
+ object.assign_attributes association_attributes
147
+ object
148
+ else
149
+ klass.new association_attributes
150
+ end
151
+ end
152
+
153
+ def attributes_for_association(association_name, attributes)
154
+ prefix = "#{association_name}_"
155
+ relevant_attributes = attributes.select { |k, v| k =~ /#{prefix}/ }
156
+ relevant_attributes.reduce({}) do |hash, (name, definition)|
157
+ new_key = name.to_s.sub(prefix, '').to_sym
158
+ hash[new_key] = definition
159
+ hash
160
+ end
161
+ end
162
+
163
+ def persist_associations
164
+ associations.each do |name, object|
165
+ object.save if object.respond_to? :save
166
+ end
167
+ end
168
+
169
+ def should_validate_assoc?(name)
170
+ method_name = assoc_validation_filter_method(name)
171
+ send(method_name)
172
+ end
173
+
174
+ def assoc_validation_filter_method(name)
175
+ filter = self.class.association_validations.fetch(name, true)
176
+ method_name = "should_validate_#{name}?".to_sym
177
+ case filter
178
+ when Symbol
179
+ filter
180
+ when TrueClass, FalseClass
181
+ self.class.send(:define_method, method_name) { filter }
182
+ method_name
183
+ else
184
+ raise NotImplementedError, "The attributes_for :validate option can only take a symbol, true, or false"
185
+ end
186
+ end
187
+
188
+ def association_for_method(name)
189
+ assoc_name = association_names.find { |an| name.match /^#{an}_/ }
190
+ if assoc_name
191
+ assoc_method_name = name.to_s.sub("#{assoc_name}_", '').to_sym
192
+ assoc = send(assoc_name)
193
+ return assoc, assoc_method_name
194
+ end
195
+ end
196
+ end
@@ -0,0 +1,124 @@
1
+ module ActiveModel
2
+ class Validator
3
+ # To make use of validators present on associated object, we need to be
4
+ # able to specify the original object that a given validator came from.
5
+ attr_accessor :target_object
6
+ end
7
+ end
8
+
9
+ module Forminate
10
+ # This module makes Forminate work with bcardarella's client_side_validations
11
+ # gem (https://github.com/bcardarella/client_side_validations).
12
+ #
13
+ # The majority of the methods contained here are overrides or alternative
14
+ # versions of methods in the client_side_validations gem. For the most part,
15
+ # they follow the shape and ideas of the original implementation, usually adding
16
+ # the ability to send messages to an associated object rather than the Forminate
17
+ # object itself.
18
+ module ClientSideValidations
19
+ extend ActiveSupport::Concern
20
+
21
+ # Where the magic happens. This overrides the client_side_validations gem's
22
+ # method of the same name, using Forminate's #association_validators rather
23
+ # than ActiveModel's _validators method. It still calls super so that it
24
+ # includes validations placed directly on the Forminate object.
25
+ def client_side_validation_hash(force = nil)
26
+ hash = super
27
+ assoc_hash = association_validators.reduce({}) do |assoc_hash, (attr, validators)|
28
+ unless [nil, :block].include?(attr)
29
+
30
+ validator_hash = validators.reduce(Hash.new { |h,k| h[k] = []}) do |kind_hash, validator|
31
+ model = validator.target_object
32
+ model_pattern = /^#{model.class.name.underscore}_/
33
+ target_attr = attr.to_s.sub(model_pattern, '').to_sym
34
+
35
+ if force.is_a? Hash
36
+ relevant_force = force.select { |k, v| k.to_s.match model_pattern }
37
+ assoc_force = relevant_force.reduce({}) do |assoc_force, (key, value)|
38
+ key = key.to_s.sub(model_pattern, '').to_sym
39
+ assoc_force.merge({ key => value })
40
+ end
41
+ else
42
+ assoc_force = force
43
+ end
44
+
45
+ if _can_use_for_client_side_validation?(model, target_attr, validator, assoc_force)
46
+ if client_side_hash = validator.client_side_hash(model, target_attr, extract_force_option(target_attr, assoc_force))
47
+ kind_hash[validator.kind] << client_side_hash.except(:on, :if, :unless)
48
+ end
49
+ end
50
+
51
+ kind_hash
52
+ end
53
+
54
+ if validator_hash.present?
55
+ assoc_hash.merge!(attr => validator_hash)
56
+ else
57
+ assoc_hash
58
+ end
59
+ else
60
+ assoc_hash
61
+ end
62
+ end
63
+ hash.merge assoc_hash
64
+ end
65
+
66
+ private
67
+
68
+ # Constructs the associated validators for use with client_side_validations.
69
+ # This is meant to mimic ActiveModel's #_validators method that the
70
+ # client_side_validations gem relies on for construction the client side
71
+ # validation hash.
72
+ def association_validators
73
+ associations.reduce({}) do |assoc_validators, (name, object)|
74
+ if should_validate_assoc?(name) && object.respond_to?(:_validators)
75
+ object._validators.each do |attr, validators|
76
+ new_validators = validators.reduce([]) do |new_validators, validator|
77
+ new_validator = validator.dup
78
+ new_validator.target_object = object
79
+ new_validators << new_validator
80
+ end
81
+ assoc_validators["#{name}_#{attr}".to_sym] = new_validators
82
+ end
83
+ end
84
+ assoc_validators
85
+ end
86
+ end
87
+
88
+ # Alternative version of the client_side_validation gem's
89
+ # #can_use_for_client_side_validation? method, allowing the target 'model'
90
+ # to be passed in as an argument.
91
+ def _can_use_for_client_side_validation?(model, attr, validator, force)
92
+ if validator_turned_off?(attr, validator, force)
93
+ result = false
94
+ else
95
+ result = ((model.respond_to?(:new_record?) && validator.options[:on] == (model.new_record? ? :create : :update)) || validator.options[:on].nil?)
96
+ result = result && validator.kind != :block
97
+
98
+ if validator.options[:if] || validator.options[:unless]
99
+ if result = can_force_validator?(attr, validator, force)
100
+ if validator.options[:if]
101
+ result = result && _run_conditional(model, validator.options[:if])
102
+ end
103
+ if validator.options[:unless]
104
+ result = result && !_run_conditional(model, validator.options[:unless])
105
+ end
106
+ end
107
+ end
108
+ end
109
+
110
+ result
111
+ end
112
+
113
+ # Alternative version of the client_side_validation gem's #run_conditional
114
+ # method, allowing the target 'model' to be passed in as an argument.
115
+ def _run_conditional(model, method_name_or_proc)
116
+ case method_name_or_proc
117
+ when Proc
118
+ method_name_or_proc.call(model)
119
+ else
120
+ model.send(method_name_or_proc)
121
+ end
122
+ end
123
+ end
124
+ end
@@ -0,0 +1,70 @@
1
+ require 'spec_helper'
2
+ require 'client_side_validations'
3
+ require 'forminate/client_side_validations'
4
+
5
+ describe Forminate::ClientSideValidations do
6
+ subject(:model) { model_class.new }
7
+
8
+ let :model_class do
9
+ Class.new do
10
+ include Forminate
11
+ include Forminate::ClientSideValidations
12
+
13
+ attribute :total
14
+ attribute :tax
15
+
16
+ attributes_for :dummy_user
17
+ attributes_for :dummy_book, :validate => false
18
+ attributes_for :dummy_credit_card, :validate => :require_credit_card?
19
+
20
+ validates_numericality_of :total
21
+
22
+ def self.name
23
+ "Cart"
24
+ end
25
+
26
+ def calculate_total
27
+ self.total = dummy_book.price || 0.0
28
+ end
29
+
30
+ def require_credit_card?
31
+ dummy_book.price && dummy_book.price > 0.0
32
+ end
33
+ end
34
+ end
35
+
36
+ describe "#client_side_validation_hash" do
37
+ it "constructs a hash of validations and messages for use with the client_side_validations gem" do
38
+ expected_hash = {
39
+ total: {
40
+ numericality: [{ messages: { numericality: "is not a number" } }]
41
+ },
42
+ dummy_user_email: {
43
+ presence: [{ message: "can't be blank" }]
44
+ }
45
+ }
46
+ expect(model.client_side_validation_hash).to eq(expected_hash)
47
+ expected_hash = {
48
+ total: {
49
+ numericality: [{ messages: { numericality: "is not a number" } }]
50
+ },
51
+ dummy_user_email: {
52
+ presence: [{ message: "can't be blank" }]
53
+ },
54
+ dummy_credit_card_number: {
55
+ presence: [{ message: "can't be blank" }],
56
+ length: [{
57
+ messages: {
58
+ minimum: "is too short (minimum is 12 characters)",
59
+ maximum: "is too long (maximum is 19 characters)"
60
+ },
61
+ minimum: 12,
62
+ maximum: 19
63
+ }]
64
+ },
65
+ }
66
+ model.dummy_book_price = 12.95
67
+ expect(model.client_side_validation_hash).to eq(expected_hash)
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,179 @@
1
+ require 'spec_helper'
2
+
3
+ describe Forminate do
4
+ subject(:model) { model_class.new }
5
+
6
+ let :model_class do
7
+ Class.new do
8
+ include Forminate
9
+
10
+ attribute :total
11
+ attribute :tax
12
+
13
+ attributes_for :dummy_user
14
+ attributes_for :dummy_book, :validate => false
15
+ attributes_for :dummy_credit_card, :validate => :require_credit_card?
16
+
17
+ validates_numericality_of :total
18
+
19
+ def self.name
20
+ "Cart"
21
+ end
22
+
23
+ def calculate_total
24
+ self.total = dummy_book.price || 0.0
25
+ end
26
+
27
+ def require_credit_card?
28
+ dummy_book.price && dummy_book.price > 0.0
29
+ end
30
+ end
31
+ end
32
+
33
+ describe ".attributes_for" do
34
+ it "adds a reader method for each attribute of the associated model" do
35
+ expect(model.respond_to?(:dummy_user_first_name)).to be_true
36
+ end
37
+
38
+ it "adds reader and writer methods for each attribute of the associated model" do
39
+ model.dummy_user_first_name = 'Mo'
40
+ expect(model.dummy_user_first_name).to eq('Mo')
41
+ end
42
+
43
+ it "adds reader methods for each associated model" do
44
+ expect(model.dummy_user).to be_an_instance_of(DummyUser)
45
+ end
46
+
47
+ it "adds the association to the list of association names" do
48
+ expect(model_class.association_names).to include(:dummy_user)
49
+ end
50
+
51
+ describe ":validate option" do
52
+ it "validates associated object based on true or false" do
53
+ model.calculate_total
54
+ expect(model.valid?).to be_false
55
+ model.dummy_user_email = 'bob@example.com'
56
+ expect(model.valid?).to be_true
57
+ end
58
+
59
+ it "validates associated objects based on a method name (as a symbol) that evaluates to true or false" do
60
+ model.calculate_total
61
+ model.dummy_user_email = 'bob@example.com'
62
+ expect(model.valid?).to be_true
63
+ model.dummy_book_price = 12.95
64
+ expect(model.valid?).to be_false
65
+ model.dummy_credit_card_number = 4242424242424242
66
+ expect(model.valid?).to be_true
67
+ end
68
+ end
69
+ end
70
+
71
+ describe ".attribute_names" do
72
+ it "includes the names of its own attributes and the attributes of associated models" do
73
+ expected_attributes = [
74
+ "total",
75
+ "tax",
76
+ "dummy_user_first_name",
77
+ "dummy_user_last_name",
78
+ "dummy_user_email",
79
+ "dummy_book_title",
80
+ "dummy_book_price",
81
+ "dummy_credit_card_number",
82
+ "dummy_credit_card_expiration",
83
+ "dummy_credit_card_cvv",
84
+ ]
85
+ expect(model_class.attribute_names).to eq(expected_attributes)
86
+ end
87
+ end
88
+
89
+ describe ".association_names" do
90
+ it "includes the names of associated models" do
91
+ expect(model_class.association_names).to eq([:dummy_user, :dummy_book, :dummy_credit_card])
92
+ end
93
+ end
94
+
95
+ describe ".association_validations" do
96
+ it "includes the names and conditions of association validations" do
97
+ expect(model_class.association_validations).to eq({ :dummy_user => true, :dummy_book => false, :dummy_credit_card => :require_credit_card? })
98
+ end
99
+ end
100
+
101
+ describe "#initialize" do
102
+ it "builds associated objects and creates reader methods" do
103
+ expect(model.dummy_user).to be_an_instance_of(DummyUser)
104
+ end
105
+
106
+ it "creates writer methods for associated objects" do
107
+ new_dummy_user = DummyUser.new(first_name: 'Mo')
108
+ expect(model.dummy_user).to_not be(new_dummy_user)
109
+ model.dummy_user = new_dummy_user
110
+ expect(model.dummy_user).to be(new_dummy_user)
111
+ end
112
+
113
+ it "sets association attributes based on an options hash" do
114
+ new_model = model_class.new(dummy_user_first_name: 'Mo', dummy_user_last_name: 'Lawson')
115
+ expect(new_model.dummy_user_first_name).to eq('Mo')
116
+ expect(new_model.dummy_user_last_name).to eq('Lawson')
117
+ end
118
+
119
+ it "sets attributes based on an options hash" do
120
+ new_model = model_class.new(total: 21.49)
121
+ expect(new_model.total).to eq(21.49)
122
+ end
123
+ end
124
+
125
+ describe "#association_names" do
126
+ it "delegates to self.association_names" do
127
+ expect(model.association_names).to eq(model_class.association_names)
128
+ end
129
+ end
130
+
131
+ describe "#associations" do
132
+ it "returns a hash of association names and associated objects" do
133
+ expect(model.associations[:dummy_user]).to be_an_instance_of(DummyUser)
134
+ end
135
+ end
136
+
137
+ describe "#save" do
138
+ context "object is valid" do
139
+ it "saves associations and returns self" do
140
+ model.dummy_user_email = 'bob@example.com'
141
+ model.calculate_total
142
+ DummyUser.any_instance.should_receive(:save)
143
+ expect(model.save).to eq(model)
144
+ end
145
+ end
146
+
147
+ context "object is not valid" do
148
+ it "does not save associations and returns false" do
149
+ DummyUser.any_instance.should_not_receive(:save)
150
+ expect(model.save).to be_false
151
+ end
152
+ end
153
+ end
154
+
155
+ context "setting an attribute using the attribute name" do
156
+ it "reflects the change on the associated object" do
157
+ model.dummy_user_first_name = 'Mo'
158
+ expect(model.dummy_user.first_name).to eq('Mo')
159
+ end
160
+ end
161
+
162
+ context "setting an attribute using the association's attribute" do
163
+ it "reflects the change on the attribute name" do
164
+ model.dummy_user.last_name = 'Lawson'
165
+ expect(model.dummy_user_last_name).to eq('Lawson')
166
+ end
167
+ end
168
+
169
+ it "delegates to attr_accessors of associated objects" do
170
+ model.dummy_user_full_name = 'Mo Lawson'
171
+ expect(model.dummy_user.full_name).to eq('Mo Lawson')
172
+ expect(model.dummy_user_full_name).to eq('Mo Lawson')
173
+ end
174
+
175
+ it "inherits the validations of its associated objects" do
176
+ model.valid?
177
+ expect(model.errors.full_messages).to eq(["Dummy user email can't be blank", "Total is not a number"])
178
+ end
179
+ end
@@ -0,0 +1,5 @@
1
+ require 'bundler/setup'
2
+ require 'forminate'
3
+
4
+ # Requires supporting files in spec/support/
5
+ Dir["#{File.dirname(__FILE__)}/support/*.rb"].each { |file| require file }
@@ -0,0 +1,10 @@
1
+ require 'active_attr'
2
+
3
+ class DummyBook
4
+ include ActiveAttr::Model
5
+
6
+ attribute :title
7
+ attribute :price
8
+
9
+ validates_presence_of :title
10
+ end
@@ -0,0 +1,13 @@
1
+ require 'active_attr'
2
+
3
+ class DummyCreditCard
4
+ include ActiveAttr::Model
5
+
6
+ attribute :number
7
+ attribute :expiration
8
+ attribute :cvv
9
+
10
+ validates_presence_of :number
11
+ validates_length_of :number, in: 12..19
12
+ end
13
+
@@ -0,0 +1,17 @@
1
+ require 'active_attr'
2
+
3
+ class DummyUser
4
+ include ActiveAttr::Model
5
+
6
+ attribute :first_name
7
+ attribute :last_name
8
+ attribute :email
9
+
10
+ attr_accessor :full_name
11
+
12
+ validates_presence_of :email
13
+
14
+ def save
15
+ # fake a persisted model
16
+ end
17
+ end
metadata ADDED
@@ -0,0 +1,158 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: forminate
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Mo Lawson
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2013-08-06 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: active_attr
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ~>
18
+ - !ruby/object:Gem::Version
19
+ version: '0.8'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ~>
25
+ - !ruby/object:Gem::Version
26
+ version: '0.8'
27
+ - !ruby/object:Gem::Dependency
28
+ name: activesupport
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - '>='
32
+ - !ruby/object:Gem::Version
33
+ version: 3.0.2
34
+ - - <
35
+ - !ruby/object:Gem::Version
36
+ version: '4.1'
37
+ type: :runtime
38
+ prerelease: false
39
+ version_requirements: !ruby/object:Gem::Requirement
40
+ requirements:
41
+ - - '>='
42
+ - !ruby/object:Gem::Version
43
+ version: 3.0.2
44
+ - - <
45
+ - !ruby/object:Gem::Version
46
+ version: '4.1'
47
+ - !ruby/object:Gem::Dependency
48
+ name: client_side_validations
49
+ requirement: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ~>
52
+ - !ruby/object:Gem::Version
53
+ version: '3.2'
54
+ type: :runtime
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ~>
59
+ - !ruby/object:Gem::Version
60
+ version: '3.2'
61
+ - !ruby/object:Gem::Dependency
62
+ name: bundler
63
+ requirement: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ~>
66
+ - !ruby/object:Gem::Version
67
+ version: '1.3'
68
+ type: :development
69
+ prerelease: false
70
+ version_requirements: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - ~>
73
+ - !ruby/object:Gem::Version
74
+ version: '1.3'
75
+ - !ruby/object:Gem::Dependency
76
+ name: rake
77
+ requirement: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - '>='
80
+ - !ruby/object:Gem::Version
81
+ version: '0'
82
+ type: :development
83
+ prerelease: false
84
+ version_requirements: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - '>='
87
+ - !ruby/object:Gem::Version
88
+ version: '0'
89
+ - !ruby/object:Gem::Dependency
90
+ name: rspec
91
+ requirement: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - ~>
94
+ - !ruby/object:Gem::Version
95
+ version: '2.11'
96
+ type: :development
97
+ prerelease: false
98
+ version_requirements: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - ~>
101
+ - !ruby/object:Gem::Version
102
+ version: '2.11'
103
+ description: Form objects for Rails applications
104
+ email:
105
+ - moklawson@gmail.com
106
+ executables: []
107
+ extensions: []
108
+ extra_rdoc_files: []
109
+ files:
110
+ - .gitignore
111
+ - .travis.yml
112
+ - Gemfile
113
+ - LICENSE.txt
114
+ - README.md
115
+ - Rakefile
116
+ - forminate.gemspec
117
+ - lib/forminate.rb
118
+ - lib/forminate/client_side_validations.rb
119
+ - lib/forminate/version.rb
120
+ - spec/lib/forminate/client_side_validations.rb
121
+ - spec/lib/forminate/client_side_validations_spec.rb
122
+ - spec/lib/forminate_spec.rb
123
+ - spec/spec_helper.rb
124
+ - spec/support/dummy_book.rb
125
+ - spec/support/dummy_credit_card.rb
126
+ - spec/support/dummy_user.rb
127
+ homepage: https://github.com/molawson/forminate
128
+ licenses:
129
+ - MIT
130
+ metadata: {}
131
+ post_install_message:
132
+ rdoc_options: []
133
+ require_paths:
134
+ - lib
135
+ required_ruby_version: !ruby/object:Gem::Requirement
136
+ requirements:
137
+ - - '>='
138
+ - !ruby/object:Gem::Version
139
+ version: '0'
140
+ required_rubygems_version: !ruby/object:Gem::Requirement
141
+ requirements:
142
+ - - '>='
143
+ - !ruby/object:Gem::Version
144
+ version: '0'
145
+ requirements: []
146
+ rubyforge_project:
147
+ rubygems_version: 2.0.3
148
+ signing_key:
149
+ specification_version: 4
150
+ summary: Create form objects from multiple Active Record and/or ActiveAttr models.
151
+ test_files:
152
+ - spec/lib/forminate/client_side_validations.rb
153
+ - spec/lib/forminate/client_side_validations_spec.rb
154
+ - spec/lib/forminate_spec.rb
155
+ - spec/spec_helper.rb
156
+ - spec/support/dummy_book.rb
157
+ - spec/support/dummy_credit_card.rb
158
+ - spec/support/dummy_user.rb