Skip to content

Commit 0be95a9

Browse files
committed
config: Init config loading
1 parent 2dd5c10 commit 0be95a9

File tree

9 files changed

+210
-32
lines changed

9 files changed

+210
-32
lines changed

src/app/handler.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ use std::collections::BTreeMap;
22
use std::path::PathBuf;
33

44
use crate::cli::{CliArgs, Command, ListFormat};
5+
use crate::config::ConfigError;
56
use crate::edit::{InputMap, sorted_input_ids};
67
use crate::input::Follows;
78
use crate::tui;
@@ -25,6 +26,9 @@ pub enum HandlerError {
2526
#[error(transparent)]
2627
FlakeEdit(#[from] crate::error::FlakeEditError),
2728

29+
#[error(transparent)]
30+
Config(#[from] ConfigError),
31+
2832
#[error("Flake not found")]
2933
FlakeNotFound,
3034
}
@@ -48,7 +52,7 @@ pub fn run(args: CliArgs) -> Result<()> {
4852
let interactive = tui::is_interactive(args.non_interactive());
4953

5054
let no_cache = args.no_cache();
51-
let state = AppState::new(editor.text(), flake_path)
55+
let state = AppState::new(editor.text(), flake_path, args.config().map(PathBuf::from))?
5256
.with_diff(args.diff())
5357
.with_no_lock(args.no_lock())
5458
.with_interactive(interactive)

src/app/state.rs

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use std::path::PathBuf;
22

33
use crate::cache::CacheConfig;
4-
use crate::config::Config;
4+
use crate::config::{Config, ConfigError};
55

66
/// Application state for a flake-edit session.
77
///
@@ -29,8 +29,12 @@ pub struct AppState {
2929
}
3030

3131
impl AppState {
32-
pub fn new(flake_text: String, flake_path: PathBuf) -> Self {
33-
Self {
32+
pub fn new(
33+
flake_text: String,
34+
flake_path: PathBuf,
35+
config_path: Option<PathBuf>,
36+
) -> Result<Self, ConfigError> {
37+
Ok(Self {
3438
flake_text,
3539
flake_path,
3640
lock_file: None,
@@ -39,8 +43,8 @@ impl AppState {
3943
interactive: true,
4044
no_cache: false,
4145
cache_path: None,
42-
config: Config::load(),
43-
}
46+
config: Config::load_from(config_path.as_deref())?,
47+
})
4448
}
4549

4650
pub fn with_diff(mut self, diff: bool) -> Self {

src/cli.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@ pub struct CliArgs {
2929
/// Path to a custom cache file (for testing or portable configs).
3030
#[arg(long)]
3131
cache: Option<String>,
32+
/// Path to a custom configuration file.
33+
#[arg(long)]
34+
config: Option<String>,
3235

3336
#[command(subcommand)]
3437
subcommand: Command,
@@ -94,6 +97,10 @@ impl CliArgs {
9497
pub fn cache(&self) -> Option<&String> {
9598
self.cache.as_ref()
9699
}
100+
101+
pub fn config(&self) -> Option<&String> {
102+
self.config.as_ref()
103+
}
97104
}
98105

99106
#[derive(Subcommand, Debug)]

src/config.rs

Lines changed: 50 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,23 @@ use std::path::{Path, PathBuf};
55
/// Default configuration embedded in the binary.
66
pub const DEFAULT_CONFIG_TOML: &str = include_str!("assets/config.toml");
77

8+
/// Error type for configuration loading failures.
9+
#[derive(Debug, thiserror::Error)]
10+
pub enum ConfigError {
11+
#[error("Failed to read config file '{path}': {source}")]
12+
Io {
13+
path: PathBuf,
14+
#[source]
15+
source: std::io::Error,
16+
},
17+
#[error("Failed to parse config file '{path}':\n{source}")]
18+
Parse {
19+
path: PathBuf,
20+
#[source]
21+
source: toml::de::Error,
22+
},
23+
}
24+
825
/// Filenames to search for project-level configuration.
926
const CONFIG_FILENAMES: &[&str] = &["flake-edit.toml", ".flake-edit.toml"];
1027

@@ -80,10 +97,39 @@ impl Config {
8097
/// 1. Project-level config (flake-edit.toml or .flake-edit.toml in current/parent dirs)
8198
/// 2. User-level config (~/.config/flake-edit/config.toml)
8299
/// 3. Default embedded config
83-
pub fn load() -> Self {
84-
Self::load_project_config()
85-
.or_else(Self::load_user_config)
86-
.unwrap_or_default()
100+
///
101+
/// Returns an error if a config file exists but is malformed.
102+
pub fn load() -> Result<Self, ConfigError> {
103+
if let Some(path) = Self::project_config_path() {
104+
return Self::try_load_from_file(&path);
105+
}
106+
if let Some(path) = Self::user_config_path() {
107+
return Self::try_load_from_file(&path);
108+
}
109+
Ok(Self::default())
110+
}
111+
112+
/// Load configuration from an explicitly specified path.
113+
///
114+
/// Returns an error if the file doesn't exist or is malformed.
115+
/// If no path is specified, falls back to the default load order.
116+
pub fn load_from(path: Option<&Path>) -> Result<Self, ConfigError> {
117+
match path {
118+
Some(p) => Self::try_load_from_file(p),
119+
None => Self::load(),
120+
}
121+
}
122+
123+
/// Try to load config from a file, returning detailed errors on failure.
124+
fn try_load_from_file(path: &Path) -> Result<Self, ConfigError> {
125+
let content = std::fs::read_to_string(path).map_err(|e| ConfigError::Io {
126+
path: path.to_path_buf(),
127+
source: e,
128+
})?;
129+
toml::from_str(&content).map_err(|e| ConfigError::Parse {
130+
path: path.to_path_buf(),
131+
source: e,
132+
})
87133
}
88134

89135
pub fn project_config_path() -> Option<PathBuf> {
@@ -105,17 +151,6 @@ impl Config {
105151
Self::xdg_config_dir()
106152
}
107153

108-
fn load_project_config() -> Option<Self> {
109-
let path = Self::project_config_path()?;
110-
Self::load_from_file(&path)
111-
}
112-
113-
/// Load user-level config from XDG config directory.
114-
fn load_user_config() -> Option<Self> {
115-
let path = Self::user_config_path()?;
116-
Self::load_from_file(&path)
117-
}
118-
119154
fn find_config_in_ancestors(start: &Path) -> Option<PathBuf> {
120155
let mut current = start.to_path_buf();
121156
loop {
@@ -131,17 +166,6 @@ impl Config {
131166
}
132167
None
133168
}
134-
135-
fn load_from_file(path: &Path) -> Option<Self> {
136-
let content = std::fs::read_to_string(path).ok()?;
137-
match toml::from_str(&content) {
138-
Ok(config) => Some(config),
139-
Err(e) => {
140-
tracing::warn!("Failed to parse config at {}: {}", path.display(), e);
141-
None
142-
}
143-
}
144-
}
145169
}
146170

147171
#[cfg(test)]

tests/cli.rs

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,11 @@ fn fixture_lock_path(name: &str) -> String {
1919
format!("{dir}/tests/fixtures/{name}.flake.lock")
2020
}
2121

22+
fn fixture_config_path(name: &str) -> String {
23+
let dir = env!("CARGO_MANIFEST_DIR");
24+
format!("{dir}/tests/fixtures/{name}.config.toml")
25+
}
26+
2227
const FIXTURE_MARKER: &str = "/tests/fixtures/";
2328

2429
/// Add redaction to filter environment-dependent fixture paths in args metadata.
@@ -38,6 +43,12 @@ fn error_filters(settings: &mut insta::Settings) {
3843
settings.add_filter(r"\.rs:\d+", ".rs:<LINE>");
3944
}
4045

46+
/// Filter fixture paths in stderr output (e.g., config parse errors).
47+
fn stderr_path_filters(settings: &mut insta::Settings) {
48+
// Replace absolute paths containing /tests/fixtures/ with [FIXTURES]/
49+
settings.add_filter(r"'[^']*(/tests/fixtures/)([^']+)'", "'[FIXTURES]/$2'");
50+
}
51+
4152
#[rstest]
4253
#[case("root")]
4354
#[case("root_alt")]
@@ -422,3 +433,52 @@ fn test_follow_auto(#[case] fixture: &str) {
422433
);
423434
});
424435
}
436+
437+
/// Test the follow --auto command with a custom config file
438+
#[rstest]
439+
#[case("centerpiece", "ignore_treefmt")] // Config ignores treefmt-nix.nixpkgs, only home-manager follows
440+
fn test_follow_auto_with_config(#[case] fixture: &str, #[case] config: &str) {
441+
let mut settings = insta::Settings::clone_current();
442+
path_redactions(&mut settings);
443+
let suffix = format!("{fixture}_{config}");
444+
settings.set_snapshot_suffix(suffix);
445+
settings.bind(|| {
446+
assert_cmd_snapshot!(
447+
cli()
448+
.arg("--flake")
449+
.arg(fixture_path(fixture))
450+
.arg("--lock-file")
451+
.arg(fixture_lock_path(fixture))
452+
.arg("--config")
453+
.arg(fixture_config_path(config))
454+
.arg("--diff")
455+
.arg("follow")
456+
.arg("--auto")
457+
);
458+
});
459+
}
460+
461+
/// Test behavior with a malformed config file (returns error with line info)
462+
#[rstest]
463+
#[case("centerpiece", "malformed")] // Malformed TOML shows parse error with line number
464+
fn test_follow_auto_with_malformed_config(#[case] fixture: &str, #[case] config: &str) {
465+
let mut settings = insta::Settings::clone_current();
466+
path_redactions(&mut settings);
467+
stderr_path_filters(&mut settings);
468+
let suffix = format!("{fixture}_{config}");
469+
settings.set_snapshot_suffix(suffix);
470+
settings.bind(|| {
471+
assert_cmd_snapshot!(
472+
cli()
473+
.arg("--flake")
474+
.arg(fixture_path(fixture))
475+
.arg("--lock-file")
476+
.arg(fixture_lock_path(fixture))
477+
.arg("--config")
478+
.arg(fixture_config_path(config))
479+
.arg("--diff")
480+
.arg("follow")
481+
.arg("--auto")
482+
);
483+
});
484+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# Test config that ignores treefmt-nix.nixpkgs for follow --auto
2+
3+
[follow.auto]
4+
# Ignore the specific nested input treefmt-nix.nixpkgs
5+
ignore = ["treefmt-nix.nixpkgs"]
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# This is a malformed config file for testing error handling
2+
3+
[follow.auto
4+
# Missing closing bracket - invalid TOML syntax
5+
ignore = ["systems"]
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
---
2+
source: tests/cli.rs
3+
info:
4+
program: flake-edit
5+
args:
6+
- "--flake"
7+
- "[FIXTURES]/centerpiece.flake.nix"
8+
- "--lock-file"
9+
- "[FIXTURES]/centerpiece.flake.lock"
10+
- "--config"
11+
- "[FIXTURES]/ignore_treefmt.config.toml"
12+
- "--diff"
13+
- follow
14+
- "--auto"
15+
env:
16+
NO_COLOR: "1"
17+
---
18+
success: true
19+
exit_code: 0
20+
----- stdout -----
21+
--- original
22+
+++ modified
23+
@@ -4,6 +4,7 @@
24+
inputs = {
25+
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
26+
home-manager.url = "github:nix-community/home-manager";
27+
+ home-manager.inputs.nixpkgs.follows = "nixpkgs";
28+
treefmt-nix.url = "github:numtide/treefmt-nix";
29+
crane.url = "github:ipetkov/crane";
30+
};
31+
32+
----- stderr -----
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
---
2+
source: tests/cli.rs
3+
info:
4+
program: flake-edit
5+
args:
6+
- "--flake"
7+
- "[FIXTURES]/centerpiece.flake.nix"
8+
- "--lock-file"
9+
- "[FIXTURES]/centerpiece.flake.lock"
10+
- "--config"
11+
- "[FIXTURES]/malformed.config.toml"
12+
- "--diff"
13+
- follow
14+
- "--auto"
15+
env:
16+
NO_COLOR: "1"
17+
---
18+
success: false
19+
exit_code: 1
20+
----- stdout -----
21+
22+
----- stderr -----
23+
Error:
24+
0: Failed to parse config file '[FIXTURES]/malformed.config.toml':
25+
TOML parse error at line 3, column 13
26+
|
27+
3 | [follow.auto
28+
| ^
29+
invalid table header
30+
expected `.`, `]`
31+
32+
1: TOML parse error at line 3, column 13
33+
|
34+
3 | [follow.auto
35+
| ^
36+
invalid table header
37+
expected `.`, `]`

0 commit comments

Comments
 (0)