Skip to content

Conversation

@LeeFred3042U
Copy link
Contributor

@LeeFred3042U LeeFred3042U commented Dec 4, 2025

Summary

This PR replaces the previous single-file catalog.yaml with a directory-based catalog stored under embedded Catalog/** folders. The one-file approach caused frequent merge conflicts because every exercise lived in the same YAML document. Splitting the catalog so each exercise has its own YAML file eliminates those conflicts and makes adding new exercises much simpler.

catalog() now loads from Catalog/Concepts and Catalog/Projects using a new loadCatalogFromFS helper. Each exercise is defined by its own YAML file (e.g. 01_hello.yaml). If loading fails—or both directories are empty—the system falls back to the existing minimal inline catalog (01_hello) to preserve current behavior.

Tests using fstest.MapFS cover directory loading, ordering by slug, invalid YAML handling, ignored non-YAML files, missing directories, and fallback logic.

No CLI behavior changes. Existing public functions (ListAll, Get, InitAll, Reset) continue to work the same with the new catalog structure.


Checklist

  • Tests pass: All relevant internal/exercises tests pass (catalog loader, FS logic, fallback behavior).
    Exercise verification (make verify or golearn verify) is expected to fail on a clean repo because exercises contain TODOs.
  • Docs updated: Explanation added in PR description (no README changes required)
  • No large new dependencies (only fstest used for tests)

Screenshots / Output (if CLI UX)

Not applicable — feature is internal.
Behavior validated through automated tests:

go test ./internal/exercises -v
=== RUN   TestLoadExercisesDir_LoadsValidYAML
--- PASS: TestLoadExercisesDir_LoadsValidYAML (0.00s)
=== RUN   TestLoadExercisesDir_RejectsEmptySlug
--- PASS: TestLoadExercisesDir_RejectsEmptySlug (0.00s)
=== RUN   TestLoadExercisesDir_IgnoresNonYAML
--- PASS: TestLoadExercisesDir_IgnoresNonYAML (0.00s)
=== RUN   TestLoadCatalogFromFS_LoadsConceptsAndProjects
--- PASS: TestLoadCatalogFromFS_LoadsConceptsAndProjects (0.00s)
=== RUN   TestLoadCatalogFromFS_MissingDirsAreIgnored
--- PASS: TestLoadCatalogFromFS_MissingDirsAreIgnored (0.00s)
=== RUN   TestLoadCatalogFromFS_InvalidYAML
--- PASS: TestLoadCatalogFromFS_InvalidYAML (0.00s)
=== RUN   TestFallbackCatalog
--- PASS: TestFallbackCatalog (0.00s)
=== RUN   TestCatalog_UsesDirectoryLoader
--- PASS: TestCatalog_UsesDirectoryLoader (0.00s)
=== RUN   TestDiscoverLocal_NoDir
--- PASS: TestDiscoverLocal_NoDir (0.00s)
PASS
ok      github.com/zhravan/golearn/internal/exercises   (cached)
go vet ./...                                         (no warnings)

Related issues

Fixes #113

Summary by CodeRabbit

  • New Features

    • Dozens of new concept and project exercises added (basics through advanced topics such as concurrency, timers, generics, iterators, and rate limiting) with guiding hints.
    • Exercise templates and initialization now respect per-exercise directory mappings for consistent scaffolding.
  • Chores

    • Catalog migrated to individual exercise entries and CI verification made more flexible.
    • Catalog loading improved with deterministic ordering and a resilient fallback.
  • Tests

    • Added comprehensive tests for catalog loading, validation, discovery, sorting, and fallback behavior.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link

coderabbitai bot commented Dec 4, 2025

Warning

Rate limit exceeded

@LeeFred3042U has exceeded the limit for the number of commits or files that can be reviewed per hour. Please wait 24 minutes and 13 seconds before requesting another review.

⌛ 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.

📥 Commits

Reviewing files that changed from the base of the PR and between 3d4c893 and ee00e3e.

📒 Files selected for processing (64)
  • .github/workflows/ci.yml (3 hunks)
  • internal/exercises/Catalog/Concepts/01_hello.yaml (1 hunks)
  • internal/exercises/Catalog/Concepts/02_values.yaml (1 hunks)
  • internal/exercises/Catalog/Concepts/03_variables.yaml (1 hunks)
  • internal/exercises/Catalog/Concepts/04_constants.yaml (1 hunks)
  • internal/exercises/Catalog/Concepts/05_for.yaml (1 hunks)
  • internal/exercises/Catalog/Concepts/06_if_else.yaml (1 hunks)
  • internal/exercises/Catalog/Concepts/07_switch.yaml (1 hunks)
  • internal/exercises/Catalog/Concepts/08_arrays.yaml (1 hunks)
  • internal/exercises/Catalog/Concepts/09_slices.yaml (1 hunks)
  • internal/exercises/Catalog/Concepts/10_maps.yaml (1 hunks)
  • internal/exercises/Catalog/Concepts/11_functions.yaml (1 hunks)
  • internal/exercises/Catalog/Concepts/12_multi_return.yaml (1 hunks)
  • internal/exercises/Catalog/Concepts/13_variadic.yaml (1 hunks)
  • internal/exercises/Catalog/Concepts/14_closures.yaml (1 hunks)
  • internal/exercises/Catalog/Concepts/15_recursion.yaml (1 hunks)
  • internal/exercises/Catalog/Concepts/16_range_built_in.yaml (1 hunks)
  • internal/exercises/Catalog/Concepts/17_pointers.yaml (1 hunks)
  • internal/exercises/Catalog/Concepts/18_strings_runes.yaml (1 hunks)
  • internal/exercises/Catalog/Concepts/19_structs.yaml (1 hunks)
  • internal/exercises/Catalog/Concepts/20_methods.yaml (1 hunks)
  • internal/exercises/Catalog/Concepts/21_interfaces.yaml (1 hunks)
  • internal/exercises/Catalog/Concepts/22_enums.yaml (1 hunks)
  • internal/exercises/Catalog/Concepts/23_struct_embedding.yaml (1 hunks)
  • internal/exercises/Catalog/Concepts/24_generics.yaml (1 hunks)
  • internal/exercises/Catalog/Concepts/25_range_iterators.yaml (1 hunks)
  • internal/exercises/Catalog/Concepts/26_errors.yaml (1 hunks)
  • internal/exercises/Catalog/Concepts/27_custom_errors.yaml (1 hunks)
  • internal/exercises/Catalog/Concepts/28_defer.yaml (1 hunks)
  • internal/exercises/Catalog/Concepts/29_go_routines.yaml (1 hunks)
  • internal/exercises/Catalog/Concepts/30_channels.yaml (1 hunks)
  • internal/exercises/Catalog/Concepts/31_mutexes.yaml (1 hunks)
  • internal/exercises/Catalog/Concepts/32_sorting.yaml (1 hunks)
  • internal/exercises/Catalog/Concepts/33_string_formatting.yaml (1 hunks)
  • internal/exercises/Catalog/Concepts/34_channel_buffering.yaml (1 hunks)
  • internal/exercises/Catalog/Concepts/35_channel_sync.yaml (1 hunks)
  • internal/exercises/Catalog/Concepts/36_json.yaml (1 hunks)
  • internal/exercises/Catalog/Concepts/37_sorting_by_functions.yaml (1 hunks)
  • internal/exercises/Catalog/Concepts/37_xml.yaml (1 hunks)
  • internal/exercises/Catalog/Concepts/38_time_formatting.yaml (1 hunks)
  • internal/exercises/Catalog/Concepts/39_channel_directions.yaml (1 hunks)
  • internal/exercises/Catalog/Concepts/39_panic.yaml (1 hunks)
  • internal/exercises/Catalog/Concepts/40_channel_select.yaml (1 hunks)
  • internal/exercises/Catalog/Concepts/41_time_delay.yaml (1 hunks)
  • internal/exercises/Catalog/Concepts/42_wait_group.yaml (1 hunks)
  • internal/exercises/Catalog/Concepts/43_worker_pools.yaml (1 hunks)
  • internal/exercises/Catalog/Concepts/44_atomic_counters.yaml (1 hunks)
  • internal/exercises/Catalog/Concepts/45_range_over_channels.yaml (1 hunks)
  • internal/exercises/Catalog/Concepts/46_non_blocking_channel_operations.yaml (1 hunks)
  • internal/exercises/Catalog/Concepts/64_timers.yaml (1 hunks)
  • internal/exercises/Catalog/Concepts/68_rate_limiting.yaml (1 hunks)
  • internal/exercises/Catalog/Projects/01_text_analyzer.yaml (1 hunks)
  • internal/exercises/Catalog/Projects/02_shape_calculator.yaml (1 hunks)
  • internal/exercises/Catalog/Projects/03_task_scheduler.yaml (1 hunks)
  • internal/exercises/Catalog/Projects/04_http_server.yaml (1 hunks)
  • internal/exercises/Catalog/Projects/05_cli_todo_list.yaml (1 hunks)
  • internal/exercises/Catalog/Projects/06_simple_chat_app.yaml (1 hunks)
  • internal/exercises/Catalog/Projects/07_image_processing_utility.yaml (1 hunks)
  • internal/exercises/Catalog/Projects/08_basic_key_value_store.yaml (1 hunks)
  • internal/exercises/Catalog/Projects/09_epoch.yaml (1 hunks)
  • internal/exercises/exercises.go (4 hunks)
  • internal/exercises/exercises_test.go (1 hunks)
  • internal/exercises/export_test.go (1 hunks)
  • internal/exercises/solutions.go (5 hunks)

Walkthrough

Replaces the single embedded internal/exercises/catalog.yaml with per-exercise YAML files under internal/exercises/Catalog/{Concepts,Projects}, implements FS-based embedded catalog loading with local overrides and a test-friendly loader, updates Exercise to include Dir and Path(), changes template/solution discovery and sandboxing to use exercise Path/Dir, adds tests and a CI tweak.

Changes

Cohort / File(s) Summary
Catalog Concepts
internal/exercises/Catalog/Concepts/*.yaml
Examples: internal/exercises/Catalog/Concepts/01_hello.yaml, .../02_values.yaml, .../03_variables.yaml, .../04_constants.yaml, .../05_for.yaml, .../06_if_else.yaml, .../07_switch.yaml, .../08_arrays.yaml, .../09_slices.yaml, .../10_maps.yaml, .../11_functions.yaml, .../12_multi_return.yaml, .../13_variadic.yaml, .../14_closures.yaml, .../15_recursion.yaml, .../16_range_built_in.yaml, .../17_pointers.yaml, .../18_strings_runes.yaml, .../19_structs.yaml, .../20_methods.yaml, .../21_interfaces.yaml, .../22_enums.yaml, .../23_struct_embedding.yaml, .../24_generics.yaml, .../25_range_iterators.yaml, .../26_errors.yaml, .../27_custom_errors.yaml, .../28_defer.yaml, .../29_go_routines.yaml, .../30_channels.yaml, .../31_mutexes.yaml, .../32_sorting.yaml, .../33_string_formatting.yaml, .../34_channel_buffering.yaml, .../35_channel_sync.yaml, .../36_json.yaml, .../37_sorting_by_functions.yaml, .../37_xml.yaml, .../38_time_formatting.yaml, .../39_channel_directions.yaml, .../39_panic.yaml, .../40_channel_select.yaml, .../41_time_delay.yaml, .../42_wait_group.yaml, .../43_worker_pools.yaml, .../44_atomic_counters.yaml, .../45_range_over_channels.yaml, .../46_non_blocking_channel_operations.yaml, .../64_timers.yaml, .../68_rate_limiting.yaml
Added many independent concept YAML files (metadata: slug, title, test_regex, hints).
Catalog Projects
internal/exercises/Catalog/Projects/*.yaml
Examples: internal/exercises/Catalog/Projects/01_text_analyzer.yaml, .../02_shape_calculator.yaml, .../03_task_scheduler.yaml, .../04_http_server.yaml, .../05_cli_todo_list.yaml, .../06_simple_chat_app.yaml, .../07_image_processing_utility.yaml, .../08_basic_key_value_store.yaml, .../09_epoch.yaml
Added project YAML definitions (metadata: slug, dir, title, test_regex, hints). Noted a typo in one slug metadata.
Deleted Catalog
internal/exercises/catalog.yaml
Removed the monolithic embedded catalog.yaml.
Core Loading & APIs
internal/exercises/exercises.go
Switched embedding to //go:embed Catalog/**; added FS-based loaders (loadCatalogFromFS, loadExercisesDir), deterministic sorting, slug validation, catalog singleton with mutex/override, Exercise.Dir field and Exercise.Path() method, updated ListAll/Get/discovery and template-copy logic, path normalization, and added ErrNoTemplates.
Solution sandboxing
internal/exercises/solutions.go
SolutionExists signature changed to accept dirName; sandbox creation and template/solution paths now use exercise srcDir (via Get), adjusted path computations and error handling.
Tests & Test helpers
internal/exercises/exercises_test.go, internal/exercises/export_test.go
Added fstest.MapFS-based tests for catalog loading/validation/sorting/fallback/local overrides; added WithTestCatalogLoader test helper to swap the global catalog loader safely.
CI workflow
.github/workflows/ci.yml
Upgraded actions/cache to v4 and replaced multiple verify steps with a loop that conditionally runs verifies for existing solution dirs.

Sequence Diagram(s)

sequenceDiagram
    autonumber
    participant Caller as Caller (ListAll / Init / Get / InitAll)
    participant Loader as catalog loader (catalogLoader)
    participant LocalFS as Local filesystem (./exercises)
    participant EmbFS as Embedded FS (Catalog/**)
    participant Copier as Template copier / Sandbox creator

    Caller->>Loader: request catalog (ListAll/Get)
    Loader->>LocalFS: discoverLocal()
    alt local exercises found
        LocalFS-->>Loader: local entries
        Loader-->>Caller: return local-first entries
    else
        Loader->>EmbFS: loadCatalogFromFS()
        EmbFS-->>Loader: YAML files per-exercise
        Loader->>Loader: parse, validate, sort
        Loader-->>Caller: return embedded catalog
    end

    Caller->>Copier: Init exercise / InitAll
    Copier->>EmbFS: templateExists(templates/<ex.Path()>)
    alt templates exist
        EmbFS-->>Copier: template tree
        Copier->>LocalFS: create dirs & write files (use ex.Path()/ex.Dir)
        LocalFS-->>Copier: ack
        Copier-->>Caller: init complete
    else
        Copier-->>Caller: ErrNoTemplates
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

  • Focus areas:
    • loadCatalogFromFS / loadExercisesDir: FS traversal, YAML unmarshalling, slug validation, sorting.
    • catalog singleton, mutex and WithTestCatalogLoader interactions.
    • Template discovery/copying, path normalization (Exercise.Path vs Slug) and cross-platform path handling.
    • SolutionExists signature change and sandbox path computations.
    • Tests that exercise fallback catalog and local override behavior.

Possibly related issues

Possibly related PRs

Suggested labels

feat, enhancement

Suggested reviewers

  • zhravan

Poem

🐰 I split the catalog, each slug found its lair,
YAML files lined up tidy, hop-hop everywhere.
Loaders fetch from folders, templates plant their seed,
Tests nibble at the edges — ready for each need. 🥕

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 58.33% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the main change: switching from a single catalog.yaml file to a directory-based catalog loader structure.
Linked Issues check ✅ Passed The PR fully implements the requirements from issue #113: directory-based catalog structure (Catalog/Concepts and Catalog/Projects), individual YAML files per exercise, and eliminates merge conflicts by distributing exercises across multiple files.
Out of Scope Changes check ✅ Passed All changes are scoped to the catalog restructuring objective. The code modifications (exercises.go, solutions.go, exercises_test.go, export_test.go, ci.yml) directly support loading from the new directory structure without introducing unrelated functionality.

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

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🧹 Nitpick comments (3)
internal/exercises/Catalog/Concepts/39_panic.yaml (1)

1-9: Pattern adherence is correct; consider removing trailing blank line.

Slug-filename alignment is correct, YAML is valid. The four hints appropriately scaffold a complex topic (panic, defer, recover). Line 9 contains a trailing blank line that could be removed for consistency with other files.

internal/exercises/Catalog/Concepts/42_wait_group.yaml (1)

12-12: Minor: Remove trailing whitespace.

Line 12 contains only whitespace. Consider removing it for cleaner formatting.

internal/exercises/Catalog/Concepts/45_range_over_channels.yaml (1)

5-5: Minor: Fix grammatical error in hint. "Range over a channels" should be "Range over channels" (no article, singular).

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3484cee and 73e9450.

📒 Files selected for processing (62)
  • internal/exercises/Catalog/Concepts/01_hello.yaml (1 hunks)
  • internal/exercises/Catalog/Concepts/02_values.yaml (1 hunks)
  • internal/exercises/Catalog/Concepts/03_variables.yaml (1 hunks)
  • internal/exercises/Catalog/Concepts/04_constants.yaml (1 hunks)
  • internal/exercises/Catalog/Concepts/05_for.yaml (1 hunks)
  • internal/exercises/Catalog/Concepts/06_if_else.yaml (1 hunks)
  • internal/exercises/Catalog/Concepts/07_switch.yaml (1 hunks)
  • internal/exercises/Catalog/Concepts/08_arrays.yaml (1 hunks)
  • internal/exercises/Catalog/Concepts/09_slices.yaml (1 hunks)
  • internal/exercises/Catalog/Concepts/10_maps.yaml (1 hunks)
  • internal/exercises/Catalog/Concepts/11_functions.yaml (1 hunks)
  • internal/exercises/Catalog/Concepts/12_multi_return.yaml (1 hunks)
  • internal/exercises/Catalog/Concepts/13_variadic.yaml (1 hunks)
  • internal/exercises/Catalog/Concepts/14_closures.yaml (1 hunks)
  • internal/exercises/Catalog/Concepts/15_recursion.yaml (1 hunks)
  • internal/exercises/Catalog/Concepts/16_range_built_in.yaml (1 hunks)
  • internal/exercises/Catalog/Concepts/17_pointers.yaml (1 hunks)
  • internal/exercises/Catalog/Concepts/18_strings_runes.yaml (1 hunks)
  • internal/exercises/Catalog/Concepts/19_structs.yaml (1 hunks)
  • internal/exercises/Catalog/Concepts/20_methods.yaml (1 hunks)
  • internal/exercises/Catalog/Concepts/21_interfaces.yaml (1 hunks)
  • internal/exercises/Catalog/Concepts/22_enums.yaml (1 hunks)
  • internal/exercises/Catalog/Concepts/23_struct_embedding.yaml (1 hunks)
  • internal/exercises/Catalog/Concepts/24_generics.yaml (1 hunks)
  • internal/exercises/Catalog/Concepts/25_range_iterators.yaml (1 hunks)
  • internal/exercises/Catalog/Concepts/26_errors.yaml (1 hunks)
  • internal/exercises/Catalog/Concepts/27_custom_errors.yaml (1 hunks)
  • internal/exercises/Catalog/Concepts/28_defer.yaml (1 hunks)
  • internal/exercises/Catalog/Concepts/29_go_routines.yaml (1 hunks)
  • internal/exercises/Catalog/Concepts/30_channels.yaml (1 hunks)
  • internal/exercises/Catalog/Concepts/31_mutexes.yaml (1 hunks)
  • internal/exercises/Catalog/Concepts/32_sorting.yaml (1 hunks)
  • internal/exercises/Catalog/Concepts/33_string_formatting.yaml (1 hunks)
  • internal/exercises/Catalog/Concepts/34_channel_buffering.yaml (1 hunks)
  • internal/exercises/Catalog/Concepts/35_channel_sync.yaml (1 hunks)
  • internal/exercises/Catalog/Concepts/36_json.yaml (1 hunks)
  • internal/exercises/Catalog/Concepts/37_sorting_by_functions.yaml (1 hunks)
  • internal/exercises/Catalog/Concepts/37_xml.yaml (1 hunks)
  • internal/exercises/Catalog/Concepts/38_time_formatting.yaml (1 hunks)
  • internal/exercises/Catalog/Concepts/39_channel_directions.yaml (1 hunks)
  • internal/exercises/Catalog/Concepts/39_panic.yaml (1 hunks)
  • internal/exercises/Catalog/Concepts/40_channel_select.yaml (1 hunks)
  • internal/exercises/Catalog/Concepts/41_time_delay.yaml (1 hunks)
  • internal/exercises/Catalog/Concepts/42_wait_group.yaml (1 hunks)
  • internal/exercises/Catalog/Concepts/43_worker_pools.yaml (1 hunks)
  • internal/exercises/Catalog/Concepts/44_atomic_counters.yaml (1 hunks)
  • internal/exercises/Catalog/Concepts/45_range_over_channels.yaml (1 hunks)
  • internal/exercises/Catalog/Concepts/46_non_blocking_channel_operations.yaml (1 hunks)
  • internal/exercises/Catalog/Concepts/64_timers.yaml (1 hunks)
  • internal/exercises/Catalog/Concepts/68_rate_limiting.yaml (1 hunks)
  • internal/exercises/Catalog/Projects/01_text_analyzer.yaml (1 hunks)
  • internal/exercises/Catalog/Projects/02_shape_calculator.yaml (1 hunks)
  • internal/exercises/Catalog/Projects/03_task_scheduler.yaml (1 hunks)
  • internal/exercises/Catalog/Projects/04_http_server.yaml (1 hunks)
  • internal/exercises/Catalog/Projects/05_cli_todo_list.yaml (1 hunks)
  • internal/exercises/Catalog/Projects/06_simple_chat_app.yaml (1 hunks)
  • internal/exercises/Catalog/Projects/07_image_processing_utility.yaml (1 hunks)
  • internal/exercises/Catalog/Projects/08_basic_key_value_store.yaml (1 hunks)
  • internal/exercises/Catalog/Projects/09_epoch.yaml (1 hunks)
  • internal/exercises/catalog.yaml (0 hunks)
  • internal/exercises/exercises.go (9 hunks)
  • internal/exercises/exercises_test.go (1 hunks)
💤 Files with no reviewable changes (1)
  • internal/exercises/catalog.yaml
🧰 Additional context used
🧠 Learnings (4)
📚 Learning: 2025-10-02T12:51:43.433Z
Learnt from: kaushalyap
Repo: zhravan/golearn PR: 94
File: internal/exercises/templates/28_defer/utils.go:0-0
Timestamp: 2025-10-02T12:51:43.433Z
Learning: In the defer exercise template (internal/exercises/templates/28_defer/utils.go), the temporary file created by CreateFile() intentionally lacks cleanup (no Close() or Remove()) as part of the pedagogical design, expecting students to implement proper resource cleanup using defer.

Applied to files:

  • internal/exercises/Catalog/Concepts/39_panic.yaml
  • internal/exercises/Catalog/Concepts/41_time_delay.yaml
  • internal/exercises/Catalog/Concepts/28_defer.yaml
  • internal/exercises/exercises.go
📚 Learning: 2025-10-04T21:41:57.043Z
Learnt from: bdeb1337
Repo: zhravan/golearn PR: 132
File: internal/exercises/solutions/06_if_else/if_else.go:1-12
Timestamp: 2025-10-04T21:41:57.043Z
Learning: In the golearn repository, exercise solutions belong in `internal/exercises/solutions/$ex/` (reference implementations), while `internal/exercises/templates/$ex/` contains starter code with TODOs and test files for learners.

Applied to files:

  • internal/exercises/Catalog/Concepts/29_go_routines.yaml
  • internal/exercises/Catalog/Concepts/01_hello.yaml
📚 Learning: 2025-10-10T15:23:15.855Z
Learnt from: kaushalyap
Repo: zhravan/golearn PR: 156
File: internal/exercises/templates/40_channel_select/40_channel_select_test.go:10-24
Timestamp: 2025-10-10T15:23:15.855Z
Learning: In the golearn repository, timing-dependent tests in exercises (like the channel select exercise) don't need to meet production-grade reliability standards. Tests that may have some flakiness due to timing are acceptable if they effectively demonstrate the concept being taught.

Applied to files:

  • internal/exercises/Catalog/Concepts/41_time_delay.yaml
📚 Learning: 2025-10-02T06:30:47.152Z
Learnt from: kaushalyap
Repo: zhravan/golearn PR: 110
File: internal/exercises/templates/28_defer/defer.go:14-19
Timestamp: 2025-10-02T06:30:47.152Z
Learning: In the defer exercise template (internal/exercises/templates/28_defer/defer.go), the unused *os.File parameter in WriteToFile is intentional and part of the pedagogical design for teaching defer concepts.

Applied to files:

  • internal/exercises/Catalog/Concepts/28_defer.yaml
🧬 Code graph analysis (1)
internal/exercises/exercises_test.go (1)
internal/exercises/exercises.go (1)
  • Exercise (22-27)
🪛 GitHub Actions: CI
internal/exercises/exercises_test.go

[error] 1-1: Go formatting check failed: Code is not formatted. Run 'go fmt ./...' to fix.

🔇 Additional comments (79)
internal/exercises/Catalog/Concepts/06_if_else.yaml (1)

1-5: Structure and pattern look good.

Slug-filename alignment is correct, YAML is valid, and the hint is appropriate for the exercise.

internal/exercises/Catalog/Concepts/11_functions.yaml (1)

1-5: Consistent with established pattern.

Slug alignment and YAML structure are correct. Hint is clear and guides students appropriately.

internal/exercises/Catalog/Concepts/19_structs.yaml (1)

1-5: Pattern adherence is solid.

Slug-filename consistency maintained, YAML valid, hint is concrete and actionable.

internal/exercises/Catalog/Concepts/18_strings_runes.yaml (1)

1-5: Valid structure and consistent naming.

Slug aligns with filename; YAML is well-formed. Hint is detailed and covers both iteration and manipulation aspects.

internal/exercises/Catalog/Concepts/05_for.yaml (1)

1-5: Pattern is consistent.

Slug-filename correspondence correct, YAML valid, hint is focused on core for-loop use case (accumulation).

internal/exercises/Catalog/Projects/05_cli_todo_list.yaml (1)

1-5: Project definition follows pattern.

Slug-filename alignment correct, YAML valid. The "(Medium)" difficulty label in the title is appropriate for projects and differs from concept entries; appears to be an intentional convention.

internal/exercises/Catalog/Concepts/32_sorting.yaml (1)

1-5: Consistent with catalog pattern.

Slug-filename alignment correct, YAML valid, hint properly references Go API using code formatting.

internal/exercises/Catalog/Concepts/25_range_iterators.yaml (1)

1-5: LGTM — well-formed exercise descriptor with clear, actionable hint.

internal/exercises/Catalog/Concepts/27_custom_errors.yaml (1)

1-5: LGTM — well-formed descriptor with concrete guidance on custom error implementations.

internal/exercises/Catalog/Concepts/09_slices.yaml (1)

1-5: LGTM — well-structured descriptor with focused hint on slice operations.

internal/exercises/Catalog/Concepts/16_range_built_in.yaml (1)

1-5: LGTM — descriptor clearly targets iteration over collections using range.

internal/exercises/Catalog/Concepts/01_hello.yaml (1)

1-5: LGTM — appropriate introductory exercise with clear expected outcome.

internal/exercises/Catalog/Concepts/30_channels.yaml (1)

1-5: LGTM — descriptor provides concrete syntax guidance for Go channels.

internal/exercises/Catalog/Concepts/23_struct_embedding.yaml (1)

1-5: LGTM — descriptor covers embedding pattern with clear guidance on field access.

internal/exercises/Catalog/Concepts/12_multi_return.yaml (1)

1-5: LGTM — descriptor provides concrete use case with specific return requirements.

internal/exercises/Catalog/Concepts/17_pointers.yaml (1)

1-5: LGTM!

Valid YAML structure with clear exercise metadata. Slug naming and hint content are appropriate for the pointers concept.

internal/exercises/Catalog/Concepts/21_interfaces.yaml (1)

1-5: LGTM!

Valid YAML structure with clear exercise metadata for the interfaces concept. Slug and hint are appropriate.

internal/exercises/Catalog/Concepts/24_generics.yaml (1)

1-5: LGTM!

Valid YAML structure with clear exercise metadata. Slug and hint content are appropriate for generics concept.

internal/exercises/Catalog/Concepts/35_channel_sync.yaml (1)

1-5: LGTM!

Valid YAML structure with appropriate exercise metadata. Minor note: hint uses "go routine" instead of "goroutine" (Go convention), but this is a minor user-facing text issue.

internal/exercises/Catalog/Projects/09_epoch.yaml (1)

1-9: LGTM!

Valid project exercise definition with appropriate metadata. The expanded structure (difficulty, topics, multiple hints) is well-suited for project-level exercises. All hints are clear and provide actionable guidance.

internal/exercises/Catalog/Concepts/02_values.yaml (1)

1-5: LGTM!

Valid YAML structure with clear exercise metadata. Slug naming and hint are appropriate for the values concept.

internal/exercises/Catalog/Concepts/15_recursion.yaml (1)

1-5: LGTM!

Valid YAML structure with clear exercise metadata. Slug and hint content are appropriate for the recursion concept.

internal/exercises/Catalog/Concepts/46_non_blocking_channel_operations.yaml (1)

1-7: LGTM!

Valid YAML structure with clear exercise metadata. Multiple hints with inline code formatting (backticks) are well-presented and provide actionable guidance for the non-blocking channel operations concept.

internal/exercises/Catalog/Concepts/28_defer.yaml (1)

1-5: LGTM!

Minimal, well-formed YAML descriptor with consistent structure. Slug, title, test_regex, and hint align with the exercise.

internal/exercises/Catalog/Projects/01_text_analyzer.yaml (1)

1-5: LGTM!

Well-formed project exercise descriptor with consistent structure and clear, actionable hints.

internal/exercises/Catalog/Concepts/31_mutexes.yaml (1)

1-5: LGTM!

Concise, well-structured exercise descriptor. Consistent indentation and style with other minimal catalog entries.

internal/exercises/Catalog/Projects/06_simple_chat_app.yaml (1)

1-5: LGTM!

Clear project exercise descriptor with actionable hints. Consistent formatting throughout.

internal/exercises/Catalog/Projects/08_basic_key_value_store.yaml (1)

1-5: LGTM!

Well-defined project exercise with comprehensive hint on persistence patterns. Consistent structure and style.

internal/exercises/Catalog/Concepts/22_enums.yaml (1)

1-5: LGTM!

Minimal, well-formed descriptor with clear pedagogical intent. Consistent with other concept exercise entries.

internal/exercises/Catalog/Projects/02_shape_calculator.yaml (1)

1-5: Verify optional fields are intentionally omitted.

This exercise lacks difficulty and topics metadata fields that are present in other catalog entries (e.g., 41_time_delay.yaml, 42_wait_group.yaml). Confirm whether these fields should be standardized across all exercises or if they are truly optional. If optional, document this convention for future contributors.

internal/exercises/Catalog/Concepts/33_string_formatting.yaml (1)

1-5: Align with optional field conventions.

Like 02_shape_calculator.yaml, this entry omits difficulty and topics. Ensure the catalog loading mechanism and migration path are documented to clarify whether these are truly optional or should be standardized.

internal/exercises/Catalog/Concepts/44_atomic_counters.yaml (1)

1-7: Clarify optional field standardization.

This entry omits difficulty and topics fields present in other exercises (e.g., 68_rate_limiting.yaml). Before merging, confirm whether these fields should be added here or whether the inconsistency is acceptable by design.

internal/exercises/Catalog/Concepts/39_channel_directions.yaml (1)

1-9: Standardize optional field usage across exercises.

This exercise, like 33_string_formatting.yaml and others, lacks difficulty and topics, while 41_time_delay.yaml and 68_rate_limiting.yaml include them. Document the expected fields or apply them consistently across all catalog entries.

internal/exercises/Catalog/Concepts/41_time_delay.yaml (1)

1-10: Good: Enriched metadata improves exercise discovery.

This entry includes difficulty and topics metadata (lines 3-4), which enhances exercise categorization. Consider adopting this pattern for all catalog entries to enable better filtering and organization.

Verify whether the catalog loader supports and processes these optional fields, and whether all exercises should include them for consistency.

internal/exercises/Catalog/Concepts/03_variables.yaml (1)

1-5: Apply consistent metadata across catalog.

This minimal entry lacks difficulty and topics fields. Given that other exercises include these, establish a standard: either all exercises should include optional fields, or the loading mechanism should be documented to clarify which fields are required vs. optional.

internal/exercises/Catalog/Concepts/42_wait_group.yaml (1)

1-11: Good: Comprehensive metadata and well-structured hints.

This exercise properly includes difficulty, topics, and detailed hints covering key WaitGroup patterns (Add/Done semantics, channel closing, loop variable capture). This is a good model for other exercises. Consider standardizing this enriched format across all catalog entries.

internal/exercises/Catalog/Concepts/68_rate_limiting.yaml (1)

1-13: Excellent: Detailed exercise definition with clear implementation guidance.

This exercise exemplifies the enriched metadata pattern with difficulty: medium, topics, and seven well-structured hints covering struct design, concurrency, edge cases, testing concerns, and initialization. Use this as a model for standardizing other catalog entries.

internal/exercises/Catalog/Concepts/08_arrays.yaml (1)

1-5: YAML structure is valid and complete.

The exercise definition follows the established catalog format with slug, title, test_regex, and hints properly defined. The slug matches the filename convention.

internal/exercises/Catalog/Concepts/20_methods.yaml (1)

1-5: Valid exercise definition with consistent structure.

The file follows the catalog format correctly. The hint appropriately guides learners toward implementing methods on structs.

internal/exercises/Catalog/Concepts/14_closures.yaml (1)

1-5: Valid catalog entry with appropriate closure exercise guidance.

The structure and content conform to the established catalog format.

internal/exercises/Catalog/Concepts/26_errors.yaml (1)

1-5: Valid catalog entry for error handling concepts.

The file structure is correct and the hint provides clear guidance for the errors exercise.

internal/exercises/Catalog/Concepts/37_xml.yaml (1)

1-8: Well-structured multi-hint exercise definition.

The XML exercise appropriately includes multiple hints that progressively guide learners through encoding/decoding concepts, struct tags, and package usage.

internal/exercises/Catalog/Concepts/04_constants.yaml (1)

1-5: Valid constants exercise definition.

The file follows established format with an appropriate hint for the constants concept.

internal/exercises/Catalog/Concepts/10_maps.yaml (1)

1-5: Valid maps exercise definition.

The exercise provides practical guidance on using maps and string operations.

internal/exercises/Catalog/Concepts/40_channel_select.yaml (1)

1-6: Valid channel select exercise definition.

The hints appropriately guide learners through concurrent channel operations and timeout patterns using Go's select and time constructs.

internal/exercises/Catalog/Concepts/34_channel_buffering.yaml (1)

1-5: LGTM. YAML structure is valid and complete.

internal/exercises/Catalog/Projects/04_http_server.yaml (1)

1-5: LGTM. Structure and content are consistent.

internal/exercises/Catalog/Concepts/29_go_routines.yaml (1)

1-5: LGTM. YAML structure and content are consistent.

internal/exercises/Catalog/Projects/03_task_scheduler.yaml (1)

1-5: LGTM. Well-structured with comprehensive hint covering multiple concepts.

internal/exercises/Catalog/Concepts/36_json.yaml (1)

1-9: LGTM. Multi-hint structure well-suited for this complex concept.

internal/exercises/Catalog/Projects/07_image_processing_utility.yaml (1)

1-5: LGTM. Clear scope and difficulty level indicated.

internal/exercises/Catalog/Concepts/13_variadic.yaml (1)

1-5: LGTM. Structure and content are consistent across the catalog.

internal/exercises/Catalog/Concepts/07_switch.yaml (1)

1-5: LGTM! Well-formed exercise definition.

The YAML structure correctly defines the switch exercise with all required fields. The hint is clear and relevant.

internal/exercises/Catalog/Concepts/43_worker_pools.yaml (1)

1-10: LGTM! Comprehensive worker pools exercise.

The exercise definition is well-structured with detailed, pedagogically sound hints that cover all key aspects of the worker pool pattern in Go.

internal/exercises/Catalog/Concepts/64_timers.yaml (1)

6-15: LGTM! Comprehensive timer implementation hints.

The hints provide excellent guidance covering timer management, concurrency safety, and proper resource cleanup.

internal/exercises/exercises_test.go (9)

12-16: Helper function is acceptable for test code.

The y() helper ignores the error from yaml.Marshal, which is acceptable in test code since:

  • It's only used with valid structs in controlled test scenarios
  • Any malformed YAML will cause test failures downstream
  • This pattern simplifies test data setup

19-56: LGTM! Well-structured test for YAML loading.

The test correctly validates that loadExercisesDir loads multiple YAML files and extracts the slugs without enforcing ordering (which is handled by loadCatalogFromFS).


59-70: LGTM! Proper validation of required slug field.

The test correctly verifies that exercises without a slug are rejected with an appropriate error message.


72-85: LGTM! Proper handling of non-YAML files.

The test correctly verifies that non-YAML files in the catalog directory are ignored during loading.


88-117: LGTM! Comprehensive test of catalog loading and sorting.

The test correctly validates that loadCatalogFromFS loads both concepts and projects and sorts them deterministically by slug.


119-136: LGTM! Proper handling of missing directories.

The test correctly verifies that missing catalog directories (e.g., Projects) are gracefully handled without errors.


138-149: LGTM! Proper error handling for invalid YAML.

The test correctly verifies that malformed YAML files are rejected with an error.


152-160: LGTM! Validates fallback catalog behavior.

The test correctly verifies that the fallback catalog provides a minimal working exercise (01_hello) when loading fails.


186-191: LGTM! Proper handling of missing local directory.

The test correctly verifies that discoverLocal() doesn't error when the exercises directory is missing, which is expected behavior.

internal/exercises/exercises.go (15)

10-10: LGTM! Necessary import for deterministic sorting.

The sort import is correctly added to support sorting exercises by slug in loadCatalogFromFS.


19-19: LGTM! Embed directive updated for directory-based catalog.

The embed directive correctly points to the new Catalog/** directory structure, replacing the single catalog.yaml file.


29-32: LGTM! Catalog struct correctly updated for programmatic loading.

Removing the YAML tags is correct since the Catalog is now constructed programmatically in loadCatalogFromFS rather than unmarshaled directly from YAML.


39-52: LGTM! Robust catalog loading with fallback.

The singleton loader correctly falls back to a minimal catalog when loading fails or returns empty results, ensuring the system always has at least one exercise available.


54-75: LGTM! Well-implemented catalog loader with proper error handling.

The function correctly:

  • Loads from both Concepts and Projects directories
  • Tolerates missing directories (returns empty slices)
  • Sorts by slug for deterministic ordering
  • Only propagates real errors (not "not exist")

77-115: LGTM! Robust directory loader with proper validation.

The function correctly:

  • Reads YAML files from a directory
  • Skips non-YAML files and directories
  • Supports both .yaml and .yml extensions
  • Validates required fields (slug)
  • Provides contextual error messages

117-128: LGTM! Appropriate fallback catalog.

The fallback catalog provides a minimal working exercise (01_hello) to ensure the system remains functional even if catalog loading fails.


132-154: LGTM! Local discovery logic remains correct.

The function properly discovers local exercises and gracefully handles missing directories, consistent with the test coverage.


156-169: LGTM! ListAll maintains expected behavior.

The function correctly prioritizes local exercises over the catalog, preserving the existing public API behavior.


171-197: LGTM! Get function correctly searches all exercise sources.

The function properly searches the catalog (both Concepts and Projects) and local exercises, maintaining the existing public API contract.


199-206: LGTM! Reset validates template existence.

The function correctly checks that the template exists in the embedded FS before attempting to copy, providing clear error messages for unsupported exercises.


208-214: LGTM! Simple and correct template existence check.

The function properly verifies template existence in the embedded FS using fs.Stat.


232-270: LGTM! Robust template copying implementation.

The function correctly:

  • Creates a clean slate by removing existing directories
  • Walks the template directory structure
  • Preserves directory hierarchy
  • Copies files with appropriate permissions (0o755 for dirs, 0o644 for files)

39-270: Excellent refactoring that achieves PR objectives.

The migration from single-file catalog.yaml to directory-based catalog loading is well-executed:

  • Eliminates merge conflicts by splitting exercises into separate files
  • Maintains backward compatibility with existing public APIs
  • Includes robust fallback mechanisms
  • Ensures deterministic ordering

The implementation follows Go best practices with clean separation of concerns and proper error handling.


216-230: Verify that all catalog exercises have corresponding templates.

The function correctly initializes both Concepts and Projects. However, it will fail if any exercise in the catalog doesn't have a matching template in the embedded FS. Ensure all catalog YAML files have corresponding template directories.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (1)
internal/exercises/exercises_test.go (1)

158-177: Test no longer exercises catalog() and can drop catalogOnce reset

This test now calls loadCatalogFromFS directly, so:

  • The name TestCatalog_UsesDirectoryLoader is slightly misleading.
  • The catalogOnce = sync.Once{} assignment on Line 160 appears unused, since nothing in this test depends on catalog() or the global once.

I’d suggest either (a) renaming the test to reflect that it’s a direct loadCatalogFromFS check, or (b) refactoring production code so that catalog() can be exercised here via an injectable FS. In both cases, removing the catalogOnce reset will simplify the test and avoid unnecessary global mutation.

🧹 Nitpick comments (5)
internal/exercises/exercises_test.go (5)

57-68: Error assertion could be more robust than string matching

Using strings.Contains(err.Error(), "missing slug") works but couples the test to the exact wording. If you expose or already have a sentinel error for this case, consider asserting on that instead; otherwise this is acceptable.


70-83: Non-YAML filtering covered; optional stronger assertion

The test correctly verifies non-YAML files are ignored. You might also assert items[0].Slug == "01_x" to make it explicit that the YAML entry is the one returned, but the length check already does most of the work.


85-114: Good coverage of concepts/projects loading and sort order

This test nicely validates that both concepts and projects are loaded and that concepts are sorted by slug. If you ever suspect sort issues for projects too, adding a second project entry here would give symmetric coverage, but not strictly required.


135-146: Invalid YAML path exercised; could document error shape

Ensuring loadCatalogFromFS returns a non-nil error for malformed YAML is good. If you settle on a stable error prefix or wrapper for catalog load failures, you could assert on that substring to document the contract, but the current check is fine.


179-184: Test name suggests missing-dir case but setup doesn’t enforce it

The test only asserts that discoverLocal() does not return an error in the current working directory; it doesn’t guarantee that the exercises directory is actually missing. If you specifically want to lock in the “no dir” behavior, consider calling discoverLocal() from within t.TempDir() (or another known-empty location) so the scenario is explicit.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 73e9450 and 75e9f3f.

📒 Files selected for processing (1)
  • internal/exercises/exercises_test.go (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
internal/exercises/exercises_test.go (1)
internal/exercises/exercises.go (1)
  • Exercise (22-27)
🔇 Additional comments (4)
internal/exercises/exercises_test.go (4)

1-16: Top-level setup and helper read cleanly

Package, imports, and the small y helper keep the tests concise and focused; no changes needed here.


18-55: Happy-path directory load test is well-shaped

The MapFS setup plus slug presence checks exercise loadExercisesDir nicely without over-constraining ordering; this is a solid baseline test.


116-133: Missing catalog subdirectory behavior is clearly specified

This test makes it explicit that missing Projects does not error and yields zero projects while still loading concepts, which matches the intended tolerant behavior.


148-156: Fallback catalog behavior is well-pinned

Asserting that the fallback has exactly one concept with slug 01_hello nicely preserves the historical behavior when directory loading fails; no changes needed.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (3)
internal/exercises/exercises.go (3)

34-40: Catalog FS loader and fallback behavior look solid

The new defaultCatalogLoader + catalog() + loadCatalogFromFS pipeline cleanly handles missing Catalog/Concepts/Catalog/Projects dirs, enforces deterministic slug ordering, and falls back to fallbackCatalog() when loading fails or yields an empty catalog; this is a nice, robust upgrade over single-file YAML. One minor improvement to consider is surfacing/logging the underlying load error when you hit the fallback path so configuration issues don’t silently degrade to the hardcoded 01_hello catalog.

Also applies to: 76-86, 88-109, 151-162


164-188: Confirm intended precedence and shape for locals vs catalog in ListAll/Get

ListAll now fully prefers discovered local exercises (returned as Concepts only) whenever any local dirs exist, while Get prioritizes catalog concepts/projects and only falls back to locals if the slug isn’t in the catalog. That asymmetry seems deliberate (per PR description: keep catalog behavior for Get/Init while exposing locals via ListAll), but it does mean you lose the concepts/projects distinction for locals and you can’t override catalog metadata via a local dir with the same slug. Please confirm this matches the CLI expectations; if you ever want local overrides for metadata too, you could invert the Get search order or merge locals into the catalog slices.

Also applies to: 190-202, 205-230


232-239: Template reset/init behavior is consistent but stricter; verify missing-template handling

Guarding Reset with templateExists is a nice UX improvement for local-only or ad‑hoc exercises, and copyExerciseTemplate’s walk/relpath logic for copying embedded templates into exercises/<slug> looks correct. In contrast, InitAll assumes every catalog concept/project has an embedded template and will fail fast if any template directory is missing; if that’s intentional (to catch catalog/template drift early), great—otherwise you may want to skip missing templates or return a sentinel like ErrNoTemplates instead. Also, if ErrNoTemplates is no longer used anywhere, consider removing it to avoid dead code.

Also applies to: 241-245, 247-260, 262-299, 301-301

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 75e9f3f and 01a387d.

📒 Files selected for processing (2)
  • internal/exercises/exercises.go (11 hunks)
  • internal/exercises/exercises_test.go (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • internal/exercises/exercises_test.go
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-10-02T12:51:43.433Z
Learnt from: kaushalyap
Repo: zhravan/golearn PR: 94
File: internal/exercises/templates/28_defer/utils.go:0-0
Timestamp: 2025-10-02T12:51:43.433Z
Learning: In the defer exercise template (internal/exercises/templates/28_defer/utils.go), the temporary file created by CreateFile() intentionally lacks cleanup (no Close() or Remove()) as part of the pedagogical design, expecting students to implement proper resource cleanup using defer.

Applied to files:

  • internal/exercises/exercises.go
🪛 GitHub Actions: CI
internal/exercises/exercises.go

[error] 1-1: Code is not formatted. Run 'go fmt ./...' to fix.

🔇 Additional comments (2)
internal/exercises/exercises.go (2)

111-149: loadExercisesDir is defensive and well-scoped

Directory scanning, YAML filtering, and validation all look good: non-YAML entries are ignored, YAML errors are wrapped with the filename, and missing slug is treated as a hard error, which should make catalog issues easy to diagnose in tests. No changes needed here.


43-46: Tighten withTestCatalogLoader and address formatting issues

The override/reset logic for defaultCatalogLoader and catalogOnce should be consolidated into a single helper using defer for safer restoration and clearer intent. Additionally, document that tests using this helper must not run in parallel. If true concurrent safety is needed in the future, consider making catalog() also respect catalogMu.

Run go fmt ./internal/exercises to fix formatting issues reported by CI.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (5)
internal/exercises/exercises.go (5)

34-72: Strengthen withTestCatalogLoader around panics and potential races

The loader indirection and test hook are a nice pattern, but two robustness points are worth considering:

  • If fn() panics, the restoration block is never reached, so defaultCatalogLoader/catalogOnce/catalogData stay in their test state and may affect later tests.
  • catalog() reads defaultCatalogLoader/catalogData without catalogMu, so if withTestCatalogLoader is ever used from tests that run in parallel with other code calling catalog(), the writes in this helper can technically race with reads in catalog().

A minimal improvement that doesn’t change semantics would be to use defer for restoration and document that this helper must not be used from tests that call t.Parallel() or otherwise access catalog() concurrently with it:

 func withTestCatalogLoader(loader func() (Catalog, error), fn func()) {
-	catalogMu.Lock()
-
-	oldLoader := defaultCatalogLoader
-
-	// override loader + reset singleton
-	defaultCatalogLoader = loader
-	catalogOnce = sync.Once{}
-	catalogData = Catalog{}
-
-	catalogMu.Unlock()
-
-	fn()
-
-	// restore loader, and reset the once/data again
-	catalogMu.Lock()
-	defaultCatalogLoader = oldLoader
-	catalogOnce = sync.Once{}
-	catalogData = Catalog{}
-	catalogMu.Unlock()
+	catalogMu.Lock()
+	oldLoader := defaultCatalogLoader
+	defaultCatalogLoader = loader
+	catalogOnce = sync.Once{}
+	catalogData = Catalog{}
+	catalogMu.Unlock()
+
+	defer func() {
+		catalogMu.Lock()
+		defaultCatalogLoader = oldLoader
+		catalogOnce = sync.Once{}
+		catalogData = Catalog{}
+		catalogMu.Unlock()
+	}()
+
+	fn()
 }

If you expect heavy parallelism in tests, you may also want to guard reads in catalog() with the same mutex or switch to an atomic.Value-style pattern for the loader and data.

Also applies to: 77-87


168-192: Confirm ListAll semantics when any local exercises exist

ListAll now returns only local exercises (mapped as Concepts) when there is at least one directory under ./exercises, and completely hides embedded catalog concepts/projects in that case.

This means that after initializing a single exercise locally, ListAll will stop showing all the other embedded catalog entries. If the CLI is intended to act as a “show me everything I could work on” view, you might instead want to merge embedded and local or at least make this precedence explicit in docs/UX.

If “locals only” is the intended behavior (a “workspace view”), it would be good to codify that expectation in a comment or docstring.

Also applies to: 194-206


208-234: Get prefers embedded metadata over local; validate this precedence

Get searches embedded concepts, then embedded projects, then locals. So if a slug exists both in the catalog and as a local directory (which will be the common case after an init), you always return the embedded metadata, not a local override.

That’s consistent with keeping catalog as the source of truth, but it’s slightly asymmetric with ListAll, which prefers local when any exist. If you want local exercises to be able to override catalog metadata (e.g., experimenting with different titles/test regexes), you might want to flip the order and search locals first.

If catalog‑first is intentional, consider a short comment here to make that precedence explicit for future maintainers.


236-243: InitAll behavior on missing templates

Reset guards with templateExists, but InitAll calls copyExerciseTemplate for every catalog entry without that check. That’s fine if you guarantee that every catalog exercise has a corresponding directory under templates/, but it means:

  • InitAll will fail on the first exercise that lacks a template (due to WalkDir’s error), even if others are valid.
  • There’s no way to distinguish “some templates missing” from other IO errors.

If partial initialization is acceptable, you might consider skipping entries without templates (using templateExists) and possibly returning ErrNoTemplates only when nothing was initialized; otherwise, the current “fail fast on any missing template” behavior is clear but a bit strict.

Also applies to: 251-264


266-303: Template copying is correct; consider preserving original file modes

The copyExerciseTemplate logic (clean target dir, WalkDir over templates/<slug>, create dirs, then copy files) looks correct and will produce the expected tree in ./exercises/<slug>.

Two minor improvement ideas, depending on how templates evolve:

  • You currently write all files with 0o644. If any template files are intended to be executable (scripts, tools), you’ll lose the exec bit. You could instead read the original fs.FileInfo mode and propagate relevant bits.
  • You ignore errors from os.RemoveAll(targetDir). That’s usually fine because subsequent MkdirAll/writes will still fail if there’s a real problem, but if you ever need more precise diagnostics, capturing that error (and failing early) could make debugging easier.

Functionally this is fine as-is; these would just tighten the ergonomics.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 01a387d and 77d4b4d.

📒 Files selected for processing (2)
  • internal/exercises/exercises.go (10 hunks)
  • internal/exercises/exercises_test.go (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • internal/exercises/exercises_test.go
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-10-02T12:51:43.433Z
Learnt from: kaushalyap
Repo: zhravan/golearn PR: 94
File: internal/exercises/templates/28_defer/utils.go:0-0
Timestamp: 2025-10-02T12:51:43.433Z
Learning: In the defer exercise template (internal/exercises/templates/28_defer/utils.go), the temporary file created by CreateFile() intentionally lacks cleanup (no Close() or Remove()) as part of the pedagogical design, expecting students to implement proper resource cleanup using defer.

Applied to files:

  • internal/exercises/exercises.go
🔇 Additional comments (2)
internal/exercises/exercises.go (2)

19-32: Catalog struct + embedded FS + fallback behavior look consistent

Embedding Catalog/**, loading into the exported Catalog{Concepts, Projects} fields, and falling back to a minimal in‑code catalog (01_hello) when nothing is found or loading fails gives you a robust default and matches the PR objective of decoupling from a monolithic YAML file. I don’t see functional issues with this struct layout or the fallback behavior as written.

Also applies to: 154-166


10-10: FS-based catalog loader is correct and nicely constrained

loadCatalogFromFS + loadExercisesDir look solid:

  • Gracefully handle missing Catalog/Concepts or Catalog/Projects by treating fs.ErrNotExist as “no entries” instead of an error.
  • Filter to .yaml/.yml only and ignore other files.
  • Fail fast on unreadable files, YAML errors, or missing slug, with helpful error messages that point to the filename.
  • Sort concepts/projects by Slug using sort.Slice for deterministic ordering.

I don’t see correctness or performance issues here for the expected catalog sizes.

Also applies to: 89-152

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 77d4b4d and 49e0d9b.

📒 Files selected for processing (8)
  • internal/exercises/Catalog/Concepts/37_sorting_by_functions.yaml (1 hunks)
  • internal/exercises/Catalog/Concepts/38_time_formatting.yaml (1 hunks)
  • internal/exercises/Catalog/Concepts/39_panic.yaml (1 hunks)
  • internal/exercises/Catalog/Concepts/41_time_delay.yaml (1 hunks)
  • internal/exercises/Catalog/Concepts/42_wait_group.yaml (1 hunks)
  • internal/exercises/Catalog/Concepts/64_timers.yaml (1 hunks)
  • internal/exercises/Catalog/Concepts/68_rate_limiting.yaml (1 hunks)
  • internal/exercises/Catalog/Projects/09_epoch.yaml (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (6)
  • internal/exercises/Catalog/Concepts/68_rate_limiting.yaml
  • internal/exercises/Catalog/Concepts/37_sorting_by_functions.yaml
  • internal/exercises/Catalog/Concepts/64_timers.yaml
  • internal/exercises/Catalog/Concepts/38_time_formatting.yaml
  • internal/exercises/Catalog/Concepts/41_time_delay.yaml
  • internal/exercises/Catalog/Projects/09_epoch.yaml
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: bdeb1337
Repo: zhravan/golearn PR: 132
File: internal/exercises/solutions/06_if_else/if_else.go:1-12
Timestamp: 2025-10-04T21:41:57.043Z
Learning: In the golearn repository, exercise solutions belong in `internal/exercises/solutions/$ex/` (reference implementations), while `internal/exercises/templates/$ex/` contains starter code with TODOs and test files for learners.
📚 Learning: 2025-10-02T12:51:43.433Z
Learnt from: kaushalyap
Repo: zhravan/golearn PR: 94
File: internal/exercises/templates/28_defer/utils.go:0-0
Timestamp: 2025-10-02T12:51:43.433Z
Learning: In the defer exercise template (internal/exercises/templates/28_defer/utils.go), the temporary file created by CreateFile() intentionally lacks cleanup (no Close() or Remove()) as part of the pedagogical design, expecting students to implement proper resource cleanup using defer.

Applied to files:

  • internal/exercises/Catalog/Concepts/42_wait_group.yaml
🪛 YAMLlint (1.37.1)
internal/exercises/Catalog/Concepts/39_panic.yaml

[error] 8-8: syntax error: found character '`' that cannot start any token

(syntax)

🔇 Additional comments (1)
internal/exercises/Catalog/Concepts/42_wait_group.yaml (1)

1-9: YAML structure and pedagogical content are sound; verify supporting exercise files exist.

The YAML definition is valid with correct indentation, all required fields (slug, title, test_regex, hints), and well-targeted hints addressing WaitGroup fundamentals, cleanup patterns, loop variable capture, and ordering expectations. Slug naming and title are clear.

Verification needed:

  • Template file at internal/exercises/templates/42_wait_group/ with starter code (per learnings, templates contain TODOs for learners)
  • Solution file at internal/exercises/solutions/42_wait_group/ with reference implementation
  • Test coverage in exercises_test.go that includes this exercise in catalog loading tests
  • Slug sequence number consistency (verify 42 is correct in ordering)

These cannot be verified without repository access, but the YAML file itself requires no corrections.

@LeeFred3042U
Copy link
Contributor Author

@kaushalyap help

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b040d7b and 457181b.

📒 Files selected for processing (1)
  • .github/workflows/ci.yml (1 hunks)
🧰 Additional context used
🧠 Learnings (1)
📓 Common learnings
Learnt from: bdeb1337
Repo: zhravan/golearn PR: 132
File: internal/exercises/solutions/06_if_else/if_else.go:1-12
Timestamp: 2025-10-04T21:41:57.043Z
Learning: In the golearn repository, exercise solutions belong in `internal/exercises/solutions/$ex/` (reference implementations), while `internal/exercises/templates/$ex/` contains starter code with TODOs and test files for learners.
🪛 actionlint (1.7.9)
.github/workflows/ci.yml

26-26: the runner of "actions/cache@v3" action is too old to run on GitHub Actions. update the action's version to fix this issue

(action)

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
internal/exercises/solutions.go (2)

54-73: Address suppressed errors in file operations.

Multiple file operation errors are silently ignored:

  • Line 65: filepath.Rel error suppressed (acceptable with fallback on line 230)
  • Line 67: os.MkdirAll error suppressed
  • Line 68: fs.ReadFile error suppressed

If ReadFile fails, an empty or partial file will be written to disk without any indication of failure. Similarly, if MkdirAll fails, the subsequent WriteFile will fail anyway, but the error context will be lost.

Apply this diff to improve error handling:

-		rel, _ := filepath.Rel(templateRoot, path)
+		rel, err := filepath.Rel(templateRoot, path)
+		if err != nil {
+			return err
+		}
 		dest := filepath.Join(workDir, rel)
-		_ = os.MkdirAll(filepath.Dir(dest), 0o755)
-		data, _ := fs.ReadFile(templatesFS, path)
+		if err := os.MkdirAll(filepath.Dir(dest), 0o755); err != nil {
+			return err
+		}
+		data, err := fs.ReadFile(templatesFS, path)
+		if err != nil {
+			return err
+		}
 		return os.WriteFile(dest, data, 0o644)

77-96: Address suppressed errors in solution file operations.

Similar to the template copying above, file operation errors are silently suppressed on lines 88, 90, and 91. This can lead to corrupted sandbox state without clear error messages.

Apply this diff to propagate errors properly:

-		rel, _ := filepath.Rel(solutionRoot, path)
+		rel, err := filepath.Rel(solutionRoot, path)
+		if err != nil {
+			return err
+		}
 		dest := filepath.Join(workDir, rel)
-		_ = os.MkdirAll(filepath.Dir(dest), 0o755)
-		data, _ := fs.ReadFile(solutionsFS, path)
+		if err := os.MkdirAll(filepath.Dir(dest), 0o755); err != nil {
+			return err
+		}
+		data, err := fs.ReadFile(solutionsFS, path)
+		if err != nil {
+			return err
+		}
 		return os.WriteFile(dest, data, 0o644)
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 457181b and db028a0.

⛔ Files ignored due to path filters (1)
  • verification.log is excluded by !**/*.log
📒 Files selected for processing (64)
  • .github/workflows/ci.yml (3 hunks)
  • internal/exercises/Catalog/Concepts/01_hello.yaml (1 hunks)
  • internal/exercises/Catalog/Concepts/02_values.yaml (1 hunks)
  • internal/exercises/Catalog/Concepts/03_variables.yaml (1 hunks)
  • internal/exercises/Catalog/Concepts/04_constants.yaml (1 hunks)
  • internal/exercises/Catalog/Concepts/05_for.yaml (1 hunks)
  • internal/exercises/Catalog/Concepts/06_if_else.yaml (1 hunks)
  • internal/exercises/Catalog/Concepts/07_switch.yaml (1 hunks)
  • internal/exercises/Catalog/Concepts/08_arrays.yaml (1 hunks)
  • internal/exercises/Catalog/Concepts/09_slices.yaml (1 hunks)
  • internal/exercises/Catalog/Concepts/10_maps.yaml (1 hunks)
  • internal/exercises/Catalog/Concepts/11_functions.yaml (1 hunks)
  • internal/exercises/Catalog/Concepts/12_multi_return.yaml (1 hunks)
  • internal/exercises/Catalog/Concepts/13_variadic.yaml (1 hunks)
  • internal/exercises/Catalog/Concepts/14_closures.yaml (1 hunks)
  • internal/exercises/Catalog/Concepts/15_recursion.yaml (1 hunks)
  • internal/exercises/Catalog/Concepts/16_range_built_in.yaml (1 hunks)
  • internal/exercises/Catalog/Concepts/17_pointers.yaml (1 hunks)
  • internal/exercises/Catalog/Concepts/18_strings_runes.yaml (1 hunks)
  • internal/exercises/Catalog/Concepts/19_structs.yaml (1 hunks)
  • internal/exercises/Catalog/Concepts/20_methods.yaml (1 hunks)
  • internal/exercises/Catalog/Concepts/21_interfaces.yaml (1 hunks)
  • internal/exercises/Catalog/Concepts/22_enums.yaml (1 hunks)
  • internal/exercises/Catalog/Concepts/23_struct_embedding.yaml (1 hunks)
  • internal/exercises/Catalog/Concepts/24_generics.yaml (1 hunks)
  • internal/exercises/Catalog/Concepts/25_range_iterators.yaml (1 hunks)
  • internal/exercises/Catalog/Concepts/26_errors.yaml (1 hunks)
  • internal/exercises/Catalog/Concepts/27_custom_errors.yaml (1 hunks)
  • internal/exercises/Catalog/Concepts/28_defer.yaml (1 hunks)
  • internal/exercises/Catalog/Concepts/29_go_routines.yaml (1 hunks)
  • internal/exercises/Catalog/Concepts/30_channels.yaml (1 hunks)
  • internal/exercises/Catalog/Concepts/31_mutexes.yaml (1 hunks)
  • internal/exercises/Catalog/Concepts/32_sorting.yaml (1 hunks)
  • internal/exercises/Catalog/Concepts/33_string_formatting.yaml (1 hunks)
  • internal/exercises/Catalog/Concepts/34_channel_buffering.yaml (1 hunks)
  • internal/exercises/Catalog/Concepts/35_channel_sync.yaml (1 hunks)
  • internal/exercises/Catalog/Concepts/36_json.yaml (1 hunks)
  • internal/exercises/Catalog/Concepts/37_sorting_by_functions.yaml (1 hunks)
  • internal/exercises/Catalog/Concepts/37_xml.yaml (1 hunks)
  • internal/exercises/Catalog/Concepts/38_time_formatting.yaml (1 hunks)
  • internal/exercises/Catalog/Concepts/39_channel_directions.yaml (1 hunks)
  • internal/exercises/Catalog/Concepts/39_panic.yaml (1 hunks)
  • internal/exercises/Catalog/Concepts/40_channel_select.yaml (1 hunks)
  • internal/exercises/Catalog/Concepts/41_time_delay.yaml (1 hunks)
  • internal/exercises/Catalog/Concepts/42_wait_group.yaml (1 hunks)
  • internal/exercises/Catalog/Concepts/43_worker_pools.yaml (1 hunks)
  • internal/exercises/Catalog/Concepts/44_atomic_counters.yaml (1 hunks)
  • internal/exercises/Catalog/Concepts/45_range_over_channels.yaml (1 hunks)
  • internal/exercises/Catalog/Concepts/46_non_blocking_channel_operations.yaml (1 hunks)
  • internal/exercises/Catalog/Concepts/64_timers.yaml (1 hunks)
  • internal/exercises/Catalog/Concepts/68_rate_limiting.yaml (1 hunks)
  • internal/exercises/Catalog/Projects/01_text_analyzer.yaml (1 hunks)
  • internal/exercises/Catalog/Projects/02_shape_calculator.yaml (1 hunks)
  • internal/exercises/Catalog/Projects/03_task_scheduler.yaml (1 hunks)
  • internal/exercises/Catalog/Projects/04_http_server.yaml (1 hunks)
  • internal/exercises/Catalog/Projects/05_cli_todo_list.yaml (1 hunks)
  • internal/exercises/Catalog/Projects/06_simple_chat_app.yaml (1 hunks)
  • internal/exercises/Catalog/Projects/07_image_processing_utility.yaml (1 hunks)
  • internal/exercises/Catalog/Projects/08_basic_key_value_store.yaml (1 hunks)
  • internal/exercises/Catalog/Projects/09_epoch.yaml (1 hunks)
  • internal/exercises/exercises.go (5 hunks)
  • internal/exercises/exercises_test.go (1 hunks)
  • internal/exercises/export_test.go (1 hunks)
  • internal/exercises/solutions.go (5 hunks)
✅ Files skipped from review due to trivial changes (2)
  • internal/exercises/Catalog/Concepts/19_structs.yaml
  • internal/exercises/Catalog/Concepts/68_rate_limiting.yaml
🚧 Files skipped from review as they are similar to previous changes (52)
  • internal/exercises/Catalog/Concepts/06_if_else.yaml
  • internal/exercises/Catalog/Concepts/43_worker_pools.yaml
  • internal/exercises/Catalog/Concepts/28_defer.yaml
  • internal/exercises/Catalog/Concepts/05_for.yaml
  • internal/exercises/Catalog/Projects/07_image_processing_utility.yaml
  • internal/exercises/Catalog/Concepts/12_multi_return.yaml
  • internal/exercises/Catalog/Concepts/46_non_blocking_channel_operations.yaml
  • internal/exercises/Catalog/Concepts/04_constants.yaml
  • internal/exercises/Catalog/Concepts/31_mutexes.yaml
  • internal/exercises/Catalog/Concepts/18_strings_runes.yaml
  • internal/exercises/Catalog/Concepts/17_pointers.yaml
  • internal/exercises/Catalog/Concepts/21_interfaces.yaml
  • internal/exercises/Catalog/Concepts/14_closures.yaml
  • internal/exercises/Catalog/Concepts/11_functions.yaml
  • internal/exercises/Catalog/Concepts/13_variadic.yaml
  • internal/exercises/Catalog/Concepts/10_maps.yaml
  • internal/exercises/Catalog/Concepts/42_wait_group.yaml
  • internal/exercises/Catalog/Projects/05_cli_todo_list.yaml
  • internal/exercises/Catalog/Concepts/39_panic.yaml
  • internal/exercises/Catalog/Concepts/37_sorting_by_functions.yaml
  • internal/exercises/Catalog/Concepts/33_string_formatting.yaml
  • internal/exercises/Catalog/Concepts/09_slices.yaml
  • internal/exercises/Catalog/Concepts/27_custom_errors.yaml
  • internal/exercises/Catalog/Concepts/45_range_over_channels.yaml
  • internal/exercises/Catalog/Projects/02_shape_calculator.yaml
  • internal/exercises/Catalog/Concepts/07_switch.yaml
  • internal/exercises/exercises_test.go
  • internal/exercises/Catalog/Concepts/36_json.yaml
  • internal/exercises/Catalog/Concepts/15_recursion.yaml
  • .github/workflows/ci.yml
  • internal/exercises/Catalog/Concepts/26_errors.yaml
  • internal/exercises/Catalog/Concepts/44_atomic_counters.yaml
  • internal/exercises/Catalog/Concepts/20_methods.yaml
  • internal/exercises/Catalog/Projects/06_simple_chat_app.yaml
  • internal/exercises/Catalog/Projects/08_basic_key_value_store.yaml
  • internal/exercises/Catalog/Concepts/37_xml.yaml
  • internal/exercises/Catalog/Concepts/35_channel_sync.yaml
  • internal/exercises/Catalog/Concepts/30_channels.yaml
  • internal/exercises/Catalog/Concepts/64_timers.yaml
  • internal/exercises/Catalog/Concepts/38_time_formatting.yaml
  • internal/exercises/Catalog/Concepts/08_arrays.yaml
  • internal/exercises/Catalog/Concepts/34_channel_buffering.yaml
  • internal/exercises/Catalog/Concepts/01_hello.yaml
  • internal/exercises/Catalog/Projects/09_epoch.yaml
  • internal/exercises/Catalog/Concepts/40_channel_select.yaml
  • internal/exercises/Catalog/Concepts/16_range_built_in.yaml
  • internal/exercises/Catalog/Concepts/39_channel_directions.yaml
  • internal/exercises/Catalog/Concepts/22_enums.yaml
  • internal/exercises/Catalog/Concepts/29_go_routines.yaml
  • internal/exercises/Catalog/Concepts/02_values.yaml
  • internal/exercises/Catalog/Projects/04_http_server.yaml
  • internal/exercises/Catalog/Concepts/32_sorting.yaml
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: bdeb1337
Repo: zhravan/golearn PR: 132
File: internal/exercises/solutions/06_if_else/if_else.go:1-12
Timestamp: 2025-10-04T21:41:57.043Z
Learning: In the golearn repository, exercise solutions belong in `internal/exercises/solutions/$ex/` (reference implementations), while `internal/exercises/templates/$ex/` contains starter code with TODOs and test files for learners.
📚 Learning: 2025-10-04T21:41:57.043Z
Learnt from: bdeb1337
Repo: zhravan/golearn PR: 132
File: internal/exercises/solutions/06_if_else/if_else.go:1-12
Timestamp: 2025-10-04T21:41:57.043Z
Learning: In the golearn repository, exercise solutions belong in `internal/exercises/solutions/$ex/` (reference implementations), while `internal/exercises/templates/$ex/` contains starter code with TODOs and test files for learners.

Applied to files:

  • internal/exercises/solutions.go
  • internal/exercises/exercises.go
🧬 Code graph analysis (2)
internal/exercises/solutions.go (1)
internal/exercises/exercises.go (1)
  • Get (166-187)
internal/exercises/export_test.go (1)
internal/exercises/exercises.go (1)
  • Catalog (40-43)
🔇 Additional comments (11)
internal/exercises/Catalog/Concepts/25_range_iterators.yaml (1)

1-5: LGTM!

The exercise descriptor is well-formed with all required fields present.

internal/exercises/Catalog/Concepts/23_struct_embedding.yaml (1)

1-5: LGTM!

The exercise descriptor is correctly structured.

internal/exercises/Catalog/Concepts/03_variables.yaml (1)

1-5: LGTM!

The exercise definition is complete and follows the established pattern.

internal/exercises/Catalog/Concepts/41_time_delay.yaml (1)

1-8: LGTM!

The exercise descriptor is well-structured with helpful hints for learners.

internal/exercises/solutions.go (1)

21-35: LGTM! Exercise metadata retrieval added.

The function now correctly retrieves exercise metadata to determine the source directory, which properly supports exercises with custom directory names (via the Dir field).

internal/exercises/export_test.go (1)

1-27: LGTM! Well-designed test helper.

The helper correctly:

  • Protects catalog state with a mutex
  • Releases the lock before calling testFunc() (allowing tests to call catalog())
  • Restores original state via defer (even on panic)
  • Forces a reload by clearing catalogData

This enables safe, isolated testing of catalog loading behavior.

internal/exercises/exercises.go (4)

24-38: LGTM! Path abstraction properly supports custom directories.

The new Dir field and Path() method cleanly support exercises where the filesystem directory name differs from the slug (e.g., Projects with numeric prefixes).


45-76: LGTM! Testable catalog loading with fallback.

The refactored singleton pattern:

  • Uses sync.Mutex instead of sync.Once to enable test resets
  • Provides catalogLoader wrapper for dependency injection
  • Falls back to a minimal in-code catalog on load failures

This design properly balances production resilience with test flexibility.


78-124: LGTM! Robust directory-based catalog loading.

The implementation correctly:

  • Loads exercises from both Catalog/Concepts and Catalog/Projects
  • Handles missing directories gracefully (returns empty slice)
  • Validates that at least one directory has content
  • Sorts exercises by slug for deterministic ordering
  • Validates required fields (slug presence)
  • Provides clear error messages for parse failures

216-245: Note the filepath.Rel fallback on line 230.

The error handling for filepath.Rel on line 228 falls back to strings.TrimPrefix on line 230. While this handles most cases, be aware that if filepath.Rel fails for reasons other than incompatible path formats, the fallback might produce unexpected results.

The current approach is acceptable for the embedded FS use case where paths are predictable, but consider logging the error if you add observability later.

internal/exercises/Catalog/Concepts/24_generics.yaml (1)

1-5: No changes needed—test_regex: ".*" is the correct and standard pattern.

All exercises use this pattern consistently, and test execution is already scoped to the exercise directory via go test -run within that directory. The broad regex is safe and intentional.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
internal/exercises/solutions.go (1)

23-101: Don’t ignore copy errors; also avoid filepath.Join for embedded roots.

Right now _ = fs.ReadFile(...) can silently produce empty output, and filepath.Join("templates", srcDir) / filepath.Join("solutions", srcDir) can be wrong on Windows for WalkDir.

 func CreateSolutionSandbox(slug string) (string, func(), error) {
 	// Look up exercise to get the correct source Directory
 	ex, err := Get(slug)
 	if err != nil {
 		return "", func() {}, err
 	}

 	srcDir := ex.Path() // Uses Dir if set, else Slug

 	if !templateExists(srcDir) {
 		return "", func() {}, errors.New("no template found for exercise")
 	}
 	if !SolutionExists(srcDir) {
 		return "", func() {}, errors.New("no embedded solution available")
 	}
 ...
-	// Copy tests from templates (using srcDir 101)
-	templateRoot := filepath.Join("templates", srcDir)
-	if err := fs.WalkDir(templatesFS, templateRoot, func(path string, d fs.DirEntry, err error) error {
+	// Copy tests from templates (using srcDir 101)
+	templateRoot := pathpkg.Join("templates", srcDir)
+	if err := fs.WalkDir(templatesFS, templateRoot, func(fpath string, d fs.DirEntry, err error) error {
 		if err != nil {
 			return err
 		}
 		if d.IsDir() {
 			return nil
 		}
-		if !strings.HasSuffix(path, "_test.go") {
+		if !strings.HasSuffix(fpath, "_test.go") {
 			return nil
 		}
-
-		rel, _ := filepath.Rel(templateRoot, path)
+		rel, err := pathpkg.Rel(templateRoot, fpath)
+		if err != nil {
+			return err
+		}
 		dest := filepath.Join(workDir, rel)
-		_ = os.MkdirAll(filepath.Dir(dest), 0o755)
-		data, _ := fs.ReadFile(templatesFS, path)
+		if err := os.MkdirAll(filepath.Dir(dest), 0o755); err != nil {
+			return err
+		}
+		data, err := fs.ReadFile(templatesFS, fpath)
+		if err != nil {
+			return err
+		}
 		return os.WriteFile(dest, data, 0o644)
 	}); err != nil {
 		cleanup()
 		return "", func() {}, err
 	}

 	// Copy solution implementation from solutions (using srcDir 101)
-	solutionRoot := filepath.Join("solutions", srcDir)
-	if err := fs.WalkDir(solutionsFS, solutionRoot, func(path string, d fs.DirEntry, err error) error {
+	solutionRoot := pathpkg.Join("solutions", srcDir)
+	if err := fs.WalkDir(solutionsFS, solutionRoot, func(fpath string, d fs.DirEntry, err error) error {
 		if err != nil {
 			return err
 		}
 		if d.IsDir() {
 			return nil
 		}
-		if strings.HasSuffix(path, "_test.go") {
+		if strings.HasSuffix(fpath, "_test.go") {
 			return nil
 		}
-
-		rel, _ := filepath.Rel(solutionRoot, path)
+		rel, err := pathpkg.Rel(solutionRoot, fpath)
+		if err != nil {
+			return err
+		}
 		dest := filepath.Join(workDir, rel)
-		_ = os.MkdirAll(filepath.Dir(dest), 0o755)
-		data, _ := fs.ReadFile(solutionsFS, path)
+		if err := os.MkdirAll(filepath.Dir(dest), 0o755); err != nil {
+			return err
+		}
+		data, err := fs.ReadFile(solutionsFS, fpath)
+		if err != nil {
+			return err
+		}
 		return os.WriteFile(dest, data, 0o644)
 	}); err != nil {
 		cleanup()
 		return "", func() {}, err
 	}
🧹 Nitpick comments (3)
internal/exercises/exercises.go (3)

75-121: Consider rejecting duplicate slugs while loading the catalog.

Right now duplicates will both load and later lookups become ambiguous.

 func loadExercisesDir(fsys fs.FS, dir string) ([]Exercise, error) {
 	var exercises []Exercise
+	seen := map[string]struct{}{}
 	entries, err := fs.ReadDir(fsys, dir)
 ...
 		if ex.Slug == "" {
 			return nil, fmt.Errorf("exercise in %s missing slug", e.Name())
 		}
+		if _, ok := seen[ex.Slug]; ok {
+			return nil, fmt.Errorf("duplicate exercise slug %q in %s", ex.Slug, dir)
+		}
+		seen[ex.Slug] = struct{}{}
 		exercises = append(exercises, ex)
 	}

220-250: Use path.Rel for embed-FS paths (avoid mixed separator logic).

fpath from fs.WalkDir over embed.FS is slash-separated, so path.Rel(root, fpath) is the natural fit; then convert to OS paths when joining to targetDir.

 	root := path.Join("templates", ex.Path())
 	return fs.WalkDir(templatesFS, root, func(fpath string, d fs.DirEntry, err error) error {
 ...
-		rel, err := filepath.Rel(root, filepath.FromSlash(fpath))
+		rel, err := path.Rel(root, fpath)
 		if err != nil {
-			rel = strings.TrimPrefix(fpath, root+"/")
+			return err
 		}
 		if rel == "." {
 			return nil
 		}
-		dest := filepath.Join(targetDir, rel)
+		dest := filepath.Join(targetDir, filepath.FromSlash(rel))

54-73: Optional: add a breadcrumb when falling back to fallbackCatalog().

Today any embedded catalog parse/read error becomes indistinguishable from “no exercises shipped” because fallback is silent; even a debug log (if you have a logger) would make support easier.

Also applies to: 123-133

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between db028a0 and 5aa3aa6.

📒 Files selected for processing (12)
  • .github/workflows/ci.yml (3 hunks)
  • internal/exercises/Catalog/Projects/01_text_analyzer.yaml (1 hunks)
  • internal/exercises/Catalog/Projects/02_shape_calculator.yaml (1 hunks)
  • internal/exercises/Catalog/Projects/03_task_scheduler.yaml (1 hunks)
  • internal/exercises/Catalog/Projects/04_http_server.yaml (1 hunks)
  • internal/exercises/Catalog/Projects/05_cli_todo_list.yaml (1 hunks)
  • internal/exercises/Catalog/Projects/06_simple_chat_app.yaml (1 hunks)
  • internal/exercises/Catalog/Projects/07_image_processing_utility.yaml (1 hunks)
  • internal/exercises/Catalog/Projects/08_basic_key_value_store.yaml (1 hunks)
  • internal/exercises/Catalog/Projects/09_epoch.yaml (1 hunks)
  • internal/exercises/exercises.go (5 hunks)
  • internal/exercises/solutions.go (5 hunks)
🚧 Files skipped from review as they are similar to previous changes (7)
  • .github/workflows/ci.yml
  • internal/exercises/Catalog/Projects/07_image_processing_utility.yaml
  • internal/exercises/Catalog/Projects/09_epoch.yaml
  • internal/exercises/Catalog/Projects/01_text_analyzer.yaml
  • internal/exercises/Catalog/Projects/03_task_scheduler.yaml
  • internal/exercises/Catalog/Projects/02_shape_calculator.yaml
  • internal/exercises/Catalog/Projects/05_cli_todo_list.yaml
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: bdeb1337
Repo: zhravan/golearn PR: 132
File: internal/exercises/solutions/06_if_else/if_else.go:1-12
Timestamp: 2025-10-04T21:41:57.043Z
Learning: In the golearn repository, exercise solutions belong in `internal/exercises/solutions/$ex/` (reference implementations), while `internal/exercises/templates/$ex/` contains starter code with TODOs and test files for learners.
📚 Learning: 2025-10-04T21:41:57.043Z
Learnt from: bdeb1337
Repo: zhravan/golearn PR: 132
File: internal/exercises/solutions/06_if_else/if_else.go:1-12
Timestamp: 2025-10-04T21:41:57.043Z
Learning: In the golearn repository, exercise solutions belong in `internal/exercises/solutions/$ex/` (reference implementations), while `internal/exercises/templates/$ex/` contains starter code with TODOs and test files for learners.

Applied to files:

  • internal/exercises/exercises.go
  • internal/exercises/solutions.go
🧬 Code graph analysis (1)
internal/exercises/solutions.go (1)
internal/exercises/exercises.go (1)
  • Get (167-188)
🔇 Additional comments (4)
internal/exercises/Catalog/Projects/04_http_server.yaml (3)

4-4: Verify test_regex pattern is intentional.

The test pattern ".*" is very broad and matches everything. Confirm this is the intended behavior for test discovery in this exercise, or consider a more specific pattern if appropriate.


1-6: The exercise directory structure is correctly configured. Both internal/exercises/templates/104_http_server/ and internal/exercises/solutions/104_http_server/ exist with appropriate files (starter code template and test file, plus reference implementation).


1-2: No action needed. The slug and dir values follow an intentional design pattern: the slug (04_http_server) serves as the user-facing exercise identifier, while the dir (104_http_server) references the internal directory structure with a category prefix. Both the solutions/104_http_server and templates/104_http_server directories exist and are properly configured.

internal/exercises/Catalog/Projects/06_simple_chat_app.yaml (1)

1-6: Catalog entry looks consistent (slug vs dir separation is clear).

slug is the external identifier, while dir cleanly maps to the templates/solutions folder name.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
internal/exercises/exercises.go (1)

220-250: Fix relative-path derivation in copyExerciseTemplate (mixing path vs filepath is brittle).
templatesFS paths (fpath) are always slash-separated. Using filepath.Rel(root, filepath.FromSlash(fpath)) mixes OS separators with slash paths and can break on Windows / non-standard separators. Prefer path.Rel for embedded paths, then convert once for the destination.

 func copyExerciseTemplate(ex Exercise) error {
@@
 	root := path.Join("templates", ex.Path())
 	return fs.WalkDir(templatesFS, root, func(fpath string, d fs.DirEntry, err error) error {
@@
-		rel, err := filepath.Rel(root, filepath.FromSlash(fpath))
-		if err != nil {
-			rel = strings.TrimPrefix(fpath, root+"/")
-		}
+		rel, err := path.Rel(root, fpath)
+		if err != nil {
+			return err
+		}
 		if rel == "." {
 			return nil
 		}
-		dest := filepath.Join(targetDir, rel)
+		dest := filepath.Join(targetDir, filepath.FromSlash(rel))
 		if d.IsDir() {
 			return os.MkdirAll(dest, 0o755)
 		}
 		data, err := fs.ReadFile(templatesFS, fpath)
🧹 Nitpick comments (2)
internal/exercises/exercises.go (2)

24-38: Validate Exercise.Dir / Exercise.Slug to prevent .. / separators (future-hardening).
ex.Path() feeds into path.Join("templates", ex.Path()). Even if today the catalog is embedded (trusted), adding basic validation keeps invariants tight and avoids accidental escapes if loader inputs ever become user-controlled.

 func loadExercisesDir(fsys fs.FS, dir string) ([]Exercise, error) {
@@
 		var ex Exercise
 		if err := yaml.Unmarshal(data, &ex); err != nil {
 			return nil, fmt.Errorf("parse %s: %w", e.Name(), err)
 		}
 		if ex.Slug == "" {
 			return nil, fmt.Errorf("exercise in %s missing slug", e.Name())
 		}
+		// Hardening: keep identifiers as simple directory names.
+		for fieldName, v := range map[string]string{"slug": ex.Slug, "dir": ex.Dir} {
+			if v == "" {
+				continue
+			}
+			if strings.Contains(v, "/") || strings.Contains(v, `\`) || strings.Contains(v, "..") || path.Clean(v) != v {
+				return nil, fmt.Errorf("%s in %s is not a safe directory name: %q", fieldName, e.Name(), v)
+			}
+		}
 		exercises = append(exercises, ex)
 	}

75-121: Detect duplicate Slug/Dir collisions during load to avoid ambiguous Get().
Right now duplicates across Concepts/Projects (or Slug vs someone else’s Dir) will “work” but Get() will return the first match (Concepts wins), which is brittle.

 func loadCatalogFromFS(fsys fs.FS) (Catalog, error) {
@@
 	if len(concepts) == 0 && len(projects) == 0 {
 		return Catalog{}, errors.New("no exercises found in Catalog/")
 	}
+	seen := map[string]string{} // key -> origin
+	check := func(origin string, ex Exercise) error {
+		for _, k := range []string{ex.Slug, ex.Dir} {
+			if k == "" {
+				continue
+			}
+			if prev, ok := seen[k]; ok {
+				return fmt.Errorf("duplicate exercise identifier %q: %s vs %s", k, prev, origin)
+			}
+			seen[k] = origin
+		}
+		return nil
+	}
+	for _, ex := range concepts {
+		if err := check("Catalog/Concepts/"+ex.Slug, ex); err != nil { return Catalog{}, err }
+	}
+	for _, ex := range projects {
+		if err := check("Catalog/Projects/"+ex.Slug, ex); err != nil { return Catalog{}, err }
+	}
 	return Catalog{Concepts: concepts, Projects: projects}, nil
 }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5aa3aa6 and 3d4c893.

📒 Files selected for processing (1)
  • internal/exercises/exercises.go (5 hunks)
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: bdeb1337
Repo: zhravan/golearn PR: 132
File: internal/exercises/solutions/06_if_else/if_else.go:1-12
Timestamp: 2025-10-04T21:41:57.043Z
Learning: In the golearn repository, exercise solutions belong in `internal/exercises/solutions/$ex/` (reference implementations), while `internal/exercises/templates/$ex/` contains starter code with TODOs and test files for learners.
📚 Learning: 2025-10-04T21:41:57.043Z
Learnt from: bdeb1337
Repo: zhravan/golearn PR: 132
File: internal/exercises/solutions/06_if_else/if_else.go:1-12
Timestamp: 2025-10-04T21:41:57.043Z
Learning: In the golearn repository, exercise solutions belong in `internal/exercises/solutions/$ex/` (reference implementations), while `internal/exercises/templates/$ex/` contains starter code with TODOs and test files for learners.

Applied to files:

  • internal/exercises/exercises.go
🔇 Additional comments (3)
internal/exercises/exercises.go (3)

21-23: Embedding Catalog/** looks correct, but keep case/paths consistent across platforms.
Go embed paths are slash-based and case-sensitive at build time on many CI environments; ensure the on-disk directory is exactly internal/exercises/Catalog/... and not a casing variant.


166-188: Get() matching Slug or Dir is good; consider clarifying precedence in docs/tests.
If a user passes a value that matches both a Concept and Project identifier, Concepts will win due to search order—fine, but worth being explicit (or prevented via duplicate detection).


45-73: WithTestCatalogLoader already handles safe test isolation correctly.

The exported WithTestCatalogLoader helper in internal/exercises/export_test.go properly addresses the concern: it locks catalogMu, resets catalogData = nil, swaps catalogLoader, then restores both under the mutex in a defer block. Tests using this helper (e.g., TestCatalogOverride) are already safe from ordering-dependent failures.

- Fixed CI workflows: resolved cache permission errors and added robust solution verification checks.

- Added tests for the new catalog loader using in-memory filesystems.
@LeeFred3042U LeeFred3042U force-pushed the feature/catalog-directory-loader branch from 17ad3da to ee00e3e Compare December 12, 2025 18:30
@LeeFred3042U
Copy link
Contributor Author

@kaushalyap suggestion/? on what I have to do here

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.

[Feature] Split the catalog to multiple directories and files

1 participant