duffel_api 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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