Page Menu
Home
DevCentral
Search
Configure Global Search
Log In
Files
F11822843
D3716.id9615.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
15 KB
Referenced Files
None
Subscribers
None
D3716.id9615.diff
View Options
diff --git a/Cargo.toml b/Cargo.toml
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,5 +1,7 @@
[workspace]
resolver = "3"
members = [
+ "axum",
+ "core",
"rocket-legacy",
]
diff --git a/axum/Cargo.toml b/axum/Cargo.toml
new file mode 100644
--- /dev/null
+++ b/axum/Cargo.toml
@@ -0,0 +1,34 @@
+[package]
+name = "limiting-factor-axum"
+version = "0.1.0"
+authors = [
+ "Sébastien Santoro <dereckson@espace-win.org>",
+]
+description = "Library to create a REST API with Axum"
+readme = "README.md"
+keywords = [
+ "API",
+ "Axum",
+ "REST",
+]
+categories = [
+ "web-programming",
+]
+license = "BSD-2-Clause"
+repository = "https://devcentral.nasqueron.org/source/limiting-factor/"
+edition = "2024"
+
+[dependencies]
+axum = "0.8.4"
+limiting-factor-core = { path="../core" }
+serde = { version = "^1.0.226", features = [ "derive" ], optional = true }
+http-body-util = "0.1.3"
+tokio = "1.47.1"
+
+[features]
+default = ["minimal"]
+
+minimal = ["serialization"]
+full = ["serialization"]
+
+serialization = ["serde"]
diff --git a/axum/README.md b/axum/README.md
new file mode 100644
--- /dev/null
+++ b/axum/README.md
@@ -0,0 +1,29 @@
+# Limiting Factor for axum
+
+This crate helps to build REST API with Axum, with less boilerplate code.
+
+## Implemented features
+### Extractors
+
+The extractor for request body is a port of the Rocket 0.4 guard added in Limiting Factor 0.8.0.
+
+If you need to read the body of the HTTP request "as is", the AxumRequestBody
+extractor allows you to read it as a string:
+
+ async fn deploy(
+ Path(site_name): Path<String>,
+ State(config): State<AlkaneConfig>,
+ body: AxumRequestBody,
+ ) -> ApiResult<Json<RecipeStatus>> {
+ let context = body.into_optional_string(); // Option<String>
+ // ...
+ }
+
+## Development
+
+Current focus is to port features used by REST API from Rocket 0.4 to Axum 0.8.4+.
+
+New features:
+ - may be added to the axum crate
+ - should be added to the core crate for the abstract part
+ - are not expected to be implemented to the rocket-legacy crate
diff --git a/axum/src/api/guards.rs b/axum/src/api/guards.rs
new file mode 100644
--- /dev/null
+++ b/axum/src/api/guards.rs
@@ -0,0 +1,152 @@
+/* -------------------------------------------------------------
+ Limiting Factor :: Axum :: API :: Guards
+ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+ Project: Nasqueron
+ License: BSD-2-Clause
+ ------------------------------------------------------------- */
+
+//! # API extractors
+//!
+//! This module provides reusable extractors to use with Axum.
+
+use axum::{
+ extract::{FromRequest, Request},
+ http::StatusCode,
+ response::{IntoResponse, Response},
+};
+use http_body_util::BodyExt;
+
+use limiting_factor_core::api::guards::{RequestBody, REQUEST_BODY_LIMIT};
+
+// New-type wrapper for Axum-specific implementations
+#[derive(Debug, Clone)]
+pub struct AxumRequestBody(pub RequestBody);
+
+impl AxumRequestBody {
+ pub fn new() -> Self {
+ Self(RequestBody::new())
+ }
+
+ // Delegate methods
+ pub fn into_string(self) -> String {
+ self.0.into_string()
+ }
+
+ pub fn into_optional_string(self) -> Option<String> {
+ self.0.into_optional_string()
+ }
+}
+
+impl From<RequestBody> for AxumRequestBody {
+ fn from(data: RequestBody) -> Self {
+ Self(data)
+ }
+}
+
+impl From<AxumRequestBody> for RequestBody {
+ fn from(body: AxumRequestBody) -> Self {
+ body.0
+ }
+}
+
+/// Error type during a request body extraction
+#[derive(Debug)]
+pub enum RequestBodyError {
+ /// Body size is greater than REQUEST_BODY_LIMIT (DoS risk)
+ TooLarge,
+
+ /// Not in UTF-8 encoding
+ InvalidEncoding,
+
+ /// I/O error
+ ReadError(String),
+}
+
+impl IntoResponse for RequestBodyError {
+ fn into_response(self) -> Response {
+ let (status, message) = match self {
+ RequestBodyError::TooLarge => (
+ StatusCode::PAYLOAD_TOO_LARGE,
+ "Request body too large".to_string(),
+ ),
+
+ RequestBodyError::InvalidEncoding => (
+ StatusCode::BAD_REQUEST,
+ "Request body contains invalid characters when trying to decode as UTF-8".to_string(),
+ ),
+
+ RequestBodyError::ReadError(err) => (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ format!("Failed to read request body: {}", err),
+ ),
+ };
+
+ (status, message).into_response()
+ }
+}
+
+impl<S> FromRequest<S> for AxumRequestBody
+where
+ S: Send + Sync,
+{
+ type Rejection = RequestBodyError;
+
+ async fn from_request(req: Request, _state: &S) -> Result<Self, Self::Rejection> {
+ // Extract the body from the request
+ let body = req.into_body();
+
+ // Collect the body with size limit
+ let collected = match body.collect().await {
+ Ok(collected) => collected,
+ Err(e) => return Err(RequestBodyError::ReadError(e.to_string())),
+ };
+
+ let bytes = collected.to_bytes();
+
+ // Check size limit
+ if bytes.len() > REQUEST_BODY_LIMIT {
+ return Err(RequestBodyError::TooLarge);
+ }
+
+ // Convert to UTF-8 string
+ let content = match String::from_utf8(bytes.to_vec()) {
+ Ok(content) => content,
+ Err(_) => return Err(RequestBodyError::InvalidEncoding),
+ };
+
+ Ok(Self(RequestBody { content }))
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[tokio::test]
+ async fn test_empty_body_extraction() {
+ use axum::body::Body;
+ use axum::http::Request;
+
+ let req = Request::builder()
+ .body(Body::empty())
+ .unwrap();
+
+ let body = AxumRequestBody::from_request(req, &()).await.unwrap();
+ assert_eq!("", body.0.content);
+ assert_eq!(None, body.into_optional_string());
+ }
+
+ #[tokio::test]
+ async fn test_body_extraction() {
+ use axum::body::Body;
+ use axum::http::Request;
+
+ let req = Request::builder()
+ .body(Body::from("lorem ipsum dolor"))
+ .unwrap();
+
+ let body = AxumRequestBody::from_request(req, &()).await.unwrap();
+ assert_eq!("lorem ipsum dolor", body.0.content);
+ assert_eq!(Some("lorem ipsum dolor".to_string()), body.into_optional_string());
+ }
+}
diff --git a/axum/src/api/mod.rs b/axum/src/api/mod.rs
new file mode 100644
--- /dev/null
+++ b/axum/src/api/mod.rs
@@ -0,0 +1,16 @@
+/* -------------------------------------------------------------
+ Limiting Factor :: Axum :: API
+ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+ Project: Nasqueron
+ License: BSD-2-Clause
+ ------------------------------------------------------------- */
+
+//! # Utilities for API.
+//!
+//! This module provides useful code to create easily APIs.
+
+/* -------------------------------------------------------------
+ Public submodules offered by this module
+ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
+
+pub mod guards;
diff --git a/axum/src/lib.rs b/axum/src/lib.rs
new file mode 100644
--- /dev/null
+++ b/axum/src/lib.rs
@@ -0,0 +1,10 @@
+/* -------------------------------------------------------------
+ Limiting Factor :: Axum
+ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+ Project: Nasqueron
+ Crate name: limiting-factor-axum
+ Description: Limiting Factor features for Axum
+ License: BSD-2-Clause
+ ------------------------------------------------------------- */
+
+pub mod api;
diff --git a/core/Cargo.toml b/core/Cargo.toml
new file mode 100644
--- /dev/null
+++ b/core/Cargo.toml
@@ -0,0 +1,21 @@
+[package]
+name = "limiting-factor-core"
+version = "1.0.0"
+authors = [
+ "Sébastien Santoro <dereckson@espace-win.org>",
+]
+description = "Library to create a REST API with web frameworks"
+readme = "../README.md"
+keywords = [
+ "API",
+ "REST",
+]
+categories = [
+ "web-programming",
+]
+license = "BSD-2-Clause"
+repository = "https://devcentral.nasqueron.org/source/limiting-factor/"
+edition = "2024"
+
+[dependencies]
+serde = { version = "1.0.226", features = ["derive"] }
diff --git a/rocket-legacy/src/api/guards.rs b/core/src/api/guards.rs
copy from rocket-legacy/src/api/guards.rs
copy to core/src/api/guards.rs
--- a/rocket-legacy/src/api/guards.rs
+++ b/core/src/api/guards.rs
@@ -1,20 +1,23 @@
+/* -------------------------------------------------------------
+ Limiting Factor :: Core :: API :: Guards
+ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+ Project: Nasqueron
+ License: BSD-2-Clause
+ ------------------------------------------------------------- */
+
//! # API guards
//!
-//! This module provides reusable guards to use with Rocket.
+//! This module provides common element for:
+//! - reusable guards to use with Rocket
+//! - reusable extractors to use with Axum
-use rocket::data::{FromDataSimple, Outcome};
-use rocket::{Data, Request};
-use rocket::http::Status;
-use rocket::Outcome::{Failure, Success};
use serde::{Deserialize, Serialize};
-use std::io::Read;
-
/// The maximum number of characters to read, to avoid DoS
-const REQUEST_BODY_LIMIT: u64 = 1_000_000;
+pub const REQUEST_BODY_LIMIT: usize = 1_000_000;
/// A String representation of the request body. Useful when you need to pass it through as is.
-#[derive(Serialize, Deserialize, PartialOrd, PartialEq, Eq, Ord)]
+#[derive(Debug, Clone, Serialize, Deserialize, PartialOrd, PartialEq, Eq, Ord)]
pub struct RequestBody {
/// The UTF-8 content of the request body
pub content: String,
@@ -42,20 +45,6 @@
}
}
-impl FromDataSimple for RequestBody {
- type Error = String;
-
- fn from_data(_request: &Request, data: Data) -> Outcome<Self, Self::Error> {
- let mut content = String::new();
-
- if let Err(e) = data.open().take(REQUEST_BODY_LIMIT).read_to_string(&mut content) {
- return Failure((Status::InternalServerError, format!("{:?}", e)));
- }
-
- Success(Self { content })
- }
-}
-
#[cfg(test)]
mod tests {
use super::*;
diff --git a/core/src/api/mod.rs b/core/src/api/mod.rs
new file mode 100644
--- /dev/null
+++ b/core/src/api/mod.rs
@@ -0,0 +1,16 @@
+/* -------------------------------------------------------------
+ Limiting Factor :: Core :: API
+ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+ Project: Nasqueron
+ License: BSD-2-Clause
+ ------------------------------------------------------------- */
+
+//! # Utilities for API.
+//!
+//! This module provides useful code to create easily APIs.
+
+/* -------------------------------------------------------------
+ Public submodules offered by this module
+ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
+
+pub mod guards;
diff --git a/core/src/lib.rs b/core/src/lib.rs
new file mode 100644
--- /dev/null
+++ b/core/src/lib.rs
@@ -0,0 +1,11 @@
+/* -------------------------------------------------------------
+ Limiting Factor :: core features
+ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+ Project: Nasqueron
+ Crate name: limiting-factor-core
+ Description: Abstract code and non-framework-dependent features
+ for Limiting Factor framework-flavoured crates.
+ License: BSD-2-Clause
+ ------------------------------------------------------------- */
+
+pub mod api;
diff --git a/rocket-legacy/Cargo.toml b/rocket-legacy/Cargo.toml
--- a/rocket-legacy/Cargo.toml
+++ b/rocket-legacy/Cargo.toml
@@ -22,6 +22,7 @@
[dependencies]
diesel = { version = "^1.4.8", features = ["postgres", "r2d2", "chrono"], optional = true }
dotenv = "^0.15.0"
+limiting-factor-core = { path="../core" }
log = "^0.4.28"
r2d2 = { version = "^0.8.10", optional = true }
rocket = "^0.4.11"
diff --git a/rocket-legacy/src/api/guards.rs b/rocket-legacy/src/api/guards.rs
--- a/rocket-legacy/src/api/guards.rs
+++ b/rocket-legacy/src/api/guards.rs
@@ -6,87 +6,42 @@
use rocket::{Data, Request};
use rocket::http::Status;
use rocket::Outcome::{Failure, Success};
-use serde::{Deserialize, Serialize};
use std::io::Read;
-/// The maximum number of characters to read, to avoid DoS
-const REQUEST_BODY_LIMIT: u64 = 1_000_000;
+use limiting_factor_core::api::guards::{RequestBody, REQUEST_BODY_LIMIT};
-/// A String representation of the request body. Useful when you need to pass it through as is.
-#[derive(Serialize, Deserialize, PartialOrd, PartialEq, Eq, Ord)]
-pub struct RequestBody {
- /// The UTF-8 content of the request body
- pub content: String,
-}
+// New-type wrapper for Rocket-specific implementations
+#[derive(Debug, Clone)]
+pub struct RocketRequestBody(pub RequestBody);
-impl RequestBody {
- pub fn new () -> Self {
- Self {
- content: String::new(),
- }
+impl RocketRequestBody {
+ pub fn new() -> Self {
+ Self(RequestBody::new())
}
- /// Convert the request body into a string
- pub fn into_string (self) -> String {
- self.content
+ // Delegate methods
+ pub fn into_string(self) -> String {
+ self.0.into_string()
}
- /// Convert the request body into a string, or None if it's empty
- pub fn into_optional_string (self) -> Option<String> {
- if self.content.is_empty() {
- None
- } else {
- Some(self.content)
- }
+ pub fn into_optional_string(self) -> Option<String> {
+ self.0.into_optional_string()
}
}
-impl FromDataSimple for RequestBody {
+const ROCKET_REQUEST_BODY_LIMIT: u64 = REQUEST_BODY_LIMIT as u64;
+
+impl FromDataSimple for RocketRequestBody {
type Error = String;
fn from_data(_request: &Request, data: Data) -> Outcome<Self, Self::Error> {
let mut content = String::new();
- if let Err(e) = data.open().take(REQUEST_BODY_LIMIT).read_to_string(&mut content) {
+ if let Err(e) = data.open().take(ROCKET_REQUEST_BODY_LIMIT).read_to_string(&mut content) {
return Failure((Status::InternalServerError, format!("{:?}", e)));
}
- Success(Self { content })
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- fn test_request_body_new () {
- let body = RequestBody::new();
- assert_eq!(0, body.content.len(), "Content should be empty");
- }
-
- #[test]
- fn test_request_body_into_string () {
- let body = RequestBody { content: "quux".to_string() };
- assert_eq!(String::from("quux"), body.into_string());
- }
-
- #[test]
- fn test_request_body_into_string_when_empty () {
- let body = RequestBody::new();
- assert_eq!(String::new(), body.into_string(), "Content should be empty");
- }
-
- #[test]
- fn test_request_body_into_optional_string () {
- let body = RequestBody { content: "quux".to_string() };
- assert_eq!(Some(String::from("quux")), body.into_optional_string());
- }
-
- #[test]
- fn test_request_body_into_optional_string_when_empty () {
- let body = RequestBody::new();
- assert_eq!(None, body.into_optional_string());
+ Success(Self(RequestBody { content }))
}
}
diff --git a/rocket-legacy/src/lib.rs b/rocket-legacy/src/lib.rs
--- a/rocket-legacy/src/lib.rs
+++ b/rocket-legacy/src/lib.rs
@@ -44,6 +44,8 @@
#[cfg(feature = "serialization")]
extern crate serde;
+extern crate limiting_factor_core;
+
/* -------------------------------------------------------------
Public modules offered by this crate
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
File Metadata
Details
Attached
Mime Type
text/plain
Expires
Fri, Sep 26, 00:35 (22 h, 18 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
3016350
Default Alt Text
D3716.id9615.diff (15 KB)
Attached To
Mode
D3716: Port RequestBody guard to an axum extractor
Attached
Detach File
Event Timeline
Log In to Comment