api_resource 0.2.1
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.
- data/.document +5 -0
- data/.rspec +3 -0
- data/Gemfile +29 -0
- data/Gemfile.lock +152 -0
- data/Guardfile +22 -0
- data/LICENSE.txt +20 -0
- data/README.rdoc +19 -0
- data/Rakefile +49 -0
- data/VERSION +1 -0
- data/api_resource.gemspec +154 -0
- data/lib/api_resource.rb +129 -0
- data/lib/api_resource/association_activation.rb +19 -0
- data/lib/api_resource/associations.rb +169 -0
- data/lib/api_resource/associations/association_proxy.rb +115 -0
- data/lib/api_resource/associations/belongs_to_remote_object_proxy.rb +16 -0
- data/lib/api_resource/associations/dynamic_resource_scope.rb +23 -0
- data/lib/api_resource/associations/has_many_remote_object_proxy.rb +16 -0
- data/lib/api_resource/associations/has_one_remote_object_proxy.rb +24 -0
- data/lib/api_resource/associations/multi_argument_resource_scope.rb +15 -0
- data/lib/api_resource/associations/multi_object_proxy.rb +73 -0
- data/lib/api_resource/associations/related_object_hash.rb +12 -0
- data/lib/api_resource/associations/relation_scope.rb +30 -0
- data/lib/api_resource/associations/resource_scope.rb +34 -0
- data/lib/api_resource/associations/scope.rb +107 -0
- data/lib/api_resource/associations/single_object_proxy.rb +81 -0
- data/lib/api_resource/attributes.rb +162 -0
- data/lib/api_resource/base.rb +587 -0
- data/lib/api_resource/callbacks.rb +49 -0
- data/lib/api_resource/connection.rb +171 -0
- data/lib/api_resource/core_extensions.rb +7 -0
- data/lib/api_resource/custom_methods.rb +119 -0
- data/lib/api_resource/exceptions.rb +87 -0
- data/lib/api_resource/formats.rb +14 -0
- data/lib/api_resource/formats/json_format.rb +25 -0
- data/lib/api_resource/formats/xml_format.rb +36 -0
- data/lib/api_resource/local.rb +12 -0
- data/lib/api_resource/log_subscriber.rb +15 -0
- data/lib/api_resource/mocks.rb +269 -0
- data/lib/api_resource/model_errors.rb +86 -0
- data/lib/api_resource/observing.rb +29 -0
- data/lib/api_resource/railtie.rb +22 -0
- data/lib/api_resource/scopes.rb +45 -0
- data/spec/lib/associations_spec.rb +656 -0
- data/spec/lib/attributes_spec.rb +121 -0
- data/spec/lib/base_spec.rb +504 -0
- data/spec/lib/callbacks_spec.rb +68 -0
- data/spec/lib/connection_spec.rb +76 -0
- data/spec/lib/local_spec.rb +20 -0
- data/spec/lib/mocks_spec.rb +28 -0
- data/spec/lib/model_errors_spec.rb +29 -0
- data/spec/spec_helper.rb +36 -0
- data/spec/support/mocks/association_mocks.rb +46 -0
- data/spec/support/mocks/error_resource_mocks.rb +21 -0
- data/spec/support/mocks/test_resource_mocks.rb +43 -0
- data/spec/support/requests/association_requests.rb +14 -0
- data/spec/support/requests/error_resource_requests.rb +25 -0
- data/spec/support/requests/test_resource_requests.rb +31 -0
- data/spec/support/test_resource.rb +64 -0
- metadata +334 -0
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'api_resource/associations/resource_scope'
|
2
|
+
|
3
|
+
module ApiResource
|
4
|
+
module Associations
|
5
|
+
class DynamicResourceScope < ResourceScope
|
6
|
+
|
7
|
+
attr_accessor :dynamic_value
|
8
|
+
# initializer - set up the dynamic value
|
9
|
+
def initialize(klass, current_scope, dynamic_value, opts = {})
|
10
|
+
self.dynamic_value = dynamic_value
|
11
|
+
super(klass, current_scope, opts)
|
12
|
+
end
|
13
|
+
# get the to_query value for this resource scope
|
14
|
+
def to_hash
|
15
|
+
hsh = self.scopes[self.current_scope].clone
|
16
|
+
hsh.each_pair do |k,v|
|
17
|
+
hsh[k] = self.dynamic_value
|
18
|
+
end
|
19
|
+
self.parent_hash.merge(hsh)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'api_resource/associations/multi_object_proxy'
|
2
|
+
module ApiResource
|
3
|
+
module Associations
|
4
|
+
class HasManyRemoteObjectProxy < MultiObjectProxy
|
5
|
+
def initialize(klass_name, contents, owner)
|
6
|
+
super
|
7
|
+
return if self.internal_object.present? || self.remote_path
|
8
|
+
# now if we have an owner and a foreign key, we set the data up to load
|
9
|
+
if owner
|
10
|
+
self.load({"service_uri" => self.klass.collection_path(self.owner.class.to_s.foreign_key => self.owner.id)}.merge(self.klass.scopes))
|
11
|
+
end
|
12
|
+
true
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'api_resource/associations/single_object_proxy'
|
2
|
+
module ApiResource
|
3
|
+
module Associations
|
4
|
+
class HasOneRemoteObjectProxy < SingleObjectProxy
|
5
|
+
def initialize(klass_name, contents, owner)
|
6
|
+
super
|
7
|
+
return if self.internal_object
|
8
|
+
# now if we have an owner and a foreign key, we set the data up to load
|
9
|
+
if owner
|
10
|
+
self.load({"service_uri" => self.klass.collection_path(self.owner.class.to_s.foreign_key => self.owner.id)}.merge(self.klass.scopes))
|
11
|
+
end
|
12
|
+
true
|
13
|
+
end
|
14
|
+
protected
|
15
|
+
# load data from the remote server
|
16
|
+
# In a has_one, we can get back an Array, so we use the first element
|
17
|
+
def load_from_remote(options)
|
18
|
+
data = super(options)
|
19
|
+
data = data.first if data.is_a?(Array)
|
20
|
+
data
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
require 'api_resource/associations/dynamic_resource_scope'
|
2
|
+
|
3
|
+
module ApiResource
|
4
|
+
module Associations
|
5
|
+
class MultiArgumentResourceScope < DynamicResourceScope
|
6
|
+
# initialize with a variable number of dynamic arguments
|
7
|
+
def initialize(klass, current_scope, *dynamic_value)
|
8
|
+
# pull off opts
|
9
|
+
opts = dynamic_value.extract_options!
|
10
|
+
# we always dynamic value to be an Array, so we don't use the splat here
|
11
|
+
super(klass, current_scope, dynamic_value.flatten, opts)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
require 'api_resource/associations/association_proxy'
|
2
|
+
|
3
|
+
module ApiResource
|
4
|
+
|
5
|
+
module Associations
|
6
|
+
|
7
|
+
class MultiObjectProxy < AssociationProxy
|
8
|
+
|
9
|
+
include Enumerable
|
10
|
+
|
11
|
+
def all
|
12
|
+
self.internal_object
|
13
|
+
end
|
14
|
+
|
15
|
+
def each(*args, &block)
|
16
|
+
self.internal_object.each(*args, &block)
|
17
|
+
end
|
18
|
+
|
19
|
+
def serializable_hash(options)
|
20
|
+
self.internal_object.collect{|obj| obj.serializable_hash(options) }
|
21
|
+
end
|
22
|
+
|
23
|
+
# force a load when calling this method
|
24
|
+
def internal_object
|
25
|
+
@internal_object ||= self.load_scope_with_options(:all, {})
|
26
|
+
end
|
27
|
+
|
28
|
+
def internal_object=(contents)
|
29
|
+
return @internal_object = contents if contents.all?{|o| o.is_a?(self.klass)}
|
30
|
+
return load(contents)
|
31
|
+
end
|
32
|
+
|
33
|
+
protected
|
34
|
+
def load_scope_with_options(scope, options)
|
35
|
+
scope = self.loaded_hash_key(scope.to_s, options)
|
36
|
+
return [] if self.remote_path.blank?
|
37
|
+
unless self.loaded[scope]
|
38
|
+
self.times_loaded += 1
|
39
|
+
self.loaded[scope] = self.load_from_remote(options)
|
40
|
+
end
|
41
|
+
self.loaded[scope].collect{|item| self.klass.new(item)}
|
42
|
+
end
|
43
|
+
|
44
|
+
def load(contents)
|
45
|
+
@internal_object = [] and return nil if contents.blank?
|
46
|
+
if contents.is_a?(Array) && contents.first.is_a?(Hash) && contents.first[self.class.remote_path_element]
|
47
|
+
settings = contents.slice!(0).with_indifferent_access
|
48
|
+
end
|
49
|
+
|
50
|
+
settings = contents.with_indifferent_access if contents.is_a?(Hash)
|
51
|
+
settings ||= {}.with_indifferent_access
|
52
|
+
|
53
|
+
raise "Invalid response for multi object relationship: #{contents}" unless settings[self.class.remote_path_element] || contents.is_a?(Array)
|
54
|
+
self.remote_path = settings.delete(self.class.remote_path_element)
|
55
|
+
|
56
|
+
settings.each do |key, value|
|
57
|
+
raise "Expected the scope #{key} to point to a hash, to #{value}" unless value.is_a?(Hash)
|
58
|
+
self.instance_eval <<-EOE, __FILE__, __LINE__ + 1
|
59
|
+
def #{key}(opts = {})
|
60
|
+
ApiResource::Associations::RelationScope.new(self, :#{key}, opts)
|
61
|
+
end
|
62
|
+
EOE
|
63
|
+
self.scopes[key.to_s] = value
|
64
|
+
end
|
65
|
+
|
66
|
+
# Create the internal object
|
67
|
+
@internal_object = contents.is_a?(Array) ? contents.collect{|item| self.klass.new(item)} : nil
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
end
|
72
|
+
|
73
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
module ApiResource
|
2
|
+
module Associations
|
3
|
+
# RelatedObjectHash, re-defines dup to be recursive
|
4
|
+
class RelatedObjectHash < HashWithIndifferentAccess
|
5
|
+
def dup
|
6
|
+
Marshal.load(Marshal.dump(self))
|
7
|
+
end
|
8
|
+
# use this behavior for clone too
|
9
|
+
alias_method :clone, :dup
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'api_resource/associations/scope'
|
2
|
+
|
3
|
+
module ApiResource
|
4
|
+
|
5
|
+
module Associations
|
6
|
+
|
7
|
+
class RelationScope < Scope
|
8
|
+
|
9
|
+
def reload
|
10
|
+
remove_instance_variable(:@internal_object) if instance_variable_defined?(:@internal_object)
|
11
|
+
self.klass.reload(self.current_scope, self.scopes[self.current_scope])
|
12
|
+
self
|
13
|
+
end
|
14
|
+
|
15
|
+
# Use this method to access the internal data, this guarantees that loading only occurs once per object
|
16
|
+
def internal_object
|
17
|
+
@internal_object ||= self.klass.send(:load_scope_with_options, self.current_scope, self.to_hash)
|
18
|
+
end
|
19
|
+
|
20
|
+
#
|
21
|
+
# class factory
|
22
|
+
def self.class_factory(hsh)
|
23
|
+
ApiResource::Associations::RelationScope
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'api_resource/associations/scope'
|
2
|
+
|
3
|
+
module ApiResource
|
4
|
+
|
5
|
+
module Associations
|
6
|
+
|
7
|
+
class ResourceScope < Scope
|
8
|
+
|
9
|
+
include Enumerable
|
10
|
+
|
11
|
+
def internal_object
|
12
|
+
@internal_object ||= self.klass.send(:find, :all, :params => self.to_hash)
|
13
|
+
end
|
14
|
+
|
15
|
+
alias_method :all, :internal_object
|
16
|
+
|
17
|
+
def each(*args, &block)
|
18
|
+
self.internal_object.each(*args, &block)
|
19
|
+
end
|
20
|
+
|
21
|
+
# get the relevant class
|
22
|
+
def self.class_factory(hsh)
|
23
|
+
case hsh.values.first
|
24
|
+
when TrueClass, FalseClass
|
25
|
+
ApiResource::Associations::ResourceScope
|
26
|
+
when Array
|
27
|
+
ApiResource::Associations::MultiArgumentResourceScope
|
28
|
+
else
|
29
|
+
ApiResource::Associations::DynamicResourceScope
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,107 @@
|
|
1
|
+
module ApiResource
|
2
|
+
|
3
|
+
module Associations
|
4
|
+
|
5
|
+
class Scope
|
6
|
+
|
7
|
+
attr_accessor :klass, :current_scope, :internal_object
|
8
|
+
|
9
|
+
attr_reader :scopes
|
10
|
+
|
11
|
+
def initialize(klass, current_scope, opts = {})
|
12
|
+
# Holds onto the association proxy this RelationScope is bound to
|
13
|
+
@klass = klass
|
14
|
+
@parent = opts.delete(:parent)
|
15
|
+
# splits on _and_ and sorts to get a consistent scope key order
|
16
|
+
@current_scope = (self.parent_scope + Array.wrap(current_scope.to_s)).sort
|
17
|
+
# define methods for the scopes of the object
|
18
|
+
|
19
|
+
klass.scopes.each do |key, val|
|
20
|
+
self.instance_eval <<-EOE, __FILE__, __LINE__ + 1
|
21
|
+
# This class always has at least one scope, adding a new one should clone this object
|
22
|
+
def #{key}(*args)
|
23
|
+
obj = self.clone
|
24
|
+
# Call reload to make it go back to the webserver the next time it loads
|
25
|
+
obj.reload
|
26
|
+
return obj.enhance_current_scope(:#{key}, *args)
|
27
|
+
end
|
28
|
+
EOE
|
29
|
+
self.scopes[key.to_s] = val
|
30
|
+
end
|
31
|
+
# Use the method current scope because it gives a string
|
32
|
+
# This expression substitutes the options from opts into the default attributes of the scope, it will only copy keys that exist in the original
|
33
|
+
self.scopes[self.current_scope] = opts.inject(self.scopes[current_scope]){|accum,(k,v)| accum.key?(k.to_s) ? accum.merge(k.to_s => v) : accum}
|
34
|
+
end
|
35
|
+
|
36
|
+
# Use this method to access the internal data, this guarantees that loading only occurs once per object
|
37
|
+
def internal_object
|
38
|
+
raise "Not Implemented: This method must be implemented in a subclass"
|
39
|
+
end
|
40
|
+
|
41
|
+
def scopes
|
42
|
+
@scopes ||= {}.with_indifferent_access
|
43
|
+
end
|
44
|
+
|
45
|
+
def scope?(scp)
|
46
|
+
self.scopes.key?(scp.to_s)
|
47
|
+
end
|
48
|
+
|
49
|
+
def current_scope
|
50
|
+
ActiveSupport::StringInquirer.new(@current_scope.join("_and_").concat("_scope"))
|
51
|
+
end
|
52
|
+
|
53
|
+
def to_hash
|
54
|
+
self.parent_hash.merge(self.scopes[self.current_scope])
|
55
|
+
end
|
56
|
+
|
57
|
+
# gets the current hash and calls to_query on it
|
58
|
+
def to_query
|
59
|
+
self.to_hash.to_query
|
60
|
+
end
|
61
|
+
|
62
|
+
def method_missing(method, *args, &block)
|
63
|
+
self.internal_object.send(method, *args, &block)
|
64
|
+
end
|
65
|
+
|
66
|
+
def reload
|
67
|
+
remove_instance_variable(:@internal_object) if instance_variable_defined?(:@internal_object)
|
68
|
+
self
|
69
|
+
end
|
70
|
+
|
71
|
+
def to_s
|
72
|
+
self.internal_object.to_s
|
73
|
+
end
|
74
|
+
|
75
|
+
def inspect
|
76
|
+
self.internal_object.inspect
|
77
|
+
end
|
78
|
+
|
79
|
+
def blank?
|
80
|
+
self.internal_object.blank?
|
81
|
+
end
|
82
|
+
alias_method :empty?, :blank?
|
83
|
+
|
84
|
+
protected
|
85
|
+
# scope from the parent
|
86
|
+
def parent_scope
|
87
|
+
ret = @parent ? Array.wrap(@parent.current_scope).collect{|el| el.gsub(/_scope$/,'')} : []
|
88
|
+
ret.collect{|el| el.split(/_and_/)}.flatten
|
89
|
+
end
|
90
|
+
# querystring hash from parent
|
91
|
+
def parent_hash
|
92
|
+
@parent ? @parent.to_hash : {}
|
93
|
+
end
|
94
|
+
def enhance_current_scope(scp, *args)
|
95
|
+
opts = args.extract_options!
|
96
|
+
check_scope(scp)
|
97
|
+
self.class.class_factory(self.scopes[scp]).new(self.klass, scp, *args, opts.merge(:parent => self))
|
98
|
+
end
|
99
|
+
# make sure we have a valid scope
|
100
|
+
def check_scope(scp)
|
101
|
+
raise ArgumentError, "Unknown scope #{scp}" unless self.scope?(scp.to_s)
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
end
|
106
|
+
|
107
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
require 'api_resource/associations/association_proxy'
|
2
|
+
|
3
|
+
module ApiResource
|
4
|
+
|
5
|
+
module Associations
|
6
|
+
|
7
|
+
class SingleObjectProxy < AssociationProxy
|
8
|
+
|
9
|
+
def serializable_hash(options = {})
|
10
|
+
return if self.internal_object.nil?
|
11
|
+
self.internal_object.serializable_hash(options)
|
12
|
+
end
|
13
|
+
|
14
|
+
def internal_object=(contents)
|
15
|
+
return @internal_object = contents if contents.is_a?(self.klass)
|
16
|
+
return @internal_object = contents if contents.respond_to?(:internal_object) && contents.internal_object.is_a?(self.klass)
|
17
|
+
return load(contents)
|
18
|
+
end
|
19
|
+
|
20
|
+
def blank?
|
21
|
+
return @internal_object.blank?
|
22
|
+
end
|
23
|
+
|
24
|
+
def present?
|
25
|
+
return @internal_object.present?
|
26
|
+
end
|
27
|
+
|
28
|
+
protected
|
29
|
+
def load_scope_with_options(scope, options)
|
30
|
+
scope = self.loaded_hash_key(scope.to_s, options)
|
31
|
+
# If the service uri is blank you can't load
|
32
|
+
return nil if self.remote_path.blank?
|
33
|
+
unless self.loaded[scope]
|
34
|
+
self.times_loaded += 1
|
35
|
+
self.loaded[scope] = self.load_from_remote(options)
|
36
|
+
end
|
37
|
+
self.klass.new(self.loaded[scope])
|
38
|
+
end
|
39
|
+
|
40
|
+
def load(contents)
|
41
|
+
# If we get something nil this should just behave like nil
|
42
|
+
return if contents.nil?
|
43
|
+
# if we get an array with a length of one, make it a hash
|
44
|
+
if contents.is_a?(self.class)
|
45
|
+
contents = contents.internal_object.serializable_hash
|
46
|
+
elsif contents.is_a?(Array) && contents.length == 1
|
47
|
+
contents = contents.first
|
48
|
+
end
|
49
|
+
raise "Expected an attributes hash got #{contents}" unless contents.is_a?(Hash)
|
50
|
+
contents = contents.with_indifferent_access
|
51
|
+
# If we don't have a 'service_uri' just assume that these are all attributes and make an object
|
52
|
+
return @internal_object = self.klass.new(contents) unless contents[self.class.remote_path_element]
|
53
|
+
# allow for symbols vs strings with these elements
|
54
|
+
self.remote_path = contents.delete(self.class.remote_path_element)
|
55
|
+
# There's only one hash here so it's hard to distinguish attributes from scopes, the key scopes_only says everything
|
56
|
+
# in this hash is a scope
|
57
|
+
no_attrs = (contents.delete(:scopes_only) || false)
|
58
|
+
attrs = {}
|
59
|
+
contents.each do |key, val|
|
60
|
+
# if this key is an attribute add it to attrs, warn if we've set scopes_only
|
61
|
+
if self.klass.attribute_names.include?(key.to_sym) && !no_attrs
|
62
|
+
attrs[key] = val
|
63
|
+
else
|
64
|
+
warn("#{key} is an attribute of #{self.klass}, beware of name collisions") if no_attrs && self.klass.attribute_names.include?(key)
|
65
|
+
raise "Expected the scope #{key} to have a hash for a value, got #{val}" unless val.is_a?(Hash)
|
66
|
+
self.instance_eval <<-EOE, __FILE__, __LINE__ + 1
|
67
|
+
def #{key}(opts = {})
|
68
|
+
ApiResource::Associations::RelationScope.new(self, :#{key}, opts)
|
69
|
+
end
|
70
|
+
EOE
|
71
|
+
self.scopes[key.to_s] = val
|
72
|
+
end
|
73
|
+
end
|
74
|
+
@internal_object = attrs.present? ? self.klass.new(attrs) : nil
|
75
|
+
end
|
76
|
+
|
77
|
+
end
|
78
|
+
|
79
|
+
end
|
80
|
+
|
81
|
+
end
|
@@ -0,0 +1,162 @@
|
|
1
|
+
module ApiResource
|
2
|
+
|
3
|
+
module Attributes
|
4
|
+
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
include ActiveModel::AttributeMethods
|
7
|
+
include ActiveModel::Dirty
|
8
|
+
|
9
|
+
included do
|
10
|
+
|
11
|
+
alias_method_chain :save, :dirty_tracking
|
12
|
+
|
13
|
+
class_inheritable_accessor :attribute_names, :public_attribute_names, :protected_attribute_names
|
14
|
+
|
15
|
+
attr_reader :attributes
|
16
|
+
|
17
|
+
self.attribute_names = []
|
18
|
+
self.public_attribute_names = []
|
19
|
+
self.protected_attribute_names = []
|
20
|
+
|
21
|
+
define_method(:attributes) do
|
22
|
+
return @attributes if @attributes
|
23
|
+
# Otherwise make the attributes hash of all the attributes
|
24
|
+
@attributes = HashWithIndifferentAccess.new
|
25
|
+
self.class.attribute_names.each do |attr|
|
26
|
+
@attributes[attr] = self.send("#{attr}")
|
27
|
+
end
|
28
|
+
@attributes
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
|
33
|
+
module ClassMethods
|
34
|
+
|
35
|
+
def define_attributes(*args)
|
36
|
+
# This is provided by ActiveModel::AttributeMethods, it should define the basic methods
|
37
|
+
# but we need to override all the setters so we do dirty tracking
|
38
|
+
define_attribute_methods args
|
39
|
+
args.each do |arg|
|
40
|
+
self.attribute_names << arg.to_sym
|
41
|
+
self.public_attribute_names << arg.to_sym
|
42
|
+
|
43
|
+
# Override the setter for dirty tracking
|
44
|
+
self.class_eval <<-EOE, __FILE__, __LINE__ + 1
|
45
|
+
def #{arg}
|
46
|
+
self.attributes[:#{arg}]
|
47
|
+
end
|
48
|
+
|
49
|
+
def #{arg}=(val)
|
50
|
+
#{arg}_will_change! unless self.#{arg} == val
|
51
|
+
self.attributes[:#{arg}] = val
|
52
|
+
end
|
53
|
+
|
54
|
+
def #{arg}?
|
55
|
+
self.attributes[:#{arg}].present?
|
56
|
+
end
|
57
|
+
EOE
|
58
|
+
end
|
59
|
+
self.attribute_names.uniq!
|
60
|
+
self.public_attribute_names.uniq!
|
61
|
+
end
|
62
|
+
|
63
|
+
def define_protected_attributes(*args)
|
64
|
+
define_attribute_methods args
|
65
|
+
args.each do |arg|
|
66
|
+
self.attribute_names << arg.to_sym
|
67
|
+
self.protected_attribute_names << arg.to_sym
|
68
|
+
|
69
|
+
# These attributes cannot be set, throw an error if you try
|
70
|
+
self.class_eval <<-EOE, __FILE__, __LINE__ + 1
|
71
|
+
|
72
|
+
def #{arg}
|
73
|
+
self.attributes[:#{arg}]
|
74
|
+
end
|
75
|
+
|
76
|
+
def #{arg}=(val)
|
77
|
+
raise "#{arg} is a protected attribute and cannot be set"
|
78
|
+
end
|
79
|
+
|
80
|
+
def #{arg}?
|
81
|
+
self.attributes[:#{arg}].present?
|
82
|
+
end
|
83
|
+
EOE
|
84
|
+
end
|
85
|
+
self.attribute_names.uniq!
|
86
|
+
self.protected_attribute_names.uniq!
|
87
|
+
end
|
88
|
+
|
89
|
+
def attribute?(name)
|
90
|
+
self.attribute_names.include?(name.to_sym)
|
91
|
+
end
|
92
|
+
|
93
|
+
def protected_attribute?(name)
|
94
|
+
self.protected_attribute_names.include?(name.to_sym)
|
95
|
+
end
|
96
|
+
|
97
|
+
def clear_attributes
|
98
|
+
self.attribute_names.clear
|
99
|
+
self.public_attribute_names.clear
|
100
|
+
self.protected_attribute_names.clear
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
module InstanceMethods
|
105
|
+
|
106
|
+
# set new attributes
|
107
|
+
def attributes=(new_attrs)
|
108
|
+
new_attrs.each_pair do |k,v|
|
109
|
+
self.send("#{k}=",v) unless k.to_sym == :id
|
110
|
+
end
|
111
|
+
new_attrs
|
112
|
+
end
|
113
|
+
|
114
|
+
def save_with_dirty_tracking(*args)
|
115
|
+
if save_without_dirty_tracking(*args)
|
116
|
+
@previously_changed = self.changes
|
117
|
+
@changed_attributes.clear
|
118
|
+
return true
|
119
|
+
else
|
120
|
+
return false
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
def set_attributes_as_current(*attrs)
|
125
|
+
@changed_attributes.clear and return if attrs.blank?
|
126
|
+
attrs.each do |attr|
|
127
|
+
@changed_attributes.delete(attr.to_s)
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
def reset_attribute_changes(*attrs)
|
132
|
+
attrs = self.class.public_attribute_names if attrs.blank?
|
133
|
+
attrs.each do |attr|
|
134
|
+
self.send("reset_#{attr}!")
|
135
|
+
end
|
136
|
+
|
137
|
+
set_attributes_as_current(*attrs)
|
138
|
+
end
|
139
|
+
|
140
|
+
def attribute?(name)
|
141
|
+
self.class.attribute?(name)
|
142
|
+
end
|
143
|
+
|
144
|
+
def protected_attribute?(name)
|
145
|
+
self.class.protected_attribute?(name)
|
146
|
+
end
|
147
|
+
|
148
|
+
def respond_to?(sym)
|
149
|
+
if sym =~ /\?$/
|
150
|
+
return true if self.attribute?($`)
|
151
|
+
elsif sym =~ /=$/
|
152
|
+
return true if self.class.public_attribute_names.include?($`)
|
153
|
+
else
|
154
|
+
return true if self.attribute?(sym.to_sym)
|
155
|
+
end
|
156
|
+
super
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
end
|
161
|
+
|
162
|
+
end
|