feat(PoC): terrors crate usage
This commit is contained in:
13
server/Cargo.lock
generated
13
server/Cargo.lock
generated
@@ -761,6 +761,13 @@ dependencies = [
|
|||||||
"zeroize",
|
"zeroize",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "arbiter-terrors-poc"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"terrors",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "arbiter-tokens-registry"
|
name = "arbiter-tokens-registry"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
@@ -4856,6 +4863,12 @@ dependencies = [
|
|||||||
"windows-sys 0.60.2",
|
"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]]
|
[[package]]
|
||||||
name = "test-log"
|
name = "test-log"
|
||||||
version = "0.2.19"
|
version = "0.2.19"
|
||||||
|
|||||||
@@ -1,5 +1,35 @@
|
|||||||
|
use crate::errors::{InternalError1, InternalError2, InvalidSignature, NotRegistered};
|
||||||
use terrors::OneOf;
|
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,)>> {
|
pub fn verify_signature(_nonce: u32, sig: &str) -> Result<(), OneOf<(InvalidSignature,)>> {
|
||||||
if sig != "ok" {
|
if sig != "ok" {
|
||||||
@@ -8,18 +38,21 @@ pub fn verify_signature(_nonce: u32, sig: &str) -> Result<(), OneOf<(InvalidSign
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn authenticate(
|
type AuthError = OneOf<(
|
||||||
id: u32,
|
NotRegistered,
|
||||||
sig: &str,
|
InvalidSignature,
|
||||||
) -> Result<u32, OneOf<(NotRegistered, InvalidSignature, Internal)>> {
|
InternalError1,
|
||||||
|
InternalError2,
|
||||||
|
)>;
|
||||||
|
|
||||||
|
pub fn authenticate(id: u32, sig: &str) -> Result<u32, AuthError> {
|
||||||
if id == 0 {
|
if id == 0 {
|
||||||
return Err(OneOf::new(NotRegistered));
|
return Err(OneOf::new(NotRegistered));
|
||||||
}
|
}
|
||||||
|
|
||||||
let nonce = crate::db::get_nonce(id)
|
// Return type AuthError lets the compiler infer the broaden target.
|
||||||
.map_err(|e| e.broaden::<(NotRegistered, InvalidSignature, Internal), _>())?;
|
let nonce = crate::db::get_nonce(id).map_err(OneOf::broaden)?;
|
||||||
verify_signature(nonce, sig)
|
verify_signature(nonce, sig).map_err(OneOf::broaden)?;
|
||||||
.map_err(|e| e.broaden::<(NotRegistered, InvalidSignature, Internal), _>())?;
|
|
||||||
|
|
||||||
Ok(nonce)
|
Ok(nonce)
|
||||||
}
|
}
|
||||||
@@ -57,8 +90,50 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn authenticate_internal_error() {
|
fn authenticate_internal_error1() {
|
||||||
let err = authenticate(99, "ok").unwrap_err();
|
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")
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
|
use crate::errors::{InternalError1, InternalError2};
|
||||||
use terrors::OneOf;
|
use terrors::OneOf;
|
||||||
use crate::errors::Internal;
|
|
||||||
|
|
||||||
// Simulates fetching a nonce from a database.
|
// Simulates fetching a nonce from a database.
|
||||||
// id=99 is a sentinel that triggers an Internal error.
|
// id=99 → InternalError1 (pool unavailable)
|
||||||
pub fn get_nonce(id: u32) -> Result<u32, OneOf<(Internal,)>> {
|
// id=98 → InternalError2 (query timeout)
|
||||||
if id == 99 {
|
pub fn get_nonce(id: u32) -> Result<u32, OneOf<(InternalError1, InternalError2)>> {
|
||||||
return Err(OneOf::new(Internal("db pool unavailable".into())));
|
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)]
|
#[cfg(test)]
|
||||||
@@ -20,9 +22,17 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[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 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");
|
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");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ use terrors::OneOf;
|
|||||||
pub enum ProtoError {
|
pub enum ProtoError {
|
||||||
NotRegistered,
|
NotRegistered,
|
||||||
InvalidSignature,
|
InvalidSignature,
|
||||||
Internal(String),
|
Internal(String), // Or Box<dyn Error>, who cares?
|
||||||
}
|
}
|
||||||
|
|
||||||
// Internal terrors types
|
// Internal terrors types
|
||||||
@@ -14,8 +14,11 @@ pub struct NotRegistered;
|
|||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct InvalidSignature;
|
pub struct InvalidSignature;
|
||||||
#[derive(Debug)]
|
#[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 {
|
impl From<NotRegistered> for ProtoError {
|
||||||
fn from(_: NotRegistered) -> Self {
|
fn from(_: NotRegistered) -> Self {
|
||||||
ProtoError::NotRegistered
|
ProtoError::NotRegistered
|
||||||
@@ -28,22 +31,61 @@ impl From<InvalidSignature> for ProtoError {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<Internal> for ProtoError {
|
impl From<InternalError1> for ProtoError {
|
||||||
fn from(e: Internal) -> Self {
|
fn from(e: InternalError1) -> Self {
|
||||||
|
ProtoError::Internal(e.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl From<InternalError2> for ProtoError {
|
||||||
|
fn from(e: InternalError2) -> Self {
|
||||||
ProtoError::Internal(e.0)
|
ProtoError::Internal(e.0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Converts the narrowed remainder after handling NotRegistered
|
/// Private helper trait for converting from OneOf<T...> where each T can be converted
|
||||||
impl From<OneOf<(InvalidSignature, Internal)>> for ProtoError {
|
/// into the target type `O` by recursively narrowing until a match is found.
|
||||||
fn from(e: OneOf<(InvalidSignature, Internal)>) -> Self {
|
///
|
||||||
match e.narrow::<InvalidSignature, _>() {
|
/// IDK why this isn't already in terrors.
|
||||||
Ok(_) => ProtoError::InvalidSignature,
|
trait DrainInto<O>: terrors::TypeSet + Sized {
|
||||||
Err(e) => {
|
fn drain(e: OneOf<Self>) -> O;
|
||||||
let Internal(msg) = e.take();
|
}
|
||||||
ProtoError::Internal(msg)
|
|
||||||
|
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]
|
#[test]
|
||||||
fn internal_converts_to_proto() {
|
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"));
|
assert!(matches!(e, ProtoError::Internal(msg) if msg == "boom"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn one_of_remainder_converts_to_proto_invalid_signature() {
|
fn one_of_remainder_converts_to_proto_invalid_signature() {
|
||||||
use terrors::OneOf;
|
use terrors::OneOf;
|
||||||
let e: OneOf<(InvalidSignature, Internal)> = OneOf::new(InvalidSignature);
|
let e: OneOf<(InvalidSignature, InternalError1)> = OneOf::new(InvalidSignature);
|
||||||
let proto = ProtoError::from(e);
|
let proto = ProtoError::from(e);
|
||||||
assert!(matches!(proto, ProtoError::InvalidSignature));
|
assert!(matches!(proto, ProtoError::InvalidSignature));
|
||||||
}
|
}
|
||||||
@@ -80,7 +122,8 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn one_of_remainder_converts_to_proto_internal() {
|
fn one_of_remainder_converts_to_proto_internal() {
|
||||||
use terrors::OneOf;
|
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);
|
let proto = ProtoError::from(e);
|
||||||
assert!(matches!(proto, ProtoError::Internal(msg) if msg == "db fail"));
|
assert!(matches!(proto, ProtoError::Internal(msg) if msg == "db fail"));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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() {
|
fn main() {
|
||||||
|
println!("=== authenticate ===");
|
||||||
run(0, "ok"); // NotRegistered
|
run(0, "ok"); // NotRegistered
|
||||||
run(1, "bad"); // InvalidSignature
|
run(1, "bad"); // InvalidSignature
|
||||||
run(99, "ok"); // Internal
|
run(99, "ok"); // InternalError1
|
||||||
|
run(98, "ok"); // InternalError2
|
||||||
run(1, "ok"); // success
|
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
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user