애플리케이션 에러 처리 문서의 검증 코드다.
//! anyhow로 하는 애플리케이션 에러 처리: Context/with_context, bail!, ensure!,
//! {} / {:#} / {:?} 출력 형식, chain()과 root_cause(), downcast_ref로 라이브러리 에러 되찾기,
//! 그리고 HTTP 상태 코드처럼 "종류별로 다르게 처리"해야 할 때의 경계를 확인한다.
//! 의존성: anyhow = "1", thiserror = "2"
//! 실행: python3 "_시스템/Rust 계획/rs_check.py" "Rust/3.개념집/25_application-errors_verify.rs"
use anyhow::{Context, Result, bail, ensure};
/// 라이브러리 쪽 에러(24번 문서처럼 thiserror로 만든 것)라고 생각한다.
#[derive(Debug, thiserror::Error)]
enum StoreError {
#[error("record {0} not found")]
NotFound(u32),
#[error("store is read-only")]
ReadOnly,
}
fn store_get(id: u32) -> Result<String, StoreError> {
match id {
1 => Ok("alice".into()),
2 => Err(StoreError::ReadOnly),
_ => Err(StoreError::NotFound(id)),
}
}
fn parse_id(raw: &str) -> Result<u32> {
let id: u32 = raw
.trim()
.parse()
.with_context(|| format!("invalid id {raw:?}"))?;
ensure!(id != 0, "id must be positive");
Ok(id)
}
fn handle(raw: &str) -> Result<String> {
if raw.is_empty() {
bail!("empty request");
}
let id = parse_id(raw).context("bad request")?;
let name = store_get(id).with_context(|| format!("failed to load user {id}"))?;
Ok(name)
}
/// 애플리케이션 경계(예: HTTP 핸들러)에서 종류별로 상태 코드를 고른다.
fn status_of(err: &anyhow::Error) -> u16 {
// chain 전체에서 라이브러리 에러를 찾는다. context로 감싸도 찾을 수 있다.
for cause in err.chain() {
if let Some(se) = cause.downcast_ref::<StoreError>() {
return match se {
StoreError::NotFound(_) => 404,
StoreError::ReadOnly => 503,
};
}
if cause.downcast_ref::<std::num::ParseIntError>().is_some() {
return 400;
}
}
500
}
fn main() {
assert_eq!(handle("1").unwrap(), "alice");
let e = handle("abc").unwrap_err();
let plain = format!("{e}");
let alt = format!("{e:#}");
let dbg = format!("{e:?}");
println!("{{}} => {plain}");
println!("{{:#}} => {alt}");
println!("{{:?}} =>\n{dbg}");
assert_eq!(plain, "bad request");
assert_eq!(
alt,
"bad request: invalid id \"abc\": invalid digit found in string"
);
// {:?}는 맨 바깥 메시지, 빈 줄, "Caused by:" 목록. (RUST_BACKTRACE가 없으면 백트레이스는 없다)
assert!(dbg.starts_with("bad request\n\nCaused by:\n"), "{dbg}");
assert!(dbg.contains(" 0: invalid id \"abc\"\n"), "{dbg}");
assert!(
dbg.contains(" 1: invalid digit found in string"),
"{dbg}"
);
let causes: Vec<String> = e.chain().map(|c| c.to_string()).collect();
assert_eq!(
causes,
vec![
"bad request",
"invalid id \"abc\"",
"invalid digit found in string"
]
);
assert_eq!(e.root_cause().to_string(), "invalid digit found in string");
// bail!, ensure!
assert_eq!(handle("").unwrap_err().to_string(), "empty request");
assert_eq!(
format!("{:#}", handle("0").unwrap_err()),
"bad request: id must be positive"
);
// context를 덧대도 원래 라이브러리 에러를 되찾을 수 있다
let e = handle("7").unwrap_err();
assert_eq!(
format!("{e:#}"),
"failed to load user 7: record 7 not found"
);
assert!(e.downcast_ref::<StoreError>().is_some());
assert!(matches!(
e.downcast_ref::<StoreError>(),
Some(StoreError::NotFound(7))
));
// 경계에서 상태 코드 고르기
assert_eq!(status_of(&handle("7").unwrap_err()), 404);
assert_eq!(status_of(&handle("2").unwrap_err()), 503);
assert_eq!(status_of(&handle("x").unwrap_err()), 400);
assert_eq!(status_of(&handle("").unwrap_err()), 500); // bail!은 문자열뿐이라 종류를 모른다
assert_eq!(status_of(&handle("0").unwrap_err()), 500); // ensure!도 마찬가지
println!("상태 코드: 404, 503, 400, 500, 500");
// anyhow::Error는 Send + Sync + 'static
fn needs<T: Send + Sync + 'static>() {}
needs::<anyhow::Error>();
println!("ALL CHECKS PASSED");
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn context_does_not_hide_source() {
let e = handle("9").unwrap_err();
assert_eq!(e.chain().count(), 2);
assert_eq!(status_of(&e), 404);
}
}