API 설계 원칙 문서의 검증 코드다.

//! 49 API 설계 원칙: 소비형 빌더와 #[must_use], 봉인 트레이트(다른 크레이트에서 구현하면 E0277),
//! 라이브러리를 고친 뒤 쓰는 쪽이 깨지는 경우(E0046·E0004·E0063·E0034·E0639)와 그것을 막는 설계,
//! 이름 규칙 린트(wrong_self_convention과 avoid-breaking-exported-api)를 rustc·clippy-driver로 확인한다.
//! 의존성: 없음 (표준 라이브러리만)
//! 실행: python3 "_시스템/Rust 계획/rs_check.py" "Rust/3.개념집/49_api-design_verify.rs"

use std::error::Error;
use std::fmt;
use std::path::{Path, PathBuf};
use std::process::{Command, Output};
use std::time::Duration;

// ---------- 1. 소비형 빌더 ----------

/// 서버 설정. 필드는 닫혀 있고 빌더로만 만든다(C-STRUCT-PRIVATE, C-BUILDER).
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ServerConfig {
    addr: String,
    timeout: Duration,
    max_body_bytes: usize,
}

impl ServerConfig {
    /// 꼭 있어야 하는 값(주소)은 빌더를 만들 때 받는다. 나머지는 기본값이 있다.
    pub fn builder(addr: impl Into<String>) -> ServerConfigBuilder {
        ServerConfigBuilder {
            addr: addr.into(),
            timeout: Duration::from_secs(30),
            max_body_bytes: 1024 * 1024,
        }
    }

    /// `get_addr`가 아니다(C-GETTER).
    pub fn addr(&self) -> &str {
        &self.addr
    }

    pub fn timeout(&self) -> Duration {
        self.timeout
    }

    pub fn max_body_bytes(&self) -> usize {
        self.max_body_bytes
    }
}

/// 타입에 붙인 `#[must_use]`: 빌더를 만들고 버리면 경고가 난다.
#[derive(Debug, Clone)]
#[must_use = "빌더는 build()를 불러야 설정이 된다"]
pub struct ServerConfigBuilder {
    addr: String,
    timeout: Duration,
    max_body_bytes: usize,
}

impl ServerConfigBuilder {
    pub fn timeout(mut self, timeout: Duration) -> Self {
        self.timeout = timeout;
        self
    }

    pub fn max_body_bytes(mut self, n: usize) -> Self {
        self.max_body_bytes = n;
        self
    }

    /// 검사는 마지막에 한 번 한다.
    pub fn build(self) -> Result<ServerConfig, BuildError> {
        if self.addr.is_empty() {
            return Err(BuildError::EmptyAddr);
        }
        if self.timeout.is_zero() {
            return Err(BuildError::ZeroTimeout);
        }
        if self.max_body_bytes == 0 {
            return Err(BuildError::ZeroBodyLimit);
        }
        Ok(ServerConfig {
            addr: self.addr,
            timeout: self.timeout,
            max_body_bytes: self.max_body_bytes,
        })
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum BuildError {
    EmptyAddr,
    ZeroTimeout,
    ZeroBodyLimit,
}

impl fmt::Display for BuildError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let msg = match self {
            BuildError::EmptyAddr => "address is empty",
            BuildError::ZeroTimeout => "timeout must be greater than zero",
            BuildError::ZeroBodyLimit => "body limit must be greater than zero",
        };
        f.write_str(msg)
    }
}

impl Error for BuildError {}

fn assert_good_error<E: Error + Send + Sync + 'static>() {}
fn assert_send_sync<T: Send + Sync>() {}

// ---------- 2. 봉인 트레이트 ----------

mod private {
    pub trait Sealed {}
}

/// 짧은 코드를 만드는 방식. 봉인되어 있어 이 크레이트 밖에서는 구현할 수 없다(C-SEALED).
pub trait Codec: private::Sealed {
    fn encode(&self, n: u64) -> String;
}

pub struct Base62;
pub struct Decimal;

impl private::Sealed for Base62 {}
impl private::Sealed for Decimal {}

const ALPHABET: &[u8; 62] = b"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";

impl Codec for Base62 {
    fn encode(&self, mut n: u64) -> String {
        if n == 0 {
            return "0".to_string();
        }
        let mut out = Vec::new();
        while n > 0 {
            out.push(ALPHABET[usize::try_from(n % 62).expect("62 미만")]);
            n /= 62;
        }
        out.reverse();
        String::from_utf8(out).expect("ASCII")
    }
}

impl Codec for Decimal {
    fn encode(&self, n: u64) -> String {
        n.to_string()
    }
}

// ---------- 3. rustc·clippy-driver 도우미 ----------

fn tmp(name: &str) -> PathBuf {
    let d = std::env::temp_dir().join(format!("vr49_{}_{name}", std::process::id()));
    let _ = std::fs::remove_dir_all(&d);
    std::fs::create_dir_all(&d).unwrap();
    d
}

fn stderr(o: &Output) -> String {
    String::from_utf8_lossy(&o.stderr).into_owned()
}

/// 라이브러리 크레이트 `store`(또는 `name`)를 rlib으로 만든다.
fn build_lib(dir: &Path, name: &str, src: &str) -> Output {
    let file = dir.join(format!("{name}.rs"));
    std::fs::write(&file, src).unwrap();
    Command::new("rustc")
        .args([
            "--edition",
            "2024",
            "--crate-type=rlib",
            "--crate-name",
            name,
        ])
        .arg("--out-dir")
        .arg(dir)
        .arg(&file)
        .output()
        .expect("rustc 실행")
}

/// 라이브러리를 쓰는 쪽 크레이트를 컴파일한다.
fn build_user(dir: &Path, lib: &str, src: &str) -> Output {
    let file = dir.join("user.rs");
    std::fs::write(&file, src).unwrap();
    Command::new("rustc")
        .args(["--edition", "2024", "--crate-type=lib", "--emit=metadata"])
        .arg("--extern")
        .arg(format!(
            "{lib}={}",
            dir.join(format!("lib{lib}.rlib")).display()
        ))
        .arg("-L")
        .arg(dir)
        .arg("--out-dir")
        .arg(dir)
        .arg(&file)
        .output()
        .expect("rustc 실행")
}

/// stderr에서 `error[E....]` 코드만 모은다.
fn error_codes(err: &str) -> Vec<String> {
    let mut codes: Vec<String> = err
        .match_indices("error[E")
        .map(|(i, _)| err[i + 6..i + 11].to_string())
        .collect();
    codes.dedup();
    codes
}

fn clippy(dir: &Path, src: &str, extra: &[&str], conf_dir: Option<&Path>) -> String {
    let file = dir.join("lint.rs");
    std::fs::write(&file, src).unwrap();
    let mut cmd = Command::new("clippy-driver");
    cmd.args(["--edition", "2024", "--crate-type=lib", "--emit=metadata"])
        .args(extra)
        .arg("--out-dir")
        .arg(dir)
        .arg(&file);
    match conf_dir {
        Some(c) => cmd.env("CLIPPY_CONF_DIR", c),
        None => cmd.env("CLIPPY_CONF_DIR", dir),
    };
    stderr(&cmd.output().expect("clippy-driver 실행"))
}

// ---------- 4. 확인 ----------

fn check_builder() {
    let cfg = ServerConfig::builder("0.0.0.0:8080")
        .timeout(Duration::from_secs(5))
        .build()
        .unwrap();
    assert_eq!(cfg.addr(), "0.0.0.0:8080");
    assert_eq!(cfg.timeout(), Duration::from_secs(5));
    assert_eq!(cfg.max_body_bytes(), 1024 * 1024);

    // 조건에 따라 설정할 때 소비형 빌더는 다시 묶어야 한다.
    let slow_network = true;
    let mut b = ServerConfig::builder("127.0.0.1:3000");
    if slow_network {
        b = b.timeout(Duration::from_secs(60));
    }
    assert_eq!(b.build().unwrap().timeout(), Duration::from_secs(60));

    let e = ServerConfig::builder("x").max_body_bytes(0).build();
    assert_eq!(e, Err(BuildError::ZeroBodyLimit));
    assert_eq!(
        e.unwrap_err().to_string(),
        "body limit must be greater than zero"
    );
    assert_eq!(
        ServerConfig::builder("").build(),
        Err(BuildError::EmptyAddr)
    );
    assert_good_error::<BuildError>();
    assert_send_sync::<ServerConfig>();
    println!("빌더: 기본값·체인·조건부 설정·검사 오류 확인");

    // 비소비형 빌더의 예: std::process::Command는 &mut self를 받고, 같은 빌더로 두 번 실행할 수 있다.
    let mut cmd = Command::new("rustc");
    cmd.arg("--version");
    let first = cmd.output().unwrap();
    let second = cmd.output().unwrap();
    assert_eq!(first.stdout, second.stdout);
    let version = String::from_utf8_lossy(&first.stdout);
    assert!(version.starts_with("rustc "), "{version}");
    println!("Command 빌더 두 번 실행: {}", version.trim());
}

fn check_must_use() {
    let d = tmp("mustuse");
    let base = "pub struct B { port: u16 }\n\
                impl B {\n\
                    pub fn new() -> Self { B { port: 80 } }\n\
                    ATTR\n\
                    pub fn port(mut self, p: u16) -> Self { self.port = p; self }\n\
                    pub fn get(&self) -> u16 { self.port }\n\
                }\n\
                impl Default for B { fn default() -> Self { Self::new() } }\n\
                pub fn forget(b: B) { b.port(8080); }\n";
    let plain = base.replace("ATTR", "");
    let marked = base.replace(
        "ATTR",
        "#[must_use = \"새 빌더를 돌려줄 뿐 원래 값은 그대로다\"]",
    );
    let r1 = clippy(&d, &plain, &[], None);
    // "error"가 없어야 조각이 실제로 컴파일된 것이다(호출이 실패해도 "must be used"는 안 나온다).
    assert!(
        !r1.contains("error") && !r1.contains("must be used"),
        "붙이지 않으면 조용히 버려진다: {r1}"
    );
    let r2 = clippy(&d, &marked, &[], None);
    assert!(
        r2.contains("unused return value of `B::port` that must be used"),
        "{r2}"
    );
    assert!(r2.contains("새 빌더를 돌려줄 뿐 원래 값은 그대로다"));
    let r3 = clippy(&d, &plain, &["-W", "clippy::pedantic"], None);
    assert!(r3.contains("clippy::return-self-not-must-use"), "{r3}");
    assert!(r3.contains("clippy::must-use-candidate"), "{r3}");
    // 타입에 #[must_use]를 붙이면 Self를 돌려주는 메서드는 따로 표시하지 않아도 된다.
    let typed = plain.replace("pub struct B", "#[must_use]\npub struct B");
    let r4 = clippy(&d, &typed, &["-W", "clippy::pedantic"], None);
    // 같은 실행에서 pedantic 린트가 실제로 돌았는지(must_use_candidate가 `get`을 짚는지)도 본다.
    assert!(r4.contains("clippy::must-use-candidate"), "{r4}");
    assert!(!r4.contains("return-self-not-must-use"), "{r4}");
    assert!(r4.contains("unused `B` that must be used"), "{r4}");
    println!(
        "must_use: 없으면 조용히 버림, 메서드나 타입에 붙이면 경고, pedantic은 return_self_not_must_use"
    );
}

const SEALED_LIB: &str = "mod private {\n    pub trait Sealed {}\n}\n\
/// 이 트레이트는 봉인되어 있다.\n\
pub trait Codec: private::Sealed {\n    fn encode(&self, n: u64) -> String;\n}\n\
pub struct Decimal;\n\
impl private::Sealed for Decimal {}\n\
impl Codec for Decimal {\n    fn encode(&self, n: u64) -> String { n.to_string() }\n}\n";

fn check_sealed() {
    assert_eq!(Base62.encode(125), "21");
    assert_eq!(Base62.encode(0), "0");
    assert_eq!(Base62.encode(61), "Z");
    assert_eq!(Decimal.encode(125), "125");
    let codecs: [&dyn Codec; 2] = [&Base62, &Decimal];
    let all: Vec<String> = codecs.iter().map(|c| c.encode(3844)).collect();
    assert_eq!(all, ["100", "3844"]);

    let d = tmp("sealed");
    let lib = build_lib(&d, "codec", SEALED_LIB);
    assert!(lib.status.success(), "{}", stderr(&lib));
    let outside = build_user(
        &d,
        "codec",
        "pub struct Hex;\nimpl codec::Codec for Hex {\n    fn encode(&self, n: u64) -> String { format!(\"{n:x}\") }\n}\n",
    );
    let e = stderr(&outside);
    assert_eq!(error_codes(&e), ["E0277"], "{e}");
    assert!(
        e.contains("the trait bound `Hex: codec::private::Sealed` is not satisfied"),
        "{e}"
    );
    assert!(e.contains("`Codec` is a \"sealed trait\""), "{e}");
    let name_it = build_user(
        &d,
        "codec",
        "pub struct Hex;\nimpl codec::private::Sealed for Hex {}\n",
    );
    let e = stderr(&name_it);
    assert_eq!(error_codes(&e), ["E0603"], "{e}");
    assert!(e.contains("module `private` is private"), "{e}");
    let use_it = build_user(
        &d,
        "codec",
        "use codec::Codec;\npub fn f() -> String { codec::Decimal.encode(7) }\n\
         pub fn g(c: &dyn codec::Codec) -> String { c.encode(7) }\n",
    );
    assert!(use_it.status.success(), "{}", stderr(&use_it));
    println!(
        "봉인 트레이트: 밖에서 구현 E0277(sealed trait 안내), Sealed 이름 부르기 E0603, 쓰기는 통과"
    );
}

/// 1판: 아무 대비 없이 공개한 라이브러리.
const NAIVE_V1: &str = "pub trait Store { fn get(&self, k: &str) -> Option<String>; }\n\
pub enum Kind { NotFound, Duplicate }\n\
pub struct Link { pub code: String, pub url: String }\n";

/// 1판을 쓰는 쪽 코드: 트레이트 구현, 모든 variant를 다룬 match, 구조체 리터럴.
const NAIVE_USER: &str = "pub struct Mem;\n\
impl store::Store for Mem { fn get(&self, _k: &str) -> Option<String> { None } }\n\
pub fn code(k: store::Kind) -> u8 { match k { store::Kind::NotFound => 1, store::Kind::Duplicate => 2 } }\n\
pub fn make() -> store::Link { store::Link { code: \"a\".into(), url: \"b\".into() } }\n";

/// 쓰는 쪽에 같은 이름(size)의 메서드를 가진 자기 트레이트가 있다.
const CLASH_USER: &str = "use store::Store;\npub struct Mem;\n\
impl Store for Mem { fn get(&self, _k: &str) -> Option<String> { None } }\n\
pub trait Ext { fn size(&self) -> usize { 1 } }\nimpl Ext for Mem {}\n\
pub fn s(m: &Mem) -> usize { m.size() + m.get(\"k\").map_or(0, |v| v.len()) }\n";

/// 1판을 미래에 대비해 공개했다면.
const READY_V1: &str = "mod private { pub trait Sealed {} }\n\
pub trait Store { fn get(&self, k: &str) -> Option<String>; }\n\
pub trait Codec: private::Sealed { fn encode(&self, n: u64) -> String; }\n\
pub struct Decimal;\nimpl private::Sealed for Decimal {}\n\
impl Codec for Decimal { fn encode(&self, n: u64) -> String { n.to_string() } }\n\
#[non_exhaustive]\npub enum Kind { NotFound, Duplicate }\n\
#[non_exhaustive]\npub struct Link { pub code: String, pub url: String }\n\
impl Link { pub fn new(code: &str, url: &str) -> Self { Link { code: code.into(), url: url.into() } } }\n";

/// 대비한 1판의 2판: 기본 몸통 있는 메서드, 봉인 트레이트의 필수 메서드, variant, 필드를 더했다.
const READY_V2: &str = "mod private { pub trait Sealed {} }\n\
pub trait Store {\n    fn get(&self, k: &str) -> Option<String>;\n    fn delete(&self, _k: &str) -> bool { false }\n}\n\
pub trait Codec: private::Sealed {\n    fn encode(&self, n: u64) -> String;\n    fn decode(&self, s: &str) -> Option<u64>;\n}\n\
pub struct Decimal;\nimpl private::Sealed for Decimal {}\n\
impl Codec for Decimal {\n    fn encode(&self, n: u64) -> String { n.to_string() }\n    fn decode(&self, s: &str) -> Option<u64> { s.parse().ok() }\n}\n\
#[non_exhaustive]\npub enum Kind { NotFound, Duplicate, Unavailable }\n\
#[non_exhaustive]\npub struct Link { pub code: String, pub url: String, pub hits: u64 }\n\
impl Link { pub fn new(code: &str, url: &str) -> Self { Link { code: code.into(), url: url.into(), hits: 0 } } }\n";

const READY_USER: &str = "pub struct Mem;\n\
impl store::Store for Mem { fn get(&self, _k: &str) -> Option<String> { None } }\n\
pub fn code(k: store::Kind) -> u8 { match k { store::Kind::NotFound => 1, store::Kind::Duplicate => 2, _ => 0 } }\n\
pub fn make() -> store::Link { store::Link::new(\"a\", \"b\") }\n\
pub fn url(l: &store::Link) -> &str { let store::Link { url, .. } = l; url }\n\
pub fn enc(c: &dyn store::Codec) -> String { c.encode(7) }\n";

/// 라이브러리 판 `lib_src`로 `user_src`를 컴파일해 오류 코드를 돌려준다(빈 목록이면 통과).
fn against(dir: &Path, lib_src: &str, user_src: &str) -> Vec<String> {
    let lib = build_lib(dir, "store", lib_src);
    assert!(lib.status.success(), "{}", stderr(&lib));
    let user = build_user(dir, "store", user_src);
    let codes = error_codes(&stderr(&user));
    assert_eq!(user.status.success(), codes.is_empty(), "{}", stderr(&user));
    codes
}

fn check_semver() {
    let d = tmp("semver");
    assert!(against(&d, NAIVE_V1, NAIVE_USER).is_empty());
    assert!(against(&d, NAIVE_V1, CLASH_USER).is_empty());

    let required_method = NAIVE_V1.replace(
        "fn get(&self, k: &str) -> Option<String>; }",
        "fn get(&self, k: &str) -> Option<String>; fn delete(&self, k: &str) -> bool; }",
    );
    let defaulted_method = NAIVE_V1.replace(
        "fn get(&self, k: &str) -> Option<String>; }",
        "fn get(&self, k: &str) -> Option<String>; fn size(&self) -> usize { 0 } }",
    );
    let new_variant = NAIVE_V1.replace("Duplicate }", "Duplicate, Unavailable }");
    let new_field = NAIVE_V1.replace("pub url: String }", "pub url: String, pub hits: u64 }");
    let late_non_exhaustive =
        NAIVE_V1.replace("pub struct Link", "#[non_exhaustive]\npub struct Link");

    let table = [
        (
            "트레이트에 필수 메서드 추가",
            against(&d, &required_method, NAIVE_USER),
        ),
        (
            "트레이트에 기본 몸통 있는 메서드 추가",
            against(&d, &defaulted_method, NAIVE_USER),
        ),
        (
            "  (쓰는 쪽에 같은 이름 메서드가 있을 때)",
            against(&d, &defaulted_method, CLASH_USER),
        ),
        ("enum에 variant 추가", against(&d, &new_variant, NAIVE_USER)),
        (
            "필드가 모두 pub인 구조체에 pub 필드 추가",
            against(&d, &new_field, NAIVE_USER),
        ),
        (
            "나중에 #[non_exhaustive] 붙이기",
            against(&d, &late_non_exhaustive, NAIVE_USER),
        ),
    ];
    for (what, codes) in &table {
        println!("{what}: {codes:?}");
    }
    let got: Vec<Vec<String>> = table.iter().map(|(_, c)| c.clone()).collect();
    let want: Vec<Vec<&str>> = vec![
        vec!["E0046"],
        vec![],
        vec!["E0034"],
        vec!["E0004"],
        vec!["E0063"],
        vec!["E0639"],
    ];
    assert_eq!(got, want);

    // 처음부터 대비해 두면 같은 변경이 쓰는 쪽을 깨지 않는다.
    assert!(against(&d, READY_V1, READY_USER).is_empty());
    assert!(against(&d, READY_V2, READY_USER).is_empty());
    // 대가: 대비한 판에서는 밖에서 구조체 리터럴로 만들 수 없다.
    let literal = format!(
        "{READY_USER}pub fn lit() -> store::Link {{ store::Link {{ code: String::new(), url: String::new() }} }}\n"
    );
    assert_eq!(against(&d, READY_V1, &literal), ["E0639"]);
    println!("대비한 1판 → 2판: 쓰는 쪽 통과, 대신 밖에서 구조체 리터럴은 E0639");
}

fn check_naming_lints() {
    let d = tmp("naming");
    let src = "STRUCT Code(String);\n\
               impl Code {\n\
                   FNS fn into_string(&self) -> String { self.0.clone() }\n\
                   FNS fn as_string(self) -> String { self.0 }\n\
               }\n\
               pub fn touch() -> String { let c = Code(String::new()); let a = c.into_string(); a + &c.as_string() }\n";
    let private = src.replace("STRUCT", "struct").replace("FNS ", "");
    let public = src.replace("STRUCT", "pub struct").replace("FNS", "pub");
    let r1 = clippy(&d, &private, &[], None);
    assert!(
        r1.contains("methods called `into_*` usually take `self` by value"),
        "{r1}"
    );
    assert!(
        r1.contains("methods called `as_*` usually take `self` by reference"),
        "{r1}"
    );
    let r2 = clippy(&d, &public, &[], None);
    // "error"가 없어야 조각이 실제로 컴파일된 것이다(호출이 실패해도 린트 이름은 안 나온다).
    assert!(
        !r2.contains("error") && !r2.contains("wrong_self_convention"),
        "{r2}"
    );
    let conf = d.join("conf");
    std::fs::create_dir_all(&conf).unwrap();
    std::fs::write(
        conf.join("clippy.toml"),
        "avoid-breaking-exported-api = false\n",
    )
    .unwrap();
    let r3 = clippy(&d, &public, &[], Some(&conf));
    assert!(r3.contains("clippy::wrong_self_convention"), "{r3}");
    println!(
        "wrong_self_convention: 비공개는 경고, 공개는 기본으로 조용, avoid-breaking-exported-api = false면 경고"
    );
}

fn main() {
    check_builder();
    check_must_use();
    check_sealed();
    check_semver();
    check_naming_lints();
    println!("ALL CHECKS PASSED");
}

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

    #[test]
    fn base62_round_numbers() {
        assert_eq!(Base62.encode(62), "10");
        assert_eq!(Base62.encode(62 * 62), "100");
    }

    #[test]
    fn error_codes_parse() {
        let e = "error[E0046]: x\nerror[E0046]: y\nerror[E0004]: z\n";
        assert_eq!(error_codes(e), ["E0046", "E0004"]);
    }

    #[test]
    fn builder_rejects_zero_timeout() {
        let r = ServerConfig::builder("a").timeout(Duration::ZERO).build();
        assert_eq!(r, Err(BuildError::ZeroTimeout));
    }
}