zendesk_api 0.0.9

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