Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CursedObjectConstructor #203

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 103 additions & 0 deletions http/src/util/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,106 @@ pub(crate) mod futures;
#[cfg(feature = "http1")]
pub(crate) mod hint;
pub(crate) mod keep_alive;

pub mod cursed {
use std::{boxed::Box, marker::PhantomData};

use xitca_service::{
fn_build,
object::{
helpers::{ServiceObject, Wrapper},
ObjectConstructor,
},
BuildService, BuildServiceExt, Service,
};

/// A trait that serves a type-level function to convert a type from one lifetime to another.
///
/// For example: `for<'a> <&'static str as Cursed>::Type<'a> = &'a str`
pub trait Cursed {
/// A projection that takes `Self` as type and a given lifetime `'a`
type Type<'a>: 'a;
}

/// Same as [Cursed] but works in value-level, rather than type-level.
///
/// Cannot be merged with [Cursed], because of _rust issue number_.
pub trait CursedMap: Cursed {
/// A map that takes `Self` as a value and given lifetime, `'a`,
/// and outputs a value of type `Self::Type<'a>`.
fn map<'a>(self) -> Self::Type<'a>
where
Self: 'a;
}

impl<B: 'static> Cursed for http::Request<B> {
type Type<'a> = Self;
}
impl<B: 'static> CursedMap for http::Request<B> {
fn map<'a>(self) -> Self::Type<'a> {
self
}
}

impl<B: 'static> Cursed for crate::request::Request<B> {
type Type<'a> = Self;
}
impl<B: 'static> CursedMap for crate::request::Request<B> {
fn map<'a>(self) -> Self::Type<'a> {
self
}
}

impl Cursed for String {
type Type<'a> = Self;
}
impl CursedMap for String {
fn map<'a>(self) -> Self::Type<'a> {
self
}
}

/// An [object constructor](ObjectConstructor) for service with [cursed](Cursed)
/// request types.
pub struct CursedObjectConstructor<Req: Cursed>(PhantomData<Req>);

pub type CursedFactoryObject<Req: Cursed, BErr, Res, Err> =
impl BuildService<Error = BErr, Service = CursedServiceObject<Req, Res, Err>>;

pub type CursedServiceObject<Req: Cursed, Res, Err> =
impl for<'r> Service<Req::Type<'r>, Response = Res, Error = Err>;

impl<I, Svc, BErr, Req, Res, Err> ObjectConstructor<I> for CursedObjectConstructor<Req>
where
I: BuildService<Service = Svc, Error = BErr> + 'static,
Svc: for<'r> Service<Req::Type<'r>, Response = Res, Error = Err> + 'static,

Req: Cursed,

// This bound is necessary to guide type inference to infer `Req`.
// Because we can't provide a static guarantee to exclude
// such rogue implementations:
// ```
// impl Cursed for &'_ str {
// type Type<'a> = &'a u8;
// }
// ```
Svc: Service<Req>,
{
type Object = CursedFactoryObject<Req, BErr, Res, Err>;

fn into_object(inner: I) -> Self::Object {
let factory = fn_build(move |_arg: ()| {
let fut = inner.build(());
async move {
let boxed_service = Box::new(Wrapper(fut.await?))
as Box<dyn for<'r> ServiceObject<Req::Type<'r>, Response = _, Error = _>>;
Ok(Wrapper(boxed_service))
}
})
.boxed_future();

Box::new(factory) as Box<dyn BuildService<Service = _, Error = _, Future = _>>
}
}
}
130 changes: 79 additions & 51 deletions http/src/util/service/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use std::{future::Future, marker::PhantomData};
use xitca_service::{pipeline::PipelineE, ready::ReadyService, BuildService, Service};

use crate::request::{BorrowReq, BorrowReqMut};
use crate::util::cursed::{Cursed, CursedMap};

/// ServiceFactory type for constructing compile time checked stateful service.
///
Expand Down Expand Up @@ -99,6 +100,29 @@ impl<'a, Req, C> Context<'a, Req, C> {
}
}

impl<Req, C> Cursed for Context<'_, Req, C>
where
Req: Cursed,
C: 'static,
{
type Type<'a> = Context<'a, Req::Type<'a>, C>;
}
impl<Req, C> CursedMap for Context<'_, Req, C>
where
Req: CursedMap,
C: 'static,
{
fn map<'a>(self) -> Self::Type<'a>
where
Self: 'a,
{
Context {
req: self.req.map(),
state: self.state,
}
}
}

impl<Req, C, T> BorrowReq<T> for Context<'_, Req, C>
where
Req: BorrowReq<T>,
Expand Down Expand Up @@ -151,7 +175,8 @@ pub struct ContextService<C, S> {

impl<Req, C, S, Res, Err> Service<Req> for ContextService<C, S>
where
S: for<'c> Service<Context<'c, Req, C>, Response = Res, Error = Err>,
S: for<'c> Service<Context<'c, Req::Type<'c>, C>, Response = Res, Error = Err>,
Req: CursedMap,
{
type Response = Res;
type Error = Err;
Expand All @@ -161,19 +186,20 @@ where
async move {
self.service
.call(Context {
req,
req: req.map(),
state: &self.state,
})
.await
}
}
}

impl<Req, C, S, R, Res, Err> ReadyService<Req> for ContextService<C, S>
impl<Req, C, S> ReadyService<Req> for ContextService<C, S>
where
S: for<'c> ReadyService<Context<'c, Req, C>, Response = Res, Error = Err, Ready = R>,
Self: Service<Req>,
S: ReadyService<Req>,
{
type Ready = R;
type Ready = S::Ready;
type ReadyFuture<'f> = impl Future<Output = Self::Ready> where Self: 'f;

#[inline]
Expand All @@ -182,51 +208,6 @@ where
}
}

pub mod object {
use super::*;

use std::{boxed::Box, marker::PhantomData};

use xitca_service::{
fn_build,
object::{
helpers::{ServiceObject, Wrapper},
ObjectConstructor,
},
BuildService, BuildServiceExt, Service,
};

pub struct ContextObjectConstructor<Req, C>(PhantomData<(Req, C)>);

pub type ContextFactoryObject<Req, C, BErr, Res, Err> =
impl BuildService<Error = BErr, Service = ContextServiceObject<Req, C, Res, Err>>;

pub type ContextServiceObject<Req, C, Res, Err> =
impl for<'c> Service<Context<'c, Req, C>, Response = Res, Error = Err>;

impl<C, I, Svc, BErr, Req, Res, Err> ObjectConstructor<I> for ContextObjectConstructor<Req, C>
where
I: BuildService<Service = Svc, Error = BErr> + 'static,
Svc: for<'c> Service<Context<'c, Req, C>, Response = Res, Error = Err> + 'static,
{
type Object = ContextFactoryObject<Req, C, BErr, Res, Err>;

fn into_object(inner: I) -> Self::Object {
let factory = fn_build(move |_arg: ()| {
let fut = inner.build(());
async move {
let boxed_service = Box::new(Wrapper(fut.await?))
as Box<dyn for<'c> ServiceObject<Context<'c, Req, C>, Response = _, Error = _>>;
Ok(Wrapper(boxed_service))
}
})
.boxed_future();

Box::new(factory) as Box<dyn BuildService<Service = _, Error = _, Future = _>>
}
}
}

#[cfg(test)]
mod test {
use std::convert::Infallible;
Expand Down Expand Up @@ -289,7 +270,8 @@ mod test {
service.call(req).await
}

let router = GenericRouter::with_custom_object::<super::object::ContextObjectConstructor<_, _>>()
let router = GenericRouter::with_cursed_object()
//let router = GenericRouter::with_custom_object::<super::object::ContextObjectConstructor<_, _>>()
.insert("/", get(fn_service(handler)))
.enclosed_fn(enclosed);

Expand All @@ -306,4 +288,50 @@ mod test {

assert_eq!(res.status().as_u16(), 200);
}

#[tokio::test]
async fn nested_lifetime_request() {
struct Req<'a> {
_r: &'a str,
}

impl Cursed for Req<'_> {
type Type<'a> = Req<'a>;
}
impl CursedMap for Req<'_> {
fn map<'a>(self) -> Self::Type<'a>
where
Self: 'a,
{
self
}
}

impl BorrowReq<http::Uri> for Req<'_> {
fn borrow(&self) -> &http::Uri {
Box::leak(Box::new(http::Uri::from_static("http://host.com/")))
}
}

async fn handler(req: Context<'_, Req<'_>, String>) -> Result<Response<()>, Infallible> {
let (_, state) = req.into_parts();
assert_eq!(state, "string_state");
Ok(Response::new(()))
}

let router = GenericRouter::with_cursed_object().insert("/", fn_service(handler));

let service = ContextBuilder::new(|| async { Ok::<_, Infallible>(String::from("string_state")) })
.service(router)
.build(())
.await
.ok()
.unwrap();

let r = String::new();

let res = service.call(Req { _r: r.as_str() }).await.unwrap();

assert_eq!(res.status().as_u16(), 200);
}
}
11 changes: 10 additions & 1 deletion http/src/util/service/router.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,11 @@ use xitca_service::{
BuildService, Service,
};

use crate::{http, request::BorrowReq};
use crate::{
http,
request::BorrowReq,
util::cursed::{Cursed, CursedObjectConstructor},
};

/// A [GenericRouter] specialized with [DefaultObjectConstructor]
pub type Router<Req, Arg, BErr, Res, Err> =
Expand Down Expand Up @@ -38,6 +42,11 @@ impl<SF> GenericRouter<(), SF> {
GenericRouter::new()
}

/// Creates a new router with a [cursed object constructor](CursedObjectConstructor).
pub fn with_cursed_object<Req: Cursed>() -> GenericRouter<CursedObjectConstructor<Req>, SF> {
GenericRouter::new()
}

/// Creates a new router with a custom [object constructor](ObjectConstructor).
pub fn with_custom_object<ObjCons>() -> GenericRouter<ObjCons, SF> {
GenericRouter::new()
Expand Down
6 changes: 3 additions & 3 deletions web/src/app/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -265,22 +265,22 @@ mod test {
let mut req = Request::default();
req.extensions_mut().insert(Foo);

let res = service.call(req).await.unwrap();
let res = Service::<Request<_>>::call(&service, req).await.unwrap();

assert_eq!(res.status().as_u16(), 200);
assert_eq!(res.headers().get(CONTENT_TYPE).unwrap(), TEXT_UTF8);

let mut req = Request::default();
*req.uri_mut() = Uri::from_static("/abc");

let res = service.call(req).await.unwrap();
let res = Service::<Request<_>>::call(&service, req).await.unwrap();
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems that type inference have a hard time with such highly generic code. Though i wouldn't mind some turbofish api to guide inference for Req like with GenericRouter::with_custom_object.

But why it doesn't require annotations in Context tests. Needs more investigation and experimenting with other possible variations.


assert_eq!(res.status().as_u16(), 404);

let mut req = Request::default();
*req.method_mut() = Method::POST;

let res = service.call(req).await.unwrap();
let res = Service::<Request<_>>::call(&service, req).await.unwrap();

assert_eq!(res.status().as_u16(), 405);
}
Expand Down