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.
Files changed (48) hide show
  1. data/.document +5 -0
  2. data/.rspec +3 -0
  3. data/Gemfile +26 -0
  4. data/Guardfile +22 -0
  5. data/LICENSE.txt +20 -0
  6. data/README.rdoc +19 -0
  7. data/Rakefile +49 -0
  8. data/VERSION +1 -0
  9. data/lib/old_api_resource.rb +70 -0
  10. data/lib/old_api_resource/associations.rb +192 -0
  11. data/lib/old_api_resource/associations/association_proxy.rb +92 -0
  12. data/lib/old_api_resource/associations/multi_object_proxy.rb +74 -0
  13. data/lib/old_api_resource/associations/related_object_hash.rb +12 -0
  14. data/lib/old_api_resource/associations/relation_scope.rb +24 -0
  15. data/lib/old_api_resource/associations/resource_scope.rb +25 -0
  16. data/lib/old_api_resource/associations/scope.rb +88 -0
  17. data/lib/old_api_resource/associations/single_object_proxy.rb +64 -0
  18. data/lib/old_api_resource/attributes.rb +162 -0
  19. data/lib/old_api_resource/base.rb +548 -0
  20. data/lib/old_api_resource/callbacks.rb +49 -0
  21. data/lib/old_api_resource/connection.rb +167 -0
  22. data/lib/old_api_resource/core_extensions.rb +7 -0
  23. data/lib/old_api_resource/custom_methods.rb +119 -0
  24. data/lib/old_api_resource/exceptions.rb +85 -0
  25. data/lib/old_api_resource/formats.rb +14 -0
  26. data/lib/old_api_resource/formats/json_format.rb +25 -0
  27. data/lib/old_api_resource/formats/xml_format.rb +36 -0
  28. data/lib/old_api_resource/log_subscriber.rb +15 -0
  29. data/lib/old_api_resource/mocks.rb +260 -0
  30. data/lib/old_api_resource/model_errors.rb +86 -0
  31. data/lib/old_api_resource/observing.rb +29 -0
  32. data/lib/old_api_resource/railtie.rb +18 -0
  33. data/old_api_resource.gemspec +134 -0
  34. data/spec/lib/associations_spec.rb +519 -0
  35. data/spec/lib/attributes_spec.rb +121 -0
  36. data/spec/lib/base_spec.rb +499 -0
  37. data/spec/lib/callbacks_spec.rb +68 -0
  38. data/spec/lib/mocks_spec.rb +28 -0
  39. data/spec/lib/model_errors_spec.rb +29 -0
  40. data/spec/spec_helper.rb +36 -0
  41. data/spec/support/mocks/association_mocks.rb +46 -0
  42. data/spec/support/mocks/error_resource_mocks.rb +21 -0
  43. data/spec/support/mocks/test_resource_mocks.rb +43 -0
  44. data/spec/support/requests/association_requests.rb +14 -0
  45. data/spec/support/requests/error_resource_requests.rb +25 -0
  46. data/spec/support/requests/test_resource_requests.rb +31 -0
  47. data/spec/support/test_resource.rb +50 -0
  48. 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