feat(PoC): terrors crate usage
Some checks failed
ci/woodpecker/pr/server-lint Pipeline failed
ci/woodpecker/pr/server-audit Pipeline was successful
ci/woodpecker/pr/server-vet Pipeline failed
ci/woodpecker/pr/server-test Pipeline was successful

This commit is contained in:
CleverWild
2026-03-15 21:11:23 +01:00
parent 66026e903a
commit 099f76166e
5 changed files with 197 additions and 39 deletions

13
server/Cargo.lock generated
View File

@@ -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"

View File

@@ -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<ProtoError>, 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<String, ProtoError> {
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<String, OneOf<(InternalError2,)>> {
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<u32, OneOf<(NotRegistered, InvalidSignature, Internal)>> {
type AuthError = OneOf<(
NotRegistered,
InvalidSignature,
InternalError1,
InternalError2,
)>;
pub fn authenticate(id: u32, sig: &str) -> Result<u32, AuthError> {
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::<crate::errors::Internal, _>().is_ok());
assert!(err.narrow::<crate::errors::InternalError1, _>().is_ok());
}
#[test]
fn authenticate_internal_error2() {
let err = authenticate(98, "ok").unwrap_err();
assert!(err.narrow::<crate::errors::InternalError2, _>().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")
);
}
}

View File

@@ -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<u32, OneOf<(Internal,)>> {
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<u32, OneOf<(InternalError1, InternalError2)>> {
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::<crate::errors::Internal>();
let internal = err.narrow::<crate::errors::InternalError1, _>().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::<crate::errors::InternalError1, _>().unwrap_err();
let internal = e.take::<crate::errors::InternalError2>();
assert_eq!(internal.0, "query timeout");
}
}

View File

@@ -5,7 +5,7 @@ use terrors::OneOf;
pub enum ProtoError {
NotRegistered,
InvalidSignature,
Internal(String),
Internal(String), // Or Box<dyn Error>, 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<ProtoError>
impl From<NotRegistered> for ProtoError {
fn from(_: NotRegistered) -> Self {
ProtoError::NotRegistered
@@ -28,22 +31,61 @@ impl From<InvalidSignature> for ProtoError {
}
}
impl From<Internal> for ProtoError {
fn from(e: Internal) -> Self {
impl From<InternalError1> for ProtoError {
fn from(e: InternalError1) -> Self {
ProtoError::Internal(e.0)
}
}
impl From<InternalError2> for ProtoError {
fn from(e: InternalError2) -> Self {
ProtoError::Internal(e.0)
}
}
// Converts the narrowed remainder after handling NotRegistered
impl From<OneOf<(InvalidSignature, Internal)>> for ProtoError {
fn from(e: OneOf<(InvalidSignature, Internal)>) -> Self {
match e.narrow::<InvalidSignature, _>() {
Ok(_) => ProtoError::InvalidSignature,
Err(e) => {
let Internal(msg) = e.take();
ProtoError::Internal(msg)
/// Private helper trait for converting from OneOf<T...> 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<O>: terrors::TypeSet + Sized {
fn drain(e: OneOf<Self>) -> O;
}
macro_rules! impl_drain_into {
($head:ident) => {
impl<$head, O> DrainInto<O> for ($head,)
where
$head: Into<O> + 'static,
{
fn drain(e: OneOf<($head,)>) -> O {
e.take().into()
}
}
};
($head:ident, $($tail:ident),+) => {
impl<$head, $($tail),+, O> DrainInto<O> for ($head, $($tail),+)
where
$head: Into<O> + 'static,
($($tail,)+): DrainInto<O>,
{
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<E: DrainInto<ProtoError>> From<OneOf<E>> for ProtoError {
fn from(e: OneOf<E>) -> 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"));
}

View File

@@ -18,9 +18,26 @@ fn run(id: u32, sig: &str) {
}
}
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"); // Internal
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
}