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.
- data/.gitignore +7 -0
- data/.rspec +2 -0
- data/.travis.yml +5 -0
- data/.yardopts +1 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +59 -0
- data/LICENSE +19 -0
- data/Rakefile +49 -0
- data/Readme.md +178 -0
- data/lib/zendesk_api.rb +10 -0
- data/lib/zendesk_api/actions.rb +176 -0
- data/lib/zendesk_api/association.rb +267 -0
- data/lib/zendesk_api/client.rb +150 -0
- data/lib/zendesk_api/collection.rb +233 -0
- data/lib/zendesk_api/configuration.rb +52 -0
- data/lib/zendesk_api/core_ext/inflection.rb +13 -0
- data/lib/zendesk_api/core_ext/modulize.rb +10 -0
- data/lib/zendesk_api/core_ext/snakecase.rb +12 -0
- data/lib/zendesk_api/lru_cache.rb +38 -0
- data/lib/zendesk_api/middleware/request/etag_cache.rb +38 -0
- data/lib/zendesk_api/middleware/request/retry.rb +39 -0
- data/lib/zendesk_api/middleware/request/upload.rb +32 -0
- data/lib/zendesk_api/middleware/response/callback.rb +19 -0
- data/lib/zendesk_api/middleware/response/deflate.rb +18 -0
- data/lib/zendesk_api/middleware/response/gzip.rb +18 -0
- data/lib/zendesk_api/middleware/response/parse_iso_dates.rb +29 -0
- data/lib/zendesk_api/rescue.rb +44 -0
- data/lib/zendesk_api/resource.rb +133 -0
- data/lib/zendesk_api/resources/forum.rb +51 -0
- data/lib/zendesk_api/resources/misc.rb +66 -0
- data/lib/zendesk_api/resources/playlist.rb +64 -0
- data/lib/zendesk_api/resources/ticket.rb +76 -0
- data/lib/zendesk_api/resources/user.rb +44 -0
- data/lib/zendesk_api/track_changes.rb +72 -0
- data/lib/zendesk_api/trackie.rb +8 -0
- data/lib/zendesk_api/verbs.rb +43 -0
- data/lib/zendesk_api/version.rb +3 -0
- data/live/Readme.md +4 -0
- data/live/activity_spec.rb +5 -0
- data/live/audit_spec.rb +5 -0
- data/live/bookmark_spec.rb +11 -0
- data/live/category_spec.rb +12 -0
- data/live/collection_spec.rb +68 -0
- data/live/crm_spec.rb +11 -0
- data/live/custom_role_spec.rb +5 -0
- data/live/forum_spec.rb +14 -0
- data/live/forum_subscription_spec.rb +12 -0
- data/live/group_membership_spec.rb +18 -0
- data/live/group_spec.rb +14 -0
- data/live/identity_spec.rb +14 -0
- data/live/locale_spec.rb +11 -0
- data/live/macro_spec.rb +5 -0
- data/live/mobile_device_spec.rb +11 -0
- data/live/organization_spec.rb +12 -0
- data/live/satisfaction_rating_spec.rb +6 -0
- data/live/setting_spec.rb +5 -0
- data/live/suspended_ticket_spec.rb +8 -0
- data/live/ticket_field_spec.rb +12 -0
- data/live/ticket_metrics_spec.rb +6 -0
- data/live/ticket_spec.rb +88 -0
- data/live/topic_comment_spec.rb +13 -0
- data/live/topic_spec.rb +18 -0
- data/live/topic_subscription_spec.rb +12 -0
- data/live/topic_vote_spec.rb +13 -0
- data/live/upload_spec.rb +9 -0
- data/live/user_spec.rb +13 -0
- data/live/view_spec.rb +6 -0
- data/spec/association_spec.rb +210 -0
- data/spec/client_spec.rb +149 -0
- data/spec/collection_spec.rb +302 -0
- data/spec/configuration_spec.rb +24 -0
- data/spec/create_resource_spec.rb +39 -0
- data/spec/data_resource_spec.rb +229 -0
- data/spec/fixtures/Argentina.gif +0 -0
- data/spec/fixtures/Argentina2.gif +0 -0
- data/spec/fixtures/credentials.yml.example +3 -0
- data/spec/fixtures/test_resources.rb +8 -0
- data/spec/fixtures/zendesk.rb +88 -0
- data/spec/lru_cache_spec.rb +26 -0
- data/spec/macros/resource_macros.rb +157 -0
- data/spec/middleware/request/etag_cache_spec.rb +17 -0
- data/spec/middleware/request/retry_spec.rb +47 -0
- data/spec/middleware/request/test.jpg +0 -0
- data/spec/middleware/request/upload_spec.rb +74 -0
- data/spec/middleware/response/callback_spec.rb +17 -0
- data/spec/middleware/response/deflate_spec.rb +15 -0
- data/spec/middleware/response/gzip_spec.rb +19 -0
- data/spec/middleware/response/parse_iso_dates_spec.rb +44 -0
- data/spec/playlist_spec.rb +95 -0
- data/spec/read_resource_spec.rb +37 -0
- data/spec/rescue_spec.rb +94 -0
- data/spec/resource_spec.rb +332 -0
- data/spec/spec_helper.rb +120 -0
- data/spec/string_spec.rb +7 -0
- data/spec/trackie_spec.rb +39 -0
- data/zendesk_api.gemspec +38 -0
- 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
|