Skip to content

Commit

Permalink
Improve BaseHTTPClient
Browse files Browse the repository at this point in the history
  • Loading branch information
paulcsmith committed Aug 24, 2019
1 parent ed1e51b commit 7e6185c
Show file tree
Hide file tree
Showing 3 changed files with 186 additions and 45 deletions.
128 changes: 97 additions & 31 deletions spec/lucky/base_http_client_spec.cr
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
require "../spec_helper"

server = TestServer.new(6226)
server = TestServer.new(test_server_port)

spawn do
server.listen
Expand All @@ -10,58 +10,124 @@ Spec.before_each do
TestServer.reset
end

class HelloWorldAction < TestAction
post "/hello" do
plain_text "unused"
end
end

class MyClient < Lucky::BaseHTTPClient
end

describe Lucky::BaseHTTPClient do
describe "#get" do
it "sends requests to correct uri (with the correct query params) and gives the right response" do
TestServer.route("hello", "world")
Lucky::Server.temp_config(host: "localhost", port: 6226) do
client = Lucky::BaseHTTPClient.new
response = client.get("hello", params: HTTP::Params.new(raw_params: {"foo" => ["bar"]}))

response.body.should eq "world"

request = server.last_request.not_nil!
params = request.query.not_nil!
params.should eq HTTP::Params.encode({"foo" => "bar"})
describe "headers" do
it "sets headers and allows chaining" do
with_fake_server(path: "/hello", response_body: "world") do
MyClient.new
.headers(accept: "text/csv")
.headers(content_type: "application/json")
.headers("Foo": "bar")
.exec(HelloWorldAction)

request = server.last_request
request.headers["accept"].should eq("text/csv")
request.headers["content-type"].should eq("application/json")
request.headers["Foo"].should eq("bar")
end
end
end

describe "#delete" do
it "sends requests to correct uri (with the correct query params) and gives the right response" do
TestServer.route("hello", "world")
Lucky::Server.temp_config(host: "localhost", port: 6226) do
client = Lucky::BaseHTTPClient.new
response = client.delete("hello")
describe "exec" do
describe "with Lucky::Action class" do
it "uses the method and path" do
with_fake_server(path: "/hello", response_body: "world") do
response = MyClient.new.exec(HelloWorldAction)

response.body.should eq "world"
request = server.last_request
request.path.should eq "/hello"
request.method.should eq("POST")
request.body.not_nil!.gets_to_end.should eq("{}")
response.body.should eq "world"
end
end

it "allows passing params" do
with_fake_server(path: "/hello", response_body: "world") do
response = MyClient.new.exec(HelloWorldAction, foo: "bar")

request = server.last_request
request.body.not_nil!.gets_to_end.should eq({foo: "bar"}.to_json)
end
end
end
end

{% for method in [:put, :patch, :post] %}
describe "with a Lucky::RouteHelper" do
it "uses the method and path" do
with_fake_server(path: "/hello", response_body: "world") do
response = MyClient.new.exec(HelloWorldAction.route)

request = server.last_request
request.path.should eq "/hello"
request.method.should eq("POST")
request.body.not_nil!.gets_to_end.should eq("{}")
response.body.should eq "world"
end
end

it "allows passing params" do
with_fake_server(path: "/hello", response_body: "world") do
response = MyClient.new.exec(HelloWorldAction.route, foo: "bar")

request = server.last_request
request.body.not_nil!.gets_to_end.should eq({foo: "bar"}.to_json)
end
end
end
end

{% for method in [:put, :patch, :post, :delete, :get] %}
describe "\#{{method.id}}" do
it "sends correct request to correct uri and gives the correct response" do
TestServer.route("hello", "world")
Lucky::Server.temp_config(host: "localhost", port: 6226) do
client = Lucky::BaseHTTPClient.new
response = client.{{method.id}}(
with_fake_server(path: "hello", response_body: "world") do
response = MyClient.new.{{method.id}}(
path: "hello",
body: { "foo" => "bar" }
foo: "bar"
)

response.body.should eq "world"
request = server.last_request
request.method.should eq({{ method.id.stringify }}.upcase)
request.path.should eq "hello"
request.body.not_nil!.gets_to_end.should eq({foo: "bar"}.to_json)
end
end

request = server.last_request.not_nil!
body = request.body.not_nil!
body.gets_to_end.should eq HTTP::Params.encode({ "foo" => "bar" })
it "works without params" do
with_fake_server(path: "hello", response_body: "world") do
response = MyClient.new.{{method.id}}(path: "hello")

response.body.should eq "world"
request = server.last_request
request.method.should eq({{ method.id.stringify }}.upcase)
request.path.should eq "hello"
request.body.not_nil!.gets_to_end.should eq("{}")
end
end
end

{% end %}
end

private def with_fake_server(path : String, response_body : String)
TestServer.route(path: path, response_body: response_body)
Lucky::Server.temp_config(host: "localhost", port: test_server_port) do
yield
end
end

private def test_server_port
6226
end

at_exit do
server.close
end
5 changes: 3 additions & 2 deletions spec/support/test_server.cr
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@ class TestServer

class_property routes : Hash(String, String) = {} of String => String

property last_request : HTTP::Request?
property! last_request : HTTP::Request?
getter port

def initialize(port : Int32)
def initialize(@port : Int32)
@server = HTTP::Server.new do |context|
last_request = context.request.dup
last_request.body = last_request.body.try(&.peek)
Expand Down
98 changes: 86 additions & 12 deletions src/lucky/base_http_client.cr
Original file line number Diff line number Diff line change
@@ -1,28 +1,102 @@
require "http/client"

class Lucky::BaseHTTPClient
# A client for making HTTP requests
#
# Makes it easy to pass params, use Lucky route helpers, and chain header methods.
abstract class Lucky::BaseHTTPClient
private getter client

@client : HTTP::Client

def initialize(host = Lucky::Server.settings.host, port = Lucky::Server.settings.port)
@client = HTTP::Client.new(host, port: port)
end

def get(path : String, headers : HTTP::Headers? = nil, params : HTTP::Params? = nil)
if params
path = path + "?#{params}"
{% for method in [:get, :put, :patch, :post] %}
def self.{{ method.id }}(*args, **named_args)
new.{{ method.id }}(*args, **named_args)
end
@client.get(path, headers: headers)
end
{% end %}

{% for method in [:put, :patch, :post] %}
# Set headers for requests
#
# ```
# # `content_type` will be normalized to `content-type`
# AppClient.new.headers(content_type: "application/json")
#
# # You can also use string keys if you want
# AppClient.new.headers("Content-Type": "application/json")
# ```

def {{method.id}}(path : String, body : Hash(String, String), headers : HTTP::Headers? = nil)
@client.{{method.id}}(path, headers: headers, form: body)
# The header call is chainable and returns the client:
#
# ```
# # content_type will be normalized to `content-type`
# AppClient.new
# .headers(content_type: "application/json")
# .headers(accept: "text/plain")
# .get("/some-path")
# ```
#
# You can also set up headers in `initialize` or in instance methods:
#
# ```
# class AppClient < Lucky::BaseHTTPClient
# def initialize
# headers(content_type: "application/json")
# end
#
# def accept_plain_text
# headers(accept: "text/plain")
# end
# end
#
# AppClient.new
# .accept_plain_text
# .get("/some-path")
# ```
def headers(**header_values)
@client.before_request do |request|
header_values.each do |key, value|
request.headers[key.to_s.gsub("-", "_")] = value.to_s
end
end
self
end

{% end %}
# Sends a request with the path and method from a Lucky::Action
#
# ```
# # Make a request without body params
# AppClient.new.exec Users::Index
#
# # Make a request with body params
# AppClient.new.exec Users::Create, user: {email: "[email protected]"}
#
# # Actions that require path params work like normal
# AppClient.new.exec Users::Show.with(user.id)
# ```
def exec(action : Lucky::Action.class, **params) : HTTP::Client::Response
exec(action.route, params)
end

def delete(path : String, headers : HTTP::Headers? = nil)
@client.delete(path, headers: headers)
# See docs for `exec`
def exec(route_helper : Lucky::RouteHelper, **params) : HTTP::Client::Response
exec(route_helper, params)
end

# See docs for `exec`
def exec(route_helper : Lucky::RouteHelper, params : NamedTuple) : HTTP::Client::Response
@client.exec(method: route_helper.method.to_s.upcase, path: route_helper.path, body: params.to_json)
end

{% for method in [:put, :patch, :post, :delete, :get] %}
def {{ method.id }}(path : String, **params) : HTTP::Client::Response
{{ method.id }}(path, params)
end

def {{ method.id }}(path : String, params : NamedTuple) : HTTP::Client::Response
@client.{{ method.id }}(path, form: params.to_json)
end
{% end %}
end

0 comments on commit 7e6185c

Please sign in to comment.