Really short version

For an “informational” HTTP APIs (like weather, geonames and similars, currencies, movies, transportations and so on) there are typically “wrapper” Ruby gems created. In most of the cases, they are mostly adding complexity and new concepts & abstractions, which are just blurring a task of access to data.

What I propose is instead of creating and using thick and independently maintained “wrapper gems”, we create a dedicated thin wrapper in each project. There is a new library, sarcastically named The Last API Wrapper (or tlaw for short) to help you define your own API wrappers in a concise, readable and declarative way.

So, next time you’ll need to integrate your Ruby/Rails project with some useful informational API, consider an option of creating your own small wrapper: with TLAW.

Longer version

Once Rubyist see a new useful HTTP API, her/his first thought would be “does it have a Ruby wrapper?”, and if not, the next would be “once, I should do a Ruby wrapper for this!”

Though, once it comes to practice, it turns out that find (or create) a decent API wrapper is not an easy task. The worst thing is, each API wrapper design challenge turns out to be some kind of “state of the art work” (while, in fact, it should be an easy routine). It could be easier when the wrapped API is described in some formal manner, but it is rare the case.

The story of four weathers

To understand what I am talking about here, let’s look at some examples of Ruby API wrappers for the same domain, and how different they are.

But first, some notes:

  1. I am talking only about information extraction (or “get”) APIs here; it is a limited sub-domain, that still deserves love and attention to approach;
  2. I will provide some real gems examples, and should state clearly that this article not intended to offend those gem authors in any way; it just shows some problems with approaches to API wrapper design;
  3. The article is “a quest with answers” (e.g. I believe that I know “how to do it right”); you can skip to an answer immediately or ignore it completely, I believe there are still some good points to think about.

Imagine you want to integrate some “weather informer” into your software. There are several open, free or cheap enough services, and probably you’d like to try some of them (or even integrate with several at once, if their coverage quality differs for different world parts). So, you look and see….

OpenWeatherMap

(After you’ve ruled out the difference between open_weather_map, open_weather and open-weather gems—the most recent is the latter):

OpenWeather::Current.city('Kharkiv', APPID: 'demo')
# => {"coord"=>{"lon"=>36.25, "lat"=>50}, "weather"=>[{"id"=>701, "main"=>"Mist", "description"=>"mist", "icon"=>"50n"}], "base"=>"stations", "main"=>{"temp"=>274.15, "pressure"=>1000, "humidity"=>97, "temp_min"=>274.15, "temp_max"=>274.15}, "visibility"=>5000, "wind"=>{"speed"=>6, "deg"=>220}, "clouds"=>{"all"=>90}, "dt"=>1487703600, "sys"=>{"type"=>1, "id"=>7355, "message"=>0.0059, "country"=>"UA", "sunrise"=>1487651560, "sunset"=>1487689506}, "id"=>706483, "name"=>"Kharkiv", "cod"=>200}
OpenWeather::Forecast.city('Kharkiv', APPID: 'demo')
# => {"city"=>{"id"=>706483, "name"=>"Kharkiv", "coord"=>{"lon"=>36.25, "lat"=>50}, "country"=>"UA", "population"=>0, "sys"=>{"population"=>0}}, "cod"=>"200", "message"=>0.075, "cnt"=>40, "list"=>[{"dt"=>1487710800, "main"=>{"temp"=>273.68, "temp_min"=>273.68, "temp_max"=>273.891, "pressure"=>992.85, "sea_level"=>1013.6, "grnd_level"=>992.85, "humidity"=>98, "temp_kf"=>-0.22}, "weather"=>[{"id"=>601, "main"=>"Snow", "description"=>"snow", "icon"=>"13n"}], "clouds"=>{"all"=>92}, "wind"=>{"speed"=>6.1, "deg"=>226.501}, "rain"=>{}, "snow"=>{"3h"=>2.333}, "sys"=>{"pod"=>"n"}, "dt_txt"=>"2017-02-21 21:00:00"}, {"dt"=>1487721600, "main"=>{"temp"=>274.07, "temp_min"=>274.07, "temp_max"=>274.209, "pressure"=>991.54, "sea_level"=>1012.3, "grnd_level"=>991.54, "humidity"=>96, "temp_kf"=>-0.14}, "weather"=>[{"id"=>500, "main"=>"Rain", "description"=>"light rain", "icon"=>"10n"}], "clouds"=>{"all"=>92}, "wind"=>{"speed"=>5.96, "deg"=>229.505}, "rain"=>{"3h"=>0.0075}, "snow"=>{"3h"=>1.017}, "sys"=>{"pod"=>"n"}, "dt_txt"=>"2017-02-22 00:00:00"}, {"dt"=>1487732400, "main"=>{"temp"=>274.19, "temp_min"=>274.19, "temp_max"=>274.257, "pressure"=>991.47, "sea_level"=>1012.16, "grnd_level"=>991.47, "humidity"=>98, "temp_kf"=>-0.07}, "weather"=>[{"id"=>500, "main"=>"Rain", "description"=>"light rain", "icon"=>"10n"}], "clouds"=>{"all"=>88}, "wind"=>{"speed"=>5.31, "deg"=>254.001}, "rain"=>{"3h"=>0.21}, "snow"=>{"3h"=>0.711}, "sys"=>{"pod"=>"n"}, "dt_txt"=>"2017-02-22 03:00:00"}, {"dt"=>1487743200, "main"=>{"temp"=>274.189, "temp_min"=>274.189, "temp_max"=>274.189, "pressure"=>992.58, "sea_level"=>1013.38, "grnd_level"=>992.58, "humidity"=>96, "temp_kf"=>0}, "weather"=>[{"id"=>500, "main"=>"Rain", "description"=>"light rain", "icon"=>"10d"}], "clouds"=>{"all"=>76}, "wind"=>{"speed"=>5.01, "deg"=>262.008}, "rain"=>{"3h"=>0.025}, "snow"=>{"3h"=>0.167}, "sys"=>{"pod"=>"d"}, "dt_txt"=>"2017-02-22 06:00:00"}, {"dt"=>1487754000, "main"=>{"temp"=>274.29, "temp_min"=>274.29, "temp_max"=>274.29, "pressure"=>993.5, "sea_level"=>1014.35, "grnd_level"=>993.5, "humidity"=>98, "temp_kf"=>0}, "weather"=>[{"id"=>600, "main"=>"Snow", "description"=>"light snow", "icon"=>"13d"}], "clouds"=>{"all"=>88}, "wind"=>{"speed"=>5.11, "deg"=>263.501}, "rain"=>{}, "snow"=>{"3h"=>0.106}, "sys"=>{"pod"=>"d"}, "dt_txt"=>"2017-02-22 09:00:00"}, {"dt"=>1487764800, "main"=>{"temp"=>274.211, "temp_min"=>274.211, "temp_max"=>274.211, "pressure"=>993.92, "sea_level"=>1014.68, "grnd_level"=>993.92, "humidity"=>96, "temp_kf"=>0}, "weather"=>[{"id"=>800, "main"=>"Clear", "description"=>"clear sky", "icon"=>"01d"}], "clouds"=>{"all"=>80}, "wind"=>{"speed"=>5.11, "deg"=>265}, "rain"=>{}, "snow"=>{"3h"=>0.0065}, "sys"=>{"pod"=>"d"}, "dt_txt"=>"2017-02-22 12:00:00"}, {"dt"=>1487775600, "main"=>{"temp"=>273.81, "temp_min"=>273.81, "temp_max"=>273.81, "pressure"=>995.28, "sea_level"=>1016.16, "grnd_level"=>995.28, "humidity"=>97, "temp_kf"=>0}, "weather"=>[{"id"=>600, "main"=>"Snow", "description"=>"light snow", "icon"=>"13d"}], "clouds"=>{"all"=>80}, "wind"=>{"speed"=>4.77, "deg"=>268.5}, "rain"=>{}, "snow"=>{"3h"=>0.0575}, "sys"=>{"pod"=>"d"}, "dt_txt"=>"2017-02-22 15:00:00"}, {"dt"=>1487786400, "main"=>{"temp"=>273.711, "temp_min"=>273.711, "temp_max"=>273.711, "pressure"=>996.65, "sea_level"=>1017.62, "grnd_level"=>996.65, "humidity"=>95, "temp_kf"=>0}, "weather"=>[{"id"=>600, "main"=>"Snow", "description"=>"light snow", "icon"=>"13n"}], "clouds"=>{"all"=>88}, "wind"=>{"speed"=>5, "deg"=>268.503}, "rain"=>{}, "snow"=>{"3h"=>0.0875}, "sys"=>{"pod"=>"n"}, "dt_txt"=>"2017-02-22 18:00:00"}, {"dt"=>1487797200, "main"=>{"temp"=>273.408, "temp_min"=>273.408, "temp_max"=>273.408, "pressure"=>997.27, "sea_level"=>1018.26, "grnd_level"=>997.27, "humidity"=>97, "temp_kf"=>0}, "weather"=>[{"id"=>600, "main"=>"Snow", "description"=>"light snow", "icon"=>"13n"}], "clouds"=>{"all"=>80}, "wind"=>{"speed"=>4.71, "deg"=>256.003}, "rain"=>{}, "snow"=>{"3h"=>0.06125}, "sys"=>{"pod"=>"n"}, "dt_txt"=>"2017-02-22 21:00:00"}, {"dt"=>1487808000, "main"=>{"temp"=>272.378, "temp_min"=>272.378, "temp_max"=>272.378, "pressure"=>996.11, "sea_level"=>1017.17, "grnd_level"=>996.11, "humidity"=>94, "temp_kf"=>0}, "weather"=>[{"id"=>600, "main"=>"Snow", "description"=>"light snow", "icon"=>"13n"}], "clouds"=>{"all"=>88}, "wind"=>{"speed"=>5.37, "deg"=>227.502}, "rain"=>{}, "snow"=>{"3h"=>0.04125}, "sys"=>{"pod"=>"n"}, "dt_txt"=>"2017-02-23 00:00:00"}, {"dt"=>1487818800, "main"=>{"temp"=>272.663, "temp_min"=>272.663, "temp_max"=>272.663, "pressure"=>992.25, "sea_level"=>1013.04, "grnd_level"=>992.25, "humidity"=>94, "temp_kf"=>0}, "weather"=>[{"id"=>600, "main"=>"Snow", "description"=>"light snow", "icon"=>"13n"}], "clouds"=>{"all"=>92}, "wind"=>{"speed"=>7.06, "deg"=>202.001}, "rain"=>{}, "snow"=>{"3h"=>1.2325}, "sys"=>{"pod"=>"n"}, "dt_txt"=>"2017-02-23 03:00:00"}, {"dt"=>1487829600, "main"=>{"temp"=>273.991, "temp_min"=>273.991, "temp_max"=>273.991, "pressure"=>989.49, "sea_level"=>1010.14, "grnd_level"=>989.49, "humidity"=>95, "temp_kf"=>0}, "weather"=>[{"id"=>601, "main"=>"Snow", "description"=>"snow", "icon"=>"13d"}], "clouds"=>{"all"=>92}, "wind"=>{"speed"=>7.16, "deg"=>225.511}, "rain"=>{}, "snow"=>{"3h"=>1.695}, "sys"=>{"pod"=>"d"}, "dt_txt"=>"2017-02-23 06:00:00"}, {"dt"=>1487840400, "main"=>{"temp"=>274.635, "temp_min"=>274.635, "temp_max"=>274.635, "pressure"=>988.5, "sea_level"=>1009.13, "grnd_level"=>988.5, "humidity"=>97, "temp_kf"=>0}, "weather"=>[{"id"=>500, "main"=>"Rain", "description"=>"light rain", "icon"=>"10d"}], "clouds"=>{"all"=>92}, "wind"=>{"speed"=>7.42, "deg"=>254.002}, "rain"=>{"3h"=>0.615}, "snow"=>{"3h"=>0.8425}, "sys"=>{"pod"=>"d"}, "dt_txt"=>"2017-02-23 09:00:00"}, {"dt"=>1487851200, "main"=>{"temp"=>274.906, "temp_min"=>274.906, "temp_max"=>274.906, "pressure"=>989.62, "sea_level"=>1010.23, "grnd_level"=>989.62, "humidity"=>96, "temp_kf"=>0}, "weather"=>[{"id"=>500, "main"=>"Rain", "description"=>"light rain", "icon"=>"10d"}], "clouds"=>{"all"=>80}, "wind"=>{"speed"=>7.06, "deg"=>271.5}, "rain"=>{"3h"=>0.16}, "snow"=>{}, "sys"=>{"pod"=>"d"}, "dt_txt"=>"2017-02-23 12:00:00"}, {"dt"=>1487862000, "main"=>{"temp"=>274.305, "temp_min"=>274.305, "temp_max"=>274.305, "pressure"=>992.74, "sea_level"=>1013.44, "grnd_level"=>992.74, "humidity"=>95, "temp_kf"=>0}, "weather"=>[{"id"=>500, "main"=>"Rain", "description"=>"light rain", "icon"=>"10d"}], "clouds"=>{"all"=>68}, "wind"=>{"speed"=>6.82, "deg"=>274.003}, "rain"=>{"3h"=>0.06}, "snow"=>{}, "sys"=>{"pod"=>"d"}, "dt_txt"=>"2017-02-23 15:00:00"}, {"dt"=>1487872800, "main"=>{"temp"=>274.26, "temp_min"=>274.26, "temp_max"=>274.26, "pressure"=>995.3, "sea_level"=>1016.17, "grnd_level"=>995.3, "humidity"=>94, "temp_kf"=>0}, "weather"=>[{"id"=>500, "main"=>"Rain", "description"=>"light rain", "icon"=>"10n"}], "clouds"=>{"all"=>88}, "wind"=>{"speed"=>6.61, "deg"=>269.505}, "rain"=>{"3h"=>0.145}, "snow"=>{"3h"=>0.0225}, "sys"=>{"pod"=>"n"}, "dt_txt"=>"2017-02-23 18:00:00"}, {"dt"=>1487883600, "main"=>{"temp"=>274.034, "temp_min"=>274.034, "temp_max"=>274.034, "pressure"=>997.16, "sea_level"=>1017.98, "grnd_level"=>997.16, "humidity"=>93, "temp_kf"=>0}, "weather"=>[{"id"=>800, "main"=>"Clear", "description"=>"clear sky", "icon"=>"01n"}], "clouds"=>{"all"=>76}, "wind"=>{"speed"=>5.21, "deg"=>260.001}, "rain"=>{}, "snow"=>{"3h"=>0.0025}, "sys"=>{"pod"=>"n"}, "dt_txt"=>"2017-02-23 21:00:00"}, {"dt"=>1487894400, "main"=>{"temp"=>273.624, "temp_min"=>273.624, "temp_max"=>273.624, "pressure"=>997.25, "sea_level"=>1018.12, "grnd_level"=>997.25, "humidity"=>93, "temp_kf"=>0}, "weather"=>[{"id"=>500, "main"=>"Rain", "description"=>"light rain", "icon"=>"10n"}], "clouds"=>{"all"=>56}, "wind"=>{"speed"=>4.31, "deg"=>228.006}, "rain"=>{"3h"=>0.01}, "snow"=>{}, "sys"=>{"pod"=>"n"}, "dt_txt"=>"2017-02-24 00:00:00"}, {"dt"=>1487905200, "main"=>{"temp"=>274.135, "temp_min"=>274.135, "temp_max"=>274.135, "pressure"=>995.97, "sea_level"=>1016.75, "grnd_level"=>995.97, "humidity"=>97, "temp_kf"=>0}, "weather"=>[{"id"=>500, "main"=>"Rain", "description"=>"light rain", "icon"=>"10n"}], "clouds"=>{"all"=>92}, "wind"=>{"speed"=>4.87, "deg"=>201.5}, "rain"=>{"3h"=>1.19}, "snow"=>{}, "sys"=>{"pod"=>"n"}, "dt_txt"=>"2017-02-24 03:00:00"}, {"dt"=>1487916000, "main"=>{"temp"=>275.031, "temp_min"=>275.031, "temp_max"=>275.031, "pressure"=>995.24, "sea_level"=>1015.91, "grnd_level"=>995.24, "humidity"=>93, "temp_kf"=>0}, "weather"=>[{"id"=>500, "main"=>"Rain", "description"=>"light rain", "icon"=>"10d"}], "clouds"=>{"all"=>56}, "wind"=>{"speed"=>5.81, "deg"=>224}, "rain"=>{"3h"=>0.39}, "snow"=>{}, "sys"=>{"pod"=>"d"}, "dt_txt"=>"2017-02-24 06:00:00"}, {"dt"=>1487926800, "main"=>{"temp"=>275.771, "temp_min"=>275.771, "temp_max"=>275.771, "pressure"=>994.97, "sea_level"=>1015.62, "grnd_level"=>994.97, "humidity"=>93, "temp_kf"=>0}, "weather"=>[{"id"=>802, "main"=>"Clouds", "description"=>"scattered clouds", "icon"=>"03d"}], "clouds"=>{"all"=>36}, "wind"=>{"speed"=>6.36, "deg"=>226}, "rain"=>{}, "snow"=>{}, "sys"=>{"pod"=>"d"}, "dt_txt"=>"2017-02-24 09:00:00"}, {"dt"=>1487937600, "main"=>{"temp"=>275.805, "temp_min"=>275.805, "temp_max"=>275.805, "pressure"=>994.12, "sea_level"=>1014.55, "grnd_level"=>994.12, "humidity"=>92, "temp_kf"=>0}, "weather"=>[{"id"=>802, "main"=>"Clouds", "description"=>"scattered clouds", "icon"=>"03d"}], "clouds"=>{"all"=>44}, "wind"=>{"speed"=>6.22, "deg"=>220.007}, "rain"=>{}, "snow"=>{}, "sys"=>{"pod"=>"d"}, "dt_txt"=>"2017-02-24 12:00:00"}, {"dt"=>1487948400, "main"=>{"temp"=>275.463, "temp_min"=>275.463, "temp_max"=>275.463, "pressure"=>994.04, "sea_level"=>1014.56, "grnd_level"=>994.04, "humidity"=>88, "temp_kf"=>0}, "weather"=>[{"id"=>803, "main"=>"Clouds", "description"=>"broken clouds", "icon"=>"04d"}], "clouds"=>{"all"=>56}, "wind"=>{"speed"=>6.38, "deg"=>227.001}, "rain"=>{}, "snow"=>{}, "sys"=>{"pod"=>"d"}, "dt_txt"=>"2017-02-24 15:00:00"}, {"dt"=>1487959200, "main"=>{"temp"=>275.483, "temp_min"=>275.483, "temp_max"=>275.483, "pressure"=>995.65, "sea_level"=>1016.13, "grnd_level"=>995.65, "humidity"=>92, "temp_kf"=>0}, "weather"=>[{"id"=>500, "main"=>"Rain", "description"=>"light rain", "icon"=>"10n"}], "clouds"=>{"all"=>92}, "wind"=>{"speed"=>5.47, "deg"=>253.501}, "rain"=>{"3h"=>0.035}, "snow"=>{}, "sys"=>{"pod"=>"n"}, "dt_txt"=>"2017-02-24 18:00:00"}, {"dt"=>1487970000, "main"=>{"temp"=>274.841, "temp_min"=>274.841, "temp_max"=>274.841, "pressure"=>997.27, "sea_level"=>1017.96, "grnd_level"=>997.27, "humidity"=>92, "temp_kf"=>0}, "weather"=>[{"id"=>500, "main"=>"Rain", "description"=>"light rain", "icon"=>"10n"}], "clouds"=>{"all"=>68}, "wind"=>{"speed"=>5.72, "deg"=>253.501}, "rain"=>{"3h"=>0.195}, "snow"=>{}, "sys"=>{"pod"=>"n"}, "dt_txt"=>"2017-02-24 21:00:00"}, {"dt"=>1487980800, "main"=>{"temp"=>274.228, "temp_min"=>274.228, "temp_max"=>274.228, "pressure"=>998.72, "sea_level"=>1019.62, "grnd_level"=>998.72, "humidity"=>94, "temp_kf"=>0}, "weather"=>[{"id"=>803, "main"=>"Clouds", "description"=>"broken clouds", "icon"=>"04n"}], "clouds"=>{"all"=>80}, "wind"=>{"speed"=>6.26, "deg"=>260.002}, "rain"=>{}, "snow"=>{}, "sys"=>{"pod"=>"n"}, "dt_txt"=>"2017-02-25 00:00:00"}, {"dt"=>1487991600, "main"=>{"temp"=>273.656, "temp_min"=>273.656, "temp_max"=>273.656, "pressure"=>1000.06, "sea_level"=>1020.9, "grnd_level"=>1000.06, "humidity"=>93, "temp_kf"=>0}, "weather"=>[{"id"=>800, "main"=>"Clear", "description"=>"clear sky", "icon"=>"01n"}], "clouds"=>{"all"=>92}, "wind"=>{"speed"=>5.06, "deg"=>259.502}, "rain"=>{}, "snow"=>{"3h"=>0.01}, "sys"=>{"pod"=>"n"}, "dt_txt"=>"2017-02-25 03:00:00"}, {"dt"=>1488002400, "main"=>{"temp"=>273.418, "temp_min"=>273.418, "temp_max"=>273.418, "pressure"=>1001.19, "sea_level"=>1022.11, "grnd_level"=>1001.19, "humidity"=>98, "temp_kf"=>0}, "weather"=>[{"id"=>600, "main"=>"Snow", "description"=>"light snow", "icon"=>"13d"}], "clouds"=>{"all"=>92}, "wind"=>{"speed"=>3.77, "deg"=>279.002}, "rain"=>{}, "snow"=>{"3h"=>0.33}, "sys"=>{"pod"=>"d"}, "dt_txt"=>"2017-02-25 06:00:00"}, {"dt"=>1488013200, "main"=>{"temp"=>273.147, "temp_min"=>273.147, "temp_max"=>273.147, "pressure"=>1002.52, "sea_level"=>1023.5, "grnd_level"=>1002.52, "humidity"=>97, "temp_kf"=>0}, "weather"=>[{"id"=>600, "main"=>"Snow", "description"=>"light snow", "icon"=>"13d"}], "clouds"=>{"all"=>92}, "wind"=>{"speed"=>4.36, "deg"=>317}, "rain"=>{}, "snow"=>{"3h"=>0.815}, "sys"=>{"pod"=>"d"}, "dt_txt"=>"2017-02-25 09:00:00"}, {"dt"=>1488024000, "main"=>{"temp"=>272.521, "temp_min"=>272.521, "temp_max"=>272.521, "pressure"=>1002.79, "sea_level"=>1023.87, "grnd_level"=>1002.79, "humidity"=>93, "temp_kf"=>0}, "weather"=>[{"id"=>600, "main"=>"Snow", "description"=>"light snow", "icon"=>"13d"}], "clouds"=>{"all"=>88}, "wind"=>{"speed"=>5.02, "deg"=>329.5}, "rain"=>{}, "snow"=>{"3h"=>0.32}, "sys"=>{"pod"=>"d"}, "dt_txt"=>"2017-02-25 12:00:00"}, {"dt"=>1488034800, "main"=>{"temp"=>270.954, "temp_min"=>270.954, "temp_max"=>270.954, "pressure"=>1004.72, "sea_level"=>1025.96, "grnd_level"=>1004.72, "humidity"=>90, "temp_kf"=>0}, "weather"=>[{"id"=>600, "main"=>"Snow", "description"=>"light snow", "icon"=>"13d"}], "clouds"=>{"all"=>48}, "wind"=>{"speed"=>4.12, "deg"=>315.006}, "rain"=>{}, "snow"=>{"3h"=>0.1}, "sys"=>{"pod"=>"d"}, "dt_txt"=>"2017-02-25 15:00:00"}, {"dt"=>1488045600, "main"=>{"temp"=>270.182, "temp_min"=>270.182, "temp_max"=>270.182, "pressure"=>1007.47, "sea_level"=>1028.86, "grnd_level"=>1007.47, "humidity"=>89, "temp_kf"=>0}, "weather"=>[{"id"=>600, "main"=>"Snow", "description"=>"light snow", "icon"=>"13n"}], "clouds"=>{"all"=>48}, "wind"=>{"speed"=>3.97, "deg"=>306.002}, "rain"=>{}, "snow"=>{"3h"=>0.045}, "sys"=>{"pod"=>"n"}, "dt_txt"=>"2017-02-25 18:00:00"}, {"dt"=>1488056400, "main"=>{"temp"=>270.061, "temp_min"=>270.061, "temp_max"=>270.061, "pressure"=>1009.16, "sea_level"=>1030.63, "grnd_level"=>1009.16, "humidity"=>90, "temp_kf"=>0}, "weather"=>[{"id"=>600, "main"=>"Snow", "description"=>"light snow", "icon"=>"13n"}], "clouds"=>{"all"=>80}, "wind"=>{"speed"=>3.68, "deg"=>286.002}, "rain"=>{}, "snow"=>{"3h"=>0.08}, "sys"=>{"pod"=>"n"}, "dt_txt"=>"2017-02-25 21:00:00"}, {"dt"=>1488067200, "main"=>{"temp"=>270.491, "temp_min"=>270.491, "temp_max"=>270.491, "pressure"=>1009.71, "sea_level"=>1031.31, "grnd_level"=>1009.71, "humidity"=>90, "temp_kf"=>0}, "weather"=>[{"id"=>600, "main"=>"Snow", "description"=>"light snow", "icon"=>"13n"}], "clouds"=>{"all"=>80}, "wind"=>{"speed"=>3.97, "deg"=>256.503}, "rain"=>{}, "snow"=>{"3h"=>0.065}, "sys"=>{"pod"=>"n"}, "dt_txt"=>"2017-02-26 00:00:00"}, {"dt"=>1488078000, "main"=>{"temp"=>270.756, "temp_min"=>270.756, "temp_max"=>270.756, "pressure"=>1009.28, "sea_level"=>1030.93, "grnd_level"=>1009.28, "humidity"=>91, "temp_kf"=>0}, "weather"=>[{"id"=>800, "main"=>"Clear", "description"=>"clear sky", "icon"=>"01n"}], "clouds"=>{"all"=>36}, "wind"=>{"speed"=>4.61, "deg"=>227.501}, "rain"=>{}, "snow"=>{"3h"=>0.03}, "sys"=>{"pod"=>"n"}, "dt_txt"=>"2017-02-26 03:00:00"}, {"dt"=>1488088800, "main"=>{"temp"=>269.44, "temp_min"=>269.44, "temp_max"=>269.44, "pressure"=>1008.91, "sea_level"=>1030.47, "grnd_level"=>1008.91, "humidity"=>90, "temp_kf"=>0}, "weather"=>[{"id"=>800, "main"=>"Clear", "description"=>"clear sky", "icon"=>"02d"}], "clouds"=>{"all"=>8}, "wind"=>{"speed"=>5.82, "deg"=>214.005}, "rain"=>{}, "snow"=>{}, "sys"=>{"pod"=>"d"}, "dt_txt"=>"2017-02-26 06:00:00"}, {"dt"=>1488099600, "main"=>{"temp"=>272.427, "temp_min"=>272.427, "temp_max"=>272.427, "pressure"=>1007.23, "sea_level"=>1028.33, "grnd_level"=>1007.23, "humidity"=>94, "temp_kf"=>0}, "weather"=>[{"id"=>801, "main"=>"Clouds", "description"=>"few clouds", "icon"=>"02d"}], "clouds"=>{"all"=>20}, "wind"=>{"speed"=>7.53, "deg"=>214.002}, "rain"=>{}, "snow"=>{}, "sys"=>{"pod"=>"d"}, "dt_txt"=>"2017-02-26 09:00:00"}, {"dt"=>1488110400, "main"=>{"temp"=>273.773, "temp_min"=>273.773, "temp_max"=>273.773, "pressure"=>1003.62, "sea_level"=>1024.55, "grnd_level"=>1003.62, "humidity"=>93, "temp_kf"=>0}, "weather"=>[{"id"=>803, "main"=>"Clouds", "description"=>"broken clouds", "icon"=>"04d"}], "clouds"=>{"all"=>56}, "wind"=>{"speed"=>9.12, "deg"=>207}, "rain"=>{}, "snow"=>{}, "sys"=>{"pod"=>"d"}, "dt_txt"=>"2017-02-26 12:00:00"}, {"dt"=>1488121200, "main"=>{"temp"=>273.546, "temp_min"=>273.546, "temp_max"=>273.546, "pressure"=>1001.14, "sea_level"=>1022.09, "grnd_level"=>1001.14, "humidity"=>89, "temp_kf"=>0}, "weather"=>[{"id"=>600, "main"=>"Snow", "description"=>"light snow", "icon"=>"13d"}], "clouds"=>{"all"=>92}, "wind"=>{"speed"=>9.61, "deg"=>215.002}, "rain"=>{}, "snow"=>{"3h"=>0.205}, "sys"=>{"pod"=>"d"}, "dt_txt"=>"2017-02-26 15:00:00"}, {"dt"=>1488132000, "main"=>{"temp"=>274.137, "temp_min"=>274.137, "temp_max"=>274.137, "pressure"=>999.57, "sea_level"=>1020.54, "grnd_level"=>999.57, "humidity"=>92, "temp_kf"=>0}, "weather"=>[{"id"=>500, "main"=>"Rain", "description"=>"light rain", "icon"=>"10n"}], "clouds"=>{"all"=>92}, "wind"=>{"speed"=>9.51, "deg"=>222.003}, "rain"=>{"3h"=>0.01}, "snow"=>{"3h"=>0.96}, "sys"=>{"pod"=>"n"}, "dt_txt"=>"2017-02-26 18:00:00"}]}

Implementation choices:

  • all foo/bar API endpoints are wrapped into Foo.bar module methods;
  • you should pass to each call even those parameters that are “constant” for your app (API key and settings);
  • the output is naked Ruby Hash, with no data parsed/converted (even timestamps), just a result of JSON.parse for an API output.

Forecast.io

ForecastIO.api_key = 'this-is-your-api-key'
ForecastIO.forecast(50.004444, 36.231389, params: {units: 'si'})
 => #<Hashie::Mash currently=#<Hashie::Mash apparentTemperature=-6.31 cloudCover=1 dewPoint=-1.46 humidity=0.93 icon="snow" ozone=329.04 precipIntensity=0.2438 precipProbability=0.49 precipType="snow" pressure=1000.47 summary="Light Snow" temperature=-0.49 time=1487709672 windBearing=225 windSpeed=6.49> daily=#<Hashie::Mash data=#<Hashie::Array [#<Hashie::Mash apparentTemperatureMax=-3.13 apparentTemperatureMaxTime=1487685600 apparentTemperatureMin=-7.11 apparentTemperatureMinTime=1487656800 cloudCover=0.88 dewPoint=-0.49 humidity=0.95 icon="snow" moonPhase=0.83 ozone=306.65 precipAccumulation=1.128 precipIntensity=0.061 precipIntensityMax=0.254 precipIntensityMaxTime=1487710800 precipProbability=0.5 precipType="snow" pressure=1005.9 summary="Light snow (1–2 cm.) starting in the evening." sunriseTime=1487651757 sunsetTime=1487689462 temperatureMax=1.99 temperatureMaxTime=1487685600 temperatureMin=-1.67 temperatureMinTime=1487656800 time=1487628000 visibility=3.85 windBearing=223 windSpeed=5.66>, #<Hashie::Mash apparentTemperatureMax=-2.07 apparentTemperatureMaxTime=1487772000 apparentTemperatureMin=-6.72 apparentTemperatureMinTime=1487718000 cloudCover=0.99 dewPoint=-0.4 humidity=0.97 icon="snow" moonPhase=0.86 ozone=362.11 precipAccumulation=2.024 precipIntensity=0.0991 precipIntensityMax=0.2769 precipIntensityMaxTime=1487718000 precipProbability=0.51 precipType="snow" pressure=1001.75 summary="Light snow (under 1 cm.) in the morning." sunriseTime=1487738041 sunsetTime=1487775964 temperatureMax=2.21 temperatureMaxTime=1487772000 temperatureMin=-1.21 temperatureMinTime=1487797200 time=1487714400 windBearing=252 windSpeed=5.1>, #<Hashie::Mash apparentTemperatureMax=-2.05 apparentTemperatureMaxTime=1487851200 apparentTemperatureMin=-8.18 apparentTemperatureMinTime=1487815200 cloudCover=0.83 dewPoint=-0.24 humidity=0.96 icon="rain" moonPhase=0.89 ozone=378.48 precipAccumulation=5.017 precipIntensity=0.2616 precipIntensityMax=0.7544 precipIntensityMaxTime=1487840400 precipProbability=0.65 precipType="snow" pressure=1000.17 summary="Mixed precipitation until afternoon." sunriseTime=1487824324 sunsetTime=1487862466 temperatureMax=3.48 temperatureMaxTime=1487851200 temperatureMin=-2 temperatureMinTime=1487808000 time=1487800800 windBearing=244 windSpeed=6.33>, #<Hashie::Mash apparentTemperatureMax=3.1 apparentTemperatureMaxTime=1487941200 apparentTemperatureMin=-8.13 apparentTemperatureMinTime=1487894400 cloudCover=0.72 dewPoint=1.58 humidity=0.95 icon="snow" moonPhase=0.92 ozone=355 precipIntensity=0.0279 precipIntensityMax=0.1295 precipIntensityMaxTime=1487916000 precipProbability=0.21 precipType="rain" pressure=1002.25 summary="Light snow in the morning." sunriseTime=1487910605 sunsetTime=1487948967 temperatureMax=7.21 temperatureMaxTime=1487941200 temperatureMin=-2.63 temperatureMinTime=1487894400 time=1487887200 windBearing=230 windSpeed=6.7>, #<Hashie::Mash apparentTemperatureMax=-1.83 apparentTemperatureMaxTime=1487973600 apparentTemperatureMin=-8.62 apparentTemperatureMinTime=1488002400 cloudCover=0.76 dewPoint=-3.18 humidity=0.85 icon="snow" moonPhase=0.96 ozone=401.9 precipAccumulation=1.11 precipIntensity=0.0508 precipIntensityMax=0.1041 precipIntensityMaxTime=1488020400 precipProbability=0.16 precipType="snow" pressure=1009.24 summary="Light snow (under 1 cm.) until evening." sunriseTime=1487996886 sunsetTime=1488035468 temperatureMax=2.87 temperatureMaxTime=1487973600 temperatureMin=-2.88 temperatureMinTime=1488056400 time=1487973600 windBearing=274 windSpeed=4.47>, #<Hashie::Mash apparentTemperatureMax=-2.36 apparentTemperatureMaxTime=1488132000 apparentTemperatureMin=-10.52 apparentTemperatureMinTime=1488067200 cloudCover=0.7 dewPoint=-2.12 humidity=0.88 icon="snow" moonPhase=0.99 ozone=351.04 precipAccumulation=1.628 precipIntensity=0.0864 precipIntensityMax=0.2591 precipIntensityMaxTime=1488106800 precipProbability=0.5 precipType="snow" pressure=1011.47 summary="Mixed precipitation until afternoon and breezy in the afternoon." sunriseTime=1488083165 sunsetTime=1488121969 temperatureMax=3.34 temperatureMaxTime=1488132000 temperatureMin=-4.63 temperatureMinTime=1488067200 time=1488060000 windBearing=241 windSpeed=7.32>, #<Hashie::Mash apparentTemperatureMax=-0.81 apparentTemperatureMaxTime=1488200400 apparentTemperatureMin=-6.73 apparentTemperatureMinTime=1488164400 cloudCover=0.73 dewPoint=-0.32 humidity=0.9 icon="snow" moonPhase=0.03 ozone=340.9 precipAccumulation=1.407 precipIntensity=0.0737 precipIntensityMax=0.2362 precipIntensityMaxTime=1488164400 precipProbability=0.49 precipType="snow" pressure=1011.43 summary="Light snow (under 1 cm.) in the morning." sunriseTime=1488169444 sunsetTime=1488208469 temperatureMax=3.46 temperatureMaxTime=1488200400 temperatureMin=-0.59 temperatureMinTime=1488168000 time=1488146400 windBearing=272 windSpeed=5.99>, #<Hashie::Mash apparentTemperatureMax=3.24 apparentTemperatureMaxTime=1488286800 apparentTemperatureMin=-6.45 apparentTemperatureMinTime=1488250800 cloudCover=0.56 dewPoint=0.67 humidity=0.9 icon="partly-cloudy-day" moonPhase=0.06 ozone=298.8 precipIntensity=0 precipIntensityMax=0 precipProbability=0 pressure=1020.19 summary="Mostly cloudy throughout the day." sunriseTime=1488255722 sunsetTime=1488294969 temperatureMax=6.31 temperatureMaxTime=1488286800 temperatureMin=-2.17 temperatureMinTime=1488250800 time=1488232800 windBearing=245 windSpeed=3.91>]> icon="snow" summary="Mixed precipitation today through Monday, with temperatures rising to 7°C on Friday."> flags=#<Hashie::Mash isd-stations=#<Hashie::Array ["333820-99999", "335060-99999", "341090-99999", "342020-99999", "342130-99999", "342140-99999", "343000-99999", "343020-99999", "343040-99999", "343070-99999", "343120-99999", "343190-99999", "343210-99999", "344010-99999", "344090-99999", "344150-99999"]> madis-stations=#<Hashie::Array ["UKHH"]> sources=#<Hashie::Array ["gfs", "cmc", "fnmoc", "isd", "madis"]> units="si"> hourly=#<Hashie::Mash data=#<Hashie::Array [#<Hashie::Mash apparentTemperature=-6.08 cloudCover=1 dewPoint=-1.25 humidity=0.93 icon="snow" ozone=328.43 precipAccumulation=0.178 precipIntensity=0.2235 precipProbability=0.46 precipType="snow" pressure=1000.69 summary="Light Snow" temperature=-0.27 time=1487707200 windBearing=225 windSpeed=6.61>, #<Hashie::Mash apparentTemperature=-6.41 cloudCover=1 dewPoint=-1.54 humidity=0.93 icon="snow" ozone=329.32 precipAccumulation=0.211 precipIntensity=0.254 precipProbability=0.5 precipType="snow" pressure=1000.37 summary="Light Snow" temperature=-0.58 time=1487710800 windBearing=226 windSpeed=6.44>, #<Hashie::Mash apparentTemperature=-6.64 cloudCover=1 dewPoint=-1.75 humidity=0.93 icon="snow" ozone=330.37 precipAccumulation=0.231 precipIntensity=0.2692 precipProbability=0.51 precipType="snow" pressure=1000.02 summary="Light Snow" temperature=-0.83 time=1487714400 windBearing=228 windSpeed=6.27>, #<Hashie::Mash apparentTemperature=-6.72 cloudCover=1 dewPoint=-1.81 humidity=0.94 icon="snow" ozone=331.67 precipAccumulation=0.239 precipIntensity=0.2769 precipProbability=0.51 precipType="snow" pressure=999.69 summary="Light Snow" temperature=-0.96 time=1487718000 windBearing=230 windSpeed=6.1>, #<Hashie::Mash apparentTemperature=-6.65 cloudCover=1 dewPoint=-1.68 humidity=0.95 icon="snow" ozone=334.2 precipAccumulation=0.234 precipIntensity=0.2692 precipProbability=0.51 precipType="snow" pressure=999.45 summary="Light Snow" temperature=-0.96 time=1487721600 windBearing=233 windSpeed=5.97>, #<Hashie::Mash apparentTemperature=-6.57 cloudCover=1 dewPoint=-1.49 humidity=0.96 icon="snow" ozone=338.81 precipAccumulation=0.206 precipIntensity=0.2388 precipProbability=0.49 precipType="snow" pressure=999.32 summary="Light Snow" temperature=-0.92 time=1487725200 windBearing=235 windSpeed=5.91>, #<Hashie::Mash apparentTemperature=-6.65 cloudCover=1 dewPoint=-1.24 humidity=0.98 icon="snow" ozone=344.65 precipAccumulation=0.17 precipIntensity=0.193 precipProbability=0.38 precipType="snow" pressure=999.28 summary="Light Snow" temperature=-0.99 time=1487728800 windBearing=238 windSpeed=5.89>, #<Hashie::Mash apparentTemperature=-6.57 cloudCover=1 dewPoint=-1.13 humidity=0.99 icon="snow" ozone=350.17 precipAccumulation=0.135 precipIntensity=0.1575 precipProbability=0.28 precipType="snow" pressure=999.36 summary="Light Snow" temperature=-0.93 time=1487732400 windBearing=241 windSpeed=5.87>, #<Hashie::Mash apparentTemperature=-6.53 cloudCover=1 dewPoint=-1.06 humidity=0.99 icon="snow" ozone=354.84 precipAccumulation=0.117 precipIntensity=0.1346 precipProbability=0.23 precipType="snow" pressure=999.62 summary="Light Snow" temperature=-0.89 time=1487736000 windBearing=244 windSpeed=5.88>, #<Hashie::Mash apparentTemperature=-6.51 cloudCover=1 dewPoint=-1.02 humidity=0.99 icon="snow" ozone=359.19 precipAccumulation=0.107 precipIntensity=0.1219 precipProbability=0.2 precipType="snow" pressure=999.97 summary="Light Snow" temperature=-0.87 time=1487739600 windBearing=248 windSpeed=5.91>, #<Hashie::Mash apparentTemperature=-6.42 cloudCover=0.99 dewPoint=-0.98 humidity=0.99 icon="snow" ozone=363.3 precipAccumulation=0.094 precipIntensity=0.1092 precipProbability=0.17 precipType="snow" pressure=1000.3 summary="Light Snow" temperature=-0.84 time=1487743200 windBearing=251 windSpeed=5.82>, #<Hashie::Mash apparentTemperature=-6.08 cloudCover=0.99 dewPoint=-0.87 humidity=0.99 icon="snow" ozone=367.12 precipAccumulation=0.074 precipIntensity=0.0864 precipProbability=0.12 precipType="snow" pressure=1000.6 summary="Light Snow" temperature=-0.73 time=1487746800 windBearing=254 windSpeed=5.44>, #<Hashie::Mash apparentTemperature=-5.46 cloudCover=0.99 dewPoint=-0.63 humidity=0.99 icon="snow" ozone=370.71 precipAccumulation=0.051 precipIntensity=0.0635 precipProbability=0.07 precipType="snow" pressure=1000.9 summary="Flurries" temperature=-0.46 time=1487750400 windBearing=257 windSpeed=4.94>, #<Hashie::Mash apparentTemperature=-4.74 cloudCover=0.99 dewPoint=-0.24 humidity=0.99 icon="cloudy" ozone=374.29 precipAccumulation=0.036 precipIntensity=0.0457 precipProbability=0.04 precipType="snow" pressure=1001.18 summary="Overcast" temperature=-0.07 time=1487754000 windBearing=259 windSpeed=4.58>, #<Hashie::Mash apparentTemperature=-4.03 cloudCover=0.99 dewPoint=0.27 humidity=0.99 icon="cloudy" ozone=378.56 precipAccumulation=0.028 precipIntensity=0.0381 precipProbability=0.03 precipType="snow" pressure=1001.41 summary="Overcast" temperature=0.46 time=1487757600 windBearing=257 windSpeed=4.47>, #<Hashie::Mash apparentTemperature=-3.31 cloudCover=1 dewPoint=0.87 humidity=0.99 icon="cloudy" ozone=382.81 precipAccumulation=0.028 precipIntensity=0.0381 precipProbability=0.03 precipType="snow" pressure=1001.61 summary="Overcast" temperature=1.06 time=1487761200 windBearing=253 windSpeed=4.52>, #<Hashie::Mash apparentTemperature=-2.68 cloudCover=0.99 dewPoint=1.41 humidity=0.99 icon="cloudy" ozone=385.1 precipAccumulation=0.028 precipIntensity=0.0381 precipProbability=0.03 precipType="snow" pressure=1001.88 summary="Overcast" temperature=1.61 time=1487764800 windBearing=251 windSpeed=4.6>, #<Hashie::Mash apparentTemperature=-2.23 cloudCover=0.98 dewPoint=1.78 humidity=0.98 icon="cloudy" ozone=384.46 precipIntensity=0.0406 precipProbability=0.03 precipType="rain" pressure=1002.3 summary="Overcast" temperature=2.01 time=1487768400 windBearing=253 windSpeed=4.7>, #<Hashie::Mash apparentTemperature=-2.07 cloudCover=0.97 dewPoint=1.94 humidity=0.98 icon="cloudy" ozone=381.85 precipIntensity=0.0432 precipProbability=0.04 precipType="rain" pressure=1002.81 summary="Overcast" temperature=2.21 time=1487772000 windBearing=257 windSpeed=4.84>, #<Hashie::Mash apparentTemperature=-2.46 cloudCover=0.96 dewPoint=1.63 humidity=0.98 icon="cloudy" ozone=378.27 precipIntensity=0.0457 precipProbability=0.04 precipType="rain" pressure=1003.35 summary="Overcast" temperature=1.96 time=1487775600 windBearing=262 windSpeed=4.98>, #<Hashie::Mash apparentTemperature=-3.17 cloudCover=0.96 dewPoint=1.05 humidity=0.97 icon="cloudy" ozone=373.33 precipAccumulation=0.033 precipIntensity=0.0432 precipProbability=0.04 precipType="snow" pressure=1003.9 summary="Overcast" temperature=1.47 time=1487779200 windBearing=269 windSpeed=5.17>, #<Hashie::Mash apparentTemperature=-4.03 cloudCover=0.96 dewPoint=0.36 humidity=0.96 icon="cloudy" ozone=367.42 precipAccumulation=0.033 precipIntensity=0.0406 precipProbability=0.03 precipType="snow" pressure=1004.43 summary="Overcast" temperature=0.9 time=1487782800 windBearing=277 windSpeed=5.44>, #<Hashie::Mash apparentTemperature=-4.79 cloudCover=0.96 dewPoint=-0.32 humidity=0.95 icon="cloudy" ozone=362.72 precipAccumulation=0.028 precipIntensity=0.0381 precipProbability=0.03 precipType="snow" pressure=1004.84 summary="Overcast" temperature=0.32 time=1487786400 windBearing=280 windSpeed=5.51>, #<Hashie::Mash apparentTemperature=-5.31 cloudCover=0.97 dewPoint=-1.01 humidity=0.95 icon="cloudy" ozone=360.09 precipAccumulation=0.025 precipIntensity=0.0305 precipProbability=0.02 precipType="snow" pressure=1005.12 summary="Overcast" temperature=-0.27 time=1487790000 windBearing=277 windSpeed=5.11>, #<Hashie::Mash apparentTemperature=-5.62 cloudCover=0.98 dewPoint=-1.63 humidity=0.94 icon="cloudy" ozone=358.68 precipAccumulation=0.018 precipIntensity=0.0229 precipProbability=0.01 precipType="snow" pressure=1005.3 summary="Overcast" temperature=-0.82 time=1487793600 windBearing=269 windSpeed=4.49>, #<Hashie::Mash apparentTemperature=-5.88 cloudCover=0.99 dewPoint=-2.06 humidity=0.94 icon="cloudy" ozone=358.06 precipIntensity=0 precipProbability=0 pressure=1005.34 summary="Overcast" temperature=-1.21 time=1487797200 windBearing=256 windSpeed=4.15>, #<Hashie::Mash apparentTemperature=-6.38 cloudCover=1 dewPoint=-2.32 humidity=0.94 icon="cloudy" ozone=358.25 precipIntensity=0 precipProbability=0 pressure=1005.23 summary="Overcast" temperature=-1.5 time=1487800800 windBearing=241 windSpeed=4.36>, #<Hashie::Mash apparentTemperature=-7.17 cloudCover=1 dewPoint=-2.54 humidity=0.95 icon="cloudy" ozone=359.23 precipIntensity=0 precipProbability=0 pressure=1004.97 summary="Overcast" temperature=-1.79 time=1487804400 windBearing=228 windSpeed=5.03>, #<Hashie::Mash apparentTemperature=-7.89 cloudCover=0.99 dewPoint=-2.71 humidity=0.95 icon="cloudy" ozone=360.49 precipIntensity=0 precipProbability=0 pressure=1004.46 summary="Overcast" temperature=-2 time=1487808000 windBearing=220 windSpeed=5.81>, #<Hashie::Mash apparentTemperature=-8.12 cloudCover=0.99 dewPoint=-2.59 humidity=0.95 icon="snow" ozone=361.85 precipAccumulation=0.109 precipIntensity=0.1118 precipProbability=0.17 precipType="snow" pressure=1003.54 summary="Light Snow" temperature=-1.88 time=1487811600 windBearing=216 windSpeed=6.55>, #<Hashie::Mash apparentTemperature=-8.18 cloudCover=0.99 dewPoint=-2.39 humidity=0.95 icon="snow" ozone=363.49 precipAccumulation=0.231 precipIntensity=0.2438 precipProbability=0.49 precipType="snow" pressure=1002.3 summary="Light Snow" temperature=-1.67 time=1487815200 windBearing=214 windSpeed=7.25>, #<Hashie::Mash apparentTemperature=-8.11 cloudCover=0.98 dewPoint=-2.17 humidity=0.95 icon="snow" ozone=365.49 precipAccumulation=0.315 precipIntensity=0.3404 precipProbability=0.54 precipType="snow" pressure=1001.06 summary="Light Snow" temperature=-1.48 time=1487818800 windBearing=214 windSpeed=7.64>, #<Hashie::Mash apparentTemperature=-7.84 cloudCover=0.99 dewPoint=-1.88 humidity=0.96 icon="snow" ozone=368.06 precipAccumulation=0.307 precipIntensity=0.3378 precipProbability=0.54 precipType="snow" pressure=999.99 summary="Light Snow" temperature=-1.34 time=1487822400 windBearing=215 windSpeed=7.46>, #<Hashie::Mash apparentTemperature=-7.37 cloudCover=0.99 dewPoint=-1.48 humidity=0.98 icon="snow" ozone=371 precipAccumulation=0.259 precipIntensity=0.2921 precipProbability=0.52 precipType="snow" pressure=999.04 summary="Light Snow" temperature=-1.14 time=1487826000 windBearing=218 windSpeed=6.97>, #<Hashie::Mash apparentTemperature=-6.81 cloudCover=0.99 dewPoint=-1 humidity=0.99 icon="snow" ozone=373.76 precipAccumulation=0.254 precipIntensity=0.2997 precipProbability=0.52 precipType="snow" pressure=998.2 summary="Light Snow" temperature=-0.81 time=1487829600 windBearing=222 windSpeed=6.67>, #<Hashie::Mash apparentTemperature=-6.12 cloudCover=1 dewPoint=-0.37 humidity=0.99 icon="snow" ozone=375.79 precipAccumulation=0.345 precipIntensity=0.4369 precipProbability=0.58 precipType="snow" pressure=997.29 summary="Light Snow" temperature=-0.23 time=1487833200 windBearing=223 windSpeed=6.82>, #<Hashie::Mash apparentTemperature=-5.21 cloudCover=1 dewPoint=0.45 humidity=0.99 icon="rain" ozone=377.63 precipIntensity=0.6325 precipProbability=0.63 precipType="rain" pressure=996.25 summary="Light Rain" temperature=0.59 time=1487836800 windBearing=225 windSpeed=7.14>, #<Hashie::Mash apparentTemperature=-4.16 cloudCover=1 dewPoint=1.33 humidity=0.99 icon="rain" ozone=380.42 precipIntensity=0.7544 precipProbability=0.65 precipType="rain" pressure=995.5 summary="Light Rain" temperature=1.49 time=1487840400 windBearing=230 windSpeed=7.39>, #<Hashie::Mash apparentTemperature=-3.17 cloudCover=1 dewPoint=2.23 humidity=0.99 icon="rain" ozone=385.35 precipIntensity=0.7493 precipProbability=0.65 precipType="rain" pressure=995.05 summary="Light Rain" temperature=2.36 time=1487844000 windBearing=239 windSpeed=7.74>, #<Hashie::Mash apparentTemperature=-2.43 cloudCover=1 dewPoint=2.98 humidity=0.99 icon="rain" ozone=391.21 precipIntensity=0.6756 precipProbability=0.63 precipType="rain" pressure=994.85 summary="Light Rain" temperature=3.09 time=1487847600 windBearing=250 windSpeed=8.36>, #<Hashie::Mash apparentTemperature=-2.05 cloudCover=1 dewPoint=3.31 humidity=0.99 icon="rain" ozone=395.56 precipIntensity=0.5664 precipProbability=0.61 precipType="rain" pressure=995.12 summary="Light Rain" temperature=3.48 time=1487851200 windBearing=258 windSpeed=8.75>, #<Hashie::Mash apparentTemperature=-2.28 cloudCover=0.85 dewPoint=2.81 humidity=0.97 icon="rain" ozone=397.38 precipIntensity=0.4089 precipProbability=0.57 precipType="rain" pressure=996.16 summary="Light Rain" temperature=3.22 time=1487854800 windBearing=265 windSpeed=8.4>, #<Hashie::Mash apparentTemperature=-2.81 cloudCover=0.66 dewPoint=1.88 humidity=0.95 icon="rain" ozone=397.67 precipIntensity=0.2159 precipProbability=0.44 precipType="rain" pressure=997.79 summary="Drizzle" temperature=2.62 time=1487858400 windBearing=273 windSpeed=7.68>, #<Hashie::Mash apparentTemperature=-3.44 cloudCover=0.53 dewPoint=1.02 humidity=0.93 icon="partly-cloudy-day" ozone=397.02 precipIntensity=0.0711 precipProbability=0.09 precipType="rain" pressure=999.44 summary="Partly Cloudy" temperature=1.96 time=1487862000 windBearing=278 windSpeed=7.14>, #<Hashie::Mash apparentTemperature=-3.94 cloudCover=0.58 dewPoint=0.59 humidity=0.94 icon="partly-cloudy-night" ozone=395.87 precipAccumulation=0.018 precipIntensity=0.0229 precipProbability=0.01 precipType="snow" pressure=1000.71 summary="Partly Cloudy" temperature=1.48 time=1487865600 windBearing=278 windSpeed=6.85>, #<Hashie::Mash apparentTemperature=-4.28 cloudCover=0.7 dewPoint=0.44 humidity=0.95 icon="partly-cloudy-night" ozone=393.78 precipAccumulation=0.02 precipIntensity=0.0254 precipProbability=0.01 precipType="snow" pressure=1001.85 summary="Mostly Cloudy" temperature=1.14 time=1487869200 windBearing=274 windSpeed=6.61>, #<Hashie::Mash apparentTemperature=-4.65 cloudCover=0.72 dewPoint=0.2 humidity=0.96 icon="partly-cloudy-night" ozone=389.92 precipAccumulation=0.023 precipIntensity=0.0305 precipProbability=0.02 precipType="snow" pressure=1002.81 summary="Mostly Cloudy" temperature=0.78 time=1487872800 windBearing=270 windSpeed=6.4>, #<Hashie::Mash apparentTemperature=-5.29 cloudCover=0.54 dewPoint=-0.42 humidity=0.96 icon="partly-cloudy-night" ozone=383.02 precipAccumulation=0.015 precipIntensity=0.0203 precipProbability=0.01 precipType="snow" pressure=1003.6 summary="Partly Cloudy" temperature=0.22 time=1487876400 windBearing=267 windSpeed=6.23>, #<Hashie::Mash apparentTemperature=-6.07 cloudCover=0.26 dewPoint=-1.22 humidity=0.95 icon="partly-cloudy-night" ozone=374.36 precipIntensity=0 precipProbability=0 pressure=1004.24 summary="Partly Cloudy" temperature=-0.46 time=1487880000 windBearing=263 windSpeed=6.07>]> icon="snow" summary="Light snow (1–3 cm.) until tomorrow morning."> latitude=50.004444 longitude=36.231389 offset=2 timezone="Europe/Kiev">

 ForecastIO.forecast(50.004444, 36.231389, time: Time.new(2017,7,1).to_i, params: {units: 'si'})
# => output is essentially the same

Implementation choices:

  • As API has one “REST”-y endpoint for all requests (future and past weather), wrapper also represents it as one method;
  • API key is global—the common sorry solution for many libraries, making it is hard to use in complicated environments (like several API keys depending on context, user or server load);
  • All additional parameters should be passed to each request “as is”, and it is required to study source API docs to know what to pass;
  • though time for forecast/history is passed explicitly, it is your’s, not library’s duty to prepare integer timestamp;
  • Result is wrapped in Hashie, which makes a bit easier to say something like response.currently.humidity.

Weather Underground

weather = Wunderground.new("your apikey")
weather.conditions_and_astronomy_for('Kharkiv')
# => {"response"=>{"version"=>"0.1", "termsofService"=>"http://www.wunderground.com/weather/api/d/terms.html", "features"=>{"conditions"=>1, "astronomy"=>1}}, "current_observation"=>{"image"=>{"url"=>"http://icons.wxug.com/graphics/wu2/logo_130x80.png", "title"=>"Weather Underground", "link"=>"http://www.wunderground.com"}, "display_location"=>{"full"=>"Kharkiv, Ukraine", "city"=>"Kharkiv", "state"=>"63", "state_name"=>"Ukraine", "country"=>"UR", "country_iso3166"=>"UA", "zip"=>"00000", "magic"=>"5", "wmo"=>"34300", "latitude"=>"49.99000168", "longitude"=>"36.20999908", "elevation"=>"168.9"}, "observation_location"=>{"full"=>"Teatral'na Square, Kharkiv, ", "city"=>"Teatral'na Square, Kharkiv", "state"=>"", "country"=>"UA", "country_iso3166"=>"UA", "latitude"=>"49.994633", "longitude"=>"36.232803", "elevation"=>"433 ft"}, "estimated"=>{}, "station_id"=>"IKHARKIV50", "observation_time"=>"Last Updated on February 22, 9:07 PM EET", "observation_time_rfc822"=>"Wed, 22 Feb 2017 21:07:56 +0200", "observation_epoch"=>"1487790476", "local_time_rfc822"=>"Wed, 22 Feb 2017 21:08:10 +0200", "local_epoch"=>"1487790490", "local_tz_short"=>"EET", "local_tz_long"=>"Europe/Kiev", "local_tz_offset"=>"+0200", "weather"=>"Overcast", "temperature_string"=>"38.5 F (3.6 C)", "temp_f"=>38.5, "temp_c"=>3.6, "relative_humidity"=>"77%", "wind_string"=>"From the South at 13.0 MPH Gusting to 14.9 MPH", "wind_dir"=>"South", "wind_degrees"=>184, "wind_mph"=>13.0, "wind_gust_mph"=>"14.9", "wind_kph"=>20.9, "wind_gust_kph"=>"24.0", "pressure_mb"=>"1003", "pressure_in"=>"29.62", "pressure_trend"=>"0", "dewpoint_string"=>"32 F (-0 C)", "dewpoint_f"=>32, "dewpoint_c"=>0, "heat_index_string"=>"NA", "heat_index_f"=>"NA", "heat_index_c"=>"NA", "windchill_string"=>"31 F (-1 C)", "windchill_f"=>"31", "windchill_c"=>"-1", "feelslike_string"=>"31 F (-1 C)", "feelslike_f"=>"31", "feelslike_c"=>"-1", "visibility_mi"=>"6.2", "visibility_km"=>"10.0", "solarradiation"=>"--", "UV"=>"0", "precip_1hr_string"=>"-999.00 in ( 0 mm)", "precip_1hr_in"=>"-999.00", "precip_1hr_metric"=>" 0", "precip_today_string"=>"-999.00 in (-25375 mm)", "precip_today_in"=>"-999.00", "precip_today_metric"=>"--", "icon"=>"cloudy", "icon_url"=>"http://icons.wxug.com/i/c/k/nt_cloudy.gif", "forecast_url"=>"http://www.wunderground.com/global/stations/34300.html", "history_url"=>"http://www.wunderground.com/weatherstation/WXDailyHistory.asp?ID=IKHARKIV50", "ob_url"=>"http://www.wunderground.com/cgi-bin/findweather/getForecast?query=49.994633,36.232803", "nowcast"=>""}, "moon_phase"=>{"percentIlluminated"=>"15", "ageOfMoon"=>"26", "phaseofMoon"=>"Waning Crescent", "hemisphere"=>"North", "current_time"=>{"hour"=>"21", "minute"=>"08"}, "sunrise"=>{"hour"=>"6", "minute"=>"32"}, "sunset"=>{"hour"=>"17", "minute"=>"05"}, "moonrise"=>{"hour"=>"3", "minute"=>"42"}, "moonset"=>{"hour"=>"12", "minute"=>"48"}}, "sun_phase"=>{"sunrise"=>{"hour"=>"6", "minute"=>"32"}, "sunset"=>{"hour"=>"17", "minute"=>"05"}}}
weather.forecast_for('Kharkiv')
# => {"response"=>{"version"=>"0.1", "termsofService"=>"http://www.wunderground.com/weather/api/d/terms.html", "features"=>{"forecast"=>1}}, "forecast"=>{"txt_forecast"=>{"date"=>"8:32 PM EET", "forecastday"=>[{"period"=>0, "icon"=>"snow", "icon_url"=>"http://icons.wxug.com/i/c/k/snow.gif", "title"=>"Wednesday", "fcttext"=>"Periods of light snow late. Lows overnight in the low 30s.", "fcttext_metric"=>"Light snow overnight. Low 0C.", "pop"=>"80"}, {"period"=>1, "icon"=>"nt_snow", "icon_url"=>"http://icons.wxug.com/i/c/k/nt_snow.gif", "title"=>"Wednesday Night", "fcttext"=>"Cloudy. Light snow likely late. Low 33F. Winds SW at 10 to 20 mph. Chance of snow 80%. Snow accumulations less than one inch.", "fcttext_metric"=>"Cloudy with periods of light snow after midnight. Low around 0C. Winds SW at 15 to 30 km/h. Chance of snow 80%. Snow accumulations less than 2cm.", "pop"=>"80"}, {"period"=>2, "icon"=>"snow", "icon_url"=>"http://icons.wxug.com/i/c/k/snow.gif", "title"=>"Thursday", "fcttext"=>"Rain and snow in the morning turning to rain in the afternoon. High near 40F. Winds WSW at 10 to 20 mph. Chance of precip 80%.", "fcttext_metric"=>"Rain and snow in the morning. The rain and snow will change to all rain in the afternoon. High 4C. Winds WSW at 15 to 30 km/h. Chance of precip 80%.", "pop"=>"80"}, {"period"=>3, "icon"=>"nt_chancerain", "icon_url"=>"http://icons.wxug.com/i/c/k/nt_chancerain.gif", "title"=>"Thursday Night", "fcttext"=>"Partly cloudy skies early followed by increasing clouds with showers developing later at night. Low 33F. Winds WSW at 10 to 20 mph. Chance of rain 50%.", "fcttext_metric"=>"Partly cloudy skies early will give way to occasional showers later during the night. Low around 0C. Winds WSW at 15 to 30 km/h. Chance of rain 50%.", "pop"=>"50"}, {"period"=>4, "icon"=>"chancerain", "icon_url"=>"http://icons.wxug.com/i/c/k/chancerain.gif", "title"=>"Friday", "fcttext"=>"Showers in the morning, then partly cloudy in the afternoon. High 46F. Winds SW at 10 to 20 mph. Chance of rain 60%.", "fcttext_metric"=>"Rain showers early with some sunshine later in the day. High 8C. Winds SW at 15 to 30 km/h. Chance of rain 60%.", "pop"=>"60"}, {"period"=>5, "icon"=>"nt_partlycloudy", "icon_url"=>"http://icons.wxug.com/i/c/k/nt_partlycloudy.gif", "title"=>"Friday Night", "fcttext"=>"Partly cloudy in the evening with more clouds for later at night. Low 36F. Winds WSW at 10 to 20 mph.", "fcttext_metric"=>"Partly cloudy skies during the evening will give way to cloudy skies overnight. Low 2C. Winds WSW at 15 to 30 km/h.", "pop"=>"20"}, {"period"=>6, "icon"=>"snow", "icon_url"=>"http://icons.wxug.com/i/c/k/snow.gif", "title"=>"Saturday", "fcttext"=>"Rain and snow in the morning transitioning to snow showers in the afternoon. Temps nearly steady in the mid to upper 30s. Winds WNW at 5 to 10 mph. Chance of precip 80%. Snow accumulations less than one inch.", "fcttext_metric"=>"Rain and snow in the morning transitioning to snow showers in the afternoon. High 4C. Winds WNW at 10 to 15 km/h. Chance of precip 80%. Snow accumulations less than 2cm.", "pop"=>"80"}, {"period"=>7, "icon"=>"nt_partlycloudy", "icon_url"=>"http://icons.wxug.com/i/c/k/nt_partlycloudy.gif", "title"=>"Saturday Night", "fcttext"=>"Partly cloudy skies. Low 26F. Winds WNW at 10 to 15 mph.", "fcttext_metric"=>"Partly cloudy skies. Low -4C. Winds WNW at 15 to 25 km/h.", "pop"=>"10"}]}, "simpleforecast"=>{"forecastday"=>[{"date"=>{"epoch"=>"1487782800", "pretty"=>"7:00 PM EET on February 22, 2017", "day"=>22, "month"=>2, "year"=>2017, "yday"=>52, "hour"=>19, "min"=>"00", "sec"=>0, "isdst"=>"0", "monthname"=>"February", "monthname_short"=>"Feb", "weekday_short"=>"Wed", "weekday"=>"Wednesday", "ampm"=>"PM", "tz_short"=>"EET", "tz_long"=>"Europe/Kiev"}, "period"=>1, "high"=>{"fahrenheit"=>"44", "celsius"=>"6"}, "low"=>{"fahrenheit"=>"33", "celsius"=>"1"}, "conditions"=>"Snow Showers", "icon"=>"snow", "icon_url"=>"http://icons.wxug.com/i/c/k/snow.gif", "skyicon"=>"", "pop"=>80, "qpf_allday"=>{"in"=>0.05, "mm"=>1}, "qpf_day"=>{"in"=>nil, "mm"=>nil}, "qpf_night"=>{"in"=>0.05, "mm"=>1}, "snow_allday"=>{"in"=>0.5, "cm"=>1.3}, "snow_day"=>{"in"=>nil, "cm"=>nil}, "snow_night"=>{"in"=>0.5, "cm"=>1.3}, "maxwind"=>{"mph"=>16, "kph"=>26, "dir"=>"", "degrees"=>0}, "avewind"=>{"mph"=>9, "kph"=>15, "dir"=>"South", "degrees"=>184}, "avehumidity"=>91, "maxhumidity"=>0, "minhumidity"=>0}, {"date"=>{"epoch"=>"1487869200", "pretty"=>"7:00 PM EET on February 23, 2017", "day"=>23, "month"=>2, "year"=>2017, "yday"=>53, "hour"=>19, "min"=>"00", "sec"=>0, "isdst"=>"0", "monthname"=>"February", "monthname_short"=>"Feb", "weekday_short"=>"Thu", "weekday"=>"Thursday", "ampm"=>"PM", "tz_short"=>"EET", "tz_long"=>"Europe/Kiev"}, "period"=>2, "high"=>{"fahrenheit"=>"40", "celsius"=>"4"}, "low"=>{"fahrenheit"=>"33", "celsius"=>"1"}, "conditions"=>"Snow", "icon"=>"snow", "icon_url"=>"http://icons.wxug.com/i/c/k/snow.gif", "skyicon"=>"", "pop"=>80, "qpf_allday"=>{"in"=>0.23, "mm"=>6}, "qpf_day"=>{"in"=>0.22, "mm"=>6}, "qpf_night"=>{"in"=>0.02, "mm"=>1}, "snow_allday"=>{"in"=>0.2, "cm"=>0.5}, "snow_day"=>{"in"=>0.2, "cm"=>0.5}, "snow_night"=>{"in"=>0.0, "cm"=>0.0}, "maxwind"=>{"mph"=>20, "kph"=>32, "dir"=>"WSW", "degrees"=>243}, "avewind"=>{"mph"=>16, "kph"=>26, "dir"=>"WSW", "degrees"=>243}, "avehumidity"=>88, "maxhumidity"=>0, "minhumidity"=>0}, {"date"=>{"epoch"=>"1487955600", "pretty"=>"7:00 PM EET on February 24, 2017", "day"=>24, "month"=>2, "year"=>2017, "yday"=>54, "hour"=>19, "min"=>"00", "sec"=>0, "isdst"=>"0", "monthname"=>"February", "monthname_short"=>"Feb", "weekday_short"=>"Fri", "weekday"=>"Friday", "ampm"=>"PM", "tz_short"=>"EET", "tz_long"=>"Europe/Kiev"}, "period"=>3, "high"=>{"fahrenheit"=>"46", "celsius"=>"8"}, "low"=>{"fahrenheit"=>"36", "celsius"=>"2"}, "conditions"=>"Chance of Rain", "icon"=>"chancerain", "icon_url"=>"http://icons.wxug.com/i/c/k/chancerain.gif", "skyicon"=>"", "pop"=>60, "qpf_allday"=>{"in"=>0.02, "mm"=>1}, "qpf_day"=>{"in"=>0.02, "mm"=>1}, "qpf_night"=>{"in"=>0.0, "mm"=>0}, "snow_allday"=>{"in"=>0.0, "cm"=>0.0}, "snow_day"=>{"in"=>0.0, "cm"=>0.0}, "snow_night"=>{"in"=>0.0, "cm"=>0.0}, "maxwind"=>{"mph"=>20, "kph"=>32, "dir"=>"SW", "degrees"=>215}, "avewind"=>{"mph"=>15, "kph"=>24, "dir"=>"SW", "degrees"=>215}, "avehumidity"=>84, "maxhumidity"=>0, "minhumidity"=>0}, {"date"=>{"epoch"=>"1488042000", "pretty"=>"7:00 PM EET on February 25, 2017", "day"=>25, "month"=>2, "year"=>2017, "yday"=>55, "hour"=>19, "min"=>"00", "sec"=>0, "isdst"=>"0", "monthname"=>"February", "monthname_short"=>"Feb", "weekday_short"=>"Sat", "weekday"=>"Saturday", "ampm"=>"PM", "tz_short"=>"EET", "tz_long"=>"Europe/Kiev"}, "period"=>4, "high"=>{"fahrenheit"=>"38", "celsius"=>"3"}, "low"=>{"fahrenheit"=>"26", "celsius"=>"-3"}, "conditions"=>"Snow", "icon"=>"snow", "icon_url"=>"http://icons.wxug.com/i/c/k/snow.gif", "skyicon"=>"", "pop"=>80, "qpf_allday"=>{"in"=>0.13, "mm"=>3}, "qpf_day"=>{"in"=>0.13, "mm"=>3}, "qpf_night"=>{"in"=>0.0, "mm"=>0}, "snow_allday"=>{"in"=>0.5, "cm"=>1.3}, "snow_day"=>{"in"=>0.5, "cm"=>1.3}, "snow_night"=>{"in"=>0.0, "cm"=>0.0}, "maxwind"=>{"mph"=>10, "kph"=>16, "dir"=>"WNW", "degrees"=>286}, "avewind"=>{"mph"=>9, "kph"=>14, "dir"=>"WNW", "degrees"=>286}, "avehumidity"=>81, "maxhumidity"=>0, "minhumidity"=>0}]}}}

Implementation choices:

  • proper API client class, with key passed on creation (hallelujah!);
  • all the multitude of source APIs features (which, in this case, could be batch-requested) are represented as (pretty unusual) methods named like <feature>_and_<feature>_and_<feature>;
  • as with OpenStreetMap, data returned “as is” (most of data even not converted from strings to numbers);

Yahoo Weather

Weather.lookup_by_location('Kharkiv', Weather::Units::CELSIUS)
# => #<Weather::Response:0x0055f192d2a008 @request_location="Kharkiv", @request_url="http://query.yahooapis.com/v1/public/yql?q=select%20*%20from%20weather.forecast%20where%20woeid%20in%20(select%20woeid%20from%20geo.places(1)%20where%20text='Kharkiv')%20and%20u='c'&format=json", @astronomy=#<Weather::Astronomy:0x0055f192d2a198 @sunrise=2017-02-22 06:32:00 +0200, @sunset=2017-02-22 17:06:00 +0200>, @location=#<Weather::Location:0x0055f192cf3fa8 @city="Kharkiv", @country="Ukraine", @region="Kharkiv Oblast">, @units=#<Weather::Units:0x0055f192cf3c88 @temperature="C", @distance="km", @pressure="mb", @speed="km/h">, @wind=#<Weather::Wind:0x0055f192cf3990 @chill=32, @direction=245, @speed=11>, @atmosphere=#<Weather::Atmosphere:0x0055f192cf3850 @humidity=86, @visibility=25, @pressure=33389.81, @barometer="steady">, @image=#<Weather::Image:0x0055f192cf3620 @url="http://l.yimg.com/a/i/us/we/52/26.gif">, @forecasts=[#<Weather::Forecast:0x0055f192cbfb18 @day="Wed", @date=2017-02-22 00:00:00 +0200, @low=1, @high=2, @text="Rain And Snow", @code=5>, #<Weather::Forecast:0x0055f192cbddb8 @day="Thu", @date=2017-02-23 00:00:00 +0200, @low=0, @high=3, @text="Rain And Snow", @code=5>, #<Weather::Forecast:0x0055f192b8a568 @day="Fri", @date=2017-02-24 00:00:00 +0200, @low=1, @high=8, @text="Mostly Cloudy", @code=28>, #<Weather::Forecast:0x0055f192b84618 @day="Sat", @date=2017-02-25 00:00:00 +0200, @low=0, @high=5, @text="Rain And Snow", @code=5>, #<Weather::Forecast:0x0055f192b81f30 @day="Sun", @date=2017-02-26 00:00:00 +0200, @low=-2, @high=3, @text="Partly Cloudy", @code=30>, #<Weather::Forecast:0x0055f192b804f0 @day="Mon", @date=2017-02-27 00:00:00 +0200, @low=0, @high=5, @text="Partly Cloudy", @code=30>, #<Weather::Forecast:0x0055f192b7e858 @day="Tue", @date=2017-02-28 00:00:00 +0200, @low=1, @high=5, @text="Partly Cloudy", @code=30>, #<Weather::Forecast:0x0055f192b7ccb0 @day="Wed", @date=2017-03-01 00:00:00 +0200, @low=1, @high=7, @text="Partly Cloudy", @code=30>, #<Weather::Forecast:0x0055f192b78570 @day="Thu", @date=2017-03-02 00:00:00 +0200, @low=0, @high=7, @text="Partly Cloudy", @code=30>, #<Weather::Forecast:0x0055f192b75e88 @day="Fri", @date=2017-03-03 00:00:00 +0200, @low=2, @high=7, @text="Mostly Cloudy", @code=28>], @condition=#<Weather::Condition:0x0055f192cf31c0 @code=26, @date=2017-02-22 20:00:00 +0200, @temp=2, @text="Cloudy">, @latitude=49.990101, @longitude=36.230301, @title="Conditions for Kharkiv, Kharkiv Oblast, UA at 08:00 PM EET", @description="<![CDATA[<img src=\"http://l.yimg.com/a/i/us/we/52/26.gif\"/>\n<BR />\n<b>Current Conditions:</b>\n<BR />Cloudy\n<BR />\n<BR />\n<b>Forecast:</b>\n<BR /> Wed - Rain And Snow. High: 2Low: 1\n<BR /> Thu - Rain And Snow. High: 3Low: 0\n<BR /> Fri - Mostly Cloudy. High: 8Low: 1\n<BR /> Sat - Rain And Snow. High: 5Low: 0\n<BR /> Sun - Partly Cloudy. High: 3Low: -2\n<BR />\n<BR />\n<a href=\"http://us.rd.yahoo.com/dailynews/rss/weather/Country__Country/*https://weather.yahoo.com/country/state/city-922137/\">Full Forecast at Yahoo! Weather</a>\n<BR />\n<BR />\n(provided by <a href=\"http://www.weather.com\" >The Weather Channel</a>)\n<BR />\n]]>">

Most prominent implementation choice:

  • Response is a set of custom classes, you should know/investigate them all beforehand; they structure corresponds to source data, but, unlike hashes, you can’t easily do something like response.each { |key, value| ....

…And so on

Now, imagine, you try to integrate some of those gems into your app, and then switch to another. Or, god bless you, you want to use two of them at the same time. Would you be happy with those inventive APIs? I guess not.

You should also consider the fact that for simple, rarely changed APIs, wrapper gems tend to be abandoned: at first, there is “everything done, nowhere to evolve”, but then author forgets to make the gem compliant to latest Ruby, simplify according to new language features… Then ceases to react to issues and PRs. Then several competing gems emerge and become abandoned.

So, in a lot of cases, people prefer to re-wrap everything by themselves—and therefore have an ability to make decisions on response structure, HTTP client selection and setting up… And, whatever, you’ll be studying only one new API (service’s) not two (service’s and gem’s).

You don’t need an API wrapper!

Re-wrapping everything from scratch looks like a “reasonable” solution for most of simple APIs (not for some behemoths like Facebook Graph or something), yet has its own downsides. Again and again, the developer needs to make some decisions on underlying HTTP client library, classes/methods structure, params passing, response parsing, pre- and post-processing of all the things… All in all, instead of one common generic API wrapper with questionable design, we have a multiton of specialized wrappers for the same API, reimplemented by each developer who needs it.

I don’t know about you, but for me, it is painful to observe how fellow developers spend dozens of man-hours to choose either of two ways: (1) find the least-abandoned wrapper for target API and try to make themselves comfortable with its architecture and quirks or (2) start the API communication from scratch and design themselves all the JSON.parse(HTTPClient.get(construct_url(.......)))))).

So, what I am trying to say:

  1. We (probably) don’t need a dedicated “wrapper gem” for each and every API in the world.
  2. But what we need is an easy way to define wrappers, flexible yet simple.

Long story short: The Last API Wrapper

The result of case study for the above APIs and tens of others (from extremely branchy and tangled Worldbank’s to one-endpoint unofficial Urbandictionary’s) was this small gem, sarcastically named The Last API Wrapper (or tlaw for short).

The LARGE goals to achieve were:

  • really easy and natural way to declaratively describe “get this from that endpoint, postprocess that way”;
  • discoverability of everything (API calls structure and parameters, and API call responses).

Now, for a moderately-sized excerpt from an extensive demo set.

Here is a usage example of OpenWeatherMap wrapper (take a look at its definition also):

# Investigating wrapper on-the-fly:

p TLAW::Examples::OpenWeatherMap
# => #<TLAW::Examples::OpenWeatherMap: call-sequence: TLAW::Examples::OpenWeatherMap.new(appid:, lang: "en", units: :standard); namespaces: current, find, forecast; docs: .describe>

p TLAW::Examples::OpenWeatherMap.describe
# TLAW::Examples::OpenWeatherMap.new(appid:, lang: "en", units: :standard)
#   API for [OpenWeatherMap](http://openweathermap.org/). Only parts
#   available for free are implemented (as only them could be tested).
#
#   See full docs at http://openweathermap.org/api
#
#   @param appid You need to receive it at http://openweathermap.org/appid (free)
#   @param lang Language of API responses (affects weather description only).
#     See http://openweathermap.org/current#multi for list of supported languages. (default = "en")
#   @param units Units for temperature and other values. Standard is Kelvin.
#     Possible values: :standard, :metric, :imperial (default = :standard)
#
#   Namespaces:
#
#   .current()
#     Allows to obtain current weather at one place, designated
#     by city, location or zip code.
#
#   .find()
#     Allows to find some place (and weather in it) by set of input
#     parameters.
#
#   .forecast()
#     Allows to obtain weather forecast for 5 days with 3-hour
#     frequency.

p TLAW::Examples::OpenWeatherMap.namespaces[:current]
# => #<TLAW::Examples::OpenWeatherMap::Current: call-sequence: current(); endpoints: city, city_id, location, zip, group; docs: .describe>

weather = TLAW::Examples::OpenWeatherMap.new(appid: ENV['OPEN_WEATHER_MAP'], units: :metric)
p weather
# => #<TLAW::Examples::OpenWeatherMap.new(appid: {your id}, lang: nil, units: :metric) namespaces: current, find, forecast; docs: .describe>

# Now, for real data
pp weather.current.city('Kharkiv')
# {"weather.id"=>800,
#  "weather.main"=>"Clear",
#  "weather.description"=>"clear sky",
#  "weather.icon"=>"http://openweathermap.org/img/w/01n.png",
#  "base"=>"cmc stations",
#  "main.temp"=>23,
#  "main.pressure"=>1013,
#  "main.humidity"=>40,
#  "main.temp_min"=>23,
#  "main.temp_max"=>23,
#  "wind.speed"=>2,
#  "wind.deg"=>190,
#  "clouds.all"=>0,
#  "dt"=>2016-09-05 20:30:00 +0300,
#  "sys.type"=>1,
#  "sys.id"=>7355,
#  "sys.message"=>0.0115,
#  "sys.country"=>"UA",
#  "sys.sunrise"=>2016-09-05 05:57:26 +0300,
#  "sys.sunset"=>2016-09-05 19:08:13 +0300,
#  "id"=>706483,
#  "name"=>"Kharkiv",
#  "cod"=>200,
#  "coord"=>#<Geo::Coord 50.000000,36.250000>}

# Response with multiple lines of data
pp weather.forecast.city('Kharkiv')
# {"city.id"=>706483,
#  "city.name"=>"Kharkiv",
#  "city.country"=>"UA",
#  "city.population"=>0,
#  "city.sys.population"=>0,
#  "cod"=>"200",
#  "message"=>0.0276,
#  "cnt"=>40,
#  "list"=>
#   #<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>,
#  "city.coord"=>#<Geo::Coord 50.000000,36.250000>}

# Hmm? What is this DataTable thingy? It is (loosy) implementation of DataFrame data type.
# You can think of it as an array of homogenous hashes -- which could be considered the main
# type of useful JSON API data.
forecasts = weather.forecast.city('Kharkiv')['list']

p forecasts.count
# => 40
p forecasts.keys
# => ["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"]
p forecasts.first
# => {"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"}

p forecasts['dt'].first(3)
# => [2016-09-06 00:00:00 +0300, 2016-09-06 03:00:00 +0300, 2016-09-06 06:00:00 +0300]

(Take a look at a full demo, it shows much more of details, and is extensively commented.)

Now, you can compare this wrapper usage with Forecast.IO one’s (the wrapper itself).

You may note that the data/calls structures are different (there was not a point to make them absolutely same, as source APIs features differ), yet what is the same is declarative wrappers definition and discoverability. And if you really need it, TLAW is flexible enough for making both wrappers structure absolutely similar, in the same declarative DSL.

TLAW’s DSL is really simple: it consists of 9 methods which are covering, for what I know, most of the cases. It is not expected to seriously grow in foreseeable future: it doesn’t need to.

So, it is a “framework for defining API wrappers”?

Yes and no. Yes, the TLAW was meant to be a framework (or foundation) for creating API wrappers quickly and “naturally”. But it doesn’t mean to be a foundation for creating of ton of new “better” gems, just based on TLAW now.

It was created to evaporate the need in any dedicated API wrapper gem, at least for simple APIs:

  • You need your weather, you install TLAW and make 10 or 30 lines of an easy DSL, which structured exactly how you need, and convert data exactly the way you like it.
  • On the next project, you need the same weather again, but have different models/base data types/controllers structure—and you create a new simple wrapper.

Note, that you’ll probably WILL create some “wrapper” in any case, just “re-wrapping” around open-weather gem objects, just adding some more levels of abstraction.)

Moreover, I believe that even with large APIs (like Twitter or TheMoviesDataBase) many of API users also need something simple (like “extract last ten of user’s tweets by his username”, or “fetch movie posters”).

Here is an example of “deliberately incomplete” API wrapper made with TLAW just for one particular task. It works, it is discoverable, it throws proper and informative errors if something went wrong… And it is 20 lines of absolutely declarative code.

Bonus: when you (probably) DO NEED an API wrapper gem

Of course, I don’t want to sound like some kind of mad prophet prohibiting people to write API wrapping gems now and then. It is up for you to decide. Yet, there are at least two clear cases when TLAW’s approach will not play really well:

  • APIs with really non-standard and complicated structure of request and/or response; like, for example MediaWiki one, which has fancy ideas for both query/parameters structure and response structure.
  • And of course, full-cycle CRUD API. It is not that proper DSL expressions can’t be invented, it is about the stateful concepts, creating-reading-updating-deleting the same object and have it explicitly stated. Well… Probably something of ActiveRecord strength could be designed for all cases in the world, but … Is it reasonable? Don’t know for sure.