├── .gitignore ├── .rustfmt.toml ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── examples ├── hello-world │ ├── Cargo.toml │ ├── config │ │ ├── app-dev.toml │ │ ├── app-test.toml │ │ └── app.toml │ ├── src │ │ └── main.rs │ └── test.json └── sea-orm-mysql │ ├── Cargo.toml │ ├── config │ ├── app-dev.toml │ └── app.toml │ └── src │ ├── entity.rs │ └── main.rs ├── predawn-core ├── Cargo.toml └── src │ ├── api_request.rs │ ├── api_response.rs │ ├── body.rs │ ├── either.rs │ ├── error.rs │ ├── from_request.rs │ ├── into_response.rs │ ├── lib.rs │ ├── macros.rs │ ├── media_type.rs │ ├── openapi.rs │ ├── request.rs │ ├── response.rs │ └── response_error.rs ├── predawn-macro-core ├── Cargo.toml └── src │ ├── lib.rs │ ├── schema_attr.rs │ ├── serde_attr.rs │ └── util.rs ├── predawn-macro ├── Cargo.toml └── src │ ├── controller.rs │ ├── docs │ ├── multi_request_media_type.md │ ├── multi_response.md │ ├── multi_response_media_type.md │ ├── multipart.md │ ├── security_scheme.md │ ├── single_response.md │ └── tag.md │ ├── lib.rs │ ├── method.rs │ ├── multi_request_media_type.rs │ ├── multi_response.rs │ ├── multi_response_media_type.rs │ ├── multipart.rs │ ├── security_scheme.rs │ ├── single_response.rs │ ├── tag.rs │ ├── to_parameters.rs │ └── util.rs ├── predawn-schema-macro ├── Cargo.toml └── src │ ├── lib.rs │ ├── to_schema.rs │ ├── types.rs │ └── util.rs ├── predawn-schema ├── Cargo.toml └── src │ ├── impls │ ├── atomic.rs │ ├── bytes.rs │ ├── ffi.rs │ ├── json.rs │ ├── map.rs │ ├── mod.rs │ ├── non_zero.rs │ ├── option.rs │ ├── primitive.rs │ ├── seq.rs │ ├── set.rs │ ├── string.rs │ ├── time.rs │ └── wrapper.rs │ ├── lib.rs │ ├── schemars_transform.rs │ └── to_schema.rs ├── predawn-sea-orm ├── Cargo.toml └── src │ ├── config.rs │ ├── data_source.rs │ ├── data_sources.rs │ ├── error.rs │ ├── function.rs │ ├── inner.rs │ ├── lib.rs │ ├── middleware.rs │ └── transaction.rs ├── predawn ├── Cargo.toml └── src │ ├── any_map.rs │ ├── app.rs │ ├── config │ ├── logger.rs │ ├── mod.rs │ ├── openapi.rs │ └── server.rs │ ├── controller.rs │ ├── environment.rs │ ├── extract │ ├── mod.rs │ ├── multipart │ │ ├── extract.rs │ │ ├── json_field.rs │ │ ├── mod.rs │ │ ├── parse_field.rs │ │ └── upload.rs │ ├── path │ │ ├── de.rs │ │ └── mod.rs │ ├── query.rs │ ├── typed_header.rs │ └── websocket │ │ ├── mod.rs │ │ ├── request.rs │ │ ├── response.rs │ │ └── socket.rs │ ├── handler │ ├── after.rs │ ├── around.rs │ ├── before.rs │ ├── catch_all_error.rs │ ├── catch_error.rs │ ├── inspect_all_error.rs │ ├── inspect_error.rs │ └── mod.rs │ ├── lib.rs │ ├── macros.rs │ ├── media_type.rs │ ├── middleware │ ├── limit.rs │ ├── mod.rs │ ├── tower_compat.rs │ └── tracing.rs │ ├── normalized_path.rs │ ├── openapi.rs │ ├── path_params.rs │ ├── payload │ ├── form.rs │ ├── json.rs │ └── mod.rs │ ├── plugin │ ├── mod.rs │ ├── openapi_json.rs │ └── ui │ │ ├── mod.rs │ │ ├── openapi_explorer.rs │ │ ├── rapidoc.rs │ │ ├── redoc.rs │ │ ├── scalar.rs │ │ └── swagger_ui.rs │ ├── response │ ├── download.rs │ ├── mod.rs │ ├── sse │ │ ├── builder.rs │ │ ├── event.rs │ │ ├── keep_alive.rs │ │ ├── mod.rs │ │ └── stream.rs │ └── to_header_value.rs │ ├── response_error.rs │ ├── route.rs │ ├── server.rs │ ├── test_client.rs │ ├── traits.rs │ └── util.rs └── todo.txt /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /Cargo.lock 3 | 4 | .DS_Store 5 | 6 | /examples/hello-world/log 7 | -------------------------------------------------------------------------------- /.rustfmt.toml: -------------------------------------------------------------------------------- 1 | edition = "2024" 2 | style_edition = "2024" 3 | unstable_features = true 4 | combine_control_expr = false 5 | format_macro_matchers = true 6 | newline_style = "Unix" 7 | reorder_impl_items = true 8 | # imports 9 | imports_granularity = "Crate" 10 | group_imports = "StdExternalCrate" 11 | # comments 12 | format_code_in_doc_comments = true 13 | normalize_comments = true 14 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | resolver = "3" 3 | members = [ 4 | "examples/*", 5 | "predawn", 6 | "predawn-core", 7 | "predawn-macro", 8 | "predawn-macro-core", 9 | "predawn-schema", 10 | "predawn-schema-macro", 11 | "predawn-sea-orm", 12 | ] 13 | 14 | [workspace.package] 15 | version = "0.9.0" 16 | edition = "2024" 17 | authors = ["zihan "] 18 | license = "MIT/Apache-2.0" 19 | homepage = "https://github.com/ZihanType/predawn" 20 | repository = "https://github.com/ZihanType/predawn" 21 | include = ["src/**/*", "Cargo.toml"] 22 | readme = "README.md" 23 | 24 | [workspace.lints.rust] 25 | rust-2024-compatibility = "warn" 26 | 27 | [workspace.dependencies] 28 | # self 29 | predawn-macro-core = { version = "0.9.0", path = "./predawn-macro-core", default-features = false } 30 | predawn-schema-macro = { version = "0.9.0", path = "./predawn-schema-macro", default-features = false } 31 | predawn-schema = { version = "0.9.0", path = "./predawn-schema", default-features = false } 32 | predawn-core = { version = "0.9.0", path = "./predawn-core", default-features = false } 33 | predawn-macro = { version = "0.9.0", path = "./predawn-macro", default-features = false } 34 | predawn = { version = "0.9.0", path = "./predawn", default-features = false } 35 | predawn-sea-orm = { version = "0.9.0", path = "./predawn-sea-orm", default-features = false } 36 | 37 | # dependencies 38 | async-trait = { version = "0.1", default-features = false } 39 | from-attr = { version = "0.1", default-features = false } 40 | proc-macro2 = { version = "1", default-features = false } 41 | quote = { version = "1", default-features = false } 42 | quote-use = { version = "0.8", default-features = false } 43 | syn = { version = "2", default-features = false } 44 | http = { version = "1", default-features = false } 45 | hyper = { version = "1", default-features = false } 46 | bytes = { version = "1", default-features = false } 47 | futures-core = { version = "0.3", default-features = false } 48 | futures-util = { version = "0.3", default-features = false } 49 | serde = { version = "1", default-features = false } 50 | serde_json = { version = "1", default-features = false } 51 | tokio = { version = "1", default-features = false } 52 | http-body = { version = "1", default-features = false } 53 | http-body-util = { version = "0.1", default-features = false } 54 | matchit = { version = "0.8", default-features = false } 55 | hyper-util = { version = "0.1", default-features = false } 56 | tracing = { version = "0.1", default-features = false } 57 | tracing-subscriber = { version = "0.3", default-features = false } 58 | mime = { version = "0.3", default-features = false } 59 | rudi = { version = "0.8", default-features = false } 60 | paste = { version = "1", default-features = false } 61 | serde_path_to_error = { version = "0.1", default-features = false } 62 | serde_html_form = { version = "0.2", default-features = false } 63 | openapiv3 = { version = "2", default-features = false } 64 | schemars = { version = "0.8", default-features = false } 65 | indexmap = { version = "2", default-features = false } 66 | macro-v = { version = "0.1", default-features = false } 67 | percent-encoding = { version = "2", default-features = false } 68 | tower = { version = "0.5", default-features = false } 69 | tower-http = { version = "0.6", default-features = false } 70 | config = { version = "0.15", default-features = false } 71 | reqwest = { version = "0.12", default-features = false } 72 | multer = { version = "3", default-features = false } 73 | sea-orm = { version = "1", default-features = false } 74 | url = { version = "2", default-features = false } 75 | headers = { version = "0.4", default-features = false } 76 | tracing-appender = { version = "0.2", default-features = false } 77 | tokio-tungstenite = { version = "0.26", default-features = false } 78 | memchr = { version = "2", default-features = false } 79 | pin-project-lite = { version = "0.2", default-features = false } 80 | async-stream = { version = "0.3", default-features = false } 81 | form_urlencoded = { version = "1", default-features = false } 82 | snafu = { version = "0.8", default-features = false } 83 | duration-str = { version = "0.13", default-features = false } 84 | log = { version = "0.4", default-features = false } 85 | error2 = { version = "0.2", default-features = false } 86 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Permission is hereby granted, free of charge, to any 2 | person obtaining a copy of this software and associated 3 | documentation files (the "Software"), to deal in the 4 | Software without restriction, including without 5 | limitation the rights to use, copy, modify, merge, 6 | publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software 8 | is furnished to do so, subject to the following 9 | conditions: 10 | 11 | The above copyright notice and this permission notice 12 | shall be included in all copies or substantial portions 13 | of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 16 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 17 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 18 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 19 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 22 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Predawn 2 | 3 | [![Crates.io version](https://img.shields.io/crates/v/predawn.svg?style=flat-square)](https://crates.io/crates/predawn) 4 | [![docs.rs docs](https://img.shields.io/badge/docs-latest-blue.svg?style=flat-square)](https://docs.rs/predawn) 5 | 6 | `predawn` is a Rust web framework like `Spring Boot`. 7 | 8 | ```rust 9 | use predawn::{ 10 | app::{run_app, Hooks}, 11 | controller, 12 | }; 13 | use rudi::Singleton; 14 | 15 | struct App; 16 | 17 | impl Hooks for App {} 18 | 19 | #[tokio::main] 20 | async fn main() { 21 | run_app::().await; 22 | } 23 | 24 | #[derive(Clone)] 25 | #[Singleton] 26 | struct Controller; 27 | 28 | #[controller] 29 | impl Controller { 30 | #[endpoint(paths = ["/"], methods = [post])] 31 | async fn hello(&self, name: String) -> String { 32 | format!("Hello {name}") 33 | } 34 | } 35 | ``` 36 | 37 | ## Features 38 | 39 | - Built-in OpenAPI support. 40 | - Automatic dependency injection. 41 | - Programmable configuration. 42 | 43 | More examples can be found in the [examples](./examples/) directories. 44 | 45 | ## More complex example 46 | 47 | ```rust 48 | use std::sync::Arc; 49 | 50 | use async_trait::async_trait; 51 | use predawn::{ 52 | app::{run_app, Hooks}, 53 | controller, 54 | }; 55 | use rudi::Singleton; 56 | 57 | struct App; 58 | 59 | impl Hooks for App {} 60 | 61 | #[tokio::main] 62 | async fn main() { 63 | run_app::().await; 64 | } 65 | 66 | #[async_trait] 67 | trait Service: Send + Sync { 68 | fn arc(self) -> Arc 69 | where 70 | Self: Sized + 'static, 71 | { 72 | Arc::new(self) 73 | } 74 | 75 | async fn hello(&self) -> String; 76 | } 77 | 78 | #[derive(Clone)] 79 | #[Singleton(binds = [Service::arc])] 80 | struct ServiceImpl; 81 | 82 | #[async_trait] 83 | impl Service for ServiceImpl { 84 | async fn hello(&self) -> String { 85 | "Hello, World!".to_string() 86 | } 87 | } 88 | 89 | #[derive(Clone)] 90 | #[Singleton] 91 | struct Controller { 92 | svc: Arc, 93 | } 94 | 95 | #[controller] 96 | impl Controller { 97 | #[endpoint(paths = ["/"], methods = [GET])] 98 | async fn hello(&self) -> String { 99 | self.svc.hello().await 100 | } 101 | } 102 | ``` 103 | 104 | ## Credits 105 | 106 | - [axum](https://github.com/tokio-rs/axum) 107 | - [poem](https://github.com/poem-web/poem) 108 | - [loco](https://github.com/loco-rs/loco) 109 | - [volo-http](https://github.com/cloudwego/volo) 110 | - [salvo](https://github.com/salvo-rs/salvo) 111 | -------------------------------------------------------------------------------- /examples/hello-world/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "hello-world" 3 | version.workspace = true 4 | edition.workspace = true 5 | authors.workspace = true 6 | license.workspace = true 7 | homepage.workspace = true 8 | repository.workspace = true 9 | include.workspace = true 10 | readme.workspace = true 11 | 12 | [lints] 13 | workspace = true 14 | 15 | [dependencies] 16 | predawn = { workspace = true, features = [ 17 | "macro", 18 | "auto-register", 19 | "tower-compat", 20 | ] } 21 | 22 | http = { workspace = true } 23 | rudi = { workspace = true, features = [ 24 | "rudi-macro", 25 | "auto-register", 26 | "tracing", 27 | ] } 28 | serde = { workspace = true, features = ["derive"] } 29 | tokio = { workspace = true, features = ["rt-multi-thread"] } 30 | tracing = { workspace = true } 31 | tower = { workspace = true, features = ["limit"] } 32 | tower-http = { workspace = true, features = ["compression-zstd"] } 33 | tracing-subscriber = { workspace = true, features = ["std", "fmt", "ansi"] } 34 | tracing-appender = { workspace = true } 35 | futures-util = { workspace = true } 36 | async-stream = { workspace = true } 37 | snafu = { workspace = true, features = ["rust_1_65", "std"] } 38 | -------------------------------------------------------------------------------- /examples/hello-world/config/app-dev.toml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /examples/hello-world/config/app-test.toml: -------------------------------------------------------------------------------- 1 | [server] 2 | port = 0 3 | -------------------------------------------------------------------------------- /examples/hello-world/config/app.toml: -------------------------------------------------------------------------------- 1 | [logger] 2 | level = "debug" 3 | -------------------------------------------------------------------------------- /examples/hello-world/test.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Alice", 3 | "age": 18 4 | } 5 | -------------------------------------------------------------------------------- /examples/sea-orm-mysql/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "sea-orm-mysql" 3 | version.workspace = true 4 | edition.workspace = true 5 | authors.workspace = true 6 | license.workspace = true 7 | homepage.workspace = true 8 | repository.workspace = true 9 | include.workspace = true 10 | readme.workspace = true 11 | 12 | [lints] 13 | workspace = true 14 | 15 | [dependencies] 16 | predawn = { workspace = true, features = ["macro", "auto-register"] } 17 | predawn-sea-orm = { workspace = true, features = [ 18 | "mysql", 19 | "runtime-tokio-rustls", 20 | ] } 21 | 22 | tokio = { workspace = true, features = ["rt-multi-thread"] } 23 | rudi = { workspace = true, features = [ 24 | "rudi-macro", 25 | "auto-register", 26 | "tracing", 27 | ] } 28 | sea-orm = { workspace = true, features = ["macros"] } 29 | serde = { workspace = true, features = ["derive"] } 30 | -------------------------------------------------------------------------------- /examples/sea-orm-mysql/config/app-dev.toml: -------------------------------------------------------------------------------- 1 | # data source with url only 2 | [data_sources.db1] 3 | url = "mysql://root:123456@localhost/workspace" 4 | 5 | # data source with full url 6 | [data_sources.db2] 7 | url = "mysql://root:123456@localhost:3306/workspace" 8 | 9 | # `default` data source with all options 10 | [data_sources.default] 11 | url = "mysql://localhost/workspace" 12 | username = "root" 13 | password = "123456" 14 | max_connections = 20 15 | min_connections = 10 16 | connect_timeout = "1d" 17 | idle_timeout = "1h" 18 | acquire_timeout = "1m" 19 | max_lifetime = "1m 30s" 20 | sqlx_logging = true 21 | sqlx_logging_level = "debug" 22 | sqlx_slow_statements_logging_settings = { level = "debug", threshold = "1m 30s 10ms" } 23 | sqlcipher_key = "sea-orm" 24 | schema_search_path = "public" 25 | test_before_acquire = true 26 | connect_lazy = true 27 | -------------------------------------------------------------------------------- /examples/sea-orm-mysql/config/app.toml: -------------------------------------------------------------------------------- 1 | [logger] 2 | level = "debug" 3 | -------------------------------------------------------------------------------- /examples/sea-orm-mysql/src/entity.rs: -------------------------------------------------------------------------------- 1 | use sea_orm::entity::prelude::*; 2 | 3 | #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] 4 | #[sea_orm(table_name = "user")] 5 | pub struct Model { 6 | #[sea_orm(primary_key)] 7 | pub id: u32, 8 | pub name: String, 9 | } 10 | 11 | #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] 12 | pub enum Relation {} 13 | 14 | impl ActiveModelBehavior for ActiveModel {} 15 | -------------------------------------------------------------------------------- /examples/sea-orm-mysql/src/main.rs: -------------------------------------------------------------------------------- 1 | mod entity; 2 | 3 | use predawn::{ 4 | ToParameters, 5 | app::{Hooks, run_app}, 6 | controller, 7 | extract::Query, 8 | handler::{Handler, HandlerExt}, 9 | middleware::Tracing, 10 | }; 11 | use predawn_sea_orm::SeaOrmMiddleware; 12 | use rudi::{Context, Singleton}; 13 | use sea_orm::{ColumnTrait, EntityTrait, QueryFilter}; 14 | use serde::Deserialize; 15 | 16 | struct App; 17 | 18 | impl Hooks for App { 19 | async fn before_run(mut cx: Context, router: H) -> (Context, impl Handler) { 20 | let db = cx.resolve_async::().await; 21 | 22 | let router = router.with(db).with(Tracing); 23 | 24 | (cx, router) 25 | } 26 | } 27 | 28 | #[tokio::main] 29 | async fn main() { 30 | run_app::().await; 31 | } 32 | 33 | #[derive(Clone)] 34 | #[Singleton] 35 | pub struct MyController {} 36 | 37 | #[controller] 38 | impl MyController { 39 | #[endpoint(paths = ["/hello"], methods = [GET])] 40 | async fn hello(&self, Query(user): Query) -> String { 41 | let User { name } = user; 42 | 43 | // create a transaction from `default` data source 44 | let txn = predawn_sea_orm::default_txn().await.unwrap(); 45 | 46 | // create a transaction from `db1` data source 47 | // let txn = predawn_sea_orm::current_txn("db1").await.unwrap(); 48 | 49 | // create a transaction from `db2` data source 50 | // let txn = predawn_sea_orm::current_txn("db2").await.unwrap(); 51 | 52 | match entity::Entity::find() 53 | .filter(entity::Column::Name.eq(&name)) 54 | .one(&txn) 55 | .await 56 | .unwrap() 57 | { 58 | Some(user) => { 59 | format!("Hello, {}!", user.name) 60 | } 61 | None => { 62 | format!("User not found: {}", name) 63 | } 64 | } 65 | } 66 | } 67 | 68 | #[derive(Debug, ToParameters, Deserialize)] 69 | pub struct User { 70 | pub name: String, 71 | } 72 | -------------------------------------------------------------------------------- /predawn-core/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "predawn-core" 3 | description = "Core types and traits for predawn" 4 | keywords = ["http", "web", "framework", "async"] 5 | version.workspace = true 6 | edition.workspace = true 7 | authors.workspace = true 8 | license.workspace = true 9 | homepage.workspace = true 10 | repository.workspace = true 11 | include.workspace = true 12 | readme.workspace = true 13 | 14 | [lints] 15 | workspace = true 16 | 17 | [dependencies] 18 | predawn-schema = { workspace = true } 19 | 20 | bytes = { workspace = true } 21 | futures-core = { workspace = true, features = ["alloc"] } 22 | futures-util = { workspace = true } 23 | hyper = { workspace = true } 24 | http = { workspace = true } 25 | http-body = { workspace = true } 26 | http-body-util = { workspace = true } 27 | mime = { workspace = true } 28 | indexmap = { workspace = true, features = ["std"] } 29 | snafu = { workspace = true, features = ["rust_1_65", "std"] } 30 | error2 = { workspace = true, features = ["snafu"] } 31 | -------------------------------------------------------------------------------- /predawn-core/src/api_response.rs: -------------------------------------------------------------------------------- 1 | use std::{borrow::Cow, collections::BTreeMap, convert::Infallible}; 2 | 3 | use bytes::{Bytes, BytesMut}; 4 | use http::StatusCode; 5 | 6 | use crate::{ 7 | body::ResponseBody, 8 | openapi::{self, Schema}, 9 | response::{MultiResponse, Response}, 10 | }; 11 | 12 | pub trait ApiResponse { 13 | fn responses( 14 | schemas: &mut BTreeMap, 15 | schemas_in_progress: &mut Vec, 16 | ) -> Option>; 17 | } 18 | 19 | impl ApiResponse for Response { 20 | fn responses( 21 | _: &mut BTreeMap, 22 | _: &mut Vec, 23 | ) -> Option> { 24 | None 25 | } 26 | } 27 | 28 | macro_rules! none_response { 29 | ($($ty:ty),+ $(,)?) => { 30 | $( 31 | impl ApiResponse for $ty { 32 | fn responses(_: &mut BTreeMap, _: &mut Vec) -> Option> { 33 | None 34 | } 35 | } 36 | )+ 37 | }; 38 | } 39 | 40 | none_response![http::response::Parts, ResponseBody, StatusCode, Infallible]; 41 | 42 | macro_rules! some_response { 43 | ($($ty:ty),+ $(,)?) => { 44 | $( 45 | impl ApiResponse for $ty { 46 | fn responses(schemas: &mut BTreeMap, schemas_in_progress: &mut Vec) -> Option> { 47 | Some(<$ty as MultiResponse>::responses(schemas, schemas_in_progress)) 48 | } 49 | } 50 | )+ 51 | }; 52 | } 53 | 54 | some_response![ 55 | (), 56 | // string 57 | &'static str, 58 | Cow<'static, str>, 59 | String, 60 | Box, 61 | // bytes 62 | &'static [u8], 63 | Cow<'static, [u8]>, 64 | Vec, 65 | Box<[u8]>, 66 | Bytes, 67 | BytesMut, 68 | ]; 69 | 70 | macro_rules! const_n_response { 71 | ($($ty:ty),+ $(,)?) => { 72 | $( 73 | impl ApiResponse for $ty { 74 | fn responses(schemas: &mut BTreeMap, schemas_in_progress: &mut Vec) -> Option> { 75 | Some(<$ty as MultiResponse>::responses(schemas, schemas_in_progress)) 76 | } 77 | } 78 | )+ 79 | }; 80 | } 81 | 82 | const_n_response![[u8; N], &'static [u8; N]]; 83 | 84 | impl ApiResponse for Result 85 | where 86 | T: ApiResponse, 87 | { 88 | fn responses( 89 | schemas: &mut BTreeMap, 90 | schemas_in_progress: &mut Vec, 91 | ) -> Option> { 92 | T::responses(schemas, schemas_in_progress) 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /predawn-core/src/body.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | borrow::Cow, 3 | convert::Infallible, 4 | pin::Pin, 5 | task::{Context, Poll}, 6 | }; 7 | 8 | use bytes::{Bytes, BytesMut}; 9 | use futures_core::TryStream; 10 | use futures_util::TryStreamExt; 11 | use http_body::SizeHint; 12 | use http_body_util::{BodyExt, Empty, Full, Limited, StreamBody, combinators::UnsyncBoxBody}; 13 | use hyper::body::{Frame, Incoming}; 14 | 15 | use crate::error::BoxError; 16 | 17 | pub type RequestBody = Limited; 18 | 19 | #[derive(Debug)] 20 | pub struct ResponseBody(UnsyncBoxBody); 21 | 22 | impl ResponseBody { 23 | pub fn new(body: B) -> Self 24 | where 25 | B: http_body::Body + Send + 'static, 26 | B::Data: Into, 27 | B::Error: Into, 28 | { 29 | Self( 30 | body.map_frame(|frame| frame.map_data(Into::into)) 31 | .map_err(Into::into) 32 | .boxed_unsync(), 33 | ) 34 | } 35 | 36 | pub fn empty() -> Self { 37 | Self::new(Empty::::new()) 38 | } 39 | 40 | pub fn from_stream(stream: S) -> Self 41 | where 42 | S: TryStream + Send + 'static, 43 | S::Ok: Into, 44 | S::Error: Into, 45 | { 46 | Self::new(StreamBody::new( 47 | stream.map_ok(|data| Frame::data(data.into())), 48 | )) 49 | } 50 | 51 | pub fn clear(&mut self) { 52 | *self = Self::empty(); 53 | } 54 | } 55 | 56 | impl Default for ResponseBody { 57 | fn default() -> Self { 58 | Self::empty() 59 | } 60 | } 61 | 62 | impl http_body::Body for ResponseBody { 63 | type Data = Bytes; 64 | type Error = BoxError; 65 | 66 | #[inline] 67 | fn poll_frame( 68 | mut self: Pin<&mut Self>, 69 | cx: &mut Context<'_>, 70 | ) -> Poll, Self::Error>>> { 71 | Pin::new(&mut self.0).poll_frame(cx) 72 | } 73 | 74 | #[inline] 75 | fn is_end_stream(&self) -> bool { 76 | self.0.is_end_stream() 77 | } 78 | 79 | #[inline] 80 | fn size_hint(&self) -> SizeHint { 81 | self.0.size_hint() 82 | } 83 | } 84 | 85 | impl From> for ResponseBody { 86 | fn from(full: Full) -> Self { 87 | Self::new(full) 88 | } 89 | } 90 | 91 | macro_rules! impl_from_by_full { 92 | ($($ty:ty),+ $(,)?) => { 93 | $( 94 | impl From<$ty> for ResponseBody { 95 | fn from(value: $ty) -> Self { 96 | ResponseBody::from(Full::from(value)) 97 | } 98 | } 99 | )+ 100 | }; 101 | } 102 | 103 | impl_from_by_full![ 104 | &'static [u8], 105 | Cow<'static, [u8]>, 106 | Vec, 107 | Bytes, 108 | &'static str, 109 | Cow<'static, str>, 110 | String, 111 | ]; 112 | 113 | impl From> for ResponseBody { 114 | fn from(value: Box) -> Self { 115 | value.to_string().into() 116 | } 117 | } 118 | 119 | impl From> for ResponseBody { 120 | fn from(value: Box<[u8]>) -> Self { 121 | Vec::from(value).into() 122 | } 123 | } 124 | 125 | impl From for ResponseBody { 126 | fn from(value: BytesMut) -> Self { 127 | value.freeze().into() 128 | } 129 | } 130 | 131 | impl From<[u8; N]> for ResponseBody { 132 | fn from(value: [u8; N]) -> Self { 133 | value.to_vec().into() 134 | } 135 | } 136 | 137 | impl From<&'static [u8; N]> for ResponseBody { 138 | fn from(value: &'static [u8; N]) -> Self { 139 | value.as_slice().into() 140 | } 141 | } 142 | 143 | impl From<()> for ResponseBody { 144 | fn from(_: ()) -> Self { 145 | Self::empty() 146 | } 147 | } 148 | 149 | impl From for ResponseBody { 150 | fn from(value: Infallible) -> Self { 151 | match value {} 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /predawn-core/src/either.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | collections::{BTreeMap, BTreeSet}, 3 | error::Error, 4 | fmt, 5 | }; 6 | 7 | use error2::{ErrorExt, Location, NextError}; 8 | use http::StatusCode; 9 | 10 | use crate::{ 11 | error::BoxError, 12 | openapi::{self, Schema, merge_responses}, 13 | response::Response, 14 | response_error::ResponseError, 15 | }; 16 | 17 | #[derive(Debug)] 18 | pub enum Either { 19 | Left(L), 20 | Right(R), 21 | } 22 | 23 | impl fmt::Display for Either 24 | where 25 | L: fmt::Display, 26 | R: fmt::Display, 27 | { 28 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 29 | match self { 30 | Either::Left(l) => fmt::Display::fmt(l, f), 31 | Either::Right(r) => fmt::Display::fmt(r, f), 32 | } 33 | } 34 | } 35 | 36 | impl Error for Either 37 | where 38 | L: Error, 39 | R: Error, 40 | { 41 | fn source(&self) -> Option<&(dyn Error + 'static)> { 42 | match self { 43 | Either::Left(l) => l.source(), 44 | Either::Right(r) => r.source(), 45 | } 46 | } 47 | } 48 | 49 | impl ErrorExt for Either 50 | where 51 | L: ErrorExt, 52 | R: ErrorExt, 53 | { 54 | fn entry(&self) -> (Location, NextError<'_>) { 55 | match self { 56 | Either::Left(l) => l.entry(), 57 | Either::Right(r) => r.entry(), 58 | } 59 | } 60 | } 61 | 62 | impl ResponseError for Either 63 | where 64 | L: ResponseError, 65 | R: ResponseError, 66 | { 67 | fn as_status(&self) -> StatusCode { 68 | match self { 69 | Either::Left(l) => l.as_status(), 70 | Either::Right(r) => r.as_status(), 71 | } 72 | } 73 | 74 | fn status_codes(codes: &mut BTreeSet) { 75 | L::status_codes(codes); 76 | R::status_codes(codes); 77 | } 78 | 79 | fn as_response(&self) -> Response { 80 | match self { 81 | Either::Left(l) => l.as_response(), 82 | Either::Right(r) => r.as_response(), 83 | } 84 | } 85 | 86 | fn responses( 87 | schemas: &mut BTreeMap, 88 | schemas_in_progress: &mut Vec, 89 | ) -> BTreeMap { 90 | let mut responses = L::responses(schemas, schemas_in_progress); 91 | merge_responses(&mut responses, R::responses(schemas, schemas_in_progress)); 92 | responses 93 | } 94 | 95 | #[doc(hidden)] 96 | fn inner(self) -> BoxError { 97 | match self { 98 | Either::Left(l) => l.inner(), 99 | Either::Right(r) => r.inner(), 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /predawn-core/src/error.rs: -------------------------------------------------------------------------------- 1 | use std::{error::Error as StdError, fmt}; 2 | 3 | use error2::Location; 4 | use http::{HeaderValue, StatusCode, header::CONTENT_TYPE}; 5 | use mime::TEXT_PLAIN_UTF_8; 6 | 7 | use crate::{response::Response, response_error::ResponseError}; 8 | 9 | /// Alias for a type-erased error type. 10 | pub type BoxError = Box; 11 | 12 | #[derive(Debug)] 13 | pub struct Error { 14 | response: Response, 15 | inner: BoxError, 16 | error_stack: Box<[Box]>, 17 | } 18 | 19 | impl Error { 20 | pub fn is(&self) -> bool 21 | where 22 | T: StdError + 'static, 23 | { 24 | self.inner.is::() 25 | } 26 | 27 | pub fn downcast_ref(&self) -> Option<&T> 28 | where 29 | T: StdError + 'static, 30 | { 31 | self.inner.downcast_ref::() 32 | } 33 | 34 | #[allow(clippy::type_complexity)] 35 | #[allow(clippy::result_large_err)] 36 | pub fn downcast(self) -> Result<(Response, T, Box<[Box]>), Self> 37 | where 38 | T: StdError + 'static, 39 | { 40 | let Self { 41 | response, 42 | inner, 43 | error_stack, 44 | } = self; 45 | 46 | match inner.downcast::() { 47 | Ok(err) => Ok((response, *err, error_stack)), 48 | Err(err) => Err(Self { 49 | response, 50 | inner: err, 51 | error_stack, 52 | }), 53 | } 54 | } 55 | 56 | pub fn status(&self) -> StatusCode { 57 | self.response.status() 58 | } 59 | 60 | pub fn response(self) -> Response { 61 | self.response 62 | } 63 | 64 | pub fn error_stack(&self) -> &[Box] { 65 | &self.error_stack 66 | } 67 | } 68 | 69 | impl From for Error 70 | where 71 | T: ResponseError, 72 | { 73 | fn from(error: T) -> Self { 74 | let response = error.as_response(); 75 | let error_stack = error.error_stack(); 76 | 77 | let inner = error.inner(); 78 | 79 | Self { 80 | response, 81 | inner, 82 | error_stack, 83 | } 84 | } 85 | } 86 | 87 | impl From<(StatusCode, BoxError)> for Error { 88 | #[track_caller] 89 | fn from((status, mut error): (StatusCode, BoxError)) -> Self { 90 | loop { 91 | match error.downcast::() { 92 | Ok(o) => { 93 | if o.inner.is::() { 94 | error = o.inner; 95 | } else { 96 | return *o; 97 | } 98 | } 99 | Err(e) => { 100 | error = e; 101 | break; 102 | } 103 | } 104 | } 105 | 106 | let response = Response::builder() 107 | .status(status) 108 | .header( 109 | CONTENT_TYPE, 110 | HeaderValue::from_static(TEXT_PLAIN_UTF_8.as_ref()), 111 | ) 112 | .body(error.to_string().into()) 113 | .unwrap(); 114 | 115 | let mut stack = Vec::new(); 116 | stack.push(format!("0: {}, at: {}", error, Location::caller()).into_boxed_str()); 117 | 118 | Self { 119 | response, 120 | inner: error, 121 | error_stack: stack.into_boxed_slice(), 122 | } 123 | } 124 | } 125 | 126 | impl From for Error { 127 | #[track_caller] 128 | #[inline] 129 | fn from(error: BoxError) -> Self { 130 | Error::from((StatusCode::INTERNAL_SERVER_ERROR, error)) 131 | } 132 | } 133 | 134 | impl fmt::Display for Error { 135 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 136 | fmt::Display::fmt(&self.inner, f) 137 | } 138 | } 139 | 140 | impl StdError for Error { 141 | fn source(&self) -> Option<&(dyn StdError + 'static)> { 142 | Some(&*self.inner) 143 | } 144 | } 145 | 146 | impl AsRef for Error { 147 | fn as_ref(&self) -> &(dyn StdError + Send + Sync + 'static) { 148 | &*self.inner 149 | } 150 | } 151 | 152 | impl AsRef for Error { 153 | fn as_ref(&self) -> &(dyn StdError + 'static) { 154 | &*self.inner 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /predawn-core/src/into_response.rs: -------------------------------------------------------------------------------- 1 | use std::{borrow::Cow, convert::Infallible}; 2 | 3 | use bytes::{Bytes, BytesMut}; 4 | use http::{HeaderValue, StatusCode, header::CONTENT_TYPE}; 5 | 6 | use crate::{ 7 | body::ResponseBody, either::Either, error::BoxError, media_type::MediaType, response::Response, 8 | response_error::ResponseError, 9 | }; 10 | 11 | pub trait IntoResponse { 12 | type Error: ResponseError; 13 | 14 | fn into_response(self) -> Result; 15 | } 16 | 17 | impl IntoResponse for Response 18 | where 19 | B: http_body::Body + Send + 'static, 20 | B::Data: Into, 21 | B::Error: Into, 22 | { 23 | type Error = Infallible; 24 | 25 | fn into_response(self) -> Result { 26 | Ok(self.map(ResponseBody::new)) 27 | } 28 | } 29 | 30 | impl IntoResponse for http::response::Parts { 31 | type Error = Infallible; 32 | 33 | fn into_response(self) -> Result { 34 | Ok(Response::from_parts(self, ResponseBody::empty())) 35 | } 36 | } 37 | 38 | impl IntoResponse for ResponseBody { 39 | type Error = Infallible; 40 | 41 | fn into_response(self) -> Result { 42 | Ok(Response::new(self)) 43 | } 44 | } 45 | 46 | impl IntoResponse for () { 47 | type Error = Infallible; 48 | 49 | fn into_response(self) -> Result { 50 | ResponseBody::empty().into_response() 51 | } 52 | } 53 | 54 | impl IntoResponse for StatusCode { 55 | type Error = Infallible; 56 | 57 | fn into_response(self) -> Result { 58 | let mut response = ().into_response()?; 59 | *response.status_mut() = self; 60 | Ok(response) 61 | } 62 | } 63 | 64 | impl IntoResponse for Infallible { 65 | type Error = Infallible; 66 | 67 | fn into_response(self) -> Result { 68 | match self {} 69 | } 70 | } 71 | 72 | macro_rules! some_impl { 73 | ($ty:ty; $($desc:tt)+) => { 74 | impl $($desc)+ 75 | { 76 | type Error = Infallible; 77 | 78 | fn into_response(self) -> Result { 79 | let mut response = Response::new(self.into()); 80 | 81 | response 82 | .headers_mut() 83 | .insert(CONTENT_TYPE, HeaderValue::from_static(<$ty as MediaType>::MEDIA_TYPE)); 84 | 85 | Ok(response) 86 | } 87 | } 88 | }; 89 | } 90 | 91 | some_impl!(String; IntoResponse for &'static str); 92 | some_impl!(String; IntoResponse for Cow<'static, str>); 93 | some_impl!(String; IntoResponse for String); 94 | some_impl!(String; IntoResponse for Box); 95 | 96 | some_impl!(Vec; IntoResponse for &'static [u8]); 97 | some_impl!(Vec; IntoResponse for Cow<'static, [u8]>); 98 | some_impl!(Vec; IntoResponse for Vec); 99 | some_impl!(Vec; IntoResponse for Bytes); 100 | some_impl!(Vec; IntoResponse for BytesMut); 101 | some_impl!(Vec; IntoResponse for Box<[u8]>); 102 | 103 | some_impl!([u8; N]; IntoResponse for [u8; N]); 104 | some_impl!([u8; N]; IntoResponse for &'static [u8; N]); 105 | 106 | impl IntoResponse for Result 107 | where 108 | T: IntoResponse, 109 | E: ResponseError, 110 | { 111 | type Error = Either; 112 | 113 | fn into_response(self) -> Result { 114 | match self { 115 | Ok(t) => t.into_response().map_err(Either::Left), 116 | Err(e) => Err(Either::Right(e)), 117 | } 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /predawn-core/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod api_request; 2 | pub mod api_response; 3 | pub mod body; 4 | pub mod either; 5 | pub mod error; 6 | pub mod from_request; 7 | pub mod into_response; 8 | mod macros; 9 | pub mod media_type; 10 | pub mod openapi; 11 | pub mod request; 12 | pub mod response; 13 | pub mod response_error; 14 | pub use error2; 15 | 16 | pub(crate) mod private { 17 | #[derive(Debug, Clone, Copy)] 18 | pub enum ViaRequestHead {} 19 | 20 | #[derive(Debug, Clone, Copy)] 21 | pub enum ViaRequest {} 22 | } 23 | -------------------------------------------------------------------------------- /predawn-core/src/macros.rs: -------------------------------------------------------------------------------- 1 | #[doc(hidden)] 2 | #[macro_export] 3 | macro_rules! impl_deref { 4 | ($ident:ident) => { 5 | impl ::core::ops::Deref for $ident { 6 | type Target = T; 7 | 8 | #[inline] 9 | fn deref(&self) -> &Self::Target { 10 | &self.0 11 | } 12 | } 13 | 14 | impl ::core::ops::DerefMut for $ident { 15 | #[inline] 16 | fn deref_mut(&mut self) -> &mut Self::Target { 17 | &mut self.0 18 | } 19 | } 20 | }; 21 | ($ident:ident : $ty:ty) => { 22 | impl ::core::ops::Deref for $ident { 23 | type Target = $ty; 24 | 25 | #[inline] 26 | fn deref(&self) -> &Self::Target { 27 | &self.0 28 | } 29 | } 30 | 31 | impl ::core::ops::DerefMut for $ident { 32 | #[inline] 33 | fn deref_mut(&mut self) -> &mut Self::Target { 34 | &mut self.0 35 | } 36 | } 37 | }; 38 | } 39 | 40 | #[doc(hidden)] 41 | #[macro_export] 42 | macro_rules! impl_display { 43 | ($ty:ty) => { 44 | impl ::core::fmt::Display for $ty { 45 | fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { 46 | ::core::fmt::Display::fmt(&self.0, f) 47 | } 48 | } 49 | }; 50 | } 51 | 52 | #[doc(hidden)] 53 | #[macro_export] 54 | macro_rules! impl_debug { 55 | ($ty:ty) => { 56 | impl ::core::fmt::Debug for $ty { 57 | fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { 58 | ::core::fmt::Debug::fmt(&self.0, f) 59 | } 60 | } 61 | }; 62 | } 63 | -------------------------------------------------------------------------------- /predawn-core/src/openapi.rs: -------------------------------------------------------------------------------- 1 | use std::collections::{BTreeMap, btree_map::Entry}; 2 | 3 | pub use predawn_schema::openapi::*; 4 | 5 | #[doc(hidden)] 6 | pub fn merge_responses( 7 | old: &mut BTreeMap, 8 | new: BTreeMap, 9 | ) { 10 | new.into_iter() 11 | .for_each(|(status, new)| match old.entry(status) { 12 | Entry::Occupied(mut old) => { 13 | let old = old.get_mut(); 14 | old.headers.extend(new.headers); 15 | old.content.extend(new.content); 16 | old.links.extend(new.links); 17 | old.extensions.extend(new.extensions); 18 | } 19 | Entry::Vacant(old) => { 20 | old.insert(new); 21 | } 22 | }); 23 | } 24 | -------------------------------------------------------------------------------- /predawn-core/src/response.rs: -------------------------------------------------------------------------------- 1 | use std::{borrow::Cow, collections::BTreeMap}; 2 | 3 | use bytes::{Bytes, BytesMut}; 4 | use http::StatusCode; 5 | 6 | use crate::{ 7 | body::ResponseBody, 8 | media_type::MultiResponseMediaType, 9 | openapi::{self, Schema}, 10 | }; 11 | 12 | pub type Response = http::Response; 13 | 14 | pub trait SingleResponse { 15 | const STATUS_CODE: u16 = 200; 16 | 17 | fn response( 18 | schemas: &mut BTreeMap, 19 | schemas_in_progress: &mut Vec, 20 | ) -> openapi::Response; 21 | } 22 | 23 | pub trait MultiResponse { 24 | fn responses( 25 | schemas: &mut BTreeMap, 26 | schemas_in_progress: &mut Vec, 27 | ) -> BTreeMap; 28 | } 29 | 30 | impl MultiResponse for T { 31 | fn responses( 32 | schemas: &mut BTreeMap, 33 | schemas_in_progress: &mut Vec, 34 | ) -> BTreeMap { 35 | let mut map = BTreeMap::new(); 36 | 37 | map.insert( 38 | StatusCode::from_u16(T::STATUS_CODE).unwrap_or_else(|_| { 39 | panic!( 40 | "`<{} as SingleResponse>::STATUS_CODE` is {}, which is not a valid status code", 41 | std::any::type_name::(), 42 | T::STATUS_CODE 43 | ) 44 | }), 45 | T::response(schemas, schemas_in_progress), 46 | ); 47 | 48 | map 49 | } 50 | } 51 | 52 | impl SingleResponse for () { 53 | fn response(_: &mut BTreeMap, _: &mut Vec) -> openapi::Response { 54 | openapi::Response::default() 55 | } 56 | } 57 | 58 | macro_rules! some_impl { 59 | ($ty:ty; $($desc:tt)+) => { 60 | impl $($desc)+ 61 | { 62 | fn response(schemas: &mut BTreeMap, schemas_in_progress: &mut Vec) -> openapi::Response { 63 | openapi::Response { 64 | content: <$ty as MultiResponseMediaType>::content(schemas, schemas_in_progress), 65 | ..Default::default() 66 | } 67 | } 68 | } 69 | }; 70 | } 71 | 72 | some_impl!(String; SingleResponse for &'static str); 73 | some_impl!(String; SingleResponse for Cow<'static, str>); 74 | some_impl!(String; SingleResponse for String); 75 | some_impl!(String; SingleResponse for Box); 76 | 77 | some_impl!(Vec; SingleResponse for &'static [u8]); 78 | some_impl!(Vec; SingleResponse for Cow<'static, [u8]>); 79 | some_impl!(Vec; SingleResponse for Vec); 80 | some_impl!(Vec; SingleResponse for Bytes); 81 | some_impl!(Vec; SingleResponse for BytesMut); 82 | some_impl!(Vec; SingleResponse for Box<[u8]>); 83 | 84 | some_impl!([u8; N]; SingleResponse for [u8; N]); 85 | some_impl!([u8; N]; SingleResponse for &'static [u8; N]); 86 | -------------------------------------------------------------------------------- /predawn-macro-core/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "predawn-macro-core" 3 | description = "Core types and functions for macros in predawn" 4 | keywords = ["predawn", "macro"] 5 | version.workspace = true 6 | edition.workspace = true 7 | authors.workspace = true 8 | license.workspace = true 9 | homepage.workspace = true 10 | repository.workspace = true 11 | include.workspace = true 12 | readme.workspace = true 13 | 14 | [lints] 15 | workspace = true 16 | 17 | [dependencies] 18 | syn = { workspace = true } 19 | from-attr = { workspace = true } 20 | proc-macro2 = { workspace = true } 21 | quote = { workspace = true } 22 | quote-use = { workspace = true } 23 | 24 | [features] 25 | default = ["__used_in_predawn"] 26 | __used_in_predawn = [] 27 | __used_in_predawn_schema = [] 28 | -------------------------------------------------------------------------------- /predawn-macro-core/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod schema_attr; 2 | mod serde_attr; 3 | pub mod util; 4 | 5 | pub use self::{schema_attr::SchemaAttr, serde_attr::SerdeAttr}; 6 | -------------------------------------------------------------------------------- /predawn-macro-core/src/schema_attr.rs: -------------------------------------------------------------------------------- 1 | use from_attr::{FlagOrValue, FromAttr}; 2 | use syn::Expr; 3 | 4 | #[derive(FromAttr, Default)] 5 | #[attribute(idents = [schema])] 6 | pub struct SchemaAttr { 7 | pub rename: Option, 8 | pub flatten: bool, 9 | pub default: FlagOrValue, 10 | } 11 | -------------------------------------------------------------------------------- /predawn-macro-core/src/serde_attr.rs: -------------------------------------------------------------------------------- 1 | use from_attr::FlagOrValue; 2 | use syn::{ 3 | Attribute, Expr, ExprLit, Lit, Meta, MetaNameValue, Token, punctuated::Punctuated, 4 | spanned::Spanned, 5 | }; 6 | 7 | pub struct SerdeAttr { 8 | pub rename: Option, 9 | pub flatten: bool, 10 | pub default: FlagOrValue, 11 | } 12 | 13 | impl SerdeAttr { 14 | pub fn new(attrs: &[Attribute]) -> Self { 15 | let mut rename = None; 16 | let mut flatten = false; 17 | let mut default = FlagOrValue::None; 18 | 19 | for attr in attrs { 20 | if !attr.path().is_ident("serde") { 21 | continue; 22 | } 23 | 24 | let Ok(meta_list) = attr.meta.require_list() else { 25 | continue; 26 | }; 27 | 28 | let Ok(nested) = 29 | meta_list.parse_args_with(Punctuated::::parse_terminated) 30 | else { 31 | continue; 32 | }; 33 | 34 | for meta in nested { 35 | let (path, ident) = { 36 | let path = meta.path(); 37 | 38 | let Some(ident) = path.get_ident() else { 39 | continue; 40 | }; 41 | 42 | (path.span(), ident.to_string()) 43 | }; 44 | 45 | match ident.as_str() { 46 | "rename" => match &meta { 47 | Meta::NameValue(MetaNameValue { 48 | value: 49 | Expr::Lit(ExprLit { 50 | lit: Lit::Str(lit_str), 51 | .. 52 | }), 53 | .. 54 | }) => { 55 | rename = Some(lit_str.value()); 56 | } 57 | _ => continue, 58 | }, 59 | "flatten" => match &meta { 60 | Meta::Path(_) => { 61 | flatten = true; 62 | } 63 | _ => continue, 64 | }, 65 | "default" => match &meta { 66 | Meta::Path(_) => { 67 | default = FlagOrValue::Flag { path }; 68 | } 69 | Meta::NameValue(MetaNameValue { 70 | value: 71 | Expr::Lit(ExprLit { 72 | lit: Lit::Str(lit_str), 73 | .. 74 | }), 75 | .. 76 | }) => { 77 | default = FlagOrValue::Value { 78 | path, 79 | value: lit_str.value(), 80 | }; 81 | } 82 | _ => { 83 | continue; 84 | } 85 | }, 86 | _ => continue, 87 | } 88 | } 89 | } 90 | 91 | Self { 92 | rename, 93 | flatten, 94 | default, 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /predawn-macro-core/src/util.rs: -------------------------------------------------------------------------------- 1 | use from_attr::FlagOrValue; 2 | use proc_macro2::TokenStream; 3 | use quote::quote; 4 | use quote_use::quote_use; 5 | use syn::{Attribute, Expr, ExprLit, Lit, Meta, MetaNameValue, Path, Type, parse_quote}; 6 | 7 | #[doc(hidden)] 8 | pub fn get_crate_name() -> TokenStream { 9 | #[cfg(feature = "__used_in_predawn")] 10 | quote! { ::predawn } 11 | 12 | #[cfg(all( 13 | feature = "__used_in_predawn_schema", 14 | not(feature = "__used_in_predawn") 15 | ))] 16 | quote! { ::predawn_schema } 17 | 18 | #[cfg(not(any(feature = "__used_in_predawn", feature = "__used_in_predawn_schema")))] 19 | compile_error!( 20 | "either `__used_in_predawn` or `__used_in_predawn_schema` feature must be enabled" 21 | ); 22 | } 23 | 24 | pub fn extract_description(attrs: &[Attribute]) -> String { 25 | let mut docs = String::new(); 26 | 27 | attrs.iter().for_each(|attr| { 28 | if !attr.path().is_ident("doc") { 29 | return; 30 | } 31 | 32 | let Meta::NameValue(MetaNameValue { 33 | value: Expr::Lit(ExprLit { 34 | lit: Lit::Str(doc), .. 35 | }), 36 | .. 37 | }) = &attr.meta 38 | else { 39 | return; 40 | }; 41 | 42 | let doc = doc.value(); 43 | 44 | if !docs.is_empty() { 45 | docs.push('\n'); 46 | } 47 | 48 | docs.push_str(doc.trim()); 49 | }); 50 | 51 | docs 52 | } 53 | 54 | pub fn remove_description(attrs: &mut Vec) { 55 | attrs.retain(|attr| !attr.path().is_ident("doc")); 56 | } 57 | 58 | pub fn generate_string_expr(s: &str) -> Expr { 59 | parse_quote! { 60 | ::std::string::ToString::to_string(#s) 61 | } 62 | } 63 | 64 | pub fn generate_default_expr( 65 | ty: &Type, 66 | serde_default: FlagOrValue, 67 | schema_default: FlagOrValue, 68 | ) -> syn::Result> { 69 | let default_expr: Expr = match (serde_default, schema_default) { 70 | (FlagOrValue::None, FlagOrValue::None) => return Ok(None), 71 | 72 | (FlagOrValue::None, FlagOrValue::Flag { .. }) 73 | | (FlagOrValue::Flag { .. }, FlagOrValue::Flag { .. }) 74 | | (FlagOrValue::Flag { .. }, FlagOrValue::None) => { 75 | parse_quote! { 76 | <#ty as ::core::default::Default>::default() 77 | } 78 | } 79 | 80 | (FlagOrValue::Value { value, .. }, FlagOrValue::None) 81 | | (FlagOrValue::Value { value, .. }, FlagOrValue::Flag { .. }) => { 82 | let path = syn::parse_str::(&value)?; 83 | 84 | parse_quote! { 85 | #path() 86 | } 87 | } 88 | 89 | (FlagOrValue::None, FlagOrValue::Value { value: expr, .. }) 90 | | (FlagOrValue::Flag { .. }, FlagOrValue::Value { value: expr, .. }) 91 | | (FlagOrValue::Value { .. }, FlagOrValue::Value { value: expr, .. }) => expr, 92 | }; 93 | 94 | Ok(Some(default_expr)) 95 | } 96 | 97 | pub fn generate_json_value(ty: &Type, expr: &Expr) -> TokenStream { 98 | let crate_name = get_crate_name(); 99 | 100 | quote_use! { 101 | # use std::{concat, stringify, file, line, column}; 102 | # use #crate_name::__internal::serde_json; 103 | 104 | serde_json::to_value::<#ty>(#expr) 105 | .expect(concat!( 106 | "failed to serialize expression `", stringify!(#expr), "` of type `", stringify!(#ty), 107 | "`, at ", file!(), ":", line!(), ":", column!() 108 | )) 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /predawn-macro/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "predawn-macro" 3 | description = "Macros for predawn" 4 | keywords = ["http", "web", "framework", "async"] 5 | version.workspace = true 6 | edition.workspace = true 7 | authors.workspace = true 8 | license.workspace = true 9 | homepage.workspace = true 10 | repository.workspace = true 11 | include.workspace = true 12 | readme.workspace = true 13 | 14 | [lib] 15 | proc-macro = true 16 | 17 | [lints] 18 | workspace = true 19 | 20 | [dependencies] 21 | predawn-macro-core = { workspace = true, features = ["__used_in_predawn"] } 22 | 23 | from-attr = { workspace = true } 24 | proc-macro2 = { workspace = true } 25 | quote = { workspace = true } 26 | quote-use = { workspace = true } 27 | syn = { workspace = true, features = ["full"] } 28 | http = { workspace = true, features = ["std"] } 29 | 30 | [dev-dependencies] 31 | # cannot contain `workspace = true` to avoid circular dependencies. 32 | predawn = { path = "../predawn", default-features = false, features = [ 33 | "macro", 34 | "auto-register", 35 | ] } 36 | 37 | serde = { workspace = true } 38 | 39 | [features] 40 | default = ["auto-register"] 41 | auto-register = [] 42 | -------------------------------------------------------------------------------- /predawn-macro/src/docs/multi_request_media_type.md: -------------------------------------------------------------------------------- 1 | Define a single request body with multiple media types. 2 | 3 | This macro will generate 3 implementations, [`MultiRequestMediaType`], [`FromRequest`] and [`ApiRequest`]. 4 | 5 | ## Example 6 | 7 | ```rust 8 | use predawn::{ 9 | define_from_request_error, 10 | payload::{Form, Json}, 11 | response_error::{ReadFormError, ReadJsonError}, 12 | MultiRequestMediaType, ToSchema, 13 | }; 14 | use serde::de::DeserializeOwned; 15 | 16 | #[derive(Debug, MultiRequestMediaType)] 17 | #[multi_request_media_type(error = ReadJsonOrFormError)] 18 | pub enum JsonOrForm { 19 | Json(Json), 20 | Form(Form), 21 | } 22 | 23 | define_from_request_error! { 24 | name: ReadJsonOrFormError, 25 | errors: [ 26 | ReadJsonError, 27 | ReadFormError, 28 | ], 29 | } 30 | ``` 31 | 32 | [`MultiRequestMediaType`]: https://docs.rs/predawn/latest/predawn/trait.MultiRequestMediaType.html 33 | [`FromRequest`]: https://docs.rs/predawn/latest/predawn/from_request/trait.FromRequest.html 34 | [`ApiRequest`]: https://docs.rs/predawn/latest/predawn/api_request/trait.ApiRequest.html 35 | -------------------------------------------------------------------------------- /predawn-macro/src/docs/multi_response.md: -------------------------------------------------------------------------------- 1 | Define a multiple response. 2 | 3 | This macro will generate 3 implementations, [`MultiResponse`], [`IntoResponse`] and [`ApiResponse`]. 4 | 5 | ## Example 6 | 7 | ```rust 8 | use predawn::{ 9 | define_into_response_error, payload::Json, response_error::WriteJsonError, MultiResponse, 10 | SingleResponse, ToSchema, 11 | }; 12 | use serde::Serialize; 13 | 14 | #[derive(Debug, Serialize, ToSchema)] 15 | pub struct ErrorSource { 16 | error_code: u16, 17 | error_message: String, 18 | } 19 | 20 | #[derive(Debug, SingleResponse)] 21 | pub struct NotFoundAccount; 22 | 23 | #[derive(MultiResponse)] 24 | #[multi_response(error = MultipleResponseError)] 25 | pub enum MultipleResponse { 26 | #[status = 200] 27 | Ok(Json), 28 | 29 | #[status = 404] 30 | NotFound(NotFoundAccount), 31 | 32 | #[status = 500] 33 | Error(Json), 34 | } 35 | 36 | define_into_response_error! { 37 | name: MultipleResponseError, 38 | errors: [ 39 | WriteJsonError, 40 | ], 41 | } 42 | ``` 43 | 44 | [`MultiResponse`]: https://docs.rs/predawn/latest/predawn/trait.MultiResponse.html 45 | [`IntoResponse`]: https://docs.rs/predawn/latest/predawn/into_response/trait.IntoResponse.html 46 | [`ApiResponse`]: https://docs.rs/predawn/latest/predawn/api_response/trait.ApiResponse.html 47 | -------------------------------------------------------------------------------- /predawn-macro/src/docs/multi_response_media_type.md: -------------------------------------------------------------------------------- 1 | Define a single response body with multiple media types. 2 | 3 | This macro will generate 4 implementations, [`MultiResponseMediaType`], [`SingleResponse`], [`IntoResponse`] and [`ApiResponse`]. 4 | 5 | ## Example 6 | 7 | ```rust 8 | use predawn::{ 9 | define_into_response_error, 10 | payload::{Form, Json}, 11 | response_error::{WriteFormError, WriteJsonError}, 12 | MultiResponseMediaType, ToSchema, 13 | }; 14 | use serde::Serialize; 15 | 16 | #[derive(Debug, MultiResponseMediaType)] 17 | // `status` is optional, default is 200 18 | #[multi_response_media_type(error = WriteJsonOrFormError, status = 200)] 19 | pub enum JsonOrForm { 20 | Json(Json), 21 | Form(Form), 22 | } 23 | 24 | define_into_response_error! { 25 | name: WriteJsonOrFormError, 26 | errors: [ 27 | WriteJsonError, 28 | WriteFormError, 29 | ], 30 | } 31 | ``` 32 | 33 | [`MultiResponseMediaType`]: https://docs.rs/predawn/latest/predawn/trait.MultiResponseMediaType.html 34 | [`SingleResponse`]: https://docs.rs/predawn/latest/predawn/trait.SingleResponse.html 35 | [`IntoResponse`]: https://docs.rs/predawn/latest/predawn/into_response/trait.IntoResponse.html 36 | [`ApiResponse`]: https://docs.rs/predawn/latest/predawn/api_response/trait.ApiResponse.html 37 | -------------------------------------------------------------------------------- /predawn-macro/src/docs/multipart.md: -------------------------------------------------------------------------------- 1 | Define a request body with `multipart/form-data` media type. 2 | 3 | This macro will generate 5 implementations, [`FromRequest`], [`ApiRequest`], [`MediaType`], [`RequestMediaType`] and [`SingleMediaType`]. 4 | 5 | ## Example 6 | 7 | ```rust 8 | use predawn::{ 9 | extract::multipart::{JsonField, Multipart, Upload}, 10 | ToSchema, 11 | }; 12 | use serde::Deserialize; 13 | 14 | #[derive(ToSchema, Multipart)] 15 | pub struct SomeMultipart { 16 | person: JsonField, 17 | message: String, 18 | files: Vec, 19 | } 20 | 21 | #[derive(ToSchema, Deserialize)] 22 | pub struct Person { 23 | name: String, 24 | age: u8, 25 | } 26 | ``` 27 | 28 | ## Note 29 | 30 | `struct`s can only be annotated with `Multipart` derive macro if all of their fields implement the [`ParseField`] trait. 31 | 32 | [`FromRequest`]: https://docs.rs/predawn/latest/predawn/from_request/trait.FromRequest.html 33 | [`ApiRequest`]: https://docs.rs/predawn/latest/predawn/api_request/trait.ApiRequest.html 34 | [`MediaType`]: https://docs.rs/predawn/latest/predawn/media_type/trait.MediaType.html 35 | [`RequestMediaType`]: https://docs.rs/predawn/latest/predawn/media_type/trait.RequestMediaType.html 36 | [`SingleMediaType`]: https://docs.rs/predawn/latest/predawn/media_type/trait.SingleMediaType.html 37 | [`ParseField`]: https://docs.rs/predawn/latest/predawn/extract/multipart/trait.ParseField.html 38 | -------------------------------------------------------------------------------- /predawn-macro/src/docs/security_scheme.md: -------------------------------------------------------------------------------- 1 | Define an OpenAPI Security Scheme. 2 | 3 | This macro will generate 1 implementation, [`SecurityScheme`]. 4 | 5 | ## Example 6 | 7 | ```rust 8 | use predawn::SecurityScheme; 9 | 10 | /// This doc will be used as the tag description 11 | #[derive(SecurityScheme)] 12 | #[api_key(in = header, name = "X-API-Key")] 13 | pub struct ApiKeyScheme; 14 | 15 | #[derive(SecurityScheme)] 16 | #[http(scheme = basic, rename = "Basic Auth")] 17 | pub struct HttpScheme; 18 | ``` 19 | 20 | `rename` is optional, default is the type name. 21 | 22 | [`SecurityScheme`]: https://docs.rs/predawn/latest/predawn/trait.SecurityScheme.html 23 | -------------------------------------------------------------------------------- /predawn-macro/src/docs/single_response.md: -------------------------------------------------------------------------------- 1 | Define a single response with headers and body. 2 | 3 | This macro will generate 3 implementations, [`SingleResponse`], [`IntoResponse`] and [`ApiResponse`]. 4 | 5 | ## Example 6 | 7 | ```rust 8 | use predawn::{payload::Json, SingleResponse, ToSchema}; 9 | use serde::Serialize; 10 | 11 | #[derive(SingleResponse)] 12 | // `status` is optional, default is 200 13 | #[single_response(status = 404)] 14 | pub struct UnitResponse; 15 | 16 | #[derive(SingleResponse)] 17 | pub struct TupleResponse( 18 | #[header = "X-Auth-Token"] pub String, 19 | // the last field, if not annotated with `#[header = "xxx"]`, 20 | // means that it will be the response body 21 | pub Json, 22 | ); 23 | 24 | // also could all fields be annotated `#[header = "xxx"]` 25 | #[derive(SingleResponse)] 26 | pub struct NamedResponse { 27 | // `AAA` will be normalized, 28 | // e.g. uppercase letters will be converted to lowercase letters 29 | #[header = "AAA"] 30 | pub header1: String, 31 | #[header = "bbb"] 32 | pub header2: String, 33 | #[header = "ccc"] 34 | pub header3: String, 35 | } 36 | ``` 37 | 38 | ## Note 39 | 40 | 1. Only types that implement the [`ToHeaderValue`] trait can be annotated by `#[header = "xxx"]`. 41 | 42 | 2. > All custom header names are lower cased upon conversion to a `HeaderName` value. This avoids the overhead of dynamically doing lower case conversion during the hash code computation and the comparison operation. 43 | 44 | Details: [HeaderName](https://docs.rs/http/latest/http/header/struct.HeaderName.html) 45 | 46 | [`SingleResponse`]: https://docs.rs/predawn/latest/predawn/trait.SingleResponse.html 47 | [`IntoResponse`]: https://docs.rs/predawn/latest/predawn/into_response/trait.IntoResponse.html 48 | [`ApiResponse`]: https://docs.rs/predawn/latest/predawn/api_response/trait.ApiResponse.html 49 | [`ToHeaderValue`]: https://docs.rs/predawn/latest/predawn/response/trait.ToHeaderValue.html 50 | -------------------------------------------------------------------------------- /predawn-macro/src/docs/tag.md: -------------------------------------------------------------------------------- 1 | Define an OpenAPI Tag. 2 | 3 | This macro will generate 1 implementation, [`Tag`]. 4 | 5 | ## Example 6 | 7 | ```rust 8 | use predawn::Tag; 9 | 10 | /// This doc will be used as the tag description 11 | #[derive(Tag)] 12 | #[tag(rename = "This a tag")] 13 | pub struct Hello; 14 | ``` 15 | 16 | `rename` is optional, default is the type name. 17 | 18 | [`Tag`]: https://docs.rs/predawn/latest/predawn/trait.Tag.html 19 | -------------------------------------------------------------------------------- /predawn-macro/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod controller; 2 | mod method; 3 | mod multi_request_media_type; 4 | mod multi_response; 5 | mod multi_response_media_type; 6 | mod multipart; 7 | mod security_scheme; 8 | mod single_response; 9 | mod tag; 10 | mod to_parameters; 11 | mod util; 12 | 13 | use from_attr::FromAttr; 14 | use proc_macro::TokenStream; 15 | use syn::{DeriveInput, ItemImpl, parse_macro_input}; 16 | 17 | #[proc_macro_attribute] 18 | pub fn controller(attr: TokenStream, item: TokenStream) -> TokenStream { 19 | let attr = match controller::ControllerAttr::from_tokens(attr.into()) { 20 | Ok(attr) => attr, 21 | Err(e) => return e.to_compile_error().into(), 22 | }; 23 | let item_impl = parse_macro_input!(item as ItemImpl); 24 | 25 | controller::generate(attr, item_impl) 26 | .unwrap_or_else(|e| e.to_compile_error()) 27 | .into() 28 | } 29 | 30 | #[proc_macro_derive(ToParameters, attributes(schema))] 31 | pub fn to_parameters(input: TokenStream) -> TokenStream { 32 | let input = parse_macro_input!(input as DeriveInput); 33 | 34 | to_parameters::generate(input) 35 | .unwrap_or_else(|e| e.to_compile_error()) 36 | .into() 37 | } 38 | 39 | #[doc = include_str!("docs/multi_request_media_type.md")] 40 | #[proc_macro_derive(MultiRequestMediaType, attributes(multi_request_media_type))] 41 | pub fn multi_request_media_type(input: TokenStream) -> TokenStream { 42 | let input = parse_macro_input!(input as DeriveInput); 43 | 44 | multi_request_media_type::generate(input) 45 | .unwrap_or_else(|e| e.to_compile_error()) 46 | .into() 47 | } 48 | 49 | #[doc = include_str!("docs/multi_response_media_type.md")] 50 | #[proc_macro_derive(MultiResponseMediaType, attributes(multi_response_media_type))] 51 | pub fn multi_response_media_type(input: TokenStream) -> TokenStream { 52 | let input = parse_macro_input!(input as DeriveInput); 53 | 54 | multi_response_media_type::generate(input) 55 | .unwrap_or_else(|e| e.to_compile_error()) 56 | .into() 57 | } 58 | 59 | #[doc = include_str!("docs/single_response.md")] 60 | #[proc_macro_derive(SingleResponse, attributes(single_response, header))] 61 | pub fn single_response(input: TokenStream) -> TokenStream { 62 | let input = parse_macro_input!(input as DeriveInput); 63 | 64 | single_response::generate(input) 65 | .unwrap_or_else(|e| e.to_compile_error()) 66 | .into() 67 | } 68 | 69 | #[doc = include_str!("docs/multi_response.md")] 70 | #[proc_macro_derive(MultiResponse, attributes(multi_response, status))] 71 | pub fn multi_response(input: TokenStream) -> TokenStream { 72 | let input = parse_macro_input!(input as DeriveInput); 73 | 74 | multi_response::generate(input) 75 | .unwrap_or_else(|e| e.to_compile_error()) 76 | .into() 77 | } 78 | 79 | #[doc = include_str!("docs/multipart.md")] 80 | #[proc_macro_derive(Multipart, attributes(schema))] 81 | pub fn multipart(input: TokenStream) -> TokenStream { 82 | let input = parse_macro_input!(input as DeriveInput); 83 | 84 | multipart::generate(input) 85 | .unwrap_or_else(|e| e.to_compile_error()) 86 | .into() 87 | } 88 | 89 | #[doc = include_str!("docs/tag.md")] 90 | #[proc_macro_derive(Tag, attributes(tag))] 91 | pub fn tag(input: TokenStream) -> TokenStream { 92 | let input = parse_macro_input!(input as DeriveInput); 93 | 94 | tag::generate(input) 95 | .unwrap_or_else(|e| e.to_compile_error()) 96 | .into() 97 | } 98 | 99 | #[doc = include_str!("docs/security_scheme.md")] 100 | #[proc_macro_derive(SecurityScheme, attributes(api_key, http))] 101 | pub fn security_scheme(input: TokenStream) -> TokenStream { 102 | let input = parse_macro_input!(input as DeriveInput); 103 | 104 | security_scheme::generate(input) 105 | .unwrap_or_else(|e| e.to_compile_error()) 106 | .into() 107 | } 108 | -------------------------------------------------------------------------------- /predawn-macro/src/method.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | 3 | use from_attr::FromIdent; 4 | use syn::{Ident, parse_quote}; 5 | 6 | #[derive(Copy, Clone, FromIdent)] 7 | pub(crate) enum Method { 8 | Get, 9 | Post, 10 | Put, 11 | Delete, 12 | Head, 13 | Options, 14 | Connect, 15 | Patch, 16 | Trace, 17 | } 18 | 19 | impl fmt::Display for Method { 20 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 21 | let ident = self.as_uppercase_ident(); 22 | write!(f, "{}", ident) 23 | } 24 | } 25 | 26 | impl Method { 27 | pub(crate) fn as_uppercase_ident(&self) -> Ident { 28 | match self { 29 | Method::Get => parse_quote!(GET), 30 | Method::Post => parse_quote!(POST), 31 | Method::Put => parse_quote!(PUT), 32 | Method::Delete => parse_quote!(DELETE), 33 | Method::Head => parse_quote!(HEAD), 34 | Method::Options => parse_quote!(OPTIONS), 35 | Method::Connect => parse_quote!(CONNECT), 36 | Method::Patch => parse_quote!(PATCH), 37 | Method::Trace => parse_quote!(TRACE), 38 | } 39 | } 40 | } 41 | 42 | pub(crate) const ENUM_METHODS: [Method; 9] = [ 43 | Method::Get, 44 | Method::Post, 45 | Method::Put, 46 | Method::Delete, 47 | Method::Head, 48 | Method::Options, 49 | Method::Connect, 50 | Method::Patch, 51 | Method::Trace, 52 | ]; 53 | -------------------------------------------------------------------------------- /predawn-macro/src/tag.rs: -------------------------------------------------------------------------------- 1 | use from_attr::{AttrsValue, FromAttr}; 2 | use proc_macro2::TokenStream; 3 | use quote::quote; 4 | use quote_use::quote_use; 5 | use syn::DeriveInput; 6 | 7 | #[derive(FromAttr, Default)] 8 | #[attribute(idents = [tag])] 9 | struct TypeAttr { 10 | rename: Option, 11 | } 12 | 13 | pub(crate) fn generate(input: DeriveInput) -> syn::Result { 14 | let DeriveInput { attrs, ident, .. } = input; 15 | 16 | let TypeAttr { rename } = match TypeAttr::from_attributes(&attrs) { 17 | Ok(Some(AttrsValue { 18 | value: type_attr, .. 19 | })) => type_attr, 20 | Ok(None) => Default::default(), 21 | Err(AttrsValue { value: e, .. }) => return Err(e), 22 | }; 23 | 24 | let ident_str = rename.unwrap_or_else(|| ident.to_string()); 25 | 26 | let description = predawn_macro_core::util::extract_description(&attrs); 27 | let description = if description.is_empty() { 28 | quote! { None } 29 | } else { 30 | let description = predawn_macro_core::util::generate_string_expr(&description); 31 | quote! { Some(#description) } 32 | }; 33 | 34 | let expand = quote_use! { 35 | # use core::default::Default; 36 | # use std::string::ToString; 37 | # use predawn::Tag; 38 | # use predawn::openapi; 39 | 40 | impl Tag for #ident { 41 | const NAME: &'static str = #ident_str; 42 | 43 | fn create() -> openapi::Tag { 44 | openapi::Tag { 45 | name: ToString::to_string(Self::NAME), 46 | description: #description, 47 | external_docs: Default::default(), 48 | extensions: Default::default(), 49 | } 50 | } 51 | } 52 | }; 53 | 54 | Ok(expand) 55 | } 56 | -------------------------------------------------------------------------------- /predawn-schema-macro/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "predawn-schema-macro" 3 | description = "Macros for predawn-schema" 4 | keywords = ["predawn", "schema", "macro"] 5 | version.workspace = true 6 | edition.workspace = true 7 | authors.workspace = true 8 | license.workspace = true 9 | homepage.workspace = true 10 | repository.workspace = true 11 | include.workspace = true 12 | readme.workspace = true 13 | 14 | [lib] 15 | proc-macro = true 16 | 17 | [lints] 18 | workspace = true 19 | 20 | [dependencies] 21 | predawn-macro-core = { workspace = true } 22 | 23 | from-attr = { workspace = true } 24 | proc-macro2 = { workspace = true } 25 | quote = { workspace = true } 26 | quote-use = { workspace = true } 27 | syn = { workspace = true } 28 | 29 | [features] 30 | default = ["__used_in_predawn"] 31 | __used_in_predawn = ["predawn-macro-core/__used_in_predawn"] 32 | __used_in_predawn_schema = ["predawn-macro-core/__used_in_predawn_schema"] 33 | -------------------------------------------------------------------------------- /predawn-schema-macro/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod to_schema; 2 | mod types; 3 | mod util; 4 | 5 | use proc_macro::TokenStream; 6 | use syn::{DeriveInput, parse_macro_input}; 7 | 8 | #[proc_macro_derive(ToSchema, attributes(schema))] 9 | pub fn to_schema(input: TokenStream) -> TokenStream { 10 | let input = parse_macro_input!(input as DeriveInput); 11 | 12 | to_schema::generate(input) 13 | .unwrap_or_else(|e| e.to_compile_error()) 14 | .into() 15 | } 16 | -------------------------------------------------------------------------------- /predawn-schema-macro/src/types.rs: -------------------------------------------------------------------------------- 1 | use syn::{Attribute, Field, Ident, Token, punctuated::Punctuated}; 2 | 3 | pub(crate) struct UnitVariant { 4 | pub(crate) attrs: Vec, 5 | pub(crate) ident: Ident, 6 | } 7 | 8 | pub(crate) struct SchemaVariant { 9 | pub(crate) attrs: Vec, 10 | pub(crate) ident: Ident, 11 | pub(crate) fields: SchemaFields, 12 | } 13 | 14 | pub(crate) enum SchemaFields { 15 | Unit, 16 | Unnamed(Box), 17 | Named(Punctuated), 18 | } 19 | 20 | pub(crate) enum SchemaProperties { 21 | NamedStruct(Punctuated), 22 | OnlyUnitEnum(Vec), 23 | NormalEnum(Vec), 24 | } 25 | -------------------------------------------------------------------------------- /predawn-schema/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "predawn-schema" 3 | description = "OpenAPI schema for predawn" 4 | keywords = ["http", "web", "framework", "openapi", "schema"] 5 | version.workspace = true 6 | edition.workspace = true 7 | authors.workspace = true 8 | license.workspace = true 9 | homepage.workspace = true 10 | repository.workspace = true 11 | include.workspace = true 12 | readme.workspace = true 13 | 14 | [lints] 15 | workspace = true 16 | 17 | [dependencies] 18 | predawn-schema-macro = { workspace = true, optional = true, features = [ 19 | "__used_in_predawn_schema", 20 | ] } 21 | 22 | openapiv3 = { workspace = true } 23 | indexmap = { workspace = true } 24 | serde_json = { workspace = true } 25 | bytes = { workspace = true } 26 | macro-v = { workspace = true } 27 | paste = { workspace = true } 28 | schemars = { workspace = true, optional = true } 29 | 30 | [features] 31 | default = ["macro"] 32 | macro = ["dep:predawn-schema-macro"] 33 | raw_value = ["serde_json/raw_value"] 34 | schemars = ["dep:schemars"] 35 | 36 | [package.metadata.docs.rs] 37 | all-features = true 38 | rustdoc-args = ["--cfg", "docsrs"] 39 | -------------------------------------------------------------------------------- /predawn-schema/src/impls/atomic.rs: -------------------------------------------------------------------------------- 1 | use std::sync::atomic::{ 2 | AtomicBool, AtomicI8, AtomicI16, AtomicI32, AtomicI64, AtomicIsize, AtomicU8, AtomicU16, 3 | AtomicU32, AtomicU64, AtomicUsize, 4 | }; 5 | 6 | use super::forward_impl; 7 | 8 | forward_impl!(AtomicBool => bool); 9 | forward_impl!(AtomicI8 => i8); 10 | forward_impl!(AtomicI16 => i16); 11 | forward_impl!(AtomicI32 => i32); 12 | forward_impl!(AtomicI64 => i64); 13 | forward_impl!(AtomicIsize => isize); 14 | forward_impl!(AtomicU8 => u8); 15 | forward_impl!(AtomicU16 => u16); 16 | forward_impl!(AtomicU32 => u32); 17 | forward_impl!(AtomicU64 => u64); 18 | forward_impl!(AtomicUsize => usize); 19 | -------------------------------------------------------------------------------- /predawn-schema/src/impls/bytes.rs: -------------------------------------------------------------------------------- 1 | use bytes::{Bytes, BytesMut}; 2 | 3 | use super::forward_impl; 4 | 5 | forward_impl!(Bytes => Vec); 6 | forward_impl!(BytesMut => Vec); 7 | -------------------------------------------------------------------------------- /predawn-schema/src/impls/ffi.rs: -------------------------------------------------------------------------------- 1 | use std::ffi::{CStr, CString}; 2 | 3 | use super::forward_impl; 4 | 5 | forward_impl!(CString => Vec); 6 | forward_impl!(CStr => Vec); 7 | -------------------------------------------------------------------------------- /predawn-schema/src/impls/json.rs: -------------------------------------------------------------------------------- 1 | use std::{borrow::Cow, collections::BTreeMap}; 2 | 3 | use openapiv3::{AnySchema, NumberType, Schema, SchemaData, SchemaKind, Type}; 4 | use serde_json::{Map, Number, Value}; 5 | 6 | use super::forward_impl; 7 | use crate::ToSchema; 8 | 9 | impl ToSchema for Value { 10 | fn title() -> Cow<'static, str> { 11 | "Any".into() 12 | } 13 | 14 | fn schema(_: &mut BTreeMap, _: &mut Vec) -> Schema { 15 | Schema { 16 | schema_data: SchemaData { 17 | title: Some(Self::title().into()), 18 | ..Default::default() 19 | }, 20 | schema_kind: SchemaKind::Any(AnySchema::default()), 21 | } 22 | } 23 | } 24 | 25 | forward_impl!(Map => BTreeMap); 26 | 27 | impl ToSchema for Number { 28 | fn title() -> Cow<'static, str> { 29 | "Number".into() 30 | } 31 | 32 | fn schema(_: &mut BTreeMap, _: &mut Vec) -> Schema { 33 | Schema { 34 | schema_data: SchemaData { 35 | title: Some(Self::title().into()), 36 | ..Default::default() 37 | }, 38 | schema_kind: SchemaKind::Type(Type::Number(NumberType::default())), 39 | } 40 | } 41 | } 42 | 43 | #[cfg_attr(docsrs, doc(cfg(feature = "raw_value")))] 44 | #[cfg(feature = "raw_value")] 45 | mod raw_value { 46 | use serde_json::{Value, value::RawValue}; 47 | 48 | use super::forward_impl; 49 | forward_impl!(RawValue => Value); 50 | } 51 | -------------------------------------------------------------------------------- /predawn-schema/src/impls/map.rs: -------------------------------------------------------------------------------- 1 | use std::{borrow::Cow, collections::BTreeMap}; 2 | 3 | use openapiv3::{AdditionalProperties, ObjectType, Schema, SchemaData, SchemaKind, Type}; 4 | 5 | use crate::ToSchema; 6 | 7 | macro_rules! map_impl { 8 | ($($desc:tt)+) => { 9 | impl $($desc)+ 10 | where 11 | V: ToSchema 12 | { 13 | fn title() -> Cow<'static, str> { 14 | format!("Map", V::title()).into() 15 | } 16 | 17 | fn schema(schemas: &mut BTreeMap, schemas_in_progress: &mut Vec) -> Schema { 18 | let ty = ObjectType { 19 | additional_properties: Some(AdditionalProperties::Schema(Box::new(V::schema_ref(schemas, schemas_in_progress)))), 20 | ..Default::default() 21 | }; 22 | 23 | Schema { 24 | schema_data: SchemaData { 25 | title: Some(Self::title().into()), 26 | ..Default::default() 27 | }, 28 | schema_kind: SchemaKind::Type(Type::Object(ty)), 29 | } 30 | } 31 | } 32 | }; 33 | } 34 | 35 | map_impl!( ToSchema for std::collections::BTreeMap); 36 | map_impl!( ToSchema for std::collections::HashMap); 37 | map_impl!( ToSchema for indexmap::map::IndexMap); 38 | -------------------------------------------------------------------------------- /predawn-schema/src/impls/mod.rs: -------------------------------------------------------------------------------- 1 | mod atomic; 2 | mod bytes; 3 | mod ffi; 4 | mod json; 5 | mod map; 6 | mod non_zero; 7 | mod option; 8 | mod primitive; 9 | mod seq; 10 | mod set; 11 | mod string; 12 | mod time; 13 | mod wrapper; 14 | 15 | use macro_v::macro_v; 16 | 17 | #[macro_v(pub(crate))] 18 | macro_rules! forward_impl { 19 | ($left:ty => $right:ty) => { 20 | impl $crate::ToSchema for $left { 21 | fn title() -> ::std::borrow::Cow<'static, str> { 22 | <$right as $crate::ToSchema>::title() 23 | } 24 | 25 | fn schema( 26 | schemas: &mut ::std::collections::BTreeMap, 27 | schemas_in_progress: &mut ::std::vec::Vec<::std::string::String>, 28 | ) -> ::openapiv3::Schema { 29 | <$right as $crate::ToSchema>::schema(schemas, schemas_in_progress) 30 | } 31 | } 32 | }; 33 | } 34 | 35 | #[cfg(test)] 36 | mod tests { 37 | use crate::ToSchema; 38 | 39 | #[test] 40 | fn aaa() { 41 | dbg!(<[i32; 4] as ToSchema>::title()); 42 | dbg!(<[i32; 5] as ToSchema>::title()); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /predawn-schema/src/impls/non_zero.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | borrow::Cow, 3 | collections::BTreeMap, 4 | num::{ 5 | NonZeroI8, NonZeroI16, NonZeroI32, NonZeroI64, NonZeroI128, NonZeroIsize, NonZeroU8, 6 | NonZeroU16, NonZeroU32, NonZeroU64, NonZeroU128, NonZeroUsize, 7 | }, 8 | }; 9 | 10 | use openapiv3::{ 11 | AnySchema, IntegerType, ReferenceOr, Schema, SchemaData, SchemaKind, Type, 12 | VariantOrUnknownOrEmpty, 13 | }; 14 | 15 | use crate::ToSchema; 16 | 17 | macro_rules! nonzero_signed_impl { 18 | ($ty:ty, $format:literal) => { 19 | impl ToSchema for $ty { 20 | fn title() -> Cow<'static, str> { 21 | stringify!($ty).into() 22 | } 23 | 24 | fn schema(_: &mut BTreeMap, _: &mut Vec) -> Schema { 25 | Schema { 26 | schema_data: SchemaData { 27 | title: Some(Self::title().into()), 28 | ..Default::default() 29 | }, 30 | schema_kind: SchemaKind::Any(AnySchema { 31 | typ: Some("integer".to_string()), 32 | format: Some($format.to_string()), 33 | not: Some(Box::new(ReferenceOr::Item(Schema { 34 | schema_data: SchemaData::default(), 35 | schema_kind: SchemaKind::Type(Type::Integer(IntegerType { 36 | format: VariantOrUnknownOrEmpty::Unknown($format.to_string()), 37 | maximum: Some(0), 38 | minimum: Some(0), 39 | ..Default::default() 40 | })), 41 | }))), 42 | ..Default::default() 43 | }), 44 | } 45 | } 46 | } 47 | }; 48 | } 49 | 50 | nonzero_signed_impl!(NonZeroI8, "int8"); 51 | nonzero_signed_impl!(NonZeroI16, "int16"); 52 | nonzero_signed_impl!(NonZeroI32, "int32"); 53 | nonzero_signed_impl!(NonZeroI64, "int64"); 54 | nonzero_signed_impl!(NonZeroI128, "int128"); 55 | nonzero_signed_impl!(NonZeroIsize, "int"); 56 | 57 | macro_rules! nonzero_unsigned_impl { 58 | ($ty:ty, $format:literal) => { 59 | impl ToSchema for $ty { 60 | fn title() -> Cow<'static, str> { 61 | stringify!($ty).into() 62 | } 63 | 64 | fn schema(_: &mut BTreeMap, _: &mut Vec) -> Schema { 65 | Schema { 66 | schema_data: SchemaData { 67 | title: Some(Self::title().into()), 68 | ..Default::default() 69 | }, 70 | schema_kind: SchemaKind::Type(Type::Integer(IntegerType { 71 | format: VariantOrUnknownOrEmpty::Unknown($format.to_string()), 72 | minimum: Some(1), 73 | ..Default::default() 74 | })), 75 | } 76 | } 77 | } 78 | }; 79 | } 80 | 81 | nonzero_unsigned_impl!(NonZeroU8, "uint8"); 82 | nonzero_unsigned_impl!(NonZeroU16, "uint16"); 83 | nonzero_unsigned_impl!(NonZeroU32, "uint32"); 84 | nonzero_unsigned_impl!(NonZeroU64, "uint64"); 85 | nonzero_unsigned_impl!(NonZeroU128, "uint128"); 86 | nonzero_unsigned_impl!(NonZeroUsize, "uint"); 87 | -------------------------------------------------------------------------------- /predawn-schema/src/impls/option.rs: -------------------------------------------------------------------------------- 1 | use std::{borrow::Cow, collections::BTreeMap}; 2 | 3 | use openapiv3::Schema; 4 | 5 | use crate::ToSchema; 6 | 7 | impl ToSchema for Option { 8 | const REQUIRED: bool = false; 9 | 10 | fn title() -> Cow<'static, str> { 11 | format!("Option<{}>", T::title()).into() 12 | } 13 | 14 | fn schema( 15 | schemas: &mut BTreeMap, 16 | schemas_in_progress: &mut Vec, 17 | ) -> Schema { 18 | let mut schema = T::schema(schemas, schemas_in_progress); 19 | 20 | schema.schema_data.nullable = true; 21 | schema.schema_data.title = Some(Self::title().into()); 22 | 23 | schema 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /predawn-schema/src/impls/primitive.rs: -------------------------------------------------------------------------------- 1 | use std::{borrow::Cow, collections::BTreeMap}; 2 | 3 | use openapiv3::{ 4 | ArrayType, BooleanType, IntegerType, NumberType, Schema, SchemaData, SchemaKind, StringType, 5 | Type, VariantOrUnknownOrEmpty, 6 | }; 7 | use paste::paste; 8 | 9 | use crate::ToSchema; 10 | 11 | impl ToSchema for bool { 12 | fn title() -> Cow<'static, str> { 13 | "bool".into() 14 | } 15 | 16 | fn schema(_: &mut BTreeMap, _: &mut Vec) -> Schema { 17 | Schema { 18 | schema_data: SchemaData { 19 | title: Some(Self::title().into()), 20 | ..Default::default() 21 | }, 22 | schema_kind: SchemaKind::Type(Type::Boolean(BooleanType::default())), 23 | } 24 | } 25 | } 26 | 27 | impl ToSchema for char { 28 | fn title() -> Cow<'static, str> { 29 | "char".into() 30 | } 31 | 32 | fn schema(_: &mut BTreeMap, _: &mut Vec) -> Schema { 33 | Schema { 34 | schema_data: SchemaData { 35 | title: Some(Self::title().into()), 36 | ..Default::default() 37 | }, 38 | schema_kind: SchemaKind::Type(Type::String(StringType { 39 | min_length: Some(1), 40 | max_length: Some(1), 41 | ..Default::default() 42 | })), 43 | } 44 | } 45 | } 46 | 47 | macro_rules! simple_impl { 48 | ($ty:ty, $ty_variant:ident, $format:literal) => { 49 | impl ToSchema for $ty { 50 | fn title() -> Cow<'static, str> { 51 | stringify!($ty).into() 52 | } 53 | 54 | fn schema(_: &mut BTreeMap, _: &mut Vec) -> Schema { 55 | Schema { 56 | schema_data: SchemaData { 57 | title: Some(Self::title().into()), 58 | ..Default::default() 59 | }, 60 | schema_kind: paste! { 61 | SchemaKind::Type(Type::$ty_variant([<$ty_variant Type>] { 62 | format: VariantOrUnknownOrEmpty::Unknown($format.to_string()), 63 | ..Default::default() 64 | })) 65 | }, 66 | } 67 | } 68 | } 69 | }; 70 | } 71 | 72 | simple_impl!(f32, Number, "float"); 73 | simple_impl!(f64, Number, "double"); 74 | simple_impl!(i8, Integer, "int8"); 75 | simple_impl!(i16, Integer, "int16"); 76 | simple_impl!(i32, Integer, "int32"); 77 | simple_impl!(i64, Integer, "int64"); 78 | simple_impl!(i128, Integer, "int128"); 79 | simple_impl!(isize, Integer, "int"); 80 | 81 | macro_rules! unsigned_impl { 82 | ($ty:ty, $format:literal) => { 83 | impl ToSchema for $ty { 84 | fn title() -> Cow<'static, str> { 85 | stringify!($ty).into() 86 | } 87 | 88 | fn schema(_: &mut BTreeMap, _: &mut Vec) -> Schema { 89 | Schema { 90 | schema_data: SchemaData { 91 | title: Some(Self::title().into()), 92 | ..Default::default() 93 | }, 94 | schema_kind: SchemaKind::Type(Type::Integer(IntegerType { 95 | format: VariantOrUnknownOrEmpty::Unknown($format.to_string()), 96 | minimum: Some(0), 97 | ..Default::default() 98 | })), 99 | } 100 | } 101 | } 102 | }; 103 | } 104 | 105 | unsigned_impl!(u8, "uint8"); 106 | unsigned_impl!(u16, "uint16"); 107 | unsigned_impl!(u32, "uint32"); 108 | unsigned_impl!(u64, "uint64"); 109 | unsigned_impl!(u128, "uint128"); 110 | unsigned_impl!(usize, "uint"); 111 | 112 | impl ToSchema for [T; N] { 113 | fn key() -> String { 114 | format!("Array{}<{}>", N, T::key()) 115 | } 116 | 117 | fn title() -> Cow<'static, str> { 118 | format!("Array{}<{}>", N, T::title()).into() 119 | } 120 | 121 | fn schema( 122 | schemas: &mut BTreeMap, 123 | schemas_in_progress: &mut Vec, 124 | ) -> Schema { 125 | let ty = ArrayType { 126 | items: Some(T::schema_ref_box(schemas, schemas_in_progress)), 127 | min_items: Some(N), 128 | max_items: Some(N), 129 | unique_items: false, 130 | }; 131 | 132 | Schema { 133 | schema_data: SchemaData { 134 | title: Some(Self::title().into()), 135 | ..Default::default() 136 | }, 137 | schema_kind: SchemaKind::Type(Type::Array(ty)), 138 | } 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /predawn-schema/src/impls/seq.rs: -------------------------------------------------------------------------------- 1 | use std::{borrow::Cow, collections::BTreeMap}; 2 | 3 | use openapiv3::{ArrayType, Schema, SchemaData, SchemaKind, Type}; 4 | 5 | use crate::ToSchema; 6 | 7 | macro_rules! seq_impl { 8 | ($($desc:tt)+) => { 9 | impl $($desc)+ 10 | where 11 | T: ToSchema 12 | { 13 | fn title() -> Cow<'static, str> { 14 | format!("List<{}>", T::title()).into() 15 | } 16 | 17 | fn schema(schemas: &mut BTreeMap, schemas_in_progress: &mut Vec) -> Schema { 18 | let ty = ArrayType { 19 | items: Some(T::schema_ref_box(schemas, schemas_in_progress)), 20 | min_items: None, 21 | max_items: None, 22 | unique_items: false, 23 | }; 24 | 25 | Schema { 26 | schema_data: SchemaData { 27 | title: Some(Self::title().into()), 28 | ..Default::default() 29 | }, 30 | schema_kind: SchemaKind::Type(Type::Array(ty)), 31 | } 32 | } 33 | } 34 | }; 35 | } 36 | 37 | seq_impl!( ToSchema for std::collections::BinaryHeap); 38 | seq_impl!( ToSchema for std::collections::LinkedList); 39 | seq_impl!( ToSchema for [T]); 40 | seq_impl!( ToSchema for Vec); 41 | seq_impl!( ToSchema for std::collections::VecDeque); 42 | -------------------------------------------------------------------------------- /predawn-schema/src/impls/set.rs: -------------------------------------------------------------------------------- 1 | use std::{borrow::Cow, collections::BTreeMap}; 2 | 3 | use openapiv3::{ArrayType, Schema, SchemaData, SchemaKind, Type}; 4 | 5 | use crate::ToSchema; 6 | 7 | macro_rules! set_impl { 8 | ($($desc:tt)+) => { 9 | impl $($desc)+ 10 | where 11 | T: ToSchema 12 | { 13 | fn title() -> Cow<'static, str> { 14 | format!("Set<{}>", T::title()).into() 15 | } 16 | 17 | fn schema(schemas: &mut BTreeMap, schemas_in_progress: &mut Vec) -> Schema { 18 | let ty = ArrayType { 19 | items: Some(T::schema_ref_box(schemas, schemas_in_progress)), 20 | min_items: None, 21 | max_items: None, 22 | unique_items: true, 23 | }; 24 | 25 | Schema { 26 | schema_data: SchemaData { 27 | title: Some(Self::title().into()), 28 | ..Default::default() 29 | }, 30 | schema_kind: SchemaKind::Type(Type::Array(ty)), 31 | } 32 | } 33 | } 34 | }; 35 | } 36 | 37 | set_impl!( ToSchema for std::collections::BTreeSet); 38 | set_impl!( ToSchema for std::collections::HashSet); 39 | set_impl!( ToSchema for indexmap::set::IndexSet); 40 | -------------------------------------------------------------------------------- /predawn-schema/src/impls/string.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | borrow::Cow, 3 | collections::BTreeMap, 4 | net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV4, SocketAddrV6}, 5 | path::{Path, PathBuf}, 6 | }; 7 | 8 | use openapiv3::{Schema, SchemaData, SchemaKind, StringType, Type, VariantOrUnknownOrEmpty}; 9 | 10 | use crate::ToSchema; 11 | 12 | macro_rules! string_impl { 13 | ($ty:ty) => { 14 | string_impl!($ty, VariantOrUnknownOrEmpty::Empty); 15 | }; 16 | ($ty:ty, $format:literal) => { 17 | string_impl!($ty, VariantOrUnknownOrEmpty::Unknown($format.to_string())); 18 | }; 19 | ($ty:ty, $format:expr) => { 20 | impl ToSchema for $ty { 21 | fn title() -> Cow<'static, str> { 22 | stringify!($ty).into() 23 | } 24 | 25 | fn schema(_: &mut BTreeMap, _: &mut Vec) -> Schema { 26 | let ty = StringType { 27 | format: $format, 28 | ..Default::default() 29 | }; 30 | 31 | Schema { 32 | schema_data: SchemaData { 33 | title: Some(Self::title().into()), 34 | ..Default::default() 35 | }, 36 | schema_kind: SchemaKind::Type(Type::String(ty)), 37 | } 38 | } 39 | } 40 | }; 41 | } 42 | 43 | string_impl!(str); 44 | string_impl!(String); 45 | string_impl!(Path); 46 | string_impl!(PathBuf); 47 | string_impl!(Ipv4Addr, "ipv4"); 48 | string_impl!(Ipv6Addr, "ipv6"); 49 | string_impl!(SocketAddrV4); 50 | string_impl!(SocketAddrV6); 51 | 52 | macro_rules! one_of_string_impl { 53 | ($ty:ty; [$($elem:ty),+ $(,)?]) => { 54 | impl ToSchema for $ty { 55 | fn title() -> Cow<'static, str> { 56 | stringify!($ty).into() 57 | } 58 | 59 | fn schema(schemas: &mut BTreeMap, schemas_in_progress: &mut Vec) -> Schema { 60 | Schema { 61 | schema_data: SchemaData { 62 | title: Some(Self::title().into()), 63 | ..Default::default() 64 | }, 65 | schema_kind: SchemaKind::OneOf { 66 | one_of: [ 67 | $( 68 | <$elem>::schema_ref(schemas, schemas_in_progress), 69 | )+ 70 | ] 71 | .to_vec(), 72 | }, 73 | } 74 | } 75 | } 76 | }; 77 | } 78 | 79 | one_of_string_impl!(IpAddr; [Ipv4Addr, Ipv6Addr]); 80 | one_of_string_impl!(SocketAddr; [SocketAddrV4, SocketAddrV6]); 81 | -------------------------------------------------------------------------------- /predawn-schema/src/impls/time.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | borrow::Cow, 3 | collections::BTreeMap, 4 | time::{Duration, SystemTime}, 5 | }; 6 | 7 | use openapiv3::{ObjectType, Schema, SchemaData, SchemaKind, Type}; 8 | 9 | use crate::ToSchema; 10 | 11 | impl ToSchema for SystemTime { 12 | fn title() -> Cow<'static, str> { 13 | "SystemTime".into() 14 | } 15 | 16 | fn schema( 17 | schemas: &mut BTreeMap, 18 | schemas_in_progress: &mut Vec, 19 | ) -> Schema { 20 | const SECS_SINCE_EPOCH: &str = "secs_since_epoch"; 21 | const NANOS_SINCE_EPOCH: &str = "nanos_since_epoch"; 22 | 23 | let mut ty = ObjectType::default(); 24 | 25 | ty.properties.insert( 26 | SECS_SINCE_EPOCH.to_string(), 27 | i64::schema_ref_box(schemas, schemas_in_progress), 28 | ); 29 | ty.properties.insert( 30 | NANOS_SINCE_EPOCH.to_string(), 31 | u32::schema_ref_box(schemas, schemas_in_progress), 32 | ); 33 | 34 | ty.required.push(SECS_SINCE_EPOCH.to_string()); 35 | ty.required.push(NANOS_SINCE_EPOCH.to_string()); 36 | 37 | Schema { 38 | schema_data: SchemaData { 39 | title: Some(Self::title().into()), 40 | ..Default::default() 41 | }, 42 | schema_kind: SchemaKind::Type(Type::Object(ty)), 43 | } 44 | } 45 | } 46 | 47 | impl ToSchema for Duration { 48 | fn title() -> Cow<'static, str> { 49 | "Duration".into() 50 | } 51 | 52 | fn schema( 53 | schemas: &mut BTreeMap, 54 | schemas_in_progress: &mut Vec, 55 | ) -> Schema { 56 | const SECS: &str = "secs"; 57 | const NANOS: &str = "nanos"; 58 | 59 | let mut ty = ObjectType::default(); 60 | 61 | ty.properties.insert( 62 | SECS.to_string(), 63 | u64::schema_ref_box(schemas, schemas_in_progress), 64 | ); 65 | ty.properties.insert( 66 | NANOS.to_string(), 67 | u32::schema_ref_box(schemas, schemas_in_progress), 68 | ); 69 | 70 | ty.required.push(SECS.to_string()); 71 | ty.required.push(NANOS.to_string()); 72 | 73 | Schema { 74 | schema_data: SchemaData { 75 | title: Some(Self::title().into()), 76 | ..Default::default() 77 | }, 78 | schema_kind: SchemaKind::Type(Type::Object(ty)), 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /predawn-schema/src/impls/wrapper.rs: -------------------------------------------------------------------------------- 1 | use std::{borrow::Cow, collections::BTreeMap}; 2 | 3 | use openapiv3::Schema; 4 | 5 | use crate::ToSchema; 6 | 7 | macro_rules! wrapper_impl { 8 | ($($desc:tt)+) => { 9 | impl $($desc)+ 10 | where 11 | T: ToSchema 12 | { 13 | fn title() -> Cow<'static, str> { 14 | T::title() 15 | } 16 | 17 | fn schema(schemas: &mut BTreeMap, schemas_in_progress: &mut Vec) -> Schema { 18 | T::schema(schemas, schemas_in_progress) 19 | } 20 | } 21 | }; 22 | } 23 | 24 | wrapper_impl!(<'a, T: ?Sized> ToSchema for &'a T); 25 | wrapper_impl!(<'a, T: ?Sized> ToSchema for &'a mut T); 26 | wrapper_impl!( ToSchema for Box); 27 | wrapper_impl!( ToSchema for std::rc::Rc); 28 | wrapper_impl!( ToSchema for std::rc::Weak); 29 | wrapper_impl!( ToSchema for std::sync::Arc); 30 | wrapper_impl!( ToSchema for std::sync::Weak); 31 | wrapper_impl!( ToSchema for std::sync::Mutex); 32 | wrapper_impl!( ToSchema for std::sync::RwLock); 33 | wrapper_impl!( ToSchema for std::cell::Cell); 34 | wrapper_impl!( ToSchema for std::cell::RefCell); 35 | wrapper_impl!(<'a, T: ?Sized + ToOwned> ToSchema for std::borrow::Cow<'a, T>); 36 | wrapper_impl!( ToSchema for std::num::Wrapping); 37 | wrapper_impl!( ToSchema for std::cmp::Reverse); 38 | -------------------------------------------------------------------------------- /predawn-schema/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![cfg_attr(docsrs, feature(doc_cfg))] 2 | 3 | mod impls; 4 | 5 | #[cfg_attr(docsrs, doc(cfg(feature = "schemars")))] 6 | #[cfg(feature = "schemars")] 7 | mod schemars_transform; 8 | 9 | #[doc(hidden)] 10 | pub mod to_schema; 11 | 12 | pub mod openapi { 13 | pub use openapiv3::*; 14 | } 15 | #[cfg_attr(docsrs, doc(cfg(feature = "macro")))] 16 | #[cfg(feature = "macro")] 17 | pub use predawn_schema_macro::ToSchema; 18 | 19 | #[cfg_attr(docsrs, doc(cfg(feature = "schemars")))] 20 | #[cfg(feature = "schemars")] 21 | pub use self::schemars_transform::schemars_transform; 22 | pub use self::to_schema::ToSchema; 23 | 24 | #[doc(hidden)] 25 | pub mod __internal { 26 | pub use serde_json; 27 | } 28 | -------------------------------------------------------------------------------- /predawn-schema/src/schemars_transform.rs: -------------------------------------------------------------------------------- 1 | use bytes::{BufMut, BytesMut}; 2 | use openapiv3::Schema; 3 | use schemars::{JsonSchema, schema::RootSchema, schema_for}; 4 | 5 | pub fn schemars_transform() -> Result { 6 | fn inner_transform(schema: RootSchema) -> Result { 7 | let mut buf = BytesMut::with_capacity(128).writer(); 8 | serde_json::to_writer(&mut buf, &schema)?; 9 | serde_json::from_slice(&buf.into_inner()) 10 | } 11 | 12 | inner_transform(schema_for!(T)) 13 | } 14 | -------------------------------------------------------------------------------- /predawn-schema/src/to_schema.rs: -------------------------------------------------------------------------------- 1 | use std::{borrow::Cow, collections::BTreeMap}; 2 | 3 | use openapiv3::{ReferenceOr, Schema}; 4 | 5 | pub trait ToSchema { 6 | const REQUIRED: bool = true; 7 | 8 | fn key() -> String { 9 | std::any::type_name::().replace("::", ".") 10 | } 11 | 12 | fn title() -> Cow<'static, str>; 13 | 14 | fn schema_ref( 15 | schemas: &mut BTreeMap, 16 | schemas_in_progress: &mut Vec, 17 | ) -> ReferenceOr { 18 | reference::(schemas, schemas_in_progress) 19 | } 20 | 21 | fn schema_ref_box( 22 | schemas: &mut BTreeMap, 23 | schemas_in_progress: &mut Vec, 24 | ) -> ReferenceOr> { 25 | reference::(schemas, schemas_in_progress) 26 | } 27 | 28 | fn schema( 29 | schemas: &mut BTreeMap, 30 | schemas_in_progress: &mut Vec, 31 | ) -> Schema; 32 | } 33 | 34 | fn reference( 35 | schemas: &mut BTreeMap, 36 | schemas_in_progress: &mut Vec, 37 | ) -> ReferenceOr 38 | where 39 | S: ToSchema + ?Sized, 40 | { 41 | let key = S::key(); 42 | 43 | let reference = ReferenceOr::Reference { 44 | reference: format!("#/components/schemas/{}", key), 45 | }; 46 | 47 | if !schemas.contains_key(&key) { 48 | // nested types 49 | if schemas_in_progress.contains(&key) { 50 | return reference; 51 | } 52 | 53 | schemas_in_progress.push(key); 54 | let schema = S::schema(schemas, schemas_in_progress); 55 | let key = schemas_in_progress.pop().expect("must have a key"); 56 | 57 | debug_assert_eq!(key, S::key()); 58 | 59 | schemas.insert(key, schema); 60 | } 61 | 62 | reference 63 | } 64 | -------------------------------------------------------------------------------- /predawn-sea-orm/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "predawn-sea-orm" 3 | description = "Sea Orm Integration for Predawn" 4 | keywords = ["sea-orm", "predawn", "middleware"] 5 | version.workspace = true 6 | edition.workspace = true 7 | authors.workspace = true 8 | license.workspace = true 9 | homepage.workspace = true 10 | repository.workspace = true 11 | include.workspace = true 12 | readme.workspace = true 13 | 14 | [lints] 15 | workspace = true 16 | 17 | [dependencies] 18 | predawn = { workspace = true } 19 | 20 | async-trait = { workspace = true } 21 | sea-orm = { workspace = true } 22 | tokio = { workspace = true, features = ["rt"] } 23 | http = { workspace = true } 24 | serde = { workspace = true, features = ["derive"] } 25 | url = { workspace = true, features = ["serde"] } 26 | rudi = { workspace = true, features = ["rudi-macro"] } 27 | snafu = { workspace = true, features = ["rust_1_65", "std"] } 28 | duration-str = { workspace = true, features = ["serde"] } 29 | 30 | [features] 31 | 32 | # database 33 | 34 | mysql = ["sea-orm/sqlx-mysql"] 35 | postgres = ["sea-orm/sqlx-postgres"] 36 | sqlite = ["sea-orm/sqlx-sqlite"] 37 | 38 | # async runtime 39 | 40 | # async-std 41 | runtime-async-std-native-tls = ["sea-orm/runtime-async-std-native-tls"] 42 | runtime-async-std-rustls = ["sea-orm/runtime-async-std-rustls"] 43 | 44 | # tokio 45 | runtime-tokio-native-tls = ["sea-orm/runtime-tokio-native-tls"] 46 | runtime-tokio-rustls = ["sea-orm/runtime-tokio-rustls"] 47 | 48 | # actix 49 | runtime-actix-native-tls = ["sea-orm/runtime-actix-native-tls"] 50 | runtime-actix-rustls = ["sea-orm/runtime-actix-rustls"] 51 | -------------------------------------------------------------------------------- /predawn-sea-orm/src/data_sources.rs: -------------------------------------------------------------------------------- 1 | use std::{collections::HashMap, sync::Arc}; 2 | 3 | use rudi::SingleOwner; 4 | use sea_orm::DatabaseConnection; 5 | 6 | use crate::{DEFAULT_DATA_SOURCE, DataSource, Error, inner::Inner}; 7 | 8 | #[derive(Debug)] 9 | pub struct DataSources(HashMap, DataSource>); 10 | 11 | impl DataSources { 12 | pub fn with_default(conn: DatabaseConnection) -> Self { 13 | let name = Arc::::from(DEFAULT_DATA_SOURCE); 14 | 15 | let mut map = HashMap::new(); 16 | map.insert(name.clone(), DataSource::new(name, conn)); 17 | 18 | Self(map) 19 | } 20 | 21 | pub fn new(map: &HashMap, DatabaseConnection>) -> Self { 22 | let map = map 23 | .iter() 24 | .map(|(name, conn)| (name.clone(), DataSource::new(name.clone(), conn.clone()))) 25 | .collect(); 26 | 27 | Self(map) 28 | } 29 | 30 | pub fn get(&self, name: &str) -> Option<&DataSource> { 31 | self.0.get(name) 32 | } 33 | 34 | pub fn standalone(&self) -> Self { 35 | let map = self 36 | .0 37 | .iter() 38 | .map(|(name, conn)| (name.clone(), conn.standalone())) 39 | .collect(); 40 | 41 | Self(map) 42 | } 43 | 44 | pub async fn commit_all(&self) -> Result<(), Error> { 45 | for source in self.0.values() { 46 | source.commit_all().await?; 47 | } 48 | 49 | Ok(()) 50 | } 51 | 52 | pub async fn rollback_all(&self) -> Result<(), Error> { 53 | for source in self.0.values() { 54 | source.rollback_all().await?; 55 | } 56 | 57 | Ok(()) 58 | } 59 | } 60 | 61 | #[SingleOwner] 62 | impl DataSources { 63 | #[di] 64 | async fn inject(#[di(ref)] Inner(map): &Inner) -> Self { 65 | Self::new(map) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /predawn-sea-orm/src/error.rs: -------------------------------------------------------------------------------- 1 | use std::{collections::BTreeSet, sync::Arc}; 2 | 3 | use http::StatusCode; 4 | use predawn::{ 5 | error2::{ErrorExt, Location, NextError}, 6 | response_error::ResponseError, 7 | }; 8 | use sea_orm::DbErr; 9 | use snafu::Snafu; 10 | 11 | use crate::Transaction; 12 | 13 | #[derive(Debug, Snafu)] 14 | #[snafu(visibility(pub(crate)))] 15 | pub enum Error { 16 | #[snafu(display("{source}"))] 17 | DbErr { 18 | #[snafu(implicit)] 19 | location: Location, 20 | source: DbErr, 21 | }, 22 | 23 | #[snafu(display("not found a data source `{name}`"))] 24 | NotFoundDataSource { 25 | #[snafu(implicit)] 26 | location: Location, 27 | name: Box, 28 | }, 29 | 30 | #[snafu(display("not set data sources in the current context"))] 31 | NotSetDataSources { 32 | #[snafu(implicit)] 33 | location: Location, 34 | }, 35 | 36 | #[snafu(display( 37 | "inconsistent data source and transaction, data source name: `{data_source_name}`, transaction name : `{transaction_name}`" 38 | ))] 39 | InconsistentDataSourceAndTransaction { 40 | #[snafu(implicit)] 41 | location: Location, 42 | data_source_name: Arc, 43 | transaction_name: Arc, 44 | txn: Transaction, 45 | }, 46 | 47 | #[snafu(display( 48 | "transaction have more than one reference, data source name: `{data_source_name}`, transaction hierarchy: `{transaction_hierarchy}`" 49 | ))] 50 | TransactionHaveMoreThanOneReference { 51 | #[snafu(implicit)] 52 | location: Location, 53 | data_source_name: Arc, 54 | transaction_hierarchy: usize, 55 | txn: Transaction, 56 | }, 57 | 58 | #[snafu(display( 59 | "nested transaction have more than one reference, data source name: `{data_source_name}`, current transaction hierarchy: `{current_transaction_hierarchy}`, nested transaction hierarchy: `{nested_transaction_hierarchy}`" 60 | ))] 61 | NestedTransactionHaveMoreThanOneReference { 62 | #[snafu(implicit)] 63 | location: Location, 64 | data_source_name: Arc, 65 | current_transaction_hierarchy: usize, 66 | nested_transaction_hierarchy: usize, 67 | txn: Transaction, 68 | }, 69 | } 70 | 71 | impl ErrorExt for Error { 72 | fn entry(&self) -> (Location, NextError<'_>) { 73 | match self { 74 | Error::DbErr { location, source } => (*location, NextError::Std(source)), 75 | 76 | Error::NotFoundDataSource { location, .. } 77 | | Error::NotSetDataSources { location } 78 | | Error::InconsistentDataSourceAndTransaction { location, .. } 79 | | Error::TransactionHaveMoreThanOneReference { location, .. } 80 | | Error::NestedTransactionHaveMoreThanOneReference { location, .. } => { 81 | (*location, NextError::None) 82 | } 83 | } 84 | } 85 | } 86 | 87 | impl ResponseError for Error { 88 | fn as_status(&self) -> StatusCode { 89 | StatusCode::INTERNAL_SERVER_ERROR 90 | } 91 | 92 | fn status_codes(codes: &mut BTreeSet) { 93 | codes.insert(StatusCode::INTERNAL_SERVER_ERROR); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /predawn-sea-orm/src/function.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use snafu::OptionExt; 4 | 5 | use crate::{ 6 | DATA_SOURCES, DEFAULT_DATA_SOURCE, DataSources, Transaction, 7 | error::{Error, NotFoundDataSourceSnafu, NotSetDataSourcesSnafu}, 8 | }; 9 | 10 | #[inline(always)] 11 | pub async fn default_txn() -> Result { 12 | current_txn(DEFAULT_DATA_SOURCE).await 13 | } 14 | 15 | pub async fn current_txn(name: &str) -> Result { 16 | let txn = data_sources()? 17 | .get(name) 18 | .context(NotFoundDataSourceSnafu { name })? 19 | .current_txn() 20 | .await?; 21 | 22 | Ok(txn) 23 | } 24 | 25 | pub async fn create_txn(name: &str) -> Result { 26 | let txn = data_sources()? 27 | .get(name) 28 | .context(NotFoundDataSourceSnafu { name })? 29 | .create_txn() 30 | .await?; 31 | 32 | Ok(txn) 33 | } 34 | 35 | pub async fn commit(txn: Transaction) -> Result<(), Error> { 36 | let data_sources = data_sources()?; 37 | 38 | let source = data_sources 39 | .get(&txn.name) 40 | .context(NotFoundDataSourceSnafu { 41 | name: txn.name.as_ref(), 42 | })?; 43 | 44 | source.commit(txn).await 45 | } 46 | 47 | pub async fn rollback(txn: Transaction) -> Result<(), Error> { 48 | let data_sources = data_sources()?; 49 | 50 | let source = data_sources 51 | .get(&txn.name) 52 | .context(NotFoundDataSourceSnafu { 53 | name: txn.name.as_ref(), 54 | })?; 55 | 56 | source.rollback(txn).await 57 | } 58 | 59 | pub fn data_sources() -> Result, Error> { 60 | DATA_SOURCES 61 | .try_with(Arc::clone) 62 | .map_err(|_| NotSetDataSourcesSnafu.build()) 63 | } 64 | -------------------------------------------------------------------------------- /predawn-sea-orm/src/inner.rs: -------------------------------------------------------------------------------- 1 | use std::{collections::HashMap, sync::Arc}; 2 | 3 | use rudi::Singleton; 4 | use sea_orm::{Database, DatabaseConnection}; 5 | 6 | use crate::DataSourcesConfig; 7 | 8 | #[derive(Debug, Clone)] 9 | pub(crate) struct Inner(pub(crate) HashMap, DatabaseConnection>); 10 | 11 | #[Singleton] 12 | impl Inner { 13 | #[di] 14 | async fn new(cfg: DataSourcesConfig) -> Self { 15 | let DataSourcesConfig { data_sources } = cfg; 16 | 17 | let mut map = HashMap::with_capacity(data_sources.len()); 18 | 19 | for (name, options) in data_sources { 20 | let conn = Database::connect(options) 21 | .await 22 | .expect("failed to create data sources"); 23 | 24 | let _ = map.insert(name.into(), conn); 25 | } 26 | 27 | Self(map) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /predawn-sea-orm/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod config; 2 | mod data_source; 3 | mod data_sources; 4 | mod error; 5 | mod function; 6 | mod inner; 7 | mod middleware; 8 | mod transaction; 9 | 10 | pub const DEFAULT_DATA_SOURCE: &str = "default"; 11 | 12 | tokio::task_local! { 13 | pub static DATA_SOURCES: std::sync::Arc; 14 | } 15 | 16 | pub use self::{ 17 | config::{ConnectOptions, DataSourcesConfig, SlowStatementsLoggingSettings}, 18 | data_source::DataSource, 19 | data_sources::DataSources, 20 | error::Error, 21 | function::{commit, create_txn, current_txn, data_sources, default_txn, rollback}, 22 | middleware::{SeaOrmHandler, SeaOrmMiddleware}, 23 | transaction::Transaction, 24 | }; 25 | -------------------------------------------------------------------------------- /predawn-sea-orm/src/middleware.rs: -------------------------------------------------------------------------------- 1 | use std::{collections::HashMap, sync::Arc}; 2 | 3 | use predawn::{ 4 | error::Error, handler::Handler, middleware::Middleware, request::Request, response::Response, 5 | }; 6 | use rudi::Transient; 7 | use sea_orm::DatabaseConnection; 8 | 9 | use crate::{DATA_SOURCES, DEFAULT_DATA_SOURCE, DataSources, inner::Inner}; 10 | 11 | #[derive(Debug, Clone)] 12 | pub struct SeaOrmMiddleware { 13 | data_sources: HashMap, DatabaseConnection>, 14 | } 15 | 16 | impl SeaOrmMiddleware { 17 | pub fn with_default(conn: DatabaseConnection) -> Self { 18 | let mut data_sources = HashMap::new(); 19 | 20 | data_sources.insert(Arc::::from(DEFAULT_DATA_SOURCE), conn); 21 | 22 | Self { data_sources } 23 | } 24 | 25 | pub fn new(map: HashMap, DatabaseConnection>) -> Self { 26 | Self { data_sources: map } 27 | } 28 | } 29 | 30 | #[Transient] 31 | impl SeaOrmMiddleware { 32 | #[di] 33 | async fn inject(Inner(map): Inner) -> Self { 34 | Self { data_sources: map } 35 | } 36 | } 37 | 38 | impl Middleware for SeaOrmMiddleware { 39 | type Output = SeaOrmHandler; 40 | 41 | fn transform(self, input: H) -> Self::Output { 42 | SeaOrmHandler { 43 | data_sources: self.data_sources, 44 | inner: input, 45 | } 46 | } 47 | } 48 | 49 | pub struct SeaOrmHandler { 50 | data_sources: HashMap, DatabaseConnection>, 51 | inner: H, 52 | } 53 | 54 | impl Handler for SeaOrmHandler { 55 | async fn call(&self, req: Request) -> Result { 56 | let data_sources = Arc::new(DataSources::new(&self.data_sources)); 57 | 58 | let result = DATA_SOURCES 59 | .scope(data_sources.clone(), async { self.inner.call(req).await }) 60 | .await; 61 | 62 | match &result { 63 | Ok(response) => { 64 | if response.status().is_success() { 65 | data_sources.commit_all().await?; 66 | } else { 67 | data_sources.rollback_all().await?; 68 | } 69 | } 70 | Err(_) => { 71 | data_sources.rollback_all().await?; 72 | } 73 | } 74 | 75 | result 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /predawn-sea-orm/src/transaction.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use async_trait::async_trait; 4 | use sea_orm::{ 5 | ConnectionTrait, DatabaseTransaction, DbBackend, DbErr, ExecResult, QueryResult, Statement, 6 | TransactionTrait, 7 | }; 8 | 9 | #[derive(Debug, Clone)] 10 | pub struct Transaction { 11 | pub(crate) name: Arc, 12 | pub(crate) inner: Arc, 13 | pub(crate) index: usize, 14 | } 15 | 16 | impl Transaction { 17 | #[inline] 18 | pub(crate) async fn begin(&self) -> Result { 19 | self.inner.begin().await 20 | } 21 | } 22 | 23 | #[async_trait] 24 | impl ConnectionTrait for Transaction { 25 | fn get_database_backend(&self) -> DbBackend { 26 | self.inner.get_database_backend() 27 | } 28 | 29 | async fn execute(&self, stmt: Statement) -> Result { 30 | self.inner.execute(stmt).await 31 | } 32 | 33 | async fn execute_unprepared(&self, sql: &str) -> Result { 34 | self.inner.execute_unprepared(sql).await 35 | } 36 | 37 | async fn query_one(&self, stmt: Statement) -> Result, DbErr> { 38 | self.inner.query_one(stmt).await 39 | } 40 | 41 | async fn query_all(&self, stmt: Statement) -> Result, DbErr> { 42 | self.inner.query_all(stmt).await 43 | } 44 | 45 | fn support_returning(&self) -> bool { 46 | self.inner.support_returning() 47 | } 48 | 49 | fn is_mock_connection(&self) -> bool { 50 | self.inner.is_mock_connection() 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /predawn/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "predawn" 3 | description = "Predawn is a Rust web framework like Spring Boot." 4 | keywords = ["http", "web", "framework", "async"] 5 | version.workspace = true 6 | edition.workspace = true 7 | authors.workspace = true 8 | license.workspace = true 9 | homepage.workspace = true 10 | repository.workspace = true 11 | include.workspace = true 12 | readme.workspace = true 13 | 14 | [lints] 15 | workspace = true 16 | 17 | [dependencies] 18 | predawn-core = { workspace = true } 19 | predawn-macro = { workspace = true, optional = true } 20 | predawn-schema = { workspace = true } 21 | predawn-schema-macro = { workspace = true, optional = true, features = [ 22 | "__used_in_predawn", 23 | ] } 24 | 25 | hyper = { workspace = true, features = ["server", "http1", "http2"] } 26 | bytes = { workspace = true } 27 | http = { workspace = true } 28 | futures-core = { workspace = true, features = ["alloc"] } 29 | futures-util = { workspace = true } 30 | matchit = { workspace = true } 31 | tokio = { workspace = true, features = ["macros", "signal"] } 32 | hyper-util = { workspace = true, features = [ 33 | "tokio", 34 | "server", 35 | "http1", 36 | "http2", 37 | ] } 38 | tracing = { workspace = true } 39 | rudi = { workspace = true, features = ["rudi-macro", "auto-register"] } 40 | paste = { workspace = true } 41 | serde = { workspace = true } 42 | serde_json = { workspace = true } 43 | serde_path_to_error = { workspace = true } 44 | mime = { workspace = true } 45 | serde_html_form = { workspace = true } 46 | indexmap = { workspace = true, features = ["std"] } 47 | percent-encoding = { workspace = true } 48 | config = { workspace = true, features = ["toml"] } 49 | tracing-subscriber = { workspace = true, features = ["std", "fmt", "ansi"] } 50 | reqwest = { workspace = true } 51 | http-body-util = { workspace = true } 52 | multer = { workspace = true } 53 | headers = { workspace = true } 54 | tokio-tungstenite = { workspace = true, features = ["connect", "handshake"] } 55 | memchr = { workspace = true, features = ["std"] } 56 | pin-project-lite = { workspace = true } 57 | form_urlencoded = { workspace = true } 58 | snafu = { workspace = true, features = ["rust_1_65", "std"] } 59 | log = { workspace = true } 60 | error2 = { workspace = true, features = ["snafu"] } 61 | 62 | # Optional dependencies 63 | tower = { workspace = true, optional = true } 64 | 65 | [features] 66 | default = ["macro", "auto-register"] 67 | macro = ["dep:predawn-macro", "dep:predawn-schema-macro"] 68 | auto-register = ["predawn-macro?/auto-register"] 69 | tower-compat = ["dep:tower"] 70 | schemars = ["predawn-schema/schemars"] 71 | 72 | [package.metadata.docs.rs] 73 | all-features = true 74 | rustdoc-args = ["--cfg", "docsrs"] 75 | -------------------------------------------------------------------------------- /predawn/src/any_map.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | any::{Any, TypeId}, 3 | collections::HashMap, 4 | }; 5 | 6 | #[derive(Default)] 7 | pub struct AnyMap(HashMap>); 8 | 9 | impl AnyMap { 10 | pub fn insert(&mut self, value: T) -> Option { 11 | self.0 12 | .insert(TypeId::of::(), Box::new(value)) 13 | .map(|boxed| *boxed.downcast().unwrap()) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /predawn/src/config/logger.rs: -------------------------------------------------------------------------------- 1 | use std::{fmt, str::FromStr}; 2 | 3 | use rudi::Singleton; 4 | use serde::{Deserialize, Deserializer, Serialize}; 5 | 6 | use super::{Config, ConfigPrefix}; 7 | 8 | #[derive(Debug, Clone, Default, Serialize, Deserialize)] 9 | #[serde(default, deny_unknown_fields)] 10 | pub struct LoggerConfig { 11 | #[serde(default)] 12 | pub level: Level, 13 | } 14 | 15 | #[Singleton(eager_create)] 16 | impl LoggerConfig { 17 | #[di] 18 | pub fn new(#[di(ref)] config: &Config) -> Self { 19 | config.get().expect("failed to load `LoggerConfig`") 20 | } 21 | } 22 | 23 | impl ConfigPrefix for LoggerConfig { 24 | const PREFIX: &'static str = "logger"; 25 | } 26 | 27 | #[derive(Debug, Default, Copy, Clone, Serialize, PartialEq, Eq)] 28 | pub enum Level { 29 | /// The "trace" level. 30 | Trace, 31 | /// The "debug" level. 32 | Debug, 33 | /// The "info" level. 34 | #[default] 35 | Info, 36 | /// The "warn" level. 37 | Warn, 38 | /// The "error" level. 39 | Error, 40 | /// Off level. 41 | Off, 42 | } 43 | 44 | impl<'de> Deserialize<'de> for Level { 45 | fn deserialize(deserializer: D) -> Result 46 | where 47 | D: Deserializer<'de>, 48 | { 49 | const VARIANTS: [&str; 6] = ["trace", "debug", "info", "warn", "error", "off"]; 50 | 51 | let s = String::deserialize(deserializer)?; 52 | s.parse() 53 | .map_err(|_| ::unknown_variant(&s, &VARIANTS)) 54 | } 55 | } 56 | 57 | #[non_exhaustive] 58 | #[derive(Debug)] 59 | pub struct ParseLevelError; 60 | 61 | impl FromStr for Level { 62 | type Err = ParseLevelError; 63 | 64 | fn from_str(s: &str) -> Result { 65 | match s { 66 | s if s.eq_ignore_ascii_case("trace") => Ok(Level::Trace), 67 | s if s.eq_ignore_ascii_case("debug") => Ok(Level::Debug), 68 | s if s.eq_ignore_ascii_case("info") => Ok(Level::Info), 69 | s if s.eq_ignore_ascii_case("warn") => Ok(Level::Warn), 70 | s if s.eq_ignore_ascii_case("error") => Ok(Level::Error), 71 | s if s.eq_ignore_ascii_case("off") => Ok(Level::Off), 72 | _ => Err(ParseLevelError), 73 | } 74 | } 75 | } 76 | 77 | impl Level { 78 | pub fn as_str(&self) -> &'static str { 79 | match self { 80 | Level::Trace => "Trace", 81 | Level::Debug => "Debug", 82 | Level::Info => "Info", 83 | Level::Warn => "Warn", 84 | Level::Error => "Error", 85 | Level::Off => "Off", 86 | } 87 | } 88 | 89 | pub fn as_tracing_level(&self) -> Option { 90 | match self { 91 | Level::Trace => Some(tracing::Level::TRACE), 92 | Level::Debug => Some(tracing::Level::DEBUG), 93 | Level::Info => Some(tracing::Level::INFO), 94 | Level::Warn => Some(tracing::Level::WARN), 95 | Level::Error => Some(tracing::Level::ERROR), 96 | Level::Off => None, 97 | } 98 | } 99 | 100 | pub fn as_tracing_level_filter(&self) -> tracing::level_filters::LevelFilter { 101 | match self { 102 | Level::Trace => tracing::level_filters::LevelFilter::TRACE, 103 | Level::Debug => tracing::level_filters::LevelFilter::DEBUG, 104 | Level::Info => tracing::level_filters::LevelFilter::INFO, 105 | Level::Warn => tracing::level_filters::LevelFilter::WARN, 106 | Level::Error => tracing::level_filters::LevelFilter::ERROR, 107 | Level::Off => tracing::level_filters::LevelFilter::OFF, 108 | } 109 | } 110 | 111 | pub fn as_log_level(&self) -> Option { 112 | match self { 113 | Level::Trace => Some(log::Level::Trace), 114 | Level::Debug => Some(log::Level::Debug), 115 | Level::Info => Some(log::Level::Info), 116 | Level::Warn => Some(log::Level::Warn), 117 | Level::Error => Some(log::Level::Error), 118 | Level::Off => None, 119 | } 120 | } 121 | 122 | pub fn as_log_level_filter(&self) -> log::LevelFilter { 123 | match self { 124 | Level::Trace => log::LevelFilter::Trace, 125 | Level::Debug => log::LevelFilter::Debug, 126 | Level::Info => log::LevelFilter::Info, 127 | Level::Warn => log::LevelFilter::Warn, 128 | Level::Error => log::LevelFilter::Error, 129 | Level::Off => log::LevelFilter::Off, 130 | } 131 | } 132 | } 133 | 134 | impl fmt::Display for Level { 135 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 136 | f.pad(self.as_str()) 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /predawn/src/config/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod logger; 2 | pub mod openapi; 3 | pub mod server; 4 | 5 | use std::{ 6 | env, 7 | ops::Deref, 8 | path::{Path, PathBuf}, 9 | sync::LazyLock, 10 | }; 11 | 12 | use config::{ConfigError, File, ValueKind}; 13 | use serde::Deserialize; 14 | 15 | use crate::environment::Environment; 16 | 17 | #[derive(Clone)] 18 | pub struct Config { 19 | inner: config::Config, 20 | } 21 | 22 | impl Config { 23 | pub fn new(config: config::Config) -> Self { 24 | Self { inner: config } 25 | } 26 | 27 | pub fn load(env: &Environment) -> Result { 28 | static DEFAULT_FOLDER: LazyLock = LazyLock::new(|| { 29 | let mut dir_path = match env::var("CARGO_MANIFEST_DIR") { 30 | Ok(dir) => PathBuf::from(dir), 31 | Err(_) => { 32 | let mut current_exe = 33 | env::current_exe().expect("failed to get current executable file path"); 34 | 35 | current_exe.pop(); 36 | 37 | current_exe 38 | } 39 | }; 40 | 41 | dir_path.push("config"); 42 | 43 | dir_path 44 | }); 45 | 46 | Self::from_folder(env, DEFAULT_FOLDER.as_path()) 47 | } 48 | 49 | pub fn from_folder(env: &Environment, path: &Path) -> Result { 50 | let app_cfg = path.join("app.toml"); 51 | let env_cfg = path.join(format!("app-{}.toml", env)); 52 | let env = config::Environment::default().separator("_"); 53 | 54 | let mut builder = config::Config::builder(); 55 | 56 | for cfg in [app_cfg, env_cfg].into_iter() { 57 | tracing::info!("try to load config `{}`", cfg.display()); 58 | 59 | if cfg.exists() { 60 | builder = builder.add_source(File::from(cfg)) 61 | } else { 62 | tracing::info!("not found config `{}`", cfg.display()); 63 | } 64 | } 65 | 66 | let config = builder.add_source(env).build()?; 67 | 68 | Ok(Self { inner: config }) 69 | } 70 | 71 | pub fn is_debug(&self) -> bool { 72 | self.inner.get_bool("debug").unwrap_or_default() 73 | } 74 | 75 | pub fn get<'de, T>(&self) -> Result 76 | where 77 | T: ConfigPrefix + Deserialize<'de>, 78 | { 79 | match self.inner.get::(T::PREFIX) { 80 | Ok(o) => Ok(o), 81 | Err(e) => { 82 | let ConfigError::NotFound(_) = &e else { 83 | return Err(e); 84 | }; 85 | 86 | let v = config::Value::new(None, ValueKind::Table(Default::default())); 87 | 88 | match T::deserialize(v) { 89 | Ok(o) => Ok(o), 90 | Err(_) => Err(e), 91 | } 92 | } 93 | } 94 | } 95 | } 96 | 97 | impl Deref for Config { 98 | type Target = config::Config; 99 | 100 | fn deref(&self) -> &Self::Target { 101 | &self.inner 102 | } 103 | } 104 | 105 | pub trait ConfigPrefix { 106 | const PREFIX: &'static str; 107 | } 108 | -------------------------------------------------------------------------------- /predawn/src/config/openapi.rs: -------------------------------------------------------------------------------- 1 | use rudi::Singleton; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | use super::{Config, ConfigPrefix}; 5 | use crate::normalized_path::NormalizedPath; 6 | 7 | #[derive(Debug, Clone, Serialize, Deserialize)] 8 | #[serde(default, deny_unknown_fields)] 9 | pub struct OpenAPIConfig { 10 | #[serde(default = "default_json_path")] 11 | pub json_path: NormalizedPath, 12 | #[serde(default = "default_swagger_ui_path")] 13 | pub swagger_ui_path: NormalizedPath, 14 | #[serde(default = "default_rapidoc_path")] 15 | pub rapidoc_path: NormalizedPath, 16 | #[serde(default = "default_scalar_path")] 17 | pub scalar_path: NormalizedPath, 18 | #[serde(default = "default_redoc_path")] 19 | pub redoc_path: NormalizedPath, 20 | #[serde(default = "default_openapi_explorer_path")] 21 | pub openapi_explorer_path: NormalizedPath, 22 | } 23 | 24 | #[Singleton(eager_create)] 25 | impl OpenAPIConfig { 26 | #[di] 27 | pub fn new(#[di(ref)] config: &Config) -> Self { 28 | config.get().expect("failed to load `OpenAPIConfig`") 29 | } 30 | } 31 | 32 | fn default_json_path() -> NormalizedPath { 33 | "/openapi.json".into() 34 | } 35 | 36 | fn default_swagger_ui_path() -> NormalizedPath { 37 | "/swagger-ui".into() 38 | } 39 | 40 | fn default_rapidoc_path() -> NormalizedPath { 41 | "/rapidoc".into() 42 | } 43 | 44 | fn default_scalar_path() -> NormalizedPath { 45 | "/scalar".into() 46 | } 47 | 48 | fn default_redoc_path() -> NormalizedPath { 49 | "/redoc".into() 50 | } 51 | 52 | fn default_openapi_explorer_path() -> NormalizedPath { 53 | "/openapi-explorer".into() 54 | } 55 | 56 | impl Default for OpenAPIConfig { 57 | fn default() -> Self { 58 | Self { 59 | json_path: default_json_path(), 60 | swagger_ui_path: default_swagger_ui_path(), 61 | rapidoc_path: default_rapidoc_path(), 62 | scalar_path: default_scalar_path(), 63 | redoc_path: default_redoc_path(), 64 | openapi_explorer_path: default_openapi_explorer_path(), 65 | } 66 | } 67 | } 68 | 69 | impl ConfigPrefix for OpenAPIConfig { 70 | const PREFIX: &'static str = "openapi"; 71 | } 72 | -------------------------------------------------------------------------------- /predawn/src/config/server.rs: -------------------------------------------------------------------------------- 1 | use std::net::{IpAddr, Ipv4Addr}; 2 | 3 | use predawn_core::request::DEFAULT_BODY_LIMIT; 4 | use rudi::Singleton; 5 | use serde::{Deserialize, Serialize}; 6 | 7 | use super::{Config, ConfigPrefix}; 8 | use crate::normalized_path::NormalizedPath; 9 | 10 | #[derive(Debug, Clone, Serialize, Deserialize)] 11 | #[serde(default, deny_unknown_fields)] 12 | pub struct ServerConfig { 13 | #[serde(default = "default_ip")] 14 | pub ip: IpAddr, 15 | #[serde(default = "default_port")] 16 | pub port: u16, 17 | #[serde(default = "default_root_path")] 18 | pub root_path: NormalizedPath, 19 | #[serde(default = "default_non_application_root_path")] 20 | pub non_application_root_path: NormalizedPath, 21 | #[serde(default = "default_request_body_limit")] 22 | pub request_body_limit: usize, 23 | } 24 | 25 | #[Singleton(eager_create)] 26 | impl ServerConfig { 27 | #[di] 28 | pub fn new(#[di(ref)] config: &Config) -> Self { 29 | config.get().expect("failed to load `ServerConfig`") 30 | } 31 | 32 | pub fn full_non_application_root_path(self) -> NormalizedPath { 33 | self.root_path.join(self.non_application_root_path) 34 | } 35 | } 36 | 37 | const fn default_ip() -> IpAddr { 38 | IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)) 39 | } 40 | 41 | const fn default_port() -> u16 { 42 | 9612 43 | } 44 | 45 | fn default_root_path() -> NormalizedPath { 46 | "/".into() 47 | } 48 | 49 | fn default_non_application_root_path() -> NormalizedPath { 50 | "/p".into() 51 | } 52 | 53 | const fn default_request_body_limit() -> usize { 54 | DEFAULT_BODY_LIMIT 55 | } 56 | 57 | impl Default for ServerConfig { 58 | fn default() -> Self { 59 | Self { 60 | ip: default_ip(), 61 | port: default_port(), 62 | root_path: default_root_path(), 63 | non_application_root_path: default_non_application_root_path(), 64 | request_body_limit: default_request_body_limit(), 65 | } 66 | } 67 | } 68 | 69 | impl ConfigPrefix for ServerConfig { 70 | const PREFIX: &'static str = "server"; 71 | } 72 | -------------------------------------------------------------------------------- /predawn/src/controller.rs: -------------------------------------------------------------------------------- 1 | use std::{collections::BTreeMap, sync::Arc}; 2 | 3 | use http::Method; 4 | use indexmap::IndexMap; 5 | use predawn_core::openapi::{Operation, Schema, SecurityScheme, Tag}; 6 | use rudi::Context; 7 | 8 | use crate::{handler::DynHandler, normalized_path::NormalizedPath}; 9 | 10 | #[doc(hidden)] 11 | pub trait Controller { 12 | #[allow(clippy::too_many_arguments)] 13 | fn insert_routes( 14 | self: Arc, 15 | cx: &mut Context, 16 | route_table: &mut IndexMap>, 17 | paths: &mut IndexMap>, 18 | schemas: &mut BTreeMap, 19 | schemas_in_progress: &mut Vec, 20 | security_schemes: &mut BTreeMap<&'static str, (&'static str, SecurityScheme)>, 21 | tags: &mut BTreeMap<&'static str, (&'static str, Tag)>, 22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /predawn/src/environment.rs: -------------------------------------------------------------------------------- 1 | use std::{env, fmt}; 2 | 3 | use serde::{Deserialize, Serialize}; 4 | 5 | pub const PREDAWN_ENV: &str = "PREDAWN_ENV"; 6 | 7 | #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)] 8 | pub enum Environment { 9 | #[serde(rename = "prod")] 10 | Prod, 11 | 12 | #[serde(rename = "dev")] 13 | Dev, 14 | 15 | #[serde(rename = "test")] 16 | Test, 17 | 18 | #[serde(untagged)] 19 | Custom(Box), 20 | } 21 | 22 | impl Environment { 23 | pub fn resolve_from_env() -> Self { 24 | match env::var(PREDAWN_ENV) { 25 | Ok(e) => Self::from(e), 26 | Err(_) => { 27 | if cfg!(debug_assertions) { 28 | Environment::Dev 29 | } else { 30 | Environment::Prod 31 | } 32 | } 33 | } 34 | } 35 | } 36 | 37 | impl fmt::Display for Environment { 38 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 39 | match self { 40 | Environment::Prod => write!(f, "prod"), 41 | Environment::Dev => write!(f, "dev"), 42 | Environment::Test => write!(f, "test"), 43 | Environment::Custom(c) => c.fmt(f), 44 | } 45 | } 46 | } 47 | 48 | impl From> for Environment { 49 | fn from(s: Box) -> Self { 50 | if s.eq_ignore_ascii_case("prod") || s.eq_ignore_ascii_case("production") { 51 | return Environment::Prod; 52 | } 53 | 54 | if s.eq_ignore_ascii_case("dev") || s.eq_ignore_ascii_case("development") { 55 | return Environment::Dev; 56 | } 57 | 58 | if s.eq_ignore_ascii_case("test") { 59 | return Environment::Test; 60 | } 61 | 62 | Environment::Custom(s) 63 | } 64 | } 65 | 66 | impl From for Environment { 67 | fn from(s: String) -> Self { 68 | s.into_boxed_str().into() 69 | } 70 | } 71 | 72 | #[cfg(test)] 73 | mod tests { 74 | use super::*; 75 | 76 | #[test] 77 | fn test_env_serialize() { 78 | let env = Environment::Prod; 79 | let serialized = serde_json::to_string(&env).unwrap(); 80 | assert_eq!(serialized, "\"prod\""); 81 | 82 | let env = Environment::Dev; 83 | let serialized = serde_json::to_string(&env).unwrap(); 84 | assert_eq!(serialized, "\"dev\""); 85 | 86 | let env = Environment::Test; 87 | let serialized = serde_json::to_string(&env).unwrap(); 88 | assert_eq!(serialized, "\"test\""); 89 | 90 | let env = Environment::Custom("foo".into()); 91 | let serialized = serde_json::to_string(&env).unwrap(); 92 | assert_eq!(serialized, "\"foo\""); 93 | } 94 | 95 | #[test] 96 | fn test_resolve_from_env() { 97 | let original = env::var(PREDAWN_ENV); 98 | 99 | unsafe { 100 | env::remove_var(PREDAWN_ENV); 101 | } 102 | assert_eq!(Environment::resolve_from_env(), Environment::Dev); 103 | 104 | unsafe { 105 | env::set_var(PREDAWN_ENV, "foo"); 106 | } 107 | assert_eq!( 108 | Environment::resolve_from_env(), 109 | Environment::Custom("foo".into()) 110 | ); 111 | 112 | if let Ok(v) = original { 113 | unsafe { 114 | env::set_var(PREDAWN_ENV, v); 115 | } 116 | } 117 | } 118 | 119 | #[test] 120 | fn test_display() { 121 | assert_eq!("prod", Environment::Prod.to_string()); 122 | assert_eq!("foo", Environment::Custom("foo".into()).to_string()); 123 | } 124 | 125 | #[test] 126 | fn test_into() { 127 | let e: Environment = Box::::from("PROD").into(); 128 | assert_eq!(e, Environment::Prod); 129 | 130 | let e: Environment = Box::::from("FOO").into(); 131 | assert_eq!(e, Environment::Custom("FOO".into())); 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /predawn/src/extract/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod multipart; 2 | mod path; 3 | mod query; 4 | mod typed_header; 5 | pub mod websocket; 6 | 7 | pub use self::{path::Path, query::Query, typed_header::TypedHeader}; 8 | -------------------------------------------------------------------------------- /predawn/src/extract/multipart/extract.rs: -------------------------------------------------------------------------------- 1 | use http_body_util::BodyExt; 2 | use mime::{FORM_DATA, MULTIPART}; 3 | use multer::Field; 4 | use predawn_core::{ 5 | body::RequestBody, 6 | from_request::{FromRequest, OptionalFromRequest}, 7 | media_type::{MediaType, RequestMediaType, has_media_type}, 8 | request::Head, 9 | }; 10 | use snafu::ResultExt; 11 | 12 | use crate::response_error::{ 13 | ByParseMultipartSnafu, InvalidMultipartContentTypeSnafu, MultipartError, 14 | }; 15 | 16 | #[doc(hidden)] 17 | #[derive(Debug)] 18 | pub struct Multipart(multer::Multipart<'static>); 19 | 20 | impl FromRequest for Multipart { 21 | type Error = MultipartError; 22 | 23 | async fn from_request(head: &mut Head, body: RequestBody) -> Result { 24 | let content_type = head.content_type().unwrap_or_default(); 25 | 26 | if !::check_content_type(content_type) { 27 | return InvalidMultipartContentTypeSnafu.fail(); 28 | } 29 | 30 | let boundary = multer::parse_boundary(content_type).context(ByParseMultipartSnafu)?; 31 | let multipart = multer::Multipart::new(body.into_data_stream(), boundary); 32 | Ok(Multipart(multipart)) 33 | } 34 | } 35 | 36 | impl OptionalFromRequest for Multipart { 37 | type Error = MultipartError; 38 | 39 | async fn from_request(head: &mut Head, body: RequestBody) -> Result, Self::Error> { 40 | let Some(content_type) = head.content_type() else { 41 | return Ok(None); 42 | }; 43 | 44 | if !::check_content_type(content_type) { 45 | return InvalidMultipartContentTypeSnafu.fail(); 46 | } 47 | 48 | let boundary = multer::parse_boundary(content_type).context(ByParseMultipartSnafu)?; 49 | let multipart = multer::Multipart::new(body.into_data_stream(), boundary); 50 | Ok(Some(Multipart(multipart))) 51 | } 52 | } 53 | 54 | impl Multipart { 55 | pub async fn next_field(&mut self) -> Result>, MultipartError> { 56 | self.0.next_field().await.context(ByParseMultipartSnafu) 57 | } 58 | } 59 | 60 | impl MediaType for Multipart { 61 | const MEDIA_TYPE: &'static str = "multipart/form-data"; 62 | } 63 | 64 | impl RequestMediaType for Multipart { 65 | fn check_content_type(content_type: &str) -> bool { 66 | has_media_type( 67 | content_type, 68 | MULTIPART.as_str(), 69 | FORM_DATA.as_str(), 70 | FORM_DATA.as_str(), 71 | None, 72 | ) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /predawn/src/extract/multipart/json_field.rs: -------------------------------------------------------------------------------- 1 | use std::{borrow::Cow, collections::BTreeMap}; 2 | 3 | use bytes::Bytes; 4 | use multer::Field; 5 | use predawn_core::{ 6 | impl_deref, 7 | openapi::{ReferenceOr, Schema}, 8 | }; 9 | use predawn_schema::ToSchema; 10 | use serde::de::DeserializeOwned; 11 | use snafu::ResultExt; 12 | 13 | use super::ParseField; 14 | use crate::response_error::{ 15 | DuplicateFieldSnafu, InvalidJsonFieldSnafu, MissingFieldSnafu, MultipartError, 16 | }; 17 | 18 | #[derive(Debug, Default, Clone, Copy)] 19 | pub struct JsonField(pub T); 20 | 21 | impl_deref!(JsonField); 22 | 23 | impl ToSchema for JsonField { 24 | const REQUIRED: bool = T::REQUIRED; 25 | 26 | fn key() -> String { 27 | T::key() 28 | } 29 | 30 | fn title() -> Cow<'static, str> { 31 | T::title() 32 | } 33 | 34 | fn schema_ref( 35 | schemas: &mut BTreeMap, 36 | schemas_in_progress: &mut Vec, 37 | ) -> ReferenceOr { 38 | T::schema_ref(schemas, schemas_in_progress) 39 | } 40 | 41 | fn schema_ref_box( 42 | schemas: &mut BTreeMap, 43 | schemas_in_progress: &mut Vec, 44 | ) -> ReferenceOr> { 45 | T::schema_ref_box(schemas, schemas_in_progress) 46 | } 47 | 48 | fn schema( 49 | schemas: &mut BTreeMap, 50 | schemas_in_progress: &mut Vec, 51 | ) -> Schema { 52 | T::schema(schemas, schemas_in_progress) 53 | } 54 | } 55 | 56 | impl ParseField for JsonField { 57 | type Holder = Result; 58 | 59 | fn default_holder(name: &'static str) -> Self::Holder { 60 | MissingFieldSnafu { name }.fail() 61 | } 62 | 63 | async fn parse_field( 64 | holder: Self::Holder, 65 | field: Field<'static>, 66 | name: &'static str, 67 | ) -> Result { 68 | if holder.is_ok() { 69 | return DuplicateFieldSnafu { name }.fail(); 70 | } 71 | 72 | let bytes = ::parse_field( 73 | ::default_holder(name), 74 | field, 75 | name, 76 | ) 77 | .await??; 78 | 79 | let f = crate::util::deserialize_json(&bytes).context(InvalidJsonFieldSnafu { name })?; 80 | Ok(Ok(JsonField(f))) 81 | } 82 | 83 | fn extract(holder: Self::Holder, _: &'static str) -> Result { 84 | holder 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /predawn/src/extract/multipart/mod.rs: -------------------------------------------------------------------------------- 1 | mod extract; 2 | mod json_field; 3 | mod parse_field; 4 | mod upload; 5 | 6 | #[cfg_attr(docsrs, doc(cfg(feature = "macro")))] 7 | #[cfg(feature = "macro")] 8 | pub use predawn_macro::Multipart; 9 | 10 | #[doc(hidden)] 11 | pub use self::extract::Multipart; 12 | pub use self::{json_field::JsonField, parse_field::ParseField, upload::Upload}; 13 | -------------------------------------------------------------------------------- /predawn/src/extract/multipart/upload.rs: -------------------------------------------------------------------------------- 1 | use std::{borrow::Cow, collections::BTreeMap}; 2 | 3 | use bytes::Bytes; 4 | use multer::Field; 5 | use predawn_core::openapi::Schema; 6 | use predawn_schema::ToSchema; 7 | use snafu::OptionExt; 8 | 9 | use super::ParseField; 10 | use crate::response_error::{ 11 | DuplicateFieldSnafu, MissingContentTypeSnafu, MissingFieldSnafu, MissingFileNameSnafu, 12 | MultipartError, 13 | }; 14 | 15 | #[derive(Debug)] 16 | pub struct Upload { 17 | field_name: &'static str, 18 | file_name: Box, 19 | content_type: Box, 20 | bytes: Bytes, 21 | } 22 | 23 | impl Upload { 24 | /// Return the name of the parameter in the multipart form. 25 | #[inline] 26 | pub fn field_name(&self) -> &'static str { 27 | self.field_name 28 | } 29 | 30 | /// Return the file name in the client's filesystem. 31 | #[inline] 32 | pub fn file_name(&self) -> &str { 33 | &self.file_name 34 | } 35 | 36 | /// Return the content type of the file. 37 | #[inline] 38 | pub fn content_type(&self) -> &str { 39 | &self.content_type 40 | } 41 | 42 | #[inline] 43 | pub fn bytes(&self) -> &Bytes { 44 | &self.bytes 45 | } 46 | 47 | #[inline] 48 | pub fn into_bytes(self) -> Bytes { 49 | self.bytes 50 | } 51 | } 52 | 53 | impl ToSchema for Upload { 54 | fn title() -> Cow<'static, str> { 55 | "Upload".into() 56 | } 57 | 58 | fn schema(_: &mut BTreeMap, _: &mut Vec) -> Schema { 59 | crate::util::binary_schema(Self::title()) 60 | } 61 | } 62 | 63 | impl ParseField for Upload { 64 | type Holder = Result; 65 | 66 | fn default_holder(name: &'static str) -> Self::Holder { 67 | MissingFieldSnafu { name }.fail() 68 | } 69 | 70 | async fn parse_field( 71 | holder: Self::Holder, 72 | field: Field<'static>, 73 | name: &'static str, 74 | ) -> Result { 75 | if holder.is_ok() { 76 | return DuplicateFieldSnafu { name }.fail(); 77 | } 78 | 79 | let file_name = field 80 | .file_name() 81 | .context(MissingFileNameSnafu { name })? 82 | .into(); 83 | 84 | let content_type = field 85 | .content_type() 86 | .context(MissingContentTypeSnafu { name })? 87 | .as_ref() 88 | .into(); 89 | 90 | let bytes = ::parse_field( 91 | ::default_holder(name), 92 | field, 93 | name, 94 | ) 95 | .await??; 96 | 97 | Ok(Ok(Upload { 98 | field_name: name, 99 | file_name, 100 | content_type, 101 | bytes, 102 | })) 103 | } 104 | 105 | fn extract(holder: Self::Holder, _: &'static str) -> Result { 106 | holder 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /predawn/src/extract/path/mod.rs: -------------------------------------------------------------------------------- 1 | mod de; 2 | 3 | use std::collections::BTreeMap; 4 | 5 | use predawn_core::{ 6 | api_request::ApiRequestHead, 7 | from_request::FromRequestHead, 8 | impl_deref, 9 | openapi::{Parameter, Schema}, 10 | request::Head, 11 | }; 12 | use serde::de::DeserializeOwned; 13 | use snafu::{IntoError, ResultExt}; 14 | 15 | use self::de::PathDeserializer; 16 | use crate::{ 17 | ToParameters, 18 | path_params::PathParams, 19 | response_error::{PathError, path_error}, 20 | }; 21 | 22 | #[derive(Debug)] 23 | pub struct Path(pub T); 24 | 25 | impl_deref!(Path); 26 | 27 | impl FromRequestHead for Path 28 | where 29 | T: DeserializeOwned, 30 | { 31 | type Error = PathError; 32 | 33 | async fn from_request_head(head: &mut Head) -> Result { 34 | let params = match head.extensions.get::() { 35 | Some(PathParams::Ok(params)) => params, 36 | Some(PathParams::Err(e)) => { 37 | return Err(path_error::InvalidUtf8InPathParamsSnafu.into_error(e.clone())); 38 | } 39 | None => return Err(path_error::MissingPathParamsSnafu.build()), 40 | }; 41 | 42 | let deserializer = PathDeserializer::new(params); 43 | 44 | let path = T::deserialize(deserializer).context(path_error::DeserializePathSnafu)?; 45 | Ok(Path(path)) 46 | } 47 | } 48 | 49 | impl ApiRequestHead for Path { 50 | fn parameters( 51 | schemas: &mut BTreeMap, 52 | schemas_in_progress: &mut Vec, 53 | ) -> Option> { 54 | Some( 55 | ::parameters(schemas, schemas_in_progress) 56 | .into_iter() 57 | .map(|parameter_data| Parameter::Path { 58 | parameter_data, 59 | style: Default::default(), 60 | }) 61 | .collect(), 62 | ) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /predawn/src/extract/query.rs: -------------------------------------------------------------------------------- 1 | use std::collections::BTreeMap; 2 | 3 | use predawn_core::{ 4 | api_request::ApiRequestHead, 5 | from_request::FromRequestHead, 6 | impl_deref, 7 | openapi::{Parameter, Schema}, 8 | request::Head, 9 | }; 10 | use serde::de::DeserializeOwned; 11 | use snafu::ResultExt; 12 | 13 | use crate::{ 14 | ToParameters, 15 | response_error::{QueryError, QuerySnafu}, 16 | }; 17 | 18 | #[derive(Debug, Clone, Copy, Default)] 19 | pub struct Query(pub T); 20 | 21 | impl_deref!(Query); 22 | 23 | impl FromRequestHead for Query 24 | where 25 | T: DeserializeOwned, 26 | { 27 | type Error = QueryError; 28 | 29 | async fn from_request_head(head: &mut Head) -> Result { 30 | let bytes = head.uri.query().unwrap_or_default().as_bytes(); 31 | let query = crate::util::deserialize_form(bytes).context(QuerySnafu)?; 32 | Ok(Query(query)) 33 | } 34 | } 35 | 36 | impl ApiRequestHead for Query { 37 | fn parameters( 38 | schemas: &mut BTreeMap, 39 | schemas_in_progress: &mut Vec, 40 | ) -> Option> { 41 | Some( 42 | ::parameters(schemas, schemas_in_progress) 43 | .into_iter() 44 | .map(|parameter_data| Parameter::Query { 45 | parameter_data, 46 | allow_reserved: Default::default(), 47 | style: Default::default(), 48 | allow_empty_value: Default::default(), 49 | }) 50 | .collect(), 51 | ) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /predawn/src/extract/typed_header.rs: -------------------------------------------------------------------------------- 1 | use std::collections::BTreeMap; 2 | 3 | use headers::Header; 4 | use predawn_core::{ 5 | api_request::ApiRequestHead, 6 | from_request::{FromRequestHead, OptionalFromRequestHead}, 7 | impl_deref, 8 | openapi::{Parameter, Schema}, 9 | request::Head, 10 | }; 11 | use snafu::IntoError; 12 | 13 | use crate::response_error::{DecodeSnafu, MissingSnafu, TypedHeaderError}; 14 | 15 | #[derive(Debug, Clone, Copy, Default)] 16 | pub struct TypedHeader(pub T); 17 | 18 | impl_deref!(TypedHeader); 19 | 20 | impl FromRequestHead for TypedHeader 21 | where 22 | T: Header, 23 | { 24 | type Error = TypedHeaderError; 25 | 26 | async fn from_request_head(head: &mut Head) -> Result { 27 | let name = T::name(); 28 | 29 | let mut values = head.headers.get_all(name).iter(); 30 | let is_missing = values.size_hint() == (0, Some(0)); 31 | 32 | match T::decode(&mut values) { 33 | Ok(o) => Ok(Self(o)), 34 | Err(e) => Err(if is_missing { 35 | MissingSnafu { name }.build() 36 | } else { 37 | DecodeSnafu { name }.into_error(e) 38 | }), 39 | } 40 | } 41 | } 42 | 43 | impl OptionalFromRequestHead for TypedHeader 44 | where 45 | T: Header, 46 | { 47 | type Error = TypedHeaderError; 48 | 49 | async fn from_request_head(head: &mut Head) -> Result, Self::Error> { 50 | let name = T::name(); 51 | 52 | let mut values = head.headers.get_all(name).iter(); 53 | let is_missing = values.size_hint() == (0, Some(0)); 54 | 55 | match T::decode(&mut values) { 56 | Ok(o) => Ok(Some(Self(o))), 57 | Err(_) if is_missing => Ok(None), 58 | Err(e) => Err(DecodeSnafu { name }.into_error(e)), 59 | } 60 | } 61 | } 62 | 63 | impl ApiRequestHead for TypedHeader { 64 | fn parameters(_: &mut BTreeMap, _: &mut Vec) -> Option> { 65 | None 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /predawn/src/extract/websocket/mod.rs: -------------------------------------------------------------------------------- 1 | mod request; 2 | mod response; 3 | mod socket; 4 | 5 | pub use tokio_tungstenite::tungstenite::protocol::{ 6 | Message, 7 | frame::{CloseFrame, Frame, Utf8Bytes, coding::CloseCode}, 8 | }; 9 | 10 | pub use self::{ 11 | request::{DefaultOnFailedUpgrade, OnFailedUpgrade, WebSocketRequest}, 12 | response::WebSocketResponse, 13 | socket::WebSocket, 14 | }; 15 | -------------------------------------------------------------------------------- /predawn/src/extract/websocket/response.rs: -------------------------------------------------------------------------------- 1 | use std::{collections::BTreeMap, convert::Infallible}; 2 | 3 | use headers::{Connection, HeaderMapExt, SecWebsocketAccept, Upgrade}; 4 | use http::{StatusCode, header::SEC_WEBSOCKET_PROTOCOL}; 5 | use hyper_util::rt::TokioIo; 6 | use predawn_core::{ 7 | api_response::ApiResponse, 8 | body::ResponseBody, 9 | into_response::IntoResponse, 10 | openapi::{self, Schema}, 11 | response::{MultiResponse, Response, SingleResponse}, 12 | }; 13 | use tokio_tungstenite::{WebSocketStream, tungstenite::protocol::Role}; 14 | 15 | use super::{OnFailedUpgrade, WebSocket, WebSocketRequest}; 16 | 17 | #[derive(Debug)] 18 | pub struct WebSocketResponse(Response); 19 | 20 | impl WebSocketResponse { 21 | pub(crate) fn new(request: WebSocketRequest, callback: C) -> WebSocketResponse 22 | where 23 | F: OnFailedUpgrade, 24 | C: FnOnce(WebSocket) -> Fut + Send + 'static, 25 | Fut: Future + Send + 'static, 26 | { 27 | let WebSocketRequest { 28 | config, 29 | protocol, 30 | sec_websocket_key, 31 | on_upgrade, 32 | on_failed_upgrade, 33 | sec_websocket_protocol: _, 34 | } = request; 35 | 36 | { 37 | let protocol = protocol.clone(); 38 | 39 | tokio::spawn(async move { 40 | let upgraded = match on_upgrade.await { 41 | Ok(upgraded) => upgraded, 42 | Err(err) => { 43 | on_failed_upgrade.call(err); 44 | return; 45 | } 46 | }; 47 | let upgraded = TokioIo::new(upgraded); 48 | 49 | let socket = 50 | WebSocketStream::from_raw_socket(upgraded, Role::Server, Some(config)).await; 51 | let socket = WebSocket { 52 | inner: socket, 53 | protocol, 54 | }; 55 | 56 | callback(socket).await; 57 | }); 58 | } 59 | 60 | let response = match sec_websocket_key { 61 | Some(sec_websocket_key) => { 62 | let mut response = http::Response::builder() 63 | .status(StatusCode::SWITCHING_PROTOCOLS) 64 | .body(ResponseBody::empty()) 65 | .unwrap(); 66 | 67 | let headers = response.headers_mut(); 68 | 69 | headers.typed_insert(Connection::upgrade()); 70 | headers.typed_insert(Upgrade::websocket()); 71 | headers.typed_insert(SecWebsocketAccept::from(sec_websocket_key)); 72 | 73 | if let Some(protocol) = protocol { 74 | headers.insert(SEC_WEBSOCKET_PROTOCOL, protocol); 75 | } 76 | 77 | response 78 | } 79 | None => { 80 | // Otherwise, we are HTTP/2+. As established in RFC 9113 section 8.5, we just respond 81 | // with a 2XX with an empty body: 82 | // . 83 | http::Response::new(ResponseBody::empty()) 84 | } 85 | }; 86 | 87 | WebSocketResponse(response) 88 | } 89 | } 90 | 91 | impl IntoResponse for WebSocketResponse { 92 | type Error = Infallible; 93 | 94 | fn into_response(self) -> Result { 95 | Ok(self.0) 96 | } 97 | } 98 | 99 | impl SingleResponse for WebSocketResponse { 100 | const STATUS_CODE: u16 = 101; 101 | 102 | fn response(_: &mut BTreeMap, _: &mut Vec) -> openapi::Response { 103 | openapi::Response { 104 | description: "A WebSocket response".to_string(), 105 | ..Default::default() 106 | } 107 | } 108 | } 109 | 110 | impl ApiResponse for WebSocketResponse { 111 | fn responses( 112 | schemas: &mut BTreeMap, 113 | schemas_in_progress: &mut Vec, 114 | ) -> Option> { 115 | Some(::responses( 116 | schemas, 117 | schemas_in_progress, 118 | )) 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /predawn/src/extract/websocket/socket.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | pin::Pin, 3 | task::{Context, Poll}, 4 | }; 5 | 6 | use futures_core::Stream; 7 | use futures_util::{SinkExt, StreamExt, sink::Sink}; 8 | use http::HeaderValue; 9 | use hyper::upgrade::Upgraded; 10 | use hyper_util::rt::TokioIo; 11 | use tokio_tungstenite::{ 12 | WebSocketStream, 13 | tungstenite::{self, Message, protocol::CloseFrame}, 14 | }; 15 | 16 | #[derive(Debug)] 17 | pub struct WebSocket { 18 | pub(crate) inner: WebSocketStream>, 19 | pub(crate) protocol: Option, 20 | } 21 | 22 | impl WebSocket { 23 | /// Receive another message. 24 | /// 25 | /// Returns `None` if the stream has closed. 26 | #[inline(always)] 27 | pub async fn recv(&mut self) -> Option> { 28 | self.inner.next().await 29 | } 30 | 31 | /// Send a message. 32 | #[inline(always)] 33 | pub async fn send(&mut self, msg: Message) -> Result<(), tungstenite::Error> { 34 | self.inner.send(msg).await 35 | } 36 | 37 | /// Gracefully close this WebSocket. 38 | #[inline(always)] 39 | pub async fn close(&mut self, msg: Option) -> Result<(), tungstenite::Error> { 40 | self.inner.close(msg).await 41 | } 42 | 43 | /// Return the selected WebSocket subprotocol, if one has been chosen. 44 | #[inline(always)] 45 | pub fn protocol(&self) -> Option<&HeaderValue> { 46 | self.protocol.as_ref() 47 | } 48 | } 49 | 50 | impl Stream for WebSocket { 51 | type Item = Result; 52 | 53 | #[inline(always)] 54 | fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { 55 | Pin::new(&mut self.inner).poll_next(cx) 56 | } 57 | } 58 | 59 | impl Sink for WebSocket { 60 | type Error = tungstenite::Error; 61 | 62 | #[inline(always)] 63 | fn poll_ready(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { 64 | Pin::new(&mut self.inner).poll_ready(cx) 65 | } 66 | 67 | #[inline(always)] 68 | fn start_send(mut self: Pin<&mut Self>, item: Message) -> Result<(), Self::Error> { 69 | Pin::new(&mut self.inner).start_send(item) 70 | } 71 | 72 | #[inline(always)] 73 | fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { 74 | Pin::new(&mut self.inner).poll_flush(cx) 75 | } 76 | 77 | #[inline(always)] 78 | fn poll_close(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { 79 | Pin::new(&mut self.inner).poll_close(cx) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /predawn/src/handler/after.rs: -------------------------------------------------------------------------------- 1 | use predawn_core::{ 2 | error::Error, into_response::IntoResponse, request::Request, response::Response, 3 | }; 4 | 5 | use crate::handler::Handler; 6 | 7 | pub struct After { 8 | pub(crate) inner: H, 9 | pub(crate) f: F, 10 | } 11 | 12 | impl Handler for After 13 | where 14 | H: Handler, 15 | F: Fn(Result) -> Fut + Send + Sync + 'static, 16 | Fut: Future> + Send, 17 | R: IntoResponse, 18 | { 19 | async fn call(&self, req: Request) -> Result { 20 | let result = self.inner.call(req).await; 21 | match (self.f)(result).await { 22 | Ok(r) => r.into_response().map_err(Into::into), 23 | Err(e) => Err(e), 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /predawn/src/handler/around.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use predawn_core::{ 4 | error::Error, into_response::IntoResponse, request::Request, response::Response, 5 | }; 6 | 7 | use crate::handler::Handler; 8 | 9 | pub struct Around { 10 | pub(crate) inner: Arc, 11 | pub(crate) f: F, 12 | } 13 | 14 | impl Handler for Around 15 | where 16 | H: Handler, 17 | F: Fn(Arc, Request) -> Fut + Send + Sync + 'static, 18 | Fut: Future> + Send, 19 | R: IntoResponse, 20 | { 21 | async fn call(&self, req: Request) -> Result { 22 | match (self.f)(self.inner.clone(), req).await { 23 | Ok(r) => r.into_response().map_err(Into::into), 24 | Err(e) => Err(e), 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /predawn/src/handler/before.rs: -------------------------------------------------------------------------------- 1 | use predawn_core::{error::Error, request::Request, response::Response}; 2 | 3 | use crate::handler::Handler; 4 | 5 | pub struct Before { 6 | pub(crate) inner: H, 7 | pub(crate) f: F, 8 | } 9 | 10 | impl Handler for Before 11 | where 12 | H: Handler, 13 | F: Fn(Request) -> Fut + Send + Sync + 'static, 14 | Fut: Future> + Send, 15 | { 16 | async fn call(&self, req: Request) -> Result { 17 | let req = (self.f)(req).await?; 18 | self.inner.call(req).await 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /predawn/src/handler/catch_all_error.rs: -------------------------------------------------------------------------------- 1 | use predawn_core::{ 2 | error::Error, into_response::IntoResponse, request::Request, response::Response, 3 | }; 4 | 5 | use crate::handler::Handler; 6 | 7 | pub struct CatchAllError { 8 | pub(crate) inner: H, 9 | pub(crate) f: F, 10 | } 11 | 12 | impl Handler for CatchAllError 13 | where 14 | H: Handler, 15 | F: Fn(Error) -> Fut + Send + Sync + 'static, 16 | Fut: Future + Send, 17 | R: IntoResponse, 18 | { 19 | async fn call(&self, req: Request) -> Result { 20 | match self.inner.call(req).await { 21 | Ok(response) => Ok(response), 22 | Err(e) => (self.f)(e).await.into_response().map_err(Into::into), 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /predawn/src/handler/catch_error.rs: -------------------------------------------------------------------------------- 1 | use std::marker::PhantomData; 2 | 3 | use predawn_core::{ 4 | error::Error, into_response::IntoResponse, request::Request, response::Response, 5 | }; 6 | 7 | use crate::handler::Handler; 8 | 9 | pub struct CatchError { 10 | pub(crate) inner: H, 11 | pub(crate) f: F, 12 | pub(crate) _marker: PhantomData, 13 | } 14 | 15 | impl Handler for CatchError 16 | where 17 | H: Handler, 18 | F: Fn(Err, Box<[Box]>) -> Fut + Send + Sync + 'static, 19 | Err: std::error::Error + Send + Sync + 'static, 20 | Fut: Future + Send, 21 | R: IntoResponse, 22 | { 23 | async fn call(&self, req: Request) -> Result { 24 | match self.inner.call(req).await { 25 | Ok(response) => Ok(response), 26 | Err(e) => match e.downcast::() { 27 | Ok((_, e, error_stack)) => (self.f)(e, error_stack) 28 | .await 29 | .into_response() 30 | .map_err(Into::into), 31 | Err(e) => Err(e), 32 | }, 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /predawn/src/handler/inspect_all_error.rs: -------------------------------------------------------------------------------- 1 | use predawn_core::{error::Error, request::Request, response::Response}; 2 | 3 | use crate::handler::Handler; 4 | 5 | pub struct InspectAllError { 6 | pub(crate) inner: H, 7 | pub(crate) f: F, 8 | } 9 | 10 | impl Handler for InspectAllError 11 | where 12 | H: Handler, 13 | F: Fn(&Error) + Send + Sync + 'static, 14 | { 15 | async fn call(&self, req: Request) -> Result { 16 | self.inner.call(req).await.inspect_err(|e| { 17 | (self.f)(e); 18 | }) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /predawn/src/handler/inspect_error.rs: -------------------------------------------------------------------------------- 1 | use std::marker::PhantomData; 2 | 3 | use predawn_core::{error::Error, request::Request, response::Response}; 4 | 5 | use crate::handler::Handler; 6 | 7 | pub struct InspectError { 8 | pub(crate) inner: H, 9 | pub(crate) f: F, 10 | pub(crate) _marker: PhantomData, 11 | } 12 | 13 | impl Handler for InspectError 14 | where 15 | H: Handler, 16 | F: Fn(&Err, &[Box]) + Send + Sync + 'static, 17 | Err: std::error::Error + Send + Sync + 'static, 18 | { 19 | async fn call(&self, req: Request) -> Result { 20 | self.inner.call(req).await.inspect_err(|e| { 21 | if let Some(err) = e.downcast_ref::() { 22 | (self.f)(err, e.error_stack()); 23 | } 24 | }) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /predawn/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![cfg_attr(docsrs, feature(doc_cfg))] 2 | 3 | extern crate self as predawn; 4 | 5 | pub mod any_map; 6 | pub mod app; 7 | pub mod config; 8 | #[doc(hidden)] 9 | pub mod controller; 10 | pub mod environment; 11 | pub mod extract; 12 | pub mod handler; 13 | mod macros; 14 | pub mod media_type; 15 | pub mod middleware; 16 | pub mod normalized_path; 17 | pub mod openapi; 18 | mod path_params; 19 | pub mod payload; 20 | pub mod plugin; 21 | pub mod response; 22 | pub mod response_error; 23 | pub mod route; 24 | pub mod server; 25 | pub mod test_client; 26 | mod traits; 27 | pub(crate) mod util; 28 | pub use error2; 29 | pub use http; 30 | pub use predawn_core::{ 31 | api_request, api_response, body, either, error, from_request, into_response, 32 | media_type::{MultiRequestMediaType, MultiResponseMediaType}, 33 | request, 34 | response::{MultiResponse, SingleResponse}, 35 | }; 36 | #[cfg_attr(docsrs, doc(cfg(feature = "macro")))] 37 | #[cfg(feature = "macro")] 38 | pub use predawn_macro::{ 39 | MultiRequestMediaType, MultiResponse, MultiResponseMediaType, SecurityScheme, SingleResponse, 40 | Tag, ToParameters, controller, 41 | }; 42 | #[cfg_attr(docsrs, doc(cfg(feature = "schemars")))] 43 | #[cfg(feature = "schemars")] 44 | pub use predawn_schema::schemars_transform; 45 | pub use predawn_schema::to_schema::ToSchema; 46 | #[cfg_attr(docsrs, doc(cfg(feature = "macro")))] 47 | #[cfg(feature = "macro")] 48 | pub use predawn_schema_macro::ToSchema; 49 | 50 | pub use self::traits::{SecurityScheme, Tag, ToParameters}; 51 | 52 | #[doc(hidden)] 53 | pub mod __internal { 54 | pub use indexmap; 55 | pub use paste; 56 | pub use rudi; 57 | pub use serde_json; 58 | } 59 | -------------------------------------------------------------------------------- /predawn/src/media_type.rs: -------------------------------------------------------------------------------- 1 | pub use predawn_core::media_type::{ 2 | MediaType, RequestMediaType, ResponseMediaType, SingleMediaType, assert_response_media_type, 3 | has_media_type, 4 | }; 5 | -------------------------------------------------------------------------------- /predawn/src/middleware/limit.rs: -------------------------------------------------------------------------------- 1 | use predawn_core::{ 2 | error::Error, 3 | request::{BodyLimit, Request}, 4 | response::Response, 5 | }; 6 | 7 | use super::Middleware; 8 | use crate::{handler::Handler, response_error::RequestBodyLimitSnafu}; 9 | 10 | pub struct RequestBodyLimit { 11 | limit: usize, 12 | } 13 | 14 | impl RequestBodyLimit { 15 | pub fn new(limit: usize) -> Self { 16 | Self { limit } 17 | } 18 | } 19 | 20 | impl Middleware for RequestBodyLimit { 21 | type Output = RequestBodyLimitHandler; 22 | 23 | fn transform(self, input: H) -> Self::Output { 24 | RequestBodyLimitHandler { 25 | limit: self.limit, 26 | inner: input, 27 | } 28 | } 29 | } 30 | 31 | pub struct RequestBodyLimitHandler { 32 | limit: usize, 33 | inner: H, 34 | } 35 | 36 | impl Handler for RequestBodyLimitHandler { 37 | async fn call(&self, mut req: Request) -> Result { 38 | let content_length = req.head.content_length(); 39 | let limit = self.limit; 40 | 41 | let limit = match content_length { 42 | Some(content_length) if content_length > limit => { 43 | return Err(RequestBodyLimitSnafu { 44 | content_length, 45 | limit, 46 | } 47 | .build() 48 | .into()); 49 | } 50 | Some(content_length) => content_length, 51 | None => limit, 52 | }; 53 | 54 | *req.body_limit() = BodyLimit(limit); 55 | 56 | self.inner.call(req).await 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /predawn/src/middleware/mod.rs: -------------------------------------------------------------------------------- 1 | mod limit; 2 | #[cfg_attr(docsrs, doc(cfg(feature = "tower-compat")))] 3 | #[cfg(feature = "tower-compat")] 4 | mod tower_compat; 5 | mod tracing; 6 | 7 | #[cfg_attr(docsrs, doc(cfg(feature = "tower-compat")))] 8 | #[cfg(feature = "tower-compat")] 9 | pub use self::tower_compat::TowerLayerCompatExt; 10 | pub use self::{ 11 | limit::{RequestBodyLimit, RequestBodyLimitHandler}, 12 | tracing::{Tracing, TracingHandler}, 13 | }; 14 | use crate::handler::Handler; 15 | 16 | pub trait Middleware { 17 | type Output: Handler; 18 | 19 | fn transform(self, input: H) -> Self::Output; 20 | } 21 | -------------------------------------------------------------------------------- /predawn/src/middleware/tower_compat.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | future::poll_fn, 3 | sync::{Arc, Mutex}, 4 | task::{Context, Poll}, 5 | }; 6 | 7 | use futures_core::future::BoxFuture; 8 | use futures_util::FutureExt; 9 | use hyper::body::Incoming; 10 | use predawn_core::{ 11 | error::Error, into_response::IntoResponse, request::Request, response::Response, 12 | }; 13 | use tower::{Layer, Service}; 14 | 15 | use super::Middleware; 16 | use crate::handler::Handler; 17 | 18 | pub trait TowerLayerCompatExt: Sized { 19 | fn compat(self) -> TowerLayerCompatMiddleware { 20 | TowerLayerCompatMiddleware(self) 21 | } 22 | } 23 | 24 | impl TowerLayerCompatExt for L {} 25 | 26 | pub struct TowerLayerCompatMiddleware(L); 27 | 28 | impl Middleware for TowerLayerCompatMiddleware 29 | where 30 | H: Handler, 31 | L: Layer>, 32 | L::Service: Service> + Send + Sync + 'static, 33 | >>::Future: Send, 34 | >>::Response: IntoResponse, 35 | >>::Error: Into, 36 | { 37 | type Output = ServiceToHandler; 38 | 39 | fn transform(self, input: H) -> Self::Output { 40 | let svc = self.0.layer(HandlerToService(Arc::new(input))); 41 | ServiceToHandler(Mutex::new(svc)) 42 | } 43 | } 44 | 45 | pub struct HandlerToService(Arc); 46 | 47 | impl Service> for HandlerToService 48 | where 49 | H: Handler, 50 | { 51 | type Error = Error; 52 | type Future = BoxFuture<'static, Result>; 53 | type Response = Response; 54 | 55 | fn poll_ready(&mut self, _: &mut Context<'_>) -> Poll> { 56 | Poll::Ready(Ok(())) 57 | } 58 | 59 | fn call(&mut self, req: http::Request) -> Self::Future { 60 | let handler = self.0.clone(); 61 | 62 | let req = Request::try_from(req).expect("not found some element in request extensions"); 63 | 64 | async move { handler.call(req).await }.boxed() 65 | } 66 | } 67 | 68 | pub struct ServiceToHandler(Mutex); 69 | 70 | impl Handler for ServiceToHandler 71 | where 72 | S: Service> + Send + Sync + 'static, 73 | S::Response: IntoResponse, 74 | S::Error: Into, 75 | S::Future: Send, 76 | { 77 | async fn call(&self, req: Request) -> Result { 78 | let svc = &self.0; 79 | 80 | poll_fn(|cx| svc.lock().unwrap().poll_ready(cx)) 81 | .await 82 | .map_err(|e| e.into())?; 83 | 84 | let fut = svc 85 | .lock() 86 | .unwrap() 87 | .call(http::Request::::from(req)); 88 | 89 | Ok(fut.await.map_err(|e| e.into())?.into_response()?) 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /predawn/src/middleware/tracing.rs: -------------------------------------------------------------------------------- 1 | use std::time::Instant; 2 | 3 | use predawn_core::{error::Error, request::Request, response::Response}; 4 | use tracing::Instrument; 5 | 6 | use super::Middleware; 7 | use crate::handler::Handler; 8 | 9 | #[derive(Clone, Copy)] 10 | pub struct Tracing; 11 | 12 | impl Middleware for Tracing { 13 | type Output = TracingHandler; 14 | 15 | fn transform(self, input: H) -> Self::Output { 16 | TracingHandler { inner: input } 17 | } 18 | } 19 | 20 | pub struct TracingHandler { 21 | inner: H, 22 | } 23 | 24 | impl Handler for TracingHandler { 25 | async fn call(&self, req: Request) -> Result { 26 | let head = &req.head; 27 | 28 | let span = ::tracing::info_span!( 29 | target: module_path!(), 30 | "request", 31 | remote_addr = %head.remote_addr(), 32 | version = ?head.version, 33 | method = %head.method, 34 | uri = %head.original_uri(), 35 | ); 36 | 37 | async move { 38 | let now = Instant::now(); 39 | let result = self.inner.call(req).await; 40 | let duration = now.elapsed(); 41 | 42 | match &result { 43 | Ok(response) => { 44 | ::tracing::info!( 45 | status = %response.status(), 46 | duration = ?duration, 47 | "response" 48 | ) 49 | } 50 | Err(error) => { 51 | ::tracing::info!( 52 | status = %error.status(), 53 | duration = ?duration, 54 | error = %error, 55 | "error" 56 | ) 57 | } 58 | }; 59 | 60 | result 61 | } 62 | .instrument(span) 63 | .await 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /predawn/src/normalized_path.rs: -------------------------------------------------------------------------------- 1 | use std::{fmt, ops::Deref}; 2 | 3 | use serde::{Deserialize, Serialize}; 4 | 5 | #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash, Default)] 6 | #[serde(into = "String", from = "&str")] 7 | pub struct NormalizedPath(String); 8 | 9 | impl NormalizedPath { 10 | pub fn new(path: &str) -> Self { 11 | if path.is_empty() || path == "/" { 12 | return Self("/".to_string()); 13 | } 14 | 15 | let segments = path.split('/'); 16 | 17 | let mut path = String::new(); 18 | 19 | for segment in segments { 20 | if segment.is_empty() { 21 | continue; 22 | } 23 | 24 | let segment = segment.trim(); 25 | 26 | if segment.is_empty() { 27 | continue; 28 | } 29 | 30 | path.push('/'); 31 | path.push_str(segment); 32 | } 33 | 34 | if path.is_empty() { 35 | path.push('/'); 36 | } 37 | 38 | Self(path) 39 | } 40 | 41 | pub fn join(self, path: Self) -> Self { 42 | let prefix = self; 43 | let postfix = path; 44 | 45 | if prefix == "/" { 46 | postfix 47 | } else if postfix == "/" { 48 | prefix 49 | } else { 50 | let mut path = prefix.0; 51 | path.push_str(&postfix.0); 52 | 53 | Self(path) 54 | } 55 | } 56 | 57 | pub fn into_inner(self) -> String { 58 | self.0 59 | } 60 | } 61 | 62 | impl fmt::Display for NormalizedPath { 63 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 64 | fmt::Display::fmt(&self.0, f) 65 | } 66 | } 67 | 68 | impl Deref for NormalizedPath { 69 | type Target = String; 70 | 71 | fn deref(&self) -> &Self::Target { 72 | &self.0 73 | } 74 | } 75 | 76 | impl<'a> From<&'a str> for NormalizedPath { 77 | fn from(path: &'a str) -> Self { 78 | NormalizedPath::new(path) 79 | } 80 | } 81 | 82 | impl From for String { 83 | fn from(path: NormalizedPath) -> Self { 84 | path.0 85 | } 86 | } 87 | 88 | impl PartialEq<&str> for NormalizedPath { 89 | fn eq(&self, other: &&str) -> bool { 90 | self.0 == *other 91 | } 92 | } 93 | 94 | impl PartialEq for &str { 95 | fn eq(&self, other: &NormalizedPath) -> bool { 96 | *self == other.0 97 | } 98 | } 99 | 100 | impl PartialEq for NormalizedPath { 101 | fn eq(&self, other: &String) -> bool { 102 | self.0 == *other 103 | } 104 | } 105 | 106 | impl PartialEq for String { 107 | fn eq(&self, other: &NormalizedPath) -> bool { 108 | *self == other.0 109 | } 110 | } 111 | 112 | #[cfg(test)] 113 | mod tests { 114 | use super::*; 115 | 116 | #[test] 117 | fn test_normalize_path() { 118 | assert_eq!(NormalizedPath::new(""), "/"); 119 | assert_eq!(NormalizedPath::new("/"), "/"); 120 | assert_eq!(NormalizedPath::new("//"), "/"); 121 | assert_eq!(NormalizedPath::new(" / / "), "/"); 122 | 123 | assert_eq!(NormalizedPath::new("a"), "/a"); 124 | assert_eq!(NormalizedPath::new("/a"), "/a"); 125 | assert_eq!(NormalizedPath::new("a/"), "/a"); 126 | assert_eq!(NormalizedPath::new("/a/"), "/a"); 127 | assert_eq!(NormalizedPath::new("//a//"), "/a"); 128 | assert_eq!(NormalizedPath::new(" // a/ /"), "/a"); 129 | 130 | assert_eq!(NormalizedPath::new("a/b"), "/a/b"); 131 | assert_eq!(NormalizedPath::new("/a/b"), "/a/b"); 132 | assert_eq!(NormalizedPath::new("a/b/"), "/a/b"); 133 | assert_eq!(NormalizedPath::new("/a/b/"), "/a/b"); 134 | assert_eq!(NormalizedPath::new("//a//b//"), "/a/b"); 135 | assert_eq!(NormalizedPath::new(" / /a // b/ / "), "/a/b"); 136 | 137 | assert_eq!( 138 | NormalizedPath::new(" / /a // hello world / d / "), 139 | "/a/hello world/d" 140 | ); 141 | } 142 | 143 | #[test] 144 | fn test_join_path() { 145 | fn join<'a>(prefix: &'a str, postfix: &'a str) -> NormalizedPath { 146 | NormalizedPath::new(prefix).join(NormalizedPath::new(postfix)) 147 | } 148 | 149 | assert_eq!(join("", ""), "/"); 150 | assert_eq!(join("/", ""), "/"); 151 | assert_eq!(join("", "/"), "/"); 152 | assert_eq!(join("/", "/"), "/"); 153 | 154 | assert_eq!(join("/a", ""), "/a"); 155 | assert_eq!(join("", "/a"), "/a"); 156 | assert_eq!(join("/a", "/"), "/a"); 157 | assert_eq!(join("/", "/a"), "/a"); 158 | assert_eq!(join("/a/", "/"), "/a"); 159 | assert_eq!(join("/", "/a/"), "/a"); 160 | 161 | assert_eq!(join("/a", "/b"), "/a/b"); 162 | assert_eq!(join("/a/", "/b"), "/a/b"); 163 | assert_eq!(join("/a", "/b/"), "/a/b"); 164 | assert_eq!(join("/a/", "/b/"), "/a/b"); 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /predawn/src/openapi.rs: -------------------------------------------------------------------------------- 1 | use std::collections::BTreeMap; 2 | 3 | use indexmap::IndexMap; 4 | pub use predawn_core::openapi::*; 5 | 6 | #[doc(hidden)] 7 | pub fn transform_parameters(parameters: Vec) -> Vec> { 8 | parameters.into_iter().map(ReferenceOr::Item).collect() 9 | } 10 | 11 | #[doc(hidden)] 12 | pub fn transform_request_body( 13 | request_body: Option, 14 | ) -> Option> { 15 | request_body.map(ReferenceOr::Item) 16 | } 17 | 18 | #[doc(hidden)] 19 | pub fn transform_responses( 20 | responses: BTreeMap, 21 | ) -> IndexMap> { 22 | responses 23 | .into_iter() 24 | .map(|(status, response)| { 25 | ( 26 | StatusCode::Code(status.as_u16()), 27 | ReferenceOr::Item(response), 28 | ) 29 | }) 30 | .collect() 31 | } 32 | -------------------------------------------------------------------------------- /predawn/src/path_params.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use matchit::Params; 4 | 5 | use crate::response_error::{InvalidUtf8InPathParams, InvalidUtf8InPathParamsSnafu}; 6 | 7 | #[derive(Debug, Clone)] 8 | pub(crate) enum PathParams { 9 | Ok(Box<[(Arc, Arc)]>), 10 | Err(InvalidUtf8InPathParams), 11 | } 12 | 13 | impl PathParams { 14 | pub(crate) fn new(params: Params<'_, '_>) -> Self { 15 | let mut ok_params = Vec::with_capacity(params.len()); 16 | let mut err_params = Vec::new(); 17 | 18 | for (k, v) in params.iter() { 19 | let key = Arc::::from(k); 20 | 21 | match percent_encoding::percent_decode(v.as_bytes()).decode_utf8() { 22 | Ok(o) => { 23 | let value = Arc::::from(o); 24 | ok_params.push((key, value)); 25 | } 26 | Err(_) => { 27 | err_params.push(key); 28 | } 29 | } 30 | } 31 | 32 | if err_params.is_empty() { 33 | Self::Ok(ok_params.into()) 34 | } else { 35 | Self::Err(InvalidUtf8InPathParamsSnafu { keys: err_params }.build()) 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /predawn/src/payload/mod.rs: -------------------------------------------------------------------------------- 1 | mod form; 2 | mod json; 3 | 4 | pub use self::{form::Form, json::Json}; 5 | -------------------------------------------------------------------------------- /predawn/src/plugin/mod.rs: -------------------------------------------------------------------------------- 1 | mod openapi_json; 2 | pub mod ui; 3 | 4 | use std::sync::Arc; 5 | 6 | use http::Method; 7 | use indexmap::IndexMap; 8 | use rudi::Context; 9 | 10 | use crate::{handler::DynHandler, normalized_path::NormalizedPath}; 11 | 12 | pub trait Plugin { 13 | fn create_route( 14 | self: Arc, 15 | cx: &mut Context, 16 | ) -> (NormalizedPath, IndexMap); 17 | } 18 | -------------------------------------------------------------------------------- /predawn/src/plugin/openapi_json.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use http::Method; 4 | use indexmap::IndexMap; 5 | use predawn_core::openapi::OpenAPI; 6 | use rudi::{Context, Singleton}; 7 | 8 | use super::Plugin; 9 | use crate::{ 10 | config::openapi::OpenAPIConfig, 11 | handler::{DynHandler, handler_fn}, 12 | normalized_path::NormalizedPath, 13 | payload::Json, 14 | }; 15 | 16 | #[derive(Clone, Copy)] 17 | pub struct OpenAPIJson; 18 | 19 | impl Plugin for OpenAPIJson { 20 | fn create_route( 21 | self: Arc, 22 | cx: &mut Context, 23 | ) -> (NormalizedPath, IndexMap) { 24 | let json_path = cx.resolve::().json_path; 25 | 26 | let api = cx.resolve::(); 27 | 28 | let handler = handler_fn(move |_| { 29 | let api = api.clone(); 30 | async move { Ok(Json(api)) } 31 | }); 32 | let handler = DynHandler::new(handler); 33 | 34 | let mut map = IndexMap::with_capacity(1); 35 | map.insert(Method::GET, handler); 36 | 37 | (json_path, map) 38 | } 39 | } 40 | 41 | #[Singleton] 42 | fn OpenAPIJsonRegister() -> OpenAPIJson { 43 | OpenAPIJson 44 | } 45 | 46 | #[Singleton(name = std::any::type_name::())] 47 | fn OpenAPIJsonToPlugin(json: OpenAPIJson) -> Arc { 48 | Arc::new(json) 49 | } 50 | -------------------------------------------------------------------------------- /predawn/src/plugin/ui/mod.rs: -------------------------------------------------------------------------------- 1 | mod openapi_explorer; 2 | mod rapidoc; 3 | mod redoc; 4 | mod scalar; 5 | mod swagger_ui; 6 | 7 | use http::{HeaderValue, Method, header::CONTENT_TYPE}; 8 | use indexmap::IndexMap; 9 | use mime::TEXT_HTML_UTF_8; 10 | use predawn_core::response::Response; 11 | use rudi::Context; 12 | 13 | pub use self::{rapidoc::RapiDoc, swagger_ui::SwaggerUI}; 14 | use crate::{ 15 | config::{Config, openapi::OpenAPIConfig, server::ServerConfig}, 16 | handler::{DynHandler, handler_fn}, 17 | normalized_path::NormalizedPath, 18 | }; 19 | 20 | pub(crate) fn create_route( 21 | cx: &mut Context, 22 | get_path: F, 23 | html: String, 24 | ) -> (NormalizedPath, IndexMap) 25 | where 26 | F: Fn(OpenAPIConfig) -> NormalizedPath, 27 | { 28 | (get_path(cx.resolve::()), create_map(html)) 29 | } 30 | 31 | pub(crate) fn json_path(cfg: &Config) -> NormalizedPath { 32 | let server_cfg = ServerConfig::new(cfg); 33 | let openapi_cfg = OpenAPIConfig::new(cfg); 34 | 35 | let full_non_application_root_path = server_cfg.full_non_application_root_path(); 36 | let normalized_json_path = openapi_cfg.json_path; 37 | 38 | full_non_application_root_path.join(normalized_json_path) 39 | } 40 | 41 | fn create_map(html: String) -> IndexMap { 42 | let handler = handler_fn(move |_| { 43 | let html = html.clone(); 44 | 45 | async move { 46 | let mut response: Response = Response::new(html.into()); 47 | response.headers_mut().insert( 48 | CONTENT_TYPE, 49 | HeaderValue::from_static(TEXT_HTML_UTF_8.as_ref()), 50 | ); 51 | Ok(response) 52 | } 53 | }); 54 | 55 | let handler = DynHandler::new(handler); 56 | 57 | let mut map = IndexMap::with_capacity(1); 58 | map.insert(Method::GET, handler); 59 | map 60 | } 61 | -------------------------------------------------------------------------------- /predawn/src/plugin/ui/openapi_explorer.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use http::Method; 4 | use indexmap::IndexMap; 5 | use rudi::{Context, Singleton}; 6 | 7 | use crate::{config::Config, handler::DynHandler, normalized_path::NormalizedPath, plugin::Plugin}; 8 | 9 | const TEMPLATE: &str = r###" 10 | 11 | 12 | 13 | 14 | 15 | 16 | {{title}} 17 | 18 | 19 | 20 | 25 | 26 | 27 | 28 | "###; 29 | 30 | #[derive(Clone, Debug)] 31 | pub struct OpenapiExplorer { 32 | description: Box, 33 | title: Box, 34 | js_url: Box, 35 | spec_url: Box, 36 | } 37 | 38 | impl Plugin for OpenapiExplorer { 39 | fn create_route( 40 | self: Arc, 41 | cx: &mut Context, 42 | ) -> (NormalizedPath, IndexMap) { 43 | super::create_route(cx, |c| c.openapi_explorer_path, self.as_html()) 44 | } 45 | } 46 | 47 | fn condition(cx: &Context) -> bool { 48 | !cx.contains_provider::() 49 | } 50 | 51 | #[Singleton(condition = condition)] 52 | fn OpenapiExplorerRegister(#[di(ref)] cfg: &Config) -> OpenapiExplorer { 53 | let json_path = super::json_path(cfg).into_inner(); 54 | OpenapiExplorer::new(json_path) 55 | } 56 | 57 | #[Singleton(name = std::any::type_name::())] 58 | fn OpenapiExplorerToPlugin(openapi_explorer: OpenapiExplorer) -> Arc { 59 | Arc::new(openapi_explorer) 60 | } 61 | 62 | impl OpenapiExplorer { 63 | pub fn new(spec_url: T) -> Self 64 | where 65 | T: Into>, 66 | { 67 | Self { 68 | description: Box::from("Openapi Explorer"), 69 | title: Box::from("Openapi Explorer"), 70 | js_url: Box::from( 71 | "https://unpkg.com/openapi-explorer@0/dist/browser/openapi-explorer.min.js", 72 | ), 73 | spec_url: spec_url.into(), 74 | } 75 | } 76 | 77 | pub fn as_html(&self) -> String { 78 | TEMPLATE 79 | .replacen("{{description}}", &self.description, 1) 80 | .replacen("{{title}}", &self.title, 1) 81 | .replacen("{{js_url}}", &self.js_url, 1) 82 | .replacen("{{spec_url}}", &self.spec_url, 1) 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /predawn/src/plugin/ui/rapidoc.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use http::Method; 4 | use indexmap::IndexMap; 5 | use rudi::{Context, Singleton}; 6 | 7 | use crate::{config::Config, handler::DynHandler, normalized_path::NormalizedPath, plugin::Plugin}; 8 | 9 | const TEMPLATE: &str = r###" 10 | 11 | 12 | 13 | 14 | 15 | 16 | {{title}} 17 | 18 | 19 | 20 | 21 | 22 | 23 | "###; 24 | 25 | #[derive(Clone, Debug)] 26 | pub struct RapiDoc { 27 | description: Box, 28 | title: Box, 29 | js_url: Box, 30 | spec_url: Box, 31 | } 32 | 33 | impl Plugin for RapiDoc { 34 | fn create_route( 35 | self: Arc, 36 | cx: &mut Context, 37 | ) -> (NormalizedPath, IndexMap) { 38 | super::create_route(cx, |c| c.rapidoc_path, self.as_html()) 39 | } 40 | } 41 | 42 | fn condition(cx: &Context) -> bool { 43 | !cx.contains_provider::() 44 | } 45 | 46 | #[Singleton(condition = condition)] 47 | fn RapiDocRegister(#[di(ref)] cfg: &Config) -> RapiDoc { 48 | let json_path = super::json_path(cfg).into_inner(); 49 | RapiDoc::new(json_path) 50 | } 51 | 52 | #[Singleton(name = std::any::type_name::())] 53 | fn RapiDocToPlugin(rapidoc: RapiDoc) -> Arc { 54 | Arc::new(rapidoc) 55 | } 56 | 57 | impl RapiDoc { 58 | pub fn new(spec_url: T) -> Self 59 | where 60 | T: Into>, 61 | { 62 | Self { 63 | description: Box::from("RapiDoc"), 64 | title: Box::from("RapiDoc"), 65 | js_url: Box::from("https://unpkg.com/rapidoc/dist/rapidoc-min.js"), 66 | spec_url: spec_url.into(), 67 | } 68 | } 69 | 70 | pub fn as_html(&self) -> String { 71 | TEMPLATE 72 | .replacen("{{description}}", &self.description, 1) 73 | .replacen("{{title}}", &self.title, 1) 74 | .replacen("{{js_url}}", &self.js_url, 1) 75 | .replacen("{{spec_url}}", &self.spec_url, 1) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /predawn/src/plugin/ui/redoc.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use http::Method; 4 | use indexmap::IndexMap; 5 | use rudi::{Context, Singleton}; 6 | 7 | use crate::{config::Config, handler::DynHandler, normalized_path::NormalizedPath, plugin::Plugin}; 8 | 9 | const TEMPLATE: &str = r###" 10 | 11 | 12 | 13 | 14 | 15 | 16 | {{title}} 17 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | "###; 28 | 29 | #[derive(Clone, Debug)] 30 | pub struct Redoc { 31 | description: Box, 32 | title: Box, 33 | js_url: Box, 34 | spec_url: Box, 35 | } 36 | 37 | impl Plugin for Redoc { 38 | fn create_route( 39 | self: Arc, 40 | cx: &mut Context, 41 | ) -> (NormalizedPath, IndexMap) { 42 | super::create_route(cx, |c| c.redoc_path, self.as_html()) 43 | } 44 | } 45 | 46 | fn condition(cx: &Context) -> bool { 47 | !cx.contains_provider::() 48 | } 49 | 50 | #[Singleton(condition = condition)] 51 | fn RedocRegister(#[di(ref)] cfg: &Config) -> Redoc { 52 | let json_path = super::json_path(cfg).into_inner(); 53 | Redoc::new(json_path) 54 | } 55 | 56 | #[Singleton(name = std::any::type_name::())] 57 | fn RedocToPlugin(redoc: Redoc) -> Arc { 58 | Arc::new(redoc) 59 | } 60 | 61 | impl Redoc { 62 | pub fn new(spec_url: T) -> Self 63 | where 64 | T: Into>, 65 | { 66 | Self { 67 | description: Box::from("Redoc"), 68 | title: Box::from("Redoc"), 69 | js_url: Box::from("https://cdn.redoc.ly/redoc/latest/bundles/redoc.standalone.js"), 70 | spec_url: spec_url.into(), 71 | } 72 | } 73 | 74 | pub fn as_html(&self) -> String { 75 | TEMPLATE 76 | .replacen("{{description}}", &self.description, 1) 77 | .replacen("{{title}}", &self.title, 1) 78 | .replacen("{{js_url}}", &self.js_url, 1) 79 | .replacen("{{spec_url}}", &self.spec_url, 1) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /predawn/src/plugin/ui/scalar.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use http::Method; 4 | use indexmap::IndexMap; 5 | use rudi::{Context, Singleton}; 6 | 7 | use crate::{config::Config, handler::DynHandler, normalized_path::NormalizedPath, plugin::Plugin}; 8 | 9 | const TEMPLATE: &str = r###" 10 | 11 | 12 | 13 | 14 | 15 | 16 | {{title}} 17 | 18 | 19 | 20 | 21 | 29 | 30 | 31 | 32 | "###; 33 | 34 | #[derive(Clone, Debug)] 35 | pub struct Scalar { 36 | description: Box, 37 | title: Box, 38 | js_url: Box, 39 | spec_url: Box, 40 | } 41 | 42 | impl Plugin for Scalar { 43 | fn create_route( 44 | self: Arc, 45 | cx: &mut Context, 46 | ) -> (NormalizedPath, IndexMap) { 47 | super::create_route(cx, |c| c.scalar_path, self.as_html()) 48 | } 49 | } 50 | 51 | fn condition(cx: &Context) -> bool { 52 | !cx.contains_provider::() 53 | } 54 | 55 | #[Singleton(condition = condition)] 56 | fn ScalarRegister(#[di(ref)] cfg: &Config) -> Scalar { 57 | let json_path = super::json_path(cfg).into_inner(); 58 | Scalar::new(json_path) 59 | } 60 | 61 | #[Singleton(name = std::any::type_name::())] 62 | fn ScalarToPlugin(scalar: Scalar) -> Arc { 63 | Arc::new(scalar) 64 | } 65 | 66 | impl Scalar { 67 | pub fn new(spec_url: T) -> Self 68 | where 69 | T: Into>, 70 | { 71 | Self { 72 | description: Box::from("Scalar"), 73 | title: Box::from("Scalar"), 74 | js_url: Box::from("https://cdn.jsdelivr.net/npm/@scalar/api-reference"), 75 | spec_url: spec_url.into(), 76 | } 77 | } 78 | 79 | pub fn as_html(&self) -> String { 80 | TEMPLATE 81 | .replacen("{{description}}", &self.description, 1) 82 | .replacen("{{title}}", &self.title, 1) 83 | .replacen("{{js_url}}", &self.js_url, 1) 84 | .replacen("{{spec_url}}", &self.spec_url, 1) 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /predawn/src/plugin/ui/swagger_ui.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use http::Method; 4 | use indexmap::IndexMap; 5 | use rudi::{Context, Singleton}; 6 | 7 | use crate::{config::Config, handler::DynHandler, normalized_path::NormalizedPath, plugin::Plugin}; 8 | 9 | const TEMPLATE: &str = r###" 10 | 11 | 12 | 13 | 14 | 15 | 16 | {{title}} 17 | 18 | 19 | 20 |
21 | 22 | 23 | 33 | 34 | 35 | "###; 36 | 37 | #[derive(Debug, Clone)] 38 | pub struct SwaggerUI { 39 | description: Box, 40 | title: Box, 41 | css_url: Box, 42 | bundle_js_url: Box, 43 | standalone_preset_url: Box, 44 | spec_url: Box, 45 | } 46 | 47 | impl Plugin for SwaggerUI { 48 | fn create_route( 49 | self: Arc, 50 | cx: &mut Context, 51 | ) -> (NormalizedPath, IndexMap) { 52 | super::create_route(cx, |c| c.swagger_ui_path, self.as_html()) 53 | } 54 | } 55 | 56 | fn condition(cx: &Context) -> bool { 57 | !cx.contains_provider::() 58 | } 59 | 60 | #[Singleton(condition = condition)] 61 | fn SwaggerUIRegister(#[di(ref)] cfg: &Config) -> SwaggerUI { 62 | let json_path = super::json_path(cfg).into_inner(); 63 | SwaggerUI::new(json_path) 64 | } 65 | 66 | #[Singleton(name = std::any::type_name::())] 67 | fn SwaggerUIToPlugin(ui: SwaggerUI) -> Arc { 68 | Arc::new(ui) 69 | } 70 | 71 | impl SwaggerUI { 72 | pub fn new(spec_url: T) -> Self 73 | where 74 | T: Into>, 75 | { 76 | Self { 77 | description: Box::from("SwaggerUI"), 78 | title: Box::from("SwaggerUI"), 79 | css_url: Box::from("https://unpkg.com/swagger-ui-dist/swagger-ui.css"), 80 | bundle_js_url: Box::from("https://unpkg.com/swagger-ui-dist/swagger-ui-bundle.js"), 81 | standalone_preset_url: Box::from( 82 | "https://unpkg.com/swagger-ui-dist/swagger-ui-standalone-preset.js", 83 | ), 84 | spec_url: spec_url.into(), 85 | } 86 | } 87 | 88 | pub fn description(mut self, description: T) -> Self 89 | where 90 | T: Into>, 91 | { 92 | self.description = description.into(); 93 | self 94 | } 95 | 96 | pub fn title(mut self, title: T) -> Self 97 | where 98 | T: Into>, 99 | { 100 | self.title = title.into(); 101 | self 102 | } 103 | 104 | pub fn css_url(mut self, css_url: T) -> Self 105 | where 106 | T: Into>, 107 | { 108 | self.css_url = css_url.into(); 109 | self 110 | } 111 | 112 | pub fn bundle_js_url(mut self, bundle_js_url: T) -> Self 113 | where 114 | T: Into>, 115 | { 116 | self.bundle_js_url = bundle_js_url.into(); 117 | self 118 | } 119 | 120 | pub fn standalone_preset_url(mut self, standalone_preset_url: T) -> Self 121 | where 122 | T: Into>, 123 | { 124 | self.standalone_preset_url = standalone_preset_url.into(); 125 | self 126 | } 127 | 128 | pub fn as_html(&self) -> String { 129 | TEMPLATE 130 | .replacen("{{description}}", &self.description, 1) 131 | .replacen("{{title}}", &self.title, 1) 132 | .replacen("{{css_url}}", &self.css_url, 1) 133 | .replacen("{{bundle_js_url}}", &self.bundle_js_url, 1) 134 | .replacen("{{standalone_preset_url}}", &self.standalone_preset_url, 1) 135 | .replacen("{{spec_url}}", &self.spec_url, 1) 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /predawn/src/response/mod.rs: -------------------------------------------------------------------------------- 1 | mod download; 2 | pub mod sse; 3 | mod to_header_value; 4 | 5 | pub use predawn_core::response::Response; 6 | 7 | pub use self::{ 8 | download::Download, 9 | to_header_value::{MaybeHeaderValue, ToHeaderValue}, 10 | }; 11 | -------------------------------------------------------------------------------- /predawn/src/response/sse/builder.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | marker::PhantomData, 3 | pin::Pin, 4 | task::{Context, Poll}, 5 | }; 6 | 7 | use bytes::Bytes; 8 | use futures_core::{Stream, TryStream}; 9 | use futures_util::TryStreamExt; 10 | use http::header::{CACHE_CONTROL, CONTENT_TYPE}; 11 | use pin_project_lite::pin_project; 12 | use predawn_core::{ 13 | body::ResponseBody, error::BoxError, media_type::MediaType, response::Response, 14 | }; 15 | use serde::Serialize; 16 | 17 | use super::{Event, EventStream, KeepAlive, keep_alive::KeepAliveStream}; 18 | use crate::response_error::EventStreamError; 19 | 20 | pub struct EventStreamBuilder { 21 | pub(crate) keep_alive: Option, 22 | pub(crate) _marker: PhantomData, 23 | } 24 | 25 | impl EventStreamBuilder { 26 | pub fn keep_alive(mut self, keep_alive: KeepAlive) -> Self { 27 | self.keep_alive = Some(keep_alive); 28 | self 29 | } 30 | } 31 | 32 | impl EventStreamBuilder { 33 | pub fn on_create_event(self) -> EventStreamBuilder 34 | where 35 | C: OnCreateEvent, 36 | { 37 | EventStreamBuilder { 38 | keep_alive: self.keep_alive, 39 | _marker: PhantomData, 40 | } 41 | } 42 | 43 | pub fn build(self, stream: S) -> EventStream 44 | where 45 | S: TryStream + Send + 'static, 46 | S::Ok: Into + Send, 47 | S::Error: Into, 48 | { 49 | EventStream { 50 | result: inner_build(self, stream), 51 | _marker: PhantomData, 52 | } 53 | } 54 | } 55 | 56 | fn inner_build( 57 | builder: EventStreamBuilder, 58 | stream: S, 59 | ) -> Result 60 | where 61 | F: OnCreateEvent, 62 | S: TryStream + Send + 'static, 63 | S::Ok: Into + Send, 64 | S::Error: Into, 65 | { 66 | pin_project! { 67 | struct SseStream { 68 | #[pin] 69 | stream: S, 70 | #[pin] 71 | keep_alive: Option, 72 | } 73 | } 74 | 75 | impl Stream for SseStream 76 | where 77 | S: Stream> + Send + 'static, 78 | { 79 | type Item = S::Item; 80 | 81 | fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { 82 | let mut this = self.project(); 83 | 84 | match this.stream.try_poll_next_unpin(cx) { 85 | Poll::Pending => { 86 | if let Some(keep_alive) = this.keep_alive.as_pin_mut() { 87 | keep_alive.poll_event(cx).map(|e| Some(Ok(e))) 88 | } else { 89 | Poll::Pending 90 | } 91 | } 92 | ok @ Poll::Ready(Some(Ok(_))) => { 93 | if let Some(keep_alive) = this.keep_alive.as_pin_mut() { 94 | keep_alive.reset(); 95 | } 96 | 97 | ok 98 | } 99 | other => other, 100 | } 101 | } 102 | } 103 | 104 | let stream = SseStream { 105 | stream: stream.map_err(Into::into).and_then(|item| async move { 106 | let item = item.into(); 107 | 108 | let data = F::data(&item); 109 | 110 | let event = Event::data(data).map_err(Box::new)?; 111 | let event = F::modify_event(item, event).map_err(Box::new)?; 112 | 113 | Ok::<_, BoxError>(event.as_bytes()) 114 | }), 115 | keep_alive: builder.keep_alive.map(KeepAliveStream::new).transpose()?, 116 | }; 117 | 118 | let body = ResponseBody::from_stream(stream); 119 | 120 | let response = http::Response::builder() 121 | .header(CONTENT_TYPE, EventStream::<()>::MEDIA_TYPE) 122 | .header(CACHE_CONTROL, "no-cache") 123 | .header("X-Accel-Buffering", "no") 124 | .body(body) 125 | .unwrap(); 126 | 127 | Ok(response) 128 | } 129 | 130 | pub trait OnCreateEvent { 131 | type Item: Send + 'static; 132 | 133 | type Data: Serialize; 134 | 135 | fn data(item: &Self::Item) -> &Self::Data; 136 | 137 | fn modify_event(item: Self::Item, event: Event) -> Result; 138 | } 139 | 140 | #[derive(Debug)] 141 | pub struct DefaultOnCreateEvent { 142 | _marker: PhantomData, 143 | } 144 | 145 | impl OnCreateEvent for DefaultOnCreateEvent 146 | where 147 | T: Serialize + Send + 'static, 148 | { 149 | type Data = T; 150 | type Item = T; 151 | 152 | fn data(item: &Self::Item) -> &Self::Data { 153 | item 154 | } 155 | 156 | fn modify_event(_: Self::Item, event: Event) -> Result { 157 | Ok(event) 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /predawn/src/response/sse/keep_alive.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | pin::Pin, 3 | task::{Context, Poll}, 4 | time::Duration, 5 | }; 6 | 7 | use bytes::Bytes; 8 | use pin_project_lite::pin_project; 9 | use tokio::time::Sleep; 10 | 11 | use super::Event; 12 | use crate::response_error::EventStreamError; 13 | 14 | #[derive(Debug, Clone)] 15 | pub struct KeepAlive { 16 | comment: Box, 17 | max_interval: Duration, 18 | } 19 | 20 | impl Default for KeepAlive { 21 | fn default() -> Self { 22 | Self { 23 | comment: Default::default(), 24 | max_interval: Duration::from_secs(15), 25 | } 26 | } 27 | } 28 | 29 | impl KeepAlive { 30 | pub fn comment>>(self, comment: T) -> Self { 31 | fn inner(mut ka: KeepAlive, comment: Box) -> KeepAlive { 32 | ka.comment = comment; 33 | ka 34 | } 35 | 36 | inner(self, comment.into()) 37 | } 38 | 39 | pub fn interval(mut self, interval: Duration) -> Self { 40 | self.max_interval = interval; 41 | self 42 | } 43 | } 44 | 45 | pin_project! { 46 | #[derive(Debug)] 47 | pub(crate) struct KeepAliveStream { 48 | event: Bytes, 49 | max_interval: Duration, 50 | 51 | #[pin] 52 | alive_timer: Sleep, 53 | } 54 | } 55 | 56 | impl KeepAliveStream { 57 | pub(crate) fn new(keep_alive: KeepAlive) -> Result { 58 | let KeepAlive { 59 | comment, 60 | max_interval, 61 | } = keep_alive; 62 | 63 | let event = Event::only_comment(comment)?; 64 | let event = event.as_bytes(); 65 | 66 | Ok(Self { 67 | event, 68 | max_interval, 69 | alive_timer: tokio::time::sleep(max_interval), 70 | }) 71 | } 72 | 73 | pub(crate) fn reset(self: Pin<&mut Self>) { 74 | let this = self.project(); 75 | 76 | this.alive_timer 77 | .reset(tokio::time::Instant::now() + *this.max_interval); 78 | } 79 | 80 | pub(crate) fn poll_event(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { 81 | let this = self.as_mut().project(); 82 | 83 | std::task::ready!(this.alive_timer.poll(cx)); 84 | 85 | let event = this.event.clone(); 86 | 87 | self.reset(); 88 | 89 | Poll::Ready(event) 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /predawn/src/response/sse/mod.rs: -------------------------------------------------------------------------------- 1 | mod builder; 2 | mod event; 3 | mod keep_alive; 4 | mod stream; 5 | 6 | pub use self::{ 7 | builder::{DefaultOnCreateEvent, EventStreamBuilder, OnCreateEvent}, 8 | event::Event, 9 | keep_alive::KeepAlive, 10 | stream::EventStream, 11 | }; 12 | -------------------------------------------------------------------------------- /predawn/src/response/sse/stream.rs: -------------------------------------------------------------------------------- 1 | use std::{collections::BTreeMap, marker::PhantomData}; 2 | 3 | use futures_core::TryStream; 4 | use http::StatusCode; 5 | use predawn_core::{ 6 | api_response::ApiResponse, 7 | error::BoxError, 8 | into_response::IntoResponse, 9 | media_type::{MediaType, MultiResponseMediaType, ResponseMediaType, SingleMediaType}, 10 | openapi::{self, AnySchema, ReferenceOr, Schema, SchemaKind}, 11 | response::{MultiResponse, Response, SingleResponse}, 12 | }; 13 | use predawn_schema::ToSchema; 14 | use serde::Serialize; 15 | 16 | use super::{DefaultOnCreateEvent, EventStreamBuilder}; 17 | use crate::response_error::EventStreamError; 18 | 19 | pub struct EventStream { 20 | pub(crate) result: Result, 21 | pub(crate) _marker: PhantomData, 22 | } 23 | 24 | impl EventStream { 25 | pub fn new(stream: S) -> Self 26 | where 27 | T: Serialize + Send + 'static, 28 | S: TryStream + Send + 'static, 29 | S::Ok: Into + Send, 30 | S::Error: Into, 31 | { 32 | Self::builder().build(stream) 33 | } 34 | 35 | pub fn builder() -> EventStreamBuilder> { 36 | EventStreamBuilder { 37 | keep_alive: None, 38 | _marker: PhantomData, 39 | } 40 | } 41 | } 42 | 43 | impl IntoResponse for EventStream { 44 | type Error = EventStreamError; 45 | 46 | fn into_response(self) -> Result { 47 | self.result 48 | } 49 | } 50 | 51 | impl MediaType for EventStream { 52 | const MEDIA_TYPE: &'static str = "text/event-stream"; 53 | } 54 | 55 | impl ResponseMediaType for EventStream {} 56 | 57 | impl SingleMediaType for EventStream { 58 | fn media_type( 59 | schemas: &mut BTreeMap, 60 | schemas_in_progress: &mut Vec, 61 | ) -> openapi::MediaType { 62 | let schema = Schema { 63 | schema_data: Default::default(), 64 | schema_kind: SchemaKind::Any(AnySchema { 65 | typ: Some("array".into()), 66 | items: Some(T::schema_ref_box(schemas, schemas_in_progress)), 67 | format: Some("event-stream".into()), 68 | ..Default::default() 69 | }), 70 | }; 71 | 72 | openapi::MediaType { 73 | schema: Some(ReferenceOr::Item(schema)), 74 | ..Default::default() 75 | } 76 | } 77 | } 78 | 79 | impl SingleResponse for EventStream { 80 | fn response( 81 | schemas: &mut BTreeMap, 82 | schemas_in_progress: &mut Vec, 83 | ) -> openapi::Response { 84 | openapi::Response { 85 | content: ::content(schemas, schemas_in_progress), 86 | ..Default::default() 87 | } 88 | } 89 | } 90 | 91 | impl ApiResponse for EventStream { 92 | fn responses( 93 | schemas: &mut BTreeMap, 94 | schemas_in_progress: &mut Vec, 95 | ) -> Option> { 96 | Some(::responses( 97 | schemas, 98 | schemas_in_progress, 99 | )) 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /predawn/src/response/to_header_value.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | borrow::Cow, 3 | fmt::Debug, 4 | net::{IpAddr, Ipv4Addr, Ipv6Addr}, 5 | }; 6 | 7 | use bytes::Bytes; 8 | use http::{HeaderValue, Uri}; 9 | 10 | #[derive(Debug, Clone)] 11 | pub enum MaybeHeaderValue { 12 | Value(HeaderValue), 13 | None, 14 | Error, 15 | } 16 | 17 | pub trait ToHeaderValue: Debug { 18 | fn to_header_value(&self) -> MaybeHeaderValue; 19 | } 20 | 21 | impl ToHeaderValue for Option { 22 | fn to_header_value(&self) -> MaybeHeaderValue { 23 | match self { 24 | Some(v) => v.to_header_value(), 25 | None => MaybeHeaderValue::None, 26 | } 27 | } 28 | } 29 | 30 | impl ToHeaderValue for bool { 31 | fn to_header_value(&self) -> MaybeHeaderValue { 32 | let s = match self { 33 | true => "true", 34 | false => "false", 35 | }; 36 | 37 | MaybeHeaderValue::Value(HeaderValue::from_static(s)) 38 | } 39 | } 40 | 41 | macro_rules! impl_to_header_value_directly { 42 | ($($ty:ty),+ $(,)?) => { 43 | $( 44 | impl ToHeaderValue for $ty { 45 | fn to_header_value(&self) -> MaybeHeaderValue { 46 | MaybeHeaderValue::Value(HeaderValue::from(*self)) 47 | } 48 | } 49 | )+ 50 | }; 51 | } 52 | 53 | impl_to_header_value_directly![i16, i32, i64, isize, u16, u32, u64, usize]; 54 | 55 | macro_rules! impl_to_header_value_str { 56 | ($($ty:ty),+ $(,)?) => { 57 | $( 58 | impl ToHeaderValue for $ty { 59 | fn to_header_value(&self) -> MaybeHeaderValue { 60 | match HeaderValue::from_str(self) { 61 | Ok(o) => MaybeHeaderValue::Value(o), 62 | Err(_) => MaybeHeaderValue::Error, 63 | } 64 | } 65 | } 66 | )+ 67 | }; 68 | } 69 | 70 | impl_to_header_value_str![&'static str, Cow<'static, str>, String, Box]; 71 | 72 | macro_rules! impl_to_header_value_bytes { 73 | ($($ty:ty),+ $(,)?) => { 74 | $( 75 | impl ToHeaderValue for $ty { 76 | fn to_header_value(&self) -> MaybeHeaderValue { 77 | match HeaderValue::from_bytes(self) { 78 | Ok(o) => MaybeHeaderValue::Value(o), 79 | Err(_) => MaybeHeaderValue::Error, 80 | } 81 | } 82 | } 83 | )+ 84 | }; 85 | } 86 | 87 | impl_to_header_value_bytes![&'static [u8], Cow<'static, [u8]>, Vec, Box<[u8]>, Bytes]; 88 | 89 | macro_rules! impl_to_header_value_by_to_string { 90 | ($($ty:ty),+ $(,)?) => { 91 | $( 92 | impl ToHeaderValue for $ty { 93 | fn to_header_value(&self) -> MaybeHeaderValue { 94 | match HeaderValue::try_from(self.to_string()) { 95 | Ok(o) => MaybeHeaderValue::Value(o), 96 | Err(_) => MaybeHeaderValue::Error, 97 | } 98 | } 99 | } 100 | )+ 101 | }; 102 | } 103 | 104 | impl_to_header_value_by_to_string![ 105 | i8, i128, u8, u128, f32, f64, Ipv4Addr, Ipv6Addr, IpAddr, Uri 106 | ]; 107 | -------------------------------------------------------------------------------- /predawn/src/route.rs: -------------------------------------------------------------------------------- 1 | use futures_util::{FutureExt, future::Either}; 2 | use http::Method; 3 | use indexmap::IndexMap; 4 | use matchit::{InsertError, Match}; 5 | use predawn_core::{error::Error, request::Request, response::Response}; 6 | use snafu::ResultExt; 7 | 8 | use crate::{ 9 | handler::{DynHandler, Handler}, 10 | path_params::PathParams, 11 | response_error::{MatchSnafu, MethodNotAllowedSnafu}, 12 | }; 13 | 14 | #[derive(Default)] 15 | pub struct MethodRouter { 16 | methods: IndexMap, 17 | } 18 | 19 | impl From> for MethodRouter { 20 | fn from(methods: IndexMap) -> Self { 21 | Self { methods } 22 | } 23 | } 24 | 25 | impl Handler for MethodRouter { 26 | fn call(&self, mut req: Request) -> impl Future> + Send { 27 | let method = &mut req.head.method; 28 | 29 | match self.methods.get(method) { 30 | Some(handler) => Either::Left(handler.call(req)), 31 | None => Either::Right( 32 | if *method != Method::HEAD { 33 | Either::Left(async { Err(MethodNotAllowedSnafu.build().into()) }) 34 | } else { 35 | *method = Method::GET; 36 | 37 | Either::Right( 38 | async move { 39 | let mut response = self.call(req).await?; 40 | response.body_mut().clear(); 41 | Ok(response) 42 | } 43 | .boxed(), 44 | ) 45 | }, 46 | ), 47 | } 48 | } 49 | } 50 | 51 | #[derive(Default)] 52 | pub struct Router { 53 | router: matchit::Router, 54 | routes: Vec<(Box, Box<[Method]>)>, 55 | } 56 | 57 | impl Router { 58 | pub fn insert(&mut self, route: S, method_router: MethodRouter) -> Result<(), InsertError> 59 | where 60 | S: Into, 61 | { 62 | fn inner_insert( 63 | router: &mut Router, 64 | route: String, 65 | method_router: MethodRouter, 66 | ) -> Result<(), InsertError> { 67 | let methods = method_router.methods.keys().cloned().collect(); 68 | 69 | router.router.insert(route.clone(), method_router)?; 70 | router.routes.push((route.into(), methods)); 71 | 72 | Ok(()) 73 | } 74 | 75 | inner_insert(self, route.into(), method_router) 76 | } 77 | 78 | pub fn at<'m, 'p>( 79 | &'m self, 80 | path: &'p str, 81 | ) -> Result, matchit::MatchError> { 82 | self.router.at(path) 83 | } 84 | 85 | pub fn routes(&self) -> &[(Box, Box<[Method]>)] { 86 | &self.routes 87 | } 88 | } 89 | 90 | impl Handler for Router { 91 | async fn call(&self, mut req: Request) -> Result { 92 | let head = &mut req.head; 93 | 94 | let matched = self.at(head.uri.path()).context(MatchSnafu)?; 95 | 96 | #[allow(unused_variables)] 97 | let prev = head.extensions.insert(PathParams::new(matched.params)); 98 | debug_assert!(prev.is_none()); 99 | 100 | matched.value.call(req).await 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /predawn/src/test_client.rs: -------------------------------------------------------------------------------- 1 | use std::net::SocketAddr; 2 | 3 | use reqwest::{Client, RequestBuilder, redirect::Policy}; 4 | use rudi::Context; 5 | use tokio::net::TcpListener; 6 | 7 | use crate::{ 8 | app::{Hooks, create_app}, 9 | environment::Environment, 10 | server::Server, 11 | }; 12 | 13 | macro_rules! impl_request_methods { 14 | ($($name:ident),+ $(,)?) => { 15 | $( 16 | pub fn $name(&self, url: &str) -> RequestBuilder { 17 | self.client.$name(format!("http://{}{}", self.addr, url)) 18 | } 19 | )+ 20 | }; 21 | } 22 | 23 | pub struct TestClient { 24 | client: Client, 25 | addr: SocketAddr, 26 | #[allow(dead_code)] 27 | cx: Context, 28 | } 29 | 30 | impl TestClient { 31 | impl_request_methods![get, post, put, delete, head, patch]; 32 | 33 | pub async fn new() -> Self { 34 | let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); 35 | let addr = listener.local_addr().unwrap(); 36 | 37 | tracing::info!("listening on {}", addr); 38 | 39 | let (cx, router) = create_app::(Environment::Test).await; 40 | 41 | tokio::spawn(async move { 42 | Server::new(listener).run(router).await.unwrap(); 43 | }); 44 | 45 | let client = Client::builder().redirect(Policy::none()).build().unwrap(); 46 | 47 | Self { client, addr, cx } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /predawn/src/traits.rs: -------------------------------------------------------------------------------- 1 | use std::collections::BTreeMap; 2 | 3 | use predawn_core::openapi::{ParameterData, Schema}; 4 | 5 | use crate::openapi; 6 | 7 | pub trait ToParameters { 8 | fn parameters( 9 | schemas: &mut BTreeMap, 10 | schemas_in_progress: &mut Vec, 11 | ) -> Vec; 12 | } 13 | 14 | pub trait Tag { 15 | const NAME: &'static str; 16 | 17 | fn create() -> openapi::Tag; 18 | } 19 | 20 | pub trait SecurityScheme { 21 | const NAME: &'static str; 22 | 23 | fn create() -> openapi::SecurityScheme; 24 | } 25 | -------------------------------------------------------------------------------- /predawn/src/util.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | 3 | use bytes::{BufMut, Bytes, BytesMut}; 4 | use predawn_core::openapi::{ 5 | Schema, SchemaData, SchemaKind, StringFormat, StringType, Type, VariantOrUnknownOrEmpty, 6 | }; 7 | use serde::{Deserialize, Serialize}; 8 | use serde_json::error::Category; 9 | use snafu::IntoError; 10 | 11 | use crate::response_error::{DataSnafu, DeserializeJsonError, EofSnafu, SyntaxSnafu}; 12 | 13 | pub(crate) fn deserialize_json<'de, T>(bytes: &'de [u8]) -> Result 14 | where 15 | T: Deserialize<'de>, 16 | { 17 | let mut deserializer = serde_json::Deserializer::from_slice(bytes); 18 | 19 | serde_path_to_error::deserialize(&mut deserializer).map_err(|err| { 20 | match err.inner().classify() { 21 | Category::Io => { 22 | if cfg!(debug_assertions) { 23 | // we don't use `serde_json::from_reader` and instead always buffer 24 | // bodies first, so we shouldn't encounter any IO errors 25 | unreachable!() 26 | } else { 27 | SyntaxSnafu.into_error(err) 28 | } 29 | } 30 | Category::Syntax => SyntaxSnafu.into_error(err), 31 | Category::Data => DataSnafu.into_error(err), 32 | Category::Eof => EofSnafu.into_error(err), 33 | } 34 | }) 35 | } 36 | 37 | pub(crate) fn deserialize_form<'de, T>( 38 | bytes: &'de [u8], 39 | ) -> Result> 40 | where 41 | T: Deserialize<'de>, 42 | { 43 | let deserializer = serde_html_form::Deserializer::new(form_urlencoded::parse(bytes)); 44 | serde_path_to_error::deserialize(deserializer) 45 | } 46 | 47 | pub(crate) fn serialize_json( 48 | value: &T, 49 | ) -> Result> 50 | where 51 | T: Serialize + ?Sized, 52 | { 53 | let mut writer = BytesMut::with_capacity(128).writer(); 54 | 55 | let mut serializer = serde_json::Serializer::new(&mut writer); 56 | serde_path_to_error::serialize(value, &mut serializer)?; 57 | 58 | Ok(writer.into_inner().freeze()) 59 | } 60 | 61 | pub(crate) fn serialize_form( 62 | value: &T, 63 | ) -> Result> 64 | where 65 | T: Serialize + ?Sized, 66 | { 67 | let mut target = String::with_capacity(128); 68 | 69 | let mut url_encoder = form_urlencoded::Serializer::for_suffix(&mut target, 0); 70 | let serializer = serde_html_form::Serializer::new(&mut url_encoder); 71 | 72 | serde_path_to_error::serialize(value, serializer)?; 73 | url_encoder.finish(); 74 | 75 | Ok(target) 76 | } 77 | 78 | pub(crate) fn binary_schema(title: Cow<'static, str>) -> Schema { 79 | let ty = StringType { 80 | format: VariantOrUnknownOrEmpty::Item(StringFormat::Binary), 81 | ..Default::default() 82 | }; 83 | 84 | Schema { 85 | schema_data: SchemaData { 86 | title: Some(title.into()), 87 | ..Default::default() 88 | }, 89 | schema_kind: SchemaKind::Type(Type::String(ty)), 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /todo.txt: -------------------------------------------------------------------------------- 1 | "/" => "" 2 | expect: /{a}/{b} 3 | actual: //1 4 | 5 | add prefix for attribute macros 6 | 7 | flatten panic 8 | pretty openapi json 9 | 丰富 Error.response.body 的错误内容 10 | 11 | discriminator 12 | ToParameters expand 13 | 14 | tempfile 15 | 16 | serde ignore 17 | 18 | SecuritySchema 19 | OAuth2 20 | OpenIDConnect 21 | HttpAuthScheme 22 | 23 | delete `predawn-sea-orm` unnacessary `async` in `async fn inject(...)` 24 | 25 | controller: condition, skip 26 | 27 | ToSchema / ToParameter 28 | / MultiRequestMediaType / MultiResponseMediaType 29 | / SingleResponse / MultiResponse : 30 | 31 | default, 32 | flatten 33 | example, 34 | deprecated, 35 | actual_type 36 | 37 | update references in doc comments by search `https://docs.rs` 38 | 39 | 推迟泛型限定 40 | refactor `Plugin` 41 | service attribute macro 42 | operation id [repeated] 43 | Extension 44 | Header 45 | Cookie 46 | Base64 47 | Html 48 | ProtoBuf 49 | split controller trait 50 | more openapi ui 51 | more ToSchema impl 52 | static_file embed_file 53 | Listener trait 54 | ExternalDocumentation 55 | end-to-end test-helper edition 2 56 | startup message 57 | handle .unwrap() 58 | validate 59 | 60 | docs, docs, docs --------------------------------------------------------------------------------