forminate 0.1.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.
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