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,52 @@
1
+ module ZendeskAPI
2
+ class Configuration
3
+ # @return [String] The basic auth username.
4
+ attr_accessor :username
5
+
6
+ # @return [String] The basic auth password.
7
+ attr_accessor :password
8
+
9
+ # @return [String] The API url. Must be https unless {#allow_http} is set.
10
+ attr_accessor :url
11
+
12
+ # @return [Boolean] Whether to attempt to retry when rate-limited (http status: 429).
13
+ attr_accessor :retry
14
+
15
+ # @return [Logger] Logger to use when logging requests.
16
+ attr_accessor :logger
17
+
18
+ # @return [Hash] Client configurations (eg ssh config) to pass to Faraday
19
+ attr_accessor :client_options
20
+
21
+ # @return [Symbol] Faraday adapter
22
+ attr_accessor :adapter
23
+
24
+ # @return [Boolean] Whether to allow non-HTTPS connections for development purposes.
25
+ attr_accessor :allow_http
26
+
27
+ # Use this cache instead of default ZendeskAPI::LRUCache.new
28
+ # - must respond to read/write/fetch e.g. ActiveSupport::Cache::MemoryStore.new)
29
+ # - pass false to disable caching
30
+ # @return [ZendeskAPI::LRUCache]
31
+ attr_accessor :cache
32
+
33
+ def initialize
34
+ @client_options = {}
35
+ self.cache = ZendeskAPI::LRUCache.new(1000)
36
+ end
37
+
38
+ # Sets accept and user_agent headers, and url.
39
+ #
40
+ # @return [Hash] Faraday-formatted hash of options.
41
+ def options
42
+ {
43
+ :headers => {
44
+ :accept => 'application/json',
45
+ :accept_encoding => 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3',
46
+ :user_agent => "ZendeskAPI API #{ZendeskAPI::VERSION}"
47
+ },
48
+ :url => @url
49
+ }.merge(client_options)
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,13 @@
1
+ require 'inflection'
2
+
3
+ Inflection.plural_rule 'forum', 'forums'
4
+
5
+ class String
6
+ def singular
7
+ Inflection.singular(self)
8
+ end
9
+
10
+ def plural
11
+ Inflection.plural(self)
12
+ end
13
+ end
@@ -0,0 +1,10 @@
1
+ # From https://github.com/rubyworks/facets/blob/master/lib/core/facets/string/modulize.rb
2
+ class String
3
+ def modulize
4
+ #gsub('__','/'). # why was this ever here?
5
+ gsub(/__(.?)/){ "::#{$1.upcase}" }.
6
+ gsub(/\/(.?)/){ "::#{$1.upcase}" }.
7
+ gsub(/(?:_+|-+)([a-z])/){ $1.upcase }.
8
+ gsub(/(\A|\s)([a-z])/){ $1 + $2.upcase }
9
+ end
10
+ end
@@ -0,0 +1,12 @@
1
+ # From https://github.com/rubyworks/facets/blob/master/lib/core/facets/string/snakecase.rb
2
+ class String
3
+ def snakecase
4
+ #gsub(/::/, '/').
5
+ gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
6
+ gsub(/([a-z\d])([A-Z])/,'\1_\2').
7
+ tr('-', '_').
8
+ gsub(/\s/, '_').
9
+ gsub(/__+/, '_').
10
+ downcase
11
+ end
12
+ end
@@ -0,0 +1,38 @@
1
+ module ZendeskAPI
2
+ # http://codesnippets.joyent.com/posts/show/12329
3
+ class ZendeskAPI::LRUCache
4
+ attr_accessor :size
5
+
6
+ def initialize(size = 10)
7
+ @size = size
8
+ @store = {}
9
+ @lru = []
10
+ end
11
+
12
+ def write(key, value)
13
+ @store[key] = value
14
+ set_lru(key)
15
+ @store.delete(@lru.pop) if @lru.size > @size
16
+ value
17
+ end
18
+
19
+ def read(key)
20
+ set_lru(key)
21
+ @store[key]
22
+ end
23
+
24
+ def fetch(key)
25
+ if @store.has_key? key
26
+ read key
27
+ else
28
+ write key, yield
29
+ end
30
+ end
31
+
32
+ private
33
+
34
+ def set_lru(key)
35
+ @lru.unshift(@lru.delete(key) || key)
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,38 @@
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
+ class EtagCache < Faraday::Middleware
9
+ def initialize(app, options = {})
10
+ @app = app
11
+ @cache = options[:cache] ||
12
+ raise("need :cache option e.g. ActiveSupport::Cache::MemoryStore.new")
13
+ @cache_key_prefix = options.fetch(:cache_key_prefix, :faraday_etags)
14
+ end
15
+
16
+ def call(env)
17
+ return @app.call(env) unless [:get, :head].include?(env[:method])
18
+ cache_key = [@cache_key_prefix, env[:url].to_s]
19
+
20
+ # send known etag
21
+ if cached = @cache.read(cache_key)
22
+ env[:request_headers]["If-None-Match"] ||= cached[:response_headers]["Etag"]
23
+ end
24
+
25
+ @app.call(env).on_complete do
26
+ if cached && env[:status] == 304 # not modified
27
+ env[:body] = cached[:body]
28
+ end
29
+
30
+ if env[:status] == 200 && env[:response_headers]["Etag"] # modified and cacheable
31
+ @cache.write(cache_key, env)
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,39 @@
1
+ require "faraday/middleware"
2
+
3
+ module ZendeskAPI
4
+ module Middleware
5
+ module Request
6
+ # Faraday middleware to handle HTTP Status 429 (rate limiting) / 503 (maintenance)
7
+ class Retry < Faraday::Middleware
8
+ DEFAULT_RETRY_AFTER = 10
9
+ ERROR_CODES = [429, 503]
10
+
11
+ def initialize(app, options={})
12
+ super(app)
13
+ @logger = options[:logger]
14
+ end
15
+
16
+ def call(env)
17
+ response = @app.call(env)
18
+
19
+ if ERROR_CODES.include?(response.env[:status])
20
+ seconds_left = (response.env[:response_headers][:retry_after] || DEFAULT_RETRY_AFTER).to_i
21
+ @logger.warn "You have been rate limited. Retrying in #{seconds_left} seconds..." if @logger
22
+
23
+ seconds_left.times do |i|
24
+ sleep 1
25
+ time_left = seconds_left - i
26
+ @logger.warn "#{time_left}..." if time_left > 0 && time_left % 5 == 0 && @logger
27
+ end
28
+
29
+ @logger.warn "" if @logger
30
+
31
+ @app.call(env)
32
+ else
33
+ response
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,32 @@
1
+ require "faraday/middleware"
2
+ require "mime/types"
3
+
4
+ module ZendeskAPI
5
+ module Middleware
6
+ module Request
7
+ class Upload < Faraday::Middleware
8
+ def call(env)
9
+ if env[:body] && env[:body][:file]
10
+ file = env[:body].delete(:file)
11
+ case file
12
+ when File
13
+ path = file.path
14
+ when String
15
+ path = file
16
+ else
17
+ warn "WARNING: Passed invalid filename #{file} of type #{file.class} to upload"
18
+ end
19
+
20
+ if path
21
+ env[:body][:filename] ||= File.basename(path)
22
+ mime_type = MIME::Types.type_for(path).first || "application/octet-stream"
23
+ env[:body][:uploaded_data] = Faraday::UploadIO.new(path, mime_type)
24
+ end
25
+ end
26
+
27
+ @app.call(env)
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,19 @@
1
+ require "faraday/response"
2
+
3
+ module ZendeskAPI
4
+ module Middleware
5
+ module Response
6
+ class Callback < Faraday::Response::Middleware
7
+ def initialize(app, client)
8
+ super(app)
9
+ @client = client
10
+ end
11
+
12
+ def on_complete(env)
13
+ super(env)
14
+ @client.callbacks.each {|c| c.call(env)}
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,18 @@
1
+ require 'faraday_middleware/response_middleware'
2
+
3
+ module ZendeskAPI
4
+ module Middleware
5
+ module Response
6
+ # Faraday middleware to handle content-encoding = inflate
7
+ class Deflate < FaradayMiddleware::ResponseMiddleware
8
+ define_parser do |body|
9
+ Zlib::Inflate.inflate(body)
10
+ end
11
+
12
+ def parse_response?(env)
13
+ super && env[:response_headers]['content-encoding'] == "deflate"
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,18 @@
1
+ require 'faraday_middleware/response_middleware'
2
+
3
+ module ZendeskAPI
4
+ module Middleware
5
+ module Response
6
+ # Faraday middleware to handle content-encoding = gzip
7
+ class Gzip < FaradayMiddleware::ResponseMiddleware
8
+ define_parser do |body|
9
+ Zlib::GzipReader.new(StringIO.new(body)).read
10
+ end
11
+
12
+ def parse_response?(env)
13
+ super && env[:response_headers]['content-encoding'] == "gzip"
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,29 @@
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
+ class ParseIsoDates < Faraday::Response::Middleware
9
+ def call(env)
10
+ response = @app.call(env)
11
+ parse_dates! response.env[:body]
12
+ response
13
+ end
14
+
15
+ private
16
+
17
+ def parse_dates!(value)
18
+ case value
19
+ when Hash then value.each { |key,element| value[key] = parse_dates!(element) }
20
+ when Array then value.each_with_index { |element, index| value[index] = parse_dates!(element) }
21
+ when /\A\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z\Z/m then Time.parse(value)
22
+ else
23
+ value
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,44 @@
1
+ module ZendeskAPI
2
+ module Rescue
3
+ def self.included(klass)
4
+ klass.extend(Methods)
5
+ klass.send(:include, Methods)
6
+ end
7
+
8
+ module Methods
9
+ def log_error(e, method = false)
10
+ if logger = (@client ? @client.config.logger : Kernel)
11
+ logger.warn "#{self} - #{method}" if method
12
+ logger.warn e.message
13
+ logger.warn e.backtrace.join("\n")
14
+ logger.warn "\t#{e.response[:body].inspect}" if e.response
15
+ end
16
+ end
17
+
18
+ def rescue_client_error(*args)
19
+ opts = args.last.is_a?(Hash) ? args.pop : {}
20
+
21
+ if args.any?
22
+ args.each do |method|
23
+ class_eval("alias :orig_#{method} :#{method}")
24
+ define_method method do |*args|
25
+ begin
26
+ send("orig_#{method}", *args)
27
+ rescue Faraday::Error::ClientError => e
28
+ log_error(e, method)
29
+ opts[:with].respond_to?(:call) ? opts[:with].call : opts[:with]
30
+ end
31
+ end
32
+ end
33
+ elsif block_given?
34
+ begin
35
+ yield
36
+ rescue Faraday::Error::ClientError => e
37
+ log_error(e)
38
+ opts[:with].respond_to?(:call) ? opts[:with].call : opts[:with]
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,133 @@
1
+ require 'zendesk_api/trackie'
2
+ require 'zendesk_api/actions'
3
+ require 'zendesk_api/association'
4
+ require 'zendesk_api/verbs'
5
+
6
+ module ZendeskAPI
7
+ # Represents a resource that only holds data.
8
+ class Data
9
+ include Associations
10
+ include Rescue
11
+
12
+ class << self
13
+ # The singular resource name taken from the class name (e.g. ZendeskAPI::Ticket -> ticket)
14
+ def singular_resource_name
15
+ @singular_resource_name ||= to_s.split("::").last.snakecase
16
+ end
17
+
18
+ # The resource name taken from the class name (e.g. ZendeskAPI::Ticket -> tickets)
19
+ def resource_name
20
+ @resource_name ||= singular_resource_name.plural
21
+ end
22
+
23
+ alias :model_key :resource_name
24
+
25
+ # Rails tries to load dependencies, which messes up automatic resource our own loading
26
+ if method_defined?(:const_missing_without_dependencies)
27
+ alias :const_missing :const_missing_without_dependencies
28
+ end
29
+
30
+ def only_send_unnested_params
31
+ @unnested_params = true
32
+ end
33
+
34
+ def unnested_params
35
+ @unnested_params ||= false
36
+ end
37
+ end
38
+
39
+ # @return [Hash] The resource's attributes
40
+ attr_reader :attributes
41
+ # @return [ZendeskAPI::Association] The association
42
+ attr_accessor :association
43
+
44
+ # Create a new resource instance.
45
+ # @param [Client] client The client to use
46
+ # @param [Hash] attributes The optional attributes that describe the resource
47
+ def initialize(client, attributes = {})
48
+ raise "Expected a Hash for attributes, got #{attributes.inspect}" unless attributes.is_a?(Hash)
49
+ @association = attributes.delete(:association) || Association.new(:class => self.class)
50
+ @client = client
51
+ @attributes = ZendeskAPI::Trackie.new(attributes)
52
+ ZendeskAPI::Client.check_deprecated_namespace_usage @attributes, self.class.singular_resource_name
53
+
54
+ @attributes.clear_changes unless new_record?
55
+ end
56
+
57
+ # Passes the method onto the attributes hash.
58
+ # If the attributes are nested (e.g. { :tickets => { :id => 1 } }), passes the method onto the nested hash.
59
+ def method_missing(*args, &block)
60
+ raise NoMethodError, ":save is not defined" if args.first.to_sym == :save
61
+ @attributes.send(*args, &block)
62
+ end
63
+
64
+ # Returns the resource id of the object or nil
65
+ def id
66
+ key?(:id) ? method_missing(:id) : nil
67
+ end
68
+
69
+ # Has this been object been created server-side? Does this by checking for an id.
70
+ def new_record?
71
+ id.nil?
72
+ end
73
+
74
+ # Returns the path to the resource
75
+ def path(*args)
76
+ @association.generate_path(self, *args)
77
+ end
78
+
79
+ def to_s
80
+ "#{self.class.singular_resource_name}: #{attributes.inspect}"
81
+ end
82
+ alias :inspect :to_s
83
+
84
+ def ==(other)
85
+ warn "Trying to compare #{other.class} to a Resource" if other && !other.is_a?(Data)
86
+ other.is_a?(self.class) && ((other.id && other.id == id) || (other.object_id == self.object_id))
87
+ end
88
+ alias :eql :==
89
+ alias :hash :id
90
+
91
+ def inspect
92
+ "#<#{self.class.name} #{@attributes.to_hash.inspect}>"
93
+ end
94
+
95
+ alias :to_param :attributes
96
+ end
97
+
98
+ # Indexable resource
99
+ class DataResource < Data
100
+ extend Verbs
101
+ end
102
+
103
+ # Represents a resource that can only GET
104
+ class ReadResource < DataResource
105
+ extend Read
106
+ end
107
+
108
+ # Represents a resource that can only POST
109
+ class CreateResource < DataResource
110
+ include Create
111
+ end
112
+
113
+ # Represents a resource that can only PUT
114
+ class UpdateResource < DataResource
115
+ include Update
116
+ end
117
+
118
+ # Represents a resource that can only DELETE
119
+ class DeleteResource < DataResource
120
+ include Destroy
121
+ end
122
+
123
+ # Represents a resource that can CRUD (create, read, update, delete).
124
+ class Resource < DataResource
125
+ extend Read
126
+ include Create
127
+
128
+ include Update
129
+ include Destroy
130
+ end
131
+
132
+ class SingularResource < Resource; end
133
+ end