my_john_deere_api 2.3.4 → 2.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (35) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +111 -1
  3. data/lib/my_john_deere_api.rb +1 -0
  4. data/lib/my_john_deere_api/client.rb +13 -3
  5. data/lib/my_john_deere_api/net_http_retry.rb +4 -0
  6. data/lib/my_john_deere_api/net_http_retry/decorator.rb +62 -0
  7. data/lib/my_john_deere_api/net_http_retry/invalid_response_error.rb +23 -0
  8. data/lib/my_john_deere_api/net_http_retry/max_retries_exceeded_error.rb +20 -0
  9. data/lib/my_john_deere_api/version.rb +1 -1
  10. data/test/lib/my_john_deere_api/client_test.rb +20 -3
  11. data/test/lib/my_john_deere_api/net_http_retry/decorator_test.rb +208 -0
  12. data/test/lib/my_john_deere_api/net_http_retry/invalid_response_error_test.rb +19 -0
  13. data/test/lib/my_john_deere_api/net_http_retry/max_retries_exceeded_error_test.rb +13 -0
  14. data/test/support/helper.rb +1 -0
  15. data/test/support/vcr/accessor/delete_failed.yml +327 -0
  16. data/test/support/vcr/accessor/delete_invalid.yml +39 -0
  17. data/test/support/vcr/accessor/delete_max_failed.yml +615 -0
  18. data/test/support/vcr/accessor/delete_retry.yml +191 -0
  19. data/test/support/vcr/accessor/delete_retry_too_soon.yml +191 -0
  20. data/test/support/vcr/accessor/get_failed.yml +390 -0
  21. data/test/support/vcr/accessor/get_invalid.yml +46 -0
  22. data/test/support/vcr/accessor/get_max_failed.yml +734 -0
  23. data/test/support/vcr/accessor/get_retry.yml +226 -0
  24. data/test/support/vcr/accessor/get_retry_too_soon.yml +226 -0
  25. data/test/support/vcr/accessor/post_failed.yml +417 -0
  26. data/test/support/vcr/accessor/post_invalid.yml +49 -0
  27. data/test/support/vcr/accessor/post_max_failed.yml +785 -0
  28. data/test/support/vcr/accessor/post_retry.yml +241 -0
  29. data/test/support/vcr/accessor/post_retry_too_soon.yml +241 -0
  30. data/test/support/vcr/accessor/put_failed.yml +372 -0
  31. data/test/support/vcr/accessor/put_invalid.yml +44 -0
  32. data/test/support/vcr/accessor/put_max_failed.yml +700 -0
  33. data/test/support/vcr/accessor/put_retry.yml +216 -0
  34. data/test/support/vcr/accessor/put_retry_too_soon.yml +216 -0
  35. metadata +30 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0b6ea750328ecdcb653d61bbadf19ab4ab23ef2fb712963bda1351736ffedb37
4
- data.tar.gz: '059eab2d2bbb7d8d716bde8f61f5ce935c46039dfbe6c82b933041e4f90fd072'
3
+ metadata.gz: ba69ef151796a688f69eba0f6a396d277baa893aa9e589922f2f2eac7b4e3a6a
4
+ data.tar.gz: e0539d88ae84a348113be06e9f81299b64a5c74598428e9d2a33ec00b0b49584
5
5
  SHA512:
6
- metadata.gz: d9e2285aa182a610b45b757707a26edad1e2ae342ce7ba62609c6a486f247fe3beaaad9e7df39a2ce2077b099b1a31edd53b1732fcdcde1d5b7253da648a7bdb
7
- data.tar.gz: a9fad3831aa447433ee0969eb839d8e21245a29e955b1602ba560b75a989c8737c10c1489474d3982a7f71398407adfb9900968a240fd3cd677b05990e9dc3de
6
+ metadata.gz: 9a40c8720b6e44da21bb5c2d0247055cf8b4f7d04e5b6d9e5d6db03778af368e64b5d2ba375aa02aa671aebdbf3d64d87c8090beb958bb7dc83ef5b1da52196e
7
+ data.tar.gz: 77bac482b0518e59b534d1916b9155c2a59e75308cc71e93354c9a3d071fa10c35362ee836f3eb456b15f612b73d503f06c735d7af942d220415a39c6943c606
data/README.md CHANGED
@@ -24,6 +24,7 @@ without having to code your own oAuth process, API requests, and pagination.
24
24
  * [Organizations](#organizations)
25
25
  * [Assets](#assets)
26
26
  * [Asset Locations](#asset-locations)
27
+ * [Fields](#fields)
27
28
  * [Direct API Requests](#direct-api-requests)
28
29
  * [GET](#get)
29
30
  * [POST](#post)
@@ -346,7 +347,7 @@ organization.links
346
347
  # => {
347
348
  # 'self' => 'https://sandboxapi.deere.com/platform/organizations/1234',
348
349
  # 'machines' => 'https://sandboxapi.deere.com/platform/organizations/1234/machines',
349
- # 'wdtCapableMachines' => 'ttps://sandboxapi.deere.com/platform/organizations/1234/machines?capability=wdt'
350
+ # 'wdtCapableMachines' => 'https://sandboxapi.deere.com/platform/organizations/1234/machines?capability=wdt'
350
351
  # }
351
352
 
352
353
  organization.assets
@@ -558,6 +559,115 @@ Note that locations are called "Asset Locations" in John Deere, but we call the
558
559
  `asset.locations`, for brevity.
559
560
 
560
561
 
562
+ ### [Fields](https://developer.deere.com/#!documentation&doc=myjohndeere%2FfieldsADS.htm)
563
+
564
+ Handles an organization's fields. Field collections support the following methods:
565
+
566
+ * all
567
+ * count
568
+ * first
569
+ * find(field\_id)
570
+
571
+ An individual field supports the following methods and associations:
572
+
573
+ * id
574
+ * name
575
+ * archived?
576
+ * links
577
+ * flags (collection of this field's flags)
578
+
579
+ The `count` method only requires loading the first page of results, so it's a relatively cheap call. On the other hand,
580
+ `all` forces the entire collection to be loaded from John Deere's API, so use with caution. Fields can be
581
+ created via the API, but there is no `create` method on this collection yet.
582
+
583
+ ```ruby
584
+ organization.fields
585
+ # => collection of fields under this organization
586
+
587
+ organization.fields.count
588
+ # => 15
589
+
590
+ organization.fields.first
591
+ # => an individual field object
592
+
593
+ field = organization.fields.find(1234)
594
+ # => an individual field object, fetched by ID
595
+
596
+ field.name
597
+ # => 'Smith Field'
598
+
599
+ field.archived?
600
+ # => false
601
+
602
+ field.links
603
+ # => a hash of API urls related to this field
604
+
605
+ field.flags
606
+ # => collection of flags belonging to this field
607
+ ```
608
+
609
+
610
+ ### [Flags](https://developer.deere.com/#!documentation&doc=.%2Fmyjohndeere%2Fflags.htm)
611
+
612
+ Handles a field's flags. Flag collections support the following methods. Note, John Deere does not provide an endpoint to retrieve a specific flag by id:
613
+
614
+ * all
615
+ * count
616
+ * first
617
+
618
+ An individual flag supports the following methods and associations:
619
+
620
+ * id
621
+ * notes
622
+ * geometry
623
+ * archived?
624
+ * proximity\_alert\_enabled?
625
+ * links
626
+
627
+ The `count` method only requires loading the first page of results, so it's a relatively cheap call. On the other hand,
628
+ `all` forces the entire collection to be loaded from John Deere's API, so use with caution. Flags can be
629
+ created via the API, but there is no `create` method on this collection yet.
630
+
631
+ ```ruby
632
+ field.flags
633
+ # => collection of flags under this field
634
+
635
+ field.flags.count
636
+ # => 15
637
+
638
+ flag = field.flags.first
639
+ # => an individual flag object
640
+
641
+ flag.notes
642
+ # => 'A big rock on the left after entering the field'
643
+
644
+ flag.geometry
645
+ # => a GeoJSON formatted hash, for example:
646
+ # {
647
+ # "type"=>"Feature",
648
+ # "geometry"=>{
649
+ # "geometries"=>[
650
+ # {
651
+ # "coordinates"=>[-95.123456, 40.123456],
652
+ # "type"=>"Point"
653
+ # }
654
+ # ],
655
+ # "type"=>"GeometryCollection"
656
+ # }
657
+ # }
658
+
659
+
660
+ field.archived?
661
+ # => false
662
+
663
+ field.proximity_alert_enabled?
664
+ # => true
665
+
666
+ field.links
667
+ # => a hash of API urls related to this flag
668
+ ```
669
+
670
+
561
671
  ## Direct API Requests
562
672
 
563
673
  While the goal of the client is to eliminate the need to make/interpret calls to the John Deere API, it's important
@@ -13,4 +13,5 @@ module MyJohnDeereApi
13
13
  autoload :Validators, 'my_john_deere_api/validators'
14
14
 
15
15
  require 'my_john_deere_api/errors'
16
+ require 'my_john_deere_api/net_http_retry'
16
17
  end
@@ -4,10 +4,11 @@ module MyJohnDeereApi
4
4
  include Helpers::CaseConversion
5
5
 
6
6
  attr_accessor :contribution_definition_id
7
- attr_reader :api_key, :api_secret, :access_token, :access_secret
7
+ attr_reader :api_key, :api_secret, :access_token, :access_secret, :http_retry_options
8
8
 
9
9
  DEFAULTS = {
10
- environment: :live
10
+ environment: :live,
11
+ http_retry: {}
11
12
  }
12
13
 
13
14
  ##
@@ -37,6 +38,7 @@ module MyJohnDeereApi
37
38
 
38
39
  self.environment = options[:environment]
39
40
  @contribution_definition_id = options[:contribution_definition_id]
41
+ @http_retry_options = options[:http_retry]
40
42
  end
41
43
 
42
44
  ##
@@ -45,7 +47,15 @@ module MyJohnDeereApi
45
47
 
46
48
  def accessor
47
49
  return @accessor if defined?(@accessor)
48
- @accessor = OAuth::AccessToken.new(consumer.user_get, access_token, access_secret)
50
+
51
+ @accessor = NetHttpRetry::Decorator.new(
52
+ OAuth::AccessToken.new(
53
+ consumer.user_get,
54
+ access_token,
55
+ access_secret
56
+ ),
57
+ http_retry_options
58
+ )
49
59
  end
50
60
 
51
61
  ##
@@ -0,0 +1,4 @@
1
+ require 'my_john_deere_api/net_http_retry/decorator'
2
+
3
+ require 'my_john_deere_api/net_http_retry/invalid_response_error'
4
+ require 'my_john_deere_api/net_http_retry/max_retries_exceeded_error'
@@ -0,0 +1,62 @@
1
+ module MyJohnDeereApi
2
+ module NetHttpRetry
3
+ class Decorator
4
+ attr_reader :object, :request_methods, :retry_delay_exponent, :max_retries, :retry_codes, :valid_codes
5
+
6
+ DEFAULTS = {
7
+ request_methods: [:get, :post, :put, :delete],
8
+ retry_delay_exponent: 2,
9
+ max_retries: 12,
10
+ retry_codes: ['429', '503'],
11
+ valid_codes: ['200', '201', '204'],
12
+ }
13
+
14
+ def initialize(object, options={})
15
+ @object = object
16
+
17
+ # defaults that can be used as-is
18
+ [:request_methods, :retry_delay_exponent, :max_retries].each do |option|
19
+ instance_variable_set(:"@#{option}", options[option] || DEFAULTS[option])
20
+ end
21
+
22
+ # defaults that require casting as string arrays
23
+ [:retry_codes, :valid_codes].each do |option|
24
+ instance_variable_set(:"@#{option}", (options[option] || DEFAULTS[option]).map(&:to_s))
25
+ end
26
+ end
27
+
28
+ def request(method_name, *args)
29
+ retries = 0
30
+ result = object.send(method_name, *args)
31
+
32
+ while retry_codes.include?(result.code)
33
+ if retries >= max_retries
34
+ raise MaxRetriesExceededError.new(method_name, "#{result.code} #{result.message}")
35
+ end
36
+
37
+ delay = [result['retry-after'].to_i, retry_delay_exponent ** retries].max
38
+ sleep(delay)
39
+
40
+ result = object.send(method_name, *args)
41
+ retries += 1
42
+ end
43
+
44
+ unless valid_codes.include?(result.code)
45
+ raise InvalidResponseError.new(result)
46
+ end
47
+
48
+ result
49
+ end
50
+
51
+ private
52
+
53
+ def method_missing(method_name, *args, &block)
54
+ if request_methods.include?(method_name)
55
+ request(method_name, *args)
56
+ else
57
+ object.send(method_name, *args, &block)
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,23 @@
1
+ module MyJohnDeereApi
2
+ module NetHttpRetry
3
+ ##
4
+ # This error is used when a single request has exceeded
5
+ # the number of retries allowed by NetHttpRetry::Decorator::MAX_RETRIES.
6
+
7
+ class InvalidResponseError < StandardError
8
+
9
+ ##
10
+ # argument is a string which describes the attempted request
11
+
12
+ def initialize(response)
13
+ message = {
14
+ code: response.code,
15
+ message: response.message,
16
+ body: response.body,
17
+ }.to_json
18
+
19
+ super(message)
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,20 @@
1
+ module MyJohnDeereApi
2
+ module NetHttpRetry
3
+ ##
4
+ # This error is used when a single request has exceeded
5
+ # the number of retries allowed by NetHttpRetry::Decorator::MAX_RETRIES.
6
+
7
+ class MaxRetriesExceededError < StandardError
8
+
9
+ ##
10
+ # argument is a string which describes the attempted request
11
+
12
+ def initialize(request_method, response_message)
13
+ message = "Max retries exceeded for #{request_method.to_s.upcase} " +
14
+ "request: #{response_message}"
15
+
16
+ super(message)
17
+ end
18
+ end
19
+ end
20
+ end
@@ -1,3 +1,3 @@
1
1
  module MyJohnDeereApi
2
- VERSION='2.3.4'
2
+ VERSION='2.5.0'
3
3
  end
@@ -6,7 +6,7 @@ describe 'MyJohnDeereApi::Client' do
6
6
  assert_equal 'thisIsATest', client.send(:camelize, :this_is_a_test)
7
7
  end
8
8
 
9
- describe '#initialize(api_key, api_secret)' do
9
+ describe '#initialize(api_key, api_secret, options={})' do
10
10
  it 'sets the api key/secret' do
11
11
  client = JD::Client.new(api_key, api_secret)
12
12
 
@@ -35,6 +35,23 @@ describe 'MyJohnDeereApi::Client' do
35
35
  client = JD::Client.new(api_key, api_secret, contribution_definition_id: contribution_definition_id)
36
36
  assert_equal contribution_definition_id, client.contribution_definition_id
37
37
  end
38
+
39
+ it 'accepts a list of parameters for NetHttpRetry' do
40
+ custom_retries = JD::NetHttpRetry::Decorator::DEFAULTS[:max_retries] + 10
41
+
42
+ VCR.use_cassette('catalog') do
43
+ new_client = JD::Client.new(
44
+ api_key,
45
+ api_secret,
46
+ contribution_definition_id: contribution_definition_id,
47
+ environment: :sandbox,
48
+ access: [access_token, access_secret],
49
+ http_retry: {max_retries: custom_retries}
50
+ )
51
+
52
+ assert_equal custom_retries, new_client.accessor.max_retries
53
+ end
54
+ end
38
55
  end
39
56
 
40
57
  describe '#contribution_definition_id' do
@@ -130,7 +147,7 @@ describe 'MyJohnDeereApi::Client' do
130
147
 
131
148
  it 'sends the request' do
132
149
  response = VCR.use_cassette('put_asset') { client.put("/assets/#{asset_id}", attributes) }
133
- puts "HASH!! #{response.inspect}" if response.is_a?(Hash)
150
+
134
151
  assert_equal '204', response.code
135
152
  assert_equal 'No Content', response.message
136
153
  end
@@ -199,7 +216,7 @@ describe 'MyJohnDeereApi::Client' do
199
216
 
200
217
  describe '#accessor' do
201
218
  it 'returns an object that can make user-specific requests' do
202
- assert_kind_of OAuth::AccessToken, accessor
219
+ assert_kind_of JD::NetHttpRetry::Decorator, accessor
203
220
  assert_kind_of OAuth::Consumer, accessor.consumer
204
221
  assert_equal access_token, accessor.token
205
222
  assert_equal access_secret, accessor.secret
@@ -0,0 +1,208 @@
1
+ require 'support/helper'
2
+
3
+ describe 'JD::NetHttpRetry::Decorator' do
4
+ REQUESTS = {
5
+ get: '/',
6
+ post: '/organizations/000000/assets',
7
+ put: '/assets/00000000-0000-0000-0000-000000000000',
8
+ delete: '/assets/00000000-0000-0000-0000-000000000000'
9
+ }
10
+
11
+ REQUEST_METHODS = REQUESTS.keys
12
+
13
+ let(:klass) { JD::NetHttpRetry::Decorator }
14
+ let(:object) { klass.new(mock, options) }
15
+
16
+ let(:options) do
17
+ {
18
+ request_methods: request_methods,
19
+ retry_delay_exponent: retry_delay_exponent,
20
+ max_retries: max_retries,
21
+ retry_codes: retry_codes,
22
+ valid_codes: valid_codes
23
+ }
24
+ end
25
+
26
+ let(:request_methods) { nil }
27
+ let(:retry_delay_exponent) { nil }
28
+ let(:max_retries) { nil }
29
+ let(:retry_codes) { nil }
30
+ let(:valid_codes) { nil }
31
+
32
+ let(:retry_values) { [13, 17, 19, 23] }
33
+ let(:exponential_retries) { (0..klass::DEFAULTS[:max_retries]-1).map{|i| 2 ** i} }
34
+
35
+ it 'wraps a "net-http"-responsive object' do
36
+ assert_kind_of OAuth::AccessToken, accessor.object
37
+ end
38
+
39
+ describe '#initialize' do
40
+ describe 'when request methods are specified' do
41
+ let(:request_methods) { [:banana, :fana, :fofana] }
42
+
43
+ it 'uses the supplied values' do
44
+ assert_equal request_methods, object.request_methods
45
+ end
46
+ end
47
+
48
+ describe 'when request methods are not specified' do
49
+ it 'uses the default values' do
50
+ assert_equal klass::DEFAULTS[:request_methods], object.request_methods
51
+ end
52
+ end
53
+
54
+ describe 'when retry_delay_exponent is specified' do
55
+ let(:retry_delay_exponent) { 42 }
56
+
57
+ it 'uses the supplied value' do
58
+ assert_equal retry_delay_exponent, object.retry_delay_exponent
59
+ end
60
+ end
61
+
62
+ describe 'when retry_delay_exponent is not specified' do
63
+ it 'uses the default value' do
64
+ assert_equal klass::DEFAULTS[:retry_delay_exponent], object.retry_delay_exponent
65
+ end
66
+ end
67
+
68
+ describe 'when max_retries is specified' do
69
+ let(:max_retries) { 42 }
70
+
71
+ it 'uses the supplied value' do
72
+ assert_equal max_retries, object.max_retries
73
+ end
74
+ end
75
+
76
+ describe 'when max_retries is not specified' do
77
+ it 'uses the default value' do
78
+ assert_equal klass::DEFAULTS[:max_retries], object.max_retries
79
+ end
80
+ end
81
+
82
+ describe 'when retry_codes are specified' do
83
+ let(:retry_codes) { ['200', '201'] }
84
+
85
+ it 'uses the supplied values' do
86
+ assert_equal retry_codes, object.retry_codes
87
+ end
88
+ end
89
+
90
+ describe 'when retry_codes are specified as integers' do
91
+ let(:retry_codes) { [200, 201] }
92
+
93
+ it 'uses the stringified versions of the supplied values' do
94
+ assert_equal retry_codes.map(&:to_s), object.retry_codes
95
+ end
96
+ end
97
+
98
+ describe 'when retry_codes are not specified' do
99
+ it 'uses the default values' do
100
+ assert_equal klass::DEFAULTS[:retry_codes], object.retry_codes
101
+ end
102
+ end
103
+
104
+ describe 'when valid_codes are specified' do
105
+ let(:valid_codes) { ['123', '234'] }
106
+
107
+ it 'uses the supplied values' do
108
+ assert_equal valid_codes, object.valid_codes
109
+ end
110
+ end
111
+
112
+ describe 'when valid_codes are specified as integers' do
113
+ let(:valid_codes) { [123, 234] }
114
+
115
+ it 'uses the stringified versions of the supplied values' do
116
+ assert_equal valid_codes.map(&:to_s), object.valid_codes
117
+ end
118
+ end
119
+
120
+ describe 'when valid_codes are not specified' do
121
+ it 'uses the default values' do
122
+ assert_kind_of Array, klass::DEFAULTS[:valid_codes]
123
+ refute klass::DEFAULTS[:valid_codes].empty?
124
+
125
+ assert_equal klass::DEFAULTS[:valid_codes], object.valid_codes
126
+ end
127
+ end
128
+ end
129
+
130
+ describe "honors Retry-After headers" do
131
+ REQUEST_METHODS.each do |request_method|
132
+ it "in #{request_method.to_s.upcase} requests" do
133
+ retry_values.each do |retry_seconds|
134
+ accessor.expects(:sleep).with(retry_seconds)
135
+ end
136
+
137
+ VCR.use_cassette("accessor/#{request_method}_retry") do
138
+ accessor.send(request_method, REQUESTS[request_method])
139
+ end
140
+ end
141
+ end
142
+ end
143
+
144
+ describe 'employs exponential wait times for automatic retries' do
145
+ REQUEST_METHODS.each do |request_method|
146
+ it "in #{request_method.to_s.upcase} requests" do
147
+ exponential_retries[0,8].each do |retry_seconds|
148
+ accessor.expects(:sleep).with(retry_seconds)
149
+ end
150
+
151
+ VCR.use_cassette("accessor/#{request_method}_failed") do
152
+ accessor.send(request_method, REQUESTS[request_method])
153
+ end
154
+ end
155
+ end
156
+ end
157
+
158
+ describe 'when Retry-After is shorter than exponential wait time' do
159
+ REQUEST_METHODS.each do |request_method|
160
+ it "chooses longer exponential time in #{request_method.to_s.upcase} requests" do
161
+ exponential_retries[0,4].each do |retry_seconds|
162
+ accessor.expects(:sleep).with(retry_seconds)
163
+ end
164
+
165
+ VCR.use_cassette("accessor/#{request_method}_retry_too_soon") do
166
+ accessor.send(request_method, REQUESTS[request_method])
167
+ end
168
+ end
169
+ end
170
+ end
171
+
172
+ describe 'when max retries have been reached' do
173
+ REQUEST_METHODS.each do |request_method|
174
+ it "returns an error for #{request_method.to_s.upcase} requests" do
175
+ exponential_retries.each do |retry_seconds|
176
+ accessor.expects(:sleep).with(retry_seconds)
177
+ end
178
+
179
+ exception = assert_raises(JD::NetHttpRetry::MaxRetriesExceededError) do
180
+ VCR.use_cassette("accessor/#{request_method}_max_failed") do
181
+ accessor.send(request_method, REQUESTS[request_method])
182
+ end
183
+ end
184
+
185
+ expected_error = "Max retries exceeded for #{request_method.to_s.upcase} request: 429 Too Many Requests"
186
+ assert_equal expected_error, exception.message
187
+ end
188
+ end
189
+ end
190
+
191
+ describe 'when an invalid response code is returned' do
192
+ REQUEST_METHODS.each do |request_method|
193
+ it "returns an error for #{request_method.to_s.upcase} requests" do
194
+ exception = assert_raises(JD::NetHttpRetry::InvalidResponseError) do
195
+ VCR.use_cassette("accessor/#{request_method}_invalid") do
196
+ accessor.send(request_method, REQUESTS[request_method])
197
+ end
198
+ end
199
+
200
+ exception_json = JSON.parse(exception.message)
201
+
202
+ assert_equal '500', exception_json['code']
203
+ assert_equal 'Internal Error', exception_json['message']
204
+ assert_equal 'You Have Died of Dysentery', exception_json['body']
205
+ end
206
+ end
207
+ end
208
+ end