Skip to content

Step 3: Config — file I/O and serialization#3

Open
OctavianTocan wants to merge 2 commits intomainfrom
rust-mentor/step-3-config-file-io
Open

Step 3: Config — file I/O and serialization#3
OctavianTocan wants to merge 2 commits intomainfrom
rust-mentor/step-3-config-file-io

Conversation

@OctavianTocan
Copy link
Copy Markdown
Owner

@OctavianTocan OctavianTocan commented Apr 24, 2026

Learning step — doc only. Read lessons/03-config-file-io-serde.md, implement the task in src/config.rs, make the tests pass.

Rust concepts: std::fs, serde deserialization, Default trait, Path/PathBuf
Source: src/core/config.ts (182 lines)
What you'll build: Config loading with JSON parsing, defaults, and file discovery

When tests pass, merge this PR.

Summary by Sourcery

Documentation:

  • Add a lesson explaining config loading with std::fs, serde-based JSON parsing, defaults, and file discovery for the Rust wt-cli project.

Rust concepts: std::fs, serde deserialization, Default trait, Path/PathBuf
Source files: src/core/config.ts (182 lines)

This PR is documentation only — no Rust code to review.
Read the lesson, implement the task, make the tests pass.
Copilot AI review requested due to automatic review settings April 24, 2026 15:33
@sourcery-ai
Copy link
Copy Markdown

sourcery-ai Bot commented Apr 24, 2026

Reviewer's Guide

Adds a new lesson on config loading and implements Rust-side configuration loading with defaults, JSON parsing, and file discovery, wiring it into the main module and types.

Sequence diagram for the new load_config process

sequenceDiagram
    actor User
    participant Main as Main_binary
    participant Config as Config_module
    participant Proc as Process_module
    participant FS as File_system
    participant Serde as Serde_JSON

    User->>Main: start wt-cli
    Main->>Config: load_config(cwd)

    Config->>Proc: run_process("git", ["rev-parse","--show-toplevel"], cwd)
    Proc-->>Config: Ok(git_root_path) or Err

    alt git root found
        Config->>FS: read_to_string(git_root_path + "/wt.config.json")
        alt wt.config.json exists
            FS-->>Config: Ok(json_string)
            Config->>Serde: from_str(json_string) as WtConfig
            Serde-->>Config: Ok(parsed_config) or Err
        else wt.config.json missing
            FS-->>Config: Err
            Config->>FS: read_to_string(git_root_path + "/package.json")
            alt package.json exists
                FS-->>Config: Ok(pkg_json_string)
                Config->>Serde: from_str(pkg_json_string) as Value
                Serde-->>Config: Ok(pkg_value) or Err
            else package.json missing
                FS-->>Config: Err
            end
        end
    else git root not found
        Proc-->>Config: Err
    end

    alt any JSON parse error
        Serde-->>Config: Err
        Config-->>Main: Err(String)
    else no config files or only partial config
        Config->>Config: defaults = get_default_config()
        Config->>Config: merged = overlay(parsed_fields, defaults)
        Config-->>Main: Ok(LoadedConfig{config: merged, source, root_path})
    end

    Main-->>User: continue with loaded configuration
Loading

Class diagram for Rust configuration types and loader module

classDiagram
    class WtConfig {
        +String worktree_dir
        +String main_branch
        +String default_base
        +String remote
        +bool auto_setup
        +u32 stale_days
        +SetupConfig setup
        +LifecycleScripts lifecycle_scripts
    }

    class SetupConfig {
        +Vec~String~ steps
    }

    class LifecycleScripts {
        +Option~String~ postsetup
        +Option~String~ preclean
    }

    class LoadedConfig {
        +WtConfig config
        +String source
        +String root_path
    }

    class ConfigModule {
        +get_default_config() WtConfig
        +load_config(cwd: &str) Result~LoadedConfig, String~
        +read_json_file(path: &str) Result~serde_json::Value, String~
    }

    WtConfig --> SetupConfig
    WtConfig --> LifecycleScripts
    LoadedConfig --> WtConfig
    ConfigModule ..> WtConfig
    ConfigModule ..> LoadedConfig
    ConfigModule ..> SetupConfig
    ConfigModule ..> LifecycleScripts
Loading

File-Level Changes

Change Details Files
Introduce documentation for step 3 explaining config loading, file I/O, and serde-based deserialization.
  • Add a new markdown lesson describing the architecture of config loading, file I/O with std::fs, and serde usage.
  • Document the expected Rust API in config.rs, including get_default_config, load_config, and read_json_file.
  • Describe how defaults, error handling, and file discovery should work for configuration.
lessons/03-config-file-io-serde.md
Implement configuration loading module that reads JSON files, applies defaults, and reports the source of configuration.
  • Create src/config.rs defining get_default_config to construct a WtConfig populated entirely from defaults.
  • Implement load_config to resolve git root via run_process, attempt reading wt.config.json, fall back to package.json#wt, and otherwise return default configuration with a 'defaults' source.
  • Add helper read_json_file to read and parse JSON files into serde_json::Value with string-based error reporting.
  • Include tests in config.rs covering default config values, serde defaults behavior, partial override behavior, invalid JSON handling, and end-to-end load_config behavior using temporary directories and git init.
src/config.rs
Enable serde defaults on configuration types and wire the new config module into the binary entrypoint.
  • Annotate WtConfig and related fields with #[serde(default)] and, where needed, explicit default provider functions to match the documented defaults (worktree_dir, main_branch, default_base, remote, auto_setup, stale_days, setup, lifecycle_scripts).
  • Ensure SetupConfig and LifecycleScripts provide appropriate Default implementations or serde defaults to support missing JSON fields.
  • Update src/main.rs to declare the new config module alongside types and process modules so it compiles and is available for future commands.
src/types.rs
src/main.rs

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 24, 2026

Warning

Rate limit exceeded

@OctavianTocan has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 57 minutes and 37 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 57 minutes and 37 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: deb6df98-cc8c-4dcb-976d-912ead7c3600

📥 Commits

Reviewing files that changed from the base of the PR and between 588f155 and b6f9d0e.

📒 Files selected for processing (1)
  • lessons/03-config-file-io-serde.md
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch rust-mentor/step-3-config-file-io

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@sourcery-ai sourcery-ai Bot left a comment

Choose a reason for hiding this comment

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

Hey - I've reviewed your changes and they look great!


Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds Step 3 lesson documentation for implementing config discovery, JSON file I/O, and serde-based deserialization with defaults in the Rust rewrite of wt-cli.

Changes:

  • Add a new lesson document describing config loading behavior (git root discovery, config file search, JSON parsing, defaults).
  • Provide an implementation outline and test skeleton for src/config.rs.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread lessons/03-config-file-io-serde.md Outdated
Comment on lines +89 to +97
**Update `src/types.rs`** — add `#[serde(default)]` annotations to WtConfig fields that have defaults:
- `worktree_dir` defaults to `"tree"`
- `main_branch` defaults to `"main"`
- `default_base` defaults to `"main"`
- `remote` defaults to `"origin"`
- `auto_setup` defaults to `true`
- `stale_days` defaults to `30`
- `setup` defaults to `SetupConfig { steps: vec![] }`
- `lifecycle_scripts` defaults to `LifecycleScripts { postsetup: None, preclean: None }`
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

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

The instruction to "add #[serde(default)] annotations" is insufficient for these fields: #[serde(default)] uses Default::default() for the field type (e.g., String -> "", bool -> false, u32 -> 0), so it won’t produce defaults like "tree", true, or 30. Consider updating the lesson to recommend #[serde(default = "...")] per field, or implementing Default for WtConfig and adding #[serde(default)] at the struct level so missing fields come from WtConfig::default().

Copilot uses AI. Check for mistakes.
Comment on lines +147 to +152
#[test]
fn parse_partial_json_overrides_defaults() {
// Create JSON: {"worktreeDir": "custom-tree", "staleDays": 14}
// Parse into WtConfig
// Assert worktree_dir is "custom-tree" but main_branch is still "main" (default)
}
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

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

The lesson (and test skeleton) uses camelCase JSON keys like worktreeDir / staleDays, but the Rust example uses snake_case fields (worktree_dir, stale_days). Unless you mention adding #[serde(rename_all = "camelCase")] (or per-field #[serde(rename = "...")]) on WtConfig and nested structs, serde_json deserialization will fail to find those fields.

Copilot uses AI. Check for mistakes.
Comment thread lessons/03-config-file-io-serde.md Outdated
Comment on lines +35 to +46
Checking if a file exists:
```rust
let path = Path::new("wt.config.json");
if path.exists() {
let content = fs::read_to_string(path)?;
}
```

**vs TypeScript:** `fs.readFileSync(path, "utf8")` — similar, but Rust makes the error handling explicit instead of throwing.

**Common gotcha:** `Path::new("file")` creates a path reference but doesn't check if the file exists. `.exists()` does a syscall. Don't check-then-read (race condition) — just read and handle the error. But for this module, we check because we want to try multiple candidate files.

Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

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

This section warns against check-then-read due to TOCTOU, but the example and later guidance still encourages path.exists() followed by read_to_string(). For the “multiple candidate files” case, it’s safer and simpler to attempt the read directly and, on io::ErrorKind::NotFound, fall back to the next candidate (avoids the race and an extra syscall).

Copilot uses AI. Check for mistakes.
Comment on lines +85 to +106
## Your Task

**File:** `src/config.rs` (create this file)

**Update `src/types.rs`** — add `#[serde(default)]` annotations to WtConfig fields that have defaults:
- `worktree_dir` defaults to `"tree"`
- `main_branch` defaults to `"main"`
- `default_base` defaults to `"main"`
- `remote` defaults to `"origin"`
- `auto_setup` defaults to `true`
- `stale_days` defaults to `30`
- `setup` defaults to `SetupConfig { steps: vec![] }`
- `lifecycle_scripts` defaults to `LifecycleScripts { postsetup: None, preclean: None }`

Implement `src/config.rs` with these functions:

**`pub fn get_default_config() -> WtConfig`**
- Returns a WtConfig with all default values (matching the TypeScript `getDefaultConfig()`)

**`pub fn load_config(cwd: &str) -> Result<LoadedConfig, String>`**
- First, find the git root by running `git rev-parse --show-toplevel` (use your `process::run_process`)
- Then try reading `wt.config.json` from that root — if it exists, parse it as WtConfig
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

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

The task refers to src/types.rs, src/process.rs, and process::run_process, but this repository currently only has src/main.rs. If this step assumes earlier lessons/PRs created those modules, it would help to explicitly say so (or link to the relevant steps); otherwise the instructions should include creating those files so the step is runnable.

Copilot uses AI. Check for mistakes.
Added the full original config.ts and a line-by-line translation
walkthrough explaining zod→serde, async readFile→sync std::fs,
candidate loop→match-on-Result, and PathBuf for path operations.
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b6f9d0eb07

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

- **Zod → serde.** Both do the same job: parse + validate + apply defaults. Zod is a runtime library. Serde is a compile-time derive macro — it generates the parsing code at build time.
- `z.string().default("tree")` → `#[serde(default = "default_worktree_dir")]`. You write a function that returns the default value. If the JSON field is missing, serde calls it.
- `z.string().optional()` → `Option<String>` with `#[serde(default)]`. `Option<T>` already defaults to `None`, so `#[serde(default)]` is enough — no custom function needed.
- `z.number().int().positive()` → `u32`. Rust's type system enforces "positive integer" at compile time. No runtime validation needed for that constraint.
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve strict positivity when translating staleDays

The walkthrough says z.number().int().positive() can be represented by u32 without extra validation, but u32 permits 0 whereas Zod positive() requires > 0. If learners implement this as written, {"staleDays": 0} becomes valid in Rust even though it is invalid in the TypeScript source, changing config semantics and potentially altering stale-cleanup behavior; the lesson should require a non-zero type or explicit runtime check.

Useful? React with 👍 / 👎.

.map_err(|e| format!("Invalid config in {}: {}", source, e))?;
return Ok(LoadedConfig { config, source: source.to_string(), root_path });
}
Err(e) if e.contains("No such file") => continue, // file doesn't exist, try next
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Replace string-matching for missing-file detection

The sample load_config loop classifies missing files using e.contains("No such file"), which depends on localized OS error text. On platforms/locales where the message differs (for example Windows), absent config files can be treated as hard failures instead of falling through to the next candidate/defaults. The lesson should demonstrate checking ErrorKind::NotFound before stringifying errors.

Useful? React with 👍 / 👎.

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.

2 participants