desk_api 0.6.0 → 0.6.1

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