물음표 연산자와 에러 전파 문서의 검증 코드다.

//! `?`가 하는 일(From 변환, Option의 None 전파), 직접 쓴 match와 같은 결과, Box<dyn Error + Send + Sync>,
//! source() 사슬, main이 Err를 돌려줄 때의 종료 코드와 출력, 그리고 컴파일 오류(E0277)를 확인한다.
//! 의존성: 없음 (표준 라이브러리만)
//! 실행: python3 "_시스템/Rust 계획/rs_check.py" "Rust/3.개념집/23_error-propagation_verify.rs"

use std::error::Error;
use std::fmt;
use std::num::ParseIntError;

// ---------- 1. 직접 만든 에러 타입과 From ----------

#[derive(Debug)]
enum ConfigError {
    Missing(&'static str),
    BadNumber(ParseIntError),
}

impl fmt::Display for ConfigError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            ConfigError::Missing(key) => write!(f, "missing key `{key}`"),
            // 원인은 source()로 내보내므로 메시지에 다시 넣지 않는다.
            ConfigError::BadNumber(_) => write!(f, "port is not a number"),
        }
    }
}

impl Error for ConfigError {
    fn source(&self) -> Option<&(dyn Error + 'static)> {
        match self {
            ConfigError::Missing(_) => None,
            ConfigError::BadNumber(e) => Some(e),
        }
    }
}

impl From<ParseIntError> for ConfigError {
    fn from(e: ParseIntError) -> Self {
        ConfigError::BadNumber(e)
    }
}

/// "port=8080" 같은 줄들에서 port 값을 찾는다.
fn find_port(text: &str) -> Option<&str> {
    let line = text.lines().find(|l| l.starts_with("port="))?; // None이면 바로 None을 돌려준다
    Some(line.trim_start_matches("port="))
}

/// `?` 버전: ParseIntError가 From을 거쳐 ConfigError로 바뀐다.
fn read_port(text: &str) -> Result<u16, ConfigError> {
    let raw = find_port(text).ok_or(ConfigError::Missing("port"))?;
    let port: u16 = raw.parse()?;
    Ok(port)
}

/// 같은 일을 match로 풀어 쓴 버전(참조서의 Err(e) => return Err(From::from(e))).
fn read_port_by_hand(text: &str) -> Result<u16, ConfigError> {
    let raw = match find_port(text) {
        Some(v) => v,
        None => return Err(ConfigError::Missing("port")),
    };
    let port: u16 = match raw.parse::<u16>() {
        Ok(v) => v,
        Err(e) => return Err(From::from(e)),
    };
    Ok(port)
}

// ---------- 2. Box<dyn Error + Send + Sync> ----------

type BoxError = Box<dyn Error + Send + Sync>;

fn parse_pair(s: &str) -> Result<(i32, i32), BoxError> {
    let (a, b) = s.split_once(',').ok_or("no comma")?; // &str도 From으로 BoxError가 된다
    let a: i32 = a.trim().parse()?; // ParseIntError -> BoxError
    let b: i32 = b.trim().parse()?;
    if b == 0 {
        return Err(format!("second value must not be zero: {s}").into()); // String -> BoxError
    }
    Ok((a, b))
}

// ---------- 3. source() 사슬 ----------

fn chain(e: &(dyn Error + 'static)) -> Vec<String> {
    let mut out = vec![e.to_string()];
    let mut cur = e.source();
    while let Some(inner) = cur {
        out.push(inner.to_string());
        cur = inner.source();
    }
    out
}

// ---------- 컴파일러 도우미 ----------

fn tmpdir(tag: &str) -> std::path::PathBuf {
    let dir = std::env::temp_dir().join(format!("vr23_{}_{}", std::process::id(), tag));
    std::fs::create_dir_all(&dir).expect("임시 폴더");
    dir
}

fn compile_errors(tag: &str, code: &str) -> String {
    let dir = tmpdir(tag);
    let file = dir.join("snippet.rs");
    std::fs::write(&file, code).expect("쓰기");
    let out = std::process::Command::new("rustc")
        .args([
            "--edition",
            "2024",
            "--crate-type=lib",
            "--emit=metadata",
            "--out-dir",
        ])
        .arg(&dir)
        .arg(&file)
        .output()
        .expect("rustc 실행");
    String::from_utf8_lossy(&out.stderr).into_owned()
}

/// 바이너리로 컴파일해 실행하고 (종료 코드, stdout, stderr)를 돌려준다.
fn build_and_run(tag: &str, code: &str) -> (Option<i32>, String, String) {
    let dir = tmpdir(tag);
    let file = dir.join("main.rs");
    let exe = dir.join("prog");
    std::fs::write(&file, code).expect("쓰기");
    let out = std::process::Command::new("rustc")
        .args(["--edition", "2024", "-o"])
        .arg(&exe)
        .arg(&file)
        .output()
        .expect("rustc 실행");
    assert!(
        out.status.success(),
        "{}",
        String::from_utf8_lossy(&out.stderr)
    );
    let run = std::process::Command::new(&exe).output().expect("실행");
    (
        run.status.code(),
        String::from_utf8_lossy(&run.stdout).into_owned(),
        String::from_utf8_lossy(&run.stderr).into_owned(),
    )
}

fn main() {
    // 1. ? 와 손으로 쓴 match는 모든 입력에서 같은 결과를 낸다.
    let inputs = [
        "port=8080",
        "host=a\nport=443",
        "port=http",
        "host=a",
        "port=70000",
    ];
    for s in inputs {
        let a = read_port(s).map_err(|e| e.to_string());
        let b = read_port_by_hand(s).map_err(|e| e.to_string());
        assert_eq!(a, b, "입력 {s:?}");
    }
    assert_eq!(read_port("port=8080").ok(), Some(8080));
    let e = read_port("port=http").unwrap_err();
    assert!(matches!(e, ConfigError::BadNumber(_)));
    assert_eq!(e.to_string(), "port is not a number");
    let e = read_port("host=a").unwrap_err();
    assert_eq!(e.to_string(), "missing key `port`");
    // 70000은 u16 범위를 넘는다.
    assert_eq!(
        chain(&read_port("port=70000").unwrap_err()),
        vec![
            "port is not a number",
            "number too large to fit in target type"
        ]
    );
    println!("1. ?와 match 풀어 쓰기: 5개 입력에서 결과가 같다");

    // Option의 ?
    assert_eq!(find_port("a=1\nb=2"), None);
    assert_eq!(find_port("port=1"), Some("1"));

    // 2. BoxError
    assert_eq!(parse_pair("3, 4").ok(), Some((3, 4)));
    assert_eq!(parse_pair("34").unwrap_err().to_string(), "no comma");
    assert_eq!(
        parse_pair("x,4").unwrap_err().to_string(),
        "invalid digit found in string"
    );
    assert_eq!(
        parse_pair("1,0").unwrap_err().to_string(),
        "second value must not be zero: 1,0"
    );
    // Send + Sync이므로 다른 스레드에서 만들어 돌려받을 수 있다.
    let from_thread = std::thread::spawn(|| parse_pair("a,b"))
        .join()
        .expect("스레드가 panic하지 않았다");
    assert!(from_thread.is_err());
    // 상자 안의 진짜 타입으로 되돌리기(downcast)
    let boxed = parse_pair("x,1").unwrap_err();
    assert!(boxed.downcast_ref::<ParseIntError>().is_some());
    println!("2. Box<dyn Error + Send + Sync>: &str, String, ParseIntError가 모두 ?로 들어간다");

    // 3. 사슬
    let c = chain(&read_port("port=-1").unwrap_err());
    println!("3. 사슬: {c:?}");
    assert_eq!(
        c,
        vec!["port is not a number", "invalid digit found in string"]
    );

    // 4. main이 Err를 돌려주면: 종료 코드 1, stderr에 "Error: " + Debug 형식
    let (code, out, err) = build_and_run(
        "main_err",
        r#"
fn main() -> Result<(), std::num::ParseIntError> {
    println!("start");
    let n: i32 = "12a".parse()?;
    println!("never {n}");
    Ok(())
}
"#,
    );
    println!("4. main이 Err: 종료 코드 {code:?}, stderr {:?}", err.trim());
    assert_eq!(code, Some(1));
    assert_eq!(out, "start\n");
    assert_eq!(err.trim(), "Error: ParseIntError { kind: InvalidDigit }");
    let (code, _, _) = build_and_run("main_ok", "fn main() -> Result<(), String> { Ok(()) }\n");
    assert_eq!(code, Some(0));

    // 5. 컴파일 오류
    // (가) 반환 타입이 ()인 함수에서 ?
    let e = compile_errors("unit", "pub fn f() { let _n: i32 = \"1\".parse()?; }\n");
    assert!(e.contains("error[E0277]"), "{e}");
    assert!(e.contains("can only be used in a function that returns `Result` or `Option`"));
    // (나) Result를 돌려주는 함수에서 Option에 ?
    let e = compile_errors(
        "mix",
        "pub fn f(v: &[i32]) -> Result<i32, String> { let x = v.first()?; Ok(*x) }\n",
    );
    assert!(e.contains("error[E0277]"), "{e}");
    assert!(
        e.contains("the `?` operator can only be used on `Result`s"),
        "{e}"
    );
    // (다) From 구현이 없으면 변환하지 못한다
    let e = compile_errors(
        "nofrom",
        "pub struct MyErr;\npub fn f() -> Result<i32, MyErr> { let n = \"1\".parse::<i32>()?; Ok(n) }\n",
    );
    assert!(e.contains("error[E0277]"), "{e}");
    assert!(
        e.contains("`?` couldn't convert the error to `MyErr`"),
        "{e}"
    );
    println!("5. 컴파일 오류 세 가지 모두 E0277");

    // 6. clippy 기본 린트: question_mark(match로 풀어 쓴 전파), result_large_err(큰 에러 타입)
    let dir = tmpdir("clippy");
    let file = dir.join("lints.rs");
    std::fs::write(
        &file,
        "pub fn f(s: &str) -> Result<i32, std::num::ParseIntError> {\n    let n = match s.parse::<i32>() {\n        Ok(v) => v,\n        Err(e) => return Err(e),\n    };\n    Ok(n)\n}\npub fn h(ok: bool) -> Result<(), [u8; 512]> {\n    if ok { Ok(()) } else { Err([0; 512]) }\n}\n",
    )
    .expect("쓰기");
    let out = std::process::Command::new("clippy-driver")
        .args([
            "--edition",
            "2024",
            "--crate-type=lib",
            "--emit=metadata",
            "--out-dir",
        ])
        .arg(&dir)
        .arg(&file)
        .output()
        .expect("clippy-driver 실행");
    let msg = String::from_utf8_lossy(&out.stderr);
    assert!(
        msg.contains("`#[warn(clippy::question_mark)]` on by default"),
        "{msg}"
    );
    assert!(
        msg.contains("`#[warn(clippy::result_large_err)]` on by default"),
        "{msg}"
    );
    println!("6. clippy::question_mark, clippy::result_large_err 기본으로 경고");

    println!("ALL CHECKS PASSED");
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn question_mark_matches_manual_match() {
        for s in ["port=1", "port=", "", "port=65535", "port=65536"] {
            assert_eq!(
                read_port(s).map_err(|e| e.to_string()),
                read_port_by_hand(s).map_err(|e| e.to_string())
            );
        }
    }

    #[test]
    fn box_error_is_send_sync() {
        fn needs<T: Send + Sync + 'static>() {}
        needs::<BoxError>();
    }
}