amadeus 0.1.0 → 1.0.0.beta1

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 +5 -5
  2. data/CHANGELOG.md +10 -0
  3. data/LICENSE +15 -0
  4. data/README.md +157 -19
  5. data/amadeus.gemspec +26 -16
  6. data/lib/amadeus.rb +31 -4
  7. data/lib/amadeus/client.rb +120 -0
  8. data/lib/amadeus/client/access_token.rb +61 -0
  9. data/lib/amadeus/client/decorator.rb +27 -0
  10. data/lib/amadeus/client/errors.rb +76 -0
  11. data/lib/amadeus/client/http.rb +137 -0
  12. data/lib/amadeus/client/location.rb +13 -0
  13. data/lib/amadeus/client/pagination.rb +103 -0
  14. data/lib/amadeus/client/request.rb +145 -0
  15. data/lib/amadeus/client/request/hash.rb +32 -0
  16. data/lib/amadeus/client/response.rb +62 -0
  17. data/lib/amadeus/client/response/parser.rb +72 -0
  18. data/lib/amadeus/client/validator.rb +62 -0
  19. data/lib/amadeus/namespaces/core.rb +51 -0
  20. data/lib/amadeus/namespaces/reference_data.rb +41 -0
  21. data/lib/amadeus/namespaces/reference_data/location.rb +42 -0
  22. data/lib/amadeus/namespaces/reference_data/locations.rb +45 -0
  23. data/lib/amadeus/namespaces/reference_data/locations/airports.rb +38 -0
  24. data/lib/amadeus/namespaces/reference_data/urls.rb +27 -0
  25. data/lib/amadeus/namespaces/reference_data/urls/checkin_links.rb +33 -0
  26. data/lib/amadeus/namespaces/shopping.rb +66 -0
  27. data/lib/amadeus/namespaces/shopping/flight_dates.rb +33 -0
  28. data/lib/amadeus/namespaces/shopping/flight_destinations.rb +30 -0
  29. data/lib/amadeus/namespaces/shopping/flight_offers.rb +36 -0
  30. data/lib/amadeus/namespaces/shopping/hotel.rb +56 -0
  31. data/lib/amadeus/namespaces/shopping/hotel/hotel_offers.rb +44 -0
  32. data/lib/amadeus/namespaces/shopping/hotel/offer.rb +58 -0
  33. data/lib/amadeus/namespaces/shopping/hotel_offers.rb +37 -0
  34. data/lib/amadeus/namespaces/travel.rb +26 -0
  35. data/lib/amadeus/namespaces/travel/analytics.rb +37 -0
  36. data/lib/amadeus/namespaces/travel/analytics/air_traffics.rb +37 -0
  37. data/lib/amadeus/namespaces/travel/analytics/fare_searches.rb +46 -0
  38. data/lib/amadeus/version.rb +4 -1
  39. metadata +161 -23
  40. data/.gitignore +0 -12
  41. data/.rspec +0 -2
  42. data/.travis.yml +0 -5
  43. data/CODE_OF_CONDUCT.md +0 -74
  44. data/Gemfile +0 -4
  45. data/LICENSE.txt +0 -21
  46. data/Rakefile +0 -6
  47. data/bin/console +0 -14
  48. data/bin/setup +0 -8
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'net/https'
4
+
5
+ module Amadeus
6
+ class Client
7
+ # A helper library to create and maintain the
8
+ # OAuth2 AccessTokens between requests
9
+ # @!visibility private
10
+ class AccessToken < Amadeus::Client::Decorator
11
+ # The number of seconds before the token expires, when
12
+ # we will already try to refresh it
13
+ TOKEN_BUFFER = 10
14
+
15
+ # The bearer token that can be used directly in API request headers
16
+ #
17
+ # @example A full token
18
+ # Bearer 12345678
19
+ def bearer_token
20
+ "Bearer #{token}"
21
+ end
22
+
23
+ private
24
+
25
+ # Returns the access token if it is still valid,
26
+ # or refreshes it if it is not (or about to expire)
27
+ def token
28
+ return @access_token if @access_token && !needs_refresh?
29
+ update_access_token
30
+ @access_token
31
+ end
32
+
33
+ # Checks if the token needs a refesh by checking if the token
34
+ # is nil or (about to) expire(d)
35
+ def needs_refresh?
36
+ @access_token.nil? ||
37
+ (Time.now + TOKEN_BUFFER) > @expires_at
38
+ end
39
+
40
+ # Fetches a new access token and stores it and its expiry date
41
+ def update_access_token
42
+ response = fetch_access_token
43
+ store_access_token(response.result)
44
+ end
45
+
46
+ # Fetches a new access token
47
+ def fetch_access_token
48
+ client.unauthenticated_request(:POST, '/v1/security/oauth2/token',
49
+ grant_type: 'client_credentials',
50
+ client_id: client.client_id,
51
+ client_secret: client.client_secret)
52
+ end
53
+
54
+ # Store an access token and calculates the expiry date
55
+ def store_access_token(data)
56
+ @access_token = data['access_token']
57
+ @expires_at = Time.now + data['expires_in']
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Amadeus
4
+ class Client
5
+ # A superclass that allows any namespaced superclass
6
+ # to easily wrap the client object
7
+ #
8
+ # @abstract
9
+ # @!visibility private
10
+ class Decorator
11
+ # The API client
12
+ # @return [Amadeus::Client]
13
+ attr_reader :client
14
+
15
+ # Initialize the namespaced client with an
16
+ # {Amadeus::Client} instance
17
+ #
18
+ # @param [Amadeus::Client] client
19
+ def initialize(client)
20
+ if client.nil?
21
+ raise(ArgumentError, 'Missing required parameter: Amadeus::Client')
22
+ end
23
+ @client = client
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Amadeus
4
+ # A custom generic Amadeus error.
5
+ #
6
+ # @abstract
7
+ class ResponseError < RuntimeError
8
+ # The response object containing the raw HTTP response and the request
9
+ # used to make the API call.
10
+ #
11
+ # @return [Amadeus::Response]
12
+ attr_reader :response
13
+
14
+ # A unique code for this type of error. Options include
15
+ # +NetworkError+, +ParserError+, +ServerError+,
16
+ # +AuthenticationError+, +NotFoundError+ and +UnknownError+.
17
+ #
18
+ # @return [String]
19
+ attr_reader :code
20
+
21
+ # The content of the response that describes the error
22
+ #
23
+ # @return [Hash]
24
+ attr_reader :description
25
+
26
+ # Initializes an error by storing the {Amadeus::Response} object
27
+ # that raised this error. The continues to determien the custom
28
+ # error message
29
+ #
30
+ # @param [Amadeus::Response] response
31
+ # @!visibility private
32
+ def initialize(response)
33
+ @response = response
34
+ @description = determine_description
35
+ super(@description)
36
+ @code = determine_code
37
+ end
38
+
39
+ # PROTECTED
40
+
41
+ def log(client)
42
+ # :nocov:
43
+ return unless client.log_level == 'warn'
44
+ client.logger.warn("Amadeus #{@code}") do
45
+ JSON.pretty_generate(@description) if @description
46
+ end
47
+ # :nocov:
48
+ end
49
+
50
+ private
51
+
52
+ def determine_description
53
+ return nil unless response && response.parsed
54
+ result = response.result
55
+ return result['errors'] if result['errors']
56
+ return result if result['error_description']
57
+ end
58
+
59
+ def determine_code
60
+ self.class.to_s.split('::').last
61
+ end
62
+ end
63
+
64
+ # This error occurs when there is some kind of error in the network
65
+ class NetworkError < Amadeus::ResponseError; end
66
+ # This error occurs when the response type was JSOn but could not be parsed
67
+ class ParserError < Amadeus::ResponseError; end
68
+ # This error occurs when there is an error on the server
69
+ class ServerError < Amadeus::ResponseError; end
70
+ # This error occurs when the client did not provide the right parameters
71
+ class ClientError < Amadeus::ResponseError; end
72
+ # This error occurs when the client did not provide the right credentials
73
+ class AuthenticationError < Amadeus::ResponseError; end
74
+ # This error occurs when the path could not be found
75
+ class NotFoundError < Amadeus::ResponseError; end
76
+ end
@@ -0,0 +1,137 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'net/https'
4
+
5
+ require 'amadeus/client/request'
6
+
7
+ module Amadeus
8
+ class Client
9
+ # A helper module for making generic API calls. It is used by
10
+ # every namespaced API method.
11
+ module HTTP
12
+ # A helper module for making generic GET requests calls. It is used by
13
+ # every namespaced API GET method.
14
+ #
15
+ # amadeus.reference_data.urls.checkin_links.get(airline: '1X')
16
+ #
17
+ # It can be used to make any generic API call that is automatically
18
+ # authenticated using your API credentials:
19
+ #
20
+ # amadeus.get('/v2/reference-data/urls/checkin-links', {
21
+ # airline: '1X'
22
+ # })
23
+ # @param [String] path the full path for the API call
24
+ # @param [Hash] params the optional GET params to pass to the API
25
+ #
26
+ def get(path, params = {})
27
+ request(:GET, path, params)
28
+ end
29
+
30
+ # A helper module for making generic POST requests calls. It is used by
31
+ # every namespaced API POST method.
32
+ #
33
+ # amadeus.foo.bar.post(some: 'data')
34
+ #
35
+ # It can be used to make any generic API call that is automatically
36
+ # authenticated using your API credentials:
37
+ #
38
+ # amadeus.post('/v2/foo/bar', { some: 'data' })
39
+ #
40
+ # To make an unauthenticated API call, make sure to pass in an explicit
41
+ # nil for the access token:
42
+ #
43
+ # amadeus.post('/v2/foo/bar', { some: 'data' }, nil)
44
+ #
45
+ # @param [String] path the full path for the API call
46
+ # @param [Hash] params the optional POST params to pass to the API
47
+ #
48
+ def post(path, params = {})
49
+ request(:POST, path, params)
50
+ end
51
+
52
+ # A more generic helper for making authenticated API calls
53
+ #
54
+ # @param [Symbol] verb the HTTP verb to use
55
+ # @param [String] path the full path for the API call
56
+ # @param [Hash] params the optional POST params to pass to the API
57
+ # @!visibility private
58
+ #
59
+ def request(verb, path, params = {})
60
+ unauthenticated_request(verb, path, params, access_token.bearer_token)
61
+ end
62
+
63
+ # Builds the URI, the request object, and makes the actual API calls.
64
+ #
65
+ # Used by the AccessToken to fetch a new Bearer Token
66
+ #
67
+ # Passes the response to a Amadeus::Response object for further
68
+ # parsing.
69
+ #
70
+ # @param [Symbol] verb the HTTP verb to use
71
+ # @param [String] path the full path for the API call
72
+ # @param [Hash] params the optional POST params to pass to the API
73
+ # @param [String] bearer_token the optional OAuth2 bearer token
74
+ #
75
+ # @!visibility private
76
+ def unauthenticated_request(verb, path, params, bearer_token = nil)
77
+ request = build_request(verb, path, params, bearer_token)
78
+ log(request)
79
+ execute(request)
80
+ end
81
+
82
+ private
83
+
84
+ # Builds a HTTP request object that contains all the information about
85
+ # this request
86
+ def build_request(verb, path, params, bearer_token)
87
+ Amadeus::Request.new(
88
+ host: @host, verb: verb, path: path, params: params,
89
+ bearer_token: bearer_token, client_version: Amadeus::VERSION,
90
+ language_version: RUBY_VERSION, app_id: @custom_app_id,
91
+ app_version: @custom_app_version, ssl: @ssl, port: @port
92
+ )
93
+ end
94
+
95
+ # Executes the request and wraps it in a Response
96
+ def execute(request)
97
+ http_response = fetch(request)
98
+ response = Amadeus::Response.new(http_response, request).parse(self)
99
+ log(response)
100
+ response.detect_error(self)
101
+ response
102
+ end
103
+
104
+ # Actually make the HTTP call, making sure to catch it in case of an error
105
+ def fetch(request)
106
+ @http.start(
107
+ request.host, request.port, use_ssl: request.ssl
108
+ ) do |http|
109
+ http.request(request.http_request)
110
+ end
111
+ rescue StandardError
112
+ error = Amadeus::NetworkError.new(nil)
113
+ error.log(self)
114
+ raise error
115
+ end
116
+
117
+ # A memoized AccessToken object, so we don't keep reauthenticating
118
+ def access_token
119
+ @access_token ||= AccessToken.new(self)
120
+ end
121
+
122
+ # Log any object
123
+ def log(object)
124
+ # :nocov:
125
+ return unless @log_level == 'debug'
126
+ logger.debug(object.class.name.to_s) do
127
+ JSON.pretty_generate(
128
+ ::Hash[object.instance_variables.map do |ivar|
129
+ [ivar, object.instance_variable_get(ivar)]
130
+ end]
131
+ )
132
+ end
133
+ # :nocov:
134
+ end
135
+ end
136
+ end
137
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Amadeus
4
+ # A list of location types, as used in searching for locations
5
+ module Location
6
+ # Airport
7
+ AIRPORT = 'AIRPORT'.freeze
8
+ # City
9
+ CITY = 'CITY'.freeze
10
+ # Any
11
+ ANY = [AIRPORT, CITY].join(',')
12
+ end
13
+ end
@@ -0,0 +1,103 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Amadeus
4
+ class Client
5
+ # Helper methods to for the {Amadeus::Response} object to help it
6
+ # paginate to next, previous, and first/last pages
7
+ module Pagination
8
+ # Finds the next page, if there is any, and otherwise
9
+ # returns nil
10
+ #
11
+ # @param [Amadeus::Response] response the response to fetch the next
12
+ # page for
13
+ # @return [Amadeus::Response]
14
+ #
15
+ # @example Fetch the next page
16
+ # page1 = amadeus.reference_data.locations.get(
17
+ # keyword: 'lon',
18
+ # subType: Amadeus::Location::ANY
19
+ # )
20
+ # page2 = amadeus.next(page1)
21
+ #
22
+ def next(response)
23
+ page(:next, response)
24
+ end
25
+
26
+ # Finds the previous page, if there is any, and otherwise
27
+ # returns nil
28
+ #
29
+ # @param [Amadeus::Response] response the response to fetch the previous
30
+ # page for
31
+ # @return [Amadeus::Response]
32
+ #
33
+ # @example Fetch the previous page
34
+ # page2 = amadeus.reference_data.locations.get(
35
+ # keyword: 'lon',
36
+ # subType: Amadeus::Location::ANY,
37
+ # page: { offset: 2 }
38
+ # )
39
+ # page1 = amadeus.previous(page1)
40
+ #
41
+ def previous(response)
42
+ page(:previous, response)
43
+ end
44
+
45
+ # Finds the last page, if there is any, and otherwise
46
+ # returns nil
47
+ #
48
+ # @param [Amadeus::Response] response the response to fetch the last page
49
+ # for
50
+ # @return [Amadeus::Response]
51
+ #
52
+ # @example Fetch the last page
53
+ # page1 = amadeus.reference_data.locations.get(
54
+ # keyword: 'lon',
55
+ # subType: Amadeus::Location::ANY
56
+ # )
57
+ # last_page = amadeus.last(page1)
58
+ #
59
+ def last(response)
60
+ page(:last, response)
61
+ end
62
+
63
+ # Finds the first page, if there is any, and otherwise
64
+ # returns nil
65
+ #
66
+ # @param [Amadeus::Response] response the response to fetch the first page
67
+ # for
68
+ # @return [Amadeus::Response]
69
+ #
70
+ # @example Fetch the first page
71
+ # page10 = amadeus.reference_data.locations.get(
72
+ # keyword: 'lon',
73
+ # subType: Amadeus::Location::ANY,
74
+ # page: { offset: 10 }
75
+ # )
76
+ # page1 = amadeus.first(page1)
77
+ #
78
+ def first(response)
79
+ page(:first, response)
80
+ end
81
+
82
+ private
83
+
84
+ # Determine the page number for the given page name
85
+ def page_number_for(name, response)
86
+ response.result['meta']['links'][name.to_s].split('=').last.to_i
87
+ rescue NoMethodError
88
+ nil
89
+ end
90
+
91
+ # Make a new API call for the same request, but with a new
92
+ # page number
93
+ def page(name, response)
94
+ page_number = page_number_for(name, response)
95
+ return nil unless page_number
96
+ params = response.request.params.clone
97
+ params['page'] ||= {}
98
+ params['page']['offset'] = page_number
99
+ request(response.request.verb, response.request.path, params)
100
+ end
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,145 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'amadeus/client/request/hash'
4
+
5
+ module Amadeus
6
+ # A Request object containing all the compiled information about this request.
7
+ class Request
8
+ include Amadeus::Request::Hash
9
+
10
+ # The host used for this API call
11
+ # @return [String]
12
+ attr_reader :host
13
+ # The port for this API call. Standard set to 443.
14
+ # @return [Number]
15
+ attr_reader :port
16
+ # Wether to use SSL for a call, defaults to true
17
+ # @return [Boolean]
18
+ attr_reader :ssl
19
+ # The scheme used to make the API call
20
+ # @return [String]
21
+ attr_reader :scheme
22
+ # The GET/POST params for the API call
23
+ # @return [Hash]
24
+ attr_reader :params
25
+ # The path of the API to be called
26
+ # @return [String]
27
+ attr_reader :path
28
+ # The verb used to make an API call (:GET or :POST)
29
+ # @return [Symbol]
30
+ attr_reader :verb
31
+ # The bearer token (if any) that is used for authentication
32
+ # @return [String]
33
+ attr_reader :bearer_token
34
+ # The headers used for the API call
35
+ # @return [Hash]
36
+ attr_reader :headers
37
+ # The library version used for this request
38
+ # @return [String]
39
+ attr_reader :client_version
40
+ # The Ruby language version used for this request
41
+ # @return [String]
42
+ attr_reader :language_version
43
+ # The custom app ID passed in for this request
44
+ # @return [String]
45
+ attr_reader :app_id
46
+ # The custom app version used for this request
47
+ # @return [String]
48
+ attr_reader :app_version
49
+
50
+ # A Request object containing all the compiled information about
51
+ # this request.
52
+ #
53
+ # @return [Amadeus::Request]
54
+ # @!visibility private
55
+ def initialize(options)
56
+ initialize_options(options)
57
+ initialize_headers
58
+ http_request
59
+ end
60
+
61
+ # Builds the request object
62
+ def http_request
63
+ @http_request ||= begin
64
+ request = request_for_verb
65
+ add_post_data(request)
66
+ add_bearer_token(request)
67
+ add_headers(request)
68
+ request
69
+ end
70
+ end
71
+
72
+ private
73
+
74
+ def initialize_options(options)
75
+ initialize_basic_call(options)
76
+ initialize_extras(options)
77
+ end
78
+
79
+ def initialize_basic_call(options)
80
+ @host = options[:host]
81
+ @port = options[:port]
82
+ @ssl = options[:ssl]
83
+ @scheme = @ssl ? 'https' : 'http'
84
+ @verb = options[:verb]
85
+ @path = options[:path]
86
+ @params = options[:params]
87
+ @bearer_token = options[:bearer_token]
88
+ end
89
+
90
+ def initialize_extras(options)
91
+ @client_version = options[:client_version]
92
+ @language_version = options[:language_version]
93
+ @app_id = options[:app_id]
94
+ @app_version = options[:app_version]
95
+ end
96
+
97
+ def initialize_headers
98
+ @headers = {
99
+ 'User-Agent' => build_user_agent,
100
+ 'Accept' => 'application/json'
101
+ }
102
+ end
103
+
104
+ def request_for_verb
105
+ method = @verb == :GET ? Net::HTTP::Get : Net::HTTP::Post
106
+ method.new(uri)
107
+ end
108
+
109
+ def add_post_data(request)
110
+ return unless @verb == :POST
111
+ @headers['Content-Type'] = 'application/x-www-form-urlencoded'
112
+ request.form_data = @params
113
+ end
114
+
115
+ def add_bearer_token(_request)
116
+ return if @bearer_token.nil?
117
+ @headers['Authorization'] = @bearer_token
118
+ end
119
+
120
+ def add_headers(request)
121
+ @headers.each do |key, value|
122
+ request[key] = value
123
+ end
124
+ end
125
+
126
+ def build_user_agent
127
+ user_agent = "amadeus-ruby/#{@client_version} ruby/#{@language_version}"
128
+ return user_agent unless @app_id
129
+ user_agent + " #{@app_id}/#{@app_version}"
130
+ end
131
+
132
+ def uri
133
+ url = "#{@scheme}://#{@host}#{@path}"
134
+ url += ":#{@port}" unless port_matches_scheme
135
+ uri = URI(url)
136
+ params = flatten_keys(@params)
137
+ uri.query = URI.encode_www_form(params) if @verb == :GET
138
+ uri
139
+ end
140
+
141
+ def port_matches_scheme
142
+ (@ssl && @port == 443) || (!@ssl && @port == 80)
143
+ end
144
+ end
145
+ end