Compare commits
16 Commits
f97b9bb51b
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 5dc358ccb8 | |||
| de24db2d2c | |||
| 61b1514b13 | |||
| bc30aa174e | |||
| 234c97ff47 | |||
| 762c6b7a41 | |||
| 3536504c10 | |||
| 2cfae96d88 | |||
| 0accc21c7c | |||
| 8782780e24 | |||
| 3f142b432f | |||
| eb0bbcdd87 | |||
| 0eb3cf9063 | |||
| 3ebd0684f0 | |||
| af0e7ad1ce | |||
| 03eb5ee1f9 |
@@ -0,0 +1,70 @@
|
|||||||
|
name: Publish to crates.io
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
dry_run:
|
||||||
|
description: 'Dry run (preview without publishing)'
|
||||||
|
required: false
|
||||||
|
default: 'true'
|
||||||
|
type: choice
|
||||||
|
options:
|
||||||
|
- 'true'
|
||||||
|
- 'false'
|
||||||
|
|
||||||
|
env:
|
||||||
|
CARGO_TERM_COLOR: always
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
publish-macros:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
outputs:
|
||||||
|
version: ${{ steps.get_version.outputs.version }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install Rust
|
||||||
|
uses: dtolnay/rust-toolchain@stable
|
||||||
|
|
||||||
|
- name: Login to crates.io
|
||||||
|
run: cargo login ${{ secrets.CRATES_TOKEN }}
|
||||||
|
|
||||||
|
- name: Get macros version
|
||||||
|
id: get_version
|
||||||
|
run: echo "version=$(cargo metadata --no-deps --format-version=1 | jq -r '.packages[] | select(.name=="desert-framework-macros") | .version')" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Publish desert-framework-macros
|
||||||
|
run: |
|
||||||
|
if [ "${{ inputs.dry_run }}" = "true" ]; then
|
||||||
|
cargo publish -p desert-framework-macros --dry-run --allow-dirty
|
||||||
|
else
|
||||||
|
cargo publish -p desert-framework-macros --allow-dirty
|
||||||
|
fi
|
||||||
|
|
||||||
|
publish-framework:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: publish-macros
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install Rust
|
||||||
|
uses: dtolnay/rust-toolchain@stable
|
||||||
|
|
||||||
|
- name: Login to crates.io
|
||||||
|
run: cargo login ${{ secrets.CRATES_TOKEN }}
|
||||||
|
|
||||||
|
- name: Wait for crates.io index
|
||||||
|
run: sleep 30
|
||||||
|
|
||||||
|
- name: Replace path dependency with version
|
||||||
|
run: |
|
||||||
|
MACROS_VERSION=${{ needs.publish-macros.outputs.version }}
|
||||||
|
sed -i "s|desert-framework-macros = { path = \"../desert-framework-macros\" }|desert-framework-macros = \"${MACROS_VERSION}\"|" desert-framework/Cargo.toml
|
||||||
|
|
||||||
|
- name: Publish desert_framework
|
||||||
|
run: |
|
||||||
|
if [ "${{ inputs.dry_run }}" = "true" ]; then
|
||||||
|
cargo publish -p desert_framework --dry-run --allow-dirty
|
||||||
|
else
|
||||||
|
cargo publish -p desert_framework --allow-dirty
|
||||||
|
fi
|
||||||
@@ -2,15 +2,20 @@
|
|||||||
|
|
||||||
[Русская версия](README_ru.md)
|
[Русская версия](README_ru.md)
|
||||||
|
|
||||||
|
[](https://crates.io/crates/desert_framework)
|
||||||
|
[](https://docs.rs/desert_framework)
|
||||||
|
|
||||||
Micro-framework for building backend applications in Rust with Axum. Provides dependency injection system and macros for declarative route definitions.
|
Micro-framework for building backend applications in Rust with Axum. Provides dependency injection system and macros for declarative route definitions.
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
```toml
|
```toml
|
||||||
[dependencies]
|
[dependencies]
|
||||||
desert-framework = "0.1.0"
|
desert-framework = "*"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
> Check [crates.io](https://crates.io/crates/desert_framework) for the latest version.
|
||||||
|
|
||||||
## Modules
|
## Modules
|
||||||
|
|
||||||
### Service — dependency injection system
|
### Service — dependency injection system
|
||||||
@@ -62,13 +67,14 @@ inject_services!(manager, "MyFunc", {
|
|||||||
### Controller — macros for Axum routes
|
### Controller — macros for Axum routes
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
use desert_framework::{controller, get, post, impl_routes};
|
use desert_framework::controller;
|
||||||
|
|
||||||
#[controller(path = "/api/users")]
|
#[controller(path = "/api/users")]
|
||||||
struct UserController {
|
struct UserController {
|
||||||
user_service: Arc<UserService>,
|
user_service: Arc<UserService>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[controller]
|
||||||
impl UserController {
|
impl UserController {
|
||||||
#[get("/")]
|
#[get("/")]
|
||||||
async fn list(&self) -> Json<Vec<User>> {
|
async fn list(&self) -> Json<Vec<User>> {
|
||||||
@@ -86,8 +92,6 @@ impl UserController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl_routes!(UserController, [list, get, create]);
|
|
||||||
|
|
||||||
// Usage in application
|
// Usage in application
|
||||||
let controller = UserController { user_service };
|
let controller = UserController { user_service };
|
||||||
let router = controller.get_router();
|
let router = controller.get_router();
|
||||||
@@ -97,17 +101,42 @@ let app = Router::new()
|
|||||||
.merge(post_controller.get_router());
|
.merge(post_controller.get_router());
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### Multiple impl blocks
|
||||||
|
|
||||||
|
Routes are discovered automatically via `inventory`. You can split methods across multiple `impl` blocks and even multiple files:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// file: user_controller.rs
|
||||||
|
#[controller(path = "/api/users")]
|
||||||
|
struct UserController { ... }
|
||||||
|
|
||||||
|
#[controller]
|
||||||
|
impl UserController {
|
||||||
|
#[get("/")]
|
||||||
|
async fn list(&self) -> Json<Vec<User>> { ... }
|
||||||
|
}
|
||||||
|
|
||||||
|
// file: user_create.rs
|
||||||
|
#[controller]
|
||||||
|
impl UserController {
|
||||||
|
#[post("/")]
|
||||||
|
async fn create(&self, Json(body): Json<CreateUser>) -> Json<User> { ... }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Both routes are automatically included in get_router()
|
||||||
|
```
|
||||||
|
|
||||||
## Macros
|
## Macros
|
||||||
|
|
||||||
| Macro | Description |
|
| Macro | Description |
|
||||||
|-------|-------------|
|
|-------|-------------|
|
||||||
| `#[controller(path = "/prefix")]` | Defines controller with base path |
|
| `#[controller(path = "/prefix")]` | Defines controller with base path (on struct) |
|
||||||
|
| `#[controller]` | Discovers route methods in impl block (on impl) |
|
||||||
| `#[get("/path")]` | GET route |
|
| `#[get("/path")]` | GET route |
|
||||||
| `#[post("/path")]` | POST route |
|
| `#[post("/path")]` | POST route |
|
||||||
| `#[put("/path")]` | PUT route |
|
| `#[put("/path")]` | PUT route |
|
||||||
| `#[delete("/path")]` | DELETE route |
|
| `#[delete("/path")]` | DELETE route |
|
||||||
| `#[patch("/path")]` | PATCH route |
|
| `#[patch("/path")]` | PATCH route |
|
||||||
| `impl_routes!(Type, [methods])` | Generates `get_router()` for controller |
|
|
||||||
| `inject_services!` | Quick service injection |
|
| `inject_services!` | Quick service injection |
|
||||||
|
|
||||||
## Route Parameters
|
## Route Parameters
|
||||||
|
|||||||
+35
-6
@@ -2,15 +2,20 @@
|
|||||||
|
|
||||||
[English version](README.md)
|
[English version](README.md)
|
||||||
|
|
||||||
|
[](https://crates.io/crates/desert_framework)
|
||||||
|
[](https://docs.rs/desert_framework)
|
||||||
|
|
||||||
Микрофреймворк для построения backend приложений на Rust с Axum. Предоставляет систему внедрения зависимостей и макросы для декларативного определения маршрутов.
|
Микрофреймворк для построения backend приложений на Rust с Axum. Предоставляет систему внедрения зависимостей и макросы для декларативного определения маршрутов.
|
||||||
|
|
||||||
## Установка
|
## Установка
|
||||||
|
|
||||||
```toml
|
```toml
|
||||||
[dependencies]
|
[dependencies]
|
||||||
desert-framework = "0.1.0"
|
desert-framework = "*"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
> Актуальную версию смотрите на [crates.io](https://crates.io/crates/desert_framework).
|
||||||
|
|
||||||
## Модули
|
## Модули
|
||||||
|
|
||||||
### Service — система внедрения зависимостей
|
### Service — система внедрения зависимостей
|
||||||
@@ -62,13 +67,14 @@ inject_services!(manager, "MyFunc", {
|
|||||||
### Controller — макросы для Axum маршрутов
|
### Controller — макросы для Axum маршрутов
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
use desert_framework::{controller, get, post, impl_routes};
|
use desert_framework::controller;
|
||||||
|
|
||||||
#[controller(path = "/api/users")]
|
#[controller(path = "/api/users")]
|
||||||
struct UserController {
|
struct UserController {
|
||||||
user_service: Arc<UserService>,
|
user_service: Arc<UserService>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[controller]
|
||||||
impl UserController {
|
impl UserController {
|
||||||
#[get("/")]
|
#[get("/")]
|
||||||
async fn list(&self) -> Json<Vec<User>> {
|
async fn list(&self) -> Json<Vec<User>> {
|
||||||
@@ -86,8 +92,6 @@ impl UserController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl_routes!(UserController, [list, get, create]);
|
|
||||||
|
|
||||||
// Использование в приложении
|
// Использование в приложении
|
||||||
let controller = UserController { user_service };
|
let controller = UserController { user_service };
|
||||||
let router = controller.get_router();
|
let router = controller.get_router();
|
||||||
@@ -97,17 +101,42 @@ let app = Router::new()
|
|||||||
.merge(post_controller.get_router());
|
.merge(post_controller.get_router());
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### Несколько impl блоков
|
||||||
|
|
||||||
|
Маршруты обнаруживаются автоматически через `inventory`. Можно разбить методы на несколько `impl` блоков и даже по разным файлам:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// файл: user_controller.rs
|
||||||
|
#[controller(path = "/api/users")]
|
||||||
|
struct UserController { ... }
|
||||||
|
|
||||||
|
#[controller]
|
||||||
|
impl UserController {
|
||||||
|
#[get("/")]
|
||||||
|
async fn list(&self) -> Json<Vec<User>> { ... }
|
||||||
|
}
|
||||||
|
|
||||||
|
// файл: user_create.rs
|
||||||
|
#[controller]
|
||||||
|
impl UserController {
|
||||||
|
#[post("/")]
|
||||||
|
async fn create(&self, Json(body): Json<CreateUser>) -> Json<User> { ... }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Оба маршрута автоматически попадут в get_router()
|
||||||
|
```
|
||||||
|
|
||||||
## Макросы
|
## Макросы
|
||||||
|
|
||||||
| Макрос | Назначение |
|
| Макрос | Назначение |
|
||||||
|--------|-----------|
|
|--------|-----------|
|
||||||
| `#[controller(path = "/prefix")]` | Определяет контроллер с базовым путём |
|
| `#[controller(path = "/prefix")]` | Определяет контроллер с базовым путём (на struct) |
|
||||||
|
| `#[controller]` | Обнаруживает route-методы в impl блоке (на impl) |
|
||||||
| `#[get("/path")]` | GET маршрут |
|
| `#[get("/path")]` | GET маршрут |
|
||||||
| `#[post("/path")]` | POST маршрут |
|
| `#[post("/path")]` | POST маршрут |
|
||||||
| `#[put("/path")]` | PUT маршрут |
|
| `#[put("/path")]` | PUT маршрут |
|
||||||
| `#[delete("/path")]` | DELETE маршрут |
|
| `#[delete("/path")]` | DELETE маршрут |
|
||||||
| `#[patch("/path")]` | PATCH маршрут |
|
| `#[patch("/path")]` | PATCH маршрут |
|
||||||
| `impl_routes!(Type, [methods])` | Генерирует `get_router()` для контроллера |
|
|
||||||
| `inject_services!` | Быстрая инъекция сервисов |
|
| `inject_services!` | Быстрая инъекция сервисов |
|
||||||
|
|
||||||
## Параметры маршрутов
|
## Параметры маршрутов
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "desert-framework-macros"
|
name = "desert-framework-macros"
|
||||||
version = "0.1.0"
|
version = "0.1.3"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
description = "Procedural macros for desert-framework"
|
||||||
|
license = "MIT"
|
||||||
|
repository = "https://github.com/Desert-Ecosystem/framework"
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
proc-macro = true
|
proc-macro = true
|
||||||
|
|||||||
@@ -3,7 +3,8 @@ use proc_macro2::TokenStream as TokenStream2;
|
|||||||
use quote::quote;
|
use quote::quote;
|
||||||
use syn::{
|
use syn::{
|
||||||
parse::{Parse, ParseStream},
|
parse::{Parse, ParseStream},
|
||||||
parse_macro_input, FnArg, ImplItemFn, ItemStruct, Meta, Pat, PatType, Token, Type,
|
parse_macro_input, FnArg, ImplItem, ImplItemFn, ItemImpl, ItemStruct, Meta, Pat, PatType,
|
||||||
|
Token, Type,
|
||||||
};
|
};
|
||||||
|
|
||||||
fn parse_controller_path(attr: TokenStream) -> String {
|
fn parse_controller_path(attr: TokenStream) -> String {
|
||||||
@@ -39,21 +40,201 @@ fn method_code(http: &str) -> u8 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn code_to_ident(code: u8) -> TokenStream2 {
|
||||||
|
match code {
|
||||||
|
0 => quote! { ::axum::routing::get },
|
||||||
|
1 => quote! { ::axum::routing::post },
|
||||||
|
2 => quote! { ::axum::routing::put },
|
||||||
|
3 => quote! { ::axum::routing::delete },
|
||||||
|
4 => quote! { ::axum::routing::patch },
|
||||||
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extract_route_info(attr: &syn::Attribute) -> (String, String) {
|
||||||
|
let method_name = attr.path().segments.last().unwrap().ident.to_string();
|
||||||
|
|
||||||
|
let path = match &attr.meta {
|
||||||
|
Meta::List(meta_list) => {
|
||||||
|
let lit: syn::LitStr =
|
||||||
|
syn::parse2(meta_list.tokens.clone()).expect("expected path string");
|
||||||
|
lit.value()
|
||||||
|
}
|
||||||
|
_ => panic!("expected #[method(\"path\")]"),
|
||||||
|
};
|
||||||
|
|
||||||
|
(method_name, path)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_route_attr(attr: &syn::Attribute) -> bool {
|
||||||
|
let ident = attr.path().segments.last().unwrap().ident.to_string();
|
||||||
|
matches!(ident.as_str(), "get" | "post" | "put" | "delete" | "patch")
|
||||||
|
}
|
||||||
|
|
||||||
/// #[controller(path = "/api")] on struct
|
/// #[controller(path = "/api")] on struct
|
||||||
#[proc_macro_attribute]
|
fn controller_on_struct(path: String, s: ItemStruct) -> TokenStream {
|
||||||
pub fn controller(attr: TokenStream, item: TokenStream) -> TokenStream {
|
|
||||||
let path = parse_controller_path(attr);
|
|
||||||
let s = parse_macro_input!(item as ItemStruct);
|
|
||||||
let name = &s.ident;
|
let name = &s.ident;
|
||||||
|
|
||||||
quote! {
|
quote! {
|
||||||
#s
|
#s
|
||||||
impl #name { pub const __CONTROLLER_PATH: &str = #path; }
|
impl #name { pub const __CONTROLLER_PATH: &str = #path; }
|
||||||
|
impl ::desert_framework::ControllerRoutes for #name {
|
||||||
|
const CONTROLLER_PATH: &'static str = #path;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.into()
|
.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Generate cleaned method + metadata consts + handler factory
|
/// #[controller] on impl block — discovers route methods automatically
|
||||||
|
fn controller_on_impl(impl_block: ItemImpl) -> TokenStream {
|
||||||
|
if impl_block.trait_.is_some() {
|
||||||
|
panic!("#[controller] on impl block is only supported for bare impls (not trait impls)");
|
||||||
|
}
|
||||||
|
|
||||||
|
let self_type = &impl_block.self_ty;
|
||||||
|
|
||||||
|
let type_name = match self_type.as_ref() {
|
||||||
|
Type::Path(type_path) => type_path.path.segments.last().unwrap().ident.clone(),
|
||||||
|
_ => panic!("#[controller] on impl block requires a named type"),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut cleaned_methods: Vec<TokenStream2> = Vec::new();
|
||||||
|
let mut factory_fns: Vec<TokenStream2> = Vec::new();
|
||||||
|
let mut inventory_submits: Vec<TokenStream2> = Vec::new();
|
||||||
|
|
||||||
|
for item in &impl_block.items {
|
||||||
|
if let ImplItem::Fn(method) = item {
|
||||||
|
let route_attr = method.attrs.iter().find(|a| is_route_attr(a));
|
||||||
|
|
||||||
|
if let Some(attr) = route_attr {
|
||||||
|
let (http_method, route_path) = extract_route_info(attr);
|
||||||
|
let code = method_code(&http_method);
|
||||||
|
let name = &method.sig.ident;
|
||||||
|
let is_async = method.sig.asyncness.is_some();
|
||||||
|
let router_fn = code_to_ident(code);
|
||||||
|
|
||||||
|
let extra: Vec<&FnArg> = method
|
||||||
|
.sig
|
||||||
|
.inputs
|
||||||
|
.iter()
|
||||||
|
.filter(|a| !matches!(a, FnArg::Receiver(_)))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let pats: Vec<&Pat> = extra
|
||||||
|
.iter()
|
||||||
|
.map(|a| match a {
|
||||||
|
FnArg::Typed(PatType { pat, .. }) => pat.as_ref(),
|
||||||
|
_ => unreachable!(),
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let tys: Vec<&Type> = extra
|
||||||
|
.iter()
|
||||||
|
.map(|a| match a {
|
||||||
|
FnArg::Typed(PatType { ty, .. }) => ty.as_ref(),
|
||||||
|
_ => unreachable!(),
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let closure = if extra.is_empty() {
|
||||||
|
if is_async {
|
||||||
|
quote! { move || async move { state.#name().await } }
|
||||||
|
} else {
|
||||||
|
quote! { move || { state.#name() } }
|
||||||
|
}
|
||||||
|
} else if is_async {
|
||||||
|
quote! {
|
||||||
|
move |#(#pats: #tys),*| async move {
|
||||||
|
state.#name(#(#pats),*).await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
quote! {
|
||||||
|
move |#(#pats: #tys),*| {
|
||||||
|
state.#name(#(#pats),*)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let factory_name = syn::Ident::new(&format!("__make_route_{}", name), name.span());
|
||||||
|
|
||||||
|
// Cleaned method (without route attribute)
|
||||||
|
let non_route_attrs: Vec<_> =
|
||||||
|
method.attrs.iter().filter(|a| !is_route_attr(a)).collect();
|
||||||
|
let vis = &method.vis;
|
||||||
|
let sig = &method.sig;
|
||||||
|
let block = &method.block;
|
||||||
|
|
||||||
|
cleaned_methods.push(quote! {
|
||||||
|
#(#non_route_attrs)*
|
||||||
|
#vis #sig #block
|
||||||
|
});
|
||||||
|
|
||||||
|
// Factory function (type-erased)
|
||||||
|
factory_fns.push(quote! {
|
||||||
|
fn #factory_name(
|
||||||
|
state: ::std::sync::Arc<dyn ::std::any::Any + Send + Sync>,
|
||||||
|
) -> ::axum::routing::MethodRouter<()> {
|
||||||
|
let state = state.downcast::<#type_name>().unwrap();
|
||||||
|
#router_fn(#closure)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// inventory::submit!
|
||||||
|
inventory_submits.push(quote! {
|
||||||
|
::desert_framework::inventory::submit! {
|
||||||
|
::desert_framework::RouteEntry {
|
||||||
|
controller_type_id: ::std::any::TypeId::of::<#type_name>(),
|
||||||
|
path: #route_path,
|
||||||
|
method: #code,
|
||||||
|
make_route: #factory_name,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
cleaned_methods.push(quote! { #method });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
cleaned_methods.push(quote! { #item });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let defaultness = &impl_block.defaultness;
|
||||||
|
let generics = &impl_block.generics;
|
||||||
|
let self_ty = &impl_block.self_ty;
|
||||||
|
let where_clause = &generics.where_clause;
|
||||||
|
|
||||||
|
quote! {
|
||||||
|
#defaultness impl #generics #self_ty #where_clause {
|
||||||
|
#(#cleaned_methods)*
|
||||||
|
}
|
||||||
|
|
||||||
|
#(#factory_fns)*
|
||||||
|
#(#inventory_submits)*
|
||||||
|
}
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── #[controller] dispatch ───
|
||||||
|
|
||||||
|
#[proc_macro_attribute]
|
||||||
|
pub fn controller(attr: TokenStream, item: TokenStream) -> TokenStream {
|
||||||
|
let input = item.clone();
|
||||||
|
if let Ok(s) = syn::parse::<ItemStruct>(input) {
|
||||||
|
let path = parse_controller_path(attr);
|
||||||
|
return controller_on_struct(path, s);
|
||||||
|
}
|
||||||
|
|
||||||
|
let input = item.clone();
|
||||||
|
if let Ok(impl_block) = syn::parse::<ItemImpl>(input) {
|
||||||
|
return controller_on_impl(impl_block);
|
||||||
|
}
|
||||||
|
|
||||||
|
panic!("#[controller] can only be applied to structs or impl blocks");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Standalone route attributes (for backward compat) ───
|
||||||
|
|
||||||
fn process_route_method(http: &str, attr: TokenStream, item: TokenStream) -> TokenStream {
|
fn process_route_method(http: &str, attr: TokenStream, item: TokenStream) -> TokenStream {
|
||||||
let method = parse_macro_input!(item as ImplItemFn);
|
let method = parse_macro_input!(item as ImplItemFn);
|
||||||
let name = &method.sig.ident;
|
let name = &method.sig.ident;
|
||||||
@@ -125,17 +306,6 @@ fn process_route_method(http: &str, attr: TokenStream, item: TokenStream) -> Tok
|
|||||||
.into()
|
.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn code_to_ident(code: u8) -> TokenStream2 {
|
|
||||||
match code {
|
|
||||||
0 => quote! { ::axum::routing::get },
|
|
||||||
1 => quote! { ::axum::routing::post },
|
|
||||||
2 => quote! { ::axum::routing::put },
|
|
||||||
3 => quote! { ::axum::routing::delete },
|
|
||||||
4 => quote! { ::axum::routing::patch },
|
|
||||||
_ => unreachable!(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[proc_macro_attribute]
|
#[proc_macro_attribute]
|
||||||
pub fn get(attr: TokenStream, item: TokenStream) -> TokenStream {
|
pub fn get(attr: TokenStream, item: TokenStream) -> TokenStream {
|
||||||
process_route_method("get", attr, item)
|
process_route_method("get", attr, item)
|
||||||
@@ -161,6 +331,8 @@ pub fn patch(attr: TokenStream, item: TokenStream) -> TokenStream {
|
|||||||
process_route_method("patch", attr, item)
|
process_route_method("patch", attr, item)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── impl_routes! (backward compat) ───
|
||||||
|
|
||||||
struct ImplRoutesInput {
|
struct ImplRoutesInput {
|
||||||
type_: syn::Path,
|
type_: syn::Path,
|
||||||
methods: Vec<syn::Ident>,
|
methods: Vec<syn::Ident>,
|
||||||
@@ -180,7 +352,6 @@ impl Parse for ImplRoutesInput {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// impl_routes!(MyCtrl, [hello, login])
|
|
||||||
#[proc_macro]
|
#[proc_macro]
|
||||||
pub fn impl_routes(input: TokenStream) -> TokenStream {
|
pub fn impl_routes(input: TokenStream) -> TokenStream {
|
||||||
let input = parse_macro_input!(input as ImplRoutesInput);
|
let input = parse_macro_input!(input as ImplRoutesInput);
|
||||||
|
|||||||
@@ -1,13 +1,16 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "desert_framework"
|
name = "desert_framework"
|
||||||
version = "0.1.0"
|
version = "0.1.3"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
description = "Micro-framework for building backend applications in Rust with Axum"
|
||||||
|
license = "MIT"
|
||||||
|
repository = "https://github.com/Desert-Ecosystem/framework"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
tokio = { version = "1.47.1", features = ["sync"] }
|
tokio = { version = "1.47.1", features = ["sync"] }
|
||||||
log = "0.4.28"
|
log = "0.4.28"
|
||||||
extended_rust = { git = "https://github.com/AlexIndustrial/extended_rust.git" }
|
|
||||||
axum = "0.8"
|
axum = "0.8"
|
||||||
|
inventory = "0.3"
|
||||||
desert-framework-macros = { path = "../desert-framework-macros" }
|
desert-framework-macros = { path = "../desert-framework-macros" }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
|
|||||||
@@ -1,4 +1,23 @@
|
|||||||
pub trait Controller {
|
use std::any::{Any, TypeId};
|
||||||
type State: Send + Sync + 'static;
|
use std::sync::Arc;
|
||||||
fn register_routes(self) -> axum::Router<Self::State>;
|
|
||||||
|
use crate::route::RouteEntry;
|
||||||
|
|
||||||
|
pub trait ControllerRoutes: Sized + Send + Sync + 'static {
|
||||||
|
const CONTROLLER_PATH: &'static str;
|
||||||
|
|
||||||
|
fn get_router(self) -> axum::Router {
|
||||||
|
let state: Arc<dyn Any + Send + Sync> = Arc::new(self);
|
||||||
|
let mut router = axum::Router::new();
|
||||||
|
|
||||||
|
for entry in inventory::iter::<RouteEntry> {
|
||||||
|
if entry.controller_type_id == TypeId::of::<Self>() {
|
||||||
|
let full_path = format!("{}{}", Self::CONTROLLER_PATH, entry.path);
|
||||||
|
let mr = (entry.make_route)(state.clone());
|
||||||
|
router = router.route(&full_path, mr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
router
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,14 @@
|
|||||||
|
extern crate self as desert_framework;
|
||||||
|
|
||||||
pub mod controller;
|
pub mod controller;
|
||||||
pub mod dependency;
|
pub mod dependency;
|
||||||
pub mod macros;
|
pub mod macros;
|
||||||
pub mod manager;
|
pub mod manager;
|
||||||
|
pub mod route;
|
||||||
pub mod service;
|
pub mod service;
|
||||||
pub mod test;
|
pub mod test;
|
||||||
|
|
||||||
|
pub use controller::ControllerRoutes;
|
||||||
pub use desert_framework_macros::*;
|
pub use desert_framework_macros::*;
|
||||||
|
pub use inventory;
|
||||||
|
pub use route::RouteEntry;
|
||||||
|
|||||||
@@ -13,6 +13,12 @@ pub struct DependencyManager {
|
|||||||
dependencies: Arc<RwLock<HashMap<TypeId, Arc<dyn Any + Send + Sync>>>>,
|
dependencies: Arc<RwLock<HashMap<TypeId, Arc<dyn Any + Send + Sync>>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Default for DependencyManager {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl DependencyManager {
|
impl DependencyManager {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
@@ -55,13 +61,13 @@ impl DependencyManager {
|
|||||||
service_instance.clone() as Arc<dyn Any + Send + Sync>,
|
service_instance.clone() as Arc<dyn Any + Send + Sync>,
|
||||||
);
|
);
|
||||||
|
|
||||||
return service_instance;
|
service_instance
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn check_by_type_id(&self, type_id: TypeId) -> bool {
|
async fn check_by_type_id(&self, type_id: TypeId) -> bool {
|
||||||
let deps = self.dependencies.read().await;
|
let deps = self.dependencies.read().await;
|
||||||
let got = deps.get(&type_id);
|
let got = deps.get(&type_id);
|
||||||
return got.is_some();
|
got.is_some()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get<T: Send + Sync + 'static + Service>(&self, from: &str) -> Option<Arc<T>> {
|
pub async fn get<T: Send + Sync + 'static + Service>(&self, from: &str) -> Option<Arc<T>> {
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
use std::any::{Any, TypeId};
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use axum::routing::MethodRouter;
|
||||||
|
|
||||||
|
pub struct RouteEntry {
|
||||||
|
pub controller_type_id: TypeId,
|
||||||
|
pub path: &'static str,
|
||||||
|
pub method: u8,
|
||||||
|
pub make_route: fn(Arc<dyn Any + Send + Sync>) -> MethodRouter,
|
||||||
|
}
|
||||||
|
|
||||||
|
inventory::collect!(RouteEntry);
|
||||||
@@ -8,7 +8,7 @@ pub trait Service {
|
|||||||
Self: Sized;
|
Self: Sized;
|
||||||
|
|
||||||
fn deps() -> Deps {
|
fn deps() -> Deps {
|
||||||
return vec![];
|
vec![]
|
||||||
}
|
}
|
||||||
|
|
||||||
fn name() -> String;
|
fn name() -> String;
|
||||||
|
|||||||
@@ -9,12 +9,14 @@ mod tests {
|
|||||||
use crate::dependency::{dep, Deps};
|
use crate::dependency::{dep, Deps};
|
||||||
use crate::manager::DependencyManager;
|
use crate::manager::DependencyManager;
|
||||||
use crate::service::Service;
|
use crate::service::Service;
|
||||||
use crate::{controller, get, impl_routes, post};
|
use crate::{controller, ControllerRoutes};
|
||||||
|
|
||||||
struct TestService1;
|
struct TestService1;
|
||||||
|
|
||||||
impl Service for TestService1 {
|
impl Service for TestService1 {
|
||||||
fn name() -> String { "TestService1".into() }
|
fn name() -> String {
|
||||||
|
"TestService1".into()
|
||||||
|
}
|
||||||
|
|
||||||
async fn new(_manager: Arc<DependencyManager>) -> Self {
|
async fn new(_manager: Arc<DependencyManager>) -> Self {
|
||||||
Self
|
Self
|
||||||
@@ -22,13 +24,17 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl TestService1 {
|
impl TestService1 {
|
||||||
fn get_hello(&self) -> &str { "hello from service 1" }
|
fn get_hello(&self) -> &str {
|
||||||
|
"hello from service 1"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct TestService2;
|
struct TestService2;
|
||||||
|
|
||||||
impl Service for TestService2 {
|
impl Service for TestService2 {
|
||||||
fn name() -> String { "TestService2".into() }
|
fn name() -> String {
|
||||||
|
"TestService2".into()
|
||||||
|
}
|
||||||
|
|
||||||
fn deps() -> Deps {
|
fn deps() -> Deps {
|
||||||
vec![dep::<TestService1>()]
|
vec![dep::<TestService1>()]
|
||||||
@@ -43,7 +49,9 @@ mod tests {
|
|||||||
struct NoDepsService;
|
struct NoDepsService;
|
||||||
|
|
||||||
impl Service for NoDepsService {
|
impl Service for NoDepsService {
|
||||||
fn name() -> String { "NoDepsService".into() }
|
fn name() -> String {
|
||||||
|
"NoDepsService".into()
|
||||||
|
}
|
||||||
|
|
||||||
async fn new(_manager: Arc<DependencyManager>) -> Self {
|
async fn new(_manager: Arc<DependencyManager>) -> Self {
|
||||||
Self
|
Self
|
||||||
@@ -87,11 +95,12 @@ mod tests {
|
|||||||
assert_eq!(s1.get_hello(), "hello from service 1");
|
assert_eq!(s1.get_hello(), "hello from service 1");
|
||||||
}
|
}
|
||||||
|
|
||||||
// === Controller Tests ===
|
// === Controller Tests (inventory-based) ===
|
||||||
|
|
||||||
#[controller(path = "/api")]
|
#[controller(path = "/api")]
|
||||||
struct TestController;
|
struct TestController;
|
||||||
|
|
||||||
|
#[controller]
|
||||||
impl TestController {
|
impl TestController {
|
||||||
#[get("/hello")]
|
#[get("/hello")]
|
||||||
async fn hello(&self) -> &'static str {
|
async fn hello(&self) -> &'static str {
|
||||||
@@ -99,24 +108,16 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[get("/items/{id}")]
|
#[get("/items/{id}")]
|
||||||
async fn get_item(
|
async fn get_item(&self, axum::extract::Path(id): axum::extract::Path<String>) -> String {
|
||||||
&self,
|
|
||||||
axum::extract::Path(id): axum::extract::Path<String>,
|
|
||||||
) -> String {
|
|
||||||
format!("item: {}", id)
|
format!("item: {}", id)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/items")]
|
#[post("/items")]
|
||||||
async fn add_item(
|
async fn add_item(&self, axum::extract::Json(body): axum::extract::Json<String>) -> String {
|
||||||
&self,
|
|
||||||
axum::extract::Json(body): axum::extract::Json<String>,
|
|
||||||
) -> String {
|
|
||||||
format!("added: {}", body)
|
format!("added: {}", body)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl_routes!(TestController, [hello, get_item, add_item]);
|
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn controller_get_hello() {
|
async fn controller_get_hello() {
|
||||||
let controller = TestController;
|
let controller = TestController;
|
||||||
@@ -134,7 +135,9 @@ mod tests {
|
|||||||
|
|
||||||
assert_eq!(response.status(), StatusCode::OK);
|
assert_eq!(response.status(), StatusCode::OK);
|
||||||
|
|
||||||
let body = axum::body::to_bytes(response.into_body(), usize::MAX).await.unwrap();
|
let body = axum::body::to_bytes(response.into_body(), usize::MAX)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
assert_eq!(body, "hello world");
|
assert_eq!(body, "hello world");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -155,7 +158,9 @@ mod tests {
|
|||||||
|
|
||||||
assert_eq!(response.status(), StatusCode::OK);
|
assert_eq!(response.status(), StatusCode::OK);
|
||||||
|
|
||||||
let body = axum::body::to_bytes(response.into_body(), usize::MAX).await.unwrap();
|
let body = axum::body::to_bytes(response.into_body(), usize::MAX)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
assert_eq!(body, "item: 42");
|
assert_eq!(body, "item: 42");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -178,7 +183,9 @@ mod tests {
|
|||||||
|
|
||||||
assert_eq!(response.status(), StatusCode::OK);
|
assert_eq!(response.status(), StatusCode::OK);
|
||||||
|
|
||||||
let body = axum::body::to_bytes(response.into_body(), usize::MAX).await.unwrap();
|
let body = axum::body::to_bytes(response.into_body(), usize::MAX)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
assert_eq!(body, "added: test item");
|
assert_eq!(body, "added: test item");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -200,9 +207,12 @@ mod tests {
|
|||||||
assert_eq!(response.status(), StatusCode::NOT_FOUND);
|
assert_eq!(response.status(), StatusCode::NOT_FOUND);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// === Multiple impl blocks ===
|
||||||
|
|
||||||
#[controller(path = "/api/other")]
|
#[controller(path = "/api/other")]
|
||||||
struct AnotherController;
|
struct AnotherController;
|
||||||
|
|
||||||
|
#[controller]
|
||||||
impl AnotherController {
|
impl AnotherController {
|
||||||
#[get("/test")]
|
#[get("/test")]
|
||||||
async fn test(&self) -> &'static str {
|
async fn test(&self) -> &'static str {
|
||||||
@@ -210,8 +220,6 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl_routes!(AnotherController, [test]);
|
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn merge_different_controllers() {
|
async fn merge_different_controllers() {
|
||||||
let c1 = TestController;
|
let c1 = TestController;
|
||||||
@@ -245,7 +253,9 @@ mod tests {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
assert_eq!(response.status(), StatusCode::OK);
|
assert_eq!(response.status(), StatusCode::OK);
|
||||||
let body = axum::body::to_bytes(response.into_body(), usize::MAX).await.unwrap();
|
let body = axum::body::to_bytes(response.into_body(), usize::MAX)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
assert_eq!(body, "from another");
|
assert_eq!(body, "from another");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user