tlaw 0.0.2 → 0.1.0.pre

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 (59) hide show
  1. checksums.yaml +4 -4
  2. data/.ruby-version +1 -0
  3. data/CHANGELOG.md +18 -2
  4. data/README.md +10 -7
  5. data/examples/demo_base.rb +2 -2
  6. data/examples/experimental/README.md +3 -0
  7. data/examples/experimental/afterthedeadline.rb +22 -0
  8. data/examples/experimental/airvisual.rb +14 -0
  9. data/examples/experimental/apixu.rb +32 -0
  10. data/examples/experimental/bing_maps.rb +18 -0
  11. data/examples/experimental/currencylayer.rb +25 -0
  12. data/examples/experimental/earthquake.rb +29 -0
  13. data/examples/experimental/freegeoip.rb +16 -0
  14. data/examples/experimental/geonames.rb +98 -0
  15. data/examples/experimental/isfdb.rb +17 -0
  16. data/examples/experimental/musicbrainz.rb +27 -0
  17. data/examples/experimental/nominatim.rb +52 -0
  18. data/examples/experimental/omdb.rb +68 -0
  19. data/examples/experimental/open_exchange_rates.rb +36 -0
  20. data/examples/experimental/open_route.rb +27 -0
  21. data/examples/experimental/open_street_map.rb +16 -0
  22. data/examples/experimental/quandl.rb +50 -0
  23. data/examples/experimental/reddit.rb +25 -0
  24. data/examples/experimental/swapi.rb +27 -0
  25. data/examples/experimental/tmdb.rb +53 -0
  26. data/examples/experimental/world_bank.rb +85 -0
  27. data/examples/experimental/world_bank_climate.rb +77 -0
  28. data/examples/experimental/wunderground.rb +66 -0
  29. data/examples/experimental/wunderground_demo.rb +7 -0
  30. data/examples/forecast_io.rb +16 -16
  31. data/examples/giphy.rb +4 -4
  32. data/examples/giphy_demo.rb +1 -1
  33. data/examples/open_weather_map.rb +64 -60
  34. data/examples/open_weather_map_demo.rb +4 -4
  35. data/examples/tmdb_demo.rb +1 -1
  36. data/examples/urbandictionary_demo.rb +2 -2
  37. data/lib/tlaw.rb +14 -15
  38. data/lib/tlaw/api.rb +108 -26
  39. data/lib/tlaw/api_path.rb +86 -87
  40. data/lib/tlaw/data_table.rb +15 -10
  41. data/lib/tlaw/dsl.rb +126 -224
  42. data/lib/tlaw/dsl/api_builder.rb +47 -0
  43. data/lib/tlaw/dsl/base_builder.rb +108 -0
  44. data/lib/tlaw/dsl/endpoint_builder.rb +26 -0
  45. data/lib/tlaw/dsl/namespace_builder.rb +86 -0
  46. data/lib/tlaw/endpoint.rb +63 -85
  47. data/lib/tlaw/formatting.rb +55 -0
  48. data/lib/tlaw/formatting/describe.rb +86 -0
  49. data/lib/tlaw/formatting/inspect.rb +52 -0
  50. data/lib/tlaw/namespace.rb +141 -98
  51. data/lib/tlaw/param.rb +45 -141
  52. data/lib/tlaw/param/type.rb +36 -49
  53. data/lib/tlaw/response_processors.rb +81 -0
  54. data/lib/tlaw/util.rb +16 -33
  55. data/lib/tlaw/version.rb +6 -3
  56. data/tlaw.gemspec +9 -9
  57. metadata +63 -13
  58. data/lib/tlaw/param_set.rb +0 -111
  59. data/lib/tlaw/response_processor.rb +0 -126
@@ -0,0 +1,66 @@
1
+ module TLAW
2
+ module Examples
3
+ class WUnderground < TLAW::API
4
+ define do
5
+ base 'http://api.wunderground.com/api/{api_key}{/features}{/lang}/q'
6
+
7
+ param :api_key, required: true
8
+ param :lang, format: 'lang:#%s'.method(:%)
9
+
10
+ FEATURES = %i[
11
+ alerts
12
+ almanac
13
+ astronomy
14
+ conditions
15
+ currenthurricane
16
+ forecast
17
+ forecast10day
18
+ geolookup
19
+ hourly
20
+ hourly10day
21
+ rawtide
22
+ tide
23
+ webcams
24
+ yesterday
25
+ ].freeze
26
+
27
+ ALL_FEATURES = %i[
28
+ history
29
+ planner
30
+ ]
31
+
32
+ shared_def :common_params do
33
+ param :features, Array, format: ->(a) { a.join('/') }
34
+ #TODO: enum: FEATURES -- doesn't work with Array
35
+ param :pws, enum: {false => 0, true => 1}
36
+ param :bestfct, enum: {false => 0, true => 1}
37
+ end
38
+
39
+ post_process { |h|
40
+ h.key?('response.error.type') and fail h['response.error.type']
41
+ }
42
+
43
+ endpoint :city, '{/country}/{city}.json' do
44
+ param :city, required: true
45
+
46
+ use_def :common_params
47
+ end
48
+
49
+ endpoint :us_zipcode do
50
+ end
51
+
52
+ endpoint :location do
53
+ end
54
+
55
+ endpoint :airport do
56
+ end
57
+
58
+ endpoint :pws do
59
+ end
60
+
61
+ endpoint :geo_ip do
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+ require_relative '../demo_base'
3
+ require_relative 'wunderground'
4
+
5
+ weather = TLAW::Examples::WUnderground.new(api_key: ENV['WUNDERGROUND'])
6
+
7
+ pp weather.city('Kharkiv', 'Ukraine', features: %i[astronomy tide])
@@ -37,6 +37,22 @@ module TLAW
37
37
  Return summary properties in the desired language. (2-letters code)
38
38
  }
39
39
 
40
+ post_process 'currently.time', &Time.method(:at)
41
+
42
+ post_process_items('minutely.data') {
43
+ post_process 'time', &Time.method(:at)
44
+ }
45
+
46
+ post_process_items('hourly.data') {
47
+ post_process 'time', &Time.method(:at)
48
+ }
49
+
50
+ post_process_items('daily.data') {
51
+ post_process 'time', &Time.method(:at)
52
+ post_process 'sunriseTime', &Time.method(:at)
53
+ post_process 'sunsetTime', &Time.method(:at)
54
+ }
55
+
40
56
  endpoint :forecast, '/{lat},{lng}' do
41
57
  desc %Q{Forecast for the next week.}
42
58
 
@@ -85,22 +101,6 @@ module TLAW
85
101
  currently, minutely, hourly, daily, alerts, flags.
86
102
  }
87
103
  end
88
-
89
- post_process 'currently.time', &Time.method(:at)
90
-
91
- post_process_items('minutely.data') {
92
- post_process 'time', &Time.method(:at)
93
- }
94
-
95
- post_process_items('hourly.data') {
96
- post_process 'time', &Time.method(:at)
97
- }
98
-
99
- post_process_items('daily.data') {
100
- post_process 'time', &Time.method(:at)
101
- post_process 'sunriseTime', &Time.method(:at)
102
- post_process 'sunsetTime', &Time.method(:at)
103
- }
104
104
  end
105
105
  end
106
106
 
data/examples/giphy.rb CHANGED
@@ -31,6 +31,10 @@ module TLAW
31
31
  desc 'Fetch GIPHY stickers (GIFs with transparent background).'
32
32
  end
33
33
 
34
+ post_process_items('data') do
35
+ post_process(/\.(size|mp4_size|webp_size|width|height|frames)/, &:to_i)
36
+ end
37
+
34
38
  endpoint :search do
35
39
  desc 'Search all GIFs by word or phrase.'
36
40
 
@@ -61,10 +65,6 @@ module TLAW
61
65
  param :tag
62
66
  param :rating, desc: 'Parental advisory rating'
63
67
  end
64
-
65
- post_process_items('data') do
66
- post_process(/\.(size|mp4_size|webp_size|width|height|frames)/, &:to_i)
67
- end
68
68
  end
69
69
  end
70
70
 
@@ -81,7 +81,7 @@ giphy.describe
81
81
  # .stickers()
82
82
  # Fetch GIPHY stickers (GIFs with transparent background).
83
83
 
84
- giphy.namespaces[:gifs].describe
84
+ giphy.namespace(:gifs).describe
85
85
  # => .gifs()
86
86
  # Fetch GIPHY GIFs.
87
87
  #
@@ -23,6 +23,56 @@ module TLAW
23
23
  param :units, enum: %i[standard metric imperial], default: :standard,
24
24
  desc: 'Units for temperature and other values. Standard is Kelvin.'
25
25
 
26
+ # OpenWeatherMap reports most of logical errors with HTTP code
27
+ # 200 and responses like {cod: "500", message: "Error message"}
28
+ post_process { |h|
29
+ !h.key?('cod') || (200...400).cover?(h['cod'].to_i) or
30
+ fail "#{h['cod']}: #{h['message']}"
31
+ }
32
+
33
+ shared_def :lat_lng do
34
+ param :lat, :to_f, required: true, desc: 'Latitude'
35
+ param :lng, :to_f, required: true, desc: 'Longitude'
36
+ end
37
+
38
+ shared_def :cluster do
39
+ param :cluster, enum: {true => 'yes', false: 'no'},
40
+ default: true,
41
+ desc: 'Use server clustering of points'
42
+ end
43
+
44
+ WEATHER_POST_PROCESSOR = lambda do |*|
45
+ # Most of the time there is exactly one weather item...
46
+ # ...but sometimes there are two. So, flatterning them looks
47
+ # more reasonable than having DataTable of 1-2 rows.
48
+ post_process { |h|
49
+ h['weather2'] = h['weather'].last if h['weather'] && h['weather'].count > 1
50
+ }
51
+ post_process('weather', &:first)
52
+
53
+ post_process('dt', &Time.method(:at))
54
+ post_process('dt_txt') { nil } # TODO: we need cleaner way to say "remove this"
55
+ post_process('sys.sunrise', &Time.method(:at))
56
+ post_process('sys.sunset', &Time.method(:at))
57
+
58
+ # https://github.com/zverok/geo_coord promo here!
59
+ post_process { |e|
60
+ e['coord'] = Geo::Coord.new(e['coord.lat'], e['coord.lon']) if e['coord.lat'] && e['coord.lon']
61
+ }
62
+ post_process('coord.lat') { nil }
63
+ post_process('coord.lon') { nil }
64
+
65
+ # See http://openweathermap.org/weather-conditions#How-to-get-icon-URL
66
+ post_process('weather.icon', &'http://openweathermap.org/img/w/%s.png'.method(:%))
67
+ end
68
+
69
+ # For endpoints returning weather in one place
70
+ instance_eval(&WEATHER_POST_PROCESSOR)
71
+
72
+ # For endpoints returning list of weathers (forecast or several
73
+ # cities).
74
+ post_process_items('list', &WEATHER_POST_PROCESSOR)
75
+
26
76
  namespace :current, '/weather' do
27
77
  desc %Q{
28
78
  Allows to obtain current weather at one place, designated
@@ -64,8 +114,7 @@ module TLAW
64
114
 
65
115
  docs 'http://openweathermap.org/current#geo'
66
116
 
67
- param :lat, :to_f, required: true, desc: 'Latitude'
68
- param :lng, :to_f, required: true, desc: 'Longitude'
117
+ use_def :lat_lng
69
118
  end
70
119
 
71
120
  endpoint :zip, '?zip={zip}{,country_code}' do
@@ -112,7 +161,7 @@ module TLAW
112
161
  param :start_with, required: true, desc: 'Beginning of city name'
113
162
  param :country_code, desc: 'ISO 3166 2-letter country code'
114
163
 
115
- param :cnt, :to_i, range: 1..50, default: 10,
164
+ param :cnt, :to_i, enum: 1..50, default: 10,
116
165
  desc: 'Max number of results to return'
117
166
 
118
167
  param :accurate, field: :type,
@@ -131,15 +180,12 @@ module TLAW
131
180
 
132
181
  docs 'http://openweathermap.org/current#cycle'
133
182
 
134
- param :lat, :to_f, required: true, desc: 'Latitude'
135
- param :lng, :to_f, required: true, desc: 'Longitude'
183
+ use_def :lat_lng
136
184
 
137
- param :cnt, :to_i, range: 1..50, default: 10,
185
+ param :cnt, :to_i, enum: 1..50, default: 10,
138
186
  desc: 'Max number of results to return'
139
187
 
140
- param :cluster, enum: {true => 'yes', false: 'no'},
141
- default: true,
142
- desc: 'Use server clustering of points'
188
+ use_def :cluster
143
189
  end
144
190
 
145
191
  # Real path is api/bbox/city - not inside /find, but logically
@@ -158,9 +204,7 @@ module TLAW
158
204
  param :zoom, :to_i, default: 10, keyword: true,
159
205
  desc: 'Map zoom level.'
160
206
 
161
- param :cluster, enum: {true => 'yes', false: 'no'},
162
- default: true,
163
- desc: 'Use server clustering of points'
207
+ use_def :cluster
164
208
  end
165
209
  end
166
210
 
@@ -177,6 +221,13 @@ module TLAW
177
221
 
178
222
  docs 'http://openweathermap.org/forecast5'
179
223
 
224
+ post_process { |e|
225
+ e['city.coord'] = Geo::Coord.new(e['city.coord.lat'], e['city.coord.lon']) \
226
+ if e['city.coord.lat'] && e['city.coord.lon']
227
+ }
228
+ post_process('city.coord.lat') { nil }
229
+ post_process('city.coord.lon') { nil }
230
+
180
231
  endpoint :city, '?q={city}{,country_code}' do
181
232
  desc %Q{
182
233
  Weather forecast by city name (with optional country code
@@ -210,56 +261,9 @@ module TLAW
210
261
 
211
262
  docs 'http://openweathermap.org/forecast5#geo5'
212
263
 
213
- param :lat, :to_f, required: true, desc: 'Latitude'
214
- param :lng, :to_f, required: true, desc: 'Longitude'
264
+ use_def :lat_lng
215
265
  end
216
-
217
- post_process { |e|
218
- e['city.coord'] = Geo::Coord.new(e['city.coord.lat'], e['city.coord.lon']) \
219
- if e['city.coord.lat'] && e['city.coord.lon']
220
- }
221
- post_process('city.coord.lat') { nil }
222
- post_process('city.coord.lon') { nil }
223
266
  end
224
-
225
- # OpenWeatherMap reports most of logical errors with HTTP code
226
- # 200 and responses like {cod: "500", message: "Error message"}
227
- post_process { |h|
228
- !h.key?('cod') || (200..400).cover?(h['cod'].to_i) or
229
- fail "#{h['cod']}: #{h['message']}"
230
- }
231
-
232
- WEATHER_POST_PROCESSOR = lambda do |*|
233
- # Most of the time there is exactly one weather item...
234
- # ...but sometimes there are two. So, flatterning them looks
235
- # more reasonable than having DataTable of 1-2 rows.
236
- post_process { |h|
237
- h['weather2'] = h['weather'].last if h['weather'] && h['weather'].count > 1
238
- }
239
- post_process('weather', &:first)
240
-
241
- post_process('dt', &Time.method(:at))
242
- post_process('dt_txt') { nil } # TODO: we need cleaner way to say "remove this"
243
- post_process('sys.sunrise', &Time.method(:at))
244
- post_process('sys.sunset', &Time.method(:at))
245
-
246
- # https://github.com/zverok/geo_coord promo here!
247
- post_process { |e|
248
- e['coord'] = Geo::Coord.new(e['coord.lat'], e['coord.lon']) if e['coord.lat'] && e['coord.lon']
249
- }
250
- post_process('coord.lat') { nil }
251
- post_process('coord.lon') { nil }
252
-
253
- # See http://openweathermap.org/weather-conditions#How-to-get-icon-URL
254
- post_process('weather.icon') { |i| "http://openweathermap.org/img/w/#{i}.png" }
255
- end
256
-
257
- # For endpoints returning weather in one place
258
- instance_eval(&WEATHER_POST_PROCESSOR)
259
-
260
- # For endpoints returning list of weathers (forecast or several
261
- # cities).
262
- post_process_items('list', &WEATHER_POST_PROCESSOR)
263
267
  end
264
268
  end
265
269
  end
@@ -53,11 +53,11 @@ p TLAW::Examples::OpenWeatherMap.describe
53
53
  # and it is just printed the most convenient way.
54
54
 
55
55
  # Let's look closer to some of those namespaces:
56
- p TLAW::Examples::OpenWeatherMap.namespaces[:current]
56
+ p TLAW::Examples::OpenWeatherMap.namespace(:current)
57
57
  # => #<TLAW::Examples::OpenWeatherMap::Current: call-sequence: current(); endpoints: city, city_id, location, zip, group; docs: .describe>
58
58
 
59
59
  # .describe, anyone?
60
- p TLAW::Examples::OpenWeatherMap.namespaces[:current].describe
60
+ p TLAW::Examples::OpenWeatherMap.namespace(:current).describe
61
61
  # .current()
62
62
  # Allows to obtain current weather at one place, designated
63
63
  # by city, location or zip code.
@@ -87,11 +87,11 @@ p TLAW::Examples::OpenWeatherMap.namespaces[:current].describe
87
87
 
88
88
  # And further:
89
89
  p TLAW::Examples::OpenWeatherMap
90
- .namespaces[:current].endpoints[:city]
90
+ .namespace(:current).endpoint(:city)
91
91
  # => #<TLAW::Examples::OpenWeatherMap::Current::City: call-sequence: city(city, country_code=nil); docs: .describe>
92
92
 
93
93
  p TLAW::Examples::OpenWeatherMap
94
- .namespaces[:current].endpoints[:city].describe
94
+ .namespace(:current).endpoint(:city).describe
95
95
  # .city(city, country_code=nil)
96
96
  # Current weather by city name (with optional country code
97
97
  # specification).
@@ -121,7 +121,7 @@ p tmdb.movies.describe
121
121
  #
122
122
  # .[](id)
123
123
 
124
- p tmdb.movies.namespaces[:[]].describe
124
+ p tmdb.movies.namespace(:[]).describe
125
125
  # .[](id)
126
126
  # @param id
127
127
  #
@@ -33,10 +33,10 @@ module TLAW
33
33
  module Examples
34
34
  class UrbanDictionary < TLAW::API
35
35
  define do
36
- desc %Q{
36
+ desc <<~D
37
37
  Really small API. Described as "official but undocumented"
38
38
  by some.
39
- }
39
+ D
40
40
 
41
41
  base 'http://api.urbandictionary.com/v0'
42
42
 
data/lib/tlaw.rb CHANGED
@@ -1,20 +1,18 @@
1
- require 'open-uri'
1
+ # frozen_string_literal: true
2
+
2
3
  require 'json'
4
+
5
+ require 'backports/2.4.0/string/match'
6
+ require 'backports/2.4.0/hash/compact'
7
+ require 'backports/2.4.0/hash/transform_values'
8
+
9
+ require 'backports/2.5.0/kernel/yield_self'
10
+ require 'backports/2.5.0/hash/transform_keys'
11
+ require 'backports/2.5.0/enumerable/all'
12
+
3
13
  require 'addressable/uri'
4
14
  require 'addressable/template'
5
15
 
6
- # Let no one know! But they in Ruby committee just too long to add
7
- # something like this to the language.
8
- #
9
- # See also https://bugs.ruby-lang.org/issues/12760
10
- #
11
- # @private
12
- class Object
13
- def derp
14
- yield self
15
- end
16
- end
17
-
18
16
  # TLAW is a framework for creating API wrappers for get-only APIs (like
19
17
  # weather, geonames and so on) or subsets of APIs (like getting data from
20
18
  # Twitter).
@@ -56,13 +54,14 @@ require_relative 'tlaw/util'
56
54
  require_relative 'tlaw/data_table'
57
55
 
58
56
  require_relative 'tlaw/param'
59
- require_relative 'tlaw/param_set'
60
57
 
61
58
  require_relative 'tlaw/api_path'
62
59
  require_relative 'tlaw/endpoint'
63
60
  require_relative 'tlaw/namespace'
64
61
  require_relative 'tlaw/api'
65
62
 
66
- require_relative 'tlaw/response_processor'
63
+ require_relative 'tlaw/formatting'
64
+
65
+ require_relative 'tlaw/response_processors'
67
66
 
68
67
  require_relative 'tlaw/dsl'
data/lib/tlaw/api.rb CHANGED
@@ -1,15 +1,20 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module TLAW
2
- # API is just a top-level {Namespace}.
4
+ # API is a main TLAW class (and the only one you need to use directly).
3
5
  #
4
- # Basically, you start creating your endpoint by descending from API
5
- # and defining namespaces and endpoints through a {DSL} like this:
6
+ # Basically, you start creating your definition by descending from API and defining namespaces and
7
+ # endpoints through a {DSL} like this:
6
8
  #
7
9
  # ```ruby
8
- # class MyCoolAPI < TLAW::API
10
+ # class SomeImagesAPI < TLAW::API
9
11
  # define do
10
12
  # base 'http://api.mycool.com'
11
13
  #
12
- # namespace :awesome do
14
+ # namespace :gifs do
15
+ # endpoint :search do
16
+ # param :query
17
+ # end
13
18
  # # ...and so on
14
19
  # end
15
20
  # end
@@ -19,40 +24,117 @@ module TLAW
19
24
  # And then, you use it:
20
25
  #
21
26
  # ```ruby
22
- # api = MyCoolAPI.new
23
- # api.awesome.cool(param: 'value')
27
+ # api = SomeImagesAPI.new
28
+ # api.gifs.search(query: 'butterfly')
24
29
  # ```
25
30
  #
26
- # See {DSL} for explanation of API definition, {Namespace} for explanation
27
- # of possible usages and {Endpoint} for real calls performing.
31
+ # See {DSL} for detailed information of API definition, and {Namespace} for explanation about
32
+ # dynamically generated methods ({API} is also an instance of a {Namespace}).
28
33
  #
29
34
  class API < Namespace
30
- # Thrown when there are an error during call. Contains real URL which
31
- # was called at the time of an error.
35
+ # Thrown when there are an error during call. Contains real URL which was called at the time of
36
+ # an error.
32
37
  class Error < RuntimeError
33
38
  end
34
39
 
35
40
  class << self
41
+ # @private
42
+ attr_reader :url_template
43
+
36
44
  # Runs the {DSL} inside your API wrapper class.
37
45
  def define(&block)
38
- DSL::APIWrapper.new(self).define(&block)
46
+ self == API and fail '#define should be called on the descendant of the TLAW::API'
47
+ DSL::ApiBuilder.new(self, &block).finalize
48
+ self
49
+ end
50
+
51
+ # @private
52
+ def setup(base_url: nil, **args)
53
+ if url_template
54
+ base_url and fail ArgumentError, "API's base_url can't be changed on redefinition"
55
+ else
56
+ base_url or fail ArgumentError, "API can't be defined without base_url"
57
+ self.url_template = base_url
58
+ end
59
+ super(symbol: nil, path: '', **args)
39
60
  end
40
61
 
41
- # Returns detailed description of an API, like this:
42
- #
43
- # ```ruby
44
- # MyCoolAPI.describe
45
- # # MyCoolAPI.new()
46
- # # This is cool API.
47
- # #
48
- # # Namespaces:
49
- # # .awesome()
50
- # # This is awesome.
51
- # ```
52
- #
53
- def describe(*)
54
- super.sub(/\A./, '')
62
+ # @private
63
+ def is_defined? # rubocop:disable Naming/PredicateName
64
+ self < API
55
65
  end
66
+
67
+ protected
68
+
69
+ attr_writer :url_template
70
+
71
+ private :parent, :parent=
72
+ end
73
+
74
+ private :parent
75
+
76
+ # Create an instance of your API descendant.
77
+ # Params to pass here correspond to `param`s defined at top level of the DSL, e.g.
78
+ #
79
+ # ```ruby
80
+ # # if you defined your API like this...
81
+ # class MyAPI < TLAW::API
82
+ # define do
83
+ # param :api_key
84
+ # # ....
85
+ # end
86
+ # end
87
+ #
88
+ # # the instance should be created like this:
89
+ # api = MyAPI.new(api_key: '<some-api-key>')
90
+ # ```
91
+ #
92
+ # If the block is passed, it is called with an instance of
93
+ # [Faraday::Connection](https://www.rubydoc.info/gems/faraday/Faraday/Connection) object which
94
+ # would be used for API requests, allowing to set up some connection configuration:
95
+ #
96
+ # ```ruby
97
+ # api = MyAPI.new(api_key: '<some-api-key>') { |conn| conn.basic_auth 'login', 'pass' }
98
+ # ```
99
+ #
100
+ # @yield [Faraday::Connection]
101
+ def initialize(**params, &block)
102
+ super(nil, **params)
103
+
104
+ @client = Faraday.new do |faraday|
105
+ faraday.use FaradayMiddleware::FollowRedirects
106
+ faraday.adapter Faraday.default_adapter
107
+ block&.call(faraday)
108
+ end
109
+ end
110
+
111
+ # @private
112
+ def request(url, **params)
113
+ @client.get(url, **params).tap(&method(:guard_errors!))
114
+ rescue Error
115
+ raise # Not catching in the next block
116
+ rescue StandardError => e
117
+ raise Error, "#{e.class} at #{url}: #{e.message}"
118
+ end
119
+
120
+ private
121
+
122
+ def guard_errors!(response)
123
+ # TODO: follow redirects
124
+ return response if (200...400).cover?(response.status)
125
+
126
+ fail Error,
127
+ "HTTP #{response.status} at #{response.env[:url]}" +
128
+ extract_message(response.body)&.yield_self { |m| ': ' + m }.to_s
129
+ end
130
+
131
+ def extract_message(body)
132
+ # FIXME: well, that's just awful
133
+ # ...minimal is at least extract *_message key (TMDB has status_message, for ex.)
134
+ data = JSON.parse(body) rescue nil
135
+ return body unless data.is_a?(Hash)
136
+
137
+ data.values_at('message', 'error').compact.first || body
56
138
  end
57
139
  end
58
140
  end