Skip to content

Commit

Permalink
Updated documentation + improved Internet interface.
Browse files Browse the repository at this point in the history
  • Loading branch information
ioquatix committed Jun 24, 2024
1 parent e1e1f13 commit fcf231a
Show file tree
Hide file tree
Showing 7 changed files with 188 additions and 362 deletions.
3 changes: 0 additions & 3 deletions gems.rb
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,4 @@

gem "localhost"
gem "rack-test"

# Optional dependency:
gem "thread-local"
end
151 changes: 151 additions & 0 deletions guides/getting-started/readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
# Getting Started

This guide explains how to get started with `Async::HTTP`.

## Installation

Add the gem to your project:

~~~ bash
$ bundle add async-http
~~~

## Core Concepts

- {ruby Async::HTTP::Client} is the main class for making HTTP requests.
- {ruby Async::HTTP::Internet} provides a simple interface for making requests to any server "on the internet".
- {ruby Async::HTTP::Server} is the main class for handling HTTP requests.
- {ruby Async::HTTP::Endpoint} can parse HTTP URLs in order to create a client or server.
- [`protocol-http`](https://github.com/socketry/protocol-http) provides the abstract HTTP protocol interfaces.

## Usage

### Making a Request

To make a request, create an instance of {ruby Async::HTTP::Internet} and call the appropriate method:

~~~ ruby
require 'async/http/internet'

Sync do
Async::HTTP::Internet.get("https://httpbin.org/get") do |response|
puts response.read
end
end
~~~

The following methods are supported:

~~~ ruby
Async::HTTP::Internet.methods(false)
# => [:patch, :options, :connect, :post, :get, :delete, :head, :trace, :put]
~~~

Using a block will automatically close the response when the block completes. If you want to keep the response open, you can manage it manually:

~~~ ruby
require 'async/http'

Sync do
response = Async::HTTP::Internet.get("https://httpbin.org/get")
puts response.read
ensure
response&.close
end
~~~

As responses are streamed, you must ensure it is closed when you are finished with it.

#### Persistence

By default, {ruby Async::HTTP::Internet} will create a {ruby Async::HTTP::Client} for each remote host you communicate with, and will keep those connections open for as long as possible. This is useful for reducing the latency of subsequent requests to the same host. When you exit the event loop, the connections will be closed automatically.

### Downloading a File

~~~ ruby
require 'async/http'
require 'async/http/internet/instance'

Sync do
# Issue a GET request to Google:
response = Async::HTTP::Internet.get("https://www.google.com/search?q=kittens")

# Save the response body to a local file:
response.save("/tmp/search.html")
ensure
response&.close
end
~~~

### Posting Data

To post data, use the `post` method:

~~~ ruby
require 'async/http'
require 'async/http/internet/instance'

data = {'life' => 42}

Sync do
# Prepare the request:
headers = [['accept', 'application/json']]
body = JSON.dump(data)

# Issues a POST request:
response = Async::HTTP::Internet.post("https://httpbin.org/anything", headers, body)

# Save the response body to a local file:
pp JSON.parse(response.read)
ensure
response&.close
end
~~~

For more complex scenarios, including HTTP APIs, consider using [async-rest](https://github.com/socketry/async-rest) instead.

### Timeouts

To set a timeout for a request, use the `Task#with_timeout` method:

~~~ ruby
require 'async/http'
require 'async/http/internet/instance'

Sync do |task|
# Request will timeout after 2 seconds
task.with_timeout(2) do
response = Async::HTTP::Internet.get "https://httpbin.org/delay/10"
ensure
response&.close
end
rescue Async::TimeoutError
puts "The request timed out"
end
~~~

### Making a Server

To create a server, use an instance of {ruby Async::HTTP::Server}:

~~~ ruby
require 'async/http'

endpoint = Async::HTTP::Endpoint.parse('http://localhost:9292')

Sync do |task|
Async(transient: true) do
server = Async::HTTP::Server.for(endpoint) do |request|
::Protocol::HTTP::Response[200, {}, ["Hello World"]]
end

server.run
end

client = Async::HTTP::Client.new(endpoint)
response = client.get("/")
puts response.read
ensure
response&.close
end
~~~
6 changes: 4 additions & 2 deletions guides/links.yaml
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
mocking:
order: 5
getting-started:
order: 0
testing:
order: 1
6 changes: 4 additions & 2 deletions guides/mocking/readme.md → guides/testing/readme.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# Mocking
# Testing

This guide explains how to modify `Async::HTTP::Client` for mocking responses in tests.
This guide explains how to use `Async::HTTP` clients and servers in your tests.

In general, you should avoid making real HTTP requests in your tests. Instead, you should use a mock server or a fake client.

## Mocking HTTP Responses

Expand Down
16 changes: 12 additions & 4 deletions lib/async/http/internet.rb
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ def client_for(endpoint)
# @parameter url [String] The URL to request, e.g. `https://www.codeotaku.com`.
# @parameter headers [Hash | Protocol::HTTP::Headers] The headers to send with the request.
# @parameter body [String | Protocol::HTTP::Body] The body to send with the request.
def call(method, url, headers = nil, body = nil)
def call(method, url, headers = nil, body = nil, &block)
endpoint = Endpoint[url]
client = self.client_for(endpoint)

Expand All @@ -48,7 +48,15 @@ def call(method, url, headers = nil, body = nil)

request = ::Protocol::HTTP::Request.new(endpoint.scheme, endpoint.authority, method, endpoint.path, nil, headers, body)

return client.call(request)
response = client.call(request)

return response unless block_given?

begin
yield response
ensure
response.close
end
end

def close
Expand All @@ -60,8 +68,8 @@ def close
end

::Protocol::HTTP::Methods.each do |name, verb|
define_method(verb.downcase) do |url, headers = nil, body = nil|
self.call(verb, url, headers, body)
define_method(verb.downcase) do |url, headers = nil, body = nil, &block|
self.call(verb, url, headers, body, &block)
end
end

Expand Down
17 changes: 14 additions & 3 deletions lib/async/http/internet/instance.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,24 @@
# Copyright, 2021-2023, by Samuel Williams.

require_relative '../internet'
require 'thread/local'

::Thread.attr_accessor :async_http_internet_instance

module Async
module HTTP
class Internet
# Provide access to a shared thread-local instance.
extend ::Thread::Local
# The global instance of the internet.
def self.instance
::Thread.current.async_http_internet_instance ||= self.new
end

class << self
::Protocol::HTTP::Methods.each do |name, verb|
define_method(verb.downcase) do |url, headers = nil, body = nil, &block|
self.instance.call(verb, url, headers, body, &block)
end
end
end
end
end
end
Loading

0 comments on commit fcf231a

Please sign in to comment.