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 +7 -0
- data/.gitignore +17 -0
- data/.travis.yml +6 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +32 -0
- data/Rakefile +5 -0
- data/forminate.gemspec +28 -0
- data/lib/forminate/client_side_validations.rb +124 -0
- data/lib/forminate/version.rb +3 -0
- data/lib/forminate.rb +196 -0
- data/spec/lib/forminate/client_side_validations.rb +124 -0
- data/spec/lib/forminate/client_side_validations_spec.rb +70 -0
- data/spec/lib/forminate_spec.rb +179 -0
- data/spec/spec_helper.rb +5 -0
- data/spec/support/dummy_book.rb +10 -0
- data/spec/support/dummy_credit_card.rb +13 -0
- data/spec/support/dummy_user.rb +17 -0
- metadata +158 -0
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
data/.travis.yml
ADDED
data/Gemfile
ADDED
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
|
+
[](https://travis-ci.org/molawson/forminate)
|
4
|
+
[](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
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
|
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
|
data/spec/spec_helper.rb
ADDED
@@ -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
|