/responsible

HTTP response parser for crystal-lang

Primary LanguageCrystalMIT LicenseMIT

Responsible

In the wise words of the modern poet Ice Cube:

So come on and chickity check yo self before you wreck yo self

Responsible is a library that makes it simple, fast an easy to check HTTP responses in crystal-lang. It provides a lightweight API for dealing with errors, logging and static, type-safe parsing. It works directly with HTTP::Client::Response objects and can function in tandem with clients that build on top of these. Third party libraries (like halite, crest and others) are supported too.

What this isn't

Responsible is not a HTTP client. It does not handle request building, execution, retries, or authentication. It deals purely with response objects once you already have them, regardless of their source.

Installation

  1. Add the dependency to your shard.yml:

    dependencies:
      responsible:
        github: place-labs/responsible
  2. Run shards install

  3. Load via:

    require "responsible"

Usage

To become responsible, prefix any response or method that returns a response with ~.

response = ~HTTP::Client.get("https://www.example.com")

This allows a clear way to express what should happen when errors occur.

response.on_server_error do
  # oh noes
end

When working with JSON, it also lets you efficiently parse to type-safe objects.

response = ~HTTP::Client.get("https://www.example.com") >> NamedTuple(message: String)
response[:message]
# => hello world

A small selection of macros are also provided to keep common tasks clean, clear and concise.

def example_request_wrapper : { message: String }
  Responsible.parse_to_return_type do
    HTTP::Client.get("https://www.example.com")
  end
end

Responsible Responses™ maintain all existing functionality of a vanilla response object.

chuck_norris_response = ~HTTP::Client.get("https://api.chucknorris.io/jokes/random")

chuck_norris_response.body
# =>
{
  "categories": [],
  "created_at": "2020-01-05 13:42:19.897976",
  "icon_url": "https://assets.chucknorris.host/img/avatar/chuck-norris.png",
  "id": "h6EF5PXvQoK5cW60-lUeDg",
  "updated_at": "2020-01-05 13:42:19.897976",
  "url": "https://api.chucknorris.io/jokes/h6EF5PXvQoK5cW60-lUeDg",
  "value": "Chuck Norris has a large, curved talon on each foot, much like the Velociraptor."
}

They also let you inject behaviour for dealing with different response scenarios.

# Global handlers - this will apply to every reponse
Responsible.on_client_error do |response|
  raise "I'm a teapot" if response.status_code == 418
end

# Local action - these apply to the target response
chuck_norris_response.on_redirect do |response|
  my_request_method(response.headers["Location"])
end

To parse to a matching type use the >> operator.

struct ChuckNorrisFact
   include JSON::Serializable
   getter created_at : Time
   getter id :  String
   getter updated_at : Time
   getter value : String
end

fact = chuck_norris_response >> ChuckNorrisFact

fact.value
# => Chuck Norris has a large, curved talon on each foot, much like the Velociraptor.

This also works inline for terse, type-safe, error checked responses.

fact = ~HTTP::Client.get("https://api.chucknorris.io/jokes/random") >> NamedTuple(value: String)
fact[:value]
# => Chuck Norris can binary search unsorted data.

If the response format is incompatible with the specified type a Responsible::Error will raise. This contains the parser exception via error.cause.

To return nil in place of a raising an error, use a nilable target type.

chuck_norris_response >> Float64?
# => nil

Again, but verbose-er-er

If the operator overloading is too terse, named methods are available.

To wrap a response object:

response = HTTP::Client.get("www.example.com")
response = Responsible::Response.new(response)

This performs the same action as using the ~ operator.

To parse into a type:

response.parse_to(MyType)

This is the same behaviour as >>.

An optional block argument supports defining custom parser error handling.

response.parse_to(MyType) do |exception|
  # Handle, raise or re-try as appropriate.
end

To return nil in case of a parser error, use:

response.parse_to?(MaybeMyType)

Third-party support

Responsible works around a minimal response object interface. When you require "responsible", support loads for HTTP::Client::Response objects by default. To support third party response objects use the Responsible.support macro.

require "Halite"

Responsible.support Halite::Response

response = ~Halite.get("https://www.example.com") >> NamedTuple(message: String)
response[:message]

Contributors