api_resource 0.4.3 → 0.5.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 (89) hide show
  1. data/VERSION +1 -1
  2. data/api_resource.gemspec +4 -74
  3. data/coverage/assets/0.5.3/app.js +88 -0
  4. data/coverage/assets/0.5.3/fancybox/blank.gif +0 -0
  5. data/coverage/assets/0.5.3/fancybox/fancy_close.png +0 -0
  6. data/coverage/assets/0.5.3/fancybox/fancy_loading.png +0 -0
  7. data/coverage/assets/0.5.3/fancybox/fancy_nav_left.png +0 -0
  8. data/coverage/assets/0.5.3/fancybox/fancy_nav_right.png +0 -0
  9. data/coverage/assets/0.5.3/fancybox/fancy_shadow_e.png +0 -0
  10. data/coverage/assets/0.5.3/fancybox/fancy_shadow_n.png +0 -0
  11. data/coverage/assets/0.5.3/fancybox/fancy_shadow_ne.png +0 -0
  12. data/coverage/assets/0.5.3/fancybox/fancy_shadow_nw.png +0 -0
  13. data/coverage/assets/0.5.3/fancybox/fancy_shadow_s.png +0 -0
  14. data/coverage/assets/0.5.3/fancybox/fancy_shadow_se.png +0 -0
  15. data/coverage/assets/0.5.3/fancybox/fancy_shadow_sw.png +0 -0
  16. data/coverage/assets/0.5.3/fancybox/fancy_shadow_w.png +0 -0
  17. data/coverage/assets/0.5.3/fancybox/fancy_title_left.png +0 -0
  18. data/coverage/assets/0.5.3/fancybox/fancy_title_main.png +0 -0
  19. data/coverage/assets/0.5.3/fancybox/fancy_title_over.png +0 -0
  20. data/coverage/assets/0.5.3/fancybox/fancy_title_right.png +0 -0
  21. data/coverage/assets/0.5.3/fancybox/fancybox-x.png +0 -0
  22. data/coverage/assets/0.5.3/fancybox/fancybox-y.png +0 -0
  23. data/coverage/assets/0.5.3/fancybox/fancybox.png +0 -0
  24. data/coverage/assets/0.5.3/fancybox/jquery.fancybox-1.3.1.css +363 -0
  25. data/coverage/assets/0.5.3/fancybox/jquery.fancybox-1.3.1.pack.js +44 -0
  26. data/coverage/assets/0.5.3/favicon_green.png +0 -0
  27. data/coverage/assets/0.5.3/favicon_red.png +0 -0
  28. data/coverage/assets/0.5.3/favicon_yellow.png +0 -0
  29. data/coverage/assets/0.5.3/highlight.css +129 -0
  30. data/coverage/assets/0.5.3/highlight.pack.js +1 -0
  31. data/coverage/assets/0.5.3/jquery-1.6.2.min.js +18 -0
  32. data/coverage/assets/0.5.3/jquery.dataTables.min.js +152 -0
  33. data/coverage/assets/0.5.3/jquery.timeago.js +141 -0
  34. data/coverage/assets/0.5.3/jquery.url.js +174 -0
  35. data/coverage/assets/0.5.3/loading.gif +0 -0
  36. data/coverage/assets/0.5.3/magnify.png +0 -0
  37. data/coverage/assets/0.5.3/smoothness/images/ui-bg_flat_0_aaaaaa_40x100.png +0 -0
  38. data/coverage/assets/0.5.3/smoothness/images/ui-bg_flat_75_ffffff_40x100.png +0 -0
  39. data/coverage/assets/0.5.3/smoothness/images/ui-bg_glass_55_fbf9ee_1x400.png +0 -0
  40. data/coverage/assets/0.5.3/smoothness/images/ui-bg_glass_65_ffffff_1x400.png +0 -0
  41. data/coverage/assets/0.5.3/smoothness/images/ui-bg_glass_75_dadada_1x400.png +0 -0
  42. data/coverage/assets/0.5.3/smoothness/images/ui-bg_glass_75_e6e6e6_1x400.png +0 -0
  43. data/coverage/assets/0.5.3/smoothness/images/ui-bg_glass_95_fef1ec_1x400.png +0 -0
  44. data/coverage/assets/0.5.3/smoothness/images/ui-bg_highlight-soft_75_cccccc_1x100.png +0 -0
  45. data/coverage/assets/0.5.3/smoothness/images/ui-icons_222222_256x240.png +0 -0
  46. data/coverage/assets/0.5.3/smoothness/images/ui-icons_2e83ff_256x240.png +0 -0
  47. data/coverage/assets/0.5.3/smoothness/images/ui-icons_454545_256x240.png +0 -0
  48. data/coverage/assets/0.5.3/smoothness/images/ui-icons_888888_256x240.png +0 -0
  49. data/coverage/assets/0.5.3/smoothness/images/ui-icons_cd0a0a_256x240.png +0 -0
  50. data/coverage/assets/0.5.3/smoothness/jquery-ui-1.8.4.custom.css +295 -0
  51. data/coverage/assets/0.5.3/stylesheet.css +383 -0
  52. data/coverage/index.html +3573 -0
  53. data/lib/api_resource/associations/abstract_scope.rb +191 -0
  54. data/lib/api_resource/associations/association_scope.rb +47 -0
  55. data/lib/api_resource/associations/belongs_to_remote_object_proxy.rb +5 -6
  56. data/lib/api_resource/associations/has_many_remote_object_proxy.rb +5 -8
  57. data/lib/api_resource/associations/has_one_remote_object_proxy.rb +12 -13
  58. data/lib/api_resource/associations/multi_object_proxy.rb +65 -39
  59. data/lib/api_resource/associations/resource_scope.rb +6 -17
  60. data/lib/api_resource/associations/scope.rb +23 -121
  61. data/lib/api_resource/associations/single_object_proxy.rb +41 -50
  62. data/lib/api_resource/associations.rb +32 -11
  63. data/lib/api_resource/attributes.rb +108 -69
  64. data/lib/api_resource/base.rb +114 -106
  65. data/lib/api_resource/local.rb +1 -1
  66. data/lib/api_resource/model_errors.rb +9 -6
  67. data/lib/api_resource/scopes.rb +53 -16
  68. data/lib/api_resource.rb +3 -1
  69. data/spec/lib/api_resource_spec.rb +3 -7
  70. data/spec/lib/associations/association_scope_spec.rb +19 -0
  71. data/spec/lib/associations_spec.rb +251 -162
  72. data/spec/lib/attributes_spec.rb +33 -15
  73. data/spec/lib/base_spec.rb +302 -64
  74. data/spec/lib/callbacks_spec.rb +4 -2
  75. data/spec/lib/local_spec.rb +5 -1
  76. data/spec/spec_helper.rb +2 -3
  77. data/spec/support/mocks/association_mocks.rb +9 -1
  78. data/spec/support/requests/association_requests.rb +5 -5
  79. data/spec/support/requests/test_resource_requests.rb +16 -4
  80. data/spec/tmp/api_resource_test_db.sqlite +0 -0
  81. metadata +68 -22
  82. data/.document +0 -5
  83. data/.rspec +0 -5
  84. data/.travis.yml +0 -4
  85. data/lib/api_resource/associations/association_proxy.rb +0 -121
  86. data/lib/api_resource/associations/dynamic_resource_scope.rb +0 -23
  87. data/lib/api_resource/associations/generic_scope.rb +0 -68
  88. data/lib/api_resource/associations/multi_argument_resource_scope.rb +0 -15
  89. data/lib/api_resource/associations/relation_scope.rb +0 -25
@@ -0,0 +1,191 @@
1
+ module ApiResource
2
+
3
+ module Associations
4
+
5
+ class AbstractScope
6
+
7
+ attr_reader :klass, :finder_opts
8
+
9
+ def initialize(klass, finder_opts = {})
10
+
11
+ # the base class for our scope, e.g. ApiResource::SomeClass
12
+ @klass = klass.is_a?(String) ? klass.constantize : klass
13
+
14
+ # load the resource definition
15
+ @klass.load_resource_definition
16
+
17
+ # the parent scope - for composing all of the finder options
18
+ @parent = finder_opts.delete(:__parent)
19
+
20
+ @finder_opts = finder_opts
21
+
22
+ # Where subscope is any scope down the chain, e.g. active.*future*
23
+ @klass.scopes.each do |scope_name, scope_definition|
24
+ self.define_subscope(scope_name, scope_definition)
25
+ end
26
+ end
27
+
28
+ def ttl
29
+ @ttl || 0
30
+ end
31
+
32
+ # Use this method to access the internal data, this guarantees that loading only occurs once per object
33
+ def internal_object
34
+ if instance_variable_defined?(:@internal_object)
35
+ return instance_variable_get(:@internal_object)
36
+ end
37
+ instance_variable_set(:@internal_object, self.load)
38
+ end
39
+
40
+ # has the scope been loaded?
41
+ def loaded?
42
+ @loaded == true
43
+ end
44
+
45
+ def load_resource_definition
46
+ self.klass.load_resource_definition
47
+ end
48
+
49
+ def scopes
50
+ @scopes ||= HashWithIndifferentAccess.new
51
+ end
52
+
53
+ def scope?(scp)
54
+ self.scopes.key?(scp.to_s)
55
+ end
56
+
57
+ # def current_scope
58
+ # ActiveSupport::StringInquirer.new(@current_scope.join("_and_").concat("_scope"))
59
+ # end
60
+
61
+ def to_hash
62
+ self.parent_hash.merge(self.finder_opts)
63
+ end
64
+
65
+ # takes empty hashes and replaces them with true so that to_query doesn't strip them out
66
+ def to_query_safe_hash(hash)
67
+ hash.each_pair do |k, v|
68
+ hash[k] = to_query_safe_hash(v) if v.is_a?(Hash)
69
+ hash[k] = true if v == {}
70
+ end
71
+ return hash
72
+ end
73
+
74
+ # gets the current hash and calls to_query on it
75
+ def to_query
76
+ #We need to add the unescape because to_query breaks on nested arrays
77
+ CGI.unescape(to_query_safe_hash(self.to_hash).to_query)
78
+ end
79
+
80
+ # unset all of our scope values and our internal object
81
+ def reload
82
+ (self.scopes.keys + [:internal_object]).each do |ivar|
83
+ if instance_variable_defined?("@#{ivar}")
84
+ remove_instance_variable("@#{ivar}")
85
+ end
86
+ end
87
+ @loaded = false
88
+ self
89
+ end
90
+
91
+ def to_s
92
+ self.internal_object.to_s
93
+ end
94
+
95
+ def inspect
96
+ self.internal_object.inspect
97
+ end
98
+
99
+ def blank?
100
+ self.internal_object.blank?
101
+ end
102
+ alias_method :empty?, :blank?
103
+
104
+ def present?
105
+ self.internal_object.present?
106
+ end
107
+
108
+ def expires_in(ttl)
109
+ ApiResource::Decorators::CachingDecorator.new(self, ttl)
110
+ end
111
+
112
+ protected
113
+
114
+ # scope_name => e.g. paged
115
+ # scope_definition => e.g. {:page => "req", "per_page" => "opt"}
116
+
117
+ def define_subscope(scope_name, scope_definition)
118
+
119
+ self.scopes[scope_name] = scope_definition
120
+
121
+ self.class_eval do
122
+
123
+ define_method(scope_name) do |*args|
124
+
125
+ unless instance_variable_defined?("@#{scope_name}")
126
+
127
+ arg_names = scope_definition.keys
128
+ arg_types = scope_definition.values
129
+
130
+ finder_opts = {
131
+ scope_name => {}
132
+ }
133
+
134
+ arg_names.each_with_index do |arg_name, i|
135
+
136
+ # If we are dealing with a scope with multiple args
137
+ if arg_types[i] == :rest
138
+ finder_opts[scope_name][arg_name] =
139
+ args.slice(i, args.count)
140
+ # Else we are only dealing with a single argument
141
+ else
142
+ if arg_types[i] == :req || args[i].present?
143
+ finder_opts[scope_name][arg_name] = args[i]
144
+ end
145
+ end
146
+ end
147
+
148
+ # if we have nothing at this point we should just pass 'true'
149
+ if finder_opts[scope_name] == {}
150
+ finder_opts[scope_name] = true
151
+ end
152
+
153
+ instance_variable_set(
154
+ "@#{scope_name}",
155
+ self.get_subscope_instance(finder_opts)
156
+ )
157
+ end
158
+ instance_variable_get("@#{scope_name}")
159
+ end
160
+ end
161
+ end
162
+
163
+ def get_subscope_instance(finder_opts)
164
+ ApiResource::Associations::Scope.new(
165
+ self, finder_opts.merge(:__parent => self)
166
+ )
167
+ end
168
+
169
+ def method_missing(method, *args, &block)
170
+ self.internal_object.send(method, *args, &block)
171
+ end
172
+
173
+ # querystring hash from parent
174
+ def parent_hash
175
+ @parent ? @parent.to_hash : {}
176
+ end
177
+
178
+ # require our subclasses to implement a way to find records
179
+ def load
180
+ raise NotImplementedError.new("#{self.class} must implement #load")
181
+ end
182
+
183
+ # make sure we have a valid scope
184
+ def check_scope(scp)
185
+ raise ArgumentError, "Unknown scope #{scp}" unless self.scope?(scp.to_s)
186
+ end
187
+ end
188
+
189
+ end
190
+
191
+ end
@@ -0,0 +1,47 @@
1
+ module ApiResource
2
+
3
+ module Associations
4
+
5
+ class AssociationScope < AbstractScope
6
+
7
+ class_attribute :remote_path_element
8
+ self.remote_path_element = :service_uri
9
+
10
+ attr_accessor :remote_path
11
+ attr_reader :owner
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, owner, opts = {})
15
+ super(klass)
16
+
17
+ @owner = owner
18
+
19
+ self.internal_object = opts
20
+ end
21
+
22
+ def ==(other)
23
+ raise "Not Implemented: This method must be implemented in a subclass"
24
+ end
25
+
26
+ def scopes
27
+ self.klass.scopes
28
+ end
29
+
30
+ protected
31
+
32
+ # get the remote URI based on our config and options
33
+ def build_load_path(options)
34
+ path = self.remote_path
35
+ # add a format if it doesn't exist and there is no query string yet
36
+ path += ".#{self.klass.format.extension}" unless path =~ /\./ || path =~/\?/
37
+ # add the query string, allowing for other user-provided options in the remote_path if we have options
38
+ unless options.blank?
39
+ path += (path =~ /\?/ ? "&" : "?") + options.to_query
40
+ end
41
+ path
42
+ end
43
+ end
44
+
45
+ end
46
+
47
+ end
@@ -2,14 +2,13 @@ require 'api_resource/associations/single_object_proxy'
2
2
  module ApiResource
3
3
  module Associations
4
4
  class BelongsToRemoteObjectProxy < SingleObjectProxy
5
- def initialize(klass_name, contents, owner)
6
- super
7
- return if self.internal_object
5
+ def initialize(klass, owner)
6
+ super(klass, owner)
7
+
8
8
  # now if we have an owner and a foreign key, we set the data up to load
9
- if owner && key = owner.send(self.klass.to_s.foreign_key)
10
- self.load({"service_uri" => self.klass.element_path(key), "scopes_only" => true}.merge(self.klass.scopes))
9
+ if key = owner.send(self.klass.to_s.foreign_key)
10
+ self.remote_path = self.klass.element_path(key)
11
11
  end
12
- true
13
12
  end
14
13
  end
15
14
  end
@@ -2,14 +2,11 @@ require 'api_resource/associations/multi_object_proxy'
2
2
  module ApiResource
3
3
  module Associations
4
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
5
+ def initialize(klass, owner)
6
+ super(klass, owner)
7
+ self.remote_path = self.klass.collection_path(
8
+ self.owner.class.to_s.foreign_key => self.owner.id
9
+ )
13
10
  end
14
11
  end
15
12
  end
@@ -2,23 +2,22 @@ require 'api_resource/associations/single_object_proxy'
2
2
  module ApiResource
3
3
  module Associations
4
4
  class HasOneRemoteObjectProxy < SingleObjectProxy
5
- def initialize(klass_name, contents, owner)
6
- super
7
- return if self.internal_object
5
+
6
+ def initialize(klass, owner)
7
+ super(klass, owner)
8
+
8
9
  # 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
10
+ self.remote_path = self.klass.collection_path(self.owner.class.to_s.foreign_key => self.owner.id)
13
11
  end
12
+
14
13
  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
14
+
15
+ def load(opts = {})
16
+ data = self.klass.connection.get(self.build_load_path(opts))
17
+ return nil if data.blank?
18
+ return self.klass.new(data.first)
21
19
  end
20
+
22
21
  end
23
22
  end
24
23
  end
@@ -1,13 +1,19 @@
1
- require 'api_resource/associations/association_proxy'
1
+ require 'api_resource/associations/association_scope'
2
2
 
3
3
  module ApiResource
4
4
 
5
5
  module Associations
6
6
 
7
- class MultiObjectProxy < AssociationProxy
7
+ class MultiObjectProxy < AssociationScope
8
8
 
9
9
  include Enumerable
10
10
 
11
+ # override the constructor to set data to nil by
12
+ # default
13
+ def initialize(klass, owner, data = nil)
14
+ super
15
+ end
16
+
11
17
  def all
12
18
  self.internal_object
13
19
  end
@@ -16,6 +22,39 @@ module ApiResource
16
22
  self.internal_object.each(*args, &block)
17
23
  end
18
24
 
25
+ def internal_object
26
+ @internal_object ||= begin
27
+ if self.remote_path.present?
28
+ self.load
29
+ else
30
+ []
31
+ end
32
+ end
33
+ end
34
+
35
+ def internal_object=(contents)
36
+ # if we were passed in a service uri, stop here
37
+ return true if self.set_remote_path_and_scopes(contents)
38
+
39
+ if contents.try(:first).is_a?(self.klass)
40
+ return @internal_object = contents
41
+ elsif contents.instance_of?(self.class)
42
+ return @internal_object = contents.internal_object
43
+ elsif contents.is_a?(Array)
44
+ return @internal_object = self.klass.instantiate_collection(
45
+ contents
46
+ )
47
+ # we have only provided the resource definition
48
+ elsif contents.nil?
49
+ return @internal_object = nil
50
+ else
51
+ raise ArgumentError.new(
52
+ "#{contents} must be a #{self.klass}, #{self.class}, " +
53
+ "Array or nil"
54
+ )
55
+ end
56
+ end
57
+
19
58
  def ==(other)
20
59
  return false if self.class != other.class
21
60
  if self.internal_object.is_a?(Array)
@@ -30,53 +69,40 @@ module ApiResource
30
69
  self.internal_object.collect{|obj| obj.serializable_hash(options) }
31
70
  end
32
71
 
33
- # force a load when calling this method
34
- def internal_object
35
- @internal_object ||= self.load_scope_with_options(:all, {})
36
- end
37
-
38
- def internal_object=(contents)
39
- return @internal_object = contents if contents.all?{|o| o.is_a?(self.klass)}
40
- return load(contents)
72
+ def load(opts = {})
73
+ data = self.klass.connection.get(self.build_load_path(opts))
74
+ @loaded = true
75
+ return [] if data.blank?
76
+ return self.klass.instantiate_collection(data)
41
77
  end
42
78
 
43
79
  protected
44
- def load_scope_with_options(scope, options)
45
- scope = self.loaded_hash_key(scope.to_s, options)
46
- return [] if self.remote_path.blank?
47
80
 
48
- self.loaded[scope] ||= begin
49
- self.times_loaded += 1
50
- self.load_from_remote(options).collect{|item| self.klass.new(item)}
51
- end
52
-
53
- end
54
-
55
- def load(contents)
56
- @internal_object = [] and return nil if contents.blank?
57
- if contents.is_a?(Array) && contents.first.is_a?(Hash) && contents.first[self.class.remote_path_element]
58
- settings = contents.slice!(0).with_indifferent_access
81
+ # set up the remote path from a set of options passed in
82
+ def set_remote_path_and_scopes(opts)
83
+ if opts.is_a?(Array) && opts.first.is_a?(Hash)
84
+ if opts.first.symbolize_keys[self.class.remote_path_element.to_sym]
85
+ service_uri_el = opts.shift
86
+ else
87
+ service_uri_el = {}
88
+ end
89
+ elsif opts.is_a?(Hash)
90
+ service_uri_el = opts
91
+ else
92
+ service_uri_el = {}
59
93
  end
60
94
 
61
- settings = contents.with_indifferent_access if contents.is_a?(Hash)
62
- settings ||= {}.with_indifferent_access
95
+ @remote_path = service_uri_el.symbolize_keys.delete(
96
+ self.class.remote_path_element.to_sym
97
+ )
63
98
 
64
- raise "Invalid response for multi object relationship: #{contents}" unless settings[self.class.remote_path_element] || contents.is_a?(Array)
65
- self.remote_path = settings.delete(self.class.remote_path_element)
66
-
67
- settings.each do |key, value|
68
- raise "Expected the scope #{key} to point to a hash, to #{value}" unless value.is_a?(Hash)
69
- self.instance_eval <<-EOE, __FILE__, __LINE__ + 1
70
- def #{key}(opts = {})
71
- @#{key} ||= ApiResource::Associations::RelationScope.new(self, :#{key}, opts)
72
- end
73
- EOE
74
- self.scopes[key.to_s] = value
99
+ service_uri_el.each_pair do |scope_name, scope_def|
100
+ self.define_subscope(scope_name, scope_def)
75
101
  end
76
102
 
77
- # Create the internal object
78
- @internal_object = contents.is_a?(Array) ? contents.collect{|item| self.klass.new(item)} : nil
103
+ return @remote_path.present?
79
104
  end
105
+
80
106
  end
81
107
 
82
108
  end
@@ -1,31 +1,20 @@
1
- require 'api_resource/associations/scope'
2
-
3
1
  module ApiResource
4
-
5
2
  module Associations
6
-
7
- class ResourceScope < Scope
3
+ class ResourceScope < AbstractScope
8
4
 
9
5
  include Enumerable
10
6
 
11
- def internal_object
12
- ApiResource.with_ttl(ttl) do
13
- @internal_object ||= self.klass.send(:find, :all, :params => self.to_hash)
14
- end
15
- end
16
-
17
7
  alias_method :all, :internal_object
18
8
 
19
9
  def each(*args, &block)
20
10
  self.internal_object.each(*args, &block)
21
11
  end
22
12
 
23
- # Used by ApiResource::Scopes to create methods with the same name
24
- # as the scope
25
- #
26
- # Weird place to have a factory... could have been on Scope or a separate class...
27
- def self.class_factory(hsh)
28
- return ApiResource::Associations::GenericScope
13
+ # perform a find with a given set of query params
14
+ def load(opts = {})
15
+ ret = self.klass.all(:params => opts)
16
+ @loaded = true
17
+ ret
29
18
  end
30
19
  end
31
20
  end
@@ -1,132 +1,34 @@
1
1
  module ApiResource
2
-
3
2
  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
- @ttl = opts.delete(:expires_in)
16
- # splits on _and_ and sorts to get a consistent scope key order
17
- @current_scope = (self.parent_scope + Array.wrap(current_scope.to_s)).sort
18
- # define methods for the scopes of the object
19
-
20
- klass.scopes.each do |key, val|
21
- self.instance_eval <<-EOE, __FILE__, __LINE__ + 1
22
- # This class always has at least one scope, adding a new one should clone this object
23
- def #{key}(*args)
24
- obj = self.clone
25
- # Call reload to make it go back to the webserver the next time it loads
26
- obj.reload
27
- return obj.enhance_current_scope(:#{key}, *args)
28
- end
29
- EOE
30
- self.scopes[key.to_s] = val
3
+ class Scope < AbstractScope
4
+
5
+ def initialize(klass, opts = {})
6
+ # see if we have a hash of options and it has a parent in it
7
+ unless opts[:__parent].respond_to?(:load)
8
+ raise ArgumentError.new(
9
+ "Scopes must have a parent object passed in that " +
10
+ "responds to #load"
11
+ )
31
12
  end
32
- # Use the method current scope because it gives a string
33
- # This expression substitutes the options from opts into the default attributes of the scope, it will only copy keys that exist in the original
34
- 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}
35
- end
36
-
37
- def ttl
38
- @ttl || 0
39
- end
40
-
41
- # Use this method to access the internal data, this guarantees that loading only occurs once per object
42
- def internal_object
43
- raise "Not Implemented: This method must be implemented in a subclass"
13
+ super(klass, opts)
44
14
  end
45
15
 
46
- def scopes
47
- @scopes ||= {}.with_indifferent_access
16
+ def load
17
+ ret = self.klass.load(self.to_hash)
18
+ @loaded = true
19
+ ret
48
20
  end
49
21
 
50
- def scope?(scp)
51
- self.scopes.key?(scp.to_s)
22
+ # we break this out here because Scope needs to pass self.klass to
23
+ # any sub-scopes. This is because Scope does not have knowledge
24
+ # of how to actually load data and delegates that to either
25
+ # a ResourceScope or an AssociationScope
26
+ def get_subscope_instance(finder_opts)
27
+ ApiResource::Associations::Scope.new(
28
+ self.klass, finder_opts.merge(:__parent => self)
29
+ )
52
30
  end
53
31
 
54
- def current_scope
55
- ActiveSupport::StringInquirer.new(@current_scope.join("_and_").concat("_scope"))
56
- end
57
-
58
- def to_hash
59
- self.parent_hash.merge(self.scopes[self.current_scope])
60
- end
61
-
62
- # takes empty hashes and replaces them with true so that to_query doesn't strip them out
63
- def to_query_safe_hash(hash)
64
- hash.each_pair do |k, v|
65
- hash[k] = to_query_safe_hash(v) if v.is_a?(Hash)
66
- hash[k] = true if v == {}
67
- end
68
- return hash
69
- end
70
-
71
- # gets the current hash and calls to_query on it
72
- def to_query
73
- #We need to add the unescape because to_query breaks on nested arrays
74
- CGI.unescape(to_query_safe_hash(self.to_hash).to_query)
75
- end
76
-
77
- def method_missing(method, *args, &block)
78
- self.internal_object.send(method, *args, &block)
79
- end
80
-
81
- def reload
82
- remove_instance_variable(:@internal_object) if instance_variable_defined?(:@internal_object)
83
- self
84
- end
85
-
86
- def to_s
87
- self.internal_object.to_s
88
- end
89
-
90
- def inspect
91
- self.internal_object.inspect
92
- end
93
-
94
- def blank?
95
- self.internal_object.blank?
96
- end
97
- alias_method :empty?, :blank?
98
-
99
- def present?
100
- self.internal_object.present?
101
- end
102
-
103
- def expires_in(ttl)
104
- ApiResource::Decorators::CachingDecorator.new(self, ttl)
105
- end
106
-
107
- protected
108
- # scope from the parent
109
- def parent_scope
110
- ret = @parent ? Array.wrap(@parent.current_scope).collect{|el| el.gsub(/_scope$/,'')} : []
111
- ret.collect{|el| el.split(/_and_/)}.flatten
112
- end
113
- # querystring hash from parent
114
- def parent_hash
115
- @parent ? @parent.to_hash : {}
116
- end
117
- def enhance_current_scope(scp, *args)
118
- opts = args.extract_options!
119
- check_scope(scp)
120
- cache_key = "a#{Digest::MD5.hexdigest((args.sort + [scp]).to_s)}"
121
- return instance_variable_get("@#{cache_key}") if instance_variable_defined?("@#{cache_key}")
122
- return instance_variable_set("@#{cache_key}", self.class.class_factory(self.scopes[scp]).new(self.klass, scp, *args, opts.merge(:parent => self)))
123
- end
124
- # make sure we have a valid scope
125
- def check_scope(scp)
126
- raise ArgumentError, "Unknown scope #{scp}" unless self.scope?(scp.to_s)
127
- end
128
32
  end
129
-
130
33
  end
131
-
132
- end
34
+ end