zendesk_api 0.0.9

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 (97) hide show
  1. data/.gitignore +7 -0
  2. data/.rspec +2 -0
  3. data/.travis.yml +5 -0
  4. data/.yardopts +1 -0
  5. data/Gemfile +6 -0
  6. data/Gemfile.lock +59 -0
  7. data/LICENSE +19 -0
  8. data/Rakefile +49 -0
  9. data/Readme.md +178 -0
  10. data/lib/zendesk_api.rb +10 -0
  11. data/lib/zendesk_api/actions.rb +176 -0
  12. data/lib/zendesk_api/association.rb +267 -0
  13. data/lib/zendesk_api/client.rb +150 -0
  14. data/lib/zendesk_api/collection.rb +233 -0
  15. data/lib/zendesk_api/configuration.rb +52 -0
  16. data/lib/zendesk_api/core_ext/inflection.rb +13 -0
  17. data/lib/zendesk_api/core_ext/modulize.rb +10 -0
  18. data/lib/zendesk_api/core_ext/snakecase.rb +12 -0
  19. data/lib/zendesk_api/lru_cache.rb +38 -0
  20. data/lib/zendesk_api/middleware/request/etag_cache.rb +38 -0
  21. data/lib/zendesk_api/middleware/request/retry.rb +39 -0
  22. data/lib/zendesk_api/middleware/request/upload.rb +32 -0
  23. data/lib/zendesk_api/middleware/response/callback.rb +19 -0
  24. data/lib/zendesk_api/middleware/response/deflate.rb +18 -0
  25. data/lib/zendesk_api/middleware/response/gzip.rb +18 -0
  26. data/lib/zendesk_api/middleware/response/parse_iso_dates.rb +29 -0
  27. data/lib/zendesk_api/rescue.rb +44 -0
  28. data/lib/zendesk_api/resource.rb +133 -0
  29. data/lib/zendesk_api/resources/forum.rb +51 -0
  30. data/lib/zendesk_api/resources/misc.rb +66 -0
  31. data/lib/zendesk_api/resources/playlist.rb +64 -0
  32. data/lib/zendesk_api/resources/ticket.rb +76 -0
  33. data/lib/zendesk_api/resources/user.rb +44 -0
  34. data/lib/zendesk_api/track_changes.rb +72 -0
  35. data/lib/zendesk_api/trackie.rb +8 -0
  36. data/lib/zendesk_api/verbs.rb +43 -0
  37. data/lib/zendesk_api/version.rb +3 -0
  38. data/live/Readme.md +4 -0
  39. data/live/activity_spec.rb +5 -0
  40. data/live/audit_spec.rb +5 -0
  41. data/live/bookmark_spec.rb +11 -0
  42. data/live/category_spec.rb +12 -0
  43. data/live/collection_spec.rb +68 -0
  44. data/live/crm_spec.rb +11 -0
  45. data/live/custom_role_spec.rb +5 -0
  46. data/live/forum_spec.rb +14 -0
  47. data/live/forum_subscription_spec.rb +12 -0
  48. data/live/group_membership_spec.rb +18 -0
  49. data/live/group_spec.rb +14 -0
  50. data/live/identity_spec.rb +14 -0
  51. data/live/locale_spec.rb +11 -0
  52. data/live/macro_spec.rb +5 -0
  53. data/live/mobile_device_spec.rb +11 -0
  54. data/live/organization_spec.rb +12 -0
  55. data/live/satisfaction_rating_spec.rb +6 -0
  56. data/live/setting_spec.rb +5 -0
  57. data/live/suspended_ticket_spec.rb +8 -0
  58. data/live/ticket_field_spec.rb +12 -0
  59. data/live/ticket_metrics_spec.rb +6 -0
  60. data/live/ticket_spec.rb +88 -0
  61. data/live/topic_comment_spec.rb +13 -0
  62. data/live/topic_spec.rb +18 -0
  63. data/live/topic_subscription_spec.rb +12 -0
  64. data/live/topic_vote_spec.rb +13 -0
  65. data/live/upload_spec.rb +9 -0
  66. data/live/user_spec.rb +13 -0
  67. data/live/view_spec.rb +6 -0
  68. data/spec/association_spec.rb +210 -0
  69. data/spec/client_spec.rb +149 -0
  70. data/spec/collection_spec.rb +302 -0
  71. data/spec/configuration_spec.rb +24 -0
  72. data/spec/create_resource_spec.rb +39 -0
  73. data/spec/data_resource_spec.rb +229 -0
  74. data/spec/fixtures/Argentina.gif +0 -0
  75. data/spec/fixtures/Argentina2.gif +0 -0
  76. data/spec/fixtures/credentials.yml.example +3 -0
  77. data/spec/fixtures/test_resources.rb +8 -0
  78. data/spec/fixtures/zendesk.rb +88 -0
  79. data/spec/lru_cache_spec.rb +26 -0
  80. data/spec/macros/resource_macros.rb +157 -0
  81. data/spec/middleware/request/etag_cache_spec.rb +17 -0
  82. data/spec/middleware/request/retry_spec.rb +47 -0
  83. data/spec/middleware/request/test.jpg +0 -0
  84. data/spec/middleware/request/upload_spec.rb +74 -0
  85. data/spec/middleware/response/callback_spec.rb +17 -0
  86. data/spec/middleware/response/deflate_spec.rb +15 -0
  87. data/spec/middleware/response/gzip_spec.rb +19 -0
  88. data/spec/middleware/response/parse_iso_dates_spec.rb +44 -0
  89. data/spec/playlist_spec.rb +95 -0
  90. data/spec/read_resource_spec.rb +37 -0
  91. data/spec/rescue_spec.rb +94 -0
  92. data/spec/resource_spec.rb +332 -0
  93. data/spec/spec_helper.rb +120 -0
  94. data/spec/string_spec.rb +7 -0
  95. data/spec/trackie_spec.rb +39 -0
  96. data/zendesk_api.gemspec +38 -0
  97. metadata +364 -0
@@ -0,0 +1,267 @@
1
+ module ZendeskAPI
2
+ # Represents an association between two resources
3
+ class Association
4
+ # @return [Hash] Options passed into the association
5
+ attr_reader :options
6
+
7
+ # Options to pass in
8
+ # * class - Required
9
+ # * parent - Parent instance
10
+ # * path - Optional path instead of resource name
11
+ def initialize(options = {})
12
+ @options = Hashie::Mash.new(options)
13
+ end
14
+
15
+ # Generate a path to the resource.
16
+ # id and <parent>_id attributes will be deleted from passed in options hash if they are used in the built path.
17
+ # Arguments that can be passed in:
18
+ # An instance, any resource instance
19
+ # Hash Options:
20
+ # * with_parent - Include the parent path (false by default)
21
+ # * with_id - Include the instance id, if possible (true)
22
+ def generate_path(*args)
23
+ options = Hashie::Mash.new(:with_id => true)
24
+ if args.last.is_a?(Hash)
25
+ original_options = args.pop
26
+ options.merge!(original_options)
27
+ end
28
+
29
+ instance = args.first
30
+
31
+ namespace = @options[:class].to_s.split("::")
32
+ namespace.delete("ZendeskAPI")
33
+ has_parent = namespace.size > 1 || (options[:with_parent] && @options.parent)
34
+
35
+ if has_parent
36
+ parent_class = @options.parent ? @options.parent.class : ZendeskAPI.get_class(namespace[0])
37
+ parent_namespace = build_parent_namespace(parent_class, instance, options, original_options)
38
+ namespace[1..1] = parent_namespace if parent_namespace
39
+ namespace[0] = parent_class.resource_name
40
+ else
41
+ namespace[0] = @options.path || @options[:class].resource_name
42
+ end
43
+
44
+ if id = extract_id(instance, options, original_options)
45
+ namespace << id
46
+ end
47
+
48
+ namespace.join("/")
49
+ end
50
+
51
+ private
52
+
53
+ def build_parent_namespace(parent_class, instance, options, original_options)
54
+ return unless association_on_parent = parent_class.associations.detect {|a| a[:class] == @options[:class] }
55
+ [
56
+ extract_parent_id(parent_class, instance, options, original_options),
57
+ @options.path || association_on_parent[:name].to_s
58
+ ]
59
+ end
60
+
61
+ def extract_parent_id(parent_class, instance, options, original_options)
62
+ parent_id_column = "#{parent_class.singular_resource_name}_id"
63
+
64
+ if @options.parent
65
+ @options.parent.id
66
+ elsif instance
67
+ instance.send(parent_id_column)
68
+ elsif options[parent_id_column]
69
+ original_options.delete(parent_id_column) || original_options.delete(parent_id_column.to_sym)
70
+ else
71
+ raise ArgumentError.new("#{@options[:class].resource_name} requires #{parent_id_column} or parent")
72
+ end
73
+ end
74
+
75
+ def extract_id(instance, options, original_options)
76
+ if options[:with_id] && !@options[:class].ancestors.include?(SingularResource)
77
+ if instance && instance.id
78
+ instance.id
79
+ elsif options[:id]
80
+ original_options.delete(:id) || original_options.delete("id")
81
+ end
82
+ end
83
+ end
84
+ end
85
+
86
+ # This module holds association method for resources.
87
+ # Associations can be loaded in three ways:
88
+ # * Commonly used resources are automatically side-loaded server side and sent along with their parent object.
89
+ # * Associated resource ids are sent and are then loaded one-by-one into the parent collection.
90
+ # * The association is represented with Rails' nested association urls (such as tickets/:id/groups) and are loaded that way.
91
+ module Associations
92
+ def self.included(base)
93
+ base.send(:extend, ClassMethods)
94
+ end
95
+
96
+ def wrap_resource(resource, klass, class_level_association)
97
+ instance_association = Association.new(class_level_association.merge(:parent => self))
98
+ case resource
99
+ when Hash
100
+ klass.new(@client, resource.merge(:association => instance_association))
101
+ when String, Fixnum
102
+ klass.new(@client, :id => resource, :association => instance_association)
103
+ else
104
+ resource.association = instance_association
105
+ resource
106
+ end
107
+ end
108
+
109
+ module ClassMethods
110
+ include Rescue
111
+
112
+ def associations
113
+ @assocations ||= []
114
+ end
115
+
116
+ # Represents a parent-to-child association between resources. Options to pass in are: class, path.
117
+ # @param [Symbol] resource_name The underlying resource name
118
+ # @param [Hash] opts The options to pass to the method definition.
119
+ def has(resource_name, class_level_options = {})
120
+ klass = get_class(class_level_options.delete(:class)) || get_class(resource_name)
121
+ class_level_association = {
122
+ :class => klass,
123
+ :name => resource_name,
124
+ :inline => class_level_options.delete(:inline),
125
+ :path => class_level_options.delete(:path)
126
+ }
127
+ associations << class_level_association
128
+ id_column = "#{resource_name}_id"
129
+
130
+ define_method "#{resource_name}_used?" do
131
+ !!instance_variable_get("@#{resource_name}")
132
+ end
133
+
134
+ define_method resource_name do |*args|
135
+ instance_options = args.last.is_a?(Hash) ? args.pop : {}
136
+
137
+ # return if cached
138
+ cached = instance_variable_get("@#{resource_name}")
139
+ return cached if cached && !instance_options[:reload]
140
+
141
+ # find and cache association
142
+ instance_association = Association.new(class_level_association.merge(:parent => self))
143
+ resource = if klass.respond_to?(:find) && resource_id = method_missing(id_column)
144
+ klass.find(@client, :id => resource_id, :association => instance_association)
145
+ elsif found = method_missing(resource_name.to_sym)
146
+ wrap_resource(found, klass, class_level_association)
147
+ elsif klass.ancestors.include?(DataResource)
148
+ rescue_client_error do
149
+ response = @client.connection.get(instance_association.generate_path(:with_parent => true))
150
+ klass.new(@client, response.body[klass.singular_resource_name].merge(:association => instance_association))
151
+ end
152
+ end
153
+
154
+ send("#{id_column}=", resource.id) if resource && has_key?(id_column)
155
+ instance_variable_set("@#{resource_name}", resource)
156
+ end
157
+
158
+ define_method "#{resource_name}=" do |resource|
159
+ resource = wrap_resource(resource, klass, class_level_association)
160
+ send("#{id_column}=", resource.id) if has_key?(id_column)
161
+ instance_variable_set("@#{resource_name}", resource)
162
+ end
163
+ end
164
+
165
+ # Represents a parent-to-children association between resources. Options to pass in are: class, path.
166
+ # @param [Symbol] resource The underlying resource name
167
+ # @param [Hash] opts The options to pass to the method definition.
168
+ def has_many(resource_name, class_level_opts = {})
169
+ klass = get_class(class_level_opts.delete(:class)) || get_class(resource_name.to_s.singular)
170
+ class_level_association = {
171
+ :class => klass,
172
+ :name => resource_name,
173
+ :inline => class_level_opts.delete(:inline),
174
+ :path => class_level_opts.delete(:path)
175
+ }
176
+ associations << class_level_association
177
+
178
+ id_column = "#{resource_name}_ids"
179
+
180
+ define_method "#{resource_name}_used?" do
181
+ !!instance_variable_get("@#{resource_name}")
182
+ end
183
+
184
+ define_method resource_name do |*args|
185
+ instance_opts = args.last.is_a?(Hash) ? args.pop : {}
186
+
187
+ # return if cached
188
+ cached = instance_variable_get("@#{resource_name}")
189
+ return cached if cached && !instance_opts[:reload]
190
+
191
+ # find and cache association
192
+ instance_association = Association.new(class_level_association.merge(:parent => self))
193
+ singular_resource_name = resource_name.to_s.singular
194
+
195
+ resources = if (ids = method_missing("#{singular_resource_name}_ids")) && ids.any?
196
+ ids.map do |id|
197
+ klass.find(@client, :id => id, :association => instance_association)
198
+ end.compact
199
+ elsif (resources = method_missing(resource_name.to_sym)) && resources.any?
200
+ resources.map do |res|
201
+ klass.new(@client, res.merge(:association => instance_association))
202
+ end
203
+ else
204
+ ZendeskAPI::Collection.new(@client, klass, instance_opts.merge(:association => instance_association))
205
+ end
206
+
207
+ send("#{id_column}=", resources.map(&:id)) if resource && has_key?(id_column)
208
+ instance_variable_set("@#{resource_name}", resources)
209
+ end
210
+
211
+ define_method "#{resource_name}=" do |resources|
212
+ if resources.is_a?(Array)
213
+ resources.map! { |attr| wrap_resource(attr, klass, class_level_association) }
214
+ send(resource_name).replace(resources)
215
+ else
216
+ resources.association = instance_association
217
+ instance_variable_set("@#{resource_name}", resources)
218
+ end
219
+
220
+ send("#{id_column}=", resources.map(&:id)) if resources && has_key?(id_column)
221
+ resource
222
+ end
223
+ end
224
+
225
+ # Allows using has and has_many without having class defined yet
226
+ # Guesses at Resource, if it's anything else and the class is later
227
+ # reopened under a different superclass, an error will be thrown
228
+ def get_class(resource)
229
+ return false if resource.nil?
230
+ res = resource.to_s.modulize
231
+
232
+ begin
233
+ const_get(res)
234
+ rescue NameError
235
+ ZendeskAPI.get_class(resource)
236
+ end
237
+ end
238
+ end
239
+ end
240
+
241
+ class << self
242
+ # Revert Rails' overwrite of const_missing
243
+ if method_defined?(:const_missing_without_dependencies)
244
+ alias :const_missing :const_missing_without_dependencies
245
+ end
246
+
247
+ # Allows using has and has_many without having class defined yet
248
+ # Guesses at Resource, if it's anything else and the class is later
249
+ # reopened under a different superclass, an error will be thrown
250
+ def get_class(resource)
251
+ return false if resource.nil?
252
+ res = resource.to_s.modulize.split("::")
253
+
254
+ begin
255
+ res[1..-1].inject(ZendeskAPI.const_get(res[0])) do |iter, k|
256
+ begin
257
+ iter.const_get(k)
258
+ rescue
259
+ iter.const_set(k, Class.new(Resource))
260
+ end
261
+ end
262
+ rescue NameError
263
+ ZendeskAPI.const_set(res[0], Class.new(Resource))
264
+ end
265
+ end
266
+ end
267
+ end
@@ -0,0 +1,150 @@
1
+ require 'faraday'
2
+ require 'faraday_middleware'
3
+
4
+ require 'zendesk_api/version'
5
+ require 'zendesk_api/rescue'
6
+ require 'zendesk_api/configuration'
7
+ require 'zendesk_api/collection'
8
+ require 'zendesk_api/lru_cache'
9
+ require 'zendesk_api/middleware/request/etag_cache'
10
+ require 'zendesk_api/middleware/request/retry'
11
+ require 'zendesk_api/middleware/request/upload'
12
+ require 'zendesk_api/middleware/response/callback'
13
+ require 'zendesk_api/middleware/response/deflate'
14
+ require 'zendesk_api/middleware/response/gzip'
15
+ require 'zendesk_api/middleware/response/parse_iso_dates'
16
+
17
+ module ZendeskAPI
18
+ class Client
19
+ include Rescue
20
+
21
+ # @return [Configuration] Config instance
22
+ attr_reader :config
23
+ # @return [Array] Custom response callbacks
24
+ attr_reader :callbacks
25
+
26
+ # Handles resources such as 'tickets'. Any options are passed to the underlying collection, except reload which disregards
27
+ # memoization and creates a new Collection instance.
28
+ # @return [Collection] Collection instance for resource
29
+ def method_missing(method, *args, &block)
30
+ method = method.to_s
31
+ options = args.last.is_a?(Hash) ? args.pop : {}
32
+ return instance_variable_get("@#{method}") if !options.delete(:reload) && instance_variable_defined?("@#{method}")
33
+ instance_variable_set("@#{method}", ZendeskAPI::Collection.new(self, ZendeskAPI.get_class(method.singular), options))
34
+ end
35
+
36
+ # Plays a view playlist.
37
+ # @param [String/Number] id View id or 'incoming'
38
+ def play(id)
39
+ ZendeskAPI::Playlist.new(self, id)
40
+ end
41
+
42
+ # Returns the current user (aka me)
43
+ # @return [ZendeskAPI::User] Current user or nil
44
+ def current_user(reload = false)
45
+ return @current_user if @current_user && !reload
46
+ @current_user = users.find(:id => 'me')
47
+ end
48
+
49
+ def current_account(reload = false)
50
+ return @current_account if @current_account && !reload
51
+ @current_account = Hashie::Mash.new(connection.get('account/resolve').body)
52
+ end
53
+
54
+ rescue_client_error :current_account
55
+
56
+ # Returns the current locale
57
+ def current_locale(reload = false)
58
+ return @locale if @locale && !reload
59
+ @locale = locales.find(:id => 'current')
60
+ end
61
+
62
+ # Creates a new {Client} instance and yields {#config}.
63
+ #
64
+ # Requires a block to be given.
65
+ #
66
+ # Does basic configuration constraints:
67
+ # * {Configuration#url} must be https unless {Configuration#allow_http} is set.
68
+ def initialize
69
+ raise ArgumentError, "block not given" unless block_given?
70
+
71
+ @config = ZendeskAPI::Configuration.new
72
+ yield config
73
+
74
+ if !config.allow_http && config.url !~ /^https/
75
+ raise ArgumentError, "zendesk_api is ssl only; url must begin with https://"
76
+ end
77
+
78
+ config.retry = !!config.retry # nil -> false
79
+
80
+ if config.logger.nil? || config.logger == true
81
+ require 'logger'
82
+ config.logger = Logger.new($stderr)
83
+ config.logger.level = Logger::WARN
84
+ end
85
+
86
+ @callbacks = []
87
+
88
+ if logger = config.logger
89
+ insert_callback do |env|
90
+ if warning = env[:response_headers]["X-Zendesk-API-Warn"]
91
+ logger.warn "WARNING: #{warning}"
92
+ end
93
+ end
94
+ end
95
+ end
96
+
97
+ # Creates a connection if there is none, otherwise returns the existing connection.
98
+ #
99
+ # @returns [Faraday::Connection] Faraday connection for the client
100
+ def connection
101
+ @connection ||= build_connection
102
+ return @connection
103
+ end
104
+
105
+ # Pushes a callback onto the stack. Callbacks are executed on responses, last in the Faraday middleware stack.
106
+ # @param [Proc] block The block to execute. Takes one parameter, env.
107
+ def insert_callback(&block)
108
+ @callbacks << block
109
+ end
110
+
111
+ # show a nice warning for people using the old style api
112
+ def self.check_deprecated_namespace_usage(attributes, name)
113
+ raise "un-nest '#{name}' from the attributes" if attributes[name].is_a?(Hash)
114
+ end
115
+
116
+ protected
117
+
118
+ # Called by {#connection} to build a connection. Can be overwritten in a
119
+ # subclass to add additional middleware and make other configuration
120
+ # changes.
121
+ #
122
+ # Uses middleware according to configuration options.
123
+ #
124
+ # Request logger if logger is not nil
125
+ #
126
+ # Retry middleware if retry is true
127
+ def build_connection
128
+ Faraday.new(config.options) do |builder|
129
+ # response
130
+ builder.use Faraday::Request::BasicAuthentication, config.username, config.password
131
+ builder.use Faraday::Response::RaiseError
132
+ builder.use ZendeskAPI::Middleware::Response::Callback, self
133
+ builder.use Faraday::Response::Logger, config.logger if config.logger
134
+ builder.use ZendeskAPI::Middleware::Response::ParseIsoDates
135
+ builder.response :json, :content_type => 'application/json'
136
+ builder.use ZendeskAPI::Middleware::Response::Gzip
137
+ builder.use ZendeskAPI::Middleware::Response::Deflate
138
+
139
+ # request
140
+ builder.use ZendeskAPI::Middleware::Request::EtagCache, :cache => config.cache
141
+ builder.use ZendeskAPI::Middleware::Request::Upload
142
+ builder.request :multipart
143
+ builder.request :json
144
+ builder.use ZendeskAPI::Middleware::Request::Retry, :logger => config.logger if config.retry # Should always be first in the stack
145
+
146
+ builder.adapter *config.adapter || Faraday.default_adapter
147
+ end
148
+ end
149
+ end
150
+ end
@@ -0,0 +1,233 @@
1
+ require 'zendesk_api/resource'
2
+ require 'zendesk_api/resources/misc'
3
+ require 'zendesk_api/resources/ticket'
4
+ require 'zendesk_api/resources/user'
5
+ require 'zendesk_api/resources/playlist'
6
+
7
+ module ZendeskAPI
8
+ # Represents a collection of resources. Lazily loaded, resources aren't
9
+ # actually fetched until explicitly needed (e.g. #each, {#fetch}).
10
+ class Collection
11
+ SPECIALLY_JOINED_PARAMS = [:include, :ids, :only]
12
+
13
+ include Rescue
14
+
15
+ # @return [ZendeskAPI::Association] The class association
16
+ attr_reader :association
17
+
18
+ # Creates a new Collection instance. Does not fetch resources.
19
+ # Additional options are: verb (default: GET), path (default: resource param), page, per_page.
20
+ # @param [Client] client The {Client} to use.
21
+ # @param [String] resource The resource being collected.
22
+ # @param [Hash] options Any additional options to be passed in.
23
+ def initialize(client, resource, options = {})
24
+ @client, @resource = client, resource.resource_name
25
+ @options = Hashie::Mash.new(options)
26
+
27
+ @verb = @options.delete(:verb)
28
+ @collection_path = @options.delete(:collection_path)
29
+
30
+ association_options = { :path => @options.delete(:path) }
31
+ association_options[:path] ||= @collection_path.join("/") if @collection_path
32
+ @association = @options.delete(:association) || Association.new(association_options.merge(:class => resource))
33
+
34
+ # some params use comma-joined strings instead of query-based arrays for multiple values
35
+ @options.each do |k, v|
36
+ if SPECIALLY_JOINED_PARAMS.include?(k.to_sym) && v.is_a?(Array)
37
+ @options[k] = v.join(',')
38
+ end
39
+ end
40
+
41
+ @collection_path ||= [@resource]
42
+ @resource_class = resource
43
+ @fetchable = true
44
+
45
+ # Used for Attachments, TicketComment
46
+ if @resource_class.superclass == ZendeskAPI::Data
47
+ @resources = []
48
+ @fetchable = false
49
+ end
50
+ end
51
+
52
+ # Passes arguments and the proper path to the resource class method.
53
+ # @param [Hash] attributes Attributes to pass to Create#create
54
+ def create(attributes = {})
55
+ attributes.merge!(:association => @association)
56
+ @resource_class.create(@client, @options.merge(attributes))
57
+ end
58
+
59
+ # (see #create)
60
+ def find(opts = {})
61
+ opts.merge!(:association => @association)
62
+ @resource_class.find(@client, @options.merge(opts))
63
+ end
64
+
65
+ # (see #create)
66
+ def update(opts = {})
67
+ opts.merge!(:association => @association)
68
+ @resource_class.update(@client, @options.merge(opts))
69
+ end
70
+
71
+ # (see #create)
72
+ def destroy(opts = {})
73
+ opts.merge!(:association => association)
74
+ @resource_class.destroy(@client, @options.merge(opts))
75
+ end
76
+
77
+ # @return [Number] The total number of resources server-side (disregarding pagination).
78
+ def count
79
+ fetch
80
+ @count
81
+ end
82
+
83
+ # Changes the per_page option. Returns self, so it can be chained. No execution.
84
+ # @return [Collection] self
85
+ def per_page(count)
86
+ @options["per_page"] = count
87
+ self
88
+ end
89
+
90
+ # Changes the page option. Returns self, so it can be chained. No execution.
91
+ # @return [Collection] self
92
+ def page(number)
93
+ @options["page"] = number
94
+ self
95
+ end
96
+
97
+ # Saves all newly created resources stored in this collection.
98
+ # @return [Collection] self
99
+ def save
100
+ if @resources
101
+ @resources.map! do |item|
102
+ unless !item.respond_to?(:save) || item.changes.empty?
103
+ item.save
104
+ end
105
+
106
+ item
107
+ end
108
+ end
109
+
110
+ self
111
+ end
112
+
113
+ def <<(item)
114
+ fetch
115
+ if item.is_a?(Resource)
116
+ if item.is_a?(@resource_class)
117
+ @resources << item
118
+ else
119
+ raise "this collection is for #{@resource_class}"
120
+ end
121
+ else
122
+ item.merge!(:association => @association) if item.is_a?(Hash)
123
+ @resources << @resource_class.new(@client, item)
124
+ end
125
+ end
126
+
127
+ def path
128
+ @association.generate_path(:with_parent => true)
129
+ end
130
+
131
+ # Executes actual GET from API and loads resources into proper class.
132
+ # @param [Boolean] reload Whether to disregard cache
133
+ def fetch(reload = false)
134
+ return @resources if @resources && (!@fetchable || !reload)
135
+ if association && association.options.parent && association.options.parent.new_record?
136
+ return @resources = []
137
+ end
138
+
139
+ if @query
140
+ path = @query
141
+ @query = nil
142
+ else
143
+ path = self.path
144
+ end
145
+
146
+ response = @client.connection.send(@verb || "get", path) do |req|
147
+ req.params.merge!(@options.delete_if {|k, v| v.nil?})
148
+ end
149
+
150
+ results = response.body[@resource_class.model_key] || response.body["results"]
151
+ @resources = results.map { |res| @resource_class.new(@client, res) }
152
+
153
+ @count = (response.body["count"] || @resources.size).to_i
154
+ @next_page, @prev_page = response.body["next_page"], response.body["previous_page"]
155
+
156
+ @resources
157
+ end
158
+
159
+ rescue_client_error :fetch, :with => lambda { Array.new }
160
+
161
+ # Alias for fetch(false)
162
+ def to_a
163
+ fetch
164
+ end
165
+
166
+ def replace(collection)
167
+ raise "this collection is for #{@resource_class}" if collection.any?{|r| !r.is_a?(@resource_class) }
168
+ @resources = collection
169
+ end
170
+
171
+ # Find the next page. Does one of three things:
172
+ # * If there is already a page number in the options hash, it increases it and invalidates the cache, returning the new page number.
173
+ # * If there is a next_page url cached, it executes a fetch on that url and returns the results.
174
+ # * Otherwise, returns an empty array.
175
+ def next
176
+ if @options["page"]
177
+ clear_cache
178
+ @options["page"] += 1
179
+ elsif @next_page
180
+ @query = @next_page
181
+ fetch(true)
182
+ else
183
+ []
184
+ end
185
+ end
186
+
187
+ # Find the previous page. Does one of three things:
188
+ # * If there is already a page number in the options hash, it increases it and invalidates the cache, returning the new page number.
189
+ # * If there is a prev_page url cached, it executes a fetch on that url and returns the results.
190
+ # * Otherwise, returns an empty array.
191
+ def prev
192
+ if @options["page"] && @options["page"] > 1
193
+ clear_cache
194
+ @options["page"] -= 1
195
+ elsif @prev_page
196
+ @query = @prev_page
197
+ fetch(true)
198
+ else
199
+ []
200
+ end
201
+ end
202
+
203
+ # Clears all cached resources and associated values.
204
+ def clear_cache
205
+ @resources = nil
206
+ @count = nil
207
+ @next_page = nil
208
+ @prev_page = nil
209
+ end
210
+
211
+ def to_ary; nil; end
212
+
213
+ # Sends methods to underlying array of resources.
214
+ def method_missing(name, *args, &block)
215
+ if Array.new.respond_to?(name)
216
+ to_a.send(name, *args, &block)
217
+ else
218
+ opts = args.last.is_a?(Hash) ? args.last : {}
219
+ opts.merge!(:collection_path => @collection_path.dup.push(name))
220
+ self.class.new(@client, @resource_class, @options.merge(opts))
221
+ end
222
+ end
223
+
224
+ alias :orig_to_s :to_s
225
+ def to_s
226
+ if @resources
227
+ @resources.inspect
228
+ else
229
+ orig_to_s
230
+ end
231
+ end
232
+ end
233
+ end