타입으로 잘못된 상태 막기 문서의 검증 코드다.

//! 타입으로 잘못된 상태 막기 문서의 주장을 확인한다: 뉴타입 섞임 막기(E0308), 검사하는 생성자와
//! 닫힌 필드(E0451), TryFrom·FromStr, bool 대신 열거형 상태, PhantomData 상태 표시(타입 상태)와
//! 없는 메서드(E0599)·소비된 값(E0382), 크기 0 표시와 NonZero.
//! 의존성: 없음 (표준 라이브러리만)
//! 실행: python3 "_시스템/Rust 계획/rs_check.py" "Rust/3.개념집/14_type-driven-design_verify.rs"

use std::marker::PhantomData;
use std::mem::size_of;
use std::num::NonZero;
use std::str::FromStr;

mod domain {
    /// 검사를 통과한 이메일만 담는다. 필드가 닫혀 있어 바깥에서 직접 만들 수 없다.
    #[derive(Debug, Clone, PartialEq, Eq, Hash)]
    pub struct Email {
        value: String,
    }

    #[derive(Debug, PartialEq, Eq)]
    pub enum EmailError {
        Empty,
        MissingAt,
        BadDomain,
    }

    impl Email {
        pub fn parse(raw: &str) -> Result<Self, EmailError> {
            let s = raw.trim();
            if s.is_empty() {
                return Err(EmailError::Empty);
            }
            let Some((local, domain)) = s.split_once('@') else {
                return Err(EmailError::MissingAt);
            };
            if local.is_empty() || !domain.contains('.') || domain.starts_with('.') {
                return Err(EmailError::BadDomain);
            }
            Ok(Self {
                value: s.to_ascii_lowercase(),
            })
        }

        pub fn as_str(&self) -> &str {
            &self.value
        }
    }

    impl TryFrom<&str> for Email {
        type Error = EmailError;
        fn try_from(raw: &str) -> Result<Self, Self::Error> {
            Email::parse(raw)
        }
    }
}

use domain::{Email, EmailError};

#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
struct Meters(f64);

impl FromStr for Meters {
    type Err = String;
    fn from_str(s: &str) -> Result<Self, Self::Err> {
        let v: f64 = s
            .trim()
            .trim_end_matches('m')
            .parse()
            .map_err(|_| format!("길이 아님: {s}"))?;
        if v.is_finite() && v >= 0.0 {
            Ok(Meters(v))
        } else {
            Err(format!("음수나 무한대: {s}"))
        }
    }
}

/// bool 두 개(connected, authenticated) 대신 상태 셋
#[derive(Debug, PartialEq)]
enum Conn {
    Disconnected,
    Connected { peer: String },
    Authenticated { peer: String, user: String },
}

impl Conn {
    fn login(self, user: &str) -> Result<Conn, (Conn, &'static str)> {
        match self {
            Conn::Connected { peer } => Ok(Conn::Authenticated {
                peer,
                user: user.to_string(),
            }),
            other => Err((other, "연결된 상태에서만 로그인할 수 있다")),
        }
    }
}

// 타입 상태: 크기 0짜리 표시 타입으로 문의 상태를 타입에 적는다.
struct Open;
struct Closed;

struct Door<State> {
    opened_count: u32,
    _state: PhantomData<State>,
}

impl Door<Closed> {
    fn new() -> Self {
        Door {
            opened_count: 0,
            _state: PhantomData,
        }
    }
    fn open(self) -> Door<Open> {
        Door {
            opened_count: self.opened_count + 1,
            _state: PhantomData,
        }
    }
}

impl Door<Open> {
    fn close(self) -> Door<Closed> {
        Door {
            opened_count: self.opened_count,
            _state: PhantomData,
        }
    }
}

impl<S> Door<S> {
    fn opened_count(&self) -> u32 {
        self.opened_count
    }
}

fn compile(code: &str, tag: &str) -> String {
    let dir = std::env::temp_dir().join(format!("vr14_{}_{}", std::process::id(), tag));
    std::fs::create_dir_all(&dir).unwrap();
    let file = dir.join("snippet.rs");
    std::fs::write(&file, code).unwrap();
    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()
}

const DOOR: &str = "use std::marker::PhantomData;\npub struct Open; pub struct Closed;\npub struct Door<S> { _s: PhantomData<S> }\nimpl Door<Closed> { pub fn new() -> Self { Door { _s: PhantomData } } pub fn open(self) -> Door<Open> { Door { _s: PhantomData } } }\nimpl Door<Open> { pub fn close(self) -> Door<Closed> { Door { _s: PhantomData } } }\n";

fn main() {
    // 1. 뉴타입: 같은 f64라도 섞이지 않는다
    let e = compile(
        "pub struct Miles(pub f64);\npub struct Kilometers(pub f64);\npub fn drive(_d: Miles) {}\npub fn f() { drive(Kilometers(3.0)); }",
        "newtype",
    );
    assert!(e.contains("E0308"), "{e}");
    println!("Kilometers를 Miles 자리에: E0308");
    assert_eq!(size_of::<Meters>(), size_of::<f64>());
    assert_eq!("12.5m".parse::<Meters>(), Ok(Meters(12.5)));
    assert!("-3".parse::<Meters>().is_err());
    assert!("abc".parse::<Meters>().is_err());

    // 2. 검사하는 생성자
    let ok = Email::parse("  Ann@Example.com ").unwrap();
    assert_eq!(ok.as_str(), "ann@example.com");
    assert_eq!(Email::parse(""), Err(EmailError::Empty));
    assert_eq!(Email::parse("ann.example.com"), Err(EmailError::MissingAt));
    assert_eq!(Email::parse("ann@localhost"), Err(EmailError::BadDomain));
    assert_eq!(Email::parse("@x.com"), Err(EmailError::BadDomain));
    let via_try: Result<Email, _> = "b@x.org".try_into();
    assert!(via_try.is_ok());

    // 바깥 모듈에서 닫힌 필드로 직접 만들면 E0451
    let e = compile(
        "mod d { pub struct Email { value: String } impl Email { pub fn as_str(&self) -> &str { &self.value } } }\npub fn f() -> usize { let e = d::Email { value: String::new() }; e.as_str().len() }",
        "private",
    );
    assert!(e.contains("E0451"), "{e}");
    println!("닫힌 필드로 직접 만들기: E0451");

    // 3. bool 대신 열거형 상태
    let c = Conn::Connected {
        peer: "10.0.0.1".into(),
    };
    let c = c.login("ann").unwrap();
    assert_eq!(
        c,
        Conn::Authenticated {
            peer: "10.0.0.1".into(),
            user: "ann".into()
        }
    );
    let (back, why) = Conn::Disconnected.login("ann").unwrap_err();
    assert_eq!(back, Conn::Disconnected);
    println!("Disconnected에서 login: {why}");

    // 4. 타입 상태
    let door = Door::<Closed>::new();
    let door = door.open();
    let door = door.close();
    let door = door.open();
    println!("문을 연 횟수 {}", door.opened_count());
    assert_eq!(door.opened_count(), 2);
    // 표시 타입은 크기를 더하지 않는다
    assert_eq!(size_of::<PhantomData<Open>>(), 0);
    assert_eq!(size_of::<Open>(), 0);
    assert_eq!(size_of::<Door<Open>>(), size_of::<u32>());

    // PhantomData 없이 타입 인자만 두면 E0392
    let e = compile("pub struct Door<S> { pub n: u32 }", "e0392");
    assert!(e.contains("E0392"), "{e}");
    println!("쓰이지 않은 타입 인자: E0392");

    // 열린 문을 또 열면 E0599 (그런 메서드가 없다)
    let e = compile(
        &format!("{DOOR}pub fn f() {{ let d = Door::<Closed>::new().open(); let _ = d.open(); }}"),
        "twice",
    );
    assert!(e.contains("E0599"), "{e}");
    println!("열린 문을 또 open: E0599");
    // 바꾼 뒤 옛 값을 쓰면 E0382
    let e = compile(
        &format!(
            "{DOOR}pub fn f() {{ let d = Door::<Closed>::new(); let o = d.open(); let _ = o.close(); let _ = d.open(); }}"
        ),
        "used",
    );
    assert!(e.contains("E0382"), "{e}");
    println!("바뀌기 전 상태를 다시 쓰기: E0382");

    // 5. NonZero
    assert_eq!(NonZero::new(0u32), None);
    let n = NonZero::new(8u32).unwrap();
    assert_eq!(n.get(), 8);
    assert_eq!(size_of::<Option<NonZero<u32>>>(), 4);

    // 6. 확인 문제 C3 실행 추적
    let inputs = [" Kim@Corp.io", "lee@corp", "", "park@corp.io"];
    let out: Vec<String> = inputs
        .iter()
        .map(|s| match Email::parse(s) {
            Ok(e) => e.as_str().to_string(),
            Err(err) => format!("{err:?}"),
        })
        .collect();
    println!("C3: {}", out.join(" | "));
    assert_eq!(
        out.join(" | "),
        "kim@corp.io | BadDomain | Empty | park@corp.io"
    );

    println!("ALL CHECKS PASSED");
}