{"id":13879086,"url":"https://github.com/molybdenum-99/tlaw","last_synced_at":"2025-04-07T12:10:24.666Z","repository":{"id":62559128,"uuid":"68136250","full_name":"molybdenum-99/tlaw","owner":"molybdenum-99","description":"The Last API Wrapper: Pragmatic API wrapper framework","archived":false,"fork":false,"pushed_at":"2023-01-21T14:56:50.000Z","size":373,"stargazers_count":125,"open_issues_count":4,"forks_count":9,"subscribers_count":5,"default_branch":"master","last_synced_at":"2025-03-31T11:03:53.418Z","etag":null,"topics":["api","api-wrapper","dsl","wrapper"],"latest_commit_sha":null,"homepage":"","language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/molybdenum-99.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE.txt","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2016-09-13T18:33:10.000Z","updated_at":"2024-04-18T11:01:25.000Z","dependencies_parsed_at":"2023-02-12T10:47:04.292Z","dependency_job_id":null,"html_url":"https://github.com/molybdenum-99/tlaw","commit_stats":null,"previous_names":[],"tags_count":3,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/molybdenum-99%2Ftlaw","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/molybdenum-99%2Ftlaw/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/molybdenum-99%2Ftlaw/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/molybdenum-99%2Ftlaw/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/molybdenum-99","download_url":"https://codeload.github.com/molybdenum-99/tlaw/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247648977,"owners_count":20972945,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["api","api-wrapper","dsl","wrapper"],"created_at":"2024-08-06T08:02:09.337Z","updated_at":"2025-04-07T12:10:24.638Z","avatar_url":"https://github.com/molybdenum-99.png","language":"Ruby","funding_links":[],"categories":["Ruby"],"sub_categories":[],"readme":"# TLAW - The Last API Wrapper\n\n[![Gem Version](https://badge.fury.io/rb/tlaw.svg)](http://badge.fury.io/rb/tlaw)\n[![Build Status](https://travis-ci.org/molybdenum-99/tlaw.svg?branch=master)](https://travis-ci.org/molybdenum-99/tlaw)\n[![Coverage Status](https://coveralls.io/repos/molybdenum-99/tlaw/badge.svg?branch=master)](https://coveralls.io/r/molybdenum-99/tlaw?branch=master)\n\n**TLAW** (pronounce it like \"tea+love\"... or whatever) is the last (and only)\nAPI wrapper framework you'll ever need for accessing _GET-only APIs_\u003csup\u003e[*](#get-only-api)\u003c/sup\u003e\nin a consistent way (think weather, search, economical indicators, geonames and so on).\n\n## Table Of Contents\n\n* [Features](#features)\n* [Why TLAW?](#why-tlaw)\n* [Usage](#usage)\n  * [URLs and params description](#urls-and-params-description)\n  * [Response processing](#response-processing)\n    * [Flat hashes](#flat-hashes)\n    * [DataTable](#datatable)\n    * [Post-processing](#post-processing)\n    * [All at once](#all-at-once)\n  * [Documentability](#documentability)\n* [Some demos](#some-demos)\n* [Installation \u0026 compatibility](#installation-\u0026-compatibility)\n* [Upcoming features](#upcoming-features)\n* [Get-only API](#get-only-api)\n* [Current status](#current-status)\n* [Links](#links)\n* [Author](#author)\n* [License](#license)\n\n## Features\n\n* **Pragmatic**: thorougly designed with tens of real world examples in mind,\n  not just your textbook's \"perfect orthogonal REST API\";\n* **Opinionated**: goal is clean and logical Ruby libary, not just mechanical\n  1-by-1 endpoint-per-method wrapper for every fancy hivemind invention;\n* **Easy and readable** definitions;\n* **Discoverable**: once API defined in TLAW terms, you can easily investigate\n  it in runtime, obtain meaningful errors like \"param `foo` is missing\n  while trying to access endpoint `bar`\" and so on;\n* **Sane metaprogramming**: allows to define entire branchy API wrapper with\n  tons of pathes and endpoints in really concise manner, while creating\n  _all_ corresponding classes/methods at definition time: so, at runtime\n  you have no 20-level dynamic dispatching, just your usual method calls\n  with clearly defined arguments and compact backtraces.\n\nTake a look at our \"model\" OpenWeatherMap [wrapper](https://github.com/molybdenum-99/tlaw/blob/master/examples/open_weather_map.rb)\nand [demo](https://github.com/molybdenum-99/tlaw/blob/master/examples/open_weather_map_demo.rb)\nof its usage, showing how all those things work in reality.\n\n## Why TLAW?\n\nThere are ton of small (and not-so-small) useful APIs about world around:\nweather, movies, geographical features, dictionaries, world countries\nstatistics... Typically, when trying to use one of them from Ruby (or,\nto be honest, from any programming language), you are stuck with two\noptions:\n\n1. Study and use (or invent and build) some custom hand-made Wrapper\n  Library™ with ton of very custom design decisions (should responses\n  be just hashes, or [Hashie](https://github.com/intridea/hashie), or\n  real classes for each kind of response? What are the inputs? Where should\n  api key go, to global param?); or\n2. Just \"go commando\" (sorry for the bad pun): construct URLs yourself,\n  parse responses yourself, control params (or ignore the control) yourself.\n\nTLAW tries to close this gap: provide a base for _breath-easy_ API description\nwhich produces solid, fast and reliable wrappers.\n\nSee also a [showcase blog post](http://zverok.github.io/blog/2017-07-31-giphy-tlaw.html) with on-the-fly\nAPI wrapper building for GIPHY.\n\n## Usage\n\n### URLs and params description\n\n```ruby\nclass Example \u003c TLAW::API\n  define do\n    base 'http://api.example.com'\n\n    param :api_key, required: true # this would be necessary for API instance creation\n    # So, the API instance would be e = Example.new(api_key: '123')\n    # ...and parameter ?api_key=123 would be added to any request\n\n    endpoint :foo # The simplest endpoint, will query \"http://api.example.com/foo\"\n    # And then you just do e.foo and obtain result\n\n    endpoint :bar, '/baz.json' # Path to query rewritten, will query \"http://api.example.com/baz.json\"\n    # Method is still e.bar, though.\n\n    # Now, for params definition:\n    endpoint :movie do\n      param :id\n    end\n    # Method call would be movie(id: '123')\n    # Generated URL would be \"http://api.example.com/movie?id=123\"\n\n    # When param is part of the path, you can use RFC 6570\n    # URL template standard:\n    endpoint :movie, '/movies/{id}'\n    # That would generate method which is called like movie('123')\n    # ...and call to \"http://api.example.com/movies/123\"\n\n    # Now, we can stack endpoints in namespaces\n    namespace :foo do # adds /foo to path\n      namespace :bar, '/baz' do # optional path parameter works\n        endpoint :blah # URL for call would be \"http://api.example.com/foo/baz/blah\"\n        # And method call would be like e.foo.bar.blah(parameters)\n      end\n\n      # URL normalization works, so you can stack in namespaces even\n      # things not related to them in source API, \"redesigning\" API on\n      # the fly.\n      endpoint :books, '/../books.json' # Real URL would be \"http://api.example.com/books\"\n      # Yet method call is still namespaced like e.foo.books\n    end\n\n    # Namespaces can have their own input parameters\n    namespace :foo, '/foo/{id}' do\n      endpoint :bar # URL would be \"http://api.example.com/foo/123/bar\n      # method call would be e.foo(123).bar\n    end\n  end\n\n  # ...and everything works in all possible and useful ways, just check\n  # docs and demos.\nend\n```\n\nSee [DSL module docs](http://www.rubydoc.info/gems/tlaw/TLAW/DSL) for\nfull description of all features (there are few, yet very powerful).\n\n### Response processing\n\nTLAW is really opinionated about response processing. Main things:\n\n1. [Hashes are \"flattened\"](#flat-hashes);\n2. [Arrays of hashes are converted to `DataTable`s](#datatable);\n3. [Post-processors for fields are easily defined](#post-processing)\n\n#### Flat hashes\n\nThe main (and usually top-level) answer of (JSON) API is a Hash/dictionary.\nTLAW takes all multilevel hashes and make them flat.\n\nHere is an example.\n\nSource API responds like:\n\n```json\n{\n  \"meta\": {\n    \"code\": \"OK\",\n  },\n  \"weather\": {\n    \"temp\": 10,\n    \"precipitation\": 138\n  },\n  \"location\": {\n    \"lat\": 123,\n    \"lon\": 456\n  }\n  ...\n}\n```\n\nBut TLAW response to `api.endpoint(params)` would return you a Hash looking\nthis way:\n\n```json\n{\n  \"meta.code\": \"OK\",\n  \"weather.temp\": 10,\n  \"weather.precipitation\": 138,\n  \"location.lat\": 123,\n  \"location.lon\": 456\n  ...\n}\n```\n\nReason? If you think of it and experiment with several examples, typically\nwith new \u0026 unexplored API you'll came up with code like:\n\n```ruby\np response\n# =\u003e 3 screens of VERY IMPORTANT RESPONSE\np response.class\n# =\u003e Hash, ah, ok\np response.keys\n# =\u003e [\"meta\", \"weather\", \"location\"], hmmm...\np response['weather']\n# =\u003e stil 2.5 screens of unintelligible details\np response['weather'].class\n# =\u003e Hash, ah!\np response['weather'].keys\n# =\u003e and ad infinitum, real APIs are easily go 6-8 levels down\n```\n\nNow, with \"opinionated\" TLAW's flattening, for _any_ API you just do\nthe one and final `response.keys` and that's it: you see every available\ndata key, deep to the deepest depth.\n\n\u003e NB: probably, in the next versions TLAW will return some Hash descendant,\n  which would also still allow you to do `response['weather']` and receive\n  that \"slice\". Or it would not :) We are experimenting!\n\n#### DataTable\n\nThe second main type of a (JSON) API answer, or of a part of an answer\nis an array of homogenous hashes, like:\n\n* list of data points (date - weather at that date);\n* list of data objects (city id - city name - latitude - longitude);\n* list of views to the data (climate model - projected temperature);\n* and so on.\n\nTLAW wraps this kind of data (array of homogenous hashes, or tables with\nnamed columns) into `DataTable` structure, which you can think of as an\nExcel spreadsheet (2d array with named columns), or loose DataFrame\npattern implementation (just like [daru](https://github.com/v0dro/daru)\nor [pandas](http://pandas.pydata.org/), but seriously simpler—and much\nmore suited to the case).\n\nImagine you have an API responding something like:\n\n```json\n{\n  \"meta\": {\"count\": 20},\n  \"data\": [\n    {\"date\": \"2016-09-01\", \"temp\": 20, \"humidity\": 40},\n    {\"date\": \"2016-09-02\", \"temp\": 21, \"humidity\": 40},\n    {\"date\": \"2016-09-03\", \"temp\": 16, \"humidity\": 36},\n    ...\n  ]\n}\n```\n\nWith TLAW, you'll see this response this way:\n\n```ruby\npp response\n{\"meta.count\"=\u003e20,\n \"data\"=\u003e#\u003cTLAW::DataTable[date, temp, humidity] x 20\u003e}\n# ^ That's all. Small and easy to grasp what is what. 3 named columns,\n#   20 similar rows.\n\nd = response['data']\n# =\u003e #\u003cTLAW::DataTable[date, temp, humidity] x 20\u003e\n\nd.count # Array-alike\n# =\u003e 20\nd.first\n# =\u003e {\"date\" =\u003e \"2016-09-01\", \"temp\" =\u003e 20, \"humidity\" =\u003e 40}\n\nd.keys # Hash-alike\n# =\u003e [\"date\", \"temp\", \"humidity\"]\nd[\"date\"]\n# =\u003e [\"2016-09-01\", \"2016-09-02\", \"2016-09-03\" ...\n\n# And stuff:\nd.to_h\n# =\u003e {\"date\" =\u003e [...], \"temp\" =\u003e [...] ....\nd.to_a\n# =\u003e [{\"date\" =\u003e ..., \"temp\" =\u003e ..., \"humidity\" =\u003e ...}, {\"date\" =\u003e ...\n\nd.columns('date', 'temp') # column-wise slice\n# =\u003e #\u003cTLAW::DataTable[date, temp] x 20\u003e\nd.columns('date', 'temp').first # and so on\n# =\u003e {\"date\" =\u003e \"2016-09-01\", \"temp\" =\u003e 20}\n```\n\nTake a look at [DataTable docs](http://www.rubydoc.info/gems/tlaw/TLAW/DataTable)\nand join designing it!\n\n#### Post-processing\n\nWhen you are not happy with result representation, you can post-process\nthem in several ways:\n\n```ruby\n# input is entire response, block can mutate it\npost_process { |hash| hash['foo'] = 'bar' }\n\n# input is entire response, and response is fully replaced with block's\n# return value\npost_process { |hash| hash['foo'] } # Now only \"foo\"s value will be response\n\n# input is value of response's key \"some_key\", return value of a block\n# becames new value of \"some_key\".\npost_process('some_key') { |val| other_val }\n\n# Post-processing each item, if response['foo'] is array:\npost_process_items('foo') {\n  # mutate entire item\n  post_process { |item| item.delete('bar') }\n\n  # if item is a Hash, replace its \"bar\" value\n  post_process('bar') { |val| val.to_s }\n}\n\n# More realistic examples:\npost_process('meta.count', \u0026:to_i)\npost_process_items('daily') {\n  post_process('date', \u0026Date.method(:parse))\n}\npost_process('auxiliary_value') { nil } # Nil's will be thrown away completely\n```\n\nSee full post-processing features descriptions in\n[DSL module docs](http://www.rubydoc.info/gems/tlaw/TLAW/DSL).\n\n#### All at once\n\nAll described response processing steps are performed in this order:\n\n* parsing and initial flattening of JSON (or XML) hash;\n* applying post-processors (and flatten the response after _each_ of them);\n* make `DataTable`s from arrays of hashes.\n\n### Documentability\n\nYou do it this way:\n\n```ruby\nclass MyAPI \u003c TLAW::API\n  desc %Q{\n    This is API, it works.\n  }\n\n  docs 'http://docs.example.com'\n\n  namespace :ns do\n    desc %Q{\n      It is some interesting thing.\n    }\n\n    docs 'http://docs.example.com/ns'\n\n    endpoint :baz do\n      desc %Q{\n        Should be useful.\n      }\n\n      docs 'http://docs.example.com/ns#baz'\n\n      param :param1,\n        desc: %Q{\n          You don't need it, really.\n        }\n    end\n  end\nend\n```\n\nAll of above is optional, but when provided, allows to investigate\nthings at runtime (in IRB/pry or test scripts). Again, look at\n[OpenWeatherMap demo](https://github.com/molybdenum-99/tlaw/blob/master/examples/open_weather_map_demo.rb),\nit shows how docs could be useful at runtime.\n\n## Some demos\n\n* Full-featured API wrappers:\n  * OpenWeatherMap: [source API docs](http://openweathermap.org/api),\n    [wrapper](https://github.com/molybdenum-99/tlaw/blob/master/examples/open_weather_map.rb),\n    extensively commented \u0026 explained\n    [demo code](https://github.com/molybdenum-99/tlaw/blob/master/examples/open_weather_map_demo.rb);\n  * ForecastIO: [API docs](https://developer.forecast.io/docs/v2),\n    [wrapper](https://github.com/molybdenum-99/tlaw/blob/master/examples/forecast_io.rb),\n    [demo code](https://github.com/molybdenum-99/tlaw/blob/master/examples/forecast_io_demo.rb);\n* Demos of \"fire-and-forget\" wrappers:\n  * Urbandictionary's small and unofficial\n    [API wrapper](https://github.com/molybdenum-99/tlaw/blob/master/examples/urbandictionary_demo.rb);\n  * [Partial wrapper](https://github.com/molybdenum-99/tlaw/blob/master/examples/tmdb_demo.rb)\n    only for some features of large [TMDB API](docs.themoviedb.apiary.io/).\n    It also shows [on-the-fly updating](https://github.com/molybdenum-99/tlaw/blob/master/examples/tmdb_demo.rb#L85)\n    of already existing API wrapper to add some features.\n\n## Installation \u0026 compatibility\n\nJust `gem install tlaw` or add it to your `Gemfile`, nothing fancy.\n\nRequired Ruby version is 2.1+, JRuby works, Rubinius seems like not.\n\n## Upcoming features\n\n_(in no particular order)_\n\n* [ ] Expose Faraday options (backends, request headers);\n* [ ] Request-headers based auth;\n* [ ] Responses caching;\n* [ ] Response headers processing DSL;\n* [ ] Paging support;\n* [ ] Frequency-limited API support (requests counting);\n* [ ] YARD docs generation for resulting wrappers;\n* [ ] More solid wrapper demos (weather sites, geonames, worldbank);\n* [ ] Approaches to testing generated wrappers (just good ol' VCR should\n      work, probably);\n* [ ] Splat parameters.\n\n## Get-only API\n\nWhat is those \"Get-only APIs\" TLAW is suited for?\n\n* It is only for _getting_ data, not changing them (though, API may use\n  HTTP POST requests in reality—for example, to transfer large request\n  objects);\n  * It would be cool if our weather APIs could allow things like\n    `POST /weather/kharkiv {sky: 'sunny', temp: '+21°C'}` in the middle\n    of December, huh? But we are not leaving in the world like this.\n    For now.\n* It has utterly simple authentication protocol like \"give us `api_key`\n  param in query\" (though, TLAW plans to support more complex authentication);\n* It typically returns JSON answers (though, TLAW supports XML via\n  awesome [crack](https://github.com/jnunemaker/crack)).\n\nAlongside already mentioned examples (weather and so on), you can build\nTLAW-backed \"get-only\" wrappers for bigger APIs (like Twitter), when\n\"gathering twits\" is all you need. (Though, to be honest, TLAW's current\nauthorization abilities is far simpler than\n[Twitter requirements](https://dev.twitter.com/oauth/application-only)).\n\n## Current status\n\nIt is version 0.0.1. It is tested and documented, but not \"tested in\nbattle\", just invented. DSL is subject to be refined and validated,\neverything could change (or broke suddenly). Tests are lot, though.\n\nWe plan to heavily utilize it for [reality](https://github.com/molybdenum-99/reality),\nthat would be serious evaluation of approaches and weeknesses.\n\n## Links\n\n* [API Docs](http://www.rubydoc.info/gems/tlaw)\n\n## Author\n\n[Victor Shepelev](http://zverok.github.io/)\n\n## License\n\n[MIT](./LICENSE.txt).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmolybdenum-99%2Ftlaw","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmolybdenum-99%2Ftlaw","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmolybdenum-99%2Ftlaw/lists"}