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.
- checksums.yaml +4 -4
- data/.ruby-version +1 -0
- data/CHANGELOG.md +18 -2
- data/README.md +10 -7
- data/examples/demo_base.rb +2 -2
- data/examples/experimental/README.md +3 -0
- data/examples/experimental/afterthedeadline.rb +22 -0
- data/examples/experimental/airvisual.rb +14 -0
- data/examples/experimental/apixu.rb +32 -0
- data/examples/experimental/bing_maps.rb +18 -0
- data/examples/experimental/currencylayer.rb +25 -0
- data/examples/experimental/earthquake.rb +29 -0
- data/examples/experimental/freegeoip.rb +16 -0
- data/examples/experimental/geonames.rb +98 -0
- data/examples/experimental/isfdb.rb +17 -0
- data/examples/experimental/musicbrainz.rb +27 -0
- data/examples/experimental/nominatim.rb +52 -0
- data/examples/experimental/omdb.rb +68 -0
- data/examples/experimental/open_exchange_rates.rb +36 -0
- data/examples/experimental/open_route.rb +27 -0
- data/examples/experimental/open_street_map.rb +16 -0
- data/examples/experimental/quandl.rb +50 -0
- data/examples/experimental/reddit.rb +25 -0
- data/examples/experimental/swapi.rb +27 -0
- data/examples/experimental/tmdb.rb +53 -0
- data/examples/experimental/world_bank.rb +85 -0
- data/examples/experimental/world_bank_climate.rb +77 -0
- data/examples/experimental/wunderground.rb +66 -0
- data/examples/experimental/wunderground_demo.rb +7 -0
- data/examples/forecast_io.rb +16 -16
- data/examples/giphy.rb +4 -4
- data/examples/giphy_demo.rb +1 -1
- data/examples/open_weather_map.rb +64 -60
- data/examples/open_weather_map_demo.rb +4 -4
- data/examples/tmdb_demo.rb +1 -1
- data/examples/urbandictionary_demo.rb +2 -2
- data/lib/tlaw.rb +14 -15
- data/lib/tlaw/api.rb +108 -26
- data/lib/tlaw/api_path.rb +86 -87
- data/lib/tlaw/data_table.rb +15 -10
- data/lib/tlaw/dsl.rb +126 -224
- data/lib/tlaw/dsl/api_builder.rb +47 -0
- data/lib/tlaw/dsl/base_builder.rb +108 -0
- data/lib/tlaw/dsl/endpoint_builder.rb +26 -0
- data/lib/tlaw/dsl/namespace_builder.rb +86 -0
- data/lib/tlaw/endpoint.rb +63 -85
- data/lib/tlaw/formatting.rb +55 -0
- data/lib/tlaw/formatting/describe.rb +86 -0
- data/lib/tlaw/formatting/inspect.rb +52 -0
- data/lib/tlaw/namespace.rb +141 -98
- data/lib/tlaw/param.rb +45 -141
- data/lib/tlaw/param/type.rb +36 -49
- data/lib/tlaw/response_processors.rb +81 -0
- data/lib/tlaw/util.rb +16 -33
- data/lib/tlaw/version.rb +6 -3
- data/tlaw.gemspec +9 -9
- metadata +63 -13
- data/lib/tlaw/param_set.rb +0 -111
- 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
|
data/examples/forecast_io.rb
CHANGED
@@ -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
|
|
data/examples/giphy_demo.rb
CHANGED
@@ -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
|
-
|
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,
|
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
|
-
|
135
|
-
param :lng, :to_f, required: true, desc: 'Longitude'
|
183
|
+
use_def :lat_lng
|
136
184
|
|
137
|
-
param :cnt, :to_i,
|
185
|
+
param :cnt, :to_i, enum: 1..50, default: 10,
|
138
186
|
desc: 'Max number of results to return'
|
139
187
|
|
140
|
-
|
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
|
-
|
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
|
-
|
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.
|
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.
|
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
|
-
.
|
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
|
-
.
|
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).
|
data/examples/tmdb_demo.rb
CHANGED
data/lib/tlaw.rb
CHANGED
@@ -1,20 +1,18 @@
|
|
1
|
-
|
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/
|
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
|
4
|
+
# API is a main TLAW class (and the only one you need to use directly).
|
3
5
|
#
|
4
|
-
# Basically, you start creating your
|
5
|
-
#
|
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
|
10
|
+
# class SomeImagesAPI < TLAW::API
|
9
11
|
# define do
|
10
12
|
# base 'http://api.mycool.com'
|
11
13
|
#
|
12
|
-
# namespace :
|
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 =
|
23
|
-
# api.
|
27
|
+
# api = SomeImagesAPI.new
|
28
|
+
# api.gifs.search(query: 'butterfly')
|
24
29
|
# ```
|
25
30
|
#
|
26
|
-
# See {DSL} for
|
27
|
-
#
|
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
|
-
#
|
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
|
-
|
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
|
-
#
|
42
|
-
#
|
43
|
-
|
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
|