项目作者: molybdenum-99

项目描述 :
The Last API Wrapper: Pragmatic API wrapper framework
高级语言: Ruby
项目地址: git://github.com/molybdenum-99/tlaw.git
创建时间: 2016-09-13T18:33:10Z
项目社区:https://github.com/molybdenum-99/tlaw

开源协议:MIT License

下载


TLAW - The Last API Wrapper

Gem Version
Build Status
Coverage Status

TLAW (pronounce it like “tea+love”… or whatever) is the last (and only)
API wrapper framework you’ll ever need for accessing GET-only APIs*
in a consistent way (think weather, search, economical indicators, geonames and so on).

Table Of Contents

Features

  • Pragmatic: thorougly designed with tens of real world examples in mind,
    not just your textbook’s “perfect orthogonal REST API”;
  • Opinionated: goal is clean and logical Ruby libary, not just mechanical
    1-by-1 endpoint-per-method wrapper for every fancy hivemind invention;
  • Easy and readable definitions;
  • Discoverable: once API defined in TLAW terms, you can easily investigate
    it in runtime, obtain meaningful errors like “param foo is missing
    while trying to access endpoint bar“ and so on;
  • Sane metaprogramming: allows to define entire branchy API wrapper with
    tons of pathes and endpoints in really concise manner, while creating
    all corresponding classes/methods at definition time: so, at runtime
    you have no 20-level dynamic dispatching, just your usual method calls
    with clearly defined arguments and compact backtraces.

Take a look at our “model” OpenWeatherMap wrapper
and demo
of its usage, showing how all those things work in reality.

Why TLAW?

There are ton of small (and not-so-small) useful APIs about world around:
weather, movies, geographical features, dictionaries, world countries
statistics… Typically, when trying to use one of them from Ruby (or,
to be honest, from any programming language), you are stuck with two
options:

  1. Study and use (or invent and build) some custom hand-made Wrapper
    Library™ with ton of very custom design decisions (should responses
    be just hashes, or Hashie, or
    real classes for each kind of response? What are the inputs? Where should
    api key go, to global param?); or
  2. Just “go commando” (sorry for the bad pun): construct URLs yourself,
    parse responses yourself, control params (or ignore the control) yourself.

TLAW tries to close this gap: provide a base for breath-easy API description
which produces solid, fast and reliable wrappers.

See also a showcase blog post with on-the-fly
API wrapper building for GIPHY.

Usage

URLs and params description

  1. class Example < TLAW::API
  2. define do
  3. base 'http://api.example.com'
  4. param :api_key, required: true # this would be necessary for API instance creation
  5. # So, the API instance would be e = Example.new(api_key: '123')
  6. # ...and parameter ?api_key=123 would be added to any request
  7. endpoint :foo # The simplest endpoint, will query "http://api.example.com/foo"
  8. # And then you just do e.foo and obtain result
  9. endpoint :bar, '/baz.json' # Path to query rewritten, will query "http://api.example.com/baz.json"
  10. # Method is still e.bar, though.
  11. # Now, for params definition:
  12. endpoint :movie do
  13. param :id
  14. end
  15. # Method call would be movie(id: '123')
  16. # Generated URL would be "http://api.example.com/movie?id=123"
  17. # When param is part of the path, you can use RFC 6570
  18. # URL template standard:
  19. endpoint :movie, '/movies/{id}'
  20. # That would generate method which is called like movie('123')
  21. # ...and call to "http://api.example.com/movies/123"
  22. # Now, we can stack endpoints in namespaces
  23. namespace :foo do # adds /foo to path
  24. namespace :bar, '/baz' do # optional path parameter works
  25. endpoint :blah # URL for call would be "http://api.example.com/foo/baz/blah"
  26. # And method call would be like e.foo.bar.blah(parameters)
  27. end
  28. # URL normalization works, so you can stack in namespaces even
  29. # things not related to them in source API, "redesigning" API on
  30. # the fly.
  31. endpoint :books, '/../books.json' # Real URL would be "http://api.example.com/books"
  32. # Yet method call is still namespaced like e.foo.books
  33. end
  34. # Namespaces can have their own input parameters
  35. namespace :foo, '/foo/{id}' do
  36. endpoint :bar # URL would be "http://api.example.com/foo/123/bar
  37. # method call would be e.foo(123).bar
  38. end
  39. end
  40. # ...and everything works in all possible and useful ways, just check
  41. # docs and demos.
  42. end

See DSL module docs for
full description of all features (there are few, yet very powerful).

Response processing

TLAW is really opinionated about response processing. Main things:

  1. Hashes are “flattened”;
  2. Arrays of hashes are converted to DataTables;
  3. Post-processors for fields are easily defined

Flat hashes

The main (and usually top-level) answer of (JSON) API is a Hash/dictionary.
TLAW takes all multilevel hashes and make them flat.

Here is an example.

Source API responds like:

  1. {
  2. "meta": {
  3. "code": "OK",
  4. },
  5. "weather": {
  6. "temp": 10,
  7. "precipitation": 138
  8. },
  9. "location": {
  10. "lat": 123,
  11. "lon": 456
  12. }
  13. ...
  14. }

But TLAW response to api.endpoint(params) would return you a Hash looking
this way:

  1. {
  2. "meta.code": "OK",
  3. "weather.temp": 10,
  4. "weather.precipitation": 138,
  5. "location.lat": 123,
  6. "location.lon": 456
  7. ...
  8. }

Reason? If you think of it and experiment with several examples, typically
with new & unexplored API you’ll came up with code like:

  1. p response
  2. # => 3 screens of VERY IMPORTANT RESPONSE
  3. p response.class
  4. # => Hash, ah, ok
  5. p response.keys
  6. # => ["meta", "weather", "location"], hmmm...
  7. p response['weather']
  8. # => stil 2.5 screens of unintelligible details
  9. p response['weather'].class
  10. # => Hash, ah!
  11. p response['weather'].keys
  12. # => and ad infinitum, real APIs are easily go 6-8 levels down

Now, with “opinionated” TLAW’s flattening, for any API you just do
the one and final response.keys and that’s it: you see every available
data key, deep to the deepest depth.

NB: probably, in the next versions TLAW will return some Hash descendant,
which would also still allow you to do response['weather'] and receive
that “slice”. Or it would not :) We are experimenting!

DataTable

The second main type of a (JSON) API answer, or of a part of an answer
is an array of homogenous hashes, like:

  • list of data points (date - weather at that date);
  • list of data objects (city id - city name - latitude - longitude);
  • list of views to the data (climate model - projected temperature);
  • and so on.

TLAW wraps this kind of data (array of homogenous hashes, or tables with
named columns) into DataTable structure, which you can think of as an
Excel spreadsheet (2d array with named columns), or loose DataFrame
pattern implementation (just like daru
or pandas, but seriously simpler—and much
more suited to the case).

Imagine you have an API responding something like:

  1. {
  2. "meta": {"count": 20},
  3. "data": [
  4. {"date": "2016-09-01", "temp": 20, "humidity": 40},
  5. {"date": "2016-09-02", "temp": 21, "humidity": 40},
  6. {"date": "2016-09-03", "temp": 16, "humidity": 36},
  7. ...
  8. ]
  9. }

With TLAW, you’ll see this response this way:

  1. pp response
  2. {"meta.count"=>20,
  3. "data"=>#<TLAW::DataTable[date, temp, humidity] x 20>}
  4. # ^ That's all. Small and easy to grasp what is what. 3 named columns,
  5. # 20 similar rows.
  6. d = response['data']
  7. # => #<TLAW::DataTable[date, temp, humidity] x 20>
  8. d.count # Array-alike
  9. # => 20
  10. d.first
  11. # => {"date" => "2016-09-01", "temp" => 20, "humidity" => 40}
  12. d.keys # Hash-alike
  13. # => ["date", "temp", "humidity"]
  14. d["date"]
  15. # => ["2016-09-01", "2016-09-02", "2016-09-03" ...
  16. # And stuff:
  17. d.to_h
  18. # => {"date" => [...], "temp" => [...] ....
  19. d.to_a
  20. # => [{"date" => ..., "temp" => ..., "humidity" => ...}, {"date" => ...
  21. d.columns('date', 'temp') # column-wise slice
  22. # => #<TLAW::DataTable[date, temp] x 20>
  23. d.columns('date', 'temp').first # and so on
  24. # => {"date" => "2016-09-01", "temp" => 20}

Take a look at DataTable docs
and join designing it!

Post-processing

When you are not happy with result representation, you can post-process
them in several ways:

  1. # input is entire response, block can mutate it
  2. post_process { |hash| hash['foo'] = 'bar' }
  3. # input is entire response, and response is fully replaced with block's
  4. # return value
  5. post_process { |hash| hash['foo'] } # Now only "foo"s value will be response
  6. # input is value of response's key "some_key", return value of a block
  7. # becames new value of "some_key".
  8. post_process('some_key') { |val| other_val }
  9. # Post-processing each item, if response['foo'] is array:
  10. post_process_items('foo') {
  11. # mutate entire item
  12. post_process { |item| item.delete('bar') }
  13. # if item is a Hash, replace its "bar" value
  14. post_process('bar') { |val| val.to_s }
  15. }
  16. # More realistic examples:
  17. post_process('meta.count', &:to_i)
  18. post_process_items('daily') {
  19. post_process('date', &Date.method(:parse))
  20. }
  21. post_process('auxiliary_value') { nil } # Nil's will be thrown away completely

See full post-processing features descriptions in
DSL module docs.

All at once

All described response processing steps are performed in this order:

  • parsing and initial flattening of JSON (or XML) hash;
  • applying post-processors (and flatten the response after each of them);
  • make DataTables from arrays of hashes.

Documentability

You do it this way:

  1. class MyAPI < TLAW::API
  2. desc %Q{
  3. This is API, it works.
  4. }
  5. docs 'http://docs.example.com'
  6. namespace :ns do
  7. desc %Q{
  8. It is some interesting thing.
  9. }
  10. docs 'http://docs.example.com/ns'
  11. endpoint :baz do
  12. desc %Q{
  13. Should be useful.
  14. }
  15. docs 'http://docs.example.com/ns#baz'
  16. param :param1,
  17. desc: %Q{
  18. You don't need it, really.
  19. }
  20. end
  21. end
  22. end

All of above is optional, but when provided, allows to investigate
things at runtime (in IRB/pry or test scripts). Again, look at
OpenWeatherMap demo,
it shows how docs could be useful at runtime.

Some demos

Installation & compatibility

Just gem install tlaw or add it to your Gemfile, nothing fancy.

Required Ruby version is 2.1+, JRuby works, Rubinius seems like not.

Upcoming features

(in no particular order)

  • Expose Faraday options (backends, request headers);
  • Request-headers based auth;
  • Responses caching;
  • Response headers processing DSL;
  • Paging support;
  • Frequency-limited API support (requests counting);
  • YARD docs generation for resulting wrappers;
  • More solid wrapper demos (weather sites, geonames, worldbank);
  • Approaches to testing generated wrappers (just good ol’ VCR should
    1. work, probably);
  • Splat parameters.

Get-only API

What is those “Get-only APIs” TLAW is suited for?

  • It is only for getting data, not changing them (though, API may use
    HTTP POST requests in reality—for example, to transfer large request
    objects);
    • It would be cool if our weather APIs could allow things like
      POST /weather/kharkiv {sky: 'sunny', temp: '+21°C'} in the middle
      of December, huh? But we are not leaving in the world like this.
      For now.
  • It has utterly simple authentication protocol like “give us api_key
    param in query” (though, TLAW plans to support more complex authentication);
  • It typically returns JSON answers (though, TLAW supports XML via
    awesome crack).

Alongside already mentioned examples (weather and so on), you can build
TLAW-backed “get-only” wrappers for bigger APIs (like Twitter), when
“gathering twits” is all you need. (Though, to be honest, TLAW’s current
authorization abilities is far simpler than
Twitter requirements).

Current status

It is version 0.0.1. It is tested and documented, but not “tested in
battle”, just invented. DSL is subject to be refined and validated,
everything could change (or broke suddenly). Tests are lot, though.

We plan to heavily utilize it for reality,
that would be serious evaluation of approaches and weeknesses.

Author

Victor Shepelev

License

MIT.