diff --git a/server/Cargo.lock b/server/Cargo.lock index 2d0b912..061f1a2 100644 --- a/server/Cargo.lock +++ b/server/Cargo.lock @@ -761,6 +761,13 @@ dependencies = [ "zeroize", ] +[[package]] +name = "arbiter-terrors-poc" +version = "0.1.0" +dependencies = [ + "terrors", +] + [[package]] name = "arbiter-tokens-registry" version = "0.1.0" @@ -4856,6 +4863,12 @@ dependencies = [ "windows-sys 0.60.2", ] +[[package]] +name = "terrors" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "987fd8c678ca950df2a18b2c6e9da6ca511d449278fab3565efe0d49c0c07a5d" + [[package]] name = "test-log" version = "0.2.19" diff --git a/server/crates/arbiter-terrors-poc/src/auth.rs b/server/crates/arbiter-terrors-poc/src/auth.rs index d4e352a..b9f6148 100644 --- a/server/crates/arbiter-terrors-poc/src/auth.rs +++ b/server/crates/arbiter-terrors-poc/src/auth.rs @@ -1,5 +1,35 @@ +use crate::errors::{InternalError1, InternalError2, InvalidSignature, NotRegistered}; use terrors::OneOf; -use crate::errors::{Internal, InvalidSignature, NotRegistered}; + +use crate::errors::ProtoError; + +// Each sub-call's error type already implements DrainInto, so we convert +// directly to ProtoError without broaden — no turbofish needed anywhere. +// +// Call chain: +// load_config() → OneOf<(InternalError2,)> → ProtoError::from +// get_nonce() → OneOf<(InternalError1, InternalError2)> → ProtoError::from +// verify_sig() → OneOf<(InvalidSignature,)> → ProtoError::from +pub fn process_request(id: u32, sig: &str) -> Result { + if id == 0 { + return Err(ProtoError::NotRegistered); + } + + let config = load_config(id).map_err(ProtoError::from)?; + let nonce = crate::db::get_nonce(id).map_err(ProtoError::from)?; + verify_signature(nonce, sig).map_err(ProtoError::from)?; + + Ok(format!("config={config} nonce={nonce} sig={sig}")) +} + +// Simulates loading a config value. +// id=97 triggers InternalError2 ("config read failed"). +fn load_config(id: u32) -> Result> { + if id == 97 { + return Err(OneOf::new(InternalError2("config read failed".to_owned()))); + } + Ok(format!("cfg-{id}")) +} pub fn verify_signature(_nonce: u32, sig: &str) -> Result<(), OneOf<(InvalidSignature,)>> { if sig != "ok" { @@ -8,18 +38,21 @@ pub fn verify_signature(_nonce: u32, sig: &str) -> Result<(), OneOf<(InvalidSign Ok(()) } -pub fn authenticate( - id: u32, - sig: &str, -) -> Result> { +type AuthError = OneOf<( + NotRegistered, + InvalidSignature, + InternalError1, + InternalError2, +)>; + +pub fn authenticate(id: u32, sig: &str) -> Result { if id == 0 { return Err(OneOf::new(NotRegistered)); } - let nonce = crate::db::get_nonce(id) - .map_err(|e| e.broaden::<(NotRegistered, InvalidSignature, Internal), _>())?; - verify_signature(nonce, sig) - .map_err(|e| e.broaden::<(NotRegistered, InvalidSignature, Internal), _>())?; + // Return type AuthError lets the compiler infer the broaden target. + let nonce = crate::db::get_nonce(id).map_err(OneOf::broaden)?; + verify_signature(nonce, sig).map_err(OneOf::broaden)?; Ok(nonce) } @@ -57,8 +90,50 @@ mod tests { } #[test] - fn authenticate_internal_error() { + fn authenticate_internal_error1() { let err = authenticate(99, "ok").unwrap_err(); - assert!(err.narrow::().is_ok()); + assert!(err.narrow::().is_ok()); + } + + #[test] + fn authenticate_internal_error2() { + let err = authenticate(98, "ok").unwrap_err(); + assert!(err.narrow::().is_ok()); + } + + #[test] + fn process_request_success() { + let result = process_request(1, "ok").unwrap(); + assert!(result.contains("nonce=42")); + } + + #[test] + fn process_request_not_registered() { + let err = process_request(0, "ok").unwrap_err(); + assert!(matches!(err, crate::errors::ProtoError::NotRegistered)); + } + + #[test] + fn process_request_invalid_signature() { + let err = process_request(1, "bad").unwrap_err(); + assert!(matches!(err, crate::errors::ProtoError::InvalidSignature)); + } + + #[test] + fn process_request_internal_from_config() { + // id=97 → load_config returns InternalError2 + let err = process_request(97, "ok").unwrap_err(); + assert!( + matches!(err, crate::errors::ProtoError::Internal(ref msg) if msg == "config read failed") + ); + } + + #[test] + fn process_request_internal_from_db() { + // id=99 → get_nonce returns InternalError1 + let err = process_request(99, "ok").unwrap_err(); + assert!( + matches!(err, crate::errors::ProtoError::Internal(ref msg) if msg == "db pool unavailable") + ); } } diff --git a/server/crates/arbiter-terrors-poc/src/db.rs b/server/crates/arbiter-terrors-poc/src/db.rs index b025505..c2c4c5a 100644 --- a/server/crates/arbiter-terrors-poc/src/db.rs +++ b/server/crates/arbiter-terrors-poc/src/db.rs @@ -1,13 +1,15 @@ +use crate::errors::{InternalError1, InternalError2}; use terrors::OneOf; -use crate::errors::Internal; // Simulates fetching a nonce from a database. -// id=99 is a sentinel that triggers an Internal error. -pub fn get_nonce(id: u32) -> Result> { - if id == 99 { - return Err(OneOf::new(Internal("db pool unavailable".into()))); +// id=99 → InternalError1 (pool unavailable) +// id=98 → InternalError2 (query timeout) +pub fn get_nonce(id: u32) -> Result> { + match id { + 99 => Err(OneOf::new(InternalError1("db pool unavailable".to_owned()))), + 98 => Err(OneOf::new(InternalError2("query timeout".to_owned()))), + _ => Ok(42), } - Ok(42) } #[cfg(test)] @@ -20,9 +22,17 @@ mod tests { } #[test] - fn get_nonce_returns_internal_error_for_sentinel() { + fn get_nonce_returns_internal_error1_for_sentinel() { let err = get_nonce(99).unwrap_err(); - let internal = err.take::(); + let internal = err.narrow::().unwrap(); assert_eq!(internal.0, "db pool unavailable"); } + + #[test] + fn get_nonce_returns_internal_error2_for_sentinel() { + let err = get_nonce(98).unwrap_err(); + let e = err.narrow::().unwrap_err(); + let internal = e.take::(); + assert_eq!(internal.0, "query timeout"); + } } diff --git a/server/crates/arbiter-terrors-poc/src/errors.rs b/server/crates/arbiter-terrors-poc/src/errors.rs index b49d15b..cb436e1 100644 --- a/server/crates/arbiter-terrors-poc/src/errors.rs +++ b/server/crates/arbiter-terrors-poc/src/errors.rs @@ -5,7 +5,7 @@ use terrors::OneOf; pub enum ProtoError { NotRegistered, InvalidSignature, - Internal(String), + Internal(String), // Or Box, who cares? } // Internal terrors types @@ -14,8 +14,11 @@ pub struct NotRegistered; #[derive(Debug)] pub struct InvalidSignature; #[derive(Debug)] -pub struct Internal(pub String); +pub struct InternalError1(pub String); +#[derive(Debug)] +pub struct InternalError2(pub String); +// Errors can be scattered across the codebase as long as they implement Into impl From for ProtoError { fn from(_: NotRegistered) -> Self { ProtoError::NotRegistered @@ -28,22 +31,61 @@ impl From for ProtoError { } } -impl From for ProtoError { - fn from(e: Internal) -> Self { +impl From for ProtoError { + fn from(e: InternalError1) -> Self { + ProtoError::Internal(e.0) + } +} +impl From for ProtoError { + fn from(e: InternalError2) -> Self { ProtoError::Internal(e.0) } } -// Converts the narrowed remainder after handling NotRegistered -impl From> for ProtoError { - fn from(e: OneOf<(InvalidSignature, Internal)>) -> Self { - match e.narrow::() { - Ok(_) => ProtoError::InvalidSignature, - Err(e) => { - let Internal(msg) = e.take(); - ProtoError::Internal(msg) +/// Private helper trait for converting from OneOf where each T can be converted +/// into the target type `O` by recursively narrowing until a match is found. +/// +/// IDK why this isn't already in terrors. +trait DrainInto: terrors::TypeSet + Sized { + fn drain(e: OneOf) -> O; +} + +macro_rules! impl_drain_into { + ($head:ident) => { + impl<$head, O> DrainInto for ($head,) + where + $head: Into + 'static, + { + fn drain(e: OneOf<($head,)>) -> O { + e.take().into() } } + }; + ($head:ident, $($tail:ident),+) => { + impl<$head, $($tail),+, O> DrainInto for ($head, $($tail),+) + where + $head: Into + 'static, + ($($tail,)+): DrainInto, + { + fn drain(e: OneOf<($head, $($tail),+)>) -> O { + match e.narrow::<$head, _>() { + Ok(h) => h.into(), + Err(rest) => <($($tail,)+)>::drain(rest), + } + } + } + impl_drain_into!($($tail),+); + }; +} + +// Generates impls for all tuple sizes from 1 up to 7 (restricted by terrors internal impl). +// Each invocation produces one impl then recurses on the tail. +impl_drain_into!(A, B, C, D, E, F, G, H, I); + +// Blanket From impl: body delegates to the recursive drain. +impl> From> for ProtoError { + fn from(e: OneOf) -> Self { + E::drain(e) } } @@ -65,14 +107,14 @@ mod tests { #[test] fn internal_converts_to_proto() { - let e: ProtoError = Internal("boom".into()).into(); + let e: ProtoError = InternalError1("boom".into()).into(); assert!(matches!(e, ProtoError::Internal(msg) if msg == "boom")); } #[test] fn one_of_remainder_converts_to_proto_invalid_signature() { use terrors::OneOf; - let e: OneOf<(InvalidSignature, Internal)> = OneOf::new(InvalidSignature); + let e: OneOf<(InvalidSignature, InternalError1)> = OneOf::new(InvalidSignature); let proto = ProtoError::from(e); assert!(matches!(proto, ProtoError::InvalidSignature)); } @@ -80,7 +122,8 @@ mod tests { #[test] fn one_of_remainder_converts_to_proto_internal() { use terrors::OneOf; - let e: OneOf<(InvalidSignature, Internal)> = OneOf::new(Internal("db fail".into())); + let e: OneOf<(InvalidSignature, InternalError1)> = + OneOf::new(InternalError1("db fail".into())); let proto = ProtoError::from(e); assert!(matches!(proto, ProtoError::Internal(msg) if msg == "db fail")); } diff --git a/server/crates/arbiter-terrors-poc/src/main.rs b/server/crates/arbiter-terrors-poc/src/main.rs index d8e2f7a..f17e180 100644 --- a/server/crates/arbiter-terrors-poc/src/main.rs +++ b/server/crates/arbiter-terrors-poc/src/main.rs @@ -18,9 +18,26 @@ fn run(id: u32, sig: &str) { } } -fn main() { - run(0, "ok"); // NotRegistered - run(1, "bad"); // InvalidSignature - run(99, "ok"); // Internal - run(1, "ok"); // success +fn run_process(id: u32, sig: &str) { + print!("process_request(id={id}, sig={sig:?}) => "); + match auth::process_request(id, sig) { + Ok(s) => println!("Ok({s})"), + Err(e) => println!("Err(ProtoError::{e:?})"), + } +} + +fn main() { + println!("=== authenticate ==="); + run(0, "ok"); // NotRegistered + run(1, "bad"); // InvalidSignature + run(99, "ok"); // InternalError1 + run(98, "ok"); // InternalError2 + run(1, "ok"); // success + + println!("\n=== process_request (Try chain) ==="); + run_process(0, "ok"); // NotRegistered (guard, no I/O) + run_process(97, "ok"); // InternalError2 from load_config + run_process(99, "ok"); // InternalError1 from get_nonce + run_process(1, "bad"); // InvalidSignature from verify_signature + run_process(1, "ok"); // success }