diff --git a/Cargo.lock b/Cargo.lock index 61ee018ad..40d943272 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -198,6 +198,12 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" +[[package]] +name = "bytes" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b" + [[package]] name = "cc" version = "1.2.10" @@ -391,6 +397,7 @@ dependencies = [ "encoding_rs", "futures-test", "futures-util", + "http", "http-types-red-badger-temporary-fork", "pin-project-lite", "serde", @@ -835,6 +842,17 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "http" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f16ca2af56261c99fba8bac40a10251ce8188205a4c448fbb745a2e4daa76fea" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + [[package]] name = "http-types-red-badger-temporary-fork" version = "2.12.0" diff --git a/crux_http/Cargo.toml b/crux_http/Cargo.toml index d5bd79c0e..6631fd3f4 100644 --- a/crux_http/Cargo.toml +++ b/crux_http/Cargo.toml @@ -15,6 +15,7 @@ default = ["encoding"] # requires web-sys for TextDecoder on wasm encoding = ["encoding_rs", "web-sys"] typegen = ["crux_core/typegen"] +http-compat = ["dep:http"] [dependencies] anyhow.workspace = true @@ -25,6 +26,7 @@ encoding_rs = { version = "0.8.35", optional = true } futures-util = "0.3" http-types = { package = "http-types-red-badger-temporary-fork", version = "2.12.0", default-features = false } pin-project-lite = "0.2.16" +http = { version = "1.1", optional = true } serde = { workspace = true, features = ["derive"] } serde_bytes = "0.11" serde_json = "1.0.137" diff --git a/crux_http/src/client.rs b/crux_http/src/client.rs index 6ac700b89..6098988d6 100644 --- a/crux_http/src/client.rs +++ b/crux_http/src/client.rs @@ -1,10 +1,10 @@ use std::fmt; use std::sync::Arc; -use crate::http::{Method, Url}; use crate::middleware::{Middleware, Next}; use crate::protocol::{EffectSender, HttpResult, ProtocolRequestBuilder}; use crate::{Config, Request, RequestBuilder, ResponseAsync, Result}; +use http_types::{Method, Url}; /// An HTTP client, capable of sending `Request`s /// diff --git a/crux_http/src/config.rs b/crux_http/src/config.rs index 75defeff0..e23072006 100644 --- a/crux_http/src/config.rs +++ b/crux_http/src/config.rs @@ -2,9 +2,11 @@ use std::{collections::HashMap, fmt::Debug}; -use http_types::headers::{HeaderName, HeaderValues, ToHeaderValues}; +use http_types::{ + headers::{HeaderName, HeaderValues, ToHeaderValues}, + Url, +}; -use crate::http::Url; use crate::Result; /// Configuration for `crux_http::Http`s and their underlying HTTP client. diff --git a/crux_http/src/error.rs b/crux_http/src/error.rs index 76dcb1470..b46e4bed2 100644 --- a/crux_http/src/error.rs +++ b/crux_http/src/error.rs @@ -6,7 +6,7 @@ pub enum HttpError { #[error("HTTP error {code}: {message}")] #[serde(skip)] Http { - code: crate::http::StatusCode, + code: http_types::StatusCode, message: String, body: Option>, }, @@ -21,8 +21,8 @@ pub enum HttpError { Timeout, } -impl From for HttpError { - fn from(e: crate::http::Error) -> Self { +impl From for HttpError { + fn from(e: http_types::Error) -> Self { HttpError::Http { code: e.status(), message: e.to_string(), @@ -50,7 +50,7 @@ mod tests { #[test] fn test_error_display() { let error = HttpError::Http { - code: crate::http::StatusCode::BadRequest, + code: http_types::StatusCode::BadRequest, message: "Bad Request".to_string(), body: None, }; diff --git a/crux_http/src/lib.rs b/crux_http/src/lib.rs index 7364d837a..9e018120b 100644 --- a/crux_http/src/lib.rs +++ b/crux_http/src/lib.rs @@ -6,7 +6,7 @@ // #![warn(missing_docs)] use crux_core::capability::CapabilityContext; -use http::Method; +use http_types::Method; use url::Url; mod config; @@ -21,7 +21,7 @@ pub mod middleware; pub mod protocol; pub mod testing; -pub use http_types::{self as http}; +pub use http_types; pub use self::{ config::Config, @@ -307,7 +307,7 @@ where /// /// When finished, the response will be wrapped in an event and dispatched to /// the app's `update function. - pub fn request(&self, method: http::Method, url: Url) -> RequestBuilder { + pub fn request(&self, method: http_types::Method, url: Url) -> RequestBuilder { RequestBuilder::new(method, url, self.clone()) } } diff --git a/crux_http/src/middleware/redirect.rs b/crux_http/src/middleware/redirect.rs index 2f6f96305..68d316f09 100644 --- a/crux_http/src/middleware/redirect.rs +++ b/crux_http/src/middleware/redirect.rs @@ -14,9 +14,9 @@ //! # } //! ``` -use crate::http::{self, headers, StatusCode, Url}; use crate::middleware::{Middleware, Next, Request}; use crate::{Client, ResponseAsync, Result}; +use http_types::{headers, StatusCode, Url}; // List of acceptable 300-series redirect codes. const REDIRECT_CODES: &[StatusCode] = &[ @@ -107,14 +107,14 @@ impl Middleware for Redirect { let res: ResponseAsync = client.send(r).await?; if REDIRECT_CODES.contains(&res.status()) { if let Some(location) = res.header(headers::LOCATION) { - let http_req: &mut http::Request = req.as_mut(); + let http_req: &mut http_types::Request = req.as_mut(); *http_req.url_mut() = match Url::parse(location.last().as_str()) { Ok(valid_url) => { base_url = valid_url; base_url.clone() } Err(e) => match e { - http::url::ParseError::RelativeUrlWithoutBase => { + http_types::url::ParseError::RelativeUrlWithoutBase => { base_url.join(location.last().as_str())? } e => return Err(e.into()), diff --git a/crux_http/src/protocol.rs b/crux_http/src/protocol.rs index be26b4607..1b6ab723d 100644 --- a/crux_http/src/protocol.rs +++ b/crux_http/src/protocol.rs @@ -212,7 +212,7 @@ impl ProtocolRequestBuilder for crate::Request { impl From for crate::ResponseAsync { fn from(effect_response: HttpResponse) -> Self { - let mut res = crate::http::Response::new(effect_response.status); + let mut res = http_types::Response::new(effect_response.status); res.set_body(effect_response.body); for header in effect_response.headers { res.append_header(header.name.as_str(), header.value); diff --git a/crux_http/src/request.rs b/crux_http/src/request.rs index 823690d1f..6c87a4740 100644 --- a/crux_http/src/request.rs +++ b/crux_http/src/request.rs @@ -1,9 +1,8 @@ -use crate::http::{ - self, +use crate::middleware::Middleware; +use http_types::{ headers::{self, HeaderName, HeaderValues, ToHeaderValues}, Body, Method, Mime, Url, }; -use crate::middleware::Middleware; use serde::Serialize; @@ -15,7 +14,7 @@ use std::sync::Arc; #[derive(Clone)] pub struct Request { /// Holds the state of the request. - req: http::Request, + req: http_types::Request, /// Holds an optional per-request middleware stack. middleware: Option>>, } @@ -31,14 +30,14 @@ impl Request { /// /// ``` /// fn main() -> crux_http::Result<()> { - /// use crux_http::http::{Url, Method}; + /// use crux_http::http_types::{Url, Method}; /// /// let url = Url::parse("https://httpbin.org/get")?; /// let req = crux_http::Request::new(Method::Get, url); /// # Ok(()) } /// ``` pub fn new(method: Method, url: Url) -> Self { - let req = http::Request::new(method, url); + let req = http_types::Request::new(method, url); Self { req, middleware: None, @@ -199,7 +198,7 @@ impl Request { /// # struct Capabilities { http: crux_http::Http } /// # fn update(caps: &Capabilities) -> crux_http::Result<()> { /// let req = caps.http.get("https://httpbin.org/get").build(); - /// assert_eq!(req.method(), crux_http::http::Method::Get); + /// assert_eq!(req.method(), crux_http::http_types::Method::Get); /// # Ok(()) } /// ``` pub fn method(&self) -> Method { @@ -214,7 +213,7 @@ impl Request { /// # enum Event {} /// # struct Capabilities { http: crux_http::Http } /// # fn update(caps: &Capabilities) -> crux_http::Result<()> { - /// use crux_http::http::Url; + /// use crux_http::http_types::Url; /// let req = caps.http.get("https://httpbin.org/get").build(); /// assert_eq!(req.url(), &Url::parse("https://httpbin.org/get")?); /// # Ok(()) } @@ -362,33 +361,33 @@ impl Request { } } -impl AsRef for Request { - fn as_ref(&self) -> &http::Headers { +impl AsRef for Request { + fn as_ref(&self) -> &http_types::Headers { self.req.as_ref() } } -impl AsMut for Request { - fn as_mut(&mut self) -> &mut http::Headers { +impl AsMut for Request { + fn as_mut(&mut self) -> &mut http_types::Headers { self.req.as_mut() } } -impl AsRef for Request { - fn as_ref(&self) -> &http::Request { +impl AsRef for Request { + fn as_ref(&self) -> &http_types::Request { &self.req } } -impl AsMut for Request { - fn as_mut(&mut self) -> &mut http::Request { +impl AsMut for Request { + fn as_mut(&mut self) -> &mut http_types::Request { &mut self.req } } -impl From for Request { - /// Converts an `http::Request` to a `crux_http::Request`. - fn from(req: http::Request) -> Self { +impl From for Request { + /// Converts an `http_types::Request` to a `crux_http::Request`. + fn from(req: http_types::Request) -> Self { Self { req, middleware: None, @@ -396,10 +395,30 @@ impl From for Request { } } +#[cfg(feature = "http-compat")] +impl> TryFrom> for Request { + type Error = anyhow::Error; + + fn try_from(req: http::Request) -> Result { + use std::str::FromStr; + let mut o = Request::new( + Method::from_str(req.method().as_str()).map_err(|e| anyhow::anyhow!(e))?, + req.uri().to_string().parse()?, + ); + + for (k, v) in req.headers().iter() { + o.append_header(k.as_str(), v.to_str()?); + } + + o.set_body(req.into_body()); + Ok(o) + } +} + #[allow(clippy::from_over_into)] -impl Into for Request { - /// Converts a `crux_http::Request` to an `http::Request`. - fn into(self) -> http::Request { +impl Into for Request { + /// Converts a `crux_http::Request` to an `http_types::Request`. + fn into(self) -> http_types::Request { self.req } } @@ -414,7 +433,7 @@ impl IntoIterator for Request { type Item = (HeaderName, HeaderValues); type IntoIter = headers::IntoIter; - /// Returns a iterator of references over the remaining items. + /// Returns an iterator of references over the remaining items. #[inline] fn into_iter(self) -> Self::IntoIter { self.req.into_iter() diff --git a/crux_http/src/request_builder.rs b/crux_http/src/request_builder.rs index effb43c5f..3628252a6 100644 --- a/crux_http/src/request_builder.rs +++ b/crux_http/src/request_builder.rs @@ -1,16 +1,14 @@ +use crate::expect::ResponseExpectation; use crate::expect::{ExpectBytes, ExpectJson, ExpectString}; use crate::middleware::Middleware; -use crate::{ - expect::ResponseExpectation, - http::{ - headers::{HeaderName, ToHeaderValues}, - Body, Method, Mime, Url, - }, -}; use crate::{Client, HttpError, Request, Response, ResponseAsync, Result}; use futures_util::future::BoxFuture; -use http_types::convert::DeserializeOwned; +use http_types::{ + convert::DeserializeOwned, + headers::{HeaderName, ToHeaderValues}, + Body, Method, Mime, Url, +}; use serde::Serialize; use std::{fmt, marker::PhantomData}; @@ -23,7 +21,7 @@ use std::{fmt, marker::PhantomData}; /// # Examples /// /// ```no_run -/// use crux_http::http::{mime::HTML}; +/// use crux_http::http_types::{mime::HTML}; /// # enum Event { ReceiveResponse(crux_http::Result>>) } /// # struct Capabilities { http: crux_http::Http } /// # fn update(caps: &Capabilities) { @@ -107,7 +105,7 @@ where /// # Examples /// /// ```no_run - /// # use crux_http::http::mime; + /// # use crux_http::http_types::mime; /// # enum Event { ReceiveResponse(crux_http::Result>>) } /// # struct Capabilities { http: crux_http::Http } /// # fn update(caps: &Capabilities) { @@ -137,7 +135,7 @@ where /// # struct Capabilities { http: crux_http::Http } /// # fn update(caps: &Capabilities) { /// use serde_json::json; - /// use crux_http::http::mime; + /// use crux_http::http_types::mime; /// caps.http /// .post("https://httpbin.org/post") /// .body(json!({"any": "Into"})) diff --git a/crux_http/src/response/decode.rs b/crux_http/src/response/decode.rs index c265fbf10..f7bfc585f 100644 --- a/crux_http/src/response/decode.rs +++ b/crux_http/src/response/decode.rs @@ -1,4 +1,4 @@ -use crate::http::Error; +use http_types::Error; use std::fmt; use std::io; diff --git a/crux_http/src/response/mod.rs b/crux_http/src/response/mod.rs index ee58dcd7c..73fd2d5aa 100644 --- a/crux_http/src/response/mod.rs +++ b/crux_http/src/response/mod.rs @@ -5,10 +5,10 @@ mod response_async; pub use self::{response::Response, response_async::ResponseAsync}; -pub(crate) fn new_headers() -> crate::http::Headers { +pub(crate) fn new_headers() -> http_types::Headers { // http-types doesn't seem to let you construct a Headers, very annoying. // So here's a horrible hack to do it. - crate::http::Request::new(crate::http::Method::Get, "https://thisisveryannoying.com") + http_types::Request::new(http_types::Method::Get, "https://thisisveryannoying.com") .as_ref() .clone() } diff --git a/crux_http/src/response/response.rs b/crux_http/src/response/response.rs index fc9e66ba1..158d6cf87 100644 --- a/crux_http/src/response/response.rs +++ b/crux_http/src/response/response.rs @@ -1,11 +1,11 @@ use super::{decode::decode_body, new_headers}; -use crate::http::{ +use http_types::{ self, headers::{self, HeaderName, HeaderValues, ToHeaderValues}, Mime, StatusCode, Version, }; -use http::{headers::CONTENT_TYPE, Headers}; +use http_types::{headers::CONTENT_TYPE, Headers}; use serde::de::DeserializeOwned; use std::fmt; @@ -14,8 +14,8 @@ use std::ops::Index; /// An HTTP Response that will be passed to in a message to an apps update function #[derive(Clone, serde::Serialize, serde::Deserialize)] pub struct Response { - version: Option, - status: http::StatusCode, + version: Option, + status: http_types::StatusCode, #[serde(with = "header_serde")] headers: Headers, body: Option, @@ -64,7 +64,7 @@ impl Response { /// /// ```no_run /// # let res = crux_http::testing::ResponseBuilder::ok().build(); - /// use crux_http::http::Version; + /// use crux_http::http_types::Version; /// assert_eq!(res.version(), Some(Version::Http1_1)); /// ``` pub fn version(&self) -> Option { @@ -146,7 +146,7 @@ impl Response { /// # let res = crux_http::testing::ResponseBuilder::ok() /// # .header("Content-Type", "application/json") /// # .build(); - /// use crux_http::http::mime; + /// use crux_http::http_types::mime; /// assert_eq!(res.content_type(), Some(mime::JSON)); /// ``` pub fn content_type(&self) -> Option { @@ -172,7 +172,7 @@ impl Response { } impl Response> { - pub(crate) fn new_with_status(status: http::StatusCode) -> Self { + pub(crate) fn new_with_status(status: http_types::StatusCode) -> Self { let headers = new_headers(); Response { @@ -290,14 +290,14 @@ impl Response> { } } -impl AsRef for Response { - fn as_ref(&self) -> &http::Headers { +impl AsRef for Response { + fn as_ref(&self) -> &http_types::Headers { &self.headers } } -impl AsMut for Response { - fn as_mut(&mut self) -> &mut http::Headers { +impl AsMut for Response { + fn as_mut(&mut self) -> &mut http_types::Headers { &mut self.headers } } @@ -362,12 +362,55 @@ where impl Eq for Response where Body: Eq {} +#[cfg(feature = "http-compat")] +impl TryInto> for Response { + type Error = (); + + fn try_into(self) -> Result, Self::Error> { + let mut response = http::Response::new(self.body.ok_or(())?); + + if let Some(version) = self.version { + let version = match version { + Version::Http0_9 => Some(http::Version::HTTP_09), + Version::Http1_0 => Some(http::Version::HTTP_10), + Version::Http1_1 => Some(http::Version::HTTP_11), + Version::Http2_0 => Some(http::Version::HTTP_2), + Version::Http3_0 => Some(http::Version::HTTP_3), + _ => None, + }; + + if let Some(version) = version { + *response.version_mut() = version; + } + } + + let mut headers = self.headers; + headers_to_hyperium_headers(&mut headers, response.headers_mut()); + + Ok(response) + } +} + +#[cfg(feature = "http-compat")] +fn headers_to_hyperium_headers(headers: &mut Headers, hyperium_headers: &mut http::HeaderMap) { + for (name, values) in headers { + let name = format!("{}", name).into_bytes(); + let name = http::header::HeaderName::from_bytes(&name).unwrap(); + + for value in values.iter() { + let value = format!("{}", value).into_bytes(); + let value = http::header::HeaderValue::from_bytes(&value).unwrap(); + hyperium_headers.append(&name, value); + } + } +} + mod header_serde { use crate::{ - http::{self, Headers}, + http_types::{self, Headers}, response::new_headers, }; - use http::headers::HeaderName; + use http_types::headers::HeaderName; use serde::{de::Error, Deserializer, Serializer}; pub fn serialize(headers: &Headers, serializer: S) -> Result diff --git a/crux_http/src/response/response_async.rs b/crux_http/src/response/response_async.rs index b7b7402ce..07d5f7408 100644 --- a/crux_http/src/response/response_async.rs +++ b/crux_http/src/response/response_async.rs @@ -1,4 +1,4 @@ -use crate::http::{ +use http_types::{ self, headers::{self, HeaderName, HeaderValues, ToHeaderValues}, Body, Mime, StatusCode, Version, @@ -20,13 +20,13 @@ pin_project_lite::pin_project! { /// use and middleware. pub struct ResponseAsync { #[pin] - res: crate::http::Response, + res: http_types::Response, } } impl ResponseAsync { /// Create a new instance. - pub(crate) fn new(res: http::Response) -> Self { + pub(crate) fn new(res: http_types::Response) -> Self { Self { res } } @@ -52,7 +52,7 @@ impl ResponseAsync { /// ```no_run /// # use crux_http::client::Client; /// # async fn middleware(client: Client) -> crux_http::Result<()> { - /// use crux_http::http::Version; + /// use crux_http::http_types::Version; /// /// let res = client.get("https://httpbin.org/get").await?; /// assert_eq!(res.version(), Some(Version::Http1_1)); @@ -148,7 +148,7 @@ impl ResponseAsync { /// ```no_run /// # use crux_http::client::Client; /// # async fn middleware(client: Client) -> crux_http::Result<()> { - /// use crux_http::http::mime; + /// use crux_http::http_types::mime; /// let res = client.get("https://httpbin.org/json").await?; /// assert_eq!(res.content_type(), Some(mime::JSON)); /// # Ok(()) } @@ -318,39 +318,39 @@ impl ResponseAsync { } } -impl From for ResponseAsync { - fn from(response: http::Response) -> Self { +impl From for ResponseAsync { + fn from(response: http_types::Response) -> Self { Self::new(response) } } #[allow(clippy::from_over_into)] -impl Into for ResponseAsync { - fn into(self) -> http::Response { +impl Into for ResponseAsync { + fn into(self) -> http_types::Response { self.res } } -impl AsRef for ResponseAsync { - fn as_ref(&self) -> &http::Headers { +impl AsRef for ResponseAsync { + fn as_ref(&self) -> &http_types::Headers { self.res.as_ref() } } -impl AsMut for ResponseAsync { - fn as_mut(&mut self) -> &mut http::Headers { +impl AsMut for ResponseAsync { + fn as_mut(&mut self) -> &mut http_types::Headers { self.res.as_mut() } } -impl AsRef for ResponseAsync { - fn as_ref(&self) -> &http::Response { +impl AsRef for ResponseAsync { + fn as_ref(&self) -> &http_types::Response { &self.res } } -impl AsMut for ResponseAsync { - fn as_mut(&mut self) -> &mut http::Response { +impl AsMut for ResponseAsync { + fn as_mut(&mut self) -> &mut http_types::Response { &mut self.res } } diff --git a/crux_http/src/testing/response_builder.rs b/crux_http/src/testing/response_builder.rs index 303ce0ac6..8ad413dbb 100644 --- a/crux_http/src/testing/response_builder.rs +++ b/crux_http/src/testing/response_builder.rs @@ -1,6 +1,4 @@ -use http::headers::{HeaderName, ToHeaderValues}; - -use crate::http; +use http_types::headers::{HeaderName, ToHeaderValues}; use crate::response::Response; @@ -14,11 +12,11 @@ pub struct ResponseBuilder { impl ResponseBuilder> { /// Constructs a new ResponseBuilder with the 200 OK status code. pub fn ok() -> ResponseBuilder> { - ResponseBuilder::with_status(http::StatusCode::Ok) + ResponseBuilder::with_status(http_types::StatusCode::Ok) } /// Constructs a new ResponseBuilder with the specified status code. - pub fn with_status(status: http::StatusCode) -> ResponseBuilder> { + pub fn with_status(status: http_types::StatusCode) -> ResponseBuilder> { let response = Response::new_with_status(status); ResponseBuilder { response }