tlaw 0.0.1

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.
@@ -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 ¯\_(ツ)_/¯