Skip to content

Commit 85d75a3

Browse files
committed
refactor(hm-config): Replace stringly-typed backend: String with a closed enum
1 parent 904721f commit 85d75a3

4 files changed

Lines changed: 50 additions & 18 deletions

File tree

crates/hm-config/src/lib.rs

Lines changed: 39 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,37 @@ pub mod creds;
1515

1616
pub const DEFAULT_API_URL: &str = "https://api.harmont.dev";
1717

18-
/// Default execution backend for `hm run` when no `--backend`/`--cloud` flag
19-
/// is given.
20-
fn default_backend() -> String {
21-
"docker".to_owned()
18+
/// Execution backend for `hm run`. Closed set parsed at the config boundary so
19+
/// invalid values are rejected at deserialize time instead of mis-dispatching
20+
/// later, and every consumer match is exhaustively checked by the compiler.
21+
#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
22+
#[serde(rename_all = "lowercase")]
23+
pub enum Backend {
24+
Docker,
25+
Cloud,
26+
}
27+
28+
impl Default for Backend {
29+
fn default() -> Self {
30+
Backend::Docker
31+
}
32+
}
33+
34+
impl Backend {
35+
/// Stable lowercase wire/CLI name (matches the `serde` representation).
36+
#[must_use]
37+
pub fn as_str(self) -> &'static str {
38+
match self {
39+
Backend::Docker => "docker",
40+
Backend::Cloud => "cloud",
41+
}
42+
}
43+
}
44+
45+
impl std::fmt::Display for Backend {
46+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
47+
f.write_str(self.as_str())
48+
}
2249
}
2350

2451
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
@@ -53,8 +80,8 @@ impl Default for Preferences {
5380

5481
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
5582
pub struct Config {
56-
#[serde(default = "default_backend")]
57-
pub backend: String,
83+
#[serde(default)]
84+
pub backend: Backend,
5885
#[serde(default)]
5986
pub cloud: CloudConfig,
6087
#[serde(default)]
@@ -64,7 +91,7 @@ pub struct Config {
6491
impl Default for Config {
6592
fn default() -> Self {
6693
Self {
67-
backend: default_backend(),
94+
backend: Backend::default(),
6895
cloud: CloudConfig::default(),
6996
preferences: Preferences::default(),
7097
}
@@ -156,7 +183,7 @@ mod tests {
156183
#[test]
157184
fn default_config_values() {
158185
let cfg = Config::default();
159-
assert_eq!(cfg.backend, "docker");
186+
assert_eq!(cfg.backend, Backend::Docker);
160187
assert_eq!(cfg.cloud.api_url, DEFAULT_API_URL);
161188
assert!(cfg.cloud.org.is_none());
162189
assert_eq!(cfg.preferences.format, "human");
@@ -241,7 +268,7 @@ org = "project-org"
241268
#[test]
242269
fn backend_defaults_docker_and_parses_and_layers() {
243270
// default
244-
assert_eq!(Config::default().backend, "docker");
271+
assert_eq!(Config::default().backend, Backend::Docker);
245272

246273
// user file sets cloud; project file sets docker -> project wins.
247274
let mut user_file = tempfile::NamedTempFile::new().unwrap();
@@ -252,19 +279,19 @@ org = "project-org"
252279

253280
let cfg =
254281
Config::load_from_paths(Some(user_file.path()), Some(project_file.path())).unwrap();
255-
assert_eq!(cfg.backend, "docker");
282+
assert_eq!(cfg.backend, Backend::Docker);
256283

257284
// user file alone parses "cloud".
258285
let cfg_user = Config::load_from_paths(Some(user_file.path()), None).unwrap();
259-
assert_eq!(cfg_user.backend, "cloud");
286+
assert_eq!(cfg_user.backend, Backend::Cloud);
260287
}
261288

262289
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
263290
async fn save_and_reload_roundtrip() {
264291
let tmp = tempfile::tempdir().unwrap();
265292
let path = tmp.path().join("config.toml");
266293
let cfg = Config {
267-
backend: default_backend(),
294+
backend: Backend::default(),
268295
cloud: CloudConfig {
269296
org: Some("saved-org".into()),
270297
api_url: DEFAULT_API_URL.to_owned(),

crates/hm/src/commands/init.rs

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -302,10 +302,15 @@ pub async fn handle(args: InitArgs) -> Result<()> {
302302
if project_config.exists() {
303303
let cfg =
304304
hm_config::Config::load_from_paths(None, Some(&project_config)).unwrap_or_default();
305-
if cfg.backend == "cloud" {
306-
tracing::info!("next step: run `hm run` to execute your pipeline on Harmont Cloud");
307-
} else {
308-
tracing::info!("next step: run `hm run` to execute your pipeline locally");
305+
match cfg.backend {
306+
hm_config::Backend::Cloud => {
307+
tracing::info!(
308+
"next step: run `hm run` to execute your pipeline on Harmont Cloud"
309+
);
310+
}
311+
hm_config::Backend::Docker => {
312+
tracing::info!("next step: run `hm run` to execute your pipeline locally");
313+
}
309314
}
310315
} else {
311316
tracing::info!("next step: run `hm run` to execute your pipeline locally");

crates/hm/src/commands/run/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ pub async fn handle(args: RunArgs, ctx: RunContext) -> Result<i32> {
4343
None
4444
}
4545
})
46-
.unwrap_or_else(|| ctx.config.backend.clone());
46+
.unwrap_or_else(|| ctx.config.backend.to_string());
4747

4848
// 2. Cloud needs auth + org resolution up front — fail fast on a missing
4949
// token before any render work. We resolve the credentials here but

crates/hm/tests/cmd_init.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -489,7 +489,7 @@ fn cloud_project_config_layers_correctly() {
489489
std::fs::write(&config_path, content).unwrap();
490490

491491
let cfg = hm_config::Config::load_from_paths(None, Some(&config_path)).unwrap();
492-
assert_eq!(cfg.backend, "cloud");
492+
assert_eq!(cfg.backend, hm_config::Backend::Cloud);
493493
assert_eq!(cfg.cloud.org.as_deref(), Some("test-org"));
494494
// Unrelated defaults survive layering.
495495
assert_eq!(cfg.preferences.format, "human");

0 commit comments

Comments
 (0)