Skip to content

Commit 940ae37

Browse files
authored
Merge pull request #104 from Tuntii/extract-validators-from-state-778271158241831590
Extract validators from App State in AsyncValidatedJson
2 parents a67c6cd + ba61055 commit 940ae37

3 files changed

Lines changed: 183 additions & 4 deletions

File tree

crates/rustapi-core/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ proptest = "1.4"
9090
rustapi-testing = { workspace = true }
9191
reqwest = { version = "0.12", features = ["json", "stream"] }
9292
async-stream = "0.3"
93+
async-trait = { workspace = true }
9394
[features]
9495
default = ["swagger-ui", "tracing"]
9596
swagger-ui = ["rustapi-openapi/swagger-ui"]

crates/rustapi-core/src/extract.rs

Lines changed: 180 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -378,8 +378,12 @@ impl<T: DeserializeOwned + AsyncValidate + Send + Sync> FromRequest for AsyncVal
378378
let value: T = json::from_slice(&body)?;
379379

380380
// Create validation context from request
381-
// TODO: Extract validators from App State
382-
let ctx = ValidationContext::default();
381+
// Check if validators are configured in App State
382+
let ctx = if let Some(ctx) = req.state().get::<ValidationContext>() {
383+
ctx.clone()
384+
} else {
385+
ValidationContext::default()
386+
};
383387

384388
// Perform full validation (sync + async)
385389
if let Err(errors) = value.validate_full(&ctx).await {
@@ -1715,4 +1719,178 @@ mod tests {
17151719
assert_eq!(cookies.get("token").unwrap().value(), "xyz789");
17161720
}
17171721
}
1722+
1723+
#[tokio::test]
1724+
async fn test_async_validated_json_with_state_context() {
1725+
use async_trait::async_trait;
1726+
use rustapi_validate::prelude::*;
1727+
use rustapi_validate::v2::{
1728+
AsyncValidationRule, DatabaseValidator, ValidationContextBuilder,
1729+
};
1730+
use serde::{Deserialize, Serialize};
1731+
1732+
struct MockDbValidator {
1733+
unique_values: Vec<String>,
1734+
}
1735+
1736+
#[async_trait]
1737+
impl DatabaseValidator for MockDbValidator {
1738+
async fn exists(
1739+
&self,
1740+
_table: &str,
1741+
_column: &str,
1742+
_value: &str,
1743+
) -> Result<bool, String> {
1744+
Ok(true)
1745+
}
1746+
async fn is_unique(
1747+
&self,
1748+
_table: &str,
1749+
_column: &str,
1750+
value: &str,
1751+
) -> Result<bool, String> {
1752+
Ok(!self.unique_values.contains(&value.to_string()))
1753+
}
1754+
async fn is_unique_except(
1755+
&self,
1756+
_table: &str,
1757+
_column: &str,
1758+
value: &str,
1759+
_except_id: &str,
1760+
) -> Result<bool, String> {
1761+
Ok(!self.unique_values.contains(&value.to_string()))
1762+
}
1763+
}
1764+
1765+
#[derive(Debug, Deserialize, Serialize)]
1766+
struct TestUser {
1767+
email: String,
1768+
}
1769+
1770+
impl Validate for TestUser {
1771+
fn validate_with_group(
1772+
&self,
1773+
_group: rustapi_validate::v2::ValidationGroup,
1774+
) -> Result<(), rustapi_validate::v2::ValidationErrors> {
1775+
Ok(())
1776+
}
1777+
}
1778+
1779+
#[async_trait]
1780+
impl AsyncValidate for TestUser {
1781+
async fn validate_async_with_group(
1782+
&self,
1783+
ctx: &ValidationContext,
1784+
_group: rustapi_validate::v2::ValidationGroup,
1785+
) -> Result<(), rustapi_validate::v2::ValidationErrors> {
1786+
let mut errors = rustapi_validate::v2::ValidationErrors::new();
1787+
1788+
let rule = AsyncUniqueRule::new("users", "email");
1789+
if let Err(e) = rule.validate_async(&self.email, ctx).await {
1790+
errors.add("email", e);
1791+
}
1792+
1793+
errors.into_result()
1794+
}
1795+
}
1796+
1797+
// Test 1: Without context in state (should fail due to missing validator)
1798+
let uri: http::Uri = "/test".parse().unwrap();
1799+
let user = TestUser {
1800+
email: "new@example.com".to_string(),
1801+
};
1802+
let body_bytes = serde_json::to_vec(&user).unwrap();
1803+
1804+
let builder = http::Request::builder()
1805+
.method(Method::POST)
1806+
.uri(uri.clone())
1807+
.header("content-type", "application/json");
1808+
let req = builder.body(()).unwrap();
1809+
let (parts, _) = req.into_parts();
1810+
1811+
// Construct Request with BodyVariant::Buffered
1812+
let mut request = Request::new(
1813+
parts,
1814+
crate::request::BodyVariant::Buffered(Bytes::from(body_bytes.clone())),
1815+
Arc::new(Extensions::new()),
1816+
PathParams::new(),
1817+
);
1818+
1819+
let result = AsyncValidatedJson::<TestUser>::from_request(&mut request).await;
1820+
1821+
assert!(result.is_err(), "Expected error when validator is missing");
1822+
let err = result.unwrap_err();
1823+
let err_str = format!("{:?}", err);
1824+
assert!(
1825+
err_str.contains("Database validator not configured")
1826+
|| err_str.contains("async_unique"),
1827+
"Error should mention missing configuration or rule: {:?}",
1828+
err_str
1829+
);
1830+
1831+
// Test 2: With context in state (should succeed)
1832+
let db_validator = MockDbValidator {
1833+
unique_values: vec!["taken@example.com".to_string()],
1834+
};
1835+
let ctx = ValidationContextBuilder::new()
1836+
.database(db_validator)
1837+
.build();
1838+
1839+
let mut extensions = Extensions::new();
1840+
extensions.insert(ctx);
1841+
1842+
let builder = http::Request::builder()
1843+
.method(Method::POST)
1844+
.uri(uri.clone())
1845+
.header("content-type", "application/json");
1846+
let req = builder.body(()).unwrap();
1847+
let (parts, _) = req.into_parts();
1848+
1849+
let mut request = Request::new(
1850+
parts,
1851+
crate::request::BodyVariant::Buffered(Bytes::from(body_bytes.clone())),
1852+
Arc::new(extensions),
1853+
PathParams::new(),
1854+
);
1855+
1856+
let result = AsyncValidatedJson::<TestUser>::from_request(&mut request).await;
1857+
assert!(
1858+
result.is_ok(),
1859+
"Expected success when validator is present and value is unique. Error: {:?}",
1860+
result.err()
1861+
);
1862+
1863+
// Test 3: With context in state (should fail validation logic)
1864+
let user_taken = TestUser {
1865+
email: "taken@example.com".to_string(),
1866+
};
1867+
let body_taken = serde_json::to_vec(&user_taken).unwrap();
1868+
1869+
let db_validator = MockDbValidator {
1870+
unique_values: vec!["taken@example.com".to_string()],
1871+
};
1872+
let ctx = ValidationContextBuilder::new()
1873+
.database(db_validator)
1874+
.build();
1875+
1876+
let mut extensions = Extensions::new();
1877+
extensions.insert(ctx);
1878+
1879+
let builder = http::Request::builder()
1880+
.method(Method::POST)
1881+
.uri("/test")
1882+
.header("content-type", "application/json");
1883+
let req = builder.body(()).unwrap();
1884+
let (parts, _) = req.into_parts();
1885+
1886+
let mut request = Request::new(
1887+
parts,
1888+
crate::request::BodyVariant::Buffered(Bytes::from(body_taken)),
1889+
Arc::new(extensions),
1890+
PathParams::new(),
1891+
);
1892+
1893+
let result = AsyncValidatedJson::<TestUser>::from_request(&mut request).await;
1894+
assert!(result.is_err(), "Expected validation error for taken email");
1895+
}
17181896
}

crates/rustapi-validate/src/v2/context.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ pub trait CustomValidator: Send + Sync {
5353
///
5454
/// user.validate_async(&ctx).await?;
5555
/// ```
56-
#[derive(Default)]
56+
#[derive(Clone, Default)]
5757
pub struct ValidationContext {
5858
database: Option<Arc<dyn DatabaseValidator>>,
5959
http: Option<Arc<dyn HttpValidator>>,
@@ -114,7 +114,7 @@ impl std::fmt::Debug for ValidationContext {
114114
}
115115

116116
/// Builder for constructing a `ValidationContext`.
117-
#[derive(Default)]
117+
#[derive(Clone, Default)]
118118
pub struct ValidationContextBuilder {
119119
database: Option<Arc<dyn DatabaseValidator>>,
120120
http: Option<Arc<dyn HttpValidator>>,

0 commit comments

Comments
 (0)