api_resource 0.4.1 → 0.4.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (116) hide show
  1. data/Gemfile +37 -0
  2. data/Gemfile.lock +190 -0
  3. data/Guardfile +27 -0
  4. data/Rakefile +49 -0
  5. data/VERSION +1 -0
  6. data/api_resource.gemspec +111 -0
  7. data/coverage/assets/0.5.3/app.js +88 -0
  8. data/coverage/assets/0.5.3/fancybox/blank.gif +0 -0
  9. data/coverage/assets/0.5.3/fancybox/fancy_close.png +0 -0
  10. data/coverage/assets/0.5.3/fancybox/fancy_loading.png +0 -0
  11. data/coverage/assets/0.5.3/fancybox/fancy_nav_left.png +0 -0
  12. data/coverage/assets/0.5.3/fancybox/fancy_nav_right.png +0 -0
  13. data/coverage/assets/0.5.3/fancybox/fancy_shadow_e.png +0 -0
  14. data/coverage/assets/0.5.3/fancybox/fancy_shadow_n.png +0 -0
  15. data/coverage/assets/0.5.3/fancybox/fancy_shadow_ne.png +0 -0
  16. data/coverage/assets/0.5.3/fancybox/fancy_shadow_nw.png +0 -0
  17. data/coverage/assets/0.5.3/fancybox/fancy_shadow_s.png +0 -0
  18. data/coverage/assets/0.5.3/fancybox/fancy_shadow_se.png +0 -0
  19. data/coverage/assets/0.5.3/fancybox/fancy_shadow_sw.png +0 -0
  20. data/coverage/assets/0.5.3/fancybox/fancy_shadow_w.png +0 -0
  21. data/coverage/assets/0.5.3/fancybox/fancy_title_left.png +0 -0
  22. data/coverage/assets/0.5.3/fancybox/fancy_title_main.png +0 -0
  23. data/coverage/assets/0.5.3/fancybox/fancy_title_over.png +0 -0
  24. data/coverage/assets/0.5.3/fancybox/fancy_title_right.png +0 -0
  25. data/coverage/assets/0.5.3/fancybox/fancybox-x.png +0 -0
  26. data/coverage/assets/0.5.3/fancybox/fancybox-y.png +0 -0
  27. data/coverage/assets/0.5.3/fancybox/fancybox.png +0 -0
  28. data/coverage/assets/0.5.3/fancybox/jquery.fancybox-1.3.1.css +363 -0
  29. data/coverage/assets/0.5.3/fancybox/jquery.fancybox-1.3.1.pack.js +44 -0
  30. data/coverage/assets/0.5.3/favicon_green.png +0 -0
  31. data/coverage/assets/0.5.3/favicon_red.png +0 -0
  32. data/coverage/assets/0.5.3/favicon_yellow.png +0 -0
  33. data/coverage/assets/0.5.3/highlight.css +129 -0
  34. data/coverage/assets/0.5.3/highlight.pack.js +1 -0
  35. data/coverage/assets/0.5.3/jquery-1.6.2.min.js +18 -0
  36. data/coverage/assets/0.5.3/jquery.dataTables.min.js +152 -0
  37. data/coverage/assets/0.5.3/jquery.timeago.js +141 -0
  38. data/coverage/assets/0.5.3/jquery.url.js +174 -0
  39. data/coverage/assets/0.5.3/loading.gif +0 -0
  40. data/coverage/assets/0.5.3/magnify.png +0 -0
  41. data/coverage/assets/0.5.3/smoothness/images/ui-bg_flat_0_aaaaaa_40x100.png +0 -0
  42. data/coverage/assets/0.5.3/smoothness/images/ui-bg_flat_75_ffffff_40x100.png +0 -0
  43. data/coverage/assets/0.5.3/smoothness/images/ui-bg_glass_55_fbf9ee_1x400.png +0 -0
  44. data/coverage/assets/0.5.3/smoothness/images/ui-bg_glass_65_ffffff_1x400.png +0 -0
  45. data/coverage/assets/0.5.3/smoothness/images/ui-bg_glass_75_dadada_1x400.png +0 -0
  46. data/coverage/assets/0.5.3/smoothness/images/ui-bg_glass_75_e6e6e6_1x400.png +0 -0
  47. data/coverage/assets/0.5.3/smoothness/images/ui-bg_glass_95_fef1ec_1x400.png +0 -0
  48. data/coverage/assets/0.5.3/smoothness/images/ui-bg_highlight-soft_75_cccccc_1x100.png +0 -0
  49. data/coverage/assets/0.5.3/smoothness/images/ui-icons_222222_256x240.png +0 -0
  50. data/coverage/assets/0.5.3/smoothness/images/ui-icons_2e83ff_256x240.png +0 -0
  51. data/coverage/assets/0.5.3/smoothness/images/ui-icons_454545_256x240.png +0 -0
  52. data/coverage/assets/0.5.3/smoothness/images/ui-icons_888888_256x240.png +0 -0
  53. data/coverage/assets/0.5.3/smoothness/images/ui-icons_cd0a0a_256x240.png +0 -0
  54. data/coverage/assets/0.5.3/smoothness/jquery-ui-1.8.4.custom.css +295 -0
  55. data/coverage/assets/0.5.3/stylesheet.css +383 -0
  56. data/coverage/index.html +3573 -0
  57. data/lib/api_resource.rb +130 -0
  58. data/lib/api_resource/association_activation.rb +19 -0
  59. data/lib/api_resource/associations.rb +218 -0
  60. data/lib/api_resource/associations/association_proxy.rb +116 -0
  61. data/lib/api_resource/associations/belongs_to_remote_object_proxy.rb +16 -0
  62. data/lib/api_resource/associations/dynamic_resource_scope.rb +23 -0
  63. data/lib/api_resource/associations/generic_scope.rb +68 -0
  64. data/lib/api_resource/associations/has_many_remote_object_proxy.rb +16 -0
  65. data/lib/api_resource/associations/has_many_through_remote_object_proxy.rb +13 -0
  66. data/lib/api_resource/associations/has_one_remote_object_proxy.rb +24 -0
  67. data/lib/api_resource/associations/multi_argument_resource_scope.rb +15 -0
  68. data/lib/api_resource/associations/multi_object_proxy.rb +84 -0
  69. data/lib/api_resource/associations/related_object_hash.rb +12 -0
  70. data/lib/api_resource/associations/relation_scope.rb +25 -0
  71. data/lib/api_resource/associations/resource_scope.rb +32 -0
  72. data/lib/api_resource/associations/scope.rb +132 -0
  73. data/lib/api_resource/associations/single_object_proxy.rb +82 -0
  74. data/lib/api_resource/attributes.rb +243 -0
  75. data/lib/api_resource/base.rb +717 -0
  76. data/lib/api_resource/callbacks.rb +45 -0
  77. data/lib/api_resource/connection.rb +195 -0
  78. data/lib/api_resource/core_extensions.rb +7 -0
  79. data/lib/api_resource/custom_methods.rb +117 -0
  80. data/lib/api_resource/decorators.rb +6 -0
  81. data/lib/api_resource/decorators/caching_decorator.rb +20 -0
  82. data/lib/api_resource/exceptions.rb +99 -0
  83. data/lib/api_resource/formats.rb +22 -0
  84. data/lib/api_resource/formats/json_format.rb +25 -0
  85. data/lib/api_resource/formats/xml_format.rb +36 -0
  86. data/lib/api_resource/local.rb +12 -0
  87. data/lib/api_resource/log_subscriber.rb +15 -0
  88. data/lib/api_resource/mocks.rb +285 -0
  89. data/lib/api_resource/model_errors.rb +82 -0
  90. data/lib/api_resource/observing.rb +27 -0
  91. data/lib/api_resource/railtie.rb +24 -0
  92. data/lib/api_resource/scopes.rb +48 -0
  93. data/nohup.out +63 -0
  94. data/spec/lib/api_resource_spec.rb +43 -0
  95. data/spec/lib/associations_spec.rb +751 -0
  96. data/spec/lib/attributes_spec.rb +191 -0
  97. data/spec/lib/base_spec.rb +655 -0
  98. data/spec/lib/callbacks_spec.rb +68 -0
  99. data/spec/lib/connection_spec.rb +137 -0
  100. data/spec/lib/local_spec.rb +20 -0
  101. data/spec/lib/mocks_spec.rb +74 -0
  102. data/spec/lib/model_errors_spec.rb +29 -0
  103. data/spec/lib/prefixes_spec.rb +107 -0
  104. data/spec/spec_helper.rb +82 -0
  105. data/spec/support/mocks/association_mocks.rb +63 -0
  106. data/spec/support/mocks/error_resource_mocks.rb +21 -0
  107. data/spec/support/mocks/prefix_model_mocks.rb +5 -0
  108. data/spec/support/mocks/test_resource_mocks.rb +44 -0
  109. data/spec/support/requests/association_requests.rb +31 -0
  110. data/spec/support/requests/error_resource_requests.rb +25 -0
  111. data/spec/support/requests/prefix_model_requests.rb +7 -0
  112. data/spec/support/requests/test_resource_requests.rb +38 -0
  113. data/spec/support/test_resource.rb +72 -0
  114. data/spec/tmp/DIR +0 -0
  115. data/spec/tmp/api_resource_test_db.sqlite +0 -0
  116. metadata +119 -3
@@ -0,0 +1,130 @@
1
+ require 'active_support'
2
+ require 'active_support/inflector'
3
+ require 'active_support/core_ext'
4
+ require 'api_resource/core_extensions'
5
+
6
+ require 'active_model'
7
+
8
+ require 'log4r'
9
+ require 'log4r/outputter/consoleoutputters'
10
+
11
+ require 'api_resource/exceptions'
12
+
13
+ require 'differ'
14
+ require 'colorize'
15
+
16
+ module ApiResource
17
+
18
+ extend ActiveSupport::Autoload
19
+
20
+ autoload :Associations
21
+ autoload :AssociationActivation
22
+ autoload :Attributes
23
+ autoload :Base
24
+ autoload :Callbacks
25
+ autoload :Connection
26
+ autoload :CustomMethods
27
+ autoload :Decorators
28
+ autoload :Formats
29
+ autoload :Local
30
+ autoload :LogSubscriber
31
+ autoload :Mocks
32
+ autoload :ModelErrors
33
+ autoload :Observing
34
+ autoload :Scopes
35
+ autoload :Validations
36
+
37
+ require 'api_resource/railtie'
38
+
39
+ mattr_writer :logger
40
+ mattr_accessor :raise_missing_definition_error; self.raise_missing_definition_error = false
41
+
42
+ DEFAULT_TIMEOUT = 10 # seconds
43
+
44
+ # Load a fix for inflections for words ending in ess
45
+ ActiveSupport::Inflector.inflections do |inflect|
46
+ inflect.singular(/ess$/i, 'ess')
47
+ end
48
+
49
+ def self.load_mocks_and_factories
50
+ require 'hash_dealer'
51
+ Mocks.clear_endpoints
52
+ Mocks.init
53
+
54
+ Dir["#{File.dirname(__FILE__)}/../spec/support/requests/*.rb"].each {|f|
55
+ require f
56
+ }
57
+ Dir["#{File.dirname(__FILE__)}/../spec/support/**/*.rb"].each {|f|
58
+ require f
59
+ }
60
+ end
61
+
62
+ class << self
63
+
64
+ delegate :site, :site=, :format, :format=,
65
+ :token, :token=, :timeout,
66
+ :open_timeout,
67
+ :reset_connection, :ttl, :ttl=,
68
+ :to => ApiResource::Base
69
+
70
+ end
71
+
72
+ def self.cache(reset = false)
73
+ @cache = nil if reset
74
+ @cache ||= begin
75
+ defined?(Rails) ? Rails.cache : ActiveSupport::Cache::MemoryStore.new
76
+ end
77
+ end
78
+
79
+ # set the timeout val and reset the connection
80
+ def self.timeout=(val)
81
+ ApiResource::Base.timeout = val
82
+ self.reset_connection
83
+ val
84
+ end
85
+
86
+ # set the timeout val and reset the connection
87
+ def self.open_timeout=(val)
88
+ ApiResource::Base.open_timeout = val
89
+ self.reset_connection
90
+ val
91
+ end
92
+ self.timeout = self.open_timeout = DEFAULT_TIMEOUT
93
+
94
+ # Run a block with a given token - useful for AroundFilters
95
+ def self.with_token(new_token, &block)
96
+ old_token = self.token
97
+ begin
98
+ self.token = new_token
99
+ yield
100
+ ensure
101
+ self.token = old_token
102
+ end
103
+ end
104
+
105
+ def self.with_ttl(new_ttl, &block)
106
+ old_ttl = self.ttl
107
+ begin
108
+ self.ttl = new_ttl
109
+ yield
110
+ ensure
111
+ self.ttl = old_ttl
112
+ end
113
+ end
114
+
115
+ # logger
116
+ def self.logger
117
+ return @logger if @logger
118
+ @logger = Log4r::Logger.new("api_resource")
119
+ @logger.outputters = [Log4r::StdoutOutputter.new('console')]
120
+ @logger.level = Log4r::INFO
121
+ @logger
122
+ end
123
+
124
+ # Use this method to enable logging in the future
125
+ # def self.logging(val = nil)
126
+ # return (@@logging || false) unless val
127
+ # return @@logging = val
128
+ # end
129
+
130
+ end
@@ -0,0 +1,19 @@
1
+ module ApiResource
2
+ module AssociationActivation
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ class_attribute :association_types
7
+ # our default association types
8
+ self.association_types = {:belongs_to => :single, :has_one => :single, :has_many => :multi}
9
+ end
10
+
11
+ module ClassMethods
12
+ def activate_associations(assoc_types = nil)
13
+ self.association_types = assoc_types unless assoc_types.nil?
14
+ self.send(:include, ApiResource::Associations)
15
+ end
16
+ end
17
+
18
+ end
19
+ end
@@ -0,0 +1,218 @@
1
+ require 'active_support'
2
+ require 'active_support/string_inquirer'
3
+ require 'api_resource/association_activation'
4
+ require 'api_resource/associations/relation_scope'
5
+ require 'api_resource/associations/resource_scope'
6
+ require 'api_resource/associations/dynamic_resource_scope'
7
+ require 'api_resource/associations/generic_scope'
8
+ require 'api_resource/associations/multi_argument_resource_scope'
9
+ require 'api_resource/associations/multi_object_proxy'
10
+ require 'api_resource/associations/single_object_proxy'
11
+ require 'api_resource/associations/belongs_to_remote_object_proxy'
12
+ require 'api_resource/associations/has_one_remote_object_proxy'
13
+ require 'api_resource/associations/has_many_remote_object_proxy'
14
+ require 'api_resource/associations/has_many_through_remote_object_proxy'
15
+ require 'api_resource/associations/related_object_hash'
16
+
17
+ module ApiResource
18
+
19
+ module Associations
20
+ extend ActiveSupport::Concern
21
+
22
+ included do
23
+
24
+ unless self.ancestors.include?(ApiResource::AssociationActivation)
25
+ raise Exception.new(
26
+ "Can't include Associations without AssociationActivation"
27
+ )
28
+ end
29
+
30
+ class_attribute :related_objects
31
+ attr_accessor :assoc_attributes
32
+
33
+ define_method(:assoc_attributes) do
34
+ @assoc_attributes ||= Hash.new
35
+ end
36
+
37
+ self.clear_related_objects
38
+
39
+ # we need to add an inherited method here, but it can't happen
40
+ # until after this module in included/extended, so it's in its own
41
+ # little mini module
42
+ extend InheritedMethod
43
+
44
+ self.define_association_methods
45
+
46
+ end
47
+
48
+ # module methods to include the proper associations in various libraries - this is usually loaded in Railties
49
+ def self.activate_active_record
50
+ ActiveRecord::Base.class_eval do
51
+ include ApiResource::AssociationActivation
52
+ self.activate_associations(
53
+ :has_many_remote => :has_many_remote,
54
+ :belongs_to_remote => :belongs_to_remote,
55
+ :has_one_remote => :has_one_remote,
56
+ )
57
+ end
58
+ end
59
+
60
+ module InheritedMethod
61
+ # define a method to reset our related objects
62
+ def inherited(descendant)
63
+ # we only want to do this in direct descendants of ApiResoruce::Base
64
+ if self == ApiResource::Base
65
+ descendant.clear_related_objects
66
+ else
67
+ descendant.clone_related_objects
68
+ end
69
+ super(descendant)
70
+ end
71
+ end
72
+
73
+ module ClassMethods
74
+
75
+ # Define the methods for creating and testing for associations, unfortunately
76
+ # scopes are different enough to require different methods :(
77
+ def define_association_methods
78
+ self.association_types.each_key do |assoc|
79
+ self.instance_eval <<-EOE, __FILE__, __LINE__ + 1
80
+ def #{assoc}(*args)
81
+ options = args.extract_options!
82
+ options = options.with_indifferent_access
83
+ # Raise an error if we have multiple args and options
84
+ raise "Invalid arguments to #{assoc}" unless options.blank? || args.length == 1
85
+ args.each do |arg|
86
+ klass_name = (options[:class_name] ? options[:class_name].to_s.classify : arg.to_s.classify)
87
+ # add this to any descendants - the other methods etc are handled by inheritance
88
+ ([self] + self.descendants).each do |klass|
89
+ #We need to merge upon itself to generate a new object since the children all share their related objects with each other
90
+ klass.related_objects = klass.related_objects.merge(:#{assoc} => klass.related_objects[:#{assoc}].merge(arg.to_sym => klass_name))
91
+ end
92
+ # We need to define reader and writer methods here
93
+ define_association_as_attribute(:#{assoc}, arg)
94
+ end
95
+ end
96
+
97
+ def #{assoc}?(name)
98
+ return self.related_objects[:#{assoc}][name.to_s.pluralize.to_sym].present? || self.related_objects[:#{assoc}][name.to_s.singularize.to_sym].present?
99
+ end
100
+
101
+ def #{assoc}_class_name(name)
102
+ raise "No such" + :#{assoc}.to_s + " association on #{name}" unless self.#{assoc}?(name)
103
+ return self.find_namespaced_class_name(self.related_objects[:#{assoc}][name.to_sym])
104
+ end
105
+
106
+ EOE
107
+ # For convenience we will define the methods for testing for the existence of an association
108
+ # and getting the class for an association as instance methods too to avoid tons of self.class calls
109
+ self.class_eval <<-EOE, __FILE__, __LINE__ + 1
110
+ def #{assoc}?(name)
111
+ return self.class.#{assoc}?(name)
112
+ end
113
+
114
+ def #{assoc}_class_name(name)
115
+ return self.class.#{assoc}_class_name(name)
116
+ end
117
+ EOE
118
+ end
119
+ end
120
+
121
+ def association?(assoc)
122
+ self.related_objects.any? do |key, value|
123
+ next if key.to_s == "scopes"
124
+ value.detect { |k,v| k.to_sym == assoc.to_sym }
125
+ end
126
+ end
127
+
128
+ def association_names
129
+ # structure is {:has_many => {"myname" => "ClassName"}}
130
+ self.related_objects.clone.delete_if{|k,v| k.to_s == "scopes"}.collect{|k,v| v.keys.collect(&:to_sym)}.flatten
131
+ end
132
+
133
+ def association_class_name(assoc)
134
+ raise ArgumentError, "#{assoc} is not a valid association of #{self}" unless self.association?(assoc)
135
+ result = self.related_objects.detect do |key,value|
136
+ ret = value.detect{|k,v| k.to_sym == assoc.to_sym }
137
+ return self.find_namespaced_class_name(ret[1]) if ret
138
+ end
139
+ end
140
+
141
+ protected
142
+
143
+ def clear_related_objects
144
+ # Hash to hold onto the definitions of the related objects
145
+ self.related_objects = RelatedObjectHash.new
146
+ self.association_types.keys.each do |type|
147
+ self.related_objects[type] = RelatedObjectHash.new({})
148
+ end
149
+
150
+ # TODO :Remove scopes from related_objects.
151
+ self.related_objects[:scopes] = RelatedObjectHash.new({})
152
+ end
153
+
154
+ def clone_related_objects
155
+ # Hash to hold onto the definitions of the related objects
156
+ self.related_objects = self.related_objects.clone
157
+ self.association_types.keys.each do |type|
158
+ self.related_objects[type] = self.related_objects[type].clone
159
+ end
160
+ # TODO :Remove scopes from related_objects.
161
+ self.related_objects[:scopes] = self.related_objects[:scopes].clone
162
+ end
163
+
164
+ def define_association_as_attribute(assoc_type, assoc_name)
165
+ # set up dirty tracking for associations
166
+
167
+ self.class_eval <<-EOE, __FILE__, __LINE__ + 1
168
+ def #{assoc_name}
169
+ self.assoc_attributes[:#{assoc_name}] ||= (self.attributes[:#{assoc_name}] || #{self.association_types[assoc_type.to_sym].to_s.classify}ObjectProxy.new(self.association_class_name('#{assoc_name}'), nil, self))
170
+ end
171
+ def #{assoc_name}=(val)
172
+ # get old internal object
173
+ old_internal_object = self.#{assoc_name}.internal_object
174
+ self.#{assoc_name}.internal_object = val
175
+ #{assoc_name}_will_change! unless self.#{assoc_name} == old_internal_object
176
+ self.#{assoc_name}.internal_object
177
+ end
178
+ def #{assoc_name}?
179
+ self.#{assoc_name}.internal_object.present?
180
+ end
181
+ EOE
182
+ end
183
+
184
+ def find_namespaced_class_name(klass)
185
+ # return the name if it is itself namespaced
186
+ return klass if klass =~ /::/
187
+ ancestors = self.name.split("::")
188
+ if ancestors.size > 1
189
+ receiver = Object
190
+ namespaces = ancestors[0..-2].collect do |mod|
191
+ receiver = receiver.const_get(mod)
192
+ end
193
+ if namespace = namespaces.reverse.detect{|ns| ns.const_defined?(klass, false)}
194
+ return namespace.const_get(klass).name
195
+ end
196
+ end
197
+
198
+ return klass
199
+ end
200
+
201
+ end
202
+
203
+ def association?(assoc)
204
+ self.class.association?(assoc)
205
+ end
206
+
207
+ def association_class_name(assoc)
208
+ self.class.association_class_name(assoc)
209
+ end
210
+
211
+ # list of all association names
212
+ def association_names
213
+ self.class.association_names
214
+ end
215
+
216
+ end
217
+
218
+ end
@@ -0,0 +1,116 @@
1
+ module ApiResource
2
+
3
+ module Associations
4
+
5
+ class AssociationProxy
6
+
7
+
8
+ cattr_accessor :remote_path_element; self.remote_path_element = :service_uri
9
+ cattr_accessor :include_class_scopes; self.include_class_scopes = true
10
+
11
+ attr_accessor :owner, :loaded, :klass, :internal_object, :remote_path, :scopes, :times_loaded
12
+
13
+ # TODO: added owner - moved it to the end because the tests don't use it - it's useful here though
14
+ def initialize(klass_name, contents, owner = nil)
15
+ raise "Cannot create an association proxy to the unknown object #{klass_name}" unless defined?(klass_name.to_s.classify)
16
+ # A simple attr_accessor for testing purposes
17
+ self.times_loaded = 0
18
+ self.owner = owner
19
+ self.klass = klass_name.to_s.classify.constantize
20
+ self.load(contents)
21
+ self.loaded = {}.with_indifferent_access
22
+ if self.class.include_class_scopes
23
+ self.scopes = self.scopes.reverse_merge(self.klass.scopes)
24
+ end
25
+ # Now that we have set up all the scopes with the load method we need to create methods
26
+ self.scopes.each do |key, _|
27
+ next if self.respond_to?(key)
28
+ self.instance_eval <<-EOE, __FILE__, __LINE__ + 1
29
+ def #{key}(opts = {})
30
+ @#{key} ||= ApiResource::Associations::RelationScope.new(self, :#{key}, opts)
31
+ end
32
+ EOE
33
+ end
34
+ end
35
+
36
+ def serializable_hash(options = {})
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.keys.include?(scp.to_s)
46
+ end
47
+
48
+ def internal_object
49
+ @internal_object ||= self.load_scope_with_options(:all, {})
50
+ end
51
+
52
+ def blank?
53
+ self.internal_object.blank?
54
+ end
55
+ alias_method :empty?, :blank?
56
+
57
+ def method_missing(method, *args, &block)
58
+ self.internal_object.send(method, *args, &block)
59
+ end
60
+
61
+ def reload
62
+ if self
63
+ remove_instance_variable(:@internal_object) if instance_variable_defined?(:@internal_object)
64
+ self.load(self.load_from_remote({}))
65
+ self
66
+ end
67
+ end
68
+
69
+ def to_s
70
+ self.internal_object.to_s
71
+ end
72
+
73
+ def inspect
74
+ self.internal_object.inspect
75
+ end
76
+
77
+ def ==(other)
78
+ raise "Not Implemented: This method must be implemented in a subclass"
79
+ end
80
+
81
+ protected
82
+ # This method loads a particular scope with a set of options from the remote server
83
+ def load_scope_with_options(scope, options)
84
+ raise "Not Implemented: This method must be implemented in a subclass"
85
+ end
86
+ # This method is a helper to initialize for loading the data passed in to create this object
87
+ def load(contents)
88
+ raise "Not Implemented: This method must be implemented in a subclass"
89
+ end
90
+
91
+ # get the remote URI based on our config and options
92
+ def build_load_path(options)
93
+ path = self.remote_path
94
+ # add a format if it doesn't exist and there is no query string yet
95
+ path += ".#{self.klass.format.extension}" unless path =~ /\./ || path =~/\?/
96
+ # add the query string, allowing for other user-provided options in the remote_path if we have options
97
+ unless options.blank?
98
+ path += (path =~ /\?/ ? "&" : "?") + options.to_query
99
+ end
100
+ path
101
+ end
102
+
103
+ # get data from the remote server
104
+ def load_from_remote(options)
105
+ self.klass.connection.get(self.build_load_path(options))
106
+ end
107
+ # This method create the key for the loaded hash, it ensures that a unique set of scopes
108
+ # with a unique set of options is only loaded once
109
+ def loaded_hash_key(scope, options)
110
+ options.to_a.sort.inject(scope) {|accum,(k,v)| accum << "_#{k}_#{v}"}
111
+ end
112
+ end
113
+
114
+ end
115
+
116
+ end