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.
- data/VERSION +1 -1
- data/api_resource.gemspec +4 -74
- data/coverage/assets/0.5.3/app.js +88 -0
- data/coverage/assets/0.5.3/fancybox/blank.gif +0 -0
- data/coverage/assets/0.5.3/fancybox/fancy_close.png +0 -0
- data/coverage/assets/0.5.3/fancybox/fancy_loading.png +0 -0
- data/coverage/assets/0.5.3/fancybox/fancy_nav_left.png +0 -0
- data/coverage/assets/0.5.3/fancybox/fancy_nav_right.png +0 -0
- data/coverage/assets/0.5.3/fancybox/fancy_shadow_e.png +0 -0
- data/coverage/assets/0.5.3/fancybox/fancy_shadow_n.png +0 -0
- data/coverage/assets/0.5.3/fancybox/fancy_shadow_ne.png +0 -0
- data/coverage/assets/0.5.3/fancybox/fancy_shadow_nw.png +0 -0
- data/coverage/assets/0.5.3/fancybox/fancy_shadow_s.png +0 -0
- data/coverage/assets/0.5.3/fancybox/fancy_shadow_se.png +0 -0
- data/coverage/assets/0.5.3/fancybox/fancy_shadow_sw.png +0 -0
- data/coverage/assets/0.5.3/fancybox/fancy_shadow_w.png +0 -0
- data/coverage/assets/0.5.3/fancybox/fancy_title_left.png +0 -0
- data/coverage/assets/0.5.3/fancybox/fancy_title_main.png +0 -0
- data/coverage/assets/0.5.3/fancybox/fancy_title_over.png +0 -0
- data/coverage/assets/0.5.3/fancybox/fancy_title_right.png +0 -0
- data/coverage/assets/0.5.3/fancybox/fancybox-x.png +0 -0
- data/coverage/assets/0.5.3/fancybox/fancybox-y.png +0 -0
- data/coverage/assets/0.5.3/fancybox/fancybox.png +0 -0
- data/coverage/assets/0.5.3/fancybox/jquery.fancybox-1.3.1.css +363 -0
- data/coverage/assets/0.5.3/fancybox/jquery.fancybox-1.3.1.pack.js +44 -0
- data/coverage/assets/0.5.3/favicon_green.png +0 -0
- data/coverage/assets/0.5.3/favicon_red.png +0 -0
- data/coverage/assets/0.5.3/favicon_yellow.png +0 -0
- data/coverage/assets/0.5.3/highlight.css +129 -0
- data/coverage/assets/0.5.3/highlight.pack.js +1 -0
- data/coverage/assets/0.5.3/jquery-1.6.2.min.js +18 -0
- data/coverage/assets/0.5.3/jquery.dataTables.min.js +152 -0
- data/coverage/assets/0.5.3/jquery.timeago.js +141 -0
- data/coverage/assets/0.5.3/jquery.url.js +174 -0
- data/coverage/assets/0.5.3/loading.gif +0 -0
- data/coverage/assets/0.5.3/magnify.png +0 -0
- data/coverage/assets/0.5.3/smoothness/images/ui-bg_flat_0_aaaaaa_40x100.png +0 -0
- data/coverage/assets/0.5.3/smoothness/images/ui-bg_flat_75_ffffff_40x100.png +0 -0
- data/coverage/assets/0.5.3/smoothness/images/ui-bg_glass_55_fbf9ee_1x400.png +0 -0
- data/coverage/assets/0.5.3/smoothness/images/ui-bg_glass_65_ffffff_1x400.png +0 -0
- data/coverage/assets/0.5.3/smoothness/images/ui-bg_glass_75_dadada_1x400.png +0 -0
- data/coverage/assets/0.5.3/smoothness/images/ui-bg_glass_75_e6e6e6_1x400.png +0 -0
- data/coverage/assets/0.5.3/smoothness/images/ui-bg_glass_95_fef1ec_1x400.png +0 -0
- data/coverage/assets/0.5.3/smoothness/images/ui-bg_highlight-soft_75_cccccc_1x100.png +0 -0
- data/coverage/assets/0.5.3/smoothness/images/ui-icons_222222_256x240.png +0 -0
- data/coverage/assets/0.5.3/smoothness/images/ui-icons_2e83ff_256x240.png +0 -0
- data/coverage/assets/0.5.3/smoothness/images/ui-icons_454545_256x240.png +0 -0
- data/coverage/assets/0.5.3/smoothness/images/ui-icons_888888_256x240.png +0 -0
- data/coverage/assets/0.5.3/smoothness/images/ui-icons_cd0a0a_256x240.png +0 -0
- data/coverage/assets/0.5.3/smoothness/jquery-ui-1.8.4.custom.css +295 -0
- data/coverage/assets/0.5.3/stylesheet.css +383 -0
- data/coverage/index.html +3573 -0
- data/lib/api_resource/associations/abstract_scope.rb +191 -0
- data/lib/api_resource/associations/association_scope.rb +47 -0
- data/lib/api_resource/associations/belongs_to_remote_object_proxy.rb +5 -6
- data/lib/api_resource/associations/has_many_remote_object_proxy.rb +5 -8
- data/lib/api_resource/associations/has_one_remote_object_proxy.rb +12 -13
- data/lib/api_resource/associations/multi_object_proxy.rb +65 -39
- data/lib/api_resource/associations/resource_scope.rb +6 -17
- data/lib/api_resource/associations/scope.rb +23 -121
- data/lib/api_resource/associations/single_object_proxy.rb +41 -50
- data/lib/api_resource/associations.rb +32 -11
- data/lib/api_resource/attributes.rb +108 -69
- data/lib/api_resource/base.rb +114 -106
- data/lib/api_resource/local.rb +1 -1
- data/lib/api_resource/model_errors.rb +9 -6
- data/lib/api_resource/scopes.rb +53 -16
- data/lib/api_resource.rb +3 -1
- data/spec/lib/api_resource_spec.rb +3 -7
- data/spec/lib/associations/association_scope_spec.rb +19 -0
- data/spec/lib/associations_spec.rb +251 -162
- data/spec/lib/attributes_spec.rb +33 -15
- data/spec/lib/base_spec.rb +302 -64
- data/spec/lib/callbacks_spec.rb +4 -2
- data/spec/lib/local_spec.rb +5 -1
- data/spec/spec_helper.rb +2 -3
- data/spec/support/mocks/association_mocks.rb +9 -1
- data/spec/support/requests/association_requests.rb +5 -5
- data/spec/support/requests/test_resource_requests.rb +16 -4
- data/spec/tmp/api_resource_test_db.sqlite +0 -0
- metadata +68 -22
- data/.document +0 -5
- data/.rspec +0 -5
- data/.travis.yml +0 -4
- data/lib/api_resource/associations/association_proxy.rb +0 -121
- data/lib/api_resource/associations/dynamic_resource_scope.rb +0 -23
- data/lib/api_resource/associations/generic_scope.rb +0 -68
- data/lib/api_resource/associations/multi_argument_resource_scope.rb +0 -15
- 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(
|
|
6
|
-
super
|
|
7
|
-
|
|
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
|
|
10
|
-
self.
|
|
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(
|
|
6
|
-
super
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
|
-
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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/
|
|
1
|
+
require 'api_resource/associations/association_scope'
|
|
2
2
|
|
|
3
3
|
module ApiResource
|
|
4
4
|
|
|
5
5
|
module Associations
|
|
6
6
|
|
|
7
|
-
class MultiObjectProxy <
|
|
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
|
-
|
|
34
|
-
|
|
35
|
-
@
|
|
36
|
-
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
|
|
62
|
-
|
|
95
|
+
@remote_path = service_uri_el.symbolize_keys.delete(
|
|
96
|
+
self.class.remote_path_element.to_sym
|
|
97
|
+
)
|
|
63
98
|
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
|
|
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
|
|
47
|
-
|
|
16
|
+
def load
|
|
17
|
+
ret = self.klass.load(self.to_hash)
|
|
18
|
+
@loaded = true
|
|
19
|
+
ret
|
|
48
20
|
end
|
|
49
21
|
|
|
50
|
-
|
|
51
|
-
|
|
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
|