데이터 모델링 연습 문서의 문제 3 풀이 코드다.
//! p3 설정 파싱: `key = value` 줄을 읽어 Config로 바꾼다. 실패는 줄 번호를 담은 에러 열거형,
//! 빠진 값은 Option과 ok_or, 기본값은 unwrap_or, 0이 아닌 포트는 NonZero<u16>.
//! 변형: workers = 0은 지금 통과한다. NonZero는 FromStr을 구현하므로 parse::<NonZero<_>>로 0을 한 번에 거른다.
//! 의존성: 없음 (표준 라이브러리만)
//! 실행: python3 "_시스템/Rust 계획/rs_check.py" "Rust/4.연습문제/14_data-modeling-practice_p3.rs"
use std::collections::HashMap;
use std::num::NonZero;
#[derive(Debug, PartialEq)]
struct Config {
host: String,
port: NonZero<u16>,
workers: usize,
}
#[derive(Debug, PartialEq)]
enum ConfigError {
BadLine { line: usize, text: String },
Duplicate { line: usize, key: String },
Missing(&'static str),
BadValue { key: &'static str, value: String },
}
fn parse_config(src: &str) -> Result<Config, ConfigError> {
let mut map: HashMap<&str, &str> = HashMap::new();
for (i, raw) in src.lines().enumerate() {
let line = raw.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
let Some((k, v)) = line.split_once('=') else {
return Err(ConfigError::BadLine {
line: i + 1,
text: line.to_string(),
});
};
let (k, v) = (k.trim(), v.trim());
if map.insert(k, v).is_some() {
return Err(ConfigError::Duplicate {
line: i + 1,
key: k.to_string(),
});
}
}
let host = map.get("host").ok_or(ConfigError::Missing("host"))?;
let port_text = map.get("port").ok_or(ConfigError::Missing("port"))?;
let port = port_text
.parse::<u16>()
.ok()
.and_then(NonZero::new)
.ok_or_else(|| ConfigError::BadValue {
key: "port",
value: port_text.to_string(),
})?;
let workers = match map.get("workers") {
None => 4,
Some(w) => w.parse::<usize>().map_err(|_| ConfigError::BadValue {
key: "workers",
value: w.to_string(),
})?,
};
Ok(Config {
host: host.to_string(),
port,
workers,
})
}
/// 변형: workers도 0을 막는다. NonZero<usize>로 바로 parse한다.
fn parse_workers(text: Option<&str>) -> Result<NonZero<usize>, ConfigError> {
match text {
None => Ok(NonZero::new(4).expect("4는 0이 아니다")),
Some(w) => w
.parse::<NonZero<usize>>()
.map_err(|_| ConfigError::BadValue {
key: "workers",
value: w.to_string(),
}),
}
}
fn main() {
let ok = parse_config("# 서버\nhost = 0.0.0.0\nport = 8080\n");
println!("{ok:?}");
assert_eq!(
ok,
Ok(Config {
host: "0.0.0.0".into(),
port: NonZero::new(8080).unwrap(),
workers: 4
})
);
let cases = [
(
"host = a\nport 8080",
"BadLine { line: 2, text: \"port 8080\" }",
),
(
"host = a\nport = 1\nhost = b",
"Duplicate { line: 3, key: \"host\" }",
),
("port = 80", "Missing(\"host\")"),
(
"host = a\nport = 0",
"BadValue { key: \"port\", value: \"0\" }",
),
(
"host = a\nport = 70000",
"BadValue { key: \"port\", value: \"70000\" }",
),
(
"host = a\nport = 1\nworkers = many",
"BadValue { key: \"workers\", value: \"many\" }",
),
];
for (src, want) in cases {
let got = format!("{:?}", parse_config(src).unwrap_err());
println!("{got}");
assert_eq!(got, want);
}
// 변형: 지금은 workers = 0이 통과한다
assert_eq!(
parse_config("host=a\nport=1\nworkers=0").unwrap().workers,
0
);
assert_eq!(parse_workers(None).unwrap().get(), 4);
assert_eq!(parse_workers(Some("16")).unwrap().get(), 16);
assert_eq!(
format!("{:?}", parse_workers(Some("0")).unwrap_err()),
"BadValue { key: \"workers\", value: \"0\" }"
);
// port도 parse::<NonZero<u16>>() 한 번으로 같은 결과
for t in ["0", "1", "8080", "65535", "65536", "70000", "-1", "abc", ""] {
let two_steps = t.parse::<u16>().ok().and_then(NonZero::new);
let one_step = t.parse::<NonZero<u16>>().ok();
assert_eq!(two_steps, one_step, "{t}");
}
println!("ALL CHECKS PASSED");
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn workers_given() {
let c = parse_config("host=h\nport=1\nworkers=16").unwrap();
assert_eq!(c.workers, 16);
assert_eq!(c.port.get(), 1);
}
}