morpheus 0.4.0 → 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.rspec +1 -1
- data/CHANGELOG.md +17 -0
- data/Gemfile +1 -1
- data/lib/morpheus.rb +3 -23
- data/lib/morpheus/base.rb +11 -11
- data/lib/morpheus/client.rb +10 -0
- data/lib/morpheus/client/associations.rb +6 -6
- data/lib/morpheus/client/cached_request_formatter.rb +20 -0
- data/lib/morpheus/client/log_subscriber.rb +2 -28
- data/lib/morpheus/client/railtie.rb +4 -4
- data/lib/morpheus/client/request_formatter.rb +50 -0
- data/lib/morpheus/client/uncached_request_formatter.rb +40 -0
- data/lib/morpheus/configuration.rb +30 -9
- data/lib/morpheus/mixins.rb +16 -0
- data/lib/morpheus/mixins/associations.rb +39 -37
- data/lib/morpheus/mixins/associations/association.rb +112 -0
- data/lib/morpheus/mixins/associations/belongs_to_association.rb +47 -0
- data/lib/morpheus/mixins/associations/has_many_association.rb +72 -0
- data/lib/morpheus/mixins/associations/has_one_association.rb +48 -0
- data/lib/morpheus/mixins/attributes.rb +97 -95
- data/lib/morpheus/mixins/conversion.rb +16 -14
- data/lib/morpheus/mixins/filtering.rb +13 -11
- data/lib/morpheus/mixins/finders.rb +47 -45
- data/lib/morpheus/mixins/introspection.rb +15 -13
- data/lib/morpheus/mixins/persistence.rb +32 -30
- data/lib/morpheus/mixins/reflections.rb +17 -15
- data/lib/morpheus/mixins/request_handling.rb +27 -25
- data/lib/morpheus/mixins/response_parsing.rb +10 -8
- data/lib/morpheus/mixins/url_support.rb +27 -25
- data/lib/morpheus/reflection.rb +5 -2
- data/lib/morpheus/type_caster.rb +1 -1
- data/lib/morpheus/version.rb +1 -1
- data/morpheus.gemspec +2 -2
- data/spec/dummy/app/resources/book.rb +1 -1
- data/spec/morpheus/base_spec.rb +35 -35
- data/spec/morpheus/client/cached_request_formatter_spec.rb +28 -0
- data/spec/morpheus/client/log_subscriber_spec.rb +53 -10
- data/spec/morpheus/client/request_formatter_spec.rb +5 -0
- data/spec/morpheus/client/uncached_request_formatter_spec.rb +29 -0
- data/spec/morpheus/configuration_spec.rb +49 -11
- data/spec/morpheus/{associations → mixins/associations}/association_spec.rb +1 -1
- data/spec/morpheus/{associations → mixins/associations}/belongs_to_association_spec.rb +1 -1
- data/spec/morpheus/{associations → mixins/associations}/has_many_association_spec.rb +1 -1
- data/spec/morpheus/{associations → mixins/associations}/has_one_association_spec.rb +1 -1
- data/spec/morpheus/mixins/associations_spec.rb +1 -1
- data/spec/morpheus/mixins/attributes_spec.rb +27 -6
- data/spec/morpheus/mixins/conversion_spec.rb +1 -1
- data/spec/morpheus/mixins/filtering_spec.rb +2 -2
- data/spec/morpheus/mixins/finders_spec.rb +1 -1
- data/spec/morpheus/mixins/introspection_spec.rb +1 -1
- data/spec/morpheus/mixins/persistence_spec.rb +1 -1
- data/spec/morpheus/mixins/reflections_spec.rb +1 -1
- data/spec/morpheus/mixins/request_handling_spec.rb +2 -2
- data/spec/morpheus/mixins/response_parsing_spec.rb +2 -2
- data/spec/morpheus/mixins/url_support_spec.rb +2 -2
- data/spec/morpheus/response_parser_spec.rb +5 -1
- data/spec/regressions/sorting_resources_spec.rb +119 -0
- data/spec/spec_helper.rb +3 -2
- metadata +159 -87
- data/lib/morpheus/associations/association.rb +0 -110
- data/lib/morpheus/associations/belongs_to_association.rb +0 -45
- data/lib/morpheus/associations/has_many_association.rb +0 -70
- data/lib/morpheus/associations/has_one_association.rb +0 -46
@@ -0,0 +1,112 @@
|
|
1
|
+
# The Association class serves as the basis for associating resources.
|
2
|
+
# Resources have an association DSL that follows that of ActiveRecord:
|
3
|
+
#
|
4
|
+
# has_many :automobiles
|
5
|
+
#
|
6
|
+
# has_one :car
|
7
|
+
#
|
8
|
+
# belongs_to :car_club
|
9
|
+
#
|
10
|
+
# This class acts as a proxy that will allow for lazy evaluation of an
|
11
|
+
# association. The proxy waits to make the request for the object until
|
12
|
+
# a method on the association is called. When this happens, because the
|
13
|
+
# method does not exist on the proxy object, method_missing will be
|
14
|
+
# called, the target object will be loaded, and then the method will be
|
15
|
+
# called on the loaded target.
|
16
|
+
module Morpheus
|
17
|
+
module Mixins
|
18
|
+
module Associations
|
19
|
+
class Association < ActiveSupport::BasicObject
|
20
|
+
|
21
|
+
# Associations can be loaded with several options.
|
22
|
+
def initialize(owner, association, settings = {})
|
23
|
+
# @owner stores the class that the association exists on.
|
24
|
+
@owner = owner
|
25
|
+
|
26
|
+
# @association stores the associated class, as named in the association
|
27
|
+
# method (i.e. :automobile, :car, :car_club)
|
28
|
+
@association = association
|
29
|
+
|
30
|
+
# @target stores the loaded object. It is not typically accessed directly,
|
31
|
+
# but instead should be accessed through the loaded_target method.
|
32
|
+
@target = settings[:target]
|
33
|
+
|
34
|
+
@filters = settings[:filters] || []
|
35
|
+
|
36
|
+
@includes = []
|
37
|
+
|
38
|
+
# @options holds the chosen options for the association. Several of these
|
39
|
+
# options are set in the subclass' initializer.
|
40
|
+
@options = settings[:options] || {}
|
41
|
+
|
42
|
+
# In some cases, the association name will not match that of the class
|
43
|
+
# that should be instantiated when it is invoked. Here, we can specify
|
44
|
+
# that this association uses a specified class as its target. When the
|
45
|
+
# request is made for the association, this class will be used to
|
46
|
+
# instantiate this object or collection.
|
47
|
+
@options[:class_name] = settings[:options][:class_name] || @association.to_s.classify
|
48
|
+
end
|
49
|
+
|
50
|
+
# The proxy implements a few methods that need to be delegated to the target
|
51
|
+
# so that they will work as expected.
|
52
|
+
def id
|
53
|
+
loaded_target.id
|
54
|
+
end
|
55
|
+
|
56
|
+
def nil?
|
57
|
+
loaded_target.nil?
|
58
|
+
end
|
59
|
+
|
60
|
+
def to_param
|
61
|
+
loaded_target.to_param
|
62
|
+
end
|
63
|
+
|
64
|
+
def try(method, *args, &block)
|
65
|
+
loaded_target.try(method, *args, &block)
|
66
|
+
end
|
67
|
+
|
68
|
+
def includes(*associations)
|
69
|
+
associations.each do |association|
|
70
|
+
@includes << association unless @includes.include?(association)
|
71
|
+
end
|
72
|
+
self
|
73
|
+
end
|
74
|
+
|
75
|
+
# This is left to be implemented by the subclasses as it will operate
|
76
|
+
# differently in each case.
|
77
|
+
def load_target!
|
78
|
+
end
|
79
|
+
|
80
|
+
private
|
81
|
+
|
82
|
+
# The loaded_target method holds a cached version of the loaded target.
|
83
|
+
# This method is used to access the proxied object. Since a request will
|
84
|
+
# be made when a method is invoked on the object, and this can happen
|
85
|
+
# very often, we are caching the target here, so that only a single
|
86
|
+
# request will be made.
|
87
|
+
def loaded_target
|
88
|
+
@target ||= load_target!
|
89
|
+
if ::Array === @target && !@filters.empty?
|
90
|
+
@filters.uniq.inject(@target.dup) do |target, filter|
|
91
|
+
filter.call(target)
|
92
|
+
end
|
93
|
+
else
|
94
|
+
@target
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
# The method_missing hook will be called when methods that do not exist
|
99
|
+
# on the proxy object are invoked. This is the point at which the proxied
|
100
|
+
# object is loaded, if it has not been loaded already.
|
101
|
+
def method_missing(m, *args, &block)
|
102
|
+
if filter = @association_class.find_filter(m)
|
103
|
+
with_filter(filter)
|
104
|
+
else
|
105
|
+
loaded_target.send(m, *args, &block)
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
module Morpheus
|
2
|
+
module Mixins
|
3
|
+
module Associations
|
4
|
+
class BelongsToAssociation < Association
|
5
|
+
|
6
|
+
# The initializer calls out to the superclass' initializer, then will set the
|
7
|
+
# options particular to itself.
|
8
|
+
def initialize(owner, association, settings = {})
|
9
|
+
super
|
10
|
+
|
11
|
+
# The foreign key is used to generate the url for a request. The default uses
|
12
|
+
# the association name with an '_id' suffix as the generated key. For example,
|
13
|
+
# belongs_to :school, will have the foreign key :school_id.
|
14
|
+
@options[:foreign_key] ||= "#{@association.to_s}_id".to_sym
|
15
|
+
|
16
|
+
# The primary key defaults to :id.
|
17
|
+
@options[:primary_key] ||= :id
|
18
|
+
|
19
|
+
# Associations can be marked as polymorphic. These associations will use
|
20
|
+
# the returned type to instantiate the associated object.
|
21
|
+
@options[:polymorphic] = settings[:options][:polymorphic] || false
|
22
|
+
|
23
|
+
# @association_class stores the class of the association, constantized
|
24
|
+
# from the named association (i.e. Automobile, Car, CarClub)
|
25
|
+
@association_class = @options[:class_name].constantize
|
26
|
+
end
|
27
|
+
|
28
|
+
def with_filter(filter)
|
29
|
+
BelongsToAssociation.new(@owner, @association, :target => @target, :filters => @filters.dup.push(filter), :options => @options)
|
30
|
+
end
|
31
|
+
|
32
|
+
# When loading the target, the association will only be loaded if the foreign_key
|
33
|
+
# has been set. Additionally, the class used to find the record will be inferred
|
34
|
+
# by calling the method which is the name of the association with a '_type' suffix.
|
35
|
+
# Alternatively, the class name can be set by using the :class_name option.
|
36
|
+
def load_target!
|
37
|
+
if association_id = @owner.send(@options[:foreign_key])
|
38
|
+
polymorphic_class = @options[:polymorphic] ? @owner.send("#{@association}_type".to_sym).constantize : @options[:class_name].constantize
|
39
|
+
attributes = [UrlBuilder.belongs_to(polymorphic_class, association_id), nil, { :id => association_id }]
|
40
|
+
polymorphic_class.find(association_id)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
module Morpheus
|
2
|
+
module Mixins
|
3
|
+
module Associations
|
4
|
+
class HasManyAssociation < Association
|
5
|
+
|
6
|
+
# The initializer calls out to the superclass' initializer and then
|
7
|
+
# sets the options particular to itself.
|
8
|
+
def initialize(owner, association, settings = {})
|
9
|
+
super
|
10
|
+
|
11
|
+
# The foreign key is used to generate the url for the association
|
12
|
+
# request when the association is transformed into a relation.
|
13
|
+
# The default is to use the class of the owner object with an '_id'
|
14
|
+
# suffix.
|
15
|
+
@options[:foreign_key] ||= "#{@owner.class.to_s.underscore}_id".to_sym
|
16
|
+
|
17
|
+
# The primary key is used in the generated url for the target. It
|
18
|
+
# defaults to :id.
|
19
|
+
@options[:primary_key] ||= :id
|
20
|
+
|
21
|
+
# @association_class stores the class of the association, constantized
|
22
|
+
# from the named association (i.e. Automobile, Car, CarClub)
|
23
|
+
@association_class = @options[:class_name].constantize
|
24
|
+
end
|
25
|
+
|
26
|
+
def with_filter(filter)
|
27
|
+
HasManyAssociation.new(@owner, @association, :target => @target, :filters => @filters.dup.push(filter), :options => @options)
|
28
|
+
end
|
29
|
+
|
30
|
+
# When loading the target, the primary key is first checked. If the
|
31
|
+
# key is nil, then an empty array is returned. Otherwise, the target
|
32
|
+
# is requested at the generated url. For a has_many :meetings
|
33
|
+
# association on a class called Course, the generated url might look
|
34
|
+
# like this: /meetings?course_id=1, where the 1 is the primary key.
|
35
|
+
def load_target!
|
36
|
+
if primary_key = @owner.send(@options[:primary_key])
|
37
|
+
Relation.new(@association.to_s.classify.constantize).where(@options[:foreign_key] => primary_key).all
|
38
|
+
else
|
39
|
+
[]
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
# The where, limit, and page methods delegate to a Relation object.
|
44
|
+
# The association generates a relation, and then calls the very
|
45
|
+
# same where, limit, or page method on that relation object.
|
46
|
+
def where(query_attributes)
|
47
|
+
transform_association_into_relation.where(query_attributes)
|
48
|
+
end
|
49
|
+
|
50
|
+
def limit(amount)
|
51
|
+
transform_association_into_relation.limit(amount)
|
52
|
+
end
|
53
|
+
|
54
|
+
def page(page_number)
|
55
|
+
transform_association_into_relation.page(page_number)
|
56
|
+
end
|
57
|
+
|
58
|
+
def transform_association_into_relation
|
59
|
+
Relation.new(@association.to_s.classify.constantize).where(@options[:foreign_key] => @owner.send(@options[:primary_key]))
|
60
|
+
end
|
61
|
+
private :transform_association_into_relation
|
62
|
+
|
63
|
+
# The append operator is used to append new resources to the association.
|
64
|
+
def <<(one_of_many)
|
65
|
+
@target ||= []
|
66
|
+
@target << one_of_many
|
67
|
+
end
|
68
|
+
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
module Morpheus
|
2
|
+
module Mixins
|
3
|
+
module Associations
|
4
|
+
class HasOneAssociation < Association
|
5
|
+
|
6
|
+
# The initializer calls out to the superclass' initializer and then
|
7
|
+
# sets the options particular to itself.
|
8
|
+
def initialize(owner, association, settings = {})
|
9
|
+
super
|
10
|
+
|
11
|
+
# The foreign key is used to generate the url for the association
|
12
|
+
# request when the association is transformed into a relation.
|
13
|
+
# The default is to use the class of the owner object with an '_id'
|
14
|
+
# suffix.
|
15
|
+
@options[:foreign_key] ||= "#{@owner.class.to_s.underscore}_id".to_sym
|
16
|
+
|
17
|
+
# The primary key is used in the generated url for the target. It
|
18
|
+
# defaults to :id.
|
19
|
+
@options[:primary_key] ||= :id
|
20
|
+
|
21
|
+
# @association_class stores the class of the association, constantized
|
22
|
+
# from the named association (i.e. Automobile, Car, CarClub)
|
23
|
+
if @options[:class_name]
|
24
|
+
@association_class = @options[:class_name].constantize
|
25
|
+
else
|
26
|
+
@association_class = @association
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def with_filter(filter)
|
31
|
+
HasOneAssociation.new(@owner, @association, :target => @target, :filters => @filters.dup.push(filter), :options => @options)
|
32
|
+
end
|
33
|
+
|
34
|
+
# When loading the target, the primary key is first checked. If the
|
35
|
+
# key is nil, then an nil is returned. Otherwise, the target
|
36
|
+
# is requested at the generated url. For a has_one :meeting
|
37
|
+
# association on a class called Course, the generated url might look
|
38
|
+
# like this: /meetings?course_id=1, where the 1 is the primary key.
|
39
|
+
def load_target!
|
40
|
+
if primary_key = @owner.send(@options[:primary_key])
|
41
|
+
Relation.new(@association_class.to_s.classify.constantize).where(@options[:foreign_key] => primary_key).limit(1).first
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -1,132 +1,134 @@
|
|
1
1
|
module Morpheus
|
2
|
-
module
|
2
|
+
module Mixins
|
3
|
+
module Attributes
|
3
4
|
|
4
|
-
|
5
|
-
|
6
|
-
end
|
7
|
-
|
8
|
-
module ClassMethods
|
9
|
-
|
10
|
-
def property(name, typecast_class = nil)
|
11
|
-
typecast_attributes.store(name.to_sym, typecast_class)
|
12
|
-
default_attributes.store(name.to_sym, nil)
|
13
|
-
|
14
|
-
define_attribute_accessor(name)
|
5
|
+
def self.included(base)
|
6
|
+
base.extend(ClassMethods)
|
15
7
|
end
|
16
8
|
|
17
|
-
|
18
|
-
@attributes ||= HashWithIndifferentAccess.new(superclass.respond_to?(:default_attributes) ? superclass.default_attributes : {})
|
19
|
-
end
|
9
|
+
module ClassMethods
|
20
10
|
|
21
|
-
|
22
|
-
|
23
|
-
|
11
|
+
def property(name, typecast_class = nil)
|
12
|
+
typecast_attributes.store(name.to_sym, typecast_class)
|
13
|
+
default_attributes.store(name.to_sym, nil)
|
24
14
|
|
25
|
-
|
26
|
-
|
27
|
-
end
|
15
|
+
define_attribute_accessor(name)
|
16
|
+
end
|
28
17
|
|
29
|
-
|
30
|
-
|
31
|
-
define_method("#{method}=") do |value|
|
32
|
-
send("#{method}_will_change!")
|
33
|
-
update_attribute(method, value)
|
18
|
+
def default_attributes
|
19
|
+
@attributes ||= HashWithIndifferentAccess.new(superclass.respond_to?(:default_attributes) ? superclass.default_attributes : {})
|
34
20
|
end
|
35
21
|
|
36
|
-
|
37
|
-
|
22
|
+
def typecast_attributes
|
23
|
+
@typecast_attributes ||= HashWithIndifferentAccess.new(superclass.respond_to?(:default_attributes) ? superclass.default_attributes : {})
|
38
24
|
end
|
39
25
|
|
40
|
-
|
41
|
-
|
42
|
-
|
26
|
+
def attribute_defined?(attribute)
|
27
|
+
default_attributes.keys.include?(attribute.to_s)
|
28
|
+
end
|
43
29
|
|
44
|
-
|
30
|
+
def define_attribute_accessor(method)
|
31
|
+
method = method.to_s.delete('=')
|
32
|
+
define_method("#{method}=") do |value|
|
33
|
+
send("#{method}_will_change!")
|
34
|
+
update_attribute(method, value)
|
35
|
+
end
|
45
36
|
|
46
|
-
|
47
|
-
|
48
|
-
|
37
|
+
define_method(method) do
|
38
|
+
attributes[method]
|
39
|
+
end
|
49
40
|
|
50
|
-
|
51
|
-
|
52
|
-
|
41
|
+
define_attribute_methods [method]
|
42
|
+
end
|
43
|
+
private :define_attribute_accessor
|
53
44
|
|
54
|
-
|
55
|
-
TypeCaster.cast(value, self.class.typecast_attributes[attribute.to_sym])
|
56
|
-
end
|
45
|
+
end
|
57
46
|
|
58
|
-
|
59
|
-
|
60
|
-
self.class.reflections.keys.each do |key|
|
61
|
-
attributes_to_reject.push(key.to_s)
|
47
|
+
def attributes
|
48
|
+
@attributes ||= self.class.default_attributes.dup
|
62
49
|
end
|
63
|
-
|
64
|
-
|
50
|
+
|
51
|
+
def read_attribute_for_validation(key)
|
52
|
+
attributes[key]
|
65
53
|
end
|
66
|
-
end
|
67
54
|
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
55
|
+
def typecast(attribute, value)
|
56
|
+
TypeCaster.cast(value, self.class.typecast_attributes[attribute.to_sym])
|
57
|
+
end
|
58
|
+
|
59
|
+
def attributes_without_basic_attributes
|
60
|
+
attributes_to_reject = %w( id errors valid )
|
61
|
+
self.class.reflections.keys.each do |key|
|
62
|
+
attributes_to_reject.push(key.to_s)
|
63
|
+
end
|
64
|
+
attributes.reject do |key, value|
|
65
|
+
attributes_to_reject.include?(key)
|
66
|
+
end
|
74
67
|
end
|
75
|
-
end
|
76
68
|
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
69
|
+
def update_attribute(attribute, value)
|
70
|
+
reflection = self.class.reflect_on_association(attribute)
|
71
|
+
if reflection
|
72
|
+
update_reflection(reflection, attribute, value)
|
73
|
+
else
|
74
|
+
attributes[attribute.to_sym] = typecast(attribute, value)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def update_reflection(reflection, attribute, value)
|
79
|
+
return unless value
|
80
|
+
if reflection.macro == :has_many # need to construct each member of array one-by-one
|
81
|
+
association_object = send(attribute)
|
82
|
+
value.each do |a_value|
|
83
|
+
if a_value.instance_of? reflection.klass
|
84
|
+
target = a_value
|
85
|
+
else
|
86
|
+
target = reflection.build_association(a_value)
|
87
|
+
end
|
88
|
+
association_object << target
|
89
|
+
end
|
90
|
+
elsif reflection.macro == :belongs_to
|
91
|
+
if value.instance_of? reflection.klass
|
92
|
+
target = value
|
84
93
|
else
|
85
|
-
|
94
|
+
if reflection.options[:polymorphic]
|
95
|
+
polymorphic_class = send("#{reflection.name}_type".to_sym)
|
96
|
+
polymorphic_class = value['type'] if value.include?('type')
|
97
|
+
target = polymorphic_class.constantize.new(value)
|
98
|
+
else
|
99
|
+
target = reflection.build_association(value)
|
100
|
+
end
|
86
101
|
end
|
87
|
-
|
88
|
-
end
|
89
|
-
elsif reflection.macro == :belongs_to
|
90
|
-
if value.instance_of? reflection.klass
|
91
|
-
target = value
|
102
|
+
send("#{attribute}=", target)
|
92
103
|
else
|
93
|
-
if reflection.
|
94
|
-
|
95
|
-
polymorphic_class = value['type'] if value.include?('type')
|
96
|
-
target = polymorphic_class.constantize.new(value)
|
104
|
+
if value.instance_of? reflection.klass
|
105
|
+
target = value
|
97
106
|
else
|
98
107
|
target = reflection.build_association(value)
|
99
108
|
end
|
109
|
+
send("#{attribute}=", target)
|
100
110
|
end
|
101
|
-
send("#{attribute}=", target)
|
102
|
-
else
|
103
|
-
if value.instance_of? reflection.klass
|
104
|
-
target = value
|
105
|
-
else
|
106
|
-
target = reflection.build_association(value)
|
107
|
-
end
|
108
|
-
send("#{attribute}=", target)
|
109
111
|
end
|
110
|
-
end
|
111
112
|
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
113
|
+
def merge_attributes(new_attributes)
|
114
|
+
new_attributes.each do |key, value|
|
115
|
+
case key.to_sym
|
116
|
+
when :errors
|
117
|
+
value.each do |k, v|
|
118
|
+
v.each do |message|
|
119
|
+
errors.add(k, message)
|
120
|
+
end
|
119
121
|
end
|
122
|
+
when :valid
|
123
|
+
@valid = value
|
124
|
+
else
|
125
|
+
update_attribute(key, value)
|
120
126
|
end
|
121
|
-
when :valid
|
122
|
-
@valid = value
|
123
|
-
else
|
124
|
-
update_attribute(key, value)
|
125
127
|
end
|
128
|
+
self
|
126
129
|
end
|
127
|
-
|
128
|
-
end
|
129
|
-
private :merge_attributes
|
130
|
+
private :merge_attributes
|
130
131
|
|
132
|
+
end
|
131
133
|
end
|
132
134
|
end
|