zendesk_api 0.1.7 → 0.1.8

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. data/.yardopts +1 -1
  2. data/Gemfile.lock +3 -1
  3. data/Rakefile +1 -0
  4. data/Readme.md +38 -0
  5. data/lib/zendesk_api.rb +2 -3
  6. data/lib/zendesk_api/actions.rb +9 -2
  7. data/lib/zendesk_api/association.rb +28 -54
  8. data/lib/zendesk_api/client.rb +20 -5
  9. data/lib/zendesk_api/collection.rb +31 -16
  10. data/lib/zendesk_api/configuration.rb +1 -0
  11. data/lib/zendesk_api/helpers.rb +1 -0
  12. data/lib/zendesk_api/lru_cache.rb +1 -0
  13. data/lib/zendesk_api/middleware/request/etag_cache.rb +1 -0
  14. data/lib/zendesk_api/middleware/request/retry.rb +2 -0
  15. data/lib/zendesk_api/middleware/request/upload.rb +1 -0
  16. data/lib/zendesk_api/middleware/response/callback.rb +1 -0
  17. data/lib/zendesk_api/middleware/response/deflate.rb +1 -0
  18. data/lib/zendesk_api/middleware/response/gzip.rb +2 -0
  19. data/lib/zendesk_api/middleware/response/logger.rb +1 -0
  20. data/lib/zendesk_api/middleware/response/parse_iso_dates.rb +1 -0
  21. data/lib/zendesk_api/rescue.rb +2 -0
  22. data/lib/zendesk_api/resource.rb +11 -6
  23. data/lib/zendesk_api/resources.rb +319 -0
  24. data/lib/zendesk_api/sideloading.rb +1 -0
  25. data/lib/zendesk_api/track_changes.rb +3 -2
  26. data/lib/zendesk_api/trackie.rb +1 -0
  27. data/lib/zendesk_api/version.rb +1 -1
  28. data/spec/association_spec.rb +1 -1
  29. data/spec/client_spec.rb +13 -2
  30. data/spec/collection_spec.rb +7 -7
  31. data/spec/data_resource_spec.rb +53 -66
  32. data/spec/read_resource_spec.rb +1 -1
  33. data/spec/resource_spec.rb +6 -6
  34. data/spec/spec_helper.rb +1 -1
  35. data/util/resource_handler.rb +68 -0
  36. data/util/verb_handler.rb +16 -0
  37. data/zendesk_api.gemspec +3 -2
  38. metadata +27 -12
  39. data/lib/zendesk_api/resources/forum.rb +0 -51
  40. data/lib/zendesk_api/resources/misc.rb +0 -79
  41. data/lib/zendesk_api/resources/ticket.rb +0 -97
  42. data/lib/zendesk_api/resources/user.rb +0 -59
data/.yardopts CHANGED
@@ -1 +1 @@
1
- --no-private --protected lib/**/*.rb - Readme.md
1
+ --tag internal --hide-tag internal --no-private --protected lib/**/*.rb -e util/resource_handler.rb -e util/verb_handler.rb - Readme.md
@@ -7,7 +7,7 @@ GIT
7
7
  PATH
8
8
  remote: .
9
9
  specs:
10
- zendesk_api (0.1.6)
10
+ zendesk_api (0.1.8)
11
11
  faraday (>= 0.8.0)
12
12
  faraday_middleware (>= 0.8.7)
13
13
  hashie
@@ -20,6 +20,7 @@ GEM
20
20
  remote: https://rubygems.org/
21
21
  specs:
22
22
  addressable (2.3.2)
23
+ bump (0.3.3)
23
24
  crack (0.3.1)
24
25
  diff-lcs (1.1.3)
25
26
  faraday (0.8.4)
@@ -54,6 +55,7 @@ PLATFORMS
54
55
  ruby
55
56
 
56
57
  DEPENDENCIES
58
+ bump
57
59
  hashie!
58
60
  jruby-openssl
59
61
  rake
data/Rakefile CHANGED
@@ -1,5 +1,6 @@
1
1
  require 'rake/testtask'
2
2
  require 'bundler/gem_tasks'
3
+ require 'bump/tasks'
3
4
 
4
5
  begin
5
6
  require 'rspec/core/rake_task'
data/Readme.md CHANGED
@@ -151,6 +151,44 @@ ticket.new_record? # => true
151
151
  ticket.save # Will POST
152
152
  ```
153
153
 
154
+ ### Side-loading
155
+
156
+ **Warning: this is an experimental feature. Abuse it and lose it.**
157
+
158
+ To facilitate a smaller number of requests and easier manipulation of associated data we allow "side-loading", or inclusion, of selected resources.
159
+
160
+ For example:
161
+ A ZendeskAPI::Ticket is associated with ZendeskAPI::User through the requester_id field.
162
+ API requests for that ticket return a structure similar to this:
163
+ ```json
164
+ "ticket": {
165
+ "id": 1,
166
+ "url": "http.....",
167
+ "requester_id": 7,
168
+ ...
169
+ }
170
+ ```
171
+
172
+ Calling ZendeskAPI::Ticket#requester automatically fetches and loads the user referenced above (`/api/v2/users/7`).
173
+ Using side-loading, however, the user can be partially loaded in the same request as the ticket.
174
+
175
+ ```ruby
176
+ tickets = client.tickets.include(:users)
177
+ # Or client.tickets(include: :users)
178
+ # Does *NOT* make a request to the server since it is already loaded
179
+ tickets.first.requester # => #<ZendeskAPI::User id=...>
180
+ ```
181
+
182
+ OR
183
+
184
+ ```ruby
185
+ ticket = client.tickets.find(:id => 1, :include => :users)
186
+ ticket.requester # => #<ZendeskAPI::User id=...>
187
+ ```
188
+
189
+ Currently, this feature is limited to only a few resources and their associations.
190
+ They are documented on [developer.zendesk.com](http://developer.zendesk.com/documentation/rest_api/introduction.html#side-loading).
191
+
154
192
  ### Special case: Custom resources paths
155
193
 
156
194
  API endpoints such as tickets/recent or topics/show_many can be accessed through chaining.
@@ -1,5 +1,4 @@
1
+ module ZendeskAPI; end
2
+
1
3
  require 'zendesk_api/core_ext/inflection'
2
4
  require 'zendesk_api/client'
3
-
4
- module ZendeskAPI
5
- end
@@ -1,7 +1,7 @@
1
1
  module ZendeskAPI
2
2
  module Save
3
3
  # If this resource hasn't been deleted, then create or save it.
4
- # Executes a POST if it is a {#new_record?}, otherwise a PUT.
4
+ # Executes a POST if it is a {Data#new_record?}, otherwise a PUT.
5
5
  # Merges returned attributes on success.
6
6
  # @return [Boolean] Success?
7
7
  def save(options={})
@@ -33,10 +33,13 @@ module ZendeskAPI
33
33
  true
34
34
  end
35
35
 
36
+ # Saves, raising an Argument error if it fails
37
+ # @raise [ArgumentError] if saving failed
36
38
  def save!(options={})
37
39
  save(options) || raise("Save failed")
38
40
  end
39
41
 
42
+ # Removes all cached associations
40
43
  def clear_associations
41
44
  self.class.associations.each do |association_data|
42
45
  name = association_data[:name]
@@ -44,6 +47,8 @@ module ZendeskAPI
44
47
  end
45
48
  end
46
49
 
50
+ # Saves associations
51
+ # Takes into account inlining, collections, and id setting on the parent resource.
47
52
  def save_associations
48
53
  self.class.associations.each do |association_data|
49
54
  association_name = association_data[:name]
@@ -71,7 +76,7 @@ module ZendeskAPI
71
76
  end
72
77
 
73
78
  # Finds a resource by an id and any options passed in.
74
- # A custom path to search at can be passed into opts. It defaults to the {DataResource.resource_name} of the class.
79
+ # A custom path to search at can be passed into opts. It defaults to the {Data.resource_name} of the class.
75
80
  # @param [Client] client The {Client} object to be used
76
81
  # @param [Hash] options Any additional GET parameters to be added
77
82
  def find(client, options = {})
@@ -115,6 +120,8 @@ module ZendeskAPI
115
120
  resource
116
121
  end
117
122
 
123
+ # Creates the resource, raising an ArgumentError if it fails
124
+ # @raise [ArgumentError] if the creation fails
118
125
  def create!(client, attributes={})
119
126
  c = create(client, attributes)
120
127
  c || raise("Create failed #{self} #{attributes}")
@@ -2,6 +2,7 @@ require 'zendesk_api/helpers'
2
2
 
3
3
  module ZendeskAPI
4
4
  # Represents an association between two resources
5
+ # @private
5
6
  class Association
6
7
  # @return [Hash] Options passed into the association
7
8
  attr_reader :options
@@ -35,7 +36,7 @@ module ZendeskAPI
35
36
  has_parent = namespace.size > 1 || (options[:with_parent] && @options.parent)
36
37
 
37
38
  if has_parent
38
- parent_class = @options.parent ? @options.parent.class : ZendeskAPI.get_class(namespace[0])
39
+ parent_class = @options.parent ? @options.parent.class : ZendeskAPI.const_get(ZendeskAPI::Helpers.modulize_string(namespace[0]))
39
40
  parent_namespace = build_parent_namespace(parent_class, instance, options, original_options)
40
41
  namespace[1..1] = parent_namespace if parent_namespace
41
42
  namespace[0] = parent_class.resource_name
@@ -50,6 +51,7 @@ module ZendeskAPI
50
51
  namespace.join("/")
51
52
  end
52
53
 
54
+ # Tries to place side loads onto given resources.
53
55
  def side_load(resources, side_loads)
54
56
  key = "#{options.name}_id"
55
57
  plural_key = "#{Inflection.singular options.name.to_s}_ids"
@@ -138,6 +140,8 @@ module ZendeskAPI
138
140
  # * Commonly used resources are automatically side-loaded server side and sent along with their parent object.
139
141
  # * Associated resource ids are sent and are then loaded one-by-one into the parent collection.
140
142
  # * The association is represented with Rails' nested association urls (such as tickets/:id/groups) and are loaded that way.
143
+ #
144
+ # @private
141
145
  module Associations
142
146
  def self.included(base)
143
147
  base.send(:extend, ClassMethods)
@@ -156,6 +160,7 @@ module ZendeskAPI
156
160
  end
157
161
  end
158
162
 
163
+ # @private
159
164
  module ClassMethods
160
165
  include Rescue
161
166
 
@@ -174,10 +179,15 @@ module ZendeskAPI
174
179
  end
175
180
 
176
181
  # Represents a parent-to-child association between resources. Options to pass in are: class, path.
177
- # @param [Symbol] resource_name The underlying resource name
178
- # @param [Hash] opts The options to pass to the method definition.
179
- def has(resource_name, class_level_options = {})
180
- klass = get_class(class_level_options.delete(:class)) || get_class(resource_name)
182
+ # @param [Symbol] resource_name_or_class The underlying resource name or a class to get it from
183
+ # @param [Hash] class_level_options The options to pass to the method definition.
184
+ def has(resource_name_or_class, class_level_options = {})
185
+ if klass = class_level_options.delete(:class)
186
+ resource_name = resource_name_or_class
187
+ else
188
+ klass = resource_name_or_class
189
+ resource_name = klass.singular_resource_name
190
+ end
181
191
 
182
192
  class_level_association = {
183
193
  :class => klass,
@@ -229,18 +239,23 @@ module ZendeskAPI
229
239
  end
230
240
 
231
241
  # Represents a parent-to-children association between resources. Options to pass in are: class, path.
232
- # @param [Symbol] resource The underlying resource name
233
- # @param [Hash] opts The options to pass to the method definition.
234
- def has_many(resource_name, class_level_opts = {})
235
- klass = get_class(class_level_opts.delete(:class)) || get_class(Inflection.singular(resource_name.to_s))
242
+ # @param [Symbol] resource_name_or_class The underlying resource name or class to get it from
243
+ # @param [Hash] class_level_options The options to pass to the method definition.
244
+ def has_many(resource_name_or_class, class_level_options = {})
245
+ if klass = class_level_options.delete(:class)
246
+ resource_name = resource_name_or_class
247
+ else
248
+ klass = resource_name_or_class
249
+ resource_name = klass.resource_name
250
+ end
236
251
 
237
252
  class_level_association = {
238
253
  :class => klass,
239
254
  :name => resource_name,
240
- :inline => class_level_opts.delete(:inline),
241
- :path => class_level_opts.delete(:path),
242
- :include => (class_level_opts.delete(:include) || klass.resource_name).to_s,
243
- :include_key => (class_level_opts.delete(:include_key) || :id).to_s,
255
+ :inline => class_level_options.delete(:inline),
256
+ :path => class_level_options.delete(:path),
257
+ :include => (class_level_options.delete(:include) || klass.resource_name).to_s,
258
+ :include_key => (class_level_options.delete(:include_key) || :id).to_s,
244
259
  :singular => false
245
260
  }
246
261
 
@@ -292,47 +307,6 @@ module ZendeskAPI
292
307
  resource
293
308
  end
294
309
  end
295
-
296
- # Allows using has and has_many without having class defined yet
297
- # Guesses at Resource, if it's anything else and the class is later
298
- # reopened under a different superclass, an error will be thrown
299
- def get_class(resource)
300
- return false if resource.nil?
301
- res = ZendeskAPI::Helpers.modulize_string(resource.to_s)
302
-
303
- begin
304
- const_get(res)
305
- rescue NameError, ArgumentError # ruby raises NameError, rails raises ArgumentError
306
- ZendeskAPI.get_class(resource)
307
- end
308
- end
309
- end
310
- end
311
-
312
- class << self
313
- # Make sure Rails' overwriting of const_missing doesn't cause trouble
314
- def const_missing(*args)
315
- Object.const_missing(*args)
316
- end
317
-
318
- # Allows using has and has_many without having class defined yet
319
- # Guesses at Resource, if it's anything else and the class is later
320
- # reopened under a different superclass, an error will be thrown
321
- def get_class(resource)
322
- return false if resource.nil?
323
- res = ZendeskAPI::Helpers.modulize_string(resource.to_s).split("::")
324
-
325
- begin
326
- res[1..-1].inject(ZendeskAPI.const_get(res[0])) do |iter, k|
327
- begin
328
- iter.const_get(k)
329
- rescue
330
- iter.const_set(k, Class.new(Resource))
331
- end
332
- end
333
- rescue NameError
334
- ZendeskAPI.const_set(res[0], Class.new(Resource))
335
- end
336
310
  end
337
311
  end
338
312
  end
@@ -17,6 +17,8 @@ require 'zendesk_api/middleware/response/parse_iso_dates'
17
17
  require 'zendesk_api/middleware/response/logger'
18
18
 
19
19
  module ZendeskAPI
20
+ # The top-level class that handles configuration and connection to the Zendesk API.
21
+ # Can also be used as an accessor to resource collections.
20
22
  class Client
21
23
  include Rescue
22
24
 
@@ -31,8 +33,14 @@ module ZendeskAPI
31
33
  def method_missing(method, *args, &block)
32
34
  method = method.to_s
33
35
  options = args.last.is_a?(Hash) ? args.pop : {}
34
- return instance_variable_get("@#{method}") if !options.delete(:reload) && instance_variable_defined?("@#{method}")
35
- instance_variable_set("@#{method}", ZendeskAPI::Collection.new(self, ZendeskAPI.get_class(Inflection.singular(method)), options))
36
+
37
+ @resource_cache[method] ||= {}
38
+
39
+ if !options[:reload] && (cached = @resource_cache[method][options.hash])
40
+ cached
41
+ else
42
+ @resource_cache[method][options.hash] = ZendeskAPI::Collection.new(self, ZendeskAPI.const_get(ZendeskAPI::Helpers.modulize_string(Inflection.singular(method))), options)
43
+ end
36
44
  end
37
45
 
38
46
  # Returns the current user (aka me)
@@ -42,6 +50,8 @@ module ZendeskAPI
42
50
  @current_user = users.find(:id => 'me')
43
51
  end
44
52
 
53
+ # Returns the current account
54
+ # @return [Hash] The attributes of the current account or nil
45
55
  def current_account(reload = false)
46
56
  return @current_account if @current_account && !reload
47
57
  @current_account = Hashie::Mash.new(connection.get('account/resolve').body)
@@ -50,6 +60,7 @@ module ZendeskAPI
50
60
  rescue_client_error :current_account
51
61
 
52
62
  # Returns the current locale
63
+ # @return [ZendeskAPI::Locale] Current locale or nil
53
64
  def current_locale(reload = false)
54
65
  return @locale if @locale && !reload
55
66
  @locale = locales.find(:id => 'current')
@@ -86,6 +97,8 @@ module ZendeskAPI
86
97
 
87
98
  @callbacks = []
88
99
 
100
+ @resource_cache = {}
101
+
89
102
  if logger = config.logger
90
103
  insert_callback do |env|
91
104
  if warning = env[:response_headers]["X-Zendesk-API-Warn"]
@@ -97,7 +110,7 @@ module ZendeskAPI
97
110
 
98
111
  # Creates a connection if there is none, otherwise returns the existing connection.
99
112
  #
100
- # @returns [Faraday::Connection] Faraday connection for the client
113
+ # @return [Faraday::Connection] Faraday connection for the client
101
114
  def connection
102
115
  @connection ||= build_connection
103
116
  return @connection
@@ -111,7 +124,9 @@ module ZendeskAPI
111
124
 
112
125
  # show a nice warning for people using the old style api
113
126
  def self.check_deprecated_namespace_usage(attributes, name)
114
- raise "un-nest '#{name}' from the attributes" if attributes[name].is_a?(Hash)
127
+ if attributes[name].is_a?(Hash)
128
+ raise "un-nest '#{name}' from the attributes"
129
+ end
115
130
  end
116
131
 
117
132
  protected
@@ -123,7 +138,7 @@ module ZendeskAPI
123
138
  # Uses middleware according to configuration options.
124
139
  #
125
140
  # Request logger if logger is not nil
126
- #
141
+ #
127
142
  # Retry middleware if retry is true
128
143
  def build_connection
129
144
  Faraday.new(config.options) do |builder|
@@ -1,7 +1,5 @@
1
1
  require 'zendesk_api/resource'
2
- require 'zendesk_api/resources/misc'
3
- require 'zendesk_api/resources/ticket'
4
- require 'zendesk_api/resources/user'
2
+ require 'zendesk_api/resources'
5
3
 
6
4
  module ZendeskAPI
7
5
  # Represents a collection of resources. Lazily loaded, resources aren't
@@ -9,6 +7,7 @@ module ZendeskAPI
9
7
  class Collection
10
8
  include ZendeskAPI::Sideloading
11
9
 
10
+ # Options passed in that are automatically converted from an array to a comma-separated list.
12
11
  SPECIALLY_JOINED_PARAMS = [:ids, :only]
13
12
 
14
13
  include Rescue
@@ -19,6 +18,9 @@ module ZendeskAPI
19
18
  # @return [Faraday::Response] The last response
20
19
  attr_reader :response
21
20
 
21
+ # @return [Hash] query options
22
+ attr_reader :options
23
+
22
24
  # Creates a new Collection instance. Does not fetch resources.
23
25
  # Additional options are: verb (default: GET), path (default: resource param), page, per_page.
24
26
  # @param [Client] client The {Client} to use.
@@ -117,10 +119,15 @@ module ZendeskAPI
117
119
  self
118
120
  end
119
121
 
122
+ # Adds an item (or items) to the list of side-loaded resources to request
123
+ # @option sideloads [Symbol or String] The item(s) to sideload
120
124
  def include(*sideloads)
121
125
  self.tap { @includes.concat(sideloads.map(&:to_s)) }
122
126
  end
123
127
 
128
+ # Adds an item to this collection
129
+ # @option item [ZendeskAPI::Data] the resource to add
130
+ # @raise [ArgumentError] if the resource doesn't belong in this collection
124
131
  def <<(item)
125
132
  fetch
126
133
  if item.is_a?(Resource)
@@ -135,6 +142,7 @@ module ZendeskAPI
135
142
  end
136
143
  end
137
144
 
145
+ # The API path to this collection
138
146
  def path
139
147
  @association.generate_path(:with_parent => true)
140
148
  end
@@ -178,18 +186,6 @@ module ZendeskAPI
178
186
  @resources
179
187
  end
180
188
 
181
- def set_page_and_count(body)
182
- @count = (body["count"] || @resources.size).to_i
183
- @next_page, @prev_page = body["next_page"], body["previous_page"]
184
-
185
- if @next_page =~ /page=(\d+)/
186
- @options["page"] = $1.to_i - 1
187
- elsif @prev_page =~ /page=(\d+)/
188
- @options["page"] = $1.to_i + 1
189
- end
190
- end
191
-
192
-
193
189
  rescue_client_error :fetch, :with => lambda { Array.new }
194
190
 
195
191
  # Alias for fetch(false)
@@ -218,12 +214,15 @@ module ZendeskAPI
218
214
  end
219
215
  end
220
216
 
217
+ # Replaces the current (loaded or not) resources with the passed in collection
218
+ # @option collection [Array] The collection to replace this one with
219
+ # @raise [ArgumentError] if any resources passed in don't belong in this collection
221
220
  def replace(collection)
222
221
  raise "this collection is for #{@resource_class}" if collection.any?{|r| !r.is_a?(@resource_class) }
223
222
  @resources = collection
224
223
  end
225
224
 
226
- # Find the next page. Does one of three things:
225
+ # Find the next page. Does one of three things:
227
226
  # * If there is already a page number in the options hash, it increases it and invalidates the cache, returning the new page number.
228
227
  # * If there is a next_page url cached, it executes a fetch on that url and returns the results.
229
228
  # * Otherwise, returns an empty array.
@@ -265,6 +264,7 @@ module ZendeskAPI
265
264
  @prev_page = nil
266
265
  end
267
266
 
267
+ # @private
268
268
  def to_ary; nil; end
269
269
 
270
270
  # Sends methods to underlying array of resources.
@@ -283,6 +283,8 @@ module ZendeskAPI
283
283
  end
284
284
 
285
285
  alias :orig_to_s :to_s
286
+
287
+ # @private
286
288
  def to_s
287
289
  if @resources
288
290
  @resources.inspect
@@ -290,5 +292,18 @@ module ZendeskAPI
290
292
  orig_to_s
291
293
  end
292
294
  end
295
+
296
+ private
297
+
298
+ def set_page_and_count(body)
299
+ @count = (body["count"] || @resources.size).to_i
300
+ @next_page, @prev_page = body["next_page"], body["previous_page"]
301
+
302
+ if @next_page =~ /page=(\d+)/
303
+ @options["page"] = $1.to_i - 1
304
+ elsif @prev_page =~ /page=(\d+)/
305
+ @options["page"] = $1.to_i + 1
306
+ end
307
+ end
293
308
  end
294
309
  end