duffel_api 0.0.1.pre.dev → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.circleci/config.yml +82 -0
- data/.github/dependabot.yml +6 -0
- data/.gitignore +9 -0
- data/.rubocop.yml +9 -9
- data/CHANGELOG.md +1 -3
- data/CODE_OF_CONDUCT.md +1 -1
- data/Gemfile +10 -2
- data/LICENSE.txt +1 -1
- data/README.md +190 -33
- data/Rakefile +1 -3
- data/bin/console +7 -0
- data/bin/setup +2 -0
- data/duffel_api.gemspec +40 -0
- data/examples/book_and_change.rb +106 -0
- data/examples/book_with_seat.rb +91 -0
- data/examples/exploring_data.rb +43 -0
- data/examples/hold_and_pay_later.rb +82 -0
- data/examples/search_and_book.rb +91 -0
- data/lib/duffel_api/api_response.rb +17 -0
- data/lib/duffel_api/api_service.rb +37 -0
- data/lib/duffel_api/client.rb +87 -0
- data/lib/duffel_api/errors/airline_error.rb +8 -0
- data/lib/duffel_api/errors/api_error.rb +8 -0
- data/lib/duffel_api/errors/authentication_error.rb +8 -0
- data/lib/duffel_api/errors/error.rb +54 -0
- data/lib/duffel_api/errors/invalid_request_error.rb +8 -0
- data/lib/duffel_api/errors/invalid_state_error.rb +8 -0
- data/lib/duffel_api/errors/rate_limit_error.rb +8 -0
- data/lib/duffel_api/errors/validation_error.rb +8 -0
- data/lib/duffel_api/list_response.rb +27 -0
- data/lib/duffel_api/middlewares/raise_duffel_errors.rb +67 -0
- data/lib/duffel_api/paginator.rb +27 -0
- data/lib/duffel_api/request.rb +64 -0
- data/lib/duffel_api/resources/aircraft.rb +26 -0
- data/lib/duffel_api/resources/airline.rb +26 -0
- data/lib/duffel_api/resources/airport.rb +40 -0
- data/lib/duffel_api/resources/base_resource.rb +16 -0
- data/lib/duffel_api/resources/offer.rb +60 -0
- data/lib/duffel_api/resources/offer_passenger.rb +28 -0
- data/lib/duffel_api/resources/offer_request.rb +34 -0
- data/lib/duffel_api/resources/order.rb +58 -0
- data/lib/duffel_api/resources/order_cancellation.rb +34 -0
- data/lib/duffel_api/resources/order_change.rb +46 -0
- data/lib/duffel_api/resources/order_change_offer.rb +46 -0
- data/lib/duffel_api/resources/order_change_request.rb +30 -0
- data/lib/duffel_api/resources/payment.rb +26 -0
- data/lib/duffel_api/resources/payment_intent.rb +52 -0
- data/lib/duffel_api/resources/refund.rb +42 -0
- data/lib/duffel_api/resources/seat_map.rb +24 -0
- data/lib/duffel_api/resources/webhook.rb +32 -0
- data/lib/duffel_api/response.rb +45 -0
- data/lib/duffel_api/services/aircraft_service.rb +36 -0
- data/lib/duffel_api/services/airlines_service.rb +36 -0
- data/lib/duffel_api/services/airports_service.rb +36 -0
- data/lib/duffel_api/services/base_service.rb +29 -0
- data/lib/duffel_api/services/offer_passengers_service.rb +30 -0
- data/lib/duffel_api/services/offer_requests_service.rb +67 -0
- data/lib/duffel_api/services/offers_service.rb +36 -0
- data/lib/duffel_api/services/order_cancellations_service.rb +75 -0
- data/lib/duffel_api/services/order_change_offers_service.rb +36 -0
- data/lib/duffel_api/services/order_change_requests_service.rb +37 -0
- data/lib/duffel_api/services/order_changes_service.rb +56 -0
- data/lib/duffel_api/services/orders_service.rb +74 -0
- data/lib/duffel_api/services/payment_intents_service.rb +56 -0
- data/lib/duffel_api/services/payments_service.rb +26 -0
- data/lib/duffel_api/services/refunds_service.rb +36 -0
- data/lib/duffel_api/services/seat_maps_service.rb +19 -0
- data/lib/duffel_api/services/webhooks_service.rb +83 -0
- data/lib/duffel_api/version.rb +1 -1
- data/lib/duffel_api.rb +51 -4
- metadata +90 -12
@@ -0,0 +1,91 @@
|
|
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
|
+
priced_offer = client.offers.get(selected_offer.id,
|
40
|
+
params: { return_available_services: true })
|
41
|
+
|
42
|
+
puts "The final price for offer #{priced_offer.id} is #{priced_offer.total_amount} " \
|
43
|
+
"#{priced_offer.total_currency}"
|
44
|
+
|
45
|
+
seat_maps = client.seat_maps.list(params: { offer_id: priced_offer.id })
|
46
|
+
|
47
|
+
available_seat = seat_maps.records.first.cabins.
|
48
|
+
first["rows"].
|
49
|
+
flat_map { |row| row["sections"] }.
|
50
|
+
flat_map { |section| section["elements"] }.
|
51
|
+
find do |element|
|
52
|
+
element["type"] == "seat" && element["available_services"].any?
|
53
|
+
end
|
54
|
+
|
55
|
+
available_seat_service = available_seat["available_services"].first
|
56
|
+
|
57
|
+
puts "Adding seat #{available_seat['designator']} costing " \
|
58
|
+
"#{available_seat_service['total_amount']} " \
|
59
|
+
"#{available_seat_service['total_currency']}"
|
60
|
+
|
61
|
+
total_amount = priced_offer.total_amount.to_f +
|
62
|
+
available_seat_service["total_amount"].to_f
|
63
|
+
|
64
|
+
order = client.orders.create(params: {
|
65
|
+
selected_offers: [priced_offer.id],
|
66
|
+
services: [{
|
67
|
+
id: available_seat_service["id"],
|
68
|
+
quantity: 1,
|
69
|
+
}],
|
70
|
+
payments: [
|
71
|
+
{
|
72
|
+
type: "balance",
|
73
|
+
amount: total_amount,
|
74
|
+
currency: priced_offer.total_currency,
|
75
|
+
},
|
76
|
+
],
|
77
|
+
passengers: [
|
78
|
+
{
|
79
|
+
id: priced_offer.passengers.first["id"],
|
80
|
+
title: "mr",
|
81
|
+
gender: "m",
|
82
|
+
given_name: "Tim",
|
83
|
+
family_name: "Rogers",
|
84
|
+
born_on: "1993-04-01",
|
85
|
+
phone_number: "+441290211999",
|
86
|
+
email: "tim@duffel.com",
|
87
|
+
},
|
88
|
+
],
|
89
|
+
})
|
90
|
+
|
91
|
+
puts "Created order #{order.id} with booking reference #{order.booking_reference}"
|
@@ -0,0 +1,43 @@
|
|
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
|
+
puts "Loading airports..."
|
10
|
+
|
11
|
+
airports = client.airports.all
|
12
|
+
|
13
|
+
puts "Got #{airports.count} airports"
|
14
|
+
|
15
|
+
puts "Found airport #{airports.first.name} (#{airports.first.iata_code})"
|
16
|
+
|
17
|
+
airport = client.airports.get(airports.first.id)
|
18
|
+
|
19
|
+
puts "Airport is located at #{airport.latitude}, #{airport.longitude}"
|
20
|
+
|
21
|
+
puts "Loading aircraft..."
|
22
|
+
|
23
|
+
aircraft = client.aircraft.all
|
24
|
+
|
25
|
+
puts "Got #{aircraft.count} aircraft"
|
26
|
+
|
27
|
+
puts "Found aircraft #{aircraft.first.name}"
|
28
|
+
|
29
|
+
single_aircraft = client.aircraft.get(aircraft.first.id)
|
30
|
+
|
31
|
+
puts "Aircraft's IATA code is #{single_aircraft.iata_code}"
|
32
|
+
|
33
|
+
puts "Loading airlines..."
|
34
|
+
|
35
|
+
airlines = client.airlines.all
|
36
|
+
|
37
|
+
puts "Got #{airlines.count} airlines"
|
38
|
+
|
39
|
+
puts "Found airline #{airlines.first.name}"
|
40
|
+
|
41
|
+
airline = client.airlines.get(airlines.first.id)
|
42
|
+
|
43
|
+
puts "Airline's IATA code is #{airline.iata_code}"
|
@@ -0,0 +1,82 @@
|
|
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: "LHR",
|
21
|
+
destination: "STN",
|
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
|
+
priced_offer = client.offers.get(selected_offer.id,
|
40
|
+
params: { return_available_services: true })
|
41
|
+
|
42
|
+
puts "The final price for offer #{priced_offer.id} is #{priced_offer.total_amount} " \
|
43
|
+
"#{priced_offer.total_currency}"
|
44
|
+
|
45
|
+
order = client.orders.create(params: {
|
46
|
+
type: "hold",
|
47
|
+
selected_offers: [priced_offer.id],
|
48
|
+
passengers: [
|
49
|
+
{
|
50
|
+
id: priced_offer.passengers.first["id"],
|
51
|
+
title: "mr",
|
52
|
+
gender: "m",
|
53
|
+
given_name: "Tim",
|
54
|
+
family_name: "Rogers",
|
55
|
+
born_on: "1993-04-01",
|
56
|
+
phone_number: "+441290211999",
|
57
|
+
email: "tim@duffel.com",
|
58
|
+
},
|
59
|
+
],
|
60
|
+
})
|
61
|
+
|
62
|
+
puts "Created hold order #{order.id} with booking reference #{order.booking_reference}"
|
63
|
+
|
64
|
+
updated_order = client.orders.get(order.id)
|
65
|
+
|
66
|
+
puts "Retrieved order and up-to-date price is #{updated_order.total_amount} " \
|
67
|
+
"#{updated_order.total_currency}"
|
68
|
+
|
69
|
+
payment = client.payments.create(params: {
|
70
|
+
order_id: order.id,
|
71
|
+
payment: {
|
72
|
+
type: "balance",
|
73
|
+
amount: updated_order.total_amount,
|
74
|
+
currency: updated_order.total_currency,
|
75
|
+
},
|
76
|
+
})
|
77
|
+
|
78
|
+
puts "Paid for order #{order.id} with payment #{payment.id}"
|
79
|
+
|
80
|
+
paid_order = client.orders.get(order.id)
|
81
|
+
|
82
|
+
puts "After payment, order has #{paid_order.documents.length} documents"
|
@@ -0,0 +1,91 @@
|
|
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
|
+
# resullts.
|
20
|
+
origin: "LHR",
|
21
|
+
destination: "STN",
|
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
|
+
priced_offer = client.offers.get(selected_offer.id,
|
40
|
+
params: { return_available_services: true })
|
41
|
+
|
42
|
+
puts "The final price for offer #{priced_offer.id} is #{priced_offer.total_amount} " \
|
43
|
+
"#{priced_offer.total_currency}"
|
44
|
+
|
45
|
+
available_service = priced_offer.available_services.first
|
46
|
+
|
47
|
+
puts "Adding an extra bag with service #{available_service['id']}, " \
|
48
|
+
"costing #{available_service['total_amount']} #{available_service['total_currency']}"
|
49
|
+
|
50
|
+
total_amount = priced_offer.total_amount.to_f + available_service["total_amount"].to_f
|
51
|
+
|
52
|
+
order = client.orders.create(params: {
|
53
|
+
selected_offers: [priced_offer.id],
|
54
|
+
services: [{
|
55
|
+
id: available_service["id"],
|
56
|
+
quantity: 1,
|
57
|
+
}],
|
58
|
+
payments: [
|
59
|
+
{
|
60
|
+
type: "balance",
|
61
|
+
amount: total_amount,
|
62
|
+
currency: priced_offer.total_currency,
|
63
|
+
},
|
64
|
+
],
|
65
|
+
passengers: [
|
66
|
+
{
|
67
|
+
id: priced_offer.passengers.first["id"],
|
68
|
+
title: "mr",
|
69
|
+
gender: "m",
|
70
|
+
given_name: "Tim",
|
71
|
+
family_name: "Rogers",
|
72
|
+
born_on: "1993-04-01",
|
73
|
+
phone_number: "+441290211999",
|
74
|
+
email: "tim@duffel.com",
|
75
|
+
},
|
76
|
+
],
|
77
|
+
})
|
78
|
+
|
79
|
+
puts "Created order #{order.id} with booking reference #{order.booking_reference}"
|
80
|
+
|
81
|
+
order_cancellation = client.order_cancellations.create(params: {
|
82
|
+
order_id: order.id,
|
83
|
+
})
|
84
|
+
|
85
|
+
puts "Requested refund quote for order #{order.id} - " \
|
86
|
+
"#{order_cancellation.refund_amount} #{order_cancellation.refund_currency} is " \
|
87
|
+
"available"
|
88
|
+
|
89
|
+
client.order_cancellations.confirm(order_cancellation.id)
|
90
|
+
|
91
|
+
puts "Confirmed refund quote for order #{order.id}"
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module DuffelAPI
|
4
|
+
# wraps a faraday response object with some accessors
|
5
|
+
class APIResponse
|
6
|
+
extend Forwardable
|
7
|
+
|
8
|
+
def initialize(response)
|
9
|
+
@response = response
|
10
|
+
end
|
11
|
+
|
12
|
+
def_delegator :@response, :headers
|
13
|
+
def_delegator :@response, :raw_body, :body
|
14
|
+
def_delegator :@response, :status_code
|
15
|
+
def_delegator :@response, :request_id
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "faraday"
|
4
|
+
require "uri"
|
5
|
+
|
6
|
+
module DuffelAPI
|
7
|
+
class APIService
|
8
|
+
def initialize(base_url, access_token, options = {})
|
9
|
+
@base_url = base_url
|
10
|
+
root_url, @path_prefix = unpack_url(base_url)
|
11
|
+
|
12
|
+
@connection = Faraday.new(root_url) do |faraday|
|
13
|
+
faraday.response :raise_duffel_errors
|
14
|
+
|
15
|
+
faraday.adapter(:net_http)
|
16
|
+
end
|
17
|
+
|
18
|
+
@headers = options[:default_headers] || {}
|
19
|
+
@headers["Authorization"] = "Bearer #{access_token}"
|
20
|
+
end
|
21
|
+
|
22
|
+
def make_request(method, path, options = {})
|
23
|
+
raise ArgumentError, "options must be a hash" unless options.is_a?(Hash)
|
24
|
+
|
25
|
+
options[:headers] ||= {}
|
26
|
+
options[:headers] = @headers.merge(options[:headers])
|
27
|
+
Request.new(@connection, method, @path_prefix + path, options).request
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def unpack_url(url)
|
33
|
+
path = URI.parse(url).path
|
34
|
+
[URI.join(url).to_s, path == "/" ? "" : path]
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module DuffelAPI
|
4
|
+
class Client
|
5
|
+
API_VERSION = "beta"
|
6
|
+
|
7
|
+
def initialize(access_token:, base_url: "https://api.duffel.com")
|
8
|
+
@api_service = APIService.new(base_url, access_token, default_options)
|
9
|
+
end
|
10
|
+
|
11
|
+
def aircraft
|
12
|
+
@aircraft ||= Services::AircraftService.new(@api_service)
|
13
|
+
end
|
14
|
+
|
15
|
+
def airlines
|
16
|
+
@airlines ||= Services::AirlinesService.new(@api_service)
|
17
|
+
end
|
18
|
+
|
19
|
+
def airports
|
20
|
+
@airports ||= Services::AirportsService.new(@api_service)
|
21
|
+
end
|
22
|
+
|
23
|
+
def offer_passengers
|
24
|
+
@offer_passengers ||= Services::OfferPassengersService.new(@api_service)
|
25
|
+
end
|
26
|
+
|
27
|
+
def offer_requests
|
28
|
+
@offer_requests ||= Services::OfferRequestsService.new(@api_service)
|
29
|
+
end
|
30
|
+
|
31
|
+
def offers
|
32
|
+
@offers ||= Services::OffersService.new(@api_service)
|
33
|
+
end
|
34
|
+
|
35
|
+
def order_cancellations
|
36
|
+
@order_cancellations ||= Services::OrderCancellationsService.new(@api_service)
|
37
|
+
end
|
38
|
+
|
39
|
+
def order_change_offers
|
40
|
+
@order_change_offers ||= Services::OrderChangeOffersService.new(@api_service)
|
41
|
+
end
|
42
|
+
|
43
|
+
def order_change_requests
|
44
|
+
@order_change_requests ||= Services::OrderChangeRequestsService.new(@api_service)
|
45
|
+
end
|
46
|
+
|
47
|
+
def order_changes
|
48
|
+
@order_changes ||= Services::OrderChangesService.new(@api_service)
|
49
|
+
end
|
50
|
+
|
51
|
+
def orders
|
52
|
+
@orders ||= Services::OrdersService.new(@api_service)
|
53
|
+
end
|
54
|
+
|
55
|
+
def payment_intents
|
56
|
+
@payment_intents ||= Services::PaymentIntentsService.new(@api_service)
|
57
|
+
end
|
58
|
+
|
59
|
+
def payments
|
60
|
+
@payments ||= Services::PaymentsService.new(@api_service)
|
61
|
+
end
|
62
|
+
|
63
|
+
def refunds
|
64
|
+
@refunds ||= Services::RefundsService.new(@api_service)
|
65
|
+
end
|
66
|
+
|
67
|
+
def seat_maps
|
68
|
+
@seat_maps ||= Services::SeatMapsService.new(@api_service)
|
69
|
+
end
|
70
|
+
|
71
|
+
def webhooks
|
72
|
+
@webhooks ||= Services::WebhooksService.new(@api_service)
|
73
|
+
end
|
74
|
+
|
75
|
+
private
|
76
|
+
|
77
|
+
def default_options
|
78
|
+
{
|
79
|
+
default_headers: {
|
80
|
+
"Duffel-Version" => API_VERSION,
|
81
|
+
"User-Agent" => "Duffel/#{API_VERSION} duffel_api_ruby/#{DuffelAPI::VERSION}",
|
82
|
+
"Content-Type" => "application/json",
|
83
|
+
},
|
84
|
+
}
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module DuffelAPI
|
4
|
+
module Errors
|
5
|
+
class Error < StandardError
|
6
|
+
attr_reader :error
|
7
|
+
|
8
|
+
def initialize(error, response = nil)
|
9
|
+
raise ArgumentError, "Duffel errors expect a hash" unless error.is_a?(Hash)
|
10
|
+
|
11
|
+
@error = error
|
12
|
+
@response = response
|
13
|
+
|
14
|
+
super(error)
|
15
|
+
end
|
16
|
+
|
17
|
+
def documentation_url
|
18
|
+
@error["documentation_url"]
|
19
|
+
end
|
20
|
+
|
21
|
+
def title
|
22
|
+
@error["title"]
|
23
|
+
end
|
24
|
+
|
25
|
+
def message
|
26
|
+
@error["message"]
|
27
|
+
end
|
28
|
+
|
29
|
+
def to_s
|
30
|
+
@error["message"]
|
31
|
+
end
|
32
|
+
|
33
|
+
def type
|
34
|
+
@error["type"]
|
35
|
+
end
|
36
|
+
|
37
|
+
def code
|
38
|
+
@error["code"]
|
39
|
+
end
|
40
|
+
|
41
|
+
def request_id
|
42
|
+
@error["request_id"]
|
43
|
+
end
|
44
|
+
|
45
|
+
def source
|
46
|
+
@error["source"]
|
47
|
+
end
|
48
|
+
|
49
|
+
def api_response
|
50
|
+
APIResponse.new(@response)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module DuffelAPI
|
4
|
+
class ListResponse
|
5
|
+
attr_reader :records
|
6
|
+
|
7
|
+
def initialize(options = {})
|
8
|
+
@response = options.fetch(:response)
|
9
|
+
@resource_class = options.fetch(:resource_class)
|
10
|
+
@unenveloped_body = options.fetch(:unenveloped_body)
|
11
|
+
|
12
|
+
@records = @unenveloped_body.map { |item| @resource_class.new(item, @response) }
|
13
|
+
end
|
14
|
+
|
15
|
+
def api_response
|
16
|
+
@api_response ||= APIResponse.new(@response)
|
17
|
+
end
|
18
|
+
|
19
|
+
def before
|
20
|
+
@response.parsed_body["meta"]["before"]
|
21
|
+
end
|
22
|
+
|
23
|
+
def after
|
24
|
+
@response.parsed_body["meta"]["after"]
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "faraday"
|
4
|
+
|
5
|
+
module DuffelAPI
|
6
|
+
module Middlewares
|
7
|
+
class RaiseDuffelErrors < Faraday::Response::Middleware
|
8
|
+
UNEXPECTED_ERROR_STATUSES = (501..599).freeze
|
9
|
+
EXPECTED_ERROR_STATUSES = (400..500).freeze
|
10
|
+
|
11
|
+
# rubocop:disable Metrics/AbcSize
|
12
|
+
def on_complete(env)
|
13
|
+
if !json?(env) || UNEXPECTED_ERROR_STATUSES.include?(env.status)
|
14
|
+
response = Response.new(env.response)
|
15
|
+
raise DuffelAPI::Errors::Error.new(generate_error_data(env), response)
|
16
|
+
end
|
17
|
+
|
18
|
+
if EXPECTED_ERROR_STATUSES.include?(env.status)
|
19
|
+
json_body ||= JSON.parse(env.body) unless env.body.empty?
|
20
|
+
error = json_body["errors"].first
|
21
|
+
error_type = error["type"]
|
22
|
+
|
23
|
+
error_class = error_class_for_type(error_type)
|
24
|
+
|
25
|
+
response = Response.new(env.response)
|
26
|
+
|
27
|
+
raise error_class.new(error, response)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
# rubocop:enable Metrics/AbcSize
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def error_class_for_type(type)
|
35
|
+
{
|
36
|
+
airline_error: DuffelAPI::Errors::AirlineError,
|
37
|
+
api_error: DuffelAPI::Errors::APIError,
|
38
|
+
authentication_error: DuffelAPI::Errors::AuthenticationError,
|
39
|
+
invalid_request_error: DuffelAPI::Errors::InvalidRequestError,
|
40
|
+
invalid_state_error: DuffelAPI::Errors::InvalidStateError,
|
41
|
+
rate_limit_error: DuffelAPI::Errors::RateLimitError,
|
42
|
+
validation_error: DuffelAPI::Errors::ValidationError,
|
43
|
+
}.fetch(type.to_sym) || DuffelAPI::Errors::Error
|
44
|
+
end
|
45
|
+
|
46
|
+
def generate_error_data(env)
|
47
|
+
{
|
48
|
+
"message" => "Something went wrong with this request\n" \
|
49
|
+
"Code: #{env.status}\n" \
|
50
|
+
"Headers: #{env.response_headers}\n" \
|
51
|
+
"Body: #{env.body}",
|
52
|
+
"code" => env.status,
|
53
|
+
}
|
54
|
+
end
|
55
|
+
|
56
|
+
def json?(env)
|
57
|
+
content_type = env.response_headers["Content-Type"] ||
|
58
|
+
env.response_headers["content-type"] || ""
|
59
|
+
|
60
|
+
content_type.include?("application/json")
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
Faraday::Response.register_middleware raise_duffel_errors: DuffelAPI::
|
67
|
+
Middlewares::RaiseDuffelErrors
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module DuffelAPI
|
4
|
+
class Paginator
|
5
|
+
def initialize(options = {})
|
6
|
+
@service = options.fetch(:service)
|
7
|
+
@options = options.fetch(:options)
|
8
|
+
end
|
9
|
+
|
10
|
+
def enumerator
|
11
|
+
response = @service.list(@options)
|
12
|
+
|
13
|
+
Enumerator.new do |yielder|
|
14
|
+
loop do
|
15
|
+
response.records.each { |item| yielder << item }
|
16
|
+
|
17
|
+
after_cursor = response.after
|
18
|
+
break if after_cursor.nil?
|
19
|
+
|
20
|
+
@options[:params] ||= {}
|
21
|
+
@options[:params] = @options[:params].merge(after: after_cursor)
|
22
|
+
response = @service.list(@options)
|
23
|
+
end
|
24
|
+
end.lazy
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|