Skip to content

Commit bdf3182

Browse files
Give the handler a request object rather than copying every header out.
1 parent 12b547b commit bdf3182

File tree

5 files changed

+124
-55
lines changed

5 files changed

+124
-55
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,4 @@
1212
*.a
1313
mkmf.log
1414
target/
15+
.idea/

Gemfile

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,5 @@ gem "rake-compiler"
1111
gem "rb_sys", "~> 0.9.63"
1212

1313
gem "minitest", "~> 5.16"
14+
15+
gem "httpx", "~> 1.4"

Gemfile.lock

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ PATH
66
GEM
77
remote: https://rubygems.org/
88
specs:
9+
http-2 (1.0.2)
10+
httpx (1.4.0)
11+
http-2 (>= 1.0.0)
912
minitest (5.25.4)
1013
rake (13.2.1)
1114
rake-compiler (1.2.9)
@@ -19,6 +22,7 @@ PLATFORMS
1922
ruby
2023

2124
DEPENDENCIES
25+
httpx (~> 1.4)
2226
hyper_ruby!
2327
minitest (~> 5.16)
2428
rake (~> 13.0)

ext/hyper_ruby/src/lib.rs

Lines changed: 68 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use magnus::block::block_proc;
22
use magnus::r_hash::ForEach;
33
use magnus::typed_data::Obj;
4-
use magnus::value::Opaque;
4+
use magnus::value::{qnil, Opaque};
55
use magnus::{function, gc, method, prelude::*, DataTypeFunctions, Error as MagnusError, RString, Ruby, TypedData, Value};
66
use bytes::Bytes;
77

@@ -35,15 +35,56 @@ impl ServerConfig {
3535
}
3636
}
3737

38+
struct RequestWithCompletion {
39+
request: Request,
40+
// sent a response back on this thread
41+
response_tx: oneshot::Sender<WarpResponse<Bytes>>,
42+
}
43+
3844
// Request type that will be sent to worker threads
3945
#[derive(Debug)]
40-
struct WorkRequest {
46+
#[magnus::wrap(class = "HyperRuby::Request")]
47+
struct Request {
4148
method: warp::http::Method,
4249
path: String,
4350
headers: warp::http::HeaderMap,
4451
body: Bytes,
45-
// sent a response back on this thread
46-
response_tx: oneshot::Sender<WarpResponse<Vec<u8>>>,
52+
}
53+
54+
impl Request {
55+
pub fn method(&self) -> String {
56+
self.method.to_string()
57+
}
58+
59+
pub fn path(&self) -> RString {
60+
RString::new(&self.path)
61+
}
62+
63+
pub fn header(&self, key: String) -> Value {
64+
match self.headers.get(key) {
65+
Some(value) => match value.to_str() {
66+
Ok(value) => RString::new(value).as_value(),
67+
Err(_) => qnil().as_value(),
68+
},
69+
None => qnil().as_value(),
70+
}
71+
}
72+
73+
pub fn body_size(&self) -> usize {
74+
self.body.len()
75+
}
76+
77+
pub fn body(&self) -> Value {
78+
if self.body.is_empty() {
79+
return qnil().as_value();
80+
}
81+
82+
let result = RString::buf_new(self.body_size());
83+
84+
// cat to the end of the string directly from the byte buffer
85+
result.cat(self.body.as_ref());
86+
result.as_value()
87+
}
4788
}
4889

4990

@@ -76,8 +117,8 @@ impl Response {
76117
struct Server {
77118
server_handle: Arc<Mutex<Option<JoinHandle<()>>>>,
78119
config: RefCell<ServerConfig>,
79-
work_rx: RefCell<Option<crossbeam_channel::Receiver<WorkRequest>>>,
80-
work_tx: RefCell<Option<Arc<crossbeam_channel::Sender<WorkRequest>>>>,
120+
work_rx: RefCell<Option<crossbeam_channel::Receiver<RequestWithCompletion>>>,
121+
work_tx: RefCell<Option<Arc<crossbeam_channel::Sender<RequestWithCompletion>>>>,
81122
}
82123

83124

@@ -134,39 +175,20 @@ impl Server {
134175

135176
match work_request {
136177
Ok(work_request) => {
137-
138-
// Create Ruby hash with request data
139-
let req_hash = magnus::RHash::new();
140-
req_hash.aset(magnus::Symbol::new("method"), work_request.method.to_string())?;
141-
req_hash.aset(magnus::Symbol::new("path"), work_request.path)?;
142-
143-
// Convert headers to Ruby hash
144-
let headers_hash = magnus::RHash::new();
145-
for (key, value) in work_request.headers.iter() {
146-
if let Ok(value_str) = value.to_str() {
147-
headers_hash.aset(key.as_str(), value_str)?;
148-
}
149-
}
150-
req_hash.aset(magnus::Symbol::new("headers"), headers_hash)?;
151-
152-
// Convert body to Ruby string
153-
req_hash.aset(magnus::Symbol::new("body"), magnus::RString::from_slice(&work_request.body[..]))?;
154-
155178
// Call the Ruby block and handle the response
156-
let warp_response = match block.call::<_, Value>([req_hash]) {
179+
let warp_response = match block.call::<_, Value>((work_request.request,)) {
157180
Ok(result) => {
158181
let ref_response = Obj::<Response>::try_convert(result).unwrap();
159182

160-
let mut response: WarpResponse<Vec<u8>>;
183+
let mut response: WarpResponse<Bytes>;
161184
let ruby = Ruby::get().unwrap(); // errors on non-Ruby thread
162185
let response_body = ruby.get_inner(ref_response.body);
163186
let ruby_response_headers = ruby.get_inner(ref_response.headers);
164187

165188
// safe because RString will not be cleared here before we copy the bytes into our own Vector.
166189
unsafe {
167190
// copy directly to bytes here so we don't have to worry about encoding checks
168-
let rust_body = Vec::from(response_body.as_slice());
169-
191+
let rust_body = Bytes::copy_from_slice(response_body.as_slice());
170192
response = WarpResponse::new(rust_body);
171193
}
172194

@@ -287,8 +309,8 @@ impl Server {
287309

288310

289311
// Helper function to create error responses
290-
fn create_error_response(error_message: &str) -> WarpResponse<Vec<u8>> {
291-
let mut response = WarpResponse::new(format!(r#"{{"error": "{}"}}"#, error_message).into_bytes());
312+
fn create_error_response(error_message: &str) -> WarpResponse<Bytes> {
313+
let mut response = WarpResponse::new(Bytes::from(format!(r#"{{"error": "{}"}}"#, error_message)));
292314
*response.status_mut() = warp::http::StatusCode::INTERNAL_SERVER_ERROR;
293315
response.headers_mut().insert(
294316
warp::http::header::CONTENT_TYPE,
@@ -302,19 +324,23 @@ async fn handle_request(
302324
path: warp::path::FullPath,
303325
headers: warp::http::HeaderMap,
304326
body: Bytes,
305-
work_tx: Arc<crossbeam_channel::Sender<WorkRequest>>,
306-
) -> Result<WarpResponse<Vec<u8>>, warp::Rejection> {
327+
work_tx: Arc<crossbeam_channel::Sender<RequestWithCompletion>>,
328+
) -> Result<WarpResponse<Bytes>, warp::Rejection> {
307329
let (response_tx, response_rx) = oneshot::channel();
308330

309-
let work_request = WorkRequest {
331+
let request = Request {
310332
method,
311333
path: path.as_str().to_string(),
312334
headers,
313-
body,
335+
body
336+
};
337+
338+
let with_completion = RequestWithCompletion {
339+
request,
314340
response_tx,
315341
};
316342

317-
if let Err(_) = work_tx.send(work_request) {
343+
if let Err(_) = work_tx.send(with_completion) {
318344
return Err(warp::reject::reject());
319345
}
320346

@@ -338,5 +364,12 @@ fn init(ruby: &Ruby) -> Result<(), MagnusError> {
338364
let response_class = module.define_class("Response", ruby.class_object())?;
339365
response_class.define_singleton_method("new", function!(Response::new, 3))?;
340366

367+
let request_class = module.define_class("Request", ruby.class_object())?;
368+
request_class.define_method("http_method", method!(Request::method, 0))?;
369+
request_class.define_method("path", method!(Request::path, 0))?;
370+
request_class.define_method("header", method!(Request::header, 1))?;
371+
request_class.define_method("body", method!(Request::body, 0))?;
372+
request_class.define_method("body_size", method!(Request::body_size, 0))?;
373+
341374
Ok(())
342375
}

test/test_hyper_ruby.rb

Lines changed: 49 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,69 @@
11
# frozen_string_literal: true
22

33
require "test_helper"
4+
require "httpx"
45

56
class TestHyperRuby < Minitest::Test
67
def test_that_it_has_a_version_number
78
refute_nil ::HyperRuby::VERSION
89
end
910

10-
def test_it_does_something_useful
11+
def test_simple_get
12+
with_server(-> (request) { handler_simple(request) }) do |client|
13+
response = client.get("/")
14+
assert_equal 200, response.status
15+
assert_equal "text/plain", response.headers["content-type"]
16+
assert_equal 'GET', response.body
17+
end
18+
end
19+
20+
def test_simple_post
21+
with_server(-> (request) { handler_to_json(request) }) do |client|
22+
response = client.post("/", body: "Hello")
23+
assert_equal 200, response.status
24+
assert_equal "application/json", response.headers["content-type"]
25+
assert_equal 'Hello', JSON.parse(response.body)["message"]
26+
end
27+
end
28+
29+
def test_large_post
30+
with_server(-> (request) { handler_to_json(request) }) do |client|
31+
response = client.post("/", body: "a" * 10_000_000)
32+
assert_equal 200, response.status
33+
assert_equal "application/json", response.headers["content-type"]
34+
assert_equal 'a' * 10_000_000, JSON.parse(response.body)["message"]
35+
end
36+
end
37+
38+
def with_server(request_handler, &block)
1139
server = HyperRuby::Server.new
1240
server.configure({ bind_address: "127.0.0.1:3010" })
13-
handler = Handler.new
1441
server.start
15-
42+
1643
# Create ruby worker threads that process requests;
17-
# 1 is usually enough, and generally handles better than multiple threads
18-
threads = 1.times.map do
19-
Thread.new do
20-
server.run_worker do |request|
21-
# Process the request in Ruby
22-
# request is a hash with :method, :path, :headers, and :body keys
23-
#puts "Processing #{request[:method]} request to #{request[:path]}"
24-
25-
# Return response string
26-
HyperRuby::Response.new(200, { 'Content-Type' => 'application/json' }, '{ "message": "Hello from Ruby worker!" }')
27-
end
44+
# 1 is usually enough, and generally handles better than multiple threads
45+
# if there's no IO (because of the GIL)
46+
worker = Thread.new do
47+
server.run_worker do |request|
48+
# Process the request in Ruby
49+
# request is a hash with :method, :path, :headers, and :body keys
50+
request_handler.call(request)
2851
end
2952
end
3053

31-
gets
32-
server.stop
54+
client = HTTPX.with(origin: "http://127.0.0.1:3010")
55+
block.call(client)
56+
57+
ensure
58+
server.stop if server
59+
worker.join if worker
3360
end
3461

35-
class Handler
36-
def call(req_info)
37-
"Hello, world!"
38-
end
62+
def handler_simple(request)
63+
HyperRuby::Response.new(200, { 'Content-Type' => 'text/plain' }, request.http_method)
64+
end
65+
66+
def handler_to_json(request)
67+
HyperRuby::Response.new(200, { 'Content-Type' => 'application/json' }, { message: request.body }.to_json)
3968
end
4069
end

0 commit comments

Comments
 (0)