tlaw 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,219 @@
1
+ #!/usr/bin/env ruby
2
+ require_relative 'demo_base'
3
+ require_relative 'open_weather_map'
4
+
5
+ # This is demonstration of TLAW (The Last API Wrapper) library's behavior
6
+ # and opinions.
7
+ #
8
+ # All of below functionality is created by this API wrapper definition:
9
+ # TODO URL
10
+
11
+ # OK, first thing is: all API wrappers created with TLAW, are
12
+ # _discoverable_ by design. In fact, you can understand all you need to
13
+ # use the API just from IRB, no need to go to rdoc.info, dig in code,
14
+ # or, my favourite thing, "see arguments explanation at original API
15
+ # site".
16
+
17
+ # Let's see:
18
+
19
+ p TLAW::Examples::OpenWeatherMap
20
+ # => #<TLAW::Examples::OpenWeatherMap: call-sequence: TLAW::Examples::OpenWeatherMap.new(appid:, lang: "en", units: :standard); namespaces: current, find, forecast; docs: .describe>
21
+
22
+ # Let's try this .describe thing which inspect recommends:
23
+
24
+ p TLAW::Examples::OpenWeatherMap.describe
25
+ # TLAW::Examples::OpenWeatherMap.new(appid:, lang: "en", units: :standard)
26
+ # API for [OpenWeatherMap](http://openweathermap.org/). Only parts
27
+ # available for free are implemented (as only them could be tested).
28
+ #
29
+ # See full docs at http://openweathermap.org/api
30
+ #
31
+ # @param appid You need to receive it at http://openweathermap.org/appid (free)
32
+ # @param lang Language of API responses (affects weather description only).
33
+ # See http://openweathermap.org/current#multi for list of supported languages. (default = "en")
34
+ # @param units Units for temperature and other values. Standard is Kelvin.
35
+ # Possible values: :standard, :metric, :imperial (default = :standard)
36
+ #
37
+ # Namespaces:
38
+ #
39
+ # .current()
40
+ # Allows to obtain current weather at one place, designated
41
+ # by city, location or zip code.
42
+ #
43
+ # .find()
44
+ # Allows to find some place (and weather in it) by set of input
45
+ # parameters.
46
+ #
47
+ # .forecast()
48
+ # Allows to obtain weather forecast for 5 days with 3-hour
49
+ # frequency.
50
+
51
+ # Note that this multiline output is produced by `p`! So, in IRB/pry
52
+ # session it would be exactly the same: you just say "something.describe",
53
+ # and it is just printed the most convenient way.
54
+
55
+ # Let's look closer to some of those namespaces:
56
+ p TLAW::Examples::OpenWeatherMap.namespaces[:current]
57
+ # => #<TLAW::Examples::OpenWeatherMap::Current: call-sequence: current(); endpoints: city, city_id, location, zip, group; docs: .describe>
58
+
59
+ # .describe, anyone?
60
+ p TLAW::Examples::OpenWeatherMap.namespaces[:current].describe
61
+ # .current()
62
+ # Allows to obtain current weather at one place, designated
63
+ # by city, location or zip code.
64
+ #
65
+ # Docs: http://openweathermap.org/current
66
+ #
67
+ #
68
+ # Endpoints:
69
+ #
70
+ # .city(city, country_code=nil)
71
+ # Current weather by city name (with optional country code
72
+ # specification).
73
+ #
74
+ # .city_id(city_id)
75
+ # Current weather by city id. Recommended by OpenWeatherMap
76
+ # docs.
77
+ #
78
+ # .location(lat, lng)
79
+ # Current weather by geographic coordinates.
80
+ #
81
+ # .zip(zip, country_code=nil)
82
+ # Current weather by ZIP code (with optional country code
83
+ # specification).
84
+ #
85
+ # .group(city_ids)
86
+ # Current weather in several cities by their ids.
87
+
88
+ # And further:
89
+ p TLAW::Examples::OpenWeatherMap
90
+ .namespaces[:current].endpoints[:city]
91
+ # => #<TLAW::Examples::OpenWeatherMap::Current::City: call-sequence: city(city, country_code=nil); docs: .describe>
92
+
93
+ p TLAW::Examples::OpenWeatherMap
94
+ .namespaces[:current].endpoints[:city].describe
95
+ # .city(city, country_code=nil)
96
+ # Current weather by city name (with optional country code
97
+ # specification).
98
+ #
99
+ # Docs: http://openweathermap.org/current#name
100
+ #
101
+ # @param city City name
102
+ # @param country_code ISO 3166 2-letter country code
103
+
104
+ # Note, that all above classes and methods are generated at moment of
105
+ # API definition, so there is no cumbersome dispatching at runtime:
106
+
107
+ p TLAW::Examples::OpenWeatherMap.instance_methods(false)
108
+ # => [:current, :find, :forecast]
109
+ p TLAW::Examples::OpenWeatherMap::Current.instance_methods(false)
110
+ # => [:city, :city_id, :location, :zip, :group]
111
+ p TLAW::Examples::OpenWeatherMap::Current.instance_method(:city).parameters
112
+ # => [[:req, :city], [:opt, :country_code]]
113
+
114
+ # E.g. namespace is a class, providing methods for all the child
115
+ # namespaces and endpoints! And all params are just method params.
116
+
117
+ # OK, let's go for some real things, not just documentation reading.
118
+
119
+ # You need to create key here: http://openweathermap.org/appid
120
+ # And run the script this way:
121
+ #
122
+ # OPEN_WEATHER_MAP={your_id} examples/open_weather_map_demo.rb
123
+ #
124
+ weather = TLAW::Examples::OpenWeatherMap
125
+ .new(appid: ENV['OPEN_WEATHER_MAP'], units: :metric)
126
+ p weather
127
+ # => #<TLAW::Examples::OpenWeatherMap.new(appid: {your id}, lang: nil, units: :metric) namespaces: current, find, forecast; docs: .describe>
128
+
129
+ # Looks familiar and nothing new.
130
+
131
+ p weather.current
132
+ # => #<current() endpoints: city, city_id, location, zip, group; docs: .describe>
133
+
134
+ # Saem.
135
+
136
+ pp weather.current.city('Kharkiv')
137
+ # {"weather.id"=>800,
138
+ # "weather.main"=>"Clear",
139
+ # "weather.description"=>"clear sky",
140
+ # "weather.icon"=>"http://openweathermap.org/img/w/01n.png",
141
+ # "base"=>"cmc stations",
142
+ # "main.temp"=>23,
143
+ # "main.pressure"=>1013,
144
+ # "main.humidity"=>40,
145
+ # "main.temp_min"=>23,
146
+ # "main.temp_max"=>23,
147
+ # "wind.speed"=>2,
148
+ # "wind.deg"=>190,
149
+ # "clouds.all"=>0,
150
+ # "dt"=>2016-09-05 20:30:00 +0300,
151
+ # "sys.type"=>1,
152
+ # "sys.id"=>7355,
153
+ # "sys.message"=>0.0115,
154
+ # "sys.country"=>"UA",
155
+ # "sys.sunrise"=>2016-09-05 05:57:26 +0300,
156
+ # "sys.sunset"=>2016-09-05 19:08:13 +0300,
157
+ # "id"=>706483,
158
+ # "name"=>"Kharkiv",
159
+ # "cod"=>200,
160
+ # "coord"=>#<Geo::Coord 50.000000,36.250000>}
161
+
162
+ # Whoa!
163
+ #
164
+ # What we see here (except for "OK, it works")?
165
+ #
166
+ # Several pretty improtant things:
167
+ #
168
+ # * TLAW response processing is _highly opinionated_. It tends do flatten
169
+ # all the hashes: original has something like
170
+ # {weather: {...}, main: {...}, sys: {...}
171
+ # * It is done, again, for the sake of _discoverability_. You, like, see
172
+ # at once all things API response proposes; you can do `response.keys`
173
+ # to understand what you got instead of `response.keys`, hm,
174
+ # `response['weather'].class`, ah, ok, `response['weather'].keys` and
175
+ # so on;
176
+ # * TLAW allows easy postprocessing of response: our example API parses
177
+ # timestamps into proper ruby times, and (just to promote related gem),
178
+ # converts "coord.lat" and "coord.lng" to one instance of a Geo::Coord,
179
+ # from https://github.com/zverok/geo_coord
180
+ #
181
+
182
+ # Finally, one HUGE design decision related to "opinionated response
183
+ # processing":
184
+ pp weather.forecast.city('Kharkiv')
185
+ # {"city.id"=>706483,
186
+ # "city.name"=>"Kharkiv",
187
+ # "city.country"=>"UA",
188
+ # "city.population"=>0,
189
+ # "city.sys.population"=>0,
190
+ # "cod"=>"200",
191
+ # "message"=>0.0276,
192
+ # "cnt"=>40,
193
+ # "list"=>
194
+ # #<TLAW::DataTable[dt, main.temp, main.temp_min, main.temp_max, main.pressure, main.sea_level, main.grnd_level, main.humidity, main.temp_kf, weather.id, weather.main, weather.description, weather.icon, clouds.all, wind.speed, wind.deg, sys.pod] x 40>,
195
+ # "city.coord"=>#<Geo::Coord 50.000000,36.250000>}
196
+
197
+ # Hmm? What is this DataTable thingy? It is (loosy) implementation of
198
+ # DataFrame data type. You can think of it as an array of homogenous
199
+ # hashes -- which could be considered the main type of useful JSON API
200
+ # data.
201
+ forecasts = weather.forecast.city('Kharkiv')['list']
202
+
203
+ p forecasts.count
204
+ # => 40
205
+ p forecasts.keys
206
+ # => ["dt", "main.temp", "main.temp_min", "main.temp_max", "main.pressure", "main.sea_level", "main.grnd_level", "main.humidity", "main.temp_kf", "weather.id", "weather.main", "weather.description", "weather.icon", "clouds.all", "wind.speed", "wind.deg", "sys.pod"]
207
+ p forecasts.first
208
+ # => {"dt"=>2016-09-06 00:00:00 +0300, "main.temp"=>12.67, "main.temp_min"=>12.67, "main.temp_max"=>15.84, "main.pressure"=>1006.67, "main.sea_level"=>1026.62, "main.grnd_level"=>1006.67, "main.humidity"=>74, "main.temp_kf"=>-3.17, "weather.id"=>800, "weather.main"=>"Clear", "weather.description"=>"clear sky", "weather.icon"=>"http://openweathermap.org/img/w/01n.png", "clouds.all"=>0, "wind.speed"=>1.26, "wind.deg"=>218.509, "sys.pod"=>"n"}
209
+
210
+ p forecasts['dt'].first(3)
211
+ # => [2016-09-06 00:00:00 +0300, 2016-09-06 03:00:00 +0300, 2016-09-06 06:00:00 +0300]
212
+
213
+ p forecasts.columns('dt', 'main.temp').to_a.first(3)
214
+ # => [{"dt"=>2016-09-06 00:00:00 +0300, "main.temp"=>12.67}, {"dt"=>2016-09-06 03:00:00 +0300, "main.temp"=>11.65}, {"dt"=>2016-09-06 06:00:00 +0300, "main.temp"=>12.41}]
215
+
216
+ # All of it works and you can check it by yourself.
217
+ #
218
+ # Again, EVERYTHING you can see in this example is created by short and
219
+ # focused API definition: TODO URL
@@ -0,0 +1,133 @@
1
+ #!/usr/bin/env ruby
2
+ require_relative 'demo_base'
3
+
4
+ # This example demonstrates how TLAW allows you to define and redefine
5
+ # API wrappers on the fly—to the extent you need and without much
6
+ # bothering—and still have all the goodies.
7
+
8
+ # For example, you have pretty large and complicated TheMoviesDatabase API:
9
+ # http://docs.themoviedb.apiary.io/
10
+ # ...and all you want is just to search for movies and get their posters.
11
+ # All existing TMDB Ruby wrappers (I know at least three) are strange.
12
+ #
13
+ # What you'll do?
14
+ #
15
+ # That's what:
16
+
17
+ class TMDB < TLAW::API
18
+ define do
19
+ base 'http://api.themoviedb.org/3'
20
+ param :api_key, required: true
21
+ param :language, default: 'en'
22
+
23
+ namespace :movies, '/movie' do
24
+ namespace :[], '/{id}' do
25
+ param :id, required: true
26
+
27
+ endpoint :images
28
+ end
29
+ end
30
+
31
+ namespace :search do
32
+ endpoint :movie do
33
+ param :query, required: true, keyword: false
34
+
35
+ post_process_items('results') {
36
+ post_process 'release_date', &Date.method(:parse)
37
+ }
38
+ end
39
+ end
40
+ end
41
+ end
42
+
43
+ # You need to run it like TMDB={your_key} ./examples/tmdb_demo.rb
44
+ tmdb = TMDB.new(api_key: ENV['TMDB'])
45
+
46
+ pp tmdb.search.movie('guardians of the galaxy')
47
+ # {"page"=>1,
48
+ # "results"=>
49
+ # #<TLAW::DataTable[poster_path, adult, overview, release_date, genre_ids, id, original_title, original_language, title, backdrop_path, popularity, vote_count, video, vote_average] x 2>,
50
+ # "total_results"=>2,
51
+ # "total_pages"=>1}
52
+
53
+ pp tmdb.search.movie('guardians of the galaxy')['results'].first
54
+ # {"poster_path"=>"/y31QB9kn3XSudA15tV7UWQ9XLuW.jpg",
55
+ # "adult"=>false,
56
+ # "overview"=>
57
+ # "Light years from Earth, 26 years after being abducted, Peter Quill finds himself the prime target of a manhunt after discovering an orb wanted by Ronan the Accuser.",
58
+ # "release_date"=>#<Date: 2014-07-30 ((2456869j,0s,0n),+0s,2299161j)>,
59
+ # "genre_ids"=>[28, 878, 12],
60
+ # "id"=>118340,
61
+ # "original_title"=>"Guardians of the Galaxy",
62
+ # "original_language"=>"en",
63
+ # "title"=>"Guardians of the Galaxy",
64
+ # "backdrop_path"=>"/bHarw8xrmQeqf3t8HpuMY7zoK4x.jpg",
65
+ # "popularity"=>12.287455,
66
+ # "vote_count"=>5067,
67
+ # "video"=>false,
68
+ # "vote_average"=>7.96}
69
+
70
+ # OK, now we have an id
71
+ pp tmdb.movies[118340].images
72
+
73
+ # Note, that [] is also namespace accessor here :) With param. See API
74
+ # description above.
75
+
76
+ pp tmdb.movies[118340].images['posters'].last
77
+ # {"aspect_ratio"=>0.666666666666667,
78
+ # "file_path"=>"/6YUodKKkqIIDx6Hk7ZkaVOxnWND.jpg",
79
+ # "height"=>1500,
80
+ # "iso_639_1"=>"ru",
81
+ # "vote_average"=>0.0,
82
+ # "vote_count"=>0,
83
+ # "width"=>1000}
84
+
85
+ # Hmm, maybe we need some path post-processing?.. How about adding it
86
+ # right now? Assuming our API is already described by someone else...
87
+ tmdb.class.define do
88
+ namespace :movies do
89
+ namespace :[] do
90
+ endpoint :images do
91
+ post_process_items 'posters' do
92
+ post_process('file_path') { |p| 'https://image.tmdb.org/t/p/original' + p }
93
+ end
94
+ end
95
+ end
96
+ end
97
+ end
98
+
99
+ pp tmdb.movies[118340].images['posters'].last
100
+ # Ah, much better!
101
+ #
102
+ # {"aspect_ratio"=>0.666666666666667,
103
+ # "file_path"=>
104
+ # "https://image.tmdb.org/t/p/original/6YUodKKkqIIDx6Hk7ZkaVOxnWND.jpg",
105
+ # "height"=>1500,
106
+ # "iso_639_1"=>"ru",
107
+ # "vote_average"=>0.0,
108
+ # "vote_count"=>0,
109
+ # "width"=>1000}
110
+
111
+
112
+ # Note, that despite not adding a bit of documentation, you still have
113
+ # your API wrapper discoverable:
114
+ p TMDB
115
+ # #<TMDB: call-sequence: TMDB.new(api_key:, language: "en"); namespaces: movies, search; docs: .describe>
116
+ p tmdb.movies.describe
117
+ # .movies()
118
+ #
119
+ #
120
+ # Namespaces:
121
+ #
122
+ # .[](id)
123
+
124
+ p tmdb.movies.namespaces[:[]].describe
125
+ # .[](id)
126
+ # @param id
127
+ #
128
+ # Endpoints:
129
+ #
130
+ # .images()
131
+
132
+ # So, you can still investigate, navigate and get meaningful API errors
133
+ # with backtraces... While you spent only like 15 lines on API description.
@@ -0,0 +1,105 @@
1
+ #!/usr/bin/env ruby
2
+ require_relative 'demo_base'
3
+
4
+ # That's an example of TLAW's strength.
5
+ #
6
+ # Urbandictionary API is really small (just two endpoints, only one of
7
+ # which is actually useful, because /random is more like a joke).
8
+ #
9
+ # But you still need to remember the protocol, parse the answer and so
10
+ # on.
11
+ #
12
+ # There is even separate gem: https://github.com/ryangreenberg/urban_dictionary
13
+ # Its `lib` folder contains 7 files, 9 classes/modules and 300 lines of
14
+ # code, I kid you not.
15
+ #
16
+ # I have no intention to offend that gem's author! I just saying that's
17
+ # what you get when you need to design everything from scratch, like HTTP
18
+ # client and params processing and response parsing and whatnot.
19
+ #
20
+ # Oh, and there is another one: https://github.com/tmiller/urban
21
+ #
22
+ # But when somebody really need them (for chatbots), they use neither,
23
+ # just redefine everything from scratch with rough net connection and
24
+ # response parsing (because both of aforementioned gems are too thick
25
+ # wrappers to rely on them):
26
+ # * https://github.com/jimmycuadra/lita-urban-dictionary
27
+ # * https://github.com/cinchrb/cinch-urbandictionary
28
+ #
29
+ # Here is our version (17 codelines, including namespacing and bit of
30
+ # docs, API definition itself takes like 7 lines only):
31
+ #
32
+ module TLAW
33
+ module Examples
34
+ class UrbanDictionary < TLAW::API
35
+ define do
36
+ desc %Q{
37
+ Really small API. Described as "official but undocumented"
38
+ by some.
39
+ }
40
+
41
+ base 'http://api.urbandictionary.com/v0'
42
+
43
+ endpoint :define, '/define?term={term}' do
44
+ param :term, required: true
45
+ end
46
+
47
+ endpoint :random, '/random'
48
+ end
49
+ end
50
+ end
51
+ end
52
+
53
+ # Usage (response is clear as tears, could be integrated anywhere):
54
+
55
+ d = TLAW::Examples::UrbanDictionary.new
56
+
57
+ p d.describe
58
+ # TLAW::Examples::UrbanDictionary.new()
59
+ # Really small API. Described as "official but undocumented"
60
+ # by some.
61
+ #
62
+ # Endpoints:
63
+ #
64
+ # .define(term)
65
+ #
66
+ # .random()
67
+
68
+ res = d.define('trollface')
69
+
70
+ pp res
71
+ # {"tags"=>
72
+ # ["troll",
73
+ # "trolling",
74
+ # "meme",
75
+ # "4chan",
76
+ # "troll face",
77
+ # "coolface",
78
+ # "trollfacing",
79
+ # "trolls",
80
+ # "backpfeifengesicht",
81
+ # "derp"],
82
+ # "result_type"=>"exact",
83
+ # "list"=>
84
+ # #<TLAW::DataTable[definition, permalink, thumbs_up, author, word, defid, current_vote, example, thumbs_down] x 7>,
85
+ # "sounds"=>[]}
86
+
87
+ pp res['list'].columns('word', 'thumbs_up', 'thumbs_down').to_a
88
+ # [{"word"=>"Trollface", "thumbs_up"=>418, "thumbs_down"=>99},
89
+ # {"word"=>"Troll Face", "thumbs_up"=>481, "thumbs_down"=>318},
90
+ # {"word"=>"Troll Face", "thumbs_up"=>340, "thumbs_down"=>184},
91
+ # {"word"=>"trollface", "thumbs_up"=>115, "thumbs_down"=>35},
92
+ # {"word"=>"trollface", "thumbs_up"=>94, "thumbs_down"=>30},
93
+ # {"word"=>"trollface", "thumbs_up"=>61, "thumbs_down"=>54},
94
+ # {"word"=>"Troll Face", "thumbs_up"=>81, "thumbs_down"=>181}]
95
+
96
+ pp d.random['list'].columns('word', 'example').first(3).to_a
97
+ # [{"word"=>"pH", "example"=>"Get that ph out of the shower.\r\n\r\n "},
98
+ # {"word"=>"spocking",
99
+ # "example"=>
100
+ # "The dirty bitch couldn’t get enough so I gave her a damn good spocking"},
101
+ # {"word"=>"mormon",
102
+ # "example"=>
103
+ # "Oh my gosh are the Smiths mormons?! We better have a party so they can bring some frickin' sweet green jello!..."}]
104
+
105
+ # That's it ¯\_(ツ)_/¯