데이터 모델링 연습 문서의 문제 1 풀이 코드다.

//! p1 주문 상태 모델링: bool 필드 대신 열거형 상태와 self를 받는 전환 메서드.
//! 변형: clone 없이 반복문에서 전환하면 E0382, 실패할 때 옛 상태를 돌려주면 clone이 필요 없다.
//! 의존성: 없음 (표준 라이브러리만)
//! 실행: python3 "_시스템/Rust 계획/rs_check.py" "Rust/4.연습문제/14_data-modeling-practice_p1.rs"

const TAG: &str = "p1";

/// 코드 조각을 rustc로 컴파일해 stderr를 돌려준다.
fn compile_errors(code: &str) -> String {
    let dir =
        std::env::temp_dir().join(format!("vrM{}_{}_{}", TAG, std::process::id(), code.len()));
    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 실행");
    let _ = std::fs::remove_dir_all(&dir);
    String::from_utf8_lossy(&out.stderr).into_owned()
}

fn expect_phrases(code: &str, phrases: &[&str]) {
    let stderr = compile_errors(code);
    for phrase in phrases {
        assert!(stderr.contains(phrase), "'{phrase}' 기대, 실제:\n{stderr}");
        println!("컴파일 확인: {phrase}");
    }
}

fn expect_ok(code: &str) {
    let stderr = compile_errors(code);
    assert!(
        !stderr.contains("error"),
        "컴파일 성공 기대, 실제:\n{stderr}"
    );
}

#[derive(Debug, Clone, PartialEq)]
enum Order {
    Pending {
        items: u32,
    },
    Paid {
        items: u32,
        amount: u64,
    },
    Shipped {
        items: u32,
        amount: u64,
        tracking: String,
    },
    Cancelled {
        reason: String,
    },
}

#[derive(Debug, PartialEq)]
enum TransitionError {
    NotAllowed {
        from: &'static str,
        action: &'static str,
    },
    EmptyTracking,
}

impl Order {
    fn new(items: u32) -> Self {
        Order::Pending { items }
    }

    fn name(&self) -> &'static str {
        match self {
            Order::Pending { .. } => "Pending",
            Order::Paid { .. } => "Paid",
            Order::Shipped { .. } => "Shipped",
            Order::Cancelled { .. } => "Cancelled",
        }
    }

    fn pay(self, amount: u64) -> Result<Order, TransitionError> {
        match self {
            Order::Pending { items } => Ok(Order::Paid { items, amount }),
            other => Err(TransitionError::NotAllowed {
                from: other.name(),
                action: "pay",
            }),
        }
    }

    fn ship(self, tracking: &str) -> Result<Order, TransitionError> {
        if tracking.trim().is_empty() {
            return Err(TransitionError::EmptyTracking);
        }
        match self {
            Order::Paid { items, amount } => Ok(Order::Shipped {
                items,
                amount,
                tracking: tracking.trim().to_string(),
            }),
            other => Err(TransitionError::NotAllowed {
                from: other.name(),
                action: "ship",
            }),
        }
    }

    fn cancel(self, reason: &str) -> Result<Order, TransitionError> {
        match self {
            Order::Pending { .. } | Order::Paid { .. } => Ok(Order::Cancelled {
                reason: reason.to_string(),
            }),
            other => Err(TransitionError::NotAllowed {
                from: other.name(),
                action: "cancel",
            }),
        }
    }

    /// 변형: 실패하면 옛 주문을 에러와 함께 돌려준다. 부르는 쪽이 clone하지 않아도 된다.
    fn try_pay(self, amount: u64) -> Result<Order, (Order, TransitionError)> {
        match self {
            Order::Pending { items } => Ok(Order::Paid { items, amount }),
            other => {
                let from = other.name();
                Err((
                    other,
                    TransitionError::NotAllowed {
                        from,
                        action: "pay",
                    },
                ))
            }
        }
    }

    /// 운송장 번호는 Shipped에만 있다.
    fn tracking(&self) -> Option<&str> {
        match self {
            Order::Shipped { tracking, .. } => Some(tracking),
            _ => None,
        }
    }
}

fn run(script: &[&str]) -> String {
    let mut order = Order::new(2);
    let mut log = Vec::new();
    for step in script {
        let (cmd, arg) = step.split_once(' ').unwrap_or((step, ""));
        let next = match cmd {
            "pay" => order.clone().pay(arg.parse().unwrap_or(0)),
            "ship" => order.clone().ship(arg),
            "cancel" => order.clone().cancel(arg),
            _ => Err(TransitionError::NotAllowed {
                from: order.name(),
                action: "unknown",
            }),
        };
        match next {
            Ok(o) => {
                order = o;
                log.push(order.name().to_string());
            }
            Err(e) => log.push(format!("거절 {e:?}")),
        }
    }
    log.join(", ")
}

fn main() {
    let o = Order::new(2).pay(5000).unwrap().ship(" CJ123 ").unwrap();
    assert_eq!(o.tracking(), Some("CJ123"));
    assert_eq!(
        Order::new(1).ship("CJ1"),
        Err(TransitionError::NotAllowed {
            from: "Pending",
            action: "ship"
        })
    );
    let out = run(&[
        "ship CJ1",
        "pay 5000",
        "pay 5000",
        "ship CJ9",
        "cancel 변심",
    ]);
    println!("{out}");
    assert_eq!(
        out,
        "거절 NotAllowed { from: \"Pending\", action: \"ship\" }, Paid, 거절 NotAllowed { from: \"Paid\", action: \"pay\" }, Shipped, 거절 NotAllowed { from: \"Shipped\", action: \"cancel\" }"
    );
    let out2 = run(&["pay 100", "ship  ", "cancel 품절"]);
    println!("{out2}");
    assert_eq!(out2, "Paid, 거절 EmptyTracking, Cancelled");

    // 변형: clone 없이 self를 받는 전환을 반복문에서 부르면 E0382
    expect_phrases(
        r#"pub enum Order { Pending, Paid }
impl Order {
    fn pay(self) -> Result<Order, &'static str> {
        match self { Order::Pending => Ok(Order::Paid), _ => Err("not allowed") }
    }
}
pub fn run(n: usize) -> usize {
    let mut order = Order::Pending;
    let mut fails = 0;
    for _ in 0..n {
        match order.pay() {
            Ok(o) => order = o,
            Err(_) => fails += 1,
        }
    }
    fails
}"#,
        &[
            "error[E0382]: use of moved value: `order`",
            "moved due to this method call, in previous iteration of loop",
            "this reinitialization might get skipped",
        ],
    );
    // 실패할 때 옛 상태를 돌려주면 clone 없이 컴파일된다
    expect_ok(
        r#"pub enum Order { Pending, Paid }
impl Order {
    fn pay(self) -> Result<Order, (Order, &'static str)> {
        match self { Order::Pending => Ok(Order::Paid), other => Err((other, "not allowed")) }
    }
}
pub fn run(n: usize) -> usize {
    let mut order = Order::Pending;
    let mut fails = 0;
    for _ in 0..n {
        match order.pay() {
            Ok(o) => order = o,
            Err((o, _)) => { order = o; fails += 1; }
        }
    }
    fails
}"#,
    );
    let paid = Order::new(3).try_pay(900).unwrap();
    let (back, err) = paid.clone().try_pay(1).unwrap_err();
    assert_eq!(back, paid);
    assert_eq!(
        err,
        TransitionError::NotAllowed {
            from: "Paid",
            action: "pay"
        }
    );
    println!("ALL CHECKS PASSED");
}

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

    #[test]
    fn cancelled_has_no_tracking() {
        let o = Order::new(1).cancel("x").unwrap();
        assert_eq!(o.tracking(), None);
        assert_eq!(o, Order::Cancelled { reason: "x".into() });
    }
}