discourse_zendesk_api 1.0.0

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 (39) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +176 -0
  3. data/lib/zendesk_api/actions.rb +334 -0
  4. data/lib/zendesk_api/association.rb +195 -0
  5. data/lib/zendesk_api/associations.rb +212 -0
  6. data/lib/zendesk_api/client.rb +243 -0
  7. data/lib/zendesk_api/collection.rb +474 -0
  8. data/lib/zendesk_api/configuration.rb +79 -0
  9. data/lib/zendesk_api/core_ext/inflection.rb +3 -0
  10. data/lib/zendesk_api/delegator.rb +5 -0
  11. data/lib/zendesk_api/error.rb +49 -0
  12. data/lib/zendesk_api/helpers.rb +24 -0
  13. data/lib/zendesk_api/lru_cache.rb +39 -0
  14. data/lib/zendesk_api/middleware/request/encode_json.rb +26 -0
  15. data/lib/zendesk_api/middleware/request/etag_cache.rb +52 -0
  16. data/lib/zendesk_api/middleware/request/raise_rate_limited.rb +31 -0
  17. data/lib/zendesk_api/middleware/request/retry.rb +59 -0
  18. data/lib/zendesk_api/middleware/request/upload.rb +86 -0
  19. data/lib/zendesk_api/middleware/request/url_based_access_token.rb +26 -0
  20. data/lib/zendesk_api/middleware/response/callback.rb +21 -0
  21. data/lib/zendesk_api/middleware/response/deflate.rb +17 -0
  22. data/lib/zendesk_api/middleware/response/gzip.rb +19 -0
  23. data/lib/zendesk_api/middleware/response/logger.rb +44 -0
  24. data/lib/zendesk_api/middleware/response/parse_iso_dates.rb +30 -0
  25. data/lib/zendesk_api/middleware/response/parse_json.rb +23 -0
  26. data/lib/zendesk_api/middleware/response/raise_error.rb +26 -0
  27. data/lib/zendesk_api/middleware/response/sanitize_response.rb +11 -0
  28. data/lib/zendesk_api/resource.rb +208 -0
  29. data/lib/zendesk_api/resources.rb +971 -0
  30. data/lib/zendesk_api/sideloading.rb +58 -0
  31. data/lib/zendesk_api/silent_mash.rb +8 -0
  32. data/lib/zendesk_api/track_changes.rb +85 -0
  33. data/lib/zendesk_api/trackie.rb +13 -0
  34. data/lib/zendesk_api/verbs.rb +65 -0
  35. data/lib/zendesk_api/version.rb +3 -0
  36. data/lib/zendesk_api.rb +4 -0
  37. data/util/resource_handler.rb +74 -0
  38. data/util/verb_handler.rb +16 -0
  39. metadata +166 -0
@@ -0,0 +1,52 @@
1
+ require "faraday/middleware"
2
+
3
+ module ZendeskAPI
4
+ module Middleware
5
+ module Request
6
+ # Request middleware that caches responses based on etags
7
+ # can be removed once this is merged: https://github.com/pengwynn/faraday_middleware/pull/42
8
+ # @private
9
+ class EtagCache < Faraday::Middleware
10
+ def initialize(app, options = {})
11
+ @app = app
12
+ @cache = options[:cache] ||
13
+ raise("need :cache option e.g. ActiveSupport::Cache::MemoryStore.new")
14
+ @cache_key_prefix = options.fetch(:cache_key_prefix, :faraday_etags)
15
+ end
16
+
17
+ def cache_key(env)
18
+ [@cache_key_prefix, env[:url].to_s]
19
+ end
20
+
21
+ def call(environment)
22
+ return @app.call(environment) unless [:get, :head].include?(environment[:method])
23
+
24
+ # send known etag
25
+ cached = @cache.read(cache_key(environment))
26
+
27
+ if cached
28
+ environment[:request_headers]["If-None-Match"] ||= cached[:response_headers]["Etag"]
29
+ end
30
+
31
+ @app.call(environment).on_complete do |env|
32
+ if cached && env[:status] == 304 # not modified
33
+ # Handle differences in serialized env keys in Faraday < 1.0 and 1.0
34
+ # See https://github.com/lostisland/faraday/pull/847
35
+ env[:body] = cached[:body]
36
+ env[:response_body] = cached[:response_body]
37
+
38
+ env[:response_headers].merge!(
39
+ :etag => cached[:response_headers][:etag],
40
+ :content_type => cached[:response_headers][:content_type],
41
+ :content_length => cached[:response_headers][:content_length],
42
+ :content_encoding => cached[:response_headers][:content_encoding]
43
+ )
44
+ elsif env[:status] == 200 && env[:response_headers]["Etag"] # modified and cacheable
45
+ @cache.write(cache_key(env), env.to_hash)
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,31 @@
1
+ require 'faraday/middleware'
2
+ require 'zendesk_api/error'
3
+
4
+ module ZendeskAPI
5
+ module Middleware
6
+ # @private
7
+ module Request
8
+ # Faraday middleware to handle HTTP Status 429 (rate limiting) / 503 (maintenance)
9
+ # @private
10
+ class RaiseRateLimited < Faraday::Middleware
11
+ ERROR_CODES = [429, 503].freeze
12
+
13
+ def initialize(app, options = {})
14
+ super(app)
15
+ @logger = options[:logger]
16
+ end
17
+
18
+ def call(env)
19
+ response = @app.call(env)
20
+
21
+ if ERROR_CODES.include?(response.env[:status])
22
+ @logger&.warn 'You have been rate limited. Raising error...'
23
+ raise Error::RateLimited, env
24
+ else
25
+ response
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,59 @@
1
+ require "faraday/middleware"
2
+ module ZendeskAPI
3
+ module Middleware
4
+ # @private
5
+ module Request
6
+ # Faraday middleware to handle HTTP Status 429 (rate limiting) / 503 (maintenance)
7
+ # @private
8
+ class Retry < Faraday::Middleware
9
+ DEFAULT_RETRY_AFTER = 10
10
+ DEFAULT_ERROR_CODES = [429, 503]
11
+
12
+ def initialize(app, options = {})
13
+ super(app)
14
+ @logger = options[:logger]
15
+ @error_codes = options.key?(:retry_codes) && options[:retry_codes] ? options[:retry_codes] : DEFAULT_ERROR_CODES
16
+ @retry_on_exception = options.key?(:retry_on_exception) && options[:retry_on_exception] ? options[:retry_on_exception] : false
17
+ end
18
+
19
+ def call(env)
20
+ original_env = env.dup
21
+ exception_happened = false
22
+ if @retry_on_exception
23
+ begin
24
+ response = @app.call(env)
25
+ rescue StandardError => e
26
+ exception_happened = true
27
+ end
28
+ else
29
+ response = @app.call(env)
30
+ end
31
+
32
+ if exception_happened || @error_codes.include?(response.env[:status])
33
+
34
+ if exception_happened
35
+ seconds_left = DEFAULT_RETRY_AFTER.to_i
36
+ @logger.warn "An exception happened, waiting #{seconds_left} seconds... #{e}" if @logger
37
+ else
38
+ seconds_left = (response.env[:response_headers][:retry_after] || DEFAULT_RETRY_AFTER).to_i
39
+ end
40
+
41
+ @logger.warn "You have been rate limited. Retrying in #{seconds_left} seconds..." if @logger
42
+
43
+ seconds_left.times do |i|
44
+ sleep 1
45
+ time_left = seconds_left - i
46
+ @logger.warn "#{time_left}..." if time_left > 0 && time_left % 5 == 0 && @logger
47
+ end
48
+
49
+ @logger.warn "" if @logger
50
+
51
+ @app.call(original_env)
52
+ else
53
+ response
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,86 @@
1
+ require "faraday/middleware"
2
+ require "mini_mime"
3
+ require 'tempfile'
4
+
5
+ module ZendeskAPI
6
+ module Middleware
7
+ module Request
8
+ # @private
9
+ class Upload < Faraday::Middleware
10
+ def call(env)
11
+ if env[:body]
12
+ set_file(env[:body], :file, true)
13
+ traverse_hash(env[:body])
14
+ end
15
+
16
+ @app.call(env)
17
+ end
18
+
19
+ private
20
+
21
+ # Sets the proper file parameters :uploaded_data and :filename
22
+ # If top_level, then it removes key and and sets the parameters directly on hash,
23
+ # otherwise it adds the parameters to hash[key]
24
+ def set_file(hash, key, top_level)
25
+ return unless hash.key?(key)
26
+
27
+ file = if hash[key].is_a?(Hash) && hash[key].key?(:file)
28
+ hash[key].delete(:file)
29
+ else
30
+ hash.delete(key)
31
+ end
32
+
33
+ case file
34
+ when File, Tempfile
35
+ path = file.path
36
+ when String
37
+ path = file
38
+ else
39
+ if defined?(ActionDispatch) && file.is_a?(ActionDispatch::Http::UploadedFile)
40
+ path = file.tempfile.path
41
+ mime_type = file.content_type
42
+ else
43
+ warn "WARNING: Passed invalid filename #{file} of type #{file.class} to upload"
44
+ end
45
+ end
46
+
47
+ if path
48
+ if !top_level
49
+ hash[key] ||= {}
50
+ hash = hash[key]
51
+ end
52
+
53
+ unless defined?(mime_type) && !mime_type.nil?
54
+ mime_type = MiniMime.lookup_by_filename(path)
55
+ mime_type = mime_type ? mime_type.content_type : "application/octet-stream"
56
+ end
57
+
58
+ hash[:filename] ||= if file.respond_to?(:original_filename)
59
+ file.original_filename
60
+ else
61
+ File.basename(path)
62
+ end
63
+
64
+ hash[:uploaded_data] = Faraday::UploadIO.new(path, mime_type, hash[:filename])
65
+ end
66
+ end
67
+
68
+ # Calls #set_file on File instances or Hashes
69
+ # of the format { :file => File (, :filename => ...) }
70
+ def traverse_hash(hash)
71
+ hash.keys.each do |key|
72
+ if hash[key].is_a?(File)
73
+ set_file(hash, key, false)
74
+ elsif hash[key].is_a?(Hash)
75
+ if hash[key].key?(:file)
76
+ set_file(hash, key, false)
77
+ else
78
+ traverse_hash(hash[key])
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,26 @@
1
+ module ZendeskAPI
2
+ # @private
3
+ module Middleware
4
+ # @private
5
+ module Request
6
+ class UrlBasedAccessToken < Faraday::Middleware
7
+ def initialize(app, token)
8
+ super(app)
9
+ @token = token
10
+ end
11
+
12
+ def call(env)
13
+ if env[:url].query
14
+ env[:url].query += '&'
15
+ else
16
+ env[:url].query = ''
17
+ end
18
+
19
+ env[:url].query += "access_token=#{@token}"
20
+
21
+ @app.call(env)
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,21 @@
1
+ require "faraday/response"
2
+
3
+ module ZendeskAPI
4
+ module Middleware
5
+ module Response
6
+ # @private
7
+ class Callback < Faraday::Response::Middleware
8
+ def initialize(app, client)
9
+ super(app)
10
+ @client = client
11
+ end
12
+
13
+ def call(env)
14
+ @app.call(env).on_complete do |env|
15
+ @client.callbacks.each { |c| c.call(env) }
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,17 @@
1
+ module ZendeskAPI
2
+ # @private
3
+ module Middleware
4
+ # @private
5
+ module Response
6
+ # Faraday middleware to handle content-encoding = inflate
7
+ # @private
8
+ class Deflate < Faraday::Response::Middleware
9
+ def on_complete(env)
10
+ if !env.body.strip.empty? && env[:response_headers]['content-encoding'] == "deflate"
11
+ env.body = Zlib::Inflate.inflate(env.body)
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,19 @@
1
+ require 'zlib'
2
+ require 'stringio'
3
+
4
+ module ZendeskAPI
5
+ # @private
6
+ module Middleware
7
+ # @private
8
+ module Response
9
+ # Faraday middleware to handle content-encoding = gzip
10
+ class Gzip < Faraday::Response::Middleware
11
+ def on_complete(env)
12
+ if !env[:body].strip.empty? && env[:response_headers]['content-encoding'] == "gzip"
13
+ env[:body] = Zlib::GzipReader.new(StringIO.new(env[:body])).read
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,44 @@
1
+ module ZendeskAPI
2
+ module Middleware
3
+ module Response
4
+ # Faraday middleware to handle logging
5
+ # @private
6
+ class Logger < Faraday::Middleware
7
+ LOG_LENGTH = 1000
8
+
9
+ def initialize(app, logger = nil)
10
+ super(app)
11
+
12
+ @logger = logger || begin
13
+ require 'logger'
14
+ ::Logger.new(STDOUT)
15
+ end
16
+ end
17
+
18
+ def call(env)
19
+ @logger.info "#{env[:method]} #{env[:url].to_s}"
20
+ @logger.debug dump_debug(env, :request_headers)
21
+
22
+ @app.call(env).on_complete do |env|
23
+ info = "Status #{env[:status]}"
24
+ info.concat(" #{env[:body].to_s[0, LOG_LENGTH]}") if (400..499).cover?(env[:status].to_i)
25
+
26
+ @logger.info info
27
+ @logger.debug dump_debug(env, :response_headers)
28
+ end
29
+ end
30
+
31
+ private
32
+
33
+ def dump_debug(env, headers_key)
34
+ info = env[headers_key].map { |k, v| " #{k}: #{v.inspect}" }.join("\n")
35
+ unless env[:body].nil?
36
+ info.concat("\n")
37
+ info.concat(env[:body].inspect)
38
+ end
39
+ info
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,30 @@
1
+ require 'time'
2
+ require "faraday/response"
3
+
4
+ module ZendeskAPI
5
+ module Middleware
6
+ module Response
7
+ # Parse ISO dates from response body
8
+ # @private
9
+ class ParseIsoDates < Faraday::Response::Middleware
10
+ def call(env)
11
+ @app.call(env).on_complete do |env|
12
+ parse_dates!(env[:body])
13
+ end
14
+ end
15
+
16
+ private
17
+
18
+ def parse_dates!(value)
19
+ case value
20
+ when Hash then value.each { |key, element| value[key] = parse_dates!(element) }
21
+ when Array then value.each_with_index { |element, index| value[index] = parse_dates!(element) }
22
+ when /\A\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z\Z/m then Time.parse(value)
23
+ else
24
+ value
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,23 @@
1
+ module ZendeskAPI
2
+ # @private
3
+ module Middleware
4
+ # @private
5
+ module Response
6
+ class ParseJson < Faraday::Response::Middleware
7
+ CONTENT_TYPE = 'Content-Type'.freeze
8
+ dependency 'json'
9
+
10
+ def on_complete(env)
11
+ type = env[:response_headers][CONTENT_TYPE].to_s
12
+ type = type.split(';', 2).first if type.index(';')
13
+
14
+ return unless type == 'application/json'
15
+
16
+ unless env[:body].strip.empty?
17
+ env[:body] = JSON.parse(env[:body])
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,26 @@
1
+ require 'zendesk_api/error'
2
+
3
+ module ZendeskAPI
4
+ module Middleware
5
+ module Response
6
+ class RaiseError < Faraday::Response::RaiseError
7
+ def call(env)
8
+ super
9
+ rescue Faraday::TimeoutError, Faraday::ConnectionFailed => e
10
+ raise Error::NetworkError.new(e, env)
11
+ end
12
+
13
+ def on_complete(env)
14
+ case env[:status]
15
+ when 404
16
+ raise Error::RecordNotFound.new(env)
17
+ when 422, 413
18
+ raise Error::RecordInvalid.new(env)
19
+ when 100..199, 400..599, 300..303, 305..399
20
+ raise Error::NetworkError.new(env)
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,11 @@
1
+ module ZendeskAPI
2
+ module Middleware
3
+ module Response
4
+ class SanitizeResponse < Faraday::Response::Middleware
5
+ def on_complete(env)
6
+ env[:body].scrub!('')
7
+ end
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,208 @@
1
+ require 'zendesk_api/helpers'
2
+ require 'zendesk_api/trackie'
3
+ require 'zendesk_api/actions'
4
+ require 'zendesk_api/association'
5
+ require 'zendesk_api/associations'
6
+ require 'zendesk_api/verbs'
7
+
8
+ module ZendeskAPI
9
+ # Represents a resource that only holds data.
10
+ class Data
11
+ include Associations
12
+
13
+ class << self
14
+ def inherited(klass)
15
+ subclasses.push(klass)
16
+ end
17
+
18
+ def subclasses
19
+ @subclasses ||= []
20
+ end
21
+
22
+ # The singular resource name taken from the class name (e.g. ZendeskAPI::Ticket -> ticket)
23
+ def singular_resource_name
24
+ @singular_resource_name ||= ZendeskAPI::Helpers.snakecase_string(to_s.split("::").last)
25
+ end
26
+
27
+ # The resource name taken from the class name (e.g. ZendeskAPI::Ticket -> tickets)
28
+ def resource_name
29
+ @resource_name ||= Inflection.plural(singular_resource_name)
30
+ end
31
+
32
+ def resource_path
33
+ [@namespace, resource_name].compact.join("/")
34
+ end
35
+
36
+ alias :model_key :resource_name
37
+
38
+ def namespace(namespace)
39
+ @namespace = namespace
40
+ end
41
+ end
42
+
43
+ # @return [Hash] The resource's attributes
44
+ attr_reader :attributes
45
+ # @return [ZendeskAPI::Association] The association
46
+ attr_accessor :association
47
+ # @return [Array] The last received errors
48
+ attr_accessor :errors
49
+ # Place to dump the last response
50
+ attr_accessor :response
51
+
52
+ # Create a new resource instance.
53
+ # @param [Client] client The client to use
54
+ # @param [Hash] attributes The optional attributes that describe the resource
55
+ def initialize(client, attributes = {})
56
+ raise "Expected a Hash for attributes, got #{attributes.inspect}" unless attributes.is_a?(Hash)
57
+ @association = attributes.delete(:association) || Association.new(:class => self.class)
58
+ @global_params = attributes.delete(:global) || {}
59
+ @client = client
60
+ @attributes = ZendeskAPI::Trackie.new(attributes)
61
+
62
+ if self.class.associations.none? { |a| a[:name] == self.class.singular_resource_name }
63
+ ZendeskAPI::Client.check_deprecated_namespace_usage @attributes, self.class.singular_resource_name
64
+ end
65
+
66
+ @attributes.clear_changes unless new_record?
67
+ end
68
+
69
+ def self.new_from_response(client, response, includes = nil)
70
+ new(client).tap do |resource|
71
+ resource.handle_response(response)
72
+ resource.set_includes(resource, includes, response.body) if includes
73
+ resource.attributes.clear_changes
74
+ end
75
+ end
76
+
77
+ # Passes the method onto the attributes hash.
78
+ # If the attributes are nested (e.g. { :tickets => { :id => 1 } }), passes the method onto the nested hash.
79
+ def method_missing(*args, &block)
80
+ raise NoMethodError, ":save is not defined" if args.first.to_sym == :save
81
+ @attributes.send(*args, &block)
82
+ end
83
+
84
+ def respond_to_missing?(method, include_private = false)
85
+ @attributes.respond_to?(method) || super
86
+ end
87
+
88
+ # Returns the resource id of the object or nil
89
+ def id
90
+ key?(:id) ? method_missing(:id) : nil
91
+ end
92
+
93
+ # Has this been object been created server-side? Does this by checking for an id.
94
+ def new_record?
95
+ id.nil?
96
+ end
97
+
98
+ # @private
99
+ def loaded_associations
100
+ self.class.associations.select do |association|
101
+ loaded = @attributes.method_missing(association[:name])
102
+ loaded && !(loaded.respond_to?(:empty?) && loaded.empty?)
103
+ end
104
+ end
105
+
106
+ # Returns the path to the resource
107
+ def path(options = {})
108
+ @association.generate_path(self, options)
109
+ end
110
+
111
+ # Passes #to_json to the underlying attributes hash
112
+ def to_json(*args)
113
+ method_missing(:to_json, *args)
114
+ end
115
+
116
+ # @private
117
+ def to_s
118
+ "#{self.class.singular_resource_name}: #{attributes.inspect}"
119
+ end
120
+ alias :inspect :to_s
121
+
122
+ # Compares resources by class and id. If id is nil, then by object_id
123
+ def ==(other)
124
+ return true if other.object_id == object_id
125
+
126
+ if other && !(other.is_a?(Data) || other.is_a?(Integer))
127
+ warn "Trying to compare #{other.class} to a Resource from #{caller.first}"
128
+ end
129
+
130
+ if other.is_a?(Data)
131
+ other.id && other.id == id
132
+ elsif other.is_a?(Integer)
133
+ id == other
134
+ else
135
+ false
136
+ end
137
+ end
138
+ alias :eql :==
139
+
140
+ # @private
141
+ def inspect
142
+ "#<#{self.class.name} #{@attributes.to_hash.inspect}>"
143
+ end
144
+
145
+ alias :to_param :attributes
146
+
147
+ private
148
+
149
+ def attributes_for_save
150
+ { self.class.singular_resource_name.to_sym => attributes.changes }
151
+ end
152
+ end
153
+
154
+ # Indexable resource
155
+ class DataResource < Data
156
+ attr_accessor :error, :error_message
157
+ extend Verbs
158
+ end
159
+
160
+ # Represents a resource that can only GET
161
+ class ReadResource < DataResource
162
+ include Read
163
+ end
164
+
165
+ # Represents a resource that can only POST
166
+ class CreateResource < DataResource
167
+ include Create
168
+ end
169
+
170
+ # Represents a resource that can only PUT
171
+ class UpdateResource < DataResource
172
+ include Update
173
+ end
174
+
175
+ # Represents a resource that can only DELETE
176
+ class DeleteResource < DataResource
177
+ include Destroy
178
+ end
179
+
180
+ # Represents a resource that can CRUD (create, read, update, delete).
181
+ class Resource < DataResource
182
+ include Read
183
+ include Create
184
+
185
+ include Update
186
+ include Destroy
187
+ end
188
+
189
+ class SingularResource < Resource
190
+ def attributes_for_save
191
+ { self.class.resource_name.to_sym => attributes.changes }
192
+ end
193
+ end
194
+
195
+ # Namespace parent class for Data/Resource classes
196
+ module DataNamespace
197
+ class << self
198
+ def included(base)
199
+ @descendants ||= []
200
+ @descendants << base
201
+ end
202
+
203
+ def descendants
204
+ @descendants || []
205
+ end
206
+ end
207
+ end
208
+ end