diff --git a/Cargo.lock b/Cargo.lock index a1f65cc..ac82fbe 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,12 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + [[package]] name = "bitflags" version = "2.10.0" @@ -18,6 +24,7 @@ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" name = "dps-config" version = "0.10.1" dependencies = [ + "anyhow", "serde_json", "serial_test", ] diff --git a/Cargo.toml b/Cargo.toml index ebc7c2d..d22f9df 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,6 +21,7 @@ path = "src/lib.rs" crate-type = ["lib"] [dependencies] +anyhow = "1" serde_json = "1" [dev-dependencies] diff --git a/README.md b/README.md index 564e63e..0dce7f7 100644 --- a/README.md +++ b/README.md @@ -126,8 +126,8 @@ These properties are only available in the Rust implementation. The Bun/TypeScri | Property | Default | Description | |----------|---------|-------------| -| `session_sub_to_user_id_fn` | Parses string as i64, returns 0 on failure | Function that converts a session `sub` string to an `i64` user ID | -| `session_user_to_sub_fn` | Returns `id` property as string | Function that extracts a `sub` string from a JSON record (`serde_json::Value`) | +| `session_sub_to_user_id_fn` | Parses string as i64, returns parse error on failure | Function that converts a session `sub` string to an `i64` user ID. Returns `anyhow::Result`. | +| `session_user_to_sub_fn` | Returns `id` property as string, error on missing/invalid | Function that extracts a `sub` string from a JSON record (`serde_json::Value`). Returns `anyhow::Result`. | Example usage: @@ -136,17 +136,35 @@ let mut config = DpsConfig::new(); // Use default: parses sub as i64 let to_user_id = config.get_session_sub_to_user_id_fn(); -assert_eq!(to_user_id("42"), 42); +assert_eq!(to_user_id("42").unwrap(), 42); // Custom converter: use length of sub as user ID -config.set_session_sub_to_user_id_fn(|sub| sub.len() as i64); +config.set_session_sub_to_user_id_fn(|sub| Ok(sub.len() as i64)); // Custom sub extractor from JSON record config.set_session_user_to_sub_fn(|record| { - record.get("sub").and_then(|v| v.as_str()).unwrap_or("").to_string() + record.get("sub").and_then(|v| v.as_str()).map(|s| s.to_string()).ok_or_else(|| anyhow::anyhow!("missing 'sub'")) }); ``` +#### Overriding from a consuming crate + +Since the functions return `anyhow::Result`, you can use `anyhow`'s convenience macros for quick error creation. + +```rust +use dps_config::DpsConfig; + +fn configure_with_custom_errors(config: &mut DpsConfig) { + // Using anyhow::bail!() for convenient error creation + config.set_session_sub_to_user_id_fn(|sub| { + if sub == "invalid" { + anyhow::bail!("user not found: {}", sub); + } + Ok(42) + }); +} +``` + ## Computed Getters Computed getters derive values from base properties and have no setters or environment variables. diff --git a/docs/llm/plans/2026/05/21-add-session-sub-to-user-id-and-session-user-to-sub-fns.md b/docs/llm/plans/2026/05/21-add-session-sub-to-user-id-and-session-user-to-sub-fns.md index 73d8147..b802e1f 100644 --- a/docs/llm/plans/2026/05/21-add-session-sub-to-user-id-and-session-user-to-sub-fns.md +++ b/docs/llm/plans/2026/05/21-add-session-sub-to-user-id-and-session-user-to-sub-fns.md @@ -4,8 +4,8 @@ Add two new function-typed properties to the Rust `DpsConfig` struct: -1. **`session_sub_to_user_id_fn`** — A function that takes a `&str` (session sub) and returns `i64` (user ID). Default: identity function that parses the string as `i64`. -2. **`session_user_to_sub_fn`** — A function that takes a JSON-like record (`serde_json::Value`) and returns a `String` (the sub). Default: returns the `id` property of the record as a string. +1. **`session_sub_to_user_id_fn`** — A function that takes a `&str` (session sub) and returns `Result>`. Default: parses the string as `i64`, returning a boxed parse error on failure. +2. **`session_user_to_sub_fn`** — A function that takes a JSON-like record (`serde_json::Value`) and returns `Result>` (the sub). Default: returns the `id` property of the record as a string, or an error if `id` is missing or invalid. These are Rust-only features. The Bun/TypeScript implementation remains unchanged. @@ -13,94 +13,95 @@ These are Rust-only features. The Bun/TypeScript implementation remains unchange ### 1. Add fields to `DpsConfig` struct (`src/lib.rs`) -Add two new fields using `Box` to store callable functions. These fields are **non-optional** internally, ensuring they always hold a valid function. +Add two new fields using `Box` to store callable functions. These fields are **non-optional** internally, ensuring they always hold a valid function. The error type for `session_sub_to_user_id_fn` uses `Box` to allow callers to return any error type. ```rust pub struct DpsConfig { // ... existing fields ... // Session conversion functions - session_sub_to_user_id_fn: Box i64 + Send + Sync>, - session_user_to_sub_fn: Box String + Send + Sync>, + session_sub_to_user_id_fn: Box Result> + Send + Sync>, + session_user_to_sub_fn: Box Result> + Send + Sync>, } ``` Note: `Send + Sync` bounds ensure the functions can be used across threads safely. -### 2. Initialize defaults in `DpsConfig::new()` +### 3. Initialize defaults in `DpsConfig::new()` ```rust Self { // ... existing initializations ... session_sub_to_user_id_fn: Box::new(|sub: &str| { - sub.parse::().unwrap_or(0) + sub.parse::().map_err(|e| Box::new(e) as Box) }), session_user_to_sub_fn: Box::new(|record: &serde_json::Value| { record .get("id") .and_then(|v| { if let Some(n) = v.as_u64() { - Some(n.to_string()) + Some(Ok(n.to_string())) } else { - v.as_str().map(|s| s.to_string()) + v.as_str().map(|s| Ok(s.to_string())) } }) - .unwrap_or_default() + .unwrap_or_else(|| Err("missing or invalid 'id' field".into())) }), } ``` -### 3. Add getters and setters +### 4. Add getters and setters ```rust // session_sub_to_user_id_fn -pub fn get_session_sub_to_user_id_fn(&self) -> &dyn Fn(&str) -> i64 { +pub fn get_session_sub_to_user_id_fn(&self) -> &dyn Fn(&str) -> Result> { self.session_sub_to_user_id_fn.as_ref() } -pub fn set_session_sub_to_user_id_fn(&mut self, f: impl Fn(&str) -> i64 + Send + Sync + 'static) { +pub fn set_session_sub_to_user_id_fn( + &mut self, + f: impl Fn(&str) -> Result> + Send + Sync + 'static, +) { self.session_sub_to_user_id_fn = Box::new(f); } // session_user_to_sub_fn -pub fn get_session_user_to_sub_fn(&self) -> &dyn Fn(&serde_json::Value) -> String { +pub fn get_session_user_to_sub_fn(&self) -> &dyn Fn(&serde_json::Value) -> Result> { self.session_user_to_sub_fn.as_ref() } pub fn set_session_user_to_sub_fn( &mut self, - f: impl Fn(&serde_json::Value) -> String + Send + Sync + 'static, + f: impl Fn(&serde_json::Value) -> Result> + Send + Sync + 'static, ) { self.session_user_to_sub_fn = Box::new(f); } ``` -### 4. Add `serde_json` dependency to `Cargo.toml` +### 5. Add dependencies to `Cargo.toml` ```toml [dependencies] serde_json = "1" ``` -### 5. Add tests +### 6. Add tests ```rust #[test] fn test_session_sub_to_user_id_fn_default() { let config = DpsConfig::new(); let to_user_id = config.get_session_sub_to_user_id_fn(); - assert_eq!(to_user_id("12345"), 12345); - assert_eq!(to_user_id("invalid"), 0); + assert_eq!(to_user_id("12345").unwrap(), 12345); + assert!(to_user_id("invalid").is_err()); } #[test] fn test_session_sub_to_user_id_fn_custom() { let mut config = DpsConfig::new(); - config.set_session_sub_to_user_id_fn(|sub| { - sub.len() as i64 - }); - assert_eq!(config.get_session_sub_to_user_id_fn()("hello"), 5); + config.set_session_sub_to_user_id_fn(|sub| Ok(sub.len() as i64)); + assert_eq!(config.get_session_sub_to_user_id_fn()("hello").unwrap(), 5); } #[test] @@ -108,21 +109,22 @@ fn test_session_user_to_sub_fn_default() { let config = DpsConfig::new(); let to_sub = config.get_session_user_to_sub_fn(); let record = serde_json::json!({ "id": 42, "name": "test" }); - assert_eq!(to_sub(&record), "42"); + assert_eq!(to_sub(&record).unwrap(), "42"); + assert!(to_sub(&serde_json::json!({})).is_err()); } #[test] fn test_session_user_to_sub_fn_custom() { let mut config = DpsConfig::new(); config.set_session_user_to_sub_fn(|record| { - record.get("sub").and_then(|v| v.as_str()).unwrap_or("").to_string() + record.get("sub").and_then(|v| v.as_str()).map(|s| s.to_string()).ok_or("missing 'sub'".into()) }); let record = serde_json::json!({ "sub": "user-123", "name": "test" }); - assert_eq!(config.get_session_user_to_sub_fn()(&record), "user-123"); + assert_eq!(config.get_session_user_to_sub_fn()(&record).unwrap(), "user-123"); } ``` -### 6. Update README.md +### 7. Update README.md Add a new section under Configuration Properties noting these are Rust-only: @@ -133,8 +135,8 @@ These properties are only available in the Rust implementation. The Bun/TypeScri | Property | Default | Description | |----------|---------|-------------| -| `session_sub_to_user_id_fn` | Parses string as i64, returns 0 on failure | Function that converts a session `sub` string to an `i64` user ID | -| `session_user_to_sub_fn` | Returns `id` property as string | Function that extracts a `sub` string from a JSON record (`serde_json::Value`) | +| `session_sub_to_user_id_fn` | Parses string as i64, returns parse error on failure | Function that converts a session `sub` string to an `i64` user ID. Returns `Result>` to allow custom error types. | +| `session_user_to_sub_fn` | Returns `id` property as string, error on missing/invalid | Function that extracts a `sub` string from a JSON record (`serde_json::Value`). Returns `Result>` for consistency. | Example usage: @@ -143,23 +145,48 @@ let mut config = DpsConfig::new(); // Use default: parses sub as i64 let to_user_id = config.get_session_sub_to_user_id_fn(); -assert_eq!(to_user_id("42"), 42); +assert_eq!(to_user_id("42").unwrap(), 42); // Custom converter: use length of sub as user ID -config.set_session_sub_to_user_id_fn(|sub| sub.len() as i64); +config.set_session_sub_to_user_id_fn(|sub| Ok(sub.len() as i64)); // Custom sub extractor from JSON record config.set_session_user_to_sub_fn(|record| { - record.get("sub").and_then(|v| v.as_str()).unwrap_or("").to_string() + record.get("sub").and_then(|v| v.as_str()).map(|s| s.to_string()).ok_or("missing 'sub'".into()) }); ``` + +#### Overriding from a consuming crate + +Since the functions return `Result<_, Box>`, you can return your own error types (e.g., using `thiserror` or `anyhow`) without conversion. + +```rust +use dps_config::DpsConfig; + +#[derive(thiserror::Error, Debug)] +enum MyUserError { + #[error("user not found: {0}")] + NotFound(String), +} + +fn configure_with_custom_errors(config: &mut DpsConfig) { + // Returning a custom thiserror type directly + config.set_session_sub_to_user_id_fn(|sub| { + if sub == "invalid" { + Err(Box::new(MyUserError::NotFound(sub.to_string()))) + } else { + Ok(42) + } + }); +} +``` ``` ## Files to Modify 1. **`Cargo.toml`** — Add `serde_json = "1"` dependency 2. **`src/lib.rs`** — Add struct fields, defaults in `new()`, getters/setters, tests -3. **`README.md`** — Document new properties with Rust-only note +3. **`README.md`** — Document new properties with Rust-only note and consuming crate example ## Order of Work diff --git a/src/lib.rs b/src/lib.rs index 95bfe01..c01ae3a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -38,6 +38,7 @@ use std::env; /// /// Note: This struct intentionally does not perform validation — consuming /// crates should validate values where required. +#[allow(clippy::type_complexity)] pub struct DpsConfig { // Global properties project_name: Option, @@ -60,8 +61,8 @@ pub struct DpsConfig { auth_api_session_ttl_seconds: Option, // Session conversion functions - session_sub_to_user_id_fn: Box i64 + Send + Sync>, - session_user_to_sub_fn: Box String + Send + Sync>, + session_sub_to_user_id_fn: Box anyhow::Result + Send + Sync>, + session_user_to_sub_fn: Box anyhow::Result + Send + Sync>, } impl DpsConfig { @@ -106,18 +107,20 @@ impl DpsConfig { auth_api_sqlite_session_pool_size: load_env_u16("DPS_AUTH_API_SQLITE_SESSION_POOL_SIZE"), auth_api_session_secret: load_env_string("DPS_AUTH_API_SESSION_SECRET"), auth_api_session_ttl_seconds: load_env_u32("DPS_AUTH_API_SESSION_TTL_SECONDS"), - session_sub_to_user_id_fn: Box::new(|sub: &str| sub.parse::().unwrap_or(0)), + session_sub_to_user_id_fn: Box::new(|sub: &str| { + sub.parse::().map_err(anyhow::Error::new) + }), session_user_to_sub_fn: Box::new(|record: &serde_json::Value| { record .get("id") .and_then(|v| { if let Some(n) = v.as_u64() { - Some(n.to_string()) + Some(Ok(n.to_string())) } else { - v.as_str().map(|s| s.to_string()) + v.as_str().map(|s| Ok(s.to_string())) } }) - .unwrap_or_default() + .unwrap_or_else(|| Err(anyhow::anyhow!("missing or invalid 'id' field"))) }), } } @@ -371,27 +374,33 @@ impl DpsConfig { /// Returns the function that converts a session `sub` string to an `i64` user ID. /// - /// Default: Parses the string as `i64`, returning `0` on failure. - pub fn get_session_sub_to_user_id_fn(&self) -> &dyn Fn(&str) -> i64 { + /// Default: Parses the string as `i64`, returning a parse error on failure. + pub fn get_session_sub_to_user_id_fn(&self) -> &dyn Fn(&str) -> anyhow::Result { self.session_sub_to_user_id_fn.as_ref() } /// Set the session sub to user ID conversion function. - pub fn set_session_sub_to_user_id_fn(&mut self, f: impl Fn(&str) -> i64 + Send + Sync + 'static) { + pub fn set_session_sub_to_user_id_fn( + &mut self, + f: impl Fn(&str) -> anyhow::Result + Send + Sync + 'static, + ) { self.session_sub_to_user_id_fn = Box::new(f); } /// Returns the function that extracts a `sub` string from a JSON record. /// - /// Default: Returns the `id` property of the record as a string. - pub fn get_session_user_to_sub_fn(&self) -> &dyn Fn(&serde_json::Value) -> String { + /// Default: Returns the `id` property of the record as a string, or an error + /// if `id` is missing or invalid. + pub fn get_session_user_to_sub_fn( + &self, + ) -> &dyn Fn(&serde_json::Value) -> anyhow::Result { self.session_user_to_sub_fn.as_ref() } /// Set the session user to sub conversion function. pub fn set_session_user_to_sub_fn( &mut self, - f: impl Fn(&serde_json::Value) -> String + Send + Sync + 'static, + f: impl Fn(&serde_json::Value) -> anyhow::Result + Send + Sync + 'static, ) { self.session_user_to_sub_fn = Box::new(f); } @@ -754,15 +763,15 @@ mod tests { fn test_session_sub_to_user_id_fn_default() { let config = DpsConfig::new(); let to_user_id = config.get_session_sub_to_user_id_fn(); - assert_eq!(to_user_id("12345"), 12345); - assert_eq!(to_user_id("invalid"), 0); + assert_eq!(to_user_id("12345").unwrap(), 12345); + assert!(to_user_id("invalid").is_err()); } #[test] fn test_session_sub_to_user_id_fn_custom() { let mut config = DpsConfig::new(); - config.set_session_sub_to_user_id_fn(|sub| sub.len() as i64); - assert_eq!(config.get_session_sub_to_user_id_fn()("hello"), 5); + config.set_session_sub_to_user_id_fn(|sub| Ok(sub.len() as i64)); + assert_eq!(config.get_session_sub_to_user_id_fn()("hello").unwrap(), 5); } #[test] @@ -770,7 +779,8 @@ mod tests { let config = DpsConfig::new(); let to_sub = config.get_session_user_to_sub_fn(); let record = serde_json::json!({ "id": 42, "name": "test" }); - assert_eq!(to_sub(&record), "42"); + assert_eq!(to_sub(&record).unwrap(), "42"); + assert!(to_sub(&serde_json::json!({})).is_err()); } #[test] @@ -780,10 +790,13 @@ mod tests { record .get("sub") .and_then(|v| v.as_str()) - .unwrap_or("") - .to_string() + .map(|s| s.to_string()) + .ok_or_else(|| anyhow::anyhow!("missing 'sub'")) }); let record = serde_json::json!({ "sub": "user-123", "name": "test" }); - assert_eq!(config.get_session_user_to_sub_fn()(&record), "user-123"); + assert_eq!( + config.get_session_user_to_sub_fn()(&record).unwrap(), + "user-123" + ); } }