duffel_api 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/.DS_Store +0 -0
  3. data/.gitignore +1 -0
  4. data/Appraisals +11 -0
  5. data/CHANGELOG.md +6 -0
  6. data/Gemfile +3 -2
  7. data/README.md +34 -1
  8. data/duffel_api.gemspec +6 -4
  9. data/examples/book_with_extra_baggage.rb +83 -0
  10. data/examples/book_with_seat.rb +1 -2
  11. data/lib/duffel_api/api_response.rb +35 -5
  12. data/lib/duffel_api/api_service.rb +18 -4
  13. data/lib/duffel_api/client.rb +24 -1
  14. data/lib/duffel_api/errors/error.rb +44 -1
  15. data/lib/duffel_api/list_response.rb +17 -0
  16. data/lib/duffel_api/middlewares/raise_duffel_errors.rb +18 -2
  17. data/lib/duffel_api/paginator.rb +11 -3
  18. data/lib/duffel_api/request.rb +32 -16
  19. data/lib/duffel_api/resources/aircraft.rb +7 -6
  20. data/lib/duffel_api/resources/airline.rb +7 -6
  21. data/lib/duffel_api/resources/airport.rb +21 -6
  22. data/lib/duffel_api/resources/base_resource.rb +3 -0
  23. data/lib/duffel_api/resources/offer_passenger.rb +11 -0
  24. data/lib/duffel_api/resources/offer_request.rb +15 -6
  25. data/lib/duffel_api/response.rb +24 -5
  26. data/lib/duffel_api/services/aircraft_service.rb +18 -0
  27. data/lib/duffel_api/services/airlines_service.rb +17 -0
  28. data/lib/duffel_api/services/airports_service.rb +18 -0
  29. data/lib/duffel_api/services/base_service.rb +17 -4
  30. data/lib/duffel_api/services/offer_passengers_service.rb +7 -0
  31. data/lib/duffel_api/services/offer_requests_service.rb +24 -0
  32. data/lib/duffel_api/services/offers_service.rb +18 -0
  33. data/lib/duffel_api/services/order_cancellations_service.rb +28 -0
  34. data/lib/duffel_api/services/order_change_offers_service.rb +18 -0
  35. data/lib/duffel_api/services/order_change_requests_service.rb +11 -0
  36. data/lib/duffel_api/services/order_changes_service.rb +17 -0
  37. data/lib/duffel_api/services/orders_service.rb +28 -0
  38. data/lib/duffel_api/services/payment_intents_service.rb +15 -0
  39. data/lib/duffel_api/services/payments_service.rb +5 -0
  40. data/lib/duffel_api/services/refunds_service.rb +10 -0
  41. data/lib/duffel_api/services/seat_maps_service.rb +6 -0
  42. data/lib/duffel_api/services/webhooks_service.rb +23 -2
  43. data/lib/duffel_api/version.rb +1 -1
  44. data/lib/duffel_api/webhook_event.rb +119 -0
  45. data/lib/duffel_api.rb +1 -0
  46. metadata +36 -6
  47. data/.circleci/config.yml +0 -82
  48. data/.github/dependabot.yml +0 -6
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bb4caaadd883236526bb2e8bdbc78b4f96f2264ae3a09f0c6b2d645eb0e63b4f
4
- data.tar.gz: 81a190fbc71c4b5033647393634e0759fdc18513809c1f81fa80dc2ca2453a44
3
+ metadata.gz: 3f34f8f28b32ebbcce50624f302a8c571322804417841653645dd6ea659f8a22
4
+ data.tar.gz: fab5945c12ed6054f01330dc2bd62cb814cd90f486b7dc36ed4caa0c8197617c
5
5
  SHA512:
6
- metadata.gz: 70950a1db91a40ba7fbc1fe2b0b232f8553ab16c497225641ca13d4244374efc9c8b5d80174477e2ab8c3dedd085ea99abc71bdc2159c3af4083ebeab9ac35f6
7
- data.tar.gz: 4686727e0c4e1d005713b44e684f91fa931f73d214a778fad5b2cb83724d5592b4d4aace1810f95d9c544d92d37a62eb903af9a4d2765d18ad9ad14a1a5d2fac
6
+ metadata.gz: 89708a5b441623dfecaee21f06bfba8da70e93b251d2587895a1b7ea3a9df1375251ae5a85de0a4550bf96ef46249a3c8d341f85c187d268cdf4fc6c6029c2da
7
+ data.tar.gz: c0e3165cf5a164dab4d2da40e9171fc2c680800be4a2f43c96e18556c75c9890555bec6cd3455c90de3c8141347bd32fc13d50eb78e33336ce8954a38387633e
data/.DS_Store ADDED
Binary file
data/.gitignore CHANGED
@@ -7,3 +7,4 @@
7
7
  /pkg/
8
8
  /spec/reports/
9
9
  /tmp/
10
+ .DS_Store
data/Appraisals ADDED
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ # vim: syntax=ruby
4
+
5
+ appraise "faraday-1" do
6
+ gem "faraday", "1.8.0"
7
+ end
8
+
9
+ appraise "faraday-2" do
10
+ gem "faraday", "2.0.1"
11
+ end
data/CHANGELOG.md CHANGED
@@ -1,3 +1,9 @@
1
+ ## [0.2.0] - 2022-01-11
2
+
3
+ - Add `WebhookEvent.genuine?` for checking whether a webhook event was genuinely sent by Duffel
4
+ - Add support for `faraday` `v2.x`
5
+ - Add YARD in-code documentation, [published to RubyDoc.info](https://rubydoc.info/github/duffelhq/duffel-api-ruby/main)
6
+
1
7
  ## [0.1.0] - 2021-12-31
2
8
 
3
9
  - Initial release
data/Gemfile CHANGED
@@ -6,14 +6,15 @@ source "https://rubygems.org"
6
6
  gemspec
7
7
 
8
8
  group :development, :test do
9
- gem "gc_ruboconfig", "~> 2.29.0"
9
+ gem "gc_ruboconfig", "~> 2.30.0"
10
10
  gem "pry", "~> 0.14.1"
11
11
  gem "rake", "~> 13.0"
12
12
  gem "rspec", "~> 3.10.0"
13
13
  gem "rspec-its", "~> 1.3.0"
14
- gem "rspec_junit_formatter", "~> 0.4.1"
14
+ gem "rspec_junit_formatter", "~> 0.5.0"
15
15
  gem "rubocop", "~> 1.24.0"
16
16
  gem "rubocop-rake", "~> 0.6.0"
17
17
  gem "simplecov", "~> 0.21.2"
18
18
  gem "webmock", "~> 3.14.0"
19
+ gem "yard", "~> 0.9.27"
19
20
  end
data/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # Duffel API Ruby client library
2
2
 
3
+ [![RubyDoc.info documentation](http://img.shields.io/badge/yard-docs-blue.svg)](https://rubydoc.info/github/duffelhq/duffel-api-ruby)
4
+
3
5
  A Ruby client library for the [Duffel API](https://duffel.com/docs/api).
4
6
 
5
7
  ## Contents
@@ -18,7 +20,7 @@ A Ruby client library for the [Duffel API](https://duffel.com/docs/api).
18
20
  In most cases, you'll want to add `duffel_api` to your project as a dependency by listing it in your `Gemfile`, and then running `bundle`:
19
21
 
20
22
  ```ruby
21
- gem "duffel_api", "~> 0.1.0"
23
+ gem "duffel_api", "~> 0.2.0"
22
24
  ```
23
25
 
24
26
  You can install `duffel_api` outside of the context of a project by running `gem install duffel_api` - for example if you want to play with the client library in `irb`.
@@ -209,4 +211,35 @@ If an error has been raised, you can call `#api_response` on the exception, whic
209
211
 
210
212
  From the `APIResponse`, you can call `#headers`, `#status_code`, `#raw_body`, `#parsed_body`, `#meta` or `#request_id` to get key information from the response.
211
213
 
214
+ ### Verifying webhooks
215
+
216
+ You can set up [webhooks](https://duffel.com/docs/guides/receiving-webhooks) with Duffel to receive notifications about events that happen in your Duffel account - for example, when an airline has a schedule change affecting one of your orders.
217
+
218
+ These webhook events are signed with a shared secret. This allows you to be sure that any webhook events are genuinely sent from Duffel when you receive them.
219
+
220
+ When you create a webhook, you'll set a secret. With that secret in mind, you can verify that a webhook is genuine like this:
221
+
222
+ ```ruby
223
+ # In Rails, you'd get this with `request.raw_post`.
224
+ request_body = '{"created_at":"2022-01-08T18:44:56.129339Z","data":{"changes":{},"object":{}},"id":"eve_0000AFEsrBKZAcKgGtZCnQ","live_mode":false,"object":"order","type":"order.updated"}'
225
+ # In Rails, you'd get this with `request.headers['X-Duffel-Signature']`.
226
+ request_signature = "t=1641667496,v1=691f25ffb1f206c0fda5bb7b1a9d60fafe42c5f42819d44a06a7cfe09486f102"
227
+
228
+ # Note that this code doesn't require your access token - `DuffelAPI::WebhookEvent`
229
+ # doesn't expect you to have a `Client` initialised
230
+ if DuffelAPI::WebhookEvent.genuine?(
231
+ request_body: request_body,
232
+ request_signature: request_signature,
233
+ webhook_secret: "a_secret"
234
+ )
235
+ puts "This is a real webhook from Duffel 🌟"
236
+ else
237
+ puts "This is a fake webhook! ☠️"
238
+ end
239
+ ```
240
+
241
+ ## Learn more
242
+
243
+ You can find complete documentation on this library's classes and methods in the in-code
244
+ documentation on [RubyDoc.info](https://rubydoc.info/github/duffelhq/duffel-api-ruby).
212
245
 
data/duffel_api.gemspec CHANGED
@@ -20,17 +20,19 @@ Gem::Specification.new do |spec|
20
20
  # Specify which files should be added to the gem when it is released.
21
21
  # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
22
22
  spec.files = Dir.chdir(File.expand_path(__dir__)) do
23
- `git ls-files -z`.split("\x0").reject { |f| f.match(%r{\A(?:test|spec|features)/}) }
23
+ `git ls-files -z`.split("\x0").reject do |f|
24
+ f.match(%r{\A(?:test|spec|features|gemfiles|.circleci|.github)/})
25
+ end
24
26
  end
25
27
 
26
28
  spec.bindir = "exe"
27
29
  spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
28
30
  spec.require_paths = ["lib"]
29
31
 
30
- spec.add_dependency "faraday", [">= 0.9.2", "< 2"]
32
+ spec.add_dependency "base16", "~> 0.0.2"
33
+ spec.add_dependency "faraday", ">= 0.9.2", "< 3"
31
34
 
32
- # Uncomment to register a new dependency of your gem
33
- # spec.add_dependency "example-gem", "~> 1.0"
35
+ spec.add_development_dependency "appraisal", "~> 2.4"
34
36
 
35
37
  # For more information and examples about making a new gem, checkout our
36
38
  # guide at: https://bundler.io/guides/creating_gem.html
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "duffel_api"
4
+
5
+ client = DuffelAPI::Client.new(
6
+ access_token: ENV["DUFFEL_ACCESS_TOKEN"],
7
+ )
8
+
9
+ # 365 days from now
10
+ departure_date = (Time.now + (60 * 60 * 24 * 365)).strftime("%Y-%m-%d")
11
+
12
+ offer_request = client.offer_requests.create(params: {
13
+ cabin_class: "economy",
14
+ passengers: [{
15
+ age: 28,
16
+ }],
17
+ slices: [{
18
+ # We use a non-sensical route to make sure we get speedy, reliable Duffel Airways
19
+ # results.
20
+ origin: "LGA",
21
+ destination: "JFK",
22
+ departure_date: departure_date,
23
+ }],
24
+ # This attribute is sent as a query parameter rather than in the body like the others.
25
+ # Worry not! The library handles this complexity for you.
26
+ return_offers: false,
27
+ })
28
+
29
+ puts "Created offer request: #{offer_request.id}"
30
+
31
+ offers = client.offers.all(params: { offer_request_id: offer_request.id })
32
+
33
+ puts "Got #{offers.count} offers"
34
+
35
+ selected_offer = offers.first
36
+
37
+ puts "Selected offer #{selected_offer.id} to book"
38
+
39
+ # Send the query param return_available_services to request ancillaries (e.g. baggage)
40
+ priced_offer = client.offers.get(selected_offer.id,
41
+ params: { return_available_services: true })
42
+
43
+ puts "The final price for offer #{priced_offer.id} is #{priced_offer.total_amount} " \
44
+ "#{priced_offer.total_currency}"
45
+
46
+ available_baggage = priced_offer.available_services.
47
+ find { |service| service["type"] == "baggage" }
48
+
49
+ puts "Adding #{available_baggage['metadata']['maximum_weight_kg']}kg extra baggage for " \
50
+ "#{available_baggage['total_amount']} " \
51
+ "#{available_baggage['total_currency']}"
52
+
53
+ total_amount = priced_offer.total_amount.to_f +
54
+ available_baggage["total_amount"].to_f
55
+
56
+ order = client.orders.create(params: {
57
+ selected_offers: [priced_offer.id],
58
+ services: [{
59
+ id: available_baggage["id"],
60
+ quantity: 1,
61
+ }],
62
+ payments: [
63
+ {
64
+ type: "balance",
65
+ amount: total_amount,
66
+ currency: priced_offer.total_currency,
67
+ },
68
+ ],
69
+ passengers: [
70
+ {
71
+ id: priced_offer.passengers.first["id"],
72
+ title: "mr",
73
+ gender: "m",
74
+ given_name: "Tim",
75
+ family_name: "Rogers",
76
+ born_on: "1993-04-01",
77
+ phone_number: "+441290211999",
78
+ email: "tim@duffel.com",
79
+ },
80
+ ],
81
+ })
82
+
83
+ puts "Created order #{order.id} with booking reference #{order.booking_reference}"
@@ -36,8 +36,7 @@ selected_offer = offers.first
36
36
 
37
37
  puts "Selected offer #{selected_offer.id} to book"
38
38
 
39
- priced_offer = client.offers.get(selected_offer.id,
40
- params: { return_available_services: true })
39
+ priced_offer = client.offers.get(selected_offer.id)
41
40
 
42
41
  puts "The final price for offer #{priced_offer.id} is #{priced_offer.total_amount} " \
43
42
  "#{priced_offer.total_currency}"
@@ -1,17 +1,47 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module DuffelAPI
4
- # wraps a faraday response object with some accessors
4
+ # An HTTP response returned from the API
5
5
  class APIResponse
6
6
  extend Forwardable
7
7
 
8
+ # Builds an `APIResponse` from a `Response`, the library's internal representation
9
+ # of an HTTP response
10
+ #
11
+ # @param response [Response]
12
+ # @return [APIResponse]
8
13
  def initialize(response)
9
14
  @response = response
10
15
  end
11
16
 
12
- def_delegator :@response, :headers
13
- def_delegator :@response, :raw_body, :body
14
- def_delegator :@response, :status_code
15
- def_delegator :@response, :request_id
17
+ # Returns the HTTP response headers
18
+ #
19
+ # @return [Hash]
20
+ def headers
21
+ @response.headers
22
+ end
23
+
24
+ # Returns the raw body of the HTTP response
25
+ #
26
+ # @return [String]
27
+ def body
28
+ @response.raw_body
29
+ end
30
+
31
+ # Returns the HTTP status code of the HTTP response
32
+ #
33
+ # @return [Integer]
34
+ def status_code
35
+ @response.status_code
36
+ end
37
+
38
+ # Returns the request ID from the Duffel API, included in the response headers.
39
+ # This could be `nil` if the response didn't make it to the Duffel API itself and,
40
+ # for example, only reached a load balancer.
41
+ #
42
+ # @return [String, nil]
43
+ def request_id
44
+ @response.request_id
45
+ end
16
46
  end
17
47
  end
@@ -4,8 +4,17 @@ require "faraday"
4
4
  require "uri"
5
5
 
6
6
  module DuffelAPI
7
+ # An internal class used within the library that is able to make requests to
8
+ # the Duffel API and handle errors
7
9
  class APIService
8
- def initialize(base_url, access_token, options = {})
10
+ # Sets up an API service based on a base URL, access token and set of default
11
+ # headers
12
+ #
13
+ # @param base_url [String] A test or live mode access token
14
+ # @param access_token [String] The URL of the Duffel API
15
+ # @param default_headers [Hash] The headers to include by default in HTTP requests
16
+ # @return [APIService]
17
+ def initialize(base_url, access_token, default_headers:)
9
18
  @base_url = base_url
10
19
  root_url, @path_prefix = unpack_url(base_url)
11
20
 
@@ -15,16 +24,21 @@ module DuffelAPI
15
24
  faraday.adapter(:net_http)
16
25
  end
17
26
 
18
- @headers = options[:default_headers] || {}
19
- @headers["Authorization"] = "Bearer #{access_token}"
27
+ @headers = default_headers.merge("Authorization" => "Bearer #{access_token}")
20
28
  end
21
29
 
30
+ # Makes a request to the API, including any defauot headers
31
+ #
32
+ # @param method [Symbol] the HTTP method to make the request with
33
+ # @param path [String] the path to make the request to
34
+ # @param options [Hash] options to be passed with `Request#new`
35
+ # @return [Request]
22
36
  def make_request(method, path, options = {})
23
37
  raise ArgumentError, "options must be a hash" unless options.is_a?(Hash)
24
38
 
25
39
  options[:headers] ||= {}
26
40
  options[:headers] = @headers.merge(options[:headers])
27
- Request.new(@connection, method, @path_prefix + path, options).request
41
+ Request.new(@connection, method, @path_prefix + path, **options).call
28
42
  end
29
43
 
30
44
  private
@@ -1,73 +1,96 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module DuffelAPI
4
+ # A client for accessing the Duffel API, configured with a provided access token and
5
+ # base URL, which provides access to API services
4
6
  class Client
5
7
  API_VERSION = "beta"
6
8
 
9
+ # Sets up the client with your access token
10
+ #
11
+ # @param access_token [String] A test or live mode access token
12
+ # @param base_url [String] The URL of the Duffel API
13
+ # @return [Client]
7
14
  def initialize(access_token:, base_url: "https://api.duffel.com")
8
- @api_service = APIService.new(base_url, access_token, default_options)
15
+ @api_service = APIService.new(base_url, access_token, **default_options)
9
16
  end
10
17
 
18
+ # @return [Services::AircraftService]
11
19
  def aircraft
12
20
  @aircraft ||= Services::AircraftService.new(@api_service)
13
21
  end
14
22
 
23
+ # @return [Services::AirlinesService]
15
24
  def airlines
16
25
  @airlines ||= Services::AirlinesService.new(@api_service)
17
26
  end
18
27
 
28
+ # @return [Services::AirportsService]
19
29
  def airports
20
30
  @airports ||= Services::AirportsService.new(@api_service)
21
31
  end
22
32
 
33
+ # @return [Services::OfferPassengersService]
23
34
  def offer_passengers
24
35
  @offer_passengers ||= Services::OfferPassengersService.new(@api_service)
25
36
  end
26
37
 
38
+ # @return [Services::OfferRequestsService]
27
39
  def offer_requests
28
40
  @offer_requests ||= Services::OfferRequestsService.new(@api_service)
29
41
  end
30
42
 
43
+ # @return [Services::OffersService]
31
44
  def offers
32
45
  @offers ||= Services::OffersService.new(@api_service)
33
46
  end
34
47
 
48
+ # @return [Services::OrderCancellationsService]
35
49
  def order_cancellations
36
50
  @order_cancellations ||= Services::OrderCancellationsService.new(@api_service)
37
51
  end
38
52
 
53
+ # @return [Services::OrderChangeOffersService]
39
54
  def order_change_offers
40
55
  @order_change_offers ||= Services::OrderChangeOffersService.new(@api_service)
41
56
  end
42
57
 
58
+ # @return [Services::OrderChangeRequestsService]
43
59
  def order_change_requests
44
60
  @order_change_requests ||= Services::OrderChangeRequestsService.new(@api_service)
45
61
  end
46
62
 
63
+ # @return [Services::OrderChangesService]
47
64
  def order_changes
48
65
  @order_changes ||= Services::OrderChangesService.new(@api_service)
49
66
  end
50
67
 
68
+ # @return [Services::OrdersService]
51
69
  def orders
52
70
  @orders ||= Services::OrdersService.new(@api_service)
53
71
  end
54
72
 
73
+ # @return [Services::PaymentIntentsService]
55
74
  def payment_intents
56
75
  @payment_intents ||= Services::PaymentIntentsService.new(@api_service)
57
76
  end
58
77
 
78
+ # @return [Services::PaymentsService]
59
79
  def payments
60
80
  @payments ||= Services::PaymentsService.new(@api_service)
61
81
  end
62
82
 
83
+ # @return [Services::RefundsService]
63
84
  def refunds
64
85
  @refunds ||= Services::RefundsService.new(@api_service)
65
86
  end
66
87
 
88
+ # @return [Services::SeatMapsService]
67
89
  def seat_maps
68
90
  @seat_maps ||= Services::SeatMapsService.new(@api_service)
69
91
  end
70
92
 
93
+ # @return [Services::WebhooksService]
71
94
  def webhooks
72
95
  @webhooks ||= Services::WebhooksService.new(@api_service)
73
96
  end
@@ -5,6 +5,14 @@ module DuffelAPI
5
5
  class Error < StandardError
6
6
  attr_reader :error
7
7
 
8
+ # Builds an error, which provides access to the raw response. In general,
9
+ # subclasses of this error (e.g. `APIError`) will be raised, apart from
10
+ # for unrecognised errors returned by the Duffel API or errors that don't
11
+ # look like standardised Duffel API errors.
12
+ #
13
+ # @param error [Hash] the parsed error data from the API
14
+ # @param response [APIResponse, nil]
15
+ # @return [Error]
8
16
  def initialize(error, response = nil)
9
17
  raise ArgumentError, "Duffel errors expect a hash" unless error.is_a?(Hash)
10
18
 
@@ -14,38 +22,73 @@ module DuffelAPI
14
22
  super(error)
15
23
  end
16
24
 
25
+ # Returns a URL where documentation about the error can be found. This can be
26
+ # `nil` for errors that don't look like standardised Duffel errors (e.g. errors
27
+ # returned by the load balancer rather than the API itself).
28
+ #
29
+ # @return [String, nil]
17
30
  def documentation_url
18
31
  @error["documentation_url"]
19
32
  end
20
33
 
34
+ # Returns the title associated with the error. This can be `nil` for errors that
35
+ # don't look like standardised Duffel errors (e.g. errors returned by the load
36
+ # balancer rather than the API itself).
37
+ #
38
+ # @return [String, nil]
21
39
  def title
22
40
  @error["title"]
23
41
  end
24
42
 
43
+ # Return the message associated with the error
44
+ #
45
+ # @return [String]
25
46
  def message
26
47
  @error["message"]
27
48
  end
28
49
 
50
+ # Returns a string representation of the error, taken from its `#message`
51
+ #
52
+ # @return [String]
29
53
  def to_s
30
54
  @error["message"]
31
55
  end
32
56
 
57
+ # Returns the type of the error. See the Duffel API reference for possible values.
58
+ # This can be `nil` for errors that don't look like standardised Duffel errors
59
+ # (e.g. errors returned by the load balancer rather than the API itself).
60
+ #
61
+ # @return [String, nil]
33
62
  def type
34
63
  @error["type"]
35
64
  end
36
65
 
66
+ # Returns the code of the error. See the Duffel API reference for possible values.
67
+ # This can be `nil` for errors that don't look like standardised Duffel errors
68
+ # (e.g. errors returned by the load balancer rather than the API itself).
69
+ #
70
+ # @return [String, nil]
37
71
  def code
38
72
  @error["code"]
39
73
  end
40
74
 
75
+ # Returns the request ID of the request that generated the error.
76
+ #
77
+ # @return [String]
41
78
  def request_id
42
- @error["request_id"]
79
+ api_response.request_id
43
80
  end
44
81
 
82
+ # Return s the source of the error.
83
+ #
84
+ # @return [Hash, nil]
45
85
  def source
46
86
  @error["source"]
47
87
  end
48
88
 
89
+ # Returns the raw API response where this error originated from
90
+ #
91
+ # @return [APIResponse]
49
92
  def api_response
50
93
  APIResponse.new(@response)
51
94
  end
@@ -1,7 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module DuffelAPI
4
+ # A page of results returned by a "list" action in the API, provides access to the
5
+ # records on the page, and metadata about the page itself.
4
6
  class ListResponse
7
+ # Returns the records contained within the page
8
+ #
9
+ # @return [Array<Resources::BaseResource>] an array of records - for example, for the
10
+ # list action for offers, this would be a list of `Resources::Offer`s
5
11
  attr_reader :records
6
12
 
7
13
  def initialize(options = {})
@@ -12,14 +18,25 @@ module DuffelAPI
12
18
  @records = @unenveloped_body.map { |item| @resource_class.new(item, @response) }
13
19
  end
14
20
 
21
+ # Returns the raw API response received for this listing request
22
+ #
23
+ # @return [APIResponse]
15
24
  def api_response
16
25
  @api_response ||= APIResponse.new(@response)
17
26
  end
18
27
 
28
+ # Returns the cursor representing the previous page of paginated results, if there is
29
+ # a previous page
30
+ #
31
+ # @return [String, nil]
19
32
  def before
20
33
  @response.parsed_body["meta"]["before"]
21
34
  end
22
35
 
36
+ # Returns the cursor representing the next page of paginated results, if there is
37
+ # a next page
38
+ #
39
+ # @return [String, nil]
23
40
  def after
24
41
  @response.parsed_body["meta"]["after"]
25
42
  end
@@ -4,11 +4,14 @@ require "faraday"
4
4
 
5
5
  module DuffelAPI
6
6
  module Middlewares
7
- class RaiseDuffelErrors < Faraday::Response::Middleware
7
+ class RaiseDuffelErrors < Faraday::Middleware
8
8
  UNEXPECTED_ERROR_STATUSES = (501..599).freeze
9
9
  EXPECTED_ERROR_STATUSES = (400..500).freeze
10
10
 
11
11
  # rubocop:disable Metrics/AbcSize
12
+ # Handles a completed (Faraday) request and raises an error, if appropriate
13
+ #
14
+ # @param [Faraday::Env] env
12
15
  def on_complete(env)
13
16
  if !json?(env) || UNEXPECTED_ERROR_STATUSES.include?(env.status)
14
17
  response = Response.new(env.response)
@@ -31,6 +34,11 @@ module DuffelAPI
31
34
 
32
35
  private
33
36
 
37
+ # Picks the correct error class to use for an error returned by the Duffel API
38
+ # based on its type
39
+ #
40
+ # @param type [Atom] the type returned by the API
41
+ # @return [Errors::Error]
34
42
  def error_class_for_type(type)
35
43
  {
36
44
  airline_error: DuffelAPI::Errors::AirlineError,
@@ -43,16 +51,24 @@ module DuffelAPI
43
51
  }.fetch(type.to_sym) || DuffelAPI::Errors::Error
44
52
  end
45
53
 
54
+ # Generates error data - specifically a message - based on the `Faraday::Env` for
55
+ # non-standard Duffel errors
56
+ #
57
+ # @param env [Faraday::Env]
58
+ # @return [Hash]
46
59
  def generate_error_data(env)
47
60
  {
48
61
  "message" => "Something went wrong with this request\n" \
49
62
  "Code: #{env.status}\n" \
50
63
  "Headers: #{env.response_headers}\n" \
51
64
  "Body: #{env.body}",
52
- "code" => env.status,
53
65
  }
54
66
  end
55
67
 
68
+ # Works out if the response is a JSON response based on the `Faraday::Env`
69
+ #
70
+ # @param env [Faraday::Env]
71
+ # @return [Boolean]
56
72
  def json?(env)
57
73
  content_type = env.response_headers["Content-Type"] ||
58
74
  env.response_headers["content-type"] || ""
@@ -1,12 +1,20 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module DuffelAPI
4
+ # An internal class used within the library to paginated automatically thruogh results
5
+ # from list actions that can be spread over multiple pages
4
6
  class Paginator
5
- def initialize(options = {})
6
- @service = options.fetch(:service)
7
- @options = options.fetch(:options)
7
+ # @param service [Services::BaseService] a service which implements `#list`
8
+ # @param options [Hash] the options originally passed to `#all`
9
+ def initialize(service:, options:)
10
+ @service = service
11
+ @options = options
8
12
  end
9
13
 
14
+ # Returns an enumerator that is able to automatically cycle through paginated data
15
+ # returned by the API
16
+ #
17
+ # @return [Enumerator]
10
18
  def enumerator
11
19
  response = @service.list(@options)
12
20