desk_api 0.6.0 → 0.6.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (57) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE.txt +27 -0
  3. data/README.md +97 -61
  4. data/lib/desk.rb +29 -1
  5. data/lib/desk_api.rb +57 -5
  6. data/lib/desk_api/client.rb +104 -47
  7. data/lib/desk_api/configuration.rb +201 -108
  8. data/lib/desk_api/default.rb +109 -52
  9. data/lib/desk_api/error.rb +90 -40
  10. data/lib/desk_api/error/bad_gateway.rb +29 -1
  11. data/lib/desk_api/error/bad_request.rb +29 -1
  12. data/lib/desk_api/error/client_error.rb +31 -2
  13. data/lib/desk_api/error/configuration_error.rb +29 -1
  14. data/lib/desk_api/error/conflict.rb +29 -1
  15. data/lib/desk_api/error/follow_redirect_error.rb +42 -0
  16. data/lib/desk_api/error/forbidden.rb +29 -1
  17. data/lib/desk_api/error/gateway_timeout.rb +29 -1
  18. data/lib/desk_api/error/internal_server_error.rb +29 -1
  19. data/lib/desk_api/error/method_not_allowed.rb +29 -1
  20. data/lib/desk_api/error/not_acceptable.rb +29 -1
  21. data/lib/desk_api/error/not_found.rb +29 -1
  22. data/lib/desk_api/error/parser_error.rb +29 -1
  23. data/lib/desk_api/error/server_error.rb +29 -1
  24. data/lib/desk_api/error/service_unavailable.rb +29 -1
  25. data/lib/desk_api/error/too_many_requests.rb +29 -1
  26. data/lib/desk_api/error/unauthorized.rb +29 -1
  27. data/lib/desk_api/error/unprocessable_entity.rb +29 -1
  28. data/lib/desk_api/error/unsupported_media_type.rb +29 -1
  29. data/lib/desk_api/rate_limit.rb +63 -21
  30. data/lib/desk_api/request/encode_json.rb +49 -7
  31. data/lib/desk_api/request/oauth.rb +62 -14
  32. data/lib/desk_api/request/retry.rb +108 -34
  33. data/lib/desk_api/resource.rb +402 -192
  34. data/lib/desk_api/response/follow_redirects.rb +99 -0
  35. data/lib/desk_api/response/parse_dates.rb +63 -21
  36. data/lib/desk_api/response/parse_json.rb +47 -5
  37. data/lib/desk_api/response/raise_error.rb +51 -10
  38. data/lib/desk_api/version.rb +30 -2
  39. data/spec/cassettes/DeskApi_Resource/_update/can_handle_action_params.yml +110 -104
  40. data/spec/cassettes/DeskApi_Resource/_update/can_handle_links.yml +426 -0
  41. data/spec/desk_api/client_spec.rb +28 -0
  42. data/spec/desk_api/configuration_spec.rb +28 -0
  43. data/spec/desk_api/default_spec.rb +28 -0
  44. data/spec/desk_api/error_spec.rb +29 -1
  45. data/spec/desk_api/rate_limit_spec.rb +28 -0
  46. data/spec/desk_api/request/encode_json_spec.rb +28 -0
  47. data/spec/desk_api/request/oauth_spec.rb +28 -0
  48. data/spec/desk_api/request/retry_spec.rb +29 -1
  49. data/spec/desk_api/resource_spec.rb +49 -12
  50. data/spec/desk_api/response/follow_redirects_spec.rb +95 -0
  51. data/spec/desk_api/response/parse_dates_spec.rb +28 -0
  52. data/spec/desk_api/response/parse_json_spec.rb +56 -9
  53. data/spec/desk_api/response/raise_error_spec.rb +28 -0
  54. data/spec/desk_api_spec.rb +28 -0
  55. data/spec/spec_helper.rb +28 -0
  56. metadata +84 -24
  57. data/LICENSE +0 -7
@@ -1,11 +1,53 @@
1
- module DeskApi::Request
2
- class EncodeJson < Faraday::Middleware
3
- dependency 'json'
1
+ # Copyright (c) 2013-2014, Salesforce.com, Inc.
2
+ # All rights reserved.
3
+ #
4
+ # Redistribution and use in source and binary forms, with or without modification,
5
+ # are permitted provided that the following conditions are met:
6
+ #
7
+ # * Redistributions of source code must retain the above copyright notice, this
8
+ # list of conditions and the following disclaimer.
9
+ #
10
+ # * Redistributions in binary form must reproduce the above copyright notice,
11
+ # this list of conditions and the following disclaimer in the documentation
12
+ # and/or other materials provided with the distribution.
13
+ #
14
+ # * Neither the name of Salesforce.com nor the names of its contributors may be
15
+ # used to endorse or promote products derived from this software without
16
+ # specific prior written permission.
17
+ #
18
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
19
+ # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
20
+ # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21
+ # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
22
+ # ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
23
+ # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
24
+ # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
25
+ # ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
26
+ # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
27
+ # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
4
28
 
5
- def call(env)
6
- env[:request_headers]['Content-Type'] = 'application/json'
7
- env[:body] = ::JSON.dump(env[:body]) if env[:body] and not env[:body].to_s.empty?
8
- @app.call env
29
+ module DeskApi
30
+ module Request
31
+ # {DeskApi::Request::EncodeJson} is the Faraday middleware
32
+ # that dumps a json string from whatever is specified in
33
+ # the request body. It also sets the "Content-Type" header.
34
+ #
35
+ # @author Thomas Stachl <tstachl@salesforce.com>
36
+ # @copyright Copyright (c) 2013-2014 Salesforce.com
37
+ # @license BSD 3-Clause License
38
+ class EncodeJson < Faraday::Middleware
39
+ dependency 'json'
40
+
41
+ # Changes the request before it gets sent
42
+ #
43
+ # @param env [Hash] the request hash
44
+ def call(env)
45
+ env[:request_headers]['Content-Type'] = 'application/json'
46
+ if env[:body] && !env[:body].to_s.empty?
47
+ env[:body] = ::JSON.dump(env[:body])
48
+ end
49
+ @app.call env
50
+ end
9
51
  end
10
52
  end
11
53
  end
@@ -1,20 +1,68 @@
1
- module DeskApi::Request
2
- class OAuth < Faraday::Middleware
3
- dependency 'simple_oauth'
1
+ # Copyright (c) 2013-2014, Salesforce.com, Inc.
2
+ # All rights reserved.
3
+ #
4
+ # Redistribution and use in source and binary forms, with or without modification,
5
+ # are permitted provided that the following conditions are met:
6
+ #
7
+ # * Redistributions of source code must retain the above copyright notice, this
8
+ # list of conditions and the following disclaimer.
9
+ #
10
+ # * Redistributions in binary form must reproduce the above copyright notice,
11
+ # this list of conditions and the following disclaimer in the documentation
12
+ # and/or other materials provided with the distribution.
13
+ #
14
+ # * Neither the name of Salesforce.com nor the names of its contributors may be
15
+ # used to endorse or promote products derived from this software without
16
+ # specific prior written permission.
17
+ #
18
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
19
+ # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
20
+ # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21
+ # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
22
+ # ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
23
+ # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
24
+ # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
25
+ # ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
26
+ # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
27
+ # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
4
28
 
5
- def initialize(app, options)
6
- super(app)
7
- @options = options
8
- end
29
+ module DeskApi
30
+ module Request
31
+ # {DeskApi::Request::OAuth} is the Faraday middleware to
32
+ # sign requests with an OAuth header.
33
+ #
34
+ # @author Thomas Stachl <tstachl@salesforce.com>
35
+ # @copyright Copyright (c) 2013-2014 Salesforce.com
36
+ # @license BSD 3-Clause License
37
+ class OAuth < Faraday::Middleware
38
+ dependency 'simple_oauth'
9
39
 
10
- def call(env)
11
- env[:request_headers]['Authorization'] = oauth(env).to_s
12
- @app.call env
13
- end
40
+ # Initializies the middleware and sets options
41
+ #
42
+ # @param app [Hash] the faraday environment hash
43
+ # @param options [Hash] additional options
44
+ def initialize(app, options)
45
+ super(app)
46
+ @options = options
47
+ end
48
+
49
+ # Changes the request before it gets sent
50
+ #
51
+ # @param env [Hash] the request hash
52
+ def call(env)
53
+ env[:request_headers]['Authorization'] = oauth(env).to_s
54
+ @app.call env
55
+ end
56
+
57
+ private
14
58
 
15
- private
16
- def oauth(env)
17
- SimpleOAuth::Header.new env[:method], env[:url].to_s, {}, @options
59
+ # Returns the OAuth header
60
+ #
61
+ # @param env [Hash] the request hash
62
+ # @return [String]
63
+ def oauth(env)
64
+ SimpleOAuth::Header.new env[:method], env[:url].to_s, {}, @options
65
+ end
18
66
  end
19
67
  end
20
68
  end
@@ -1,47 +1,121 @@
1
- module DeskApi::Request
2
- class Retry < Faraday::Middleware
3
- def initialize(app, options = {})
4
- @max = options[:max] || 3
5
- @interval = options[:interval] || 10
6
- super(app)
7
- end
1
+ # Copyright (c) 2013-2014, Salesforce.com, Inc.
2
+ # All rights reserved.
3
+ #
4
+ # Redistribution and use in source and binary forms, with or without modification,
5
+ # are permitted provided that the following conditions are met:
6
+ #
7
+ # * Redistributions of source code must retain the above copyright notice, this
8
+ # list of conditions and the following disclaimer.
9
+ #
10
+ # * Redistributions in binary form must reproduce the above copyright notice,
11
+ # this list of conditions and the following disclaimer in the documentation
12
+ # and/or other materials provided with the distribution.
13
+ #
14
+ # * Neither the name of Salesforce.com nor the names of its contributors may be
15
+ # used to endorse or promote products derived from this software without
16
+ # specific prior written permission.
17
+ #
18
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
19
+ # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
20
+ # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21
+ # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
22
+ # ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
23
+ # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
24
+ # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
25
+ # ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
26
+ # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
27
+ # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
8
28
 
9
- def call(env)
10
- retries = @max
11
- request_body = env[:body]
12
- begin
13
- env[:body] = request_body
14
- @app.call(env)
15
- rescue DeskApi::Error::TooManyRequests => e
16
- if retries > 0
17
- retries = 0
18
- sleep e.rate_limit.reset_in
19
- retry
29
+ module DeskApi
30
+ module Request
31
+ # {DeskApi::Request::Retry} is a Faraday middleware that
32
+ # retries failed requests up to 3 times. It also includes
33
+ # desk.com's rate limiting which are retried only once.
34
+ #
35
+ # @author Thomas Stachl <tstachl@salesforce.com>
36
+ # @copyright Copyright (c) 2013-2014 Salesforce.com
37
+ # @license BSD 3-Clause License
38
+ class Retry < Faraday::Middleware
39
+ class << self
40
+ # Returns an array of errors that should be retried.
41
+ #
42
+ # @return [Array]
43
+ def errors
44
+ @exceptions ||= [
45
+ Errno::ETIMEDOUT,
46
+ 'Timeout::Error',
47
+ Faraday::Error::TimeoutError,
48
+ DeskApi::Error::TooManyRequests
49
+ ]
20
50
  end
21
- raise
22
- rescue exception_matcher
23
- if retries > 0
24
- retries -= 1
25
- sleep @interval
51
+ end
52
+
53
+ # Initializies the middleware and sets options
54
+ #
55
+ # @param app [Hash] the faraday environment hash
56
+ # @param options [Hash] additional options
57
+ def initialize(app, options = {})
58
+ @max = options[:max] || 3
59
+ @interval = options[:interval] || 10
60
+ super(app)
61
+ end
62
+
63
+ # Rescues exceptions and retries the request
64
+ #
65
+ # @param env [Hash] the request hash
66
+ def call(env)
67
+ retries = @max
68
+ request_body = env[:body]
69
+ begin
70
+ env[:body] = request_body
71
+ @app.call(env)
72
+ rescue exception_matcher => err
73
+ raise unless calc(err, retries) { |x| retries = x } > 0
74
+ sleep interval(err)
26
75
  retry
27
76
  end
28
- raise
29
77
  end
30
- end
31
78
 
32
- def exception_matcher
33
- exceptions = [Errno::ETIMEDOUT, 'Timeout::Error', Faraday::Error::TimeoutError]
34
- matcher = Module.new
35
- (class << matcher; self; end).class_eval do
36
- define_method(:===) do |error|
37
- exceptions.any? do |ex|
38
- if ex.is_a? Module then error.is_a? ex
39
- else error.class.to_s == ex.to_s
79
+ # Calculates the retries based on the error
80
+ #
81
+ # @param err [StandardError] the error that has been thrown
82
+ # @param retries [Integer] current retry count
83
+ # @return [Integer]
84
+ def calc(err, retries, &block)
85
+ # retry only once
86
+ if err.kind_of?(DeskApi::Error::TooManyRequests) && retries == @max - 1
87
+ block.call(0)
88
+ else
89
+ block.call(retries - 1)
90
+ end
91
+ end
92
+
93
+ # Returns the interval for the specific error
94
+ #
95
+ # @param err [StandardError] the error that has been thrown
96
+ # @return [Integer]
97
+ def interval(err)
98
+ if err.kind_of?(DeskApi::Error::TooManyRequests)
99
+ err.rate_limit.reset_in
100
+ else
101
+ @interval
102
+ end
103
+ end
104
+
105
+ # Returns an exception matcher
106
+ #
107
+ # @return [Module]
108
+ def exception_matcher
109
+ matcher = Module.new
110
+ (class << matcher; self; end).class_eval do
111
+ define_method(:===) do |error|
112
+ Retry.errors.any? do |ex|
113
+ ex.is_a?(Module) ? error.is_a?(ex) : error.class.to_s == ex.to_s
40
114
  end
41
115
  end
42
116
  end
117
+ matcher
43
118
  end
44
- matcher
45
119
  end
46
120
  end
47
121
  end
@@ -1,250 +1,460 @@
1
- class DeskApi::Resource
2
- # by_url is deprecated on resources
3
- extend Forwardable
4
- def_delegator :@_client, :by_url, :by_url
5
-
6
- class << self
7
- def build_self_link(link, params = {})
8
- link = {'href'=>link} if link.kind_of?(String)
9
- {'_links'=>{'self'=>link}}
1
+ # Copyright (c) 2013-2014, Salesforce.com, Inc.
2
+ # All rights reserved.
3
+ #
4
+ # Redistribution and use in source and binary forms, with or without modification,
5
+ # are permitted provided that the following conditions are met:
6
+ #
7
+ # * Redistributions of source code must retain the above copyright notice, this
8
+ # list of conditions and the following disclaimer.
9
+ #
10
+ # * Redistributions in binary form must reproduce the above copyright notice,
11
+ # this list of conditions and the following disclaimer in the documentation
12
+ # and/or other materials provided with the distribution.
13
+ #
14
+ # * Neither the name of Salesforce.com nor the names of its contributors may be
15
+ # used to endorse or promote products derived from this software without
16
+ # specific prior written permission.
17
+ #
18
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
19
+ # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
20
+ # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21
+ # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
22
+ # ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
23
+ # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
24
+ # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
25
+ # ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
26
+ # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
27
+ # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28
+
29
+ module DeskApi
30
+ # {DeskApi::Resource} holds most of the magic of this wrapper. Basically
31
+ # everything that comes back from Desk.com's API is a Resource, it keeps
32
+ # track of all the data, connects you to other resources through links
33
+ # and allows you access to embedded resources.
34
+ #
35
+ # @author Thomas Stachl <tstachl@salesforce.com>
36
+ # @copyright Copyright (c) 2013-2014 Salesforce.com
37
+ # @license BSD 3-Clause License
38
+ #
39
+ # @example get a cases {DeskApi::Resource}
40
+ # cases_resource = DeskApi.cases
41
+ class Resource
42
+ extend Forwardable
43
+ def_delegator :@_client, :by_url, :by_url
44
+
45
+ class << self
46
+ # Returns a {DeskApi::Resource} definition with a self link
47
+ #
48
+ # @param link [String/Hash] the self href as string or hash
49
+ # @return [Hash]
50
+ def build_self_link(link, params = {})
51
+ link = {'href'=>link} if link.kind_of?(String)
52
+ {'_links'=>{'self'=>link}}
53
+ end
10
54
  end
11
- end
12
-
13
- def initialize(client, definition = {}, loaded = false)
14
- reset!
15
- @_client, @_definition, @_loaded = client, definition, loaded
16
- # better default
17
- end
18
-
19
- def create(params = {})
20
- new_resource(@_client.post(clean_base_url, params).body, true)
21
- end
22
-
23
- def update(params = {})
24
- changes = filter_update_actions params
25
- params.each_pair{ |key, value| send("#{key}=", value) if respond_to?("#{key}=") }
26
- changes.merge!(@_changed.clone)
27
- @_changed = {}
28
- @_definition = @_client.patch(href, changes).body
29
- end
30
-
31
- def delete
32
- @_client.delete(href).status === 204
33
- end
34
55
 
35
- def search(params = {})
36
- params = { q: params } if params.kind_of?(String)
37
- url = Addressable::URI.parse(clean_base_url + '/search')
38
- url.query_values = params
39
- new_resource(self.class.build_self_link(url.to_s))
40
- end
56
+ # Initializes a new {DeskApi::Resource} object
57
+ #
58
+ # @param client [DeskApi::Client] the client to be used
59
+ # @param definition [Hash] a defintion for the resource
60
+ # @param loaded [Boolean] indicator of the loading state
61
+ # @return [DeskApi::Resource] the new resource
62
+ def initialize(client, definition = {}, loaded = false)
63
+ reset!
64
+ @_client, @_definition, @_loaded = client, definition, loaded
65
+ end
41
66
 
42
- def find(id, options = {})
43
- res = new_resource(self.class.build_self_link("#{clean_base_url}/#{id}"))
44
- res.embed(*(options[:embed].kind_of?(Array) ? options[:embed] : [options[:embed]])) if options[:embed]
45
- res.exec!
46
- end
47
- alias_method :by_id, :find
67
+ # This method will POST to the Desk.com API and create a
68
+ # new resource
69
+ #
70
+ # @param params [Hash] the params to create the resource
71
+ # @return [DeskApi::Resource] the newly created resource
72
+ def create(params = {})
73
+ new_resource(@_client.post(clean_base_url, params).body, true)
74
+ end
48
75
 
49
- def next!
50
- self.load
51
- next_page = @_definition['_links']['next']
76
+ # Use this method to update a {DeskApi::Resource}, it'll
77
+ # PATCH changes to the Desk.com API
78
+ #
79
+ # @param params [Hash] the params to update the resource
80
+ # @return [DeskApi::Resource] the updated resource
81
+ def update(params = {})
82
+ changes = filter_update_actions params
83
+ changes.merge!(filter_links(params)) # quickfix
84
+ params.each_pair{ |key, value| send("#{key}=", value) if respond_to?("#{key}=") }
85
+ changes.merge!(@_changed.clone)
86
+
87
+ reset!
88
+ @_definition, @_loaded = [@_client.patch(href, changes).body, true]
89
+
90
+ self
91
+ end
52
92
 
53
- if next_page
54
- @_definition = self.class.build_self_link(next_page)
55
- self.reset!
93
+ # Deletes the {DeskApi::Resource}.
94
+ #
95
+ # @return [Boolean] has the resource been deleted?
96
+ def delete
97
+ @_client.delete(href).status === 204
56
98
  end
57
99
 
58
- end
100
+ # Using this method allows you to hit the search endpoint
101
+ #
102
+ # @param params [Hash] the search params
103
+ # @return [DeskApi::Resource] the search page resource
104
+ def search(params = {})
105
+ params = { q: params } if params.kind_of?(String)
106
+ url = Addressable::URI.parse(clean_base_url + '/search')
107
+ url.query_values = params
108
+ new_resource(self.class.build_self_link(url.to_s))
109
+ end
59
110
 
60
- def all(&block)
61
- raise ArgumentError, "Block must be given for #all" unless block_given?
62
- each_page do |page, page_num|
63
- page.entries.each { |resource| yield resource, page_num }
111
+ # Returns a {DeskApi::Resource} based on the given id
112
+ #
113
+ # @param id [String/Integer] the id of the resource
114
+ # @param options [Hash] additional options (currently only embed is supported)
115
+ # @return [DeskApi::Resource] the requested resource
116
+ def find(id, options = {})
117
+ res = new_resource(self.class.build_self_link("#{clean_base_url}/#{id}"))
118
+ res.embed(*(options[:embed].kind_of?(Array) ? options[:embed] : [options[:embed]])) if options[:embed]
119
+ res.exec!
120
+ end
121
+ alias_method :by_id, :find
122
+
123
+ # Change self to the next page
124
+ #
125
+ # @return [Desk::Resource] self
126
+ def next!
127
+ self.load
128
+ next_page = @_definition['_links']['next']
129
+
130
+ if next_page
131
+ @_definition = self.class.build_self_link(next_page)
132
+ self.reset!
133
+ end
64
134
  end
65
- end
66
135
 
67
- def each_page
68
- raise ArgumentError, "Block must be given for #each_page" unless block_given?
69
- page = self.first.per_page(self.query_params['per_page'] || 1000).dup
70
- begin
71
- yield page, page.page
72
- end while page.next!
73
- rescue NoMethodError => err
74
- raise NoMethodError, "#each_page and #all are only available on resources which offer pagination"
75
- end
136
+ # Paginate through all the resources on a give page {DeskApi::Resource}
137
+ #
138
+ # @raise [NoMethodError] if self is not a page resource
139
+ # @raise [ArgumentError] if no block is given
140
+ # @yield [DeskApi::Resource] the current resource
141
+ # @yield [Integer] the current page number
142
+ def all
143
+ raise ArgumentError, "Block must be given for #all" unless block_given?
144
+ each_page do |page, page_num|
145
+ page.entries.each { |resource| yield resource, page_num }
146
+ end
147
+ end
76
148
 
149
+ # Paginate through each page on a give page {DeskApi::Resource}
150
+ #
151
+ # @raise [NoMethodError] if self is not a page resource
152
+ # @raise [ArgumentError] if no block is given
153
+ # @yield [DeskApi::Resource] the current page resource
154
+ # @yield [Integer] the current page number
155
+ def each_page
156
+ raise ArgumentError, "Block must be given for #each_page" unless block_given?
157
+
158
+ begin
159
+ page = self.first.per_page(self.query_params['per_page'] || 1000).dup
160
+ rescue NoMethodError => err
161
+ raise NoMethodError, "#each_page and #all are only available on resources which offer pagination"
162
+ end
77
163
 
78
- def embed(*embedds)
79
- # make sure we don't try to embed anything that's not defined
80
- # add it to the query
81
- self.tap{ |res| res.query_params = { embed: embedds.join(',') } }
82
- end
164
+ begin
165
+ yield page, page.page
166
+ end while page.next!
167
+ end
83
168
 
84
- def get_self
85
- @_definition['_links']['self']
86
- end
169
+ # Allows you to embed/sideload resources
170
+ #
171
+ # @example embed customers with their cases
172
+ # my_cases = client.cases.embed(:customers)
173
+ # @example embed assigned_user and assigned_group
174
+ # my_cases = client.cases.embed(:assigned_user, :assigned_group)
175
+ # @param embedds [Symbol/String] whatever you want to embed
176
+ # @return [Desk::Resource] self
177
+ def embed(*embedds)
178
+ # make sure we don't try to embed anything that's not defined
179
+ # add it to the query
180
+ self.tap{ |res| res.query_params = { embed: embedds.join(',') } }
181
+ end
87
182
 
88
- def href
89
- get_self['href']
90
- end
91
- alias_method :get_href, :href
183
+ # Returns the self link hash
184
+ #
185
+ # @return [Hash] self link hash
186
+ def get_self
187
+ @_definition['_links']['self']
188
+ end
92
189
 
93
- def href=(value)
94
- @_definition['_links']['self']['href'] = value
95
- end
190
+ # Returns the self link href
191
+ #
192
+ # @return [String] self link href
193
+ def href
194
+ get_self['href']
195
+ end
196
+ alias_method :get_href, :href
197
+
198
+ # Set the self link href
199
+ #
200
+ # @return [DeskApi::Resource] self
201
+ def href=(value)
202
+ @_definition['_links']['self']['href'] = value
203
+ self
204
+ end
96
205
 
97
- def to_hash
98
- self.load
206
+ # Returns a hash based on the current definition of the resource
207
+ #
208
+ # @return [Hash] definition hash
209
+ def to_hash
210
+ self.load
99
211
 
100
- {}.tap do |hash|
101
- @_definition.each do |k, v|
102
- hash[k] = v
212
+ {}.tap do |hash|
213
+ @_definition.each do |k, v|
214
+ hash[k] = v
215
+ end
103
216
  end
104
217
  end
105
- end
106
218
 
107
- def resource_type
108
- get_self['class']
109
- end
110
-
111
- [:page, :per_page].each do |method|
112
- define_method(method) do |value = nil|
113
- unless value
114
- self.exec! if self.query_params_include?(method.to_s) == nil
115
- return self.query_params_include?(method.to_s).to_i
116
- end
117
- self.tap{ |res| res.query_params = Hash[method.to_s, value.to_s] }
219
+ # Returns the given resource type
220
+ #
221
+ # @return [String] resource type/class
222
+ def resource_type
223
+ get_self['class']
118
224
  end
119
- end
120
225
 
121
- def query_params
122
- Addressable::URI.parse(href).query_values || {}
123
- end
124
226
 
125
- def query_params_include?(param)
126
- query_params.include?(param) ? query_params[param] : nil
127
- end
227
+ # Get/set the page and per_page query params
228
+ #
229
+ # @param value [Integer/Nil] the value to use
230
+ # @return [Integer/DeskApi::Resource]
231
+ %w(page per_page).each do |method|
232
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
233
+ def #{method}(value = nil)
234
+ unless value
235
+ exec! if query_params_include?('#{method}') == nil
236
+ return query_params_include?('#{method}').to_i
237
+ end
238
+ tap{ |res| res.query_params = Hash['#{method}', value.to_s] }
239
+ end
240
+ RUBY
241
+ end
128
242
 
129
- def query_params=(params = {})
130
- return href if params.empty?
243
+ # Converts the current self href query params to a hash
244
+ #
245
+ # @return [Hash] current self href query params
246
+ def query_params
247
+ Addressable::URI.parse(href).query_values || {}
248
+ end
131
249
 
132
- params.keys.each{ |key| params[key] = params[key].join(',') if params[key].is_a?(Array) }
250
+ # Checks if the specified param is included
251
+ #
252
+ # @param param [String] the param to check for
253
+ # @return [Boolean]
254
+ def query_params_include?(param)
255
+ query_params.include?(param) ? query_params[param] : nil
256
+ end
133
257
 
134
- uri = Addressable::URI.parse(href)
135
- params = (uri.query_values || {}).merge(params)
258
+ # Sets the query params based on the provided hash
259
+ #
260
+ # @param params [Hash] the query params
261
+ # @return [String] the generated href
262
+ def query_params=(params = {})
263
+ return href if params.empty?
136
264
 
137
- @_loaded = false unless params == uri.query_values
265
+ params.keys.each{ |key| params[key] = params[key].join(',') if params[key].is_a?(Array) }
138
266
 
139
- uri.query_values = params
140
- self.href = uri.to_s
141
- end
267
+ uri = Addressable::URI.parse(href)
268
+ params = (uri.query_values || {}).merge(params)
142
269
 
143
- def respond_to?(method, include_private = false)
144
- self.load
145
- meth = method.to_s
270
+ @_loaded = false unless params == uri.query_values
146
271
 
147
- return true if is_embedded?(meth)
148
- return true if is_link?(meth)
149
- return true if meth.end_with?('=') and is_field?(meth[0...-1])
150
- return true if is_field?(meth)
272
+ uri.query_values = params
273
+ self.href = uri.to_s
274
+ end
151
275
 
152
- super
153
- end
276
+ # Checks if this resource responds to a specific method
277
+ #
278
+ # @param method [String/Symbol]
279
+ # @return [Boolean]
280
+ def respond_to?(method)
281
+ self.load
282
+ meth = method.to_s
154
283
 
155
- def reload!
156
- self.exec! true
157
- end
158
- alias_method :load!, :reload!
284
+ return true if is_embedded?(meth)
285
+ return true if is_link?(meth)
286
+ return true if meth.end_with?('=') and is_field?(meth[0...-1])
287
+ return true if is_field?(meth)
159
288
 
160
- def load
161
- self.exec! unless @_loaded
162
- end
289
+ super
290
+ end
163
291
 
164
- def loaded?
165
- @_loaded
166
- end
292
+ # Reloads the current resource
293
+ #
294
+ # @return [DeskApi::Resource] self
295
+ def load!
296
+ self.exec! true
297
+ end
298
+ alias_method :reload!, :load!
167
299
 
168
- protected
300
+ # Only loads the current resource if it isn't loaded yet
301
+ #
302
+ # @return [DeskApi::Resource] self
303
+ def load
304
+ self.exec! unless @_loaded
305
+ end
169
306
 
170
- def clean_base_url
171
- Addressable::URI.parse(href).path.gsub(/\/(search|\d+)$/, '')
172
- end
307
+ # Is the current resource loaded?
308
+ #
309
+ # @return [Boolean]
310
+ def loaded?
311
+ @_loaded
312
+ end
173
313
 
174
- def exec!(reload = false)
175
- return self if @_loaded and !reload
176
- @_definition, @_loaded = @_client.get(href).body, true
177
- self
178
- end
314
+ protected
315
+
316
+ # Returns a clean base url
317
+ #
318
+ # @example removes the search if called from a search resource
319
+ # '/api/v2/cases/search' => '/api/v2/cases'
320
+ # @example removes the id if your on a specific resource
321
+ # '/api/v2/cases/1' => '/api/v2/cases'
322
+ # @return [String] the clean base url
323
+ def clean_base_url
324
+ Addressable::URI.parse(href).path.gsub(/\/(search|\d+)$/, '')
325
+ end
179
326
 
180
- def reset!
181
- @_links, @_embedded, @_changed, @_loaded = {}, {}, {}, false
182
- self
183
- end
327
+ # Executes the request to the Desk.com API if the resource
328
+ # is not loaded yet
329
+ #
330
+ # @param reload [Boolean] should reload the resource
331
+ # @return [DeskApi::Resource] self
332
+ def exec!(reload = false)
333
+ return self if @_loaded and !reload
334
+ @_definition, @_loaded = @_client.get(href).body, true
335
+ self
336
+ end
337
+
338
+ # Resets a {DeskApi::Resource} to an empty state
339
+ #
340
+ # @return [DeskApi::Resource] self
341
+ def reset!
342
+ @_links, @_embedded, @_changed, @_loaded = {}, {}, {}, false
343
+ self
344
+ end
184
345
 
185
- private
186
- attr_accessor :_client, :_loaded, :_changed, :_embedded, :_links, :_definition
346
+ private
347
+ attr_accessor :_client, :_loaded, :_changed, :_embedded, :_links, :_definition
187
348
 
188
- def filter_update_actions(params = {})
189
- params.select{ |key, _| key.to_s.include?('_action') }
190
- end
349
+ # Filters update actions from the params
350
+ #
351
+ # @see http://dev.desk.com/API/customers/#update
352
+ # @param params [Hash]
353
+ # @return [Hash]
354
+ def filter_update_actions(params = {})
355
+ params.select{ |key, _| key.to_s.include?('_action') }
356
+ end
191
357
 
192
- def is_field?(method)
193
- @_definition.key?(method)
194
- end
358
+ # Filters the links
359
+ #
360
+ # @param params [Hash]
361
+ # @return [Hash]
362
+ def filter_links(params = {})
363
+ params.select{ |key, _| key.to_s == '_links' }
364
+ end
195
365
 
196
- def is_link?(method)
197
- @_definition.key?('_links') and @_definition['_links'].key?(method)
198
- end
366
+ # Checks if the given `method` is a field on the current
367
+ # resource definition
368
+ #
369
+ # @param method [String/Symbol]
370
+ # @return [Boolean]
371
+ def is_field?(method)
372
+ @_definition.key?(method)
373
+ end
199
374
 
200
- def is_embedded?(method)
201
- @_definition.key?('_embedded') and @_definition['_embedded'].key?(method)
202
- end
375
+ # Checks if the given `method` is a link on the current
376
+ # resource definition
377
+ #
378
+ # @param method [String/Symbol]
379
+ # @return [Boolean]
380
+ def is_link?(method)
381
+ @_definition.key?('_links') and @_definition['_links'].key?(method)
382
+ end
203
383
 
204
- def get_field_value(method)
205
- @_changed.key?(method) ? @_changed[method] : @_definition[method]
206
- end
384
+ # Checks if the given `method` is embedded in the current
385
+ # resource definition
386
+ #
387
+ # @param method [String/Symbol]
388
+ # @return [Boolean]
389
+ def is_embedded?(method)
390
+ @_definition.key?('_embedded') and @_definition['_embedded'].key?(method)
391
+ end
207
392
 
208
- def get_embedded_resource(method)
209
- return @_embedded[method] if @_embedded.key?(method)
210
- @_embedded[method] = @_definition['_embedded'][method]
393
+ # Returns the field value from the changed or definition hash
394
+ #
395
+ # @param method [String/Symbol]
396
+ # @return [Mixed]
397
+ def get_field_value(method)
398
+ @_changed.key?(method) ? @_changed[method] : @_definition[method]
399
+ end
211
400
 
212
- if @_embedded[method].kind_of?(Array)
213
- @_embedded[method].tap do |ary|
214
- ary.map!{ |definition| new_resource(definition, true) } unless ary.first.kind_of?(self.class)
401
+ # Returns the embedded resource
402
+ #
403
+ # @param method [String/Symbol]
404
+ # @return [DeskApi::Resource]
405
+ def get_embedded_resource(method)
406
+ return @_embedded[method] if @_embedded.key?(method)
407
+ @_embedded[method] = @_definition['_embedded'][method]
408
+
409
+ if @_embedded[method].kind_of?(Array)
410
+ @_embedded[method].tap do |ary|
411
+ ary.map!{ |definition| new_resource(definition, true) } unless ary.first.kind_of?(self.class)
412
+ end
413
+ else
414
+ @_embedded[method] = new_resource(@_embedded[method], true)
215
415
  end
216
- else
217
- @_embedded[method] = new_resource(@_embedded[method], true)
218
416
  end
219
- end
220
417
 
221
- def get_linked_resource(method)
222
- return @_links[method] if @_links.key?(method)
223
- @_links[method] = @_definition['_links'][method]
418
+ # Returns the linked resource
419
+ #
420
+ # @param method [String/Symbol]
421
+ # @return [DeskApi::Resource]
422
+ def get_linked_resource(method)
423
+ return @_links[method] if @_links.key?(method)
424
+ @_links[method] = @_definition['_links'][method]
224
425
 
225
- if @_links[method] and not @_links[method].kind_of?(self.class)
226
- @_links[method] = new_resource(self.class.build_self_link(@_links[method]))
426
+ if @_links[method] and not @_links[method].kind_of?(self.class)
427
+ @_links[method] = new_resource(self.class.build_self_link(@_links[method]))
428
+ end
227
429
  end
228
- end
229
430
 
230
- def new_resource(definition, loaded=false, client=@_client)
231
- self.class.new(client, definition, loaded)
232
- end
233
-
234
- def request(method, url, params={}, client=@_client)
235
- client.send(method, url, params)
236
- end
431
+ # Creates a new resource
432
+ #
433
+ # @param definition [Hash]
434
+ # @param loaded [Boolean]
435
+ # @param client [DeskApi::Client]
436
+ def new_resource(definition, loaded = false, client = @_client)
437
+ self.class.new(client, definition, loaded)
438
+ end
237
439
 
238
- def method_missing(method, *args, &block)
239
- self.load
440
+ # Returns the requested embedded resource, linked resource or field value,
441
+ # also allows to set a new field value
442
+ #
443
+ # @param method [String/Symbol]
444
+ # @param args [Mixed]
445
+ # @param block [Proc]
446
+ # @return [Mixed]
447
+ def method_missing(method, *args, &block)
448
+ self.load
240
449
 
241
- meth = method.to_s
450
+ meth = method.to_s
242
451
 
243
- return get_embedded_resource(meth) if is_embedded?(meth)
244
- return get_linked_resource(meth) if is_link?(meth)
245
- return @_changed[meth[0...-1]] = args.first if meth.end_with?('=') and is_field?(meth[0...-1])
246
- return get_field_value(meth) if is_field?(meth)
452
+ return get_embedded_resource(meth) if is_embedded?(meth)
453
+ return get_linked_resource(meth) if is_link?(meth)
454
+ return @_changed[meth[0...-1]] = args.first if meth.end_with?('=') and is_field?(meth[0...-1])
455
+ return get_field_value(meth) if is_field?(meth)
247
456
 
248
- super(method, *args, &block)
457
+ super(method, *args, &block)
458
+ end
249
459
  end
250
460
  end