more Actix work

This commit is contained in:
Greg Johnston 2024-01-05 09:04:46 -05:00
parent 60efaefff4
commit e1a9856ca9
7 changed files with 77 additions and 165 deletions

View File

@ -17,8 +17,6 @@ use actix_web::{
use futures::{Stream, StreamExt};
use http::StatusCode;
use leptos::{
leptos_server::{server_fn_by_path, Payload},
server_fn::Encoding,
ssr::render_to_stream_with_prefix_undisposed_with_context_and_block_replacement,
*,
};
@ -27,6 +25,7 @@ use leptos_meta::*;
use leptos_router::*;
use parking_lot::RwLock;
use regex::Regex;
use server_fn::request::actix::ActixRequest;
use std::{
fmt::{Debug, Display},
future::Future,
@ -185,113 +184,34 @@ pub fn handle_server_fns_with_context(
async move {
let additional_context = additional_context.clone();
let path = params.into_inner();
let accept_header = req
.headers()
.get("Accept")
.and_then(|value| value.to_str().ok());
if let Some(server_fn) = server_fn_by_path(path.as_str()) {
let body_ref: &[u8] = &body;
let path = req.path();
if let Some(mut service) =
server_fn::actix::get_server_fn_service(path)
{
let runtime = create_runtime();
// Add additional info to the context of the server function
additional_context();
let res_options = ResponseOptions::default();
// provide HttpRequest as context in server scope
provide_context(req.clone());
provide_context(res_options.clone());
let res_parts = ResponseOptions::default();
provide_context(res_parts.clone());
// we consume the body here (using the web::Bytes extractor), but it is required for things
// like MultipartForm
if req
.headers()
.get("Content-Type")
.and_then(|value| value.to_str().ok())
.map(|value| {
value.starts_with("multipart/form-data; boundary=")
})
== Some(true)
{
provide_context(body.clone());
let mut res =
service.0.run(ActixRequest::from(req)).await.take();
// Override StatusCode if it was set in a Resource or Element
if let Some(status) = res_parts.0.read().status {
*res.status_mut() = status;
}
let query = req.query_string().as_bytes();
// Use provided ResponseParts headers if they exist
let headers = res.headers_mut();
for (k, v) in
std::mem::take(&mut res_parts.0.write().headers)
{
headers.append(k.clone(), v.clone());
}
let data = match &server_fn.encoding() {
Encoding::Url | Encoding::Cbor => body_ref,
Encoding::GetJSON | Encoding::GetCBOR => query,
};
let res = match server_fn.call((), data).await {
Ok(serialized) => {
let res_options =
use_context::<ResponseOptions>().unwrap();
let mut res: HttpResponseBuilder =
HttpResponse::Ok();
let res_parts = res_options.0.write();
// if accept_header isn't set to one of these, it's a form submit
// redirect back to the referrer if not redirect has been set
if accept_header != Some("application/json")
&& accept_header
!= Some("application/x-www-form-urlencoded")
&& accept_header != Some("application/cbor")
{
// Location will already be set if redirect() has been used
let has_location_set =
res_parts.headers.get("Location").is_some();
if !has_location_set {
let referer = req
.headers()
.get("Referer")
.and_then(|value| value.to_str().ok())
.unwrap_or("/");
res = HttpResponse::SeeOther();
res.insert_header(("Location", referer))
.content_type("application/json");
}
};
// Override StatusCode if it was set in a Resource or Element
if let Some(status) = res_parts.status {
res.status(status);
}
// Use provided ResponseParts headers if they exist
let _count = res_parts
.headers
.clone()
.into_iter()
.map(|(k, v)| {
res.append_header((k, v));
})
.count();
match serialized {
Payload::Binary(data) => {
res.content_type("application/cbor");
res.body(Bytes::from(data))
}
Payload::Url(data) => {
res.content_type(
"application/x-www-form-urlencoded",
);
res.body(data)
}
Payload::Json(data) => {
res.content_type("application/json");
res.body(data)
}
}
}
Err(e) => HttpResponse::InternalServerError().body(
serde_json::to_string(&e)
.unwrap_or_else(|_| e.to_string()),
),
};
// clean up the scope
runtime.dispose();
res
@ -1388,62 +1308,6 @@ impl LeptosRoutes for &mut ServiceConfig {
}
}
/// A helper to make it easier to use Actix extractors in server functions. This takes
/// a handler function as its argument. The handler follows similar rules to an Actix
/// [Handler]: it is an async function that receives arguments that
/// will be extracted from the request and returns some value.
///
/// ```rust,ignore
/// use leptos::*;
/// use serde::Deserialize;
/// #[derive(Deserialize)]
/// struct Search {
/// q: String,
/// }
///
/// #[server(ExtractoServerFn, "/api")]
/// pub async fn extractor_server_fn() -> Result<String, ServerFnError> {
/// use actix_web::dev::ConnectionInfo;
/// use actix_web::web::{Data, Query};
///
/// extract(
/// |data: Data<String>, search: Query<Search>, connection: ConnectionInfo| async move {
/// format!(
/// "data = {}\nsearch = {}\nconnection = {:?}",
/// data.into_inner(),
/// search.q,
/// connection
/// )
/// },
/// )
/// .await
/// }
/// ```
pub async fn extract<F, E>(
f: F,
) -> Result<<<F as Extractor<E>>::Future as Future>::Output, ServerFnError>
where
F: Extractor<E>,
E: actix_web::FromRequest,
<E as actix_web::FromRequest>::Error: Display,
<F as Extractor<E>>::Future: Future,
{
let req = use_context::<actix_web::HttpRequest>()
.expect("HttpRequest should have been provided via context");
let input = if let Some(body) = use_context::<Bytes>() {
let (_, mut payload) = actix_http::h1::Payload::create(false);
payload.unread_data(body);
E::from_request(&req, &mut dev::Payload::from(payload))
} else {
E::extract(&req)
}
.await
.map_err(|e| ServerFnError::ServerError(e.to_string()))?;
Ok(f.call(input).await)
}
/// A helper to make it easier to use Axum extractors in server functions, with a
/// simpler API than [`extract()`].
///

View File

@ -242,7 +242,6 @@ async fn handle_server_fns_inner(
additional_context();
provide_context(parts);
// Add this so that we can set headers and status of the response
provide_context(ResponseOptions::default());
let mut res = service.run(req).await;

View File

@ -307,6 +307,9 @@ pub mod actix {
ServerFnTraitObj,
};
use actix_web::{HttpRequest, HttpResponse};
use http::Method;
#[doc(hidden)]
pub use send_wrapper::SendWrapper;
inventory::collect!(ServerFnTraitObj<ActixRequest, ActixResponse>);
@ -333,6 +336,12 @@ pub mod actix {
);
}
pub fn server_fn_paths() -> impl Iterator<Item = (&'static str, Method)> {
REGISTERED_SERVER_FUNCTIONS
.iter()
.map(|item| (item.path(), item.method()))
}
pub async fn handle_server_fn(req: HttpRequest) -> HttpResponse {
let path = req.uri().path();
if let Some(server_fn) = REGISTERED_SERVER_FUNCTIONS.get(path) {
@ -343,11 +352,6 @@ pub mod actix {
service = middleware.layer(service);
}
service.0.run(ActixRequest::from(req)).await.0.take()
/*server_fn
.run(ActixRequest(SendWrapper::new(req)))
.await
.0
.take()*/
} else {
HttpResponse::BadRequest().body(format!(
"Could not find a server function at the route {path}. \
@ -361,4 +365,17 @@ pub mod actix {
))
}
}
pub fn get_server_fn_service(
path: &str,
) -> Option<BoxedService<ActixRequest, ActixResponse>> {
REGISTERED_SERVER_FUNCTIONS.get(path).map(|server_fn| {
let middleware = (server_fn.middleware)();
let mut service = BoxedService::new(server_fn.clone());
for middleware in middleware {
service = middleware.layer(service);
}
service
})
}
}

View File

@ -97,6 +97,7 @@ mod axum {
#[cfg(feature = "actix")]
mod actix {
use crate::{
request::actix::ActixRequest,
response::{actix::ActixResponse, Res},
ServerFnError,
};
@ -121,9 +122,29 @@ mod actix {
Box::pin(async move {
inner.await.unwrap_or_else(|e| {
let err = ServerFnError::from(e);
ActixResponse::error_response(err).into_inner()
ActixResponse::error_response(err).take()
})
})
}
}
impl<S> super::Service<ActixRequest, ActixResponse> for S
where
S: actix_web::dev::Service<HttpRequest, Response = HttpResponse>,
S::Future: Send + 'static,
S::Error: Into<ServerFnError> + Debug + Display + 'static,
{
fn run(
&mut self,
req: ActixRequest,
) -> Pin<Box<dyn Future<Output = ActixResponse> + Send>> {
let inner = self.call(req.0.take());
Box::pin(async move {
ActixResponse::from(inner.await.unwrap_or_else(|e| {
let err = ServerFnError::from(e);
ActixResponse::error_response(err).take()
}))
})
}
}
}

View File

@ -7,6 +7,12 @@ use std::future::Future;
pub struct ActixRequest(pub(crate) SendWrapper<HttpRequest>);
impl ActixRequest {
pub fn take(self) -> HttpRequest {
self.0.take()
}
}
impl From<HttpRequest> for ActixRequest {
fn from(value: HttpRequest) -> Self {
ActixRequest(SendWrapper::new(value))

View File

@ -12,11 +12,17 @@ use std::fmt::{Debug, Display};
pub struct ActixResponse(pub(crate) SendWrapper<HttpResponse>);
impl ActixResponse {
pub fn into_inner(self) -> HttpResponse {
pub fn take(self) -> HttpResponse {
self.0.take()
}
}
impl From<HttpResponse> for ActixResponse {
fn from(value: HttpResponse) -> Self {
Self(SendWrapper::new(value))
}
}
impl<CustErr> Res<CustErr> for ActixResponse
where
CustErr: Display + Debug + 'static,

View File

@ -308,7 +308,6 @@ pub fn server_macro_impl(
#server_fn_path::client::browser::BrowserClient
};
// TODO Actix etc
let req = if !cfg!(feature = "ssr") {
quote! {
#server_fn_path::request::BrowserMockReq