old_api_resource 0.3.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.
- data/.document +5 -0
- data/.rspec +3 -0
- data/Gemfile +26 -0
- data/Guardfile +22 -0
- data/LICENSE.txt +20 -0
- data/README.rdoc +19 -0
- data/Rakefile +49 -0
- data/VERSION +1 -0
- data/lib/old_api_resource.rb +70 -0
- data/lib/old_api_resource/associations.rb +192 -0
- data/lib/old_api_resource/associations/association_proxy.rb +92 -0
- data/lib/old_api_resource/associations/multi_object_proxy.rb +74 -0
- data/lib/old_api_resource/associations/related_object_hash.rb +12 -0
- data/lib/old_api_resource/associations/relation_scope.rb +24 -0
- data/lib/old_api_resource/associations/resource_scope.rb +25 -0
- data/lib/old_api_resource/associations/scope.rb +88 -0
- data/lib/old_api_resource/associations/single_object_proxy.rb +64 -0
- data/lib/old_api_resource/attributes.rb +162 -0
- data/lib/old_api_resource/base.rb +548 -0
- data/lib/old_api_resource/callbacks.rb +49 -0
- data/lib/old_api_resource/connection.rb +167 -0
- data/lib/old_api_resource/core_extensions.rb +7 -0
- data/lib/old_api_resource/custom_methods.rb +119 -0
- data/lib/old_api_resource/exceptions.rb +85 -0
- data/lib/old_api_resource/formats.rb +14 -0
- data/lib/old_api_resource/formats/json_format.rb +25 -0
- data/lib/old_api_resource/formats/xml_format.rb +36 -0
- data/lib/old_api_resource/log_subscriber.rb +15 -0
- data/lib/old_api_resource/mocks.rb +260 -0
- data/lib/old_api_resource/model_errors.rb +86 -0
- data/lib/old_api_resource/observing.rb +29 -0
- data/lib/old_api_resource/railtie.rb +18 -0
- data/old_api_resource.gemspec +134 -0
- data/spec/lib/associations_spec.rb +519 -0
- data/spec/lib/attributes_spec.rb +121 -0
- data/spec/lib/base_spec.rb +499 -0
- data/spec/lib/callbacks_spec.rb +68 -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 +50 -0
- metadata +286 -0
@@ -0,0 +1,74 @@
|
|
1
|
+
require 'old_api_resource/associations/association_proxy'
|
2
|
+
|
3
|
+
module OldApiResource
|
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.klass.connection.get("#{self.remote_path}.#{self.klass.format.extension}?#{options.to_query}")
|
40
|
+
end
|
41
|
+
self.loaded[scope].collect{|item| self.klass.new(item)}
|
42
|
+
end
|
43
|
+
|
44
|
+
def load(contents)
|
45
|
+
# If we have a blank array or it's just nil then we should just return after setting internal_object to a blank array
|
46
|
+
@internal_object = [] and return nil if (contents.is_a?(Array) && contents.blank?) || contents.nil?
|
47
|
+
if contents.is_a?(Array) && contents.first.is_a?(Hash) && contents.first[self.class.remote_path_element]
|
48
|
+
settings = contents.slice!(0).with_indifferent_access
|
49
|
+
end
|
50
|
+
|
51
|
+
settings = contents if contents.is_a?(Hash)
|
52
|
+
settings ||= {}.with_indifferent_access
|
53
|
+
|
54
|
+
raise "Invalid response for multi object relationship: #{contents}" unless settings[self.class.remote_path_element] || contents.is_a?(Array)
|
55
|
+
self.remote_path = settings.delete(self.class.remote_path_element)
|
56
|
+
|
57
|
+
settings.each do |key, value|
|
58
|
+
raise "Expected the scope #{key} to point to a hash, to #{value}" unless value.is_a?(Hash)
|
59
|
+
self.instance_eval <<-EOE, __FILE__, __LINE__ + 1
|
60
|
+
def #{key}(opts = {})
|
61
|
+
OldApiResource::Associations::RelationScope.new(self, :#{key}, opts)
|
62
|
+
end
|
63
|
+
EOE
|
64
|
+
self.scopes[key.to_s] = value
|
65
|
+
end
|
66
|
+
|
67
|
+
# Create the internal object
|
68
|
+
@internal_object = contents.is_a?(Array) ? contents.collect{|item| self.klass.new(item)} : nil
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
end
|
73
|
+
|
74
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
module OldApiResource
|
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,24 @@
|
|
1
|
+
require 'old_api_resource/associations/scope'
|
2
|
+
|
3
|
+
module OldApiResource
|
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.scopes[self.current_scope])
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'old_api_resource/associations/scope'
|
2
|
+
|
3
|
+
module OldApiResource
|
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.scopes[self.current_scope])
|
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
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
module OldApiResource
|
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
|
+
@current_scope = Array.wrap(current_scope.to_s)
|
15
|
+
# define methods for the scopes of the object
|
16
|
+
|
17
|
+
klass.scopes.each do |key, val|
|
18
|
+
self.instance_eval <<-EOE, __FILE__, __LINE__ + 1
|
19
|
+
# This class always has at least one scope, adding a new one should clone this object
|
20
|
+
def #{key}(opts = {})
|
21
|
+
obj = self.clone
|
22
|
+
# Call reload to make it go back to the webserver the next time it loads
|
23
|
+
obj.reload
|
24
|
+
obj.enhance_current_scope(:#{key}, opts)
|
25
|
+
return obj
|
26
|
+
end
|
27
|
+
EOE
|
28
|
+
self.scopes[key.to_s] = val
|
29
|
+
end
|
30
|
+
# Use the method current scope because it gives a string
|
31
|
+
# This expression substitutes the options from opts into the default attributes of the scope, it will only copy keys that exist in the original
|
32
|
+
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}
|
33
|
+
end
|
34
|
+
|
35
|
+
# Use this method to access the internal data, this guarantees that loading only occurs once per object
|
36
|
+
def internal_object
|
37
|
+
raise "Not Implemented: This method must be implemented in a subclass"
|
38
|
+
end
|
39
|
+
|
40
|
+
def scopes
|
41
|
+
@scopes ||= {}.with_indifferent_access
|
42
|
+
end
|
43
|
+
|
44
|
+
def scope?(scp)
|
45
|
+
self.scopes.key?(scp.to_s)
|
46
|
+
end
|
47
|
+
|
48
|
+
def current_scope
|
49
|
+
ActiveSupport::StringInquirer.new(@current_scope.join("_and_").concat("_scope"))
|
50
|
+
end
|
51
|
+
|
52
|
+
def to_query
|
53
|
+
self.scopes[self.current_scope].to_query
|
54
|
+
end
|
55
|
+
|
56
|
+
def method_missing(method, *args, &block)
|
57
|
+
self.internal_object.send(method, *args, &block)
|
58
|
+
end
|
59
|
+
|
60
|
+
def reload
|
61
|
+
remove_instance_variable(:@internal_object) if instance_variable_defined?(:@internal_object)
|
62
|
+
self
|
63
|
+
end
|
64
|
+
|
65
|
+
def to_s
|
66
|
+
self.internal_object.to_s
|
67
|
+
end
|
68
|
+
|
69
|
+
def inspect
|
70
|
+
self.internal_object.inspect
|
71
|
+
end
|
72
|
+
|
73
|
+
protected
|
74
|
+
def enhance_current_scope(scp, opts)
|
75
|
+
scp = scp.to_s
|
76
|
+
raise ArgumentError, "Unknown scope #{scp}" unless self.scope?(scp)
|
77
|
+
# Hold onto the attributes related to the old scope that we're going to chain to
|
78
|
+
current_scope_hash = self.scopes[self.current_scope]
|
79
|
+
# This sets the new current scope making them unique and sorted to make it order independent
|
80
|
+
@current_scope = @current_scope.concat([scp.to_s]).uniq.sort
|
81
|
+
# This sets up the new options for the current scope, it merges the defaults for the new scope then substitutes from opts
|
82
|
+
self.scopes[self.current_scope] = opts.inject(current_scope_hash.merge(self.scopes[scp.to_s])){|accum,(k,v)| accum.key?(k.to_s) ? accum.merge(k.to_s => v) : accum }
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
end
|
87
|
+
|
88
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
require 'old_api_resource/associations/association_proxy'
|
2
|
+
|
3
|
+
module OldApiResource
|
4
|
+
|
5
|
+
module Associations
|
6
|
+
|
7
|
+
class SingleObjectProxy < AssociationProxy
|
8
|
+
|
9
|
+
def serializable_hash(options = {})
|
10
|
+
self.internal_object.serializable_hash(options)
|
11
|
+
end
|
12
|
+
|
13
|
+
def internal_object=(contents)
|
14
|
+
return @internal_object = contents if contents.is_a?(self.klass)
|
15
|
+
return load(contents)
|
16
|
+
end
|
17
|
+
|
18
|
+
protected
|
19
|
+
def load_scope_with_options(scope, options)
|
20
|
+
scope = self.loaded_hash_key(scope.to_s, options)
|
21
|
+
# If the service uri is blank you can't load
|
22
|
+
return nil if self.remote_path.blank?
|
23
|
+
unless self.loaded[scope]
|
24
|
+
self.times_loaded += 1
|
25
|
+
self.loaded[scope] = self.klass.connection.get("#{self.remote_path}.#{self.klass.format.extension}?#{options.to_query}")
|
26
|
+
end
|
27
|
+
self.klass.new(self.loaded[scope])
|
28
|
+
end
|
29
|
+
|
30
|
+
def load(contents)
|
31
|
+
# If we get something nil this should just behave like nil
|
32
|
+
return if contents.nil?
|
33
|
+
raise "Expected an attributes hash got #{contents}" unless contents.is_a?(Hash)
|
34
|
+
# If we don't have a 'service_uri' just assume that these are all attributes and make an object
|
35
|
+
return @internal_object = self.klass.new(contents) unless contents[self.class.remote_path_element]
|
36
|
+
# allow for symbols vs strings with these elements
|
37
|
+
self.remote_path = contents.delete(self.class.remote_path_element) || contents.delete(self.class.remote_path_element.to_s)
|
38
|
+
# There's only one hash here so it's hard to distinguish attributes from scopes, the key scopes_only says everything
|
39
|
+
# in this hash is a scope
|
40
|
+
no_attrs = (contents.delete("scopes_only") || contents.delete(:scopes_only) || false)
|
41
|
+
attrs = {}
|
42
|
+
contents.each do |key, val|
|
43
|
+
# if this key is an attribute add it to attrs, warn if we've set scopes_only
|
44
|
+
if self.klass.attribute_names.include?(key) && !no_attrs
|
45
|
+
attrs[key] = val
|
46
|
+
else
|
47
|
+
warn("#{key} is an attribute of #{self.klass}, beware of name collisions") if no_attrs && self.klass.attribute_names.include?(key)
|
48
|
+
raise "Expected the scope #{key} to have a hash for a value, got #{val}" unless val.is_a?(Hash)
|
49
|
+
self.instance_eval <<-EOE, __FILE__, __LINE__ + 1
|
50
|
+
def #{key}(opts = {})
|
51
|
+
OldApiResource::Associations::RelationScope.new(self, :#{key}, opts)
|
52
|
+
end
|
53
|
+
EOE
|
54
|
+
self.scopes[key.to_s] = val
|
55
|
+
end
|
56
|
+
end
|
57
|
+
@internal_object = attrs.present? ? self.klass.new(attrs) : nil
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|
61
|
+
|
62
|
+
end
|
63
|
+
|
64
|
+
end
|
@@ -0,0 +1,162 @@
|
|
1
|
+
module OldApiResource
|
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)
|
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
|