tlaw 0.0.2 → 0.1.0.pre

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