api_resource 0.4.3 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
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