Skip to content

Support multiple config file formats and refactor config module#2104

Merged
mre merged 15 commits intomasterfrom
feat-1930
Apr 2, 2026
Merged

Support multiple config file formats and refactor config module#2104
mre merged 15 commits intomasterfrom
feat-1930

Conversation

@mre
Copy link
Copy Markdown
Member

@mre mre commented Mar 25, 2026

This PR resolves #1930 by adding support for loading configuration from pyproject.toml and Cargo.toml. I've also decided to add package.json to the mix, as this is a very common way to configure tools in the JavaScript ecosystem.

While implementing this, I realized the config module had grown out of proportion, which caused me to split it up into logical submodules to keep the core high-level structs clean. (Sorry if that PR got a bit large due to that. I'm mostly just moving stuff around and adding the trait + impls.)

To handle the different config formats, I introduced a ConfigLoader trait. I went through a few iterations with this. Initially, I thought about a simple, imperative approach where we just attempt to strictly deserialize the entire file and return an option. However, because our config uses strict validation (denying unknown fields), a single typo by the user would cause the parsing to fail, and we would silently skip the file instead of telling them about the typo.

To fix this, I split the trait into is_match and load. The is_match method does a lightweight check just to see if the specific lychee section exists in the file. If it does, we pass it to load, which enforces strict schema deserialization and properly bubbles up validation errors so the user knows what went wrong.

Implementation-wise, I deliberately avoided heavyweight dependencies like the cargo_toml crate, which builds massive ASTs for the entire manifest. Instead, we use minimal, custom envelope structs with serde to extract only the lychee blocks. This keeps our binary size small and compilation fast. I also made sure that Cargo package metadata takes precedence over workspace metadata if both are present.

Finally, I added some tests for each loader to ensure invariants (valid blocks are mapped correctly, missing keys are ignored, precedence works as intended, and malformed schemas produce the expected errors).

Copy link
Copy Markdown
Member

@thomas-zahner thomas-zahner left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Awesome PR!

Comment thread lychee-bin/src/config/loaders/cargo_toml.rs Outdated
Comment thread lychee-bin/src/config/loaders/mod.rs Outdated
Comment thread lychee-bin/src/config/loaders/mod.rs Outdated
Comment thread lychee-bin/src/config/loaders/mod.rs Outdated
Comment thread lychee-bin/src/config/loaders/package_json.rs Outdated
Comment thread lychee-bin/src/config/loaders/package_json.rs Outdated
Comment thread lychee-bin/src/config/loaders/pyproject_toml.rs
Comment thread lychee-bin/src/main.rs Outdated
Comment thread lychee-bin/tests/cli.rs Outdated
Comment thread lychee-bin/tests/cli.rs Outdated
@mre mre force-pushed the feat-1930 branch 2 times, most recently from 677c5fe to 752a50a Compare March 26, 2026 23:19
@mre
Copy link
Copy Markdown
Member Author

mre commented Mar 26, 2026

Addressed all comments. (Thanks, Thomas!)

Also added a test which verifies that if the user points directly to a configuration file that doesn't contain a valid lychee section (like a basic package.json), the tool will output an error message "No valid lychee configuration found in" instead of silently accepting it and using default configuration. =)

Any thoughts?

@thomas-zahner
Copy link
Copy Markdown
Member

Also added a test which verifies that if the user points directly to a configuration file that doesn't contain a valid lychee section (like a basic package.json), the tool will output an error message "No valid lychee configuration found in" instead of silently accepting it and using default configuration. =)

That's great, thank you! Now I just think that pyproject still falls back to the default config and behaves differently, we could update that if possible and add that to the new test, which currently only covers package.json.

Copy link
Copy Markdown
Member

@katrinafyi katrinafyi left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Splitting it up into traits and loader module seems reasonable. Some comments.

Comment thread lychee-bin/src/config/loaders/cargo_toml.rs Outdated
Comment thread lychee-bin/src/config/loaders/mod.rs Outdated
mre added 9 commits March 31, 2026 14:28
This PR resolves #1930 by adding support for loading configuration from pyproject.toml and Cargo.toml. I've also decided to add package.json to the mix, as this is a very common way to configure tools in the JavaScript ecosystem.

While implementing this, I realized the config module had grown out of proportion, which caused me to split it up into logical submodules to keep the core high-level structs clean.

To handle the different config formats, I introduced a ConfigLoader trait. I went through a few iterations with this. Initially, I thought about a simple, imperative approach where we just attempt to strictly deserialize the entire file and return an option. However, because our config uses strict validation (denying unknown fields), a single typo by the user would cause the parsing to fail, and we would silently skip the file instead of telling them about the typo.

To fix this, I split the trait into is_match and load. The is_match method does a lightweight check just to see if the specific lychee section exists in the file. If it does, we pass it to load, which enforces strict schema deserialization and properly bubbles up validation errors so the user knows what went wrong.

Implementation-wise, I deliberately avoided heavyweight dependencies like the cargo_toml crate, which builds massive ASTs for the entire manifest. Instead, we use minimal, custom envelope structs with serde to extract only the lychee blocks. This keeps our binary size small and compilation fast. I also made sure that Cargo package metadata takes precedence over workspace metadata if both are present.

Finally, I added comprehensive tests for each loader to ensure that valid blocks are mapped correctly, missing keys are ignored, precedence works as intended, and malformed schemas produce the expected errors.
- Consolidate config loaders into a single constant
- Simplify Cargo.toml loader matching logic
- Make config sections in pyproject.toml and package.json required
- Improve error reporting when config section is missing
- Update tests to reflect stricter config matching
- Replace separate is_match/load methods with single load() returning
  ConfigMatch enum
- Use consistent .context() error handling instead of mixed approaches
- Remove unwrap_or_default() fallback in pyproject.toml loader
- Extend tests to cover missing lychee sections in all config file types
- Fix clippy large_enum_variant warning by boxing Config
- Replace separate is_match/load methods with single load() returning ConfigMatch enum
- Use consistent .context() error handling instead of mixed approaches
- Remove unwrap_or_default() fallback in pyproject.toml loader
- Extend tests to cover missing lychee sections in all config file types
- Fix clippy large_enum_variant warning by boxing Config
Comment thread lychee-bin/src/config/loaders/mod.rs Outdated
Comment thread lychee-bin/src/config/loaders/mod.rs Outdated
@mre
Copy link
Copy Markdown
Member Author

mre commented Apr 2, 2026

This was fun! The code turned out much better than my initial attempt thanks to both of your feedback. Much appreciated. I will go ahead and merge this. :)

@mre mre merged commit d5d22bc into master Apr 2, 2026
8 checks passed
@mre mre deleted the feat-1930 branch April 2, 2026 22:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support for pyproject.toml (and cargo.toml)

3 participants