zendesk_api 0.1.7 → 0.1.8

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 (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