Crate rupring

source ·
Expand description

§Get Started

There is only one dependency.

cargo add rupring

And you can write your server like this:

#[derive(Debug, Clone, Copy)]
#[rupring::Module(controllers=[HomeController{}], modules=[])]
pub struct RootModule {}

#[derive(Debug, Clone)]
#[rupring::Controller(prefix=/, routes=[hello, echo])]
pub struct HomeController {}

#[rupring::Get(path = /)]
pub fn hello(_request: rupring::Request) -> rupring::Response {
    rupring::Response::new().text("Hello, World!".to_string())
}

#[rupring::Get(path = /echo)]
pub fn echo(request: rupring::Request) -> rupring::Response {
    rupring::Response::new().text(request.body)
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
    let app = rupring::RupringFactory::create(RootModule {});

    app.listen(3000).await?;

    Ok(())
}

§Request

You can access any value provided in an HTTP Request through the Request parameter.

#[rupring::Get(path = /:id)]
pub fn hello(request: rupring::Request) -> rupring::Response {
    let method = request.method;
    assert_eq!(method, rupring::Method::GET);

    let path = request.path;
    assert_eq!(path, "/");

    let body = request.body;
    assert_eq!(body, "");

    let headers = request.headers;
    let content_type = headers.get("content-type").unwrap();
    assert_eq!(content_type, "text/plain");

    let id = request.path_param("id").unwrap();
    assert_eq!(id, "123");

    let query = request.query_param("query").unwrap();
    assert_eq!(query, "asdf");

    ...
}

§Request: Path Param

For path parameters, auto binding is provided through annotation.

The annotation name can be one of Path, path, or PathVariable.

#[rupring::Get(path = /echo/:id)]
pub fn echo(
    #[PathVariable="id"] id: i32
) -> rupring::Response {
    println!("id: {}", id);

    rupring::Response::new().text(request.body)
}

If the Path Param is optional, just wrap the type in Option.

#[rupring::Get(path = /echo/:id)]
pub fn echo(
    #[PathVariable="id"] id: Option<i32>
) -> rupring::Response {
    ...
}

If you need Swagger documentation for the Path Param, you should add the Description annotation. Description can also be used as Desc, desc, etc.

#[rupring::Get(path = /echo/:id)]
pub fn echo(
    #[path="id"] #[desc="asdf"] id: i32
) -> rupring::Response {
    println!("id: {}", id);

    rupring::Response::new().text(request.body)
}

If you want to define a custom type for PathParam, you can implement the ParamStringDeserializer trait.

impl ParamStringDeserializer<SomeCustomType> for ParamString {
    type Error = ();

    fn deserialize(&self) -> Result<SomeCustomType, Self::Error> {
        ...
    }
}

§Response

You can create a response like this:

#[rupring::Get(path = /)]
pub fn hello(_request: rupring::Request) -> rupring::Response {
    rupring::Response::new().text("Hello, World!".to_string())
}

You can also return a json value like this:

#[derive(serde::Serialize)]
struct User {
    name: String,
}

#[rupring::Get(path = /user)]
pub fn get_user(_request: rupring::Request) -> rupring::Response {
    rupring::Response::new().json(User {
        name: "John".to_string(),
    })
}

You can set the status code like this:

#[rupring::Get(path = /asdf)]
pub fn not_found(_request: rupring::Request) -> rupring::Response {
    rupring::Response::new().text("not found".to_string()).status(404)
}

You can set the header like this:

#[rupring::Get(path = /)]
pub fn hello(_request: rupring::Request) -> rupring::Response {
    rupring::Response::new()
        .text("Hello, World!".to_string())
        .header("content-type", "text/plain".to_string())
}

If you want, you can receive it as a parameter instead of creating the response directly.

#[rupring::Get(path = /)]
pub fn hello(_request: rupring::Request, response: rupring::Response) -> rupring::Response {
    response
        .text("Hello, World!".to_string())
        .header("content-type", "text/plain".to_string())
}

This is especially useful when you need to inherit and use Response through middleware.

If you want to redirect, you can use Response’s redirect method.

#[rupring::Get(path = /)]
pub fn hello(_request: rupring::Request) -> rupring::Response {
    rupring::Response::new().redirect("/hello")
}

This method automatically sets status to 302 unless you set it to 300-308.

§Middleware

rupring provides middleware features for common logic processing.

If you want to log requests for all APIs that exist in a module, you can apply middleware in the form below.

First, define a middleware function.

pub fn logger_middleware(
    request: rupring::Request,
    response: rupring::Response,
    next: NextFunction,
) -> rupring::Response {
    println!(
        "Request: {} {}",
        request.method.to_string(),
        request.path.to_string()
    );

    next(request, response)
}

The above function only records logs and forwards them to the next middleware or route function. If you want to return a response immediately without forwarding, just return the response without calling the next function.

And you can register the middleware function just defined in the module or controller unit.

#[derive(Debug, Clone, Copy)]
#[rupring::Module(
    controllers=[HomeController{}],
    modules=[UserModule{}],
    providers=[],
    middlewares=[logger_middleware]
)]
pub struct RootModule {}

// or Controller
#[derive(Debug, Clone)]
#[rupring::Controller(prefix=/, routes=[get_user], middlewares=[logger_middleware])]
pub struct UserController {}

Middleware registered in a module is recursively applied to the routes of controllers registered in that module and to child modules. On the other hand, middleware registered in a controller applies only to the routes of that controller.

The priorities in which middleware is applied are as follows:

  1. Middleware of the same unit is executed in the order defined in the array.
  2. If module middleware and controller middleware exist at the same time, module middleware is executed first.
  3. If the parent module’s middleware and the child module’s middleware exist at the same time, the parent module middleware is executed first.

§Dependency Injection

rupring provides DI feature.

If you want to implement and DI a Provider that contains simple functionality, you can do it like this: First, define the Provider.

#[derive(Debug, Clone, Default)]
pub struct HomeService {}

impl HomeService {
    pub fn hello(&self) -> String {
        "hello!!".to_string()
    }
}

impl rupring::IProvider for HomeService {
    fn provide(&self, di_context: &rupring::DIContext) -> Box<dyn std::any::Any> {
        Box::new(HomeService {})
    }
}

Second, add it as a dependency to the module you want to use.

#[derive(Debug, Clone, Copy)]
#[rupring::Module(controllers=[HomeController{}], modules=[], providers=[HomeService::default()])]
pub struct RootModule {}

And, you can use it by getting it from the router through the request object.

#[rupring::Get(path = /)]
pub fn hello(request: rupring::Request) -> rupring::Response {
    let home_service = request.get_provider::<HomeService>().unwrap();

    rupring::Response::new().text(home_service.hello())
}

If a provider requires another provider, you must specify the dependency cycle as follows:

impl rupring::IProvider for HomeService {
    fn dependencies(&self) -> Vec<TypeId> {
        vec![TypeId::of::<HomeRepository>()]
    }

    fn provide(&self, di_context: &rupring::DIContext) -> Box<dyn std::any::Any> {
        Box::new(HomeService {
            home_repository: di_context.get::<HomeRepository>().unwrap().to_owned(),
        })
    }
}

If you need mutables within the provider, you must ensure thread safety through Mutex or Atomic as follows:

#[derive(Debug, Clone, Default)]
pub struct CounterService {
    counter: Arc<Mutex<i32>>,
}

impl CounterService {
    pub fn new() -> Self {
        CounterService {
            counter: Arc::new(Mutex::new(0)),
        }
    }

    pub fn increment(&self) {
        let mut counter = self.counter.lock().unwrap();
        *counter += 1;
    }

    pub fn get(&self) -> i32 {
        let counter = self.counter.lock().unwrap();
        *counter
    }
}

If you need to abstract based on a trait, you need to box it twice as follows:

pub trait IUserService {
    fn get_user(&self) -> String;
}

#[derive(Debug, Clone, Default)]
pub struct UserService {}

impl IUserService for UserService {
    fn get_user(&self) -> String {
        "user".to_string()
    }
}

impl rupring::IProvider for UserService {
    fn dependencies(&self) -> Vec<TypeId> {
        vec![]
    }

    fn provide(&self, _di_context: &rupring::DIContext) -> Box<dyn std::any::Any> {
        let service: Box<dyn IUserService> = Box::new(UserService::default());
        return Box::new(service);
    }
}

// ...

#[rupring::Get(path = /user)]
pub fn get_user(request: rupring::Request) -> rupring::Response {
    let user_service = request.get_provider::<Box<dyn IUserService>>().unwrap();

    rupring::Response::new().text(user_service.get_user())
}

Additionally, shortcuts are provided for defining DI components. For example, the code below automatically creates an IProvider object “inject_counter_service” that can be passed to modules.

#[rupring_macro::Injectable]
fn inject_counter_service(something: SomethingRepository) -> CounterService {
    CounterService::new(something)
}
...
#[derive(Debug, Clone, Copy)]
#[rupring::Module(
    controllers=[HomeController{}],
    modules=[UserModule{}],
    providers=[inject_counter_service{}],
    middlewares=[]
)]

It automatically receives DI based on parameters.

The injectable annotation can also be explicitly named.

#[rupring_macro::Injectable(CounterServiceFactory)] // or #[rupring_macro::Injectable(name=CounterServiceFactory)]
fn inject_counter_service(something: SomethingRepository) -> CounterService {
    CounterService::new(something)
}
...
#[derive(Debug, Clone, Copy)]
#[rupring::Module(
    controllers=[HomeController{}],
    modules=[UserModule{}],
    providers=[CounterServiceFactory{}],
    middlewares=[]
)]

§Swagger

When rupring starts the server, it automatically serves swagger documents to the /docs path.

Additional annotations such as summary, description, and tags are provided for swagger documentation.

#[rupring::Get(path = /echo/:id)]
#[summary = "echo API"]
#[description = "It's echo API"]
#[tags = ["echo"]]
pub fn echo(
    #[path="id"] #[description="just integer id"] id: Option<i32>
) -> rupring::Response {
    ...

Details are still being implemented.

Modules§

Structs§

Traits§

Type Aliases§

Attribute Macros§