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.
- 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
|