Skip to content

Commit

Permalink
The response became an instance of Fetch::Response
Browse files Browse the repository at this point in the history
  • Loading branch information
ursm committed Jul 6, 2024
1 parent ac891ac commit 2978bb5
Show file tree
Hide file tree
Showing 7 changed files with 253 additions and 30 deletions.
17 changes: 14 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ require 'fetch-api'

res = Fetch::API.fetch('https://example.com')

# res is a Rack::Response object
puts res.body.first
# res is a Fetch::Response object
puts res.body
```

or
Expand All @@ -43,13 +43,24 @@ include Fetch::API
res = fetch('https://example.com')
```

Supported options are as follows:
Options for `fetch` method:

- `method`: HTTP method (default: `'GET'`)
- `headers`: Request headers (default: `{}`)
- `body`: Request body (default: `nil`)
- `redirect`: Follow redirects (one of `follow`, `error`, `manual`, default: `follow`)

Methods of `Fetch::Response` object:

- `body`: Response body (String)
- `headers`: Response headers
- `ok`: Whether the response was successful or not (status code is in the range 200-299)
- `redirected`: Whether the response is the result of a redirect
- `status`: Status code (e.g. `200`, `404`)
- `status_text`: Status text (e.g. `'OK'`, `'Not Found'`)
- `url`: Response URL
- `json(**args)`: An object that parses the response body as JSON. The arguments are passed to `JSON.parse`

### Post JSON

``` ruby
Expand Down
31 changes: 24 additions & 7 deletions lib/fetch/client.rb
Original file line number Diff line number Diff line change
@@ -1,21 +1,24 @@
require_relative '../fetch'
require_relative 'form_data'
require_relative 'headers'
require_relative 'response'
require_relative 'url_search_params'

require 'marcel'
require 'net/http'
require 'rack/response'
require 'singleton'
require 'uri'

module Fetch
class Client
include Singleton

def fetch(resource, method: 'GET', headers: {}, body: nil, redirect: 'follow')
def fetch(resource, method: 'GET', headers: [], body: nil, redirect: 'follow', _redirected: false)
uri = URI.parse(resource)
req = Net::HTTP.const_get(method.capitalize).new(uri)

headers = Headers.new(headers) unless headers.is_a?(Headers)

headers.each do |k, v|
req[k] = v
end
Expand Down Expand Up @@ -49,23 +52,37 @@ def fetch(resource, method: 'GET', headers: {}, body: nil, redirect: 'follow')
when Net::HTTPRedirection
case redirect
when 'follow'
fetch(res['Location'], method:, headers:, body:, redirect:)
fetch(res['Location'], method:, headers:, body:, redirect:, _redirected: true)
when 'error'
raise RedirectError, "redirected to #{res['Location']}"
when 'manual'
to_rack_response(res)
to_response(resource, res, _redirected)
else
raise ArgumentError, "invalid redirect option: #{redirect}"
end
else
to_rack_response(res)
to_response(resource, res, _redirected)
end
end

private

def to_rack_response(res)
Rack::Response.new(res.body, res.code, res.to_hash)
def to_response(url, res, redirected)
headers = Headers.new

res.each do |k, vs|
vs.split(', ').each do |v|
headers.append k, v
end
end

Response.new(
url: ,
status: res.code.to_i,
headers: ,
body: res.body,
redirected:
)
end
end
end
50 changes: 50 additions & 0 deletions lib/fetch/headers.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
module Fetch
class Headers
include Enumerable

def initialize(init = [])
@entries = []

init.each do |k, v|
append k, v
end
end

attr_reader :entries

def append(key, value)
@entries << [key.to_s.downcase, value]
end

def delete(key)
@entries.delete_if {|k,| k == key.to_s.downcase }
end

def get(key)
@entries.select {|k,| k == key.to_s.downcase }.map(&:last).join(', ')
end

def has(key)
@entries.any? {|k,| k == key.to_s.downcase }
end

def keys
@entries.map(&:first)
end

def set(key, value)
delete key
append key, value
end

def values
@entries.map(&:last)
end

def each(&block)
return enum_for(:each) unless block_given?

@entries.each(&block)
end
end
end
18 changes: 18 additions & 0 deletions lib/fetch/response.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
require 'json'
require 'rack/utils'

module Fetch
Response = Data.define(:url, :status, :headers, :body, :redirected) {
def ok
status.between?(200, 299)
end

def status_text
Rack::Utils::HTTP_STATUS_CODES[status]
end

def json(**opts)
JSON.parse(body, **opts)
end
}
end
44 changes: 24 additions & 20 deletions spec/fetch/api_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,25 +4,35 @@
include Fetch::API

example 'simple get' do
stub_request :get, 'http://example.com'
stub_request(:get, 'http://example.com').to_return(
headers: {
'Content-Type' => 'text/plain'
},

body: 'Hello, world!'
)

res = fetch('http://example.com')

expect(res).to be_ok
expect(res.url).to eq('http://example.com')
expect(res.status).to eq(200)
expect(res.headers.to_h).to eq('content-type' => 'text/plain')
expect(res.body).to eq('Hello, world!')
expect(res.redirected).to eq(false)
end

example 'https' do
stub_request :get, 'https://example.com'

res = fetch('https://example.com')

expect(res).to be_ok
expect(res.status).to eq(200)
end

example 'post JSON' do
stub_request :post, 'http://example.com'

res = fetch('http://example.com', **{
fetch 'http://example.com', **{
method: 'POST',

headers: {
Expand All @@ -32,9 +42,7 @@
body: {
name: 'Alice'
}.to_json
})

expect(res).to be_ok
}

expect(WebMock).to have_requested(:post, 'http://example.com').with(
headers: {
Expand All @@ -48,12 +56,10 @@
example 'post form' do
stub_request :post, 'http://example.com'

res = fetch('http://example.com', **{
fetch 'http://example.com', **{
method: 'POST',
body: Fetch::URLSearchParams.new(name: 'Alice')
})

expect(res).to be_ok
}

expect(WebMock).to have_requested(:post, 'http://example.com').with(
headers: {
Expand All @@ -67,10 +73,8 @@
example 'post multipart' do
stub_request :post, 'http://example.com'

res = nil

File.open 'spec/fixtures/files/foo.txt' do |f|
res = fetch('http://example.com', **{
fetch 'http://example.com', **{
method: 'POST',

headers: {
Expand All @@ -81,11 +85,9 @@
name: 'Alice',
file: f
)
})
}
end

expect(res).to be_ok

expect(WebMock).to have_requested(:post, 'http://example.com').with(
headers: {
'Content-Type' => 'multipart/form-data'
Expand All @@ -104,7 +106,8 @@

res = fetch('http://example.com', redirect: 'follow')

expect(res).to be_ok
expect(res.status).to eq(200)
expect(res.redirected).to eq(true)

expect(WebMock).to have_requested(:get, 'http://example.com/redirected')
end
Expand All @@ -115,7 +118,7 @@
})

expect {
fetch('http://example.com', redirect: 'error')
fetch 'http://example.com', redirect: 'error'
}.to raise_error(Fetch::RedirectError)
end

Expand All @@ -126,6 +129,7 @@

res = fetch('http://example.com', redirect: 'manual')

expect(res).to be_redirect
expect(res.status).to eq(302)
expect(res.redirected).to eq(false)
end
end
96 changes: 96 additions & 0 deletions spec/fetch/headers_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
require 'spec_helper'

RSpec.describe Fetch::Headers do
example 'append' do
headers = Fetch::Headers.new

headers.append :foo, 'bar'
headers.append :foo, 'baz'

expect(headers.entries).to eq([
['foo', 'bar'],
['foo', 'baz']
])
end

example 'delete' do
headers = Fetch::Headers.new([
[:foo, 'bar'],
[:baz, 'qux'],
[:baz, 'quux']
])

headers.delete :baz

expect(headers.entries).to eq([
['foo', 'bar']
])
end

example 'get' do
headers = Fetch::Headers.new([
[:foo, 'bar'],
[:baz, 'qux'],
[:baz, 'quux']
])

expect(headers.get(:foo)).to eq('bar')
expect(headers.get(:baz)).to eq('qux, quux')
end

example 'has' do
headers = Fetch::Headers.new(foo: 'bar')

expect(headers.has(:foo)).to eq(true)
expect(headers.has(:bar)).to eq(false)
end

example 'keys' do
headers = Fetch::Headers.new([
[:foo, 'bar'],
[:baz, 'qux'],
[:baz, 'quux']
])

expect(headers.keys).to eq(%w[foo baz baz])
end

example 'set' do
headers = Fetch::Headers.new([
[:foo, 'bar'],
[:baz, 'qux'],
[:baz, 'quux']
])

headers.set :baz, 'foobar'

expect(headers.entries).to eq([
['foo', 'bar'],
['baz', 'foobar']
])
end

example 'values' do
headers = Fetch::Headers.new([
[:foo, 'bar'],
[:baz, 'qux'],
[:baz, 'quux']
])

expect(headers.values).to eq(%w[bar qux quux])
end

example 'each' do
headers = Fetch::Headers.new([
[:foo, 'bar'],
[:baz, 'qux'],
[:baz, 'quux']
])

expect(headers.to_a).to eq([
['foo', 'bar'],
['baz', 'qux'],
['baz', 'quux']
])
end
end
Loading

0 comments on commit 2978bb5

Please sign in to comment.