From 73e945009fd24d7b4c1ea89860aa102584e1b195 Mon Sep 17 00:00:00 2001 From: LeeFred3042U <109694901+LeeFred3042U@users.noreply.github.com> Date: Thu, 4 Dec 2025 13:39:36 +0530 Subject: [PATCH 1/7] feat(exercises): switch catalog.yaml to directory-based catalog loader --- .../exercises/Catalog/Concepts/01_hello.yaml | 5 + .../exercises/Catalog/Concepts/02_values.yaml | 5 + .../Catalog/Concepts/03_variables.yaml | 5 + .../Catalog/Concepts/04_constants.yaml | 5 + .../exercises/Catalog/Concepts/05_for.yaml | 5 + .../Catalog/Concepts/06_if_else.yaml | 5 + .../exercises/Catalog/Concepts/07_switch.yaml | 5 + .../exercises/Catalog/Concepts/08_arrays.yaml | 5 + .../exercises/Catalog/Concepts/09_slices.yaml | 5 + .../exercises/Catalog/Concepts/10_maps.yaml | 5 + .../Catalog/Concepts/11_functions.yaml | 5 + .../Catalog/Concepts/12_multi_return.yaml | 5 + .../Catalog/Concepts/13_variadic.yaml | 5 + .../Catalog/Concepts/14_closures.yaml | 5 + .../Catalog/Concepts/15_recursion.yaml | 5 + .../Catalog/Concepts/16_range_built_in.yaml | 5 + .../Catalog/Concepts/17_pointers.yaml | 5 + .../Catalog/Concepts/18_strings_runes.yaml | 5 + .../Catalog/Concepts/19_structs.yaml | 5 + .../Catalog/Concepts/20_methods.yaml | 5 + .../Catalog/Concepts/21_interfaces.yaml | 5 + .../exercises/Catalog/Concepts/22_enums.yaml | 5 + .../Catalog/Concepts/23_struct_embedding.yaml | 5 + .../Catalog/Concepts/24_generics.yaml | 5 + .../Catalog/Concepts/25_range_iterators.yaml | 5 + .../exercises/Catalog/Concepts/26_errors.yaml | 5 + .../Catalog/Concepts/27_custom_errors.yaml | 5 + .../exercises/Catalog/Concepts/28_defer.yaml | 5 + .../Catalog/Concepts/29_go_routines.yaml | 5 + .../Catalog/Concepts/30_channels.yaml | 5 + .../Catalog/Concepts/31_mutexes.yaml | 5 + .../Catalog/Concepts/32_sorting.yaml | 5 + .../Concepts/33_string_formatting.yaml | 5 + .../Concepts/34_channel_buffering.yaml | 5 + .../Catalog/Concepts/35_channel_sync.yaml | 5 + .../exercises/Catalog/Concepts/36_json.yaml | 9 + .../Concepts/37_sorting_by_functions.yaml | 10 + .../exercises/Catalog/Concepts/37_xml.yaml | 8 + .../Catalog/Concepts/38_time_formatting.yaml | 10 + .../Concepts/39_channel_directions.yaml | 9 + .../exercises/Catalog/Concepts/39_panic.yaml | 9 + .../Catalog/Concepts/40_channel_select.yaml | 6 + .../Catalog/Concepts/41_time_delay.yaml | 10 + .../Catalog/Concepts/42_wait_group.yaml | 12 + .../Catalog/Concepts/43_worker_pools.yaml | 10 + .../Catalog/Concepts/44_atomic_counters.yaml | 7 + .../Concepts/45_range_over_channels.yaml | 5 + .../46_non_blocking_channel_operations.yaml | 7 + .../exercises/Catalog/Concepts/64_timers.yaml | 15 + .../Catalog/Concepts/68_rate_limiting.yaml | 13 + .../Catalog/Projects/01_text_analyzer.yaml | 5 + .../Catalog/Projects/02_shape_calculator.yaml | 5 + .../Catalog/Projects/03_task_scheduler.yaml | 5 + .../Catalog/Projects/04_http_server.yaml | 5 + .../Catalog/Projects/05_cli_todo_list.yaml | 5 + .../Catalog/Projects/06_simple_chat_app.yaml | 5 + .../Projects/07_image_processing_utility.yaml | 5 + .../Projects/08_basic_key_value_store.yaml | 5 + .../exercises/Catalog/Projects/09_epoch.yaml | 9 + internal/exercises/catalog.yaml | 366 ------------------ internal/exercises/exercises.go | 142 +++++-- internal/exercises/exercises_test.go | 191 +++++++++ 62 files changed, 667 insertions(+), 396 deletions(-) create mode 100644 internal/exercises/Catalog/Concepts/01_hello.yaml create mode 100644 internal/exercises/Catalog/Concepts/02_values.yaml create mode 100644 internal/exercises/Catalog/Concepts/03_variables.yaml create mode 100644 internal/exercises/Catalog/Concepts/04_constants.yaml create mode 100644 internal/exercises/Catalog/Concepts/05_for.yaml create mode 100644 internal/exercises/Catalog/Concepts/06_if_else.yaml create mode 100644 internal/exercises/Catalog/Concepts/07_switch.yaml create mode 100644 internal/exercises/Catalog/Concepts/08_arrays.yaml create mode 100644 internal/exercises/Catalog/Concepts/09_slices.yaml create mode 100644 internal/exercises/Catalog/Concepts/10_maps.yaml create mode 100644 internal/exercises/Catalog/Concepts/11_functions.yaml create mode 100644 internal/exercises/Catalog/Concepts/12_multi_return.yaml create mode 100644 internal/exercises/Catalog/Concepts/13_variadic.yaml create mode 100644 internal/exercises/Catalog/Concepts/14_closures.yaml create mode 100644 internal/exercises/Catalog/Concepts/15_recursion.yaml create mode 100644 internal/exercises/Catalog/Concepts/16_range_built_in.yaml create mode 100644 internal/exercises/Catalog/Concepts/17_pointers.yaml create mode 100644 internal/exercises/Catalog/Concepts/18_strings_runes.yaml create mode 100644 internal/exercises/Catalog/Concepts/19_structs.yaml create mode 100644 internal/exercises/Catalog/Concepts/20_methods.yaml create mode 100644 internal/exercises/Catalog/Concepts/21_interfaces.yaml create mode 100644 internal/exercises/Catalog/Concepts/22_enums.yaml create mode 100644 internal/exercises/Catalog/Concepts/23_struct_embedding.yaml create mode 100644 internal/exercises/Catalog/Concepts/24_generics.yaml create mode 100644 internal/exercises/Catalog/Concepts/25_range_iterators.yaml create mode 100644 internal/exercises/Catalog/Concepts/26_errors.yaml create mode 100644 internal/exercises/Catalog/Concepts/27_custom_errors.yaml create mode 100644 internal/exercises/Catalog/Concepts/28_defer.yaml create mode 100644 internal/exercises/Catalog/Concepts/29_go_routines.yaml create mode 100644 internal/exercises/Catalog/Concepts/30_channels.yaml create mode 100644 internal/exercises/Catalog/Concepts/31_mutexes.yaml create mode 100644 internal/exercises/Catalog/Concepts/32_sorting.yaml create mode 100644 internal/exercises/Catalog/Concepts/33_string_formatting.yaml create mode 100644 internal/exercises/Catalog/Concepts/34_channel_buffering.yaml create mode 100644 internal/exercises/Catalog/Concepts/35_channel_sync.yaml create mode 100644 internal/exercises/Catalog/Concepts/36_json.yaml create mode 100644 internal/exercises/Catalog/Concepts/37_sorting_by_functions.yaml create mode 100644 internal/exercises/Catalog/Concepts/37_xml.yaml create mode 100644 internal/exercises/Catalog/Concepts/38_time_formatting.yaml create mode 100644 internal/exercises/Catalog/Concepts/39_channel_directions.yaml create mode 100644 internal/exercises/Catalog/Concepts/39_panic.yaml create mode 100644 internal/exercises/Catalog/Concepts/40_channel_select.yaml create mode 100644 internal/exercises/Catalog/Concepts/41_time_delay.yaml create mode 100644 internal/exercises/Catalog/Concepts/42_wait_group.yaml create mode 100644 internal/exercises/Catalog/Concepts/43_worker_pools.yaml create mode 100644 internal/exercises/Catalog/Concepts/44_atomic_counters.yaml create mode 100644 internal/exercises/Catalog/Concepts/45_range_over_channels.yaml create mode 100644 internal/exercises/Catalog/Concepts/46_non_blocking_channel_operations.yaml create mode 100644 internal/exercises/Catalog/Concepts/64_timers.yaml create mode 100644 internal/exercises/Catalog/Concepts/68_rate_limiting.yaml create mode 100644 internal/exercises/Catalog/Projects/01_text_analyzer.yaml create mode 100644 internal/exercises/Catalog/Projects/02_shape_calculator.yaml create mode 100644 internal/exercises/Catalog/Projects/03_task_scheduler.yaml create mode 100644 internal/exercises/Catalog/Projects/04_http_server.yaml create mode 100644 internal/exercises/Catalog/Projects/05_cli_todo_list.yaml create mode 100644 internal/exercises/Catalog/Projects/06_simple_chat_app.yaml create mode 100644 internal/exercises/Catalog/Projects/07_image_processing_utility.yaml create mode 100644 internal/exercises/Catalog/Projects/08_basic_key_value_store.yaml create mode 100644 internal/exercises/Catalog/Projects/09_epoch.yaml delete mode 100644 internal/exercises/catalog.yaml create mode 100644 internal/exercises/exercises_test.go diff --git a/internal/exercises/Catalog/Concepts/01_hello.yaml b/internal/exercises/Catalog/Concepts/01_hello.yaml new file mode 100644 index 0000000..68d8e5f --- /dev/null +++ b/internal/exercises/Catalog/Concepts/01_hello.yaml @@ -0,0 +1,5 @@ +- slug: 01_hello + title: Hello, Go! + test_regex: ".*" + hints: + - Implement Hello() to return 'Hello, Go!' diff --git a/internal/exercises/Catalog/Concepts/02_values.yaml b/internal/exercises/Catalog/Concepts/02_values.yaml new file mode 100644 index 0000000..5228dea --- /dev/null +++ b/internal/exercises/Catalog/Concepts/02_values.yaml @@ -0,0 +1,5 @@ +- slug: 02_values + title: Values + test_regex: ".*" + hints: + - Use fmt.Sprintf to format values. diff --git a/internal/exercises/Catalog/Concepts/03_variables.yaml b/internal/exercises/Catalog/Concepts/03_variables.yaml new file mode 100644 index 0000000..8c29eed --- /dev/null +++ b/internal/exercises/Catalog/Concepts/03_variables.yaml @@ -0,0 +1,5 @@ +- slug: 03_variables + title: Variables + test_regex: ".*" + hints: + - Use short declarations (:=) and return multiple values. diff --git a/internal/exercises/Catalog/Concepts/04_constants.yaml b/internal/exercises/Catalog/Concepts/04_constants.yaml new file mode 100644 index 0000000..1a20306 --- /dev/null +++ b/internal/exercises/Catalog/Concepts/04_constants.yaml @@ -0,0 +1,5 @@ +- slug: 04_constants + title: Constants + test_regex: ".*" + hints: + - Use math.Pi and constant expressions. diff --git a/internal/exercises/Catalog/Concepts/05_for.yaml b/internal/exercises/Catalog/Concepts/05_for.yaml new file mode 100644 index 0000000..2c17973 --- /dev/null +++ b/internal/exercises/Catalog/Concepts/05_for.yaml @@ -0,0 +1,5 @@ +- slug: 05_for + title: For + test_regex: ".*" + hints: + - Accumulate a sum with a for loop. diff --git a/internal/exercises/Catalog/Concepts/06_if_else.yaml b/internal/exercises/Catalog/Concepts/06_if_else.yaml new file mode 100644 index 0000000..4ae1305 --- /dev/null +++ b/internal/exercises/Catalog/Concepts/06_if_else.yaml @@ -0,0 +1,5 @@ +- slug: 06_if_else + title: If/Else + test_regex: ".*" + hints: + - Handle negative, zero, and positive cases. diff --git a/internal/exercises/Catalog/Concepts/07_switch.yaml b/internal/exercises/Catalog/Concepts/07_switch.yaml new file mode 100644 index 0000000..192da78 --- /dev/null +++ b/internal/exercises/Catalog/Concepts/07_switch.yaml @@ -0,0 +1,5 @@ +- slug: 07_switch + title: Switch + test_regex: ".*" + hints: + - Match multiple cases for weekend days. diff --git a/internal/exercises/Catalog/Concepts/08_arrays.yaml b/internal/exercises/Catalog/Concepts/08_arrays.yaml new file mode 100644 index 0000000..cc42efa --- /dev/null +++ b/internal/exercises/Catalog/Concepts/08_arrays.yaml @@ -0,0 +1,5 @@ +- slug: 08_arrays + title: Arrays + test_regex: ".*" + hints: + - Iterate with range over a fixed-size array. diff --git a/internal/exercises/Catalog/Concepts/09_slices.yaml b/internal/exercises/Catalog/Concepts/09_slices.yaml new file mode 100644 index 0000000..6df261d --- /dev/null +++ b/internal/exercises/Catalog/Concepts/09_slices.yaml @@ -0,0 +1,5 @@ +- slug: 09_slices + title: Slices + test_regex: ".*" + hints: + - Append values then compute a sum. diff --git a/internal/exercises/Catalog/Concepts/10_maps.yaml b/internal/exercises/Catalog/Concepts/10_maps.yaml new file mode 100644 index 0000000..a6e2e5f --- /dev/null +++ b/internal/exercises/Catalog/Concepts/10_maps.yaml @@ -0,0 +1,5 @@ +- slug: 10_maps + title: Maps + test_regex: ".*" + hints: + - Use strings.Fields and map[string]int for word counts. diff --git a/internal/exercises/Catalog/Concepts/11_functions.yaml b/internal/exercises/Catalog/Concepts/11_functions.yaml new file mode 100644 index 0000000..a5bef19 --- /dev/null +++ b/internal/exercises/Catalog/Concepts/11_functions.yaml @@ -0,0 +1,5 @@ +- slug: 11_functions + title: Functions + test_regex: ".*" + hints: + - Pass a function and call it. diff --git a/internal/exercises/Catalog/Concepts/12_multi_return.yaml b/internal/exercises/Catalog/Concepts/12_multi_return.yaml new file mode 100644 index 0000000..8f4b142 --- /dev/null +++ b/internal/exercises/Catalog/Concepts/12_multi_return.yaml @@ -0,0 +1,5 @@ +- slug: 12_multi_return + title: Multiple Return Values + test_regex: ".*" + hints: + - Return quotient, remainder, and an error for divide-by-zero. diff --git a/internal/exercises/Catalog/Concepts/13_variadic.yaml b/internal/exercises/Catalog/Concepts/13_variadic.yaml new file mode 100644 index 0000000..542e6f5 --- /dev/null +++ b/internal/exercises/Catalog/Concepts/13_variadic.yaml @@ -0,0 +1,5 @@ +- slug: 13_variadic + title: Variadic Functions + test_regex: ".*" + hints: + - Use '...' to accept any number of ints and sum them. diff --git a/internal/exercises/Catalog/Concepts/14_closures.yaml b/internal/exercises/Catalog/Concepts/14_closures.yaml new file mode 100644 index 0000000..cc92e60 --- /dev/null +++ b/internal/exercises/Catalog/Concepts/14_closures.yaml @@ -0,0 +1,5 @@ +- slug: 14_closures + title: Closures + test_regex: ".*" + hints: + - Implement a function that returns a closure, capturing an outer variable. diff --git a/internal/exercises/Catalog/Concepts/15_recursion.yaml b/internal/exercises/Catalog/Concepts/15_recursion.yaml new file mode 100644 index 0000000..5fbe301 --- /dev/null +++ b/internal/exercises/Catalog/Concepts/15_recursion.yaml @@ -0,0 +1,5 @@ +- slug: 15_recursion + title: Recursion + test_regex: ".*" + hints: + - Implement a recursive factorial function. diff --git a/internal/exercises/Catalog/Concepts/16_range_built_in.yaml b/internal/exercises/Catalog/Concepts/16_range_built_in.yaml new file mode 100644 index 0000000..dfd3e14 --- /dev/null +++ b/internal/exercises/Catalog/Concepts/16_range_built_in.yaml @@ -0,0 +1,5 @@ +- slug: 16_range_built_in + title: Range over Built-in Types + test_regex: ".*" + hints: + - Use range to iterate over a slice and a map. diff --git a/internal/exercises/Catalog/Concepts/17_pointers.yaml b/internal/exercises/Catalog/Concepts/17_pointers.yaml new file mode 100644 index 0000000..19061cb --- /dev/null +++ b/internal/exercises/Catalog/Concepts/17_pointers.yaml @@ -0,0 +1,5 @@ +- slug: 17_pointers + title: Pointers + test_regex: ".*" + hints: + - Write a function that takes a pointer, modifies the value, and returns the pointer. diff --git a/internal/exercises/Catalog/Concepts/18_strings_runes.yaml b/internal/exercises/Catalog/Concepts/18_strings_runes.yaml new file mode 100644 index 0000000..7868c11 --- /dev/null +++ b/internal/exercises/Catalog/Concepts/18_strings_runes.yaml @@ -0,0 +1,5 @@ +- slug: 18_strings_runes + title: Strings and Runes + test_regex: ".*" + hints: + - Iterate over a string with range to count runes, and demonstrate string manipulation. diff --git a/internal/exercises/Catalog/Concepts/19_structs.yaml b/internal/exercises/Catalog/Concepts/19_structs.yaml new file mode 100644 index 0000000..a818496 --- /dev/null +++ b/internal/exercises/Catalog/Concepts/19_structs.yaml @@ -0,0 +1,5 @@ +- slug: 19_structs + title: Structs + test_regex: ".*" + hints: + - Define a struct with fields for name and age, then create an instance. diff --git a/internal/exercises/Catalog/Concepts/20_methods.yaml b/internal/exercises/Catalog/Concepts/20_methods.yaml new file mode 100644 index 0000000..14f5cd8 --- /dev/null +++ b/internal/exercises/Catalog/Concepts/20_methods.yaml @@ -0,0 +1,5 @@ +- slug: 20_methods + title: Methods + test_regex: ".*" + hints: + - Add a method to a struct that calculates something based on its fields. diff --git a/internal/exercises/Catalog/Concepts/21_interfaces.yaml b/internal/exercises/Catalog/Concepts/21_interfaces.yaml new file mode 100644 index 0000000..cc3e393 --- /dev/null +++ b/internal/exercises/Catalog/Concepts/21_interfaces.yaml @@ -0,0 +1,5 @@ +- slug: 21_interfaces + title: Interfaces + test_regex: ".*" + hints: + - Define an interface and implement it for a struct. diff --git a/internal/exercises/Catalog/Concepts/22_enums.yaml b/internal/exercises/Catalog/Concepts/22_enums.yaml new file mode 100644 index 0000000..e9408e7 --- /dev/null +++ b/internal/exercises/Catalog/Concepts/22_enums.yaml @@ -0,0 +1,5 @@ +- slug: 22_enums + title: Enums + test_regex: ".*" + hints: + - Use iota to create a set of related constants as an enumeration. diff --git a/internal/exercises/Catalog/Concepts/23_struct_embedding.yaml b/internal/exercises/Catalog/Concepts/23_struct_embedding.yaml new file mode 100644 index 0000000..1f0898e --- /dev/null +++ b/internal/exercises/Catalog/Concepts/23_struct_embedding.yaml @@ -0,0 +1,5 @@ +- slug: 23_struct_embedding + title: Struct Embedding + test_regex: ".*" + hints: + - Embed one struct within another and access the inner struct's fields directly. diff --git a/internal/exercises/Catalog/Concepts/24_generics.yaml b/internal/exercises/Catalog/Concepts/24_generics.yaml new file mode 100644 index 0000000..81b0cd5 --- /dev/null +++ b/internal/exercises/Catalog/Concepts/24_generics.yaml @@ -0,0 +1,5 @@ +- slug: 24_generics + title: Generics + test_regex: ".*" + hints: + - Write a generic function that works with different types. diff --git a/internal/exercises/Catalog/Concepts/25_range_iterators.yaml b/internal/exercises/Catalog/Concepts/25_range_iterators.yaml new file mode 100644 index 0000000..a640fa3 --- /dev/null +++ b/internal/exercises/Catalog/Concepts/25_range_iterators.yaml @@ -0,0 +1,5 @@ +- slug: 25_range_iterators + title: Range over Iterators + test_regex: ".*" + hints: + - Implement a custom iterator and use range over it. diff --git a/internal/exercises/Catalog/Concepts/26_errors.yaml b/internal/exercises/Catalog/Concepts/26_errors.yaml new file mode 100644 index 0000000..9a70347 --- /dev/null +++ b/internal/exercises/Catalog/Concepts/26_errors.yaml @@ -0,0 +1,5 @@ +- slug: 26_errors + title: Errors + test_regex: ".*" + hints: + - Write a function that returns an error and handle it. diff --git a/internal/exercises/Catalog/Concepts/27_custom_errors.yaml b/internal/exercises/Catalog/Concepts/27_custom_errors.yaml new file mode 100644 index 0000000..2b8d08e --- /dev/null +++ b/internal/exercises/Catalog/Concepts/27_custom_errors.yaml @@ -0,0 +1,5 @@ +- slug: 27_custom_errors + title: Custom Errors + test_regex: ".*" + hints: + - Define a custom error type and return it from a function. diff --git a/internal/exercises/Catalog/Concepts/28_defer.yaml b/internal/exercises/Catalog/Concepts/28_defer.yaml new file mode 100644 index 0000000..cddaaa3 --- /dev/null +++ b/internal/exercises/Catalog/Concepts/28_defer.yaml @@ -0,0 +1,5 @@ +- slug: 28_defer + title: Defer + test_regex: ".*" + hints: + - Use `f.Close()` with `defer` keyword. diff --git a/internal/exercises/Catalog/Concepts/29_go_routines.yaml b/internal/exercises/Catalog/Concepts/29_go_routines.yaml new file mode 100644 index 0000000..5c0ca69 --- /dev/null +++ b/internal/exercises/Catalog/Concepts/29_go_routines.yaml @@ -0,0 +1,5 @@ +- slug: 29_go_routines + title: Go Routines + test_regex: ".*" + hints: + - Use `go` keyword to execute functions concurrently using go routines. diff --git a/internal/exercises/Catalog/Concepts/30_channels.yaml b/internal/exercises/Catalog/Concepts/30_channels.yaml new file mode 100644 index 0000000..a099925 --- /dev/null +++ b/internal/exercises/Catalog/Concepts/30_channels.yaml @@ -0,0 +1,5 @@ +- slug: 30_channels + title: Channels + test_regex: ".*" + hints: + - Use `make(chan T)` to create a channel and `<-` to send and receive on it. diff --git a/internal/exercises/Catalog/Concepts/31_mutexes.yaml b/internal/exercises/Catalog/Concepts/31_mutexes.yaml new file mode 100644 index 0000000..cec0466 --- /dev/null +++ b/internal/exercises/Catalog/Concepts/31_mutexes.yaml @@ -0,0 +1,5 @@ +- slug: 31_mutexes + title: Mutexes + test_regex: ".*" + hints: + - Use `sync.Mutex` to synchronize access to a shared resource. diff --git a/internal/exercises/Catalog/Concepts/32_sorting.yaml b/internal/exercises/Catalog/Concepts/32_sorting.yaml new file mode 100644 index 0000000..747b00f --- /dev/null +++ b/internal/exercises/Catalog/Concepts/32_sorting.yaml @@ -0,0 +1,5 @@ +- slug: 32_sorting + title: Sorting + test_regex: ".*" + hints: + - Use `slice.Sort` for sorting slices. diff --git a/internal/exercises/Catalog/Concepts/33_string_formatting.yaml b/internal/exercises/Catalog/Concepts/33_string_formatting.yaml new file mode 100644 index 0000000..4132064 --- /dev/null +++ b/internal/exercises/Catalog/Concepts/33_string_formatting.yaml @@ -0,0 +1,5 @@ +- slug: 33_string_formatting + title: String Formatting + test_regex: ".*" + hints: + - Use `fmt.Sprintf` and `%s`, `%d`, `%f` formatting directives to format strings, integers, and floats. diff --git a/internal/exercises/Catalog/Concepts/34_channel_buffering.yaml b/internal/exercises/Catalog/Concepts/34_channel_buffering.yaml new file mode 100644 index 0000000..ae6fd80 --- /dev/null +++ b/internal/exercises/Catalog/Concepts/34_channel_buffering.yaml @@ -0,0 +1,5 @@ +- slug: 34_channel_buffering + title: Channel Buffering + test_regex: ".*" + hints: + - Use `make(chan T, N)` to create a buffered channel and `<-` to send and receive on it. diff --git a/internal/exercises/Catalog/Concepts/35_channel_sync.yaml b/internal/exercises/Catalog/Concepts/35_channel_sync.yaml new file mode 100644 index 0000000..2ee4376 --- /dev/null +++ b/internal/exercises/Catalog/Concepts/35_channel_sync.yaml @@ -0,0 +1,5 @@ +- slug: 35_channel_sync + title: Channel Synchronization + test_regex: ".*" + hints: + - Use a buffered boolean channel and wait till go routine completes. diff --git a/internal/exercises/Catalog/Concepts/36_json.yaml b/internal/exercises/Catalog/Concepts/36_json.yaml new file mode 100644 index 0000000..99357c8 --- /dev/null +++ b/internal/exercises/Catalog/Concepts/36_json.yaml @@ -0,0 +1,9 @@ +- slug: 36_json + title: JSON + test_regex: ".*" + hints: + - Use the encoding/json package to work with JSON data. + - Implement MarshalPerson to convert a struct into JSON using json.Marshal. + - Implement UnmarshalPerson to convert a JSON string into a struct using json.Unmarshal. + - Handle and return errors properly in both functions. + diff --git a/internal/exercises/Catalog/Concepts/37_sorting_by_functions.yaml b/internal/exercises/Catalog/Concepts/37_sorting_by_functions.yaml new file mode 100644 index 0000000..b8fff45 --- /dev/null +++ b/internal/exercises/Catalog/Concepts/37_sorting_by_functions.yaml @@ -0,0 +1,10 @@ +- slug: 37_sorting_by_functions + title: "Sorting by Functions" + difficulty: beginner + topics: ["slices", "sorting", "functions"] + hints: + - "Implement sort.Interface with Len(), Less(), and Swap() methods" + - "Create custom slice types like ByName and ByAge (e.g., type ByName []Person)" + - "Use sort.Sort() to sort slices with your custom comparison logic" + - "Remember to make copies of slices to avoid modifying the original" + diff --git a/internal/exercises/Catalog/Concepts/37_xml.yaml b/internal/exercises/Catalog/Concepts/37_xml.yaml new file mode 100644 index 0000000..311e4af --- /dev/null +++ b/internal/exercises/Catalog/Concepts/37_xml.yaml @@ -0,0 +1,8 @@ +- slug: 37_xml + title: XML Encoding and Decoding + test_regex: ".*" + hints: + - Use encoding/xml package for marshaling and unmarshaling XML data. + - Add XML struct tags using `xml:"fieldname"` to map struct fields to XML elements. + - Use xml.Marshal to convert structs to XML bytes. + - Use xml.Unmarshal to parse XML bytes into structs. diff --git a/internal/exercises/Catalog/Concepts/38_time_formatting.yaml b/internal/exercises/Catalog/Concepts/38_time_formatting.yaml new file mode 100644 index 0000000..d5e56cc --- /dev/null +++ b/internal/exercises/Catalog/Concepts/38_time_formatting.yaml @@ -0,0 +1,10 @@ +- slug: 38_time_formatting + title: "Time Formatting" + difficulty: beginner + topics: ["time", "formatting", "parsing"] + hints: + - "Use time.Now().Format() to format current time with a specific layout" + - "Use time.Parse() to parse time strings with known layouts" + - "Use time.LoadLocation() to work with different timezones" + - "Extract time components using .Date() and .Clock() methods" + diff --git a/internal/exercises/Catalog/Concepts/39_channel_directions.yaml b/internal/exercises/Catalog/Concepts/39_channel_directions.yaml new file mode 100644 index 0000000..3b1c8f8 --- /dev/null +++ b/internal/exercises/Catalog/Concepts/39_channel_directions.yaml @@ -0,0 +1,9 @@ +- slug: 39_channel_directions + title: Channel Directions + test_regex: ".*" + hints: + - Create two channels, one for sending jobs and one for receiving results. + - Start 5 workers using go routines. + - Send 50 jobs to the worker pool. + - Receive results from the worker pool and store them in a `rs` slice. + - Enforce type safety by specifying the channel directions in worker function. diff --git a/internal/exercises/Catalog/Concepts/39_panic.yaml b/internal/exercises/Catalog/Concepts/39_panic.yaml new file mode 100644 index 0000000..d4afdf9 --- /dev/null +++ b/internal/exercises/Catalog/Concepts/39_panic.yaml @@ -0,0 +1,9 @@ +- slug: 39_panic + title: Panic and Recover + test_regex: ".*" + hints: + - "Use `panic()` to simulate runtime errors when appropriate." + - "Use `defer` with `recover()` to catch and handle panics." + - "Recovering from panics allows graceful handling of unexpected situations." + - "Remember: `recover()` only works inside a deferred function." + diff --git a/internal/exercises/Catalog/Concepts/40_channel_select.yaml b/internal/exercises/Catalog/Concepts/40_channel_select.yaml new file mode 100644 index 0000000..0c63e99 --- /dev/null +++ b/internal/exercises/Catalog/Concepts/40_channel_select.yaml @@ -0,0 +1,6 @@ +- slug: 40_channel_select + title: Channel Select + test_regex: ".*" + hints: + - Use `select` to handle multiple channel operations concurrently. + - Use `time.After` to simulate a 5 microsecond timeout. diff --git a/internal/exercises/Catalog/Concepts/41_time_delay.yaml b/internal/exercises/Catalog/Concepts/41_time_delay.yaml new file mode 100644 index 0000000..ccce01e --- /dev/null +++ b/internal/exercises/Catalog/Concepts/41_time_delay.yaml @@ -0,0 +1,10 @@ +- slug: 41_time_delay + title: "Delays and Timers in Go" + difficulty: beginner + topics: ["time", "sleep", "channels", "timer"] + test_regex: ".*" + hints: + - "Use time.Sleep() to pause execution for a duration." + - "Convert milliseconds to time.Duration using time.Millisecond." + - "Use a buffered channel to send a signal after waiting." + - "Use time.After or time.Sleep to trigger events after delays." diff --git a/internal/exercises/Catalog/Concepts/42_wait_group.yaml b/internal/exercises/Catalog/Concepts/42_wait_group.yaml new file mode 100644 index 0000000..aa4c45f --- /dev/null +++ b/internal/exercises/Catalog/Concepts/42_wait_group.yaml @@ -0,0 +1,12 @@ +- slug: 42_wait_group + title: WaitGroups + test_regex: ".*" + difficulty: beginner + topics: ["concurrency", "sync", "goroutines"] + hints: + - "Use sync.WaitGroup to wait for all launched goroutines." + - "Call wg.Add(1) before starting a goroutine and wg.Done() when it finishes." + - "Close result channels after wg.Wait() so collectors can range over them." + - "Capture loop variables correctly inside goroutines (e.g., `n := n`)." + - "Return results as a slice — order does not need to match input order" + diff --git a/internal/exercises/Catalog/Concepts/43_worker_pools.yaml b/internal/exercises/Catalog/Concepts/43_worker_pools.yaml new file mode 100644 index 0000000..ac6ae8e --- /dev/null +++ b/internal/exercises/Catalog/Concepts/43_worker_pools.yaml @@ -0,0 +1,10 @@ +- slug: 43_worker_pools + title: Worker Pools + test_regex: ".*" + hints: + - Create separate channels for jobs (send work) and results (receive processed data). + - Launch multiple worker goroutines that read from jobs channel and write to results channel. + - Use channel directions (<-chan for receive-only, chan<- for send-only) in worker function signature. + - Close the jobs channel after sending all work to signal workers to finish. + - Use strings.TrimSpace() to clean message strings by removing leading/trailing whitespace. + - Collect exactly len(logs) results to ensure all work is processed. diff --git a/internal/exercises/Catalog/Concepts/44_atomic_counters.yaml b/internal/exercises/Catalog/Concepts/44_atomic_counters.yaml new file mode 100644 index 0000000..d6d5772 --- /dev/null +++ b/internal/exercises/Catalog/Concepts/44_atomic_counters.yaml @@ -0,0 +1,7 @@ +- slug: 44_atomic_counters + title: Atomic Counters + test_regex: ".*" + hints: + - Use `sync/atomic` package to create and increment atomic counter. + - Launch 10,000 anonymous go routines that simultaneously increment the counter. + - Use WaitGroups to wait for all go routines to finish. diff --git a/internal/exercises/Catalog/Concepts/45_range_over_channels.yaml b/internal/exercises/Catalog/Concepts/45_range_over_channels.yaml new file mode 100644 index 0000000..3e85183 --- /dev/null +++ b/internal/exercises/Catalog/Concepts/45_range_over_channels.yaml @@ -0,0 +1,5 @@ +- slug: 45_range_over_channels + title: Range over channels + test_regex: ".*" + hints: + - Use `for i := range ch {...}` syntax to range over a channels. diff --git a/internal/exercises/Catalog/Concepts/46_non_blocking_channel_operations.yaml b/internal/exercises/Catalog/Concepts/46_non_blocking_channel_operations.yaml new file mode 100644 index 0000000..34932ae --- /dev/null +++ b/internal/exercises/Catalog/Concepts/46_non_blocking_channel_operations.yaml @@ -0,0 +1,7 @@ +- slug: 46_non_blocking_channel_operations + title: Non-blocking channel operations + test_regex: ".*" + hints: + - Use `default` case in select to handle non-blocking channel operations. + - Safely increment `dropped` using mutex `Lock()` and `Unlock()`. + diff --git a/internal/exercises/Catalog/Concepts/64_timers.yaml b/internal/exercises/Catalog/Concepts/64_timers.yaml new file mode 100644 index 0000000..552fdfe --- /dev/null +++ b/internal/exercises/Catalog/Concepts/64_timers.yaml @@ -0,0 +1,15 @@ +- slug: 64_timers + title: "Timers" + difficulty: medium + topics: ["time", "goroutines", "concurrency", "synchronization"] + test_regex: ".*" + hints: + - "Use a map[string]*time.Timer to track active timers by key." + - "Use a mutex to safely access the timers map concurrently." + - "Start a timer with time.AfterFunc(d, fn) to run a callback after duration d." + - "If Start is called again for an existing key, stop and replace the old timer." + - "Stop should remove the timer from the map and prevent the callback from firing." + - "Reset can call timer.Reset(d) to reschedule the same callback." + - "Remember to remove timers from the map after they fire to avoid memory leaks." + - "Write tests to verify Start, Stop, and Reset behavior, including concurrency scenarios." + diff --git a/internal/exercises/Catalog/Concepts/68_rate_limiting.yaml b/internal/exercises/Catalog/Concepts/68_rate_limiting.yaml new file mode 100644 index 0000000..deab3ad --- /dev/null +++ b/internal/exercises/Catalog/Concepts/68_rate_limiting.yaml @@ -0,0 +1,13 @@ +- slug: 68_rate_limiting + title: Rate Limiting + test_regex: ".*" + difficulty: medium + topics: ["time", "concurrency", "maps"] + hints: + - "Implement a RateLimiter struct that tracks request timestamps per key using a map[string][]time.Time." + - "Use a mutex to safely handle concurrent access to the map." + - "In the Allow method, remove timestamps older than the interval and check if the number of recent requests exceeds the limit." + - "Reset should clear the request history for a key; if the key does not exist, do nothing." + - "Consider edge cases: multiple keys, concurrent access, and requests exactly at the interval boundary." + - "In tests, use time.Sleep with a small buffer above the interval to avoid flakiness." + - "Initialize the timestamps map in NewRateLimiter to prevent nil pointer errors." diff --git a/internal/exercises/Catalog/Projects/01_text_analyzer.yaml b/internal/exercises/Catalog/Projects/01_text_analyzer.yaml new file mode 100644 index 0000000..5c8a4da --- /dev/null +++ b/internal/exercises/Catalog/Projects/01_text_analyzer.yaml @@ -0,0 +1,5 @@ +- slug: 01_text_analyzer + title: Text Analyzer (Easy) + test_regex: ".*" + hints: + - Implement functions to count characters, words, and unique words in a given text. diff --git a/internal/exercises/Catalog/Projects/02_shape_calculator.yaml b/internal/exercises/Catalog/Projects/02_shape_calculator.yaml new file mode 100644 index 0000000..fafb0b9 --- /dev/null +++ b/internal/exercises/Catalog/Projects/02_shape_calculator.yaml @@ -0,0 +1,5 @@ +- slug: 02_shape_calculator + title: Shape Area Calculator (Medium) + test_regex: ".*" + hints: + - Define structs for different shapes, implement methods to calculate their areas, and use an interface for common shape behavior. diff --git a/internal/exercises/Catalog/Projects/03_task_scheduler.yaml b/internal/exercises/Catalog/Projects/03_task_scheduler.yaml new file mode 100644 index 0000000..adc0802 --- /dev/null +++ b/internal/exercises/Catalog/Projects/03_task_scheduler.yaml @@ -0,0 +1,5 @@ +- slug: 03_task_scheduler + title: Task Scheduler (Hard) + test_regex: ".*" + hints: + - Create a task scheduler with features like adding/removing tasks, a custom iterator, closures for task execution, and custom error handling. diff --git a/internal/exercises/Catalog/Projects/04_http_server.yaml b/internal/exercises/Catalog/Projects/04_http_server.yaml new file mode 100644 index 0000000..cd5727a --- /dev/null +++ b/internal/exercises/Catalog/Projects/04_http_server.yaml @@ -0,0 +1,5 @@ +- slug: 04_http_server + title: HTTP Server (Easy) + test_regex: ".*" + hints: + - Implement a basic HTTP server that responds to GET requests. diff --git a/internal/exercises/Catalog/Projects/05_cli_todo_list.yaml b/internal/exercises/Catalog/Projects/05_cli_todo_list.yaml new file mode 100644 index 0000000..654b7bf --- /dev/null +++ b/internal/exercises/Catalog/Projects/05_cli_todo_list.yaml @@ -0,0 +1,5 @@ +- slug: 05_cli_todo_list + title: CLI Todo List (Medium) + test_regex: ".*" + hints: + - Build a command-line tool to manage a todo list, including adding, listing, and completing tasks. diff --git a/internal/exercises/Catalog/Projects/06_simple_chat_app.yaml b/internal/exercises/Catalog/Projects/06_simple_chat_app.yaml new file mode 100644 index 0000000..672a141 --- /dev/null +++ b/internal/exercises/Catalog/Projects/06_simple_chat_app.yaml @@ -0,0 +1,5 @@ +- slug: 06_simple_chat_app + title: Simple Chat Application (Medium) + test_regex: ".*" + hints: + - Create a basic client-server chat application. diff --git a/internal/exercises/Catalog/Projects/07_image_processing_utility.yaml b/internal/exercises/Catalog/Projects/07_image_processing_utility.yaml new file mode 100644 index 0000000..05e2fbd --- /dev/null +++ b/internal/exercises/Catalog/Projects/07_image_processing_utility.yaml @@ -0,0 +1,5 @@ +- slug: 07_image_processing_utility + title: Image Processing Utility (Hard) + test_regex: ".*" + hints: + - Develop a command-line utility for basic image transformations like resizing or converting to grayscale. diff --git a/internal/exercises/Catalog/Projects/08_basic_key_value_store.yaml b/internal/exercises/Catalog/Projects/08_basic_key_value_store.yaml new file mode 100644 index 0000000..84e672a --- /dev/null +++ b/internal/exercises/Catalog/Projects/08_basic_key_value_store.yaml @@ -0,0 +1,5 @@ +- slug: 08_basic_key_value_store + title: Basic Key-Value Store (Hard) + test_regex: ".*" + hints: + - Implement an in-memory key-value store with basic CRUD operations and optional persistence. diff --git a/internal/exercises/Catalog/Projects/09_epoch.yaml b/internal/exercises/Catalog/Projects/09_epoch.yaml new file mode 100644 index 0000000..8370a0c --- /dev/null +++ b/internal/exercises/Catalog/Projects/09_epoch.yaml @@ -0,0 +1,9 @@ +- slug: 09_epoch + title: "Epoch Conversion" + difficulty: beginner + topics: ["time", "epoch", "unix"] + hints: + - "Use Go's `time.Unix()` to convert an epoch to time." + - "Use `t.Unix()` to convert time back to epoch." + - "Remember Go’s `time.Parse` can help parse date strings." + diff --git a/internal/exercises/catalog.yaml b/internal/exercises/catalog.yaml deleted file mode 100644 index 37b779b..0000000 --- a/internal/exercises/catalog.yaml +++ /dev/null @@ -1,366 +0,0 @@ -concepts: -- slug: 01_hello - title: Hello, Go! - test_regex: ".*" - hints: - - Implement Hello() to return 'Hello, Go!' -- slug: 02_values - title: Values - test_regex: ".*" - hints: - - Use fmt.Sprintf to format values. -- slug: 03_variables - title: Variables - test_regex: ".*" - hints: - - Use short declarations (:=) and return multiple values. -- slug: 04_constants - title: Constants - test_regex: ".*" - hints: - - Use math.Pi and constant expressions. -- slug: 05_for - title: For - test_regex: ".*" - hints: - - Accumulate a sum with a for loop. -- slug: 06_if_else - title: If/Else - test_regex: ".*" - hints: - - Handle negative, zero, and positive cases. -- slug: 07_switch - title: Switch - test_regex: ".*" - hints: - - Match multiple cases for weekend days. -- slug: 08_arrays - title: Arrays - test_regex: ".*" - hints: - - Iterate with range over a fixed-size array. -- slug: 09_slices - title: Slices - test_regex: ".*" - hints: - - Append values then compute a sum. -- slug: 10_maps - title: Maps - test_regex: ".*" - hints: - - Use strings.Fields and map[string]int for word counts. -- slug: 11_functions - title: Functions - test_regex: ".*" - hints: - - Pass a function and call it. -- slug: 12_multi_return - title: Multiple Return Values - test_regex: ".*" - hints: - - Return quotient, remainder, and an error for divide-by-zero. -- slug: 13_variadic - title: Variadic Functions - test_regex: ".*" - hints: - - Use '...' to accept any number of ints and sum them. -- slug: 14_closures - title: Closures - test_regex: ".*" - hints: - - Implement a function that returns a closure, capturing an outer variable. -- slug: 15_recursion - title: Recursion - test_regex: ".*" - hints: - - Implement a recursive factorial function. -- slug: 16_range_built_in - title: Range over Built-in Types - test_regex: ".*" - hints: - - Use range to iterate over a slice and a map. -- slug: 17_pointers - title: Pointers - test_regex: ".*" - hints: - - Write a function that takes a pointer, modifies the value, and returns the pointer. -- slug: 18_strings_runes - title: Strings and Runes - test_regex: ".*" - hints: - - Iterate over a string with range to count runes, and demonstrate string manipulation. -- slug: 19_structs - title: Structs - test_regex: ".*" - hints: - - Define a struct with fields for name and age, then create an instance. -- slug: 20_methods - title: Methods - test_regex: ".*" - hints: - - Add a method to a struct that calculates something based on its fields. -- slug: 21_interfaces - title: Interfaces - test_regex: ".*" - hints: - - Define an interface and implement it for a struct. -- slug: 22_enums - title: Enums - test_regex: ".*" - hints: - - Use iota to create a set of related constants as an enumeration. -- slug: 23_struct_embedding - title: Struct Embedding - test_regex: ".*" - hints: - - Embed one struct within another and access the inner struct's fields directly. -- slug: 24_generics - title: Generics - test_regex: ".*" - hints: - - Write a generic function that works with different types. -- slug: 25_range_iterators - title: Range over Iterators - test_regex: ".*" - hints: - - Implement a custom iterator and use range over it. -- slug: 26_errors - title: Errors - test_regex: ".*" - hints: - - Write a function that returns an error and handle it. -- slug: 27_custom_errors - title: Custom Errors - test_regex: ".*" - hints: - - Define a custom error type and return it from a function. -- slug: 28_defer - title: Defer - test_regex: ".*" - hints: - - Use `f.Close()` with `defer` keyword. -- slug: 29_go_routines - title: Go Routines - test_regex: ".*" - hints: - - Use `go` keyword to execute functions concurrently using go routines. -- slug: 30_channels - title: Channels - test_regex: ".*" - hints: - - Use `make(chan T)` to create a channel and `<-` to send and receive on it. -- slug: 31_mutexes - title: Mutexes - test_regex: ".*" - hints: - - Use `sync.Mutex` to synchronize access to a shared resource. -- slug: 32_sorting - title: Sorting - test_regex: ".*" - hints: - - Use `slice.Sort` for sorting slices. -- slug: 33_string_formatting - title: String Formatting - test_regex: ".*" - hints: - - Use `fmt.Sprintf` and `%s`, `%d`, `%f` formatting directives to format strings, integers, and floats. -- slug: 34_channel_buffering - title: Channel Buffering - test_regex: ".*" - hints: - - Use `make(chan T, N)` to create a buffered channel and `<-` to send and receive on it. -- slug: 39_channel_directions - title: Channel Directions - test_regex: ".*" - hints: - - Create two channels, one for sending jobs and one for receiving results. - - Start 5 workers using go routines. - - Send 50 jobs to the worker pool. - - Receive results from the worker pool and store them in a `rs` slice. - - Enforce type safety by specifying the channel directions in worker function. -- slug: 35_channel_sync - title: Channel Synchronization - test_regex: ".*" - hints: - - Use a buffered boolean channel and wait till go routine completes. -- slug: 37_xml - title: XML Encoding and Decoding - test_regex: ".*" - hints: - - Use encoding/xml package for marshaling and unmarshaling XML data. - - Add XML struct tags using `xml:"fieldname"` to map struct fields to XML elements. - - Use xml.Marshal to convert structs to XML bytes. - - Use xml.Unmarshal to parse XML bytes into structs. -- slug: 40_channel_select - title: Channel Select - test_regex: ".*" - hints: - - Use `select` to handle multiple channel operations concurrently. - - Use `time.After` to simulate a 5 microsecond timeout. -- slug: 41_time_delay - title: "Delays and Timers in Go" - difficulty: beginner - topics: ["time", "sleep", "channels", "timer"] - test_regex: ".*" - hints: - - "Use time.Sleep() to pause execution for a duration." - - "Convert milliseconds to time.Duration using time.Millisecond." - - "Use a buffered channel to send a signal after waiting." - - "Use time.After or time.Sleep to trigger events after delays." -- slug: 42_wait_group - title: WaitGroups - test_regex: ".*" - difficulty: beginner - topics: ["concurrency", "sync", "goroutines"] - hints: - - "Use sync.WaitGroup to wait for all launched goroutines." - - "Call wg.Add(1) before starting a goroutine and wg.Done() when it finishes." - - "Close result channels after wg.Wait() so collectors can range over them." - - "Capture loop variables correctly inside goroutines (e.g., `n := n`)." - - "Return results as a slice — order does not need to match input order" - -- slug: 43_worker_pools - title: Worker Pools - test_regex: ".*" - hints: - - Create separate channels for jobs (send work) and results (receive processed data). - - Launch multiple worker goroutines that read from jobs channel and write to results channel. - - Use channel directions (<-chan for receive-only, chan<- for send-only) in worker function signature. - - Close the jobs channel after sending all work to signal workers to finish. - - Use strings.TrimSpace() to clean message strings by removing leading/trailing whitespace. - - Collect exactly len(logs) results to ensure all work is processed. -- slug: 44_atomic_counters - title: Atomic Counters - test_regex: ".*" - hints: - - Use `sync/atomic` package to create and increment atomic counter. - - Launch 10,000 anonymous go routines that simultaneously increment the counter. - - Use WaitGroups to wait for all go routines to finish. -- slug: 45_range_over_channels - title: Range over channels - test_regex: ".*" - hints: - - Use `for i := range ch {...}` syntax to range over a channels. -- slug: 46_non_blocking_channel_operations - title: Non-blocking channel operations - test_regex: ".*" - hints: - - Use `default` case in select to handle non-blocking channel operations. - - Safely increment `dropped` using mutex `Lock()` and `Unlock()`. - -projects: -- slug: 101_text_analyzer - title: Text Analyzer (Easy) - test_regex: ".*" - hints: - - Implement functions to count characters, words, and unique words in a given text. -- slug: 102_shape_calculator - title: Shape Area Calculator (Medium) - test_regex: ".*" - hints: - - Define structs for different shapes, implement methods to calculate their areas, and use an interface for common shape behavior. -- slug: 103_task_scheduler - title: Task Scheduler (Hard) - test_regex: ".*" - hints: - - Create a task scheduler with features like adding/removing tasks, a custom iterator, closures for task execution, and custom error handling. -- slug: 104_http_server - title: HTTP Server (Easy) - test_regex: ".*" - hints: - - Implement a basic HTTP server that responds to GET requests. -- slug: 105_cli_todo_list - title: CLI Todo List (Medium) - test_regex: ".*" - hints: - - Build a command-line tool to manage a todo list, including adding, listing, and completing tasks. -- slug: 106_simple_chat_app - title: Simple Chat Application (Medium) - test_regex: ".*" - hints: - - Create a basic client-server chat application. -- slug: 107_image_processing_utility - title: Image Processing Utility (Hard) - test_regex: ".*" - hints: - - Develop a command-line utility for basic image transformations like resizing or converting to grayscale. -- slug: 108_basic_key_value_store - title: Basic Key-Value Store (Hard) - test_regex: ".*" - hints: - - Implement an in-memory key-value store with basic CRUD operations and optional persistence. -- slug: 36_json - title: JSON - test_regex: ".*" - hints: - - Use the encoding/json package to work with JSON data. - - Implement MarshalPerson to convert a struct into JSON using json.Marshal. - - Implement UnmarshalPerson to convert a JSON string into a struct using json.Unmarshal. - - Handle and return errors properly in both functions. - -- slug: 109_epoch - title: "Epoch Conversion" - difficulty: beginner - topics: ["time", "epoch", "unix"] - hints: - - "Use Go's `time.Unix()` to convert an epoch to time." - - "Use `t.Unix()` to convert time back to epoch." - - "Remember Go’s `time.Parse` can help parse date strings." - -- slug: 37_sorting_by_functions - title: "Sorting by Functions" - difficulty: beginner - topics: ["slices", "sorting", "functions"] - hints: - - "Implement sort.Interface with Len(), Less(), and Swap() methods" - - "Create custom slice types like ByName and ByAge (e.g., type ByName []Person)" - - "Use sort.Sort() to sort slices with your custom comparison logic" - - "Remember to make copies of slices to avoid modifying the original" - -- slug: 38_time_formatting - title: "Time Formatting" - difficulty: beginner - topics: ["time", "formatting", "parsing"] - hints: - - "Use time.Now().Format() to format current time with a specific layout" - - "Use time.Parse() to parse time strings with known layouts" - - "Use time.LoadLocation() to work with different timezones" - - "Extract time components using .Date() and .Clock() methods" - -- slug: 39_panic - title: Panic and Recover - test_regex: ".*" - hints: - - "Use `panic()` to simulate runtime errors when appropriate." - - "Use `defer` with `recover()` to catch and handle panics." - - "Recovering from panics allows graceful handling of unexpected situations." - - "Remember: `recover()` only works inside a deferred function." - -- slug: 64_timers - title: "Timers" - difficulty: medium - topics: ["time", "goroutines", "concurrency", "synchronization"] - test_regex: ".*" - hints: - - "Use a map[string]*time.Timer to track active timers by key." - - "Use a mutex to safely access the timers map concurrently." - - "Start a timer with time.AfterFunc(d, fn) to run a callback after duration d." - - "If Start is called again for an existing key, stop and replace the old timer." - - "Stop should remove the timer from the map and prevent the callback from firing." - - "Reset can call timer.Reset(d) to reschedule the same callback." - - "Remember to remove timers from the map after they fire to avoid memory leaks." - - "Write tests to verify Start, Stop, and Reset behavior, including concurrency scenarios." - -- slug: 68_rate_limiting - title: Rate Limiting - test_regex: ".*" - difficulty: medium - topics: ["time", "concurrency", "maps"] - hints: - - "Implement a RateLimiter struct that tracks request timestamps per key using a map[string][]time.Time." - - "Use a mutex to safely handle concurrent access to the map." - - "In the Allow method, remove timestamps older than the interval and check if the number of recent requests exceeds the limit." - - "Reset should clear the request history for a key; if the key does not exist, do nothing." - - "Consider edge cases: multiple keys, concurrent access, and requests exactly at the interval boundary." - - "In tests, use time.Sleep with a small buffer above the interval to avoid flakiness." - - "Initialize the timestamps map in NewRateLimiter to prevent nil pointer errors." diff --git a/internal/exercises/exercises.go b/internal/exercises/exercises.go index 7f588d6..725b7e5 100644 --- a/internal/exercises/exercises.go +++ b/internal/exercises/exercises.go @@ -7,6 +7,7 @@ import ( "io/fs" "os" "path/filepath" + "sort" "sync" "gopkg.in/yaml.v3" @@ -15,7 +16,7 @@ import ( //go:embed templates/** var templatesFS embed.FS -//go:embed catalog.yaml +//go:embed Catalog/** var catalogFS embed.FS type Exercise struct { @@ -26,8 +27,8 @@ type Exercise struct { } type Catalog struct { - Concepts []Exercise `yaml:"concepts"` - Projects []Exercise `yaml:"projects"` + Concepts []Exercise + Projects []Exercise } var ( @@ -35,31 +36,14 @@ var ( catalogData Catalog ) +// Get the singleton catalog instance +// Loads from embedded FS on first call +// or falls back to default if loading fails. func catalog() Catalog { catalogOnce.Do(func() { - b, err := catalogFS.ReadFile("catalog.yaml") - if err != nil { - // Fallback minimal catalog - catalogData = Catalog{ - Concepts: []Exercise{{ - Slug: "01_hello", - Title: "Hello, Go!", - TestRegex: ".*", - Hints: []string{"Implement Hello() to return 'Hello, Go!'"}, - }}, - } - return - } - var cat Catalog - if err := yaml.Unmarshal(b, &cat); err != nil { - catalogData = Catalog{ - Concepts: []Exercise{{ - Slug: "01_hello", - Title: "Hello, Go!", - TestRegex: ".*", - Hints: []string{"Implement Hello() to return 'Hello, Go!'"}, - }}, - } + cat, err := loadCatalogFromFS(catalogFS) + if err != nil || (len(cat.Concepts) == 0 && len(cat.Projects) == 0) { + catalogData = fallbackCatalog() return } catalogData = cat @@ -67,6 +51,84 @@ func catalog() Catalog { return catalogData } +// Load catalog from a given FS +// Returns a Catalog struct. +func loadCatalogFromFS(fsys fs.FS) (Catalog, error) { + concepts, err := loadExercisesDir(fsys, "Catalog/Concepts") + if err != nil && !errors.Is(err, fs.ErrNotExist) { + return Catalog{}, err + } + + projects, err := loadExercisesDir(fsys, "Catalog/Projects") + if err != nil && !errors.Is(err, fs.ErrNotExist) { + return Catalog{}, err + } + + // Deterministic ordering + sort.Slice(concepts, func(i, j int) bool { return concepts[i].Slug < concepts[j].Slug }) + sort.Slice(projects, func(i, j int) bool { return projects[i].Slug < projects[j].Slug }) + + return Catalog{ + Concepts: concepts, + Projects: projects, + }, nil +} + +// Load all exercises from a given directory in the FS +// Returns a slice of Exercise structs. +func loadExercisesDir(fsys fs.FS, dir string) ([]Exercise, error) { + entries, err := fs.ReadDir(fsys, dir) + if err != nil { + return nil, err + } + + var out []Exercise + + for _, e := range entries { + if e.IsDir() { + continue + } + + name := e.Name() + if filepath.Ext(name) != ".yaml" && filepath.Ext(name) != ".yml" { + continue + } + + b, err := fs.ReadFile(fsys, filepath.Join(dir, name)) + if err != nil { + return nil, err + } + + var ex Exercise + if err := yaml.Unmarshal(b, &ex); err != nil { + return nil, fmt.Errorf("%s: %w", name, err) + } + + if ex.Slug == "" { + return nil, fmt.Errorf("%s: missing slug", name) + } + + out = append(out, ex) + } + + return out, nil +} + +// Fallback catalog in case of errors +// or no embedded exercises found. +func fallbackCatalog() Catalog { + return Catalog{ + Concepts: []Exercise{{ + Slug: "01_hello", + Title: "Hello, Go!", + TestRegex: ".*", + Hints: []string{"Implement Hello() to return 'Hello, Go!'"}, + }}, + } +} + +// Discover local exercises from "exercises" directory +// Returns a slice of Exercise structs. func discoverLocal() ([]Exercise, error) { var items []Exercise entries, err := os.ReadDir("exercises") @@ -76,6 +138,7 @@ func discoverLocal() ([]Exercise, error) { if err != nil { return nil, err } + for _, e := range entries { if e.IsDir() { slug := e.Name() @@ -90,6 +153,7 @@ func discoverLocal() ([]Exercise, error) { return items, nil } +// List all exercises from catalog or local exercises. func ListAll() (Catalog, error) { locals, err := discoverLocal() if err != nil { @@ -97,14 +161,16 @@ func ListAll() (Catalog, error) { } if len(locals) > 0 { - // For simplicity, if local exercises are present, we'll only return them for now. - // A more robust solution might merge local and catalog exercises. + // If local exercises exist, prefer them. return Catalog{Concepts: locals}, nil } + return catalog(), nil } +// Get exercise by slug from catalog or local exercises. func Get(slug string) (Exercise, error) { + // Search catalog for _, ex := range catalog().Concepts { if ex.Slug == slug { return ex, nil @@ -115,6 +181,8 @@ func Get(slug string) (Exercise, error) { return ex, nil } } + + // Search local locals, err := discoverLocal() if err != nil { return Exercise{}, err @@ -124,23 +192,29 @@ func Get(slug string) (Exercise, error) { return ex, nil } } + return Exercise{}, fmt.Errorf("exercise not found: %s", slug) } +// Reset exercise template for a given slug +// from embedded FS to local exercises dir. func Reset(ex Exercise) error { - // Only supported for built-in embedded templates if !templateExists(ex.Slug) { return fmt.Errorf("reset unsupported for non-embedded exercise '%s'", ex.Slug) } return copyExerciseTemplate(ex.Slug) } +// Check if template for given slug exists +// in embedded FS. func templateExists(slug string) bool { root := filepath.Join("templates", slug) _, err := fs.Stat(templatesFS, root) return err == nil } +// Initialize all exercises from embedded templates +// into local exercises directory. func InitAll() error { for _, ex := range catalog().Concepts { if err := copyExerciseTemplate(ex.Slug); err != nil { @@ -155,9 +229,12 @@ func InitAll() error { return nil } +// Copy exercise template from embedded FS to local exercises dir +// for a given slug. func copyExerciseTemplate(slug string) error { targetDir := filepath.Join("exercises", slug) - // Remove and recreate to ensure a clean state + + // Clean slate _ = os.RemoveAll(targetDir) if err := os.MkdirAll(targetDir, 0o755); err != nil { return err @@ -168,17 +245,22 @@ func copyExerciseTemplate(slug string) error { if err != nil { return err } + rel, err := filepath.Rel(root, path) if err != nil { return err } + if rel == "." { return nil } + dest := filepath.Join(targetDir, rel) + if d.IsDir() { return os.MkdirAll(dest, 0o755) } + data, err := fs.ReadFile(templatesFS, path) if err != nil { return err diff --git a/internal/exercises/exercises_test.go b/internal/exercises/exercises_test.go new file mode 100644 index 0000000..8469d69 --- /dev/null +++ b/internal/exercises/exercises_test.go @@ -0,0 +1,191 @@ +package exercises + +import ( + "sync" + "strings" + "testing" + "testing/fstest" + + "gopkg.in/yaml.v3" +) + +// Helper to build YAML quickly +func y(v any) []byte { + b, _ := yaml.Marshal(v) + return b +} + +// +func TestLoadExercisesDir_LoadsValidYAML(t *testing.T) { + fsys := fstest.MapFS{ + "Catalog/Concepts/a.yaml": &fstest.MapFile{ + Data: y(Exercise{ + Slug: "02_vars", + Title: "Variables", + TestRegex: ".*", + Hints: []string{"x := 1"}, + }), + }, + "Catalog/Concepts/b.yaml": &fstest.MapFile{ + Data: y(Exercise{ + Slug: "01_hello", + Title: "Hello", + TestRegex: ".*", + }), + }, + } + + items, err := loadExercisesDir(fsys, "Catalog/Concepts") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if len(items) != 2 { + t.Fatalf("expected 2 items, got %d", len(items)) + } + + // Unsorted read → sorted in loadCatalogFromFS, but loadExercisesDir just loads. + found := map[string]bool{ + items[0].Slug: true, + items[1].Slug: true, + } + + if !found["01_hello"] || !found["02_vars"] { + t.Fatalf("missing expected slugs: %v", found) + } +} + + +func TestLoadExercisesDir_RejectsEmptySlug(t *testing.T) { + fsys := fstest.MapFS{ + "Catalog/Concepts/bad.yaml": &fstest.MapFile{ + Data: []byte("title: NoSlug"), + }, + } + + _, err := loadExercisesDir(fsys, "Catalog/Concepts") + if err == nil || !strings.Contains(err.Error(), "missing slug") { + t.Fatalf("expected missing slug error, got: %v", err) + } +} + +func TestLoadExercisesDir_IgnoresNonYAML(t *testing.T) { + fsys := fstest.MapFS{ + "Catalog/Concepts/readme.txt": &fstest.MapFile{Data: []byte("ignore me")}, + "Catalog/Concepts/x.yaml": &fstest.MapFile{Data: y(Exercise{Slug: "01_x"})}, + } + + items, err := loadExercisesDir(fsys, "Catalog/Concepts") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if len(items) != 1 { + t.Fatalf("expected 1 YAML item, got %d", len(items)) + } +} + + +func TestLoadCatalogFromFS_LoadsConceptsAndProjects(t *testing.T) { + fsys := fstest.MapFS{ + "Catalog/Concepts/a.yaml": &fstest.MapFile{ + Data: y(Exercise{Slug: "02_b"}), + }, + "Catalog/Concepts/b.yaml": &fstest.MapFile{ + Data: y(Exercise{Slug: "01_a"}), + }, + "Catalog/Projects/c.yaml": &fstest.MapFile{ + Data: y(Exercise{Slug: "10_p"}), + }, + } + + cat, err := loadCatalogFromFS(fsys) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if len(cat.Concepts) != 2 { + t.Fatalf("expected 2 concepts, got %d", len(cat.Concepts)) + } + if len(cat.Projects) != 1 { + t.Fatalf("expected 1 project, got %d", len(cat.Projects)) + } + + // Check sorted order + if cat.Concepts[0].Slug != "01_a" || cat.Concepts[1].Slug != "02_b" { + t.Fatalf("concepts not sorted: %+v", cat.Concepts) + } +} + +func TestLoadCatalogFromFS_MissingDirsAreIgnored(t *testing.T) { + fsys := fstest.MapFS{ + "Catalog/Concepts/x.yaml": &fstest.MapFile{ + Data: y(Exercise{Slug: "01_a"}), + }, + } + + cat, err := loadCatalogFromFS(fsys) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if len(cat.Projects) != 0 { + t.Fatalf("expected zero projects") + } + if len(cat.Concepts) != 1 { + t.Fatalf("expected one concept") + } +} + +func TestLoadCatalogFromFS_InvalidYAML(t *testing.T) { + fsys := fstest.MapFS{ + "Catalog/Concepts/bad.yaml": &fstest.MapFile{ + Data: []byte("{invalid yaml} : ???"), + }, + } + + _, err := loadCatalogFromFS(fsys) + if err == nil { + t.Fatal("expected YAML error, got nil") + } +} + + +func TestFallbackCatalog(t *testing.T) { + f := fallbackCatalog() + if len(f.Concepts) != 1 { + t.Fatalf("fallback should have exactly 1 concept") + } + if f.Concepts[0].Slug != "01_hello" { + t.Fatalf("fallback wrong slug: %s", f.Concepts[0].Slug) + } +} + + +func TestCatalog_UsesDirectoryLoader(t *testing.T) { + // Reset catalogOnce + catalogOnce = sync.Once{} + + fsys := fstest.MapFS{ + "Catalog/Concepts/a.yaml": &fstest.MapFile{ + Data: y(Exercise{Slug: "01_test"}), + }, + } + + // Call loader directly + cat, err := loadCatalogFromFS(fsys) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if len(cat.Concepts) != 1 || cat.Concepts[0].Slug != "01_test" { + t.Fatalf("loadCatalogFromFS did not correctly read from FS") + } +} + + + +func TestDiscoverLocal_NoDir(t *testing.T) { + _, err := discoverLocal() + if err != nil { + t.Fatalf("should not error if exercises directory is missing") + } +} From 75e9f3f5a52b5e6965a358dc079927abc23b71e1 Mon Sep 17 00:00:00 2001 From: LeeFred3042U <109694901+LeeFred3042U@users.noreply.github.com> Date: Thu, 4 Dec 2025 14:09:59 +0530 Subject: [PATCH 2/7] gofmt internal/exercises/exercises_test.go --- internal/exercises/exercises_test.go | 43 ++++++++++++---------------- 1 file changed, 18 insertions(+), 25 deletions(-) diff --git a/internal/exercises/exercises_test.go b/internal/exercises/exercises_test.go index 8469d69..e7f98d5 100644 --- a/internal/exercises/exercises_test.go +++ b/internal/exercises/exercises_test.go @@ -1,8 +1,8 @@ package exercises import ( - "sync" "strings" + "sync" "testing" "testing/fstest" @@ -15,7 +15,6 @@ func y(v any) []byte { return b } -// func TestLoadExercisesDir_LoadsValidYAML(t *testing.T) { fsys := fstest.MapFS{ "Catalog/Concepts/a.yaml": &fstest.MapFile{ @@ -55,7 +54,6 @@ func TestLoadExercisesDir_LoadsValidYAML(t *testing.T) { } } - func TestLoadExercisesDir_RejectsEmptySlug(t *testing.T) { fsys := fstest.MapFS{ "Catalog/Concepts/bad.yaml": &fstest.MapFile{ @@ -84,7 +82,6 @@ func TestLoadExercisesDir_IgnoresNonYAML(t *testing.T) { } } - func TestLoadCatalogFromFS_LoadsConceptsAndProjects(t *testing.T) { fsys := fstest.MapFS{ "Catalog/Concepts/a.yaml": &fstest.MapFile{ @@ -148,7 +145,6 @@ func TestLoadCatalogFromFS_InvalidYAML(t *testing.T) { } } - func TestFallbackCatalog(t *testing.T) { f := fallbackCatalog() if len(f.Concepts) != 1 { @@ -159,29 +155,26 @@ func TestFallbackCatalog(t *testing.T) { } } - func TestCatalog_UsesDirectoryLoader(t *testing.T) { - // Reset catalogOnce - catalogOnce = sync.Once{} - - fsys := fstest.MapFS{ - "Catalog/Concepts/a.yaml": &fstest.MapFile{ - Data: y(Exercise{Slug: "01_test"}), - }, - } - - // Call loader directly - cat, err := loadCatalogFromFS(fsys) - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - - if len(cat.Concepts) != 1 || cat.Concepts[0].Slug != "01_test" { - t.Fatalf("loadCatalogFromFS did not correctly read from FS") - } -} + // Reset catalogOnce + catalogOnce = sync.Once{} + + fsys := fstest.MapFS{ + "Catalog/Concepts/a.yaml": &fstest.MapFile{ + Data: y(Exercise{Slug: "01_test"}), + }, + } + // Call loader directly + cat, err := loadCatalogFromFS(fsys) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if len(cat.Concepts) != 1 || cat.Concepts[0].Slug != "01_test" { + t.Fatalf("loadCatalogFromFS did not correctly read from FS") + } +} func TestDiscoverLocal_NoDir(t *testing.T) { _, err := discoverLocal() From 01a387d7fde3f6605a1880047a58d9cebd9b7f96 Mon Sep 17 00:00:00 2001 From: LeeFred3042U <109694901+LeeFred3042U@users.noreply.github.com> Date: Thu, 4 Dec 2025 14:19:52 +0530 Subject: [PATCH 3/7] test: add safe catalog override helper and remove sync.Once copying --- internal/exercises/exercises.go | 53 +++++++++++++++++++++------- internal/exercises/exercises_test.go | 13 +++++++ 2 files changed, 54 insertions(+), 12 deletions(-) diff --git a/internal/exercises/exercises.go b/internal/exercises/exercises.go index 725b7e5..be1c0ff 100644 --- a/internal/exercises/exercises.go +++ b/internal/exercises/exercises.go @@ -31,17 +31,51 @@ type Catalog struct { Projects []Exercise } +// --- Catalog Loader Infrastructure --- + +// defaultCatalogLoader loads the catalog from embedded FS. +// Tests override this to inject fake catalogs. +var defaultCatalogLoader = func() (Catalog, error) { + return loadCatalogFromFS(catalogFS) +} + var ( + catalogMu sync.Mutex catalogOnce sync.Once catalogData Catalog ) -// Get the singleton catalog instance -// Loads from embedded FS on first call +// withTestCatalogLoader temporarily overrides the catalog loader +// and resets internal singleton state for the duration of the test. +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() +} + + +// Get the singleton catalog instance. +// Loads from embedded FS on first call, // or falls back to default if loading fails. func catalog() Catalog { catalogOnce.Do(func() { - cat, err := loadCatalogFromFS(catalogFS) + cat, err := defaultCatalogLoader() if err != nil || (len(cat.Concepts) == 0 && len(cat.Projects) == 0) { catalogData = fallbackCatalog() return @@ -90,7 +124,7 @@ func loadExercisesDir(fsys fs.FS, dir string) ([]Exercise, error) { } name := e.Name() - if filepath.Ext(name) != ".yaml" && filepath.Ext(name) != ".yml" { + if ext := filepath.Ext(name); ext != ".yaml" && ext != ".yml" { continue } @@ -161,7 +195,6 @@ func ListAll() (Catalog, error) { } if len(locals) > 0 { - // If local exercises exist, prefer them. return Catalog{Concepts: locals}, nil } @@ -205,16 +238,13 @@ func Reset(ex Exercise) error { return copyExerciseTemplate(ex.Slug) } -// Check if template for given slug exists -// in embedded FS. func templateExists(slug string) bool { root := filepath.Join("templates", slug) _, err := fs.Stat(templatesFS, root) return err == nil } -// Initialize all exercises from embedded templates -// into local exercises directory. +// Initialize all exercises from embedded templates. func InitAll() error { for _, ex := range catalog().Concepts { if err := copyExerciseTemplate(ex.Slug); err != nil { @@ -229,8 +259,7 @@ func InitAll() error { return nil } -// Copy exercise template from embedded FS to local exercises dir -// for a given slug. +// Copy exercise template from embedded FS to local exercises dir. func copyExerciseTemplate(slug string) error { targetDir := filepath.Join("exercises", slug) @@ -269,4 +298,4 @@ func copyExerciseTemplate(slug string) error { }) } -var ErrNoTemplates = errors.New("no templates found") +var ErrNoTemplates = errors.New("no templates found") \ No newline at end of file diff --git a/internal/exercises/exercises_test.go b/internal/exercises/exercises_test.go index e7f98d5..bd144f1 100644 --- a/internal/exercises/exercises_test.go +++ b/internal/exercises/exercises_test.go @@ -182,3 +182,16 @@ func TestDiscoverLocal_NoDir(t *testing.T) { t.Fatalf("should not error if exercises directory is missing") } } + +func TestCatalogOverride(t *testing.T) { + withTestCatalogLoader(func() (Catalog, error) { + return Catalog{ + Concepts: []Exercise{{Slug: "01_mock"}}, + }, nil + }, func() { + c := catalog() + if len(c.Concepts) != 1 || c.Concepts[0].Slug != "01_mock" { + t.Fatalf("expected mock catalog") + } + }) +} From 77d4b4d39059a91398efcacfb28d41d967b0e301 Mon Sep 17 00:00:00 2001 From: LeeFred3042U <109694901+LeeFred3042U@users.noreply.github.com> Date: Thu, 4 Dec 2025 15:12:19 +0530 Subject: [PATCH 4/7] fix: run go fmt on internal/exercises/exercises.go and internal/exercises/exercises_test.go --- internal/exercises/exercises.go | 31 ++++++++++++++-------------- internal/exercises/exercises_test.go | 20 +++++++++--------- 2 files changed, 25 insertions(+), 26 deletions(-) diff --git a/internal/exercises/exercises.go b/internal/exercises/exercises.go index be1c0ff..54f8c5d 100644 --- a/internal/exercises/exercises.go +++ b/internal/exercises/exercises.go @@ -48,28 +48,27 @@ var ( // withTestCatalogLoader temporarily overrides the catalog loader // and resets internal singleton state for the duration of the test. func withTestCatalogLoader(loader func() (Catalog, error), fn func()) { - catalogMu.Lock() + catalogMu.Lock() - oldLoader := defaultCatalogLoader + oldLoader := defaultCatalogLoader - // override loader + reset singleton - defaultCatalogLoader = loader - catalogOnce = sync.Once{} - catalogData = Catalog{} + // override loader + reset singleton + defaultCatalogLoader = loader + catalogOnce = sync.Once{} + catalogData = Catalog{} - catalogMu.Unlock() + catalogMu.Unlock() - fn() + fn() - // restore loader, and reset the once/data again - catalogMu.Lock() - defaultCatalogLoader = oldLoader - catalogOnce = sync.Once{} - catalogData = Catalog{} - catalogMu.Unlock() + // restore loader, and reset the once/data again + catalogMu.Lock() + defaultCatalogLoader = oldLoader + catalogOnce = sync.Once{} + catalogData = Catalog{} + catalogMu.Unlock() } - // Get the singleton catalog instance. // Loads from embedded FS on first call, // or falls back to default if loading fails. @@ -298,4 +297,4 @@ func copyExerciseTemplate(slug string) error { }) } -var ErrNoTemplates = errors.New("no templates found") \ No newline at end of file +var ErrNoTemplates = errors.New("no templates found") diff --git a/internal/exercises/exercises_test.go b/internal/exercises/exercises_test.go index bd144f1..a3c4ac8 100644 --- a/internal/exercises/exercises_test.go +++ b/internal/exercises/exercises_test.go @@ -184,14 +184,14 @@ func TestDiscoverLocal_NoDir(t *testing.T) { } func TestCatalogOverride(t *testing.T) { - withTestCatalogLoader(func() (Catalog, error) { - return Catalog{ - Concepts: []Exercise{{Slug: "01_mock"}}, - }, nil - }, func() { - c := catalog() - if len(c.Concepts) != 1 || c.Concepts[0].Slug != "01_mock" { - t.Fatalf("expected mock catalog") - } - }) + withTestCatalogLoader(func() (Catalog, error) { + return Catalog{ + Concepts: []Exercise{{Slug: "01_mock"}}, + }, nil + }, func() { + c := catalog() + if len(c.Concepts) != 1 || c.Concepts[0].Slug != "01_mock" { + t.Fatalf("expected mock catalog") + } + }) } From 49e0d9b8127166afe4afa4b5746c2ad79952dcdc Mon Sep 17 00:00:00 2001 From: LeeFred3042U <109694901+LeeFred3042U@users.noreply.github.com> Date: Thu, 4 Dec 2025 15:54:07 +0530 Subject: [PATCH 5/7] fix: Inconsistent metadata fields --- .../Concepts/37_sorting_by_functions.yaml | 11 ++++------- .../Catalog/Concepts/38_time_formatting.yaml | 11 ++++------- .../exercises/Catalog/Concepts/39_panic.yaml | 9 ++++----- .../Catalog/Concepts/41_time_delay.yaml | 10 ++++------ .../Catalog/Concepts/42_wait_group.yaml | 13 +++++-------- .../exercises/Catalog/Concepts/64_timers.yaml | 18 ++++++++---------- .../Catalog/Concepts/68_rate_limiting.yaml | 16 +++++++--------- .../exercises/Catalog/Projects/09_epoch.yaml | 9 +++------ 8 files changed, 39 insertions(+), 58 deletions(-) diff --git a/internal/exercises/Catalog/Concepts/37_sorting_by_functions.yaml b/internal/exercises/Catalog/Concepts/37_sorting_by_functions.yaml index b8fff45..594545c 100644 --- a/internal/exercises/Catalog/Concepts/37_sorting_by_functions.yaml +++ b/internal/exercises/Catalog/Concepts/37_sorting_by_functions.yaml @@ -1,10 +1,7 @@ - slug: 37_sorting_by_functions title: "Sorting by Functions" - difficulty: beginner - topics: ["slices", "sorting", "functions"] hints: - - "Implement sort.Interface with Len(), Less(), and Swap() methods" - - "Create custom slice types like ByName and ByAge (e.g., type ByName []Person)" - - "Use sort.Sort() to sort slices with your custom comparison logic" - - "Remember to make copies of slices to avoid modifying the original" - + - Implement sort.Interface with Len(), Less(), and Swap() methods + - Create custom slice types like ByName and ByAge (e.g., type ByName []Person) + - Use sort.Sort() to sort slices with your custom comparison logic + - Remember to make copies of slices to avoid modifying the original \ No newline at end of file diff --git a/internal/exercises/Catalog/Concepts/38_time_formatting.yaml b/internal/exercises/Catalog/Concepts/38_time_formatting.yaml index d5e56cc..d69db97 100644 --- a/internal/exercises/Catalog/Concepts/38_time_formatting.yaml +++ b/internal/exercises/Catalog/Concepts/38_time_formatting.yaml @@ -1,10 +1,7 @@ - slug: 38_time_formatting title: "Time Formatting" - difficulty: beginner - topics: ["time", "formatting", "parsing"] hints: - - "Use time.Now().Format() to format current time with a specific layout" - - "Use time.Parse() to parse time strings with known layouts" - - "Use time.LoadLocation() to work with different timezones" - - "Extract time components using .Date() and .Clock() methods" - + - Use time.Now().Format() to format current time with a specific layout + - Use time.Parse() to parse time strings with known layouts + - Use time.LoadLocation() to work with different timezones + - Extract time components using .Date() and .Clock() methods \ No newline at end of file diff --git a/internal/exercises/Catalog/Concepts/39_panic.yaml b/internal/exercises/Catalog/Concepts/39_panic.yaml index d4afdf9..26e8932 100644 --- a/internal/exercises/Catalog/Concepts/39_panic.yaml +++ b/internal/exercises/Catalog/Concepts/39_panic.yaml @@ -2,8 +2,7 @@ title: Panic and Recover test_regex: ".*" hints: - - "Use `panic()` to simulate runtime errors when appropriate." - - "Use `defer` with `recover()` to catch and handle panics." - - "Recovering from panics allows graceful handling of unexpected situations." - - "Remember: `recover()` only works inside a deferred function." - + - Use `panic()` to simulate runtime errors when appropriate. + - Use `defer` with `recover()` to catch and handle panics. + - Recovering from panics allows graceful handling of unexpected situations. + - Remember: `recover()` only works inside a deferred function. diff --git a/internal/exercises/Catalog/Concepts/41_time_delay.yaml b/internal/exercises/Catalog/Concepts/41_time_delay.yaml index ccce01e..a1a870d 100644 --- a/internal/exercises/Catalog/Concepts/41_time_delay.yaml +++ b/internal/exercises/Catalog/Concepts/41_time_delay.yaml @@ -1,10 +1,8 @@ - slug: 41_time_delay title: "Delays and Timers in Go" - difficulty: beginner - topics: ["time", "sleep", "channels", "timer"] test_regex: ".*" hints: - - "Use time.Sleep() to pause execution for a duration." - - "Convert milliseconds to time.Duration using time.Millisecond." - - "Use a buffered channel to send a signal after waiting." - - "Use time.After or time.Sleep to trigger events after delays." + - Use time.Sleep() to pause execution for a duration. + - Convert milliseconds to time.Duration using time.Millisecond. + - Use a buffered channel to send a signal after waiting. + - Use time.After or time.Sleep to trigger events after delays. diff --git a/internal/exercises/Catalog/Concepts/42_wait_group.yaml b/internal/exercises/Catalog/Concepts/42_wait_group.yaml index aa4c45f..e166f4e 100644 --- a/internal/exercises/Catalog/Concepts/42_wait_group.yaml +++ b/internal/exercises/Catalog/Concepts/42_wait_group.yaml @@ -1,12 +1,9 @@ - slug: 42_wait_group title: WaitGroups test_regex: ".*" - difficulty: beginner - topics: ["concurrency", "sync", "goroutines"] hints: - - "Use sync.WaitGroup to wait for all launched goroutines." - - "Call wg.Add(1) before starting a goroutine and wg.Done() when it finishes." - - "Close result channels after wg.Wait() so collectors can range over them." - - "Capture loop variables correctly inside goroutines (e.g., `n := n`)." - - "Return results as a slice — order does not need to match input order" - + - Use sync.WaitGroup to wait for all launched goroutines. + - Call wg.Add(1) before starting a goroutine and wg.Done() when it finishes. + - Close result channels after wg.Wait() so collectors can range over them. + - Capture loop variables correctly inside goroutines (e.g., `n := n`). + - Return results as a slice — order does not need to match input order. \ No newline at end of file diff --git a/internal/exercises/Catalog/Concepts/64_timers.yaml b/internal/exercises/Catalog/Concepts/64_timers.yaml index 552fdfe..3c989e0 100644 --- a/internal/exercises/Catalog/Concepts/64_timers.yaml +++ b/internal/exercises/Catalog/Concepts/64_timers.yaml @@ -1,15 +1,13 @@ - slug: 64_timers title: "Timers" - difficulty: medium - topics: ["time", "goroutines", "concurrency", "synchronization"] test_regex: ".*" hints: - - "Use a map[string]*time.Timer to track active timers by key." - - "Use a mutex to safely access the timers map concurrently." - - "Start a timer with time.AfterFunc(d, fn) to run a callback after duration d." - - "If Start is called again for an existing key, stop and replace the old timer." - - "Stop should remove the timer from the map and prevent the callback from firing." - - "Reset can call timer.Reset(d) to reschedule the same callback." - - "Remember to remove timers from the map after they fire to avoid memory leaks." - - "Write tests to verify Start, Stop, and Reset behavior, including concurrency scenarios." + - Use a map[string]*time.Timer to track active timers by key. + - Use a mutex to safely access the timers map concurrently. + - Start a timer with time.AfterFunc(d, fn) to run a callback after duration d. + - If Start is called again for an existing key, stop and replace the old timer. + - Stop should remove the timer from the map and prevent the callback from firing. + - Reset can call timer.Reset(d) to reschedule the same callback. + - Remember to remove timers from the map after they fire to avoid memory leaks. + - Write tests to verify Start, Stop, and Reset behavior, including concurrency scenarios. diff --git a/internal/exercises/Catalog/Concepts/68_rate_limiting.yaml b/internal/exercises/Catalog/Concepts/68_rate_limiting.yaml index deab3ad..f21cc8c 100644 --- a/internal/exercises/Catalog/Concepts/68_rate_limiting.yaml +++ b/internal/exercises/Catalog/Concepts/68_rate_limiting.yaml @@ -1,13 +1,11 @@ - slug: 68_rate_limiting title: Rate Limiting test_regex: ".*" - difficulty: medium - topics: ["time", "concurrency", "maps"] hints: - - "Implement a RateLimiter struct that tracks request timestamps per key using a map[string][]time.Time." - - "Use a mutex to safely handle concurrent access to the map." - - "In the Allow method, remove timestamps older than the interval and check if the number of recent requests exceeds the limit." - - "Reset should clear the request history for a key; if the key does not exist, do nothing." - - "Consider edge cases: multiple keys, concurrent access, and requests exactly at the interval boundary." - - "In tests, use time.Sleep with a small buffer above the interval to avoid flakiness." - - "Initialize the timestamps map in NewRateLimiter to prevent nil pointer errors." + - Implement a RateLimiter struct that tracks request timestamps per key using a map[string][]time.Time. + - Use a mutex to safely handle concurrent access to the map. + - In the Allow method, remove timestamps older than the interval and check if the number of recent requests exceeds the limit. + - Reset should clear the request history for a key; if the key does not exist, do nothing. + - Consider edge cases: multiple keys, concurrent access, and requests exactly at the interval boundary. + - In tests, use time.Sleep with a small buffer above the interval to avoid flakiness. + - Initialize the timestamps map in NewRateLimiter to prevent nil pointer errors. diff --git a/internal/exercises/Catalog/Projects/09_epoch.yaml b/internal/exercises/Catalog/Projects/09_epoch.yaml index 8370a0c..c608b55 100644 --- a/internal/exercises/Catalog/Projects/09_epoch.yaml +++ b/internal/exercises/Catalog/Projects/09_epoch.yaml @@ -1,9 +1,6 @@ - slug: 09_epoch title: "Epoch Conversion" - difficulty: beginner - topics: ["time", "epoch", "unix"] hints: - - "Use Go's `time.Unix()` to convert an epoch to time." - - "Use `t.Unix()` to convert time back to epoch." - - "Remember Go’s `time.Parse` can help parse date strings." - + - Use Go's `time.Unix()` to convert an epoch to time. + - Use `t.Unix()` to convert time back to epoch. + - Remember Go’s `time.Parse` can help parse date strings. From a7539fa165ac938e84a44fb1aa162c304df03c1c Mon Sep 17 00:00:00 2001 From: LeeFred3042U <109694901+LeeFred3042U@users.noreply.github.com> Date: Thu, 4 Dec 2025 16:14:35 +0530 Subject: [PATCH 6/7] fix: YAML syntax error in hint string. --- internal/exercises/Catalog/Concepts/39_panic.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/exercises/Catalog/Concepts/39_panic.yaml b/internal/exercises/Catalog/Concepts/39_panic.yaml index 26e8932..13026ab 100644 --- a/internal/exercises/Catalog/Concepts/39_panic.yaml +++ b/internal/exercises/Catalog/Concepts/39_panic.yaml @@ -5,4 +5,4 @@ - Use `panic()` to simulate runtime errors when appropriate. - Use `defer` with `recover()` to catch and handle panics. - Recovering from panics allows graceful handling of unexpected situations. - - Remember: `recover()` only works inside a deferred function. + - Remember: '`recover()` only works inside a deferred function.' \ No newline at end of file From ee00e3e8a4b6297d4a888b620c5c4570569dab62 Mon Sep 17 00:00:00 2001 From: LeeFred3042U <109694901+LeeFred3042U@users.noreply.github.com> Date: Fri, 12 Dec 2025 23:59:46 +0530 Subject: [PATCH 7/7] feat: migrate catalog to directory structure and fix CI - Fixed CI workflows: resolved cache permission errors and added robust solution verification checks. - Added tests for the new catalog loader using in-memory filesystems. --- .github/workflows/ci.yml | 16 +- .../exercises/Catalog/Concepts/01_hello.yaml | 10 +- .../exercises/Catalog/Concepts/02_values.yaml | 10 +- .../Catalog/Concepts/03_variables.yaml | 10 +- .../Catalog/Concepts/04_constants.yaml | 10 +- .../exercises/Catalog/Concepts/05_for.yaml | 10 +- .../Catalog/Concepts/06_if_else.yaml | 10 +- .../exercises/Catalog/Concepts/07_switch.yaml | 10 +- .../exercises/Catalog/Concepts/08_arrays.yaml | 10 +- .../exercises/Catalog/Concepts/09_slices.yaml | 10 +- .../exercises/Catalog/Concepts/10_maps.yaml | 10 +- .../Catalog/Concepts/11_functions.yaml | 10 +- .../Catalog/Concepts/12_multi_return.yaml | 10 +- .../Catalog/Concepts/13_variadic.yaml | 10 +- .../Catalog/Concepts/14_closures.yaml | 10 +- .../Catalog/Concepts/15_recursion.yaml | 10 +- .../Catalog/Concepts/16_range_built_in.yaml | 10 +- .../Catalog/Concepts/17_pointers.yaml | 10 +- .../Catalog/Concepts/18_strings_runes.yaml | 10 +- .../Catalog/Concepts/19_structs.yaml | 10 +- .../Catalog/Concepts/20_methods.yaml | 10 +- .../Catalog/Concepts/21_interfaces.yaml | 10 +- .../exercises/Catalog/Concepts/22_enums.yaml | 10 +- .../Catalog/Concepts/23_struct_embedding.yaml | 10 +- .../Catalog/Concepts/24_generics.yaml | 10 +- .../Catalog/Concepts/25_range_iterators.yaml | 10 +- .../exercises/Catalog/Concepts/26_errors.yaml | 10 +- .../Catalog/Concepts/27_custom_errors.yaml | 10 +- .../exercises/Catalog/Concepts/28_defer.yaml | 10 +- .../Catalog/Concepts/29_go_routines.yaml | 10 +- .../Catalog/Concepts/30_channels.yaml | 10 +- .../Catalog/Concepts/31_mutexes.yaml | 10 +- .../Catalog/Concepts/32_sorting.yaml | 10 +- .../Concepts/33_string_formatting.yaml | 10 +- .../Concepts/34_channel_buffering.yaml | 10 +- .../Catalog/Concepts/35_channel_sync.yaml | 10 +- .../exercises/Catalog/Concepts/36_json.yaml | 16 +- .../Concepts/37_sorting_by_functions.yaml | 14 +- .../exercises/Catalog/Concepts/37_xml.yaml | 16 +- .../Catalog/Concepts/38_time_formatting.yaml | 14 +- .../Concepts/39_channel_directions.yaml | 18 +- .../exercises/Catalog/Concepts/39_panic.yaml | 15 +- .../Catalog/Concepts/40_channel_select.yaml | 12 +- .../Catalog/Concepts/41_time_delay.yaml | 16 +- .../Catalog/Concepts/42_wait_group.yaml | 18 +- .../Catalog/Concepts/43_worker_pools.yaml | 20 +- .../Catalog/Concepts/44_atomic_counters.yaml | 14 +- .../Concepts/45_range_over_channels.yaml | 10 +- .../46_non_blocking_channel_operations.yaml | 12 +- .../exercises/Catalog/Concepts/64_timers.yaml | 26 +- .../Catalog/Concepts/68_rate_limiting.yaml | 22 +- .../Catalog/Projects/01_text_analyzer.yaml | 11 +- .../Catalog/Projects/02_shape_calculator.yaml | 11 +- .../Catalog/Projects/03_task_scheduler.yaml | 11 +- .../Catalog/Projects/04_http_server.yaml | 11 +- .../Catalog/Projects/05_cli_todo_list.yaml | 11 +- .../Catalog/Projects/06_simple_chat_app.yaml | 11 +- .../Projects/07_image_processing_utility.yaml | 11 +- .../Projects/08_basic_key_value_store.yaml | 11 +- .../exercises/Catalog/Projects/09_epoch.yaml | 13 +- internal/exercises/exercises.go | 237 ++++++++---------- internal/exercises/exercises_test.go | 39 ++- internal/exercises/export_test.go | 27 ++ internal/exercises/solutions.go | 68 +++-- 64 files changed, 546 insertions(+), 535 deletions(-) create mode 100644 internal/exercises/export_test.go diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 28a6062..2361c59 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,7 +20,7 @@ jobs: go-version: '1.22' - name: Cache Go modules - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: ~/go/pkg/mod key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} @@ -80,10 +80,14 @@ jobs: - name: Test verify command with solutions (should pass) run: | echo "Testing that solution exercises pass..." - # Test a few key exercises with their solutions - ./bin/golearn verify 01_hello --solution - ./bin/golearn verify 36_json --solution - ./bin/golearn verify 37_xml --solution + for ex in 01_hello 36_json 37_xml; do + if [ -d "./internal/exercises/solutions/$ex" ]; then + echo "Running verify --solution for $ex" + ./bin/golearn verify "$ex" --solution + else + echo "Skipping $ex: no solution directory present" + fi + done - name: Test specific template exercises (should fail) run: | @@ -193,4 +197,4 @@ jobs: - name: Test watch command (timeout after 5 seconds) run: | - timeout 5s ./bin/golearn watch || true + timeout 5s ./bin/golearn watch || true \ No newline at end of file diff --git a/internal/exercises/Catalog/Concepts/01_hello.yaml b/internal/exercises/Catalog/Concepts/01_hello.yaml index 68d8e5f..489bd01 100644 --- a/internal/exercises/Catalog/Concepts/01_hello.yaml +++ b/internal/exercises/Catalog/Concepts/01_hello.yaml @@ -1,5 +1,5 @@ -- slug: 01_hello - title: Hello, Go! - test_regex: ".*" - hints: - - Implement Hello() to return 'Hello, Go!' +slug: 01_hello +title: Hello, Go! +test_regex: ".*" +hints: + - Implement Hello() to return 'Hello, Go!' diff --git a/internal/exercises/Catalog/Concepts/02_values.yaml b/internal/exercises/Catalog/Concepts/02_values.yaml index 5228dea..89f8676 100644 --- a/internal/exercises/Catalog/Concepts/02_values.yaml +++ b/internal/exercises/Catalog/Concepts/02_values.yaml @@ -1,5 +1,5 @@ -- slug: 02_values - title: Values - test_regex: ".*" - hints: - - Use fmt.Sprintf to format values. +slug: 02_values +title: Values +test_regex: ".*" +hints: + - Use fmt.Sprintf to format values. diff --git a/internal/exercises/Catalog/Concepts/03_variables.yaml b/internal/exercises/Catalog/Concepts/03_variables.yaml index 8c29eed..bba4d07 100644 --- a/internal/exercises/Catalog/Concepts/03_variables.yaml +++ b/internal/exercises/Catalog/Concepts/03_variables.yaml @@ -1,5 +1,5 @@ -- slug: 03_variables - title: Variables - test_regex: ".*" - hints: - - Use short declarations (:=) and return multiple values. +slug: 03_variables +title: Variables +test_regex: ".*" +hints: + - Use short declarations (:=) and return multiple values. diff --git a/internal/exercises/Catalog/Concepts/04_constants.yaml b/internal/exercises/Catalog/Concepts/04_constants.yaml index 1a20306..05f46fa 100644 --- a/internal/exercises/Catalog/Concepts/04_constants.yaml +++ b/internal/exercises/Catalog/Concepts/04_constants.yaml @@ -1,5 +1,5 @@ -- slug: 04_constants - title: Constants - test_regex: ".*" - hints: - - Use math.Pi and constant expressions. +slug: 04_constants +title: Constants +test_regex: ".*" +hints: + - Use math.Pi and constant expressions. diff --git a/internal/exercises/Catalog/Concepts/05_for.yaml b/internal/exercises/Catalog/Concepts/05_for.yaml index 2c17973..04376c8 100644 --- a/internal/exercises/Catalog/Concepts/05_for.yaml +++ b/internal/exercises/Catalog/Concepts/05_for.yaml @@ -1,5 +1,5 @@ -- slug: 05_for - title: For - test_regex: ".*" - hints: - - Accumulate a sum with a for loop. +slug: 05_for +title: For +test_regex: ".*" +hints: + - Accumulate a sum with a for loop. diff --git a/internal/exercises/Catalog/Concepts/06_if_else.yaml b/internal/exercises/Catalog/Concepts/06_if_else.yaml index 4ae1305..666da6f 100644 --- a/internal/exercises/Catalog/Concepts/06_if_else.yaml +++ b/internal/exercises/Catalog/Concepts/06_if_else.yaml @@ -1,5 +1,5 @@ -- slug: 06_if_else - title: If/Else - test_regex: ".*" - hints: - - Handle negative, zero, and positive cases. +slug: 06_if_else +title: If/Else +test_regex: ".*" +hints: + - Handle negative, zero, and positive cases. diff --git a/internal/exercises/Catalog/Concepts/07_switch.yaml b/internal/exercises/Catalog/Concepts/07_switch.yaml index 192da78..2efaace 100644 --- a/internal/exercises/Catalog/Concepts/07_switch.yaml +++ b/internal/exercises/Catalog/Concepts/07_switch.yaml @@ -1,5 +1,5 @@ -- slug: 07_switch - title: Switch - test_regex: ".*" - hints: - - Match multiple cases for weekend days. +slug: 07_switch +title: Switch +test_regex: ".*" +hints: + - Match multiple cases for weekend days. diff --git a/internal/exercises/Catalog/Concepts/08_arrays.yaml b/internal/exercises/Catalog/Concepts/08_arrays.yaml index cc42efa..0e140d9 100644 --- a/internal/exercises/Catalog/Concepts/08_arrays.yaml +++ b/internal/exercises/Catalog/Concepts/08_arrays.yaml @@ -1,5 +1,5 @@ -- slug: 08_arrays - title: Arrays - test_regex: ".*" - hints: - - Iterate with range over a fixed-size array. +slug: 08_arrays +title: Arrays +test_regex: ".*" +hints: + - Iterate with range over a fixed-size array. diff --git a/internal/exercises/Catalog/Concepts/09_slices.yaml b/internal/exercises/Catalog/Concepts/09_slices.yaml index 6df261d..ec57aed 100644 --- a/internal/exercises/Catalog/Concepts/09_slices.yaml +++ b/internal/exercises/Catalog/Concepts/09_slices.yaml @@ -1,5 +1,5 @@ -- slug: 09_slices - title: Slices - test_regex: ".*" - hints: - - Append values then compute a sum. +slug: 09_slices +title: Slices +test_regex: ".*" +hints: + - Append values then compute a sum. diff --git a/internal/exercises/Catalog/Concepts/10_maps.yaml b/internal/exercises/Catalog/Concepts/10_maps.yaml index a6e2e5f..6ebc4ed 100644 --- a/internal/exercises/Catalog/Concepts/10_maps.yaml +++ b/internal/exercises/Catalog/Concepts/10_maps.yaml @@ -1,5 +1,5 @@ -- slug: 10_maps - title: Maps - test_regex: ".*" - hints: - - Use strings.Fields and map[string]int for word counts. +slug: 10_maps +title: Maps +test_regex: ".*" +hints: + - Use strings.Fields and map[string]int for word counts. diff --git a/internal/exercises/Catalog/Concepts/11_functions.yaml b/internal/exercises/Catalog/Concepts/11_functions.yaml index a5bef19..92dc548 100644 --- a/internal/exercises/Catalog/Concepts/11_functions.yaml +++ b/internal/exercises/Catalog/Concepts/11_functions.yaml @@ -1,5 +1,5 @@ -- slug: 11_functions - title: Functions - test_regex: ".*" - hints: - - Pass a function and call it. +slug: 11_functions +title: Functions +test_regex: ".*" +hints: + - Pass a function and call it. diff --git a/internal/exercises/Catalog/Concepts/12_multi_return.yaml b/internal/exercises/Catalog/Concepts/12_multi_return.yaml index 8f4b142..1ac2478 100644 --- a/internal/exercises/Catalog/Concepts/12_multi_return.yaml +++ b/internal/exercises/Catalog/Concepts/12_multi_return.yaml @@ -1,5 +1,5 @@ -- slug: 12_multi_return - title: Multiple Return Values - test_regex: ".*" - hints: - - Return quotient, remainder, and an error for divide-by-zero. +slug: 12_multi_return +title: Multiple Return Values +test_regex: ".*" +hints: + - Return quotient, remainder, and an error for divide-by-zero. diff --git a/internal/exercises/Catalog/Concepts/13_variadic.yaml b/internal/exercises/Catalog/Concepts/13_variadic.yaml index 542e6f5..e3c070a 100644 --- a/internal/exercises/Catalog/Concepts/13_variadic.yaml +++ b/internal/exercises/Catalog/Concepts/13_variadic.yaml @@ -1,5 +1,5 @@ -- slug: 13_variadic - title: Variadic Functions - test_regex: ".*" - hints: - - Use '...' to accept any number of ints and sum them. +slug: 13_variadic +title: Variadic Functions +test_regex: ".*" +hints: + - Use '...' to accept any number of ints and sum them. diff --git a/internal/exercises/Catalog/Concepts/14_closures.yaml b/internal/exercises/Catalog/Concepts/14_closures.yaml index cc92e60..9446718 100644 --- a/internal/exercises/Catalog/Concepts/14_closures.yaml +++ b/internal/exercises/Catalog/Concepts/14_closures.yaml @@ -1,5 +1,5 @@ -- slug: 14_closures - title: Closures - test_regex: ".*" - hints: - - Implement a function that returns a closure, capturing an outer variable. +slug: 14_closures +title: Closures +test_regex: ".*" +hints: + - Implement a function that returns a closure, capturing an outer variable. diff --git a/internal/exercises/Catalog/Concepts/15_recursion.yaml b/internal/exercises/Catalog/Concepts/15_recursion.yaml index 5fbe301..ad48bfc 100644 --- a/internal/exercises/Catalog/Concepts/15_recursion.yaml +++ b/internal/exercises/Catalog/Concepts/15_recursion.yaml @@ -1,5 +1,5 @@ -- slug: 15_recursion - title: Recursion - test_regex: ".*" - hints: - - Implement a recursive factorial function. +slug: 15_recursion +title: Recursion +test_regex: ".*" +hints: + - Implement a recursive factorial function. diff --git a/internal/exercises/Catalog/Concepts/16_range_built_in.yaml b/internal/exercises/Catalog/Concepts/16_range_built_in.yaml index dfd3e14..8b33ddf 100644 --- a/internal/exercises/Catalog/Concepts/16_range_built_in.yaml +++ b/internal/exercises/Catalog/Concepts/16_range_built_in.yaml @@ -1,5 +1,5 @@ -- slug: 16_range_built_in - title: Range over Built-in Types - test_regex: ".*" - hints: - - Use range to iterate over a slice and a map. +slug: 16_range_built_in +title: Range over Built-in Types +test_regex: ".*" +hints: + - Use range to iterate over a slice and a map. diff --git a/internal/exercises/Catalog/Concepts/17_pointers.yaml b/internal/exercises/Catalog/Concepts/17_pointers.yaml index 19061cb..07818a2 100644 --- a/internal/exercises/Catalog/Concepts/17_pointers.yaml +++ b/internal/exercises/Catalog/Concepts/17_pointers.yaml @@ -1,5 +1,5 @@ -- slug: 17_pointers - title: Pointers - test_regex: ".*" - hints: - - Write a function that takes a pointer, modifies the value, and returns the pointer. +slug: 17_pointers +title: Pointers +test_regex: ".*" +hints: + - Write a function that takes a pointer, modifies the value, and returns the pointer. diff --git a/internal/exercises/Catalog/Concepts/18_strings_runes.yaml b/internal/exercises/Catalog/Concepts/18_strings_runes.yaml index 7868c11..dc24276 100644 --- a/internal/exercises/Catalog/Concepts/18_strings_runes.yaml +++ b/internal/exercises/Catalog/Concepts/18_strings_runes.yaml @@ -1,5 +1,5 @@ -- slug: 18_strings_runes - title: Strings and Runes - test_regex: ".*" - hints: - - Iterate over a string with range to count runes, and demonstrate string manipulation. +slug: 18_strings_runes +title: Strings and Runes +test_regex: ".*" +hints: + - Iterate over a string with range to count runes, and demonstrate string manipulation. diff --git a/internal/exercises/Catalog/Concepts/19_structs.yaml b/internal/exercises/Catalog/Concepts/19_structs.yaml index a818496..8512d76 100644 --- a/internal/exercises/Catalog/Concepts/19_structs.yaml +++ b/internal/exercises/Catalog/Concepts/19_structs.yaml @@ -1,5 +1,5 @@ -- slug: 19_structs - title: Structs - test_regex: ".*" - hints: - - Define a struct with fields for name and age, then create an instance. +slug: 19_structs +title: Structs +test_regex: ".*" +hints: + - Define a struct with fields for name and age, then create an instance. diff --git a/internal/exercises/Catalog/Concepts/20_methods.yaml b/internal/exercises/Catalog/Concepts/20_methods.yaml index 14f5cd8..cabc1c9 100644 --- a/internal/exercises/Catalog/Concepts/20_methods.yaml +++ b/internal/exercises/Catalog/Concepts/20_methods.yaml @@ -1,5 +1,5 @@ -- slug: 20_methods - title: Methods - test_regex: ".*" - hints: - - Add a method to a struct that calculates something based on its fields. +slug: 20_methods +title: Methods +test_regex: ".*" +hints: + - Add a method to a struct that calculates something based on its fields. diff --git a/internal/exercises/Catalog/Concepts/21_interfaces.yaml b/internal/exercises/Catalog/Concepts/21_interfaces.yaml index cc3e393..6c3eedf 100644 --- a/internal/exercises/Catalog/Concepts/21_interfaces.yaml +++ b/internal/exercises/Catalog/Concepts/21_interfaces.yaml @@ -1,5 +1,5 @@ -- slug: 21_interfaces - title: Interfaces - test_regex: ".*" - hints: - - Define an interface and implement it for a struct. +slug: 21_interfaces +title: Interfaces +test_regex: ".*" +hints: + - Define an interface and implement it for a struct. diff --git a/internal/exercises/Catalog/Concepts/22_enums.yaml b/internal/exercises/Catalog/Concepts/22_enums.yaml index e9408e7..ba10471 100644 --- a/internal/exercises/Catalog/Concepts/22_enums.yaml +++ b/internal/exercises/Catalog/Concepts/22_enums.yaml @@ -1,5 +1,5 @@ -- slug: 22_enums - title: Enums - test_regex: ".*" - hints: - - Use iota to create a set of related constants as an enumeration. +slug: 22_enums +title: Enums +test_regex: ".*" +hints: + - Use iota to create a set of related constants as an enumeration. diff --git a/internal/exercises/Catalog/Concepts/23_struct_embedding.yaml b/internal/exercises/Catalog/Concepts/23_struct_embedding.yaml index 1f0898e..38116c7 100644 --- a/internal/exercises/Catalog/Concepts/23_struct_embedding.yaml +++ b/internal/exercises/Catalog/Concepts/23_struct_embedding.yaml @@ -1,5 +1,5 @@ -- slug: 23_struct_embedding - title: Struct Embedding - test_regex: ".*" - hints: - - Embed one struct within another and access the inner struct's fields directly. +slug: 23_struct_embedding +title: Struct Embedding +test_regex: ".*" +hints: + - Embed one struct within another and access the inner struct's fields directly. diff --git a/internal/exercises/Catalog/Concepts/24_generics.yaml b/internal/exercises/Catalog/Concepts/24_generics.yaml index 81b0cd5..2f0d5e4 100644 --- a/internal/exercises/Catalog/Concepts/24_generics.yaml +++ b/internal/exercises/Catalog/Concepts/24_generics.yaml @@ -1,5 +1,5 @@ -- slug: 24_generics - title: Generics - test_regex: ".*" - hints: - - Write a generic function that works with different types. +slug: 24_generics +title: Generics +test_regex: ".*" +hints: + - Write a generic function that works with different types. diff --git a/internal/exercises/Catalog/Concepts/25_range_iterators.yaml b/internal/exercises/Catalog/Concepts/25_range_iterators.yaml index a640fa3..0687a91 100644 --- a/internal/exercises/Catalog/Concepts/25_range_iterators.yaml +++ b/internal/exercises/Catalog/Concepts/25_range_iterators.yaml @@ -1,5 +1,5 @@ -- slug: 25_range_iterators - title: Range over Iterators - test_regex: ".*" - hints: - - Implement a custom iterator and use range over it. +slug: 25_range_iterators +title: Range over Iterators +test_regex: ".*" +hints: + - Implement a custom iterator and use range over it. diff --git a/internal/exercises/Catalog/Concepts/26_errors.yaml b/internal/exercises/Catalog/Concepts/26_errors.yaml index 9a70347..d597ff1 100644 --- a/internal/exercises/Catalog/Concepts/26_errors.yaml +++ b/internal/exercises/Catalog/Concepts/26_errors.yaml @@ -1,5 +1,5 @@ -- slug: 26_errors - title: Errors - test_regex: ".*" - hints: - - Write a function that returns an error and handle it. +slug: 26_errors +title: Errors +test_regex: ".*" +hints: + - Write a function that returns an error and handle it. diff --git a/internal/exercises/Catalog/Concepts/27_custom_errors.yaml b/internal/exercises/Catalog/Concepts/27_custom_errors.yaml index 2b8d08e..b6f4a56 100644 --- a/internal/exercises/Catalog/Concepts/27_custom_errors.yaml +++ b/internal/exercises/Catalog/Concepts/27_custom_errors.yaml @@ -1,5 +1,5 @@ -- slug: 27_custom_errors - title: Custom Errors - test_regex: ".*" - hints: - - Define a custom error type and return it from a function. +slug: 27_custom_errors +title: Custom Errors +test_regex: ".*" +hints: + - Define a custom error type and return it from a function. diff --git a/internal/exercises/Catalog/Concepts/28_defer.yaml b/internal/exercises/Catalog/Concepts/28_defer.yaml index cddaaa3..925ed82 100644 --- a/internal/exercises/Catalog/Concepts/28_defer.yaml +++ b/internal/exercises/Catalog/Concepts/28_defer.yaml @@ -1,5 +1,5 @@ -- slug: 28_defer - title: Defer - test_regex: ".*" - hints: - - Use `f.Close()` with `defer` keyword. +slug: 28_defer +title: Defer +test_regex: ".*" +hints: + - Use `f.Close()` with `defer` keyword. diff --git a/internal/exercises/Catalog/Concepts/29_go_routines.yaml b/internal/exercises/Catalog/Concepts/29_go_routines.yaml index 5c0ca69..bb231f9 100644 --- a/internal/exercises/Catalog/Concepts/29_go_routines.yaml +++ b/internal/exercises/Catalog/Concepts/29_go_routines.yaml @@ -1,5 +1,5 @@ -- slug: 29_go_routines - title: Go Routines - test_regex: ".*" - hints: - - Use `go` keyword to execute functions concurrently using go routines. +slug: 29_go_routines +title: Go Routines +test_regex: ".*" +hints: + - Use `go` keyword to execute functions concurrently using go routines. diff --git a/internal/exercises/Catalog/Concepts/30_channels.yaml b/internal/exercises/Catalog/Concepts/30_channels.yaml index a099925..256f78b 100644 --- a/internal/exercises/Catalog/Concepts/30_channels.yaml +++ b/internal/exercises/Catalog/Concepts/30_channels.yaml @@ -1,5 +1,5 @@ -- slug: 30_channels - title: Channels - test_regex: ".*" - hints: - - Use `make(chan T)` to create a channel and `<-` to send and receive on it. +slug: 30_channels +title: Channels +test_regex: ".*" +hints: + - Use `make(chan T)` to create a channel and `<-` to send and receive on it. diff --git a/internal/exercises/Catalog/Concepts/31_mutexes.yaml b/internal/exercises/Catalog/Concepts/31_mutexes.yaml index cec0466..762dae8 100644 --- a/internal/exercises/Catalog/Concepts/31_mutexes.yaml +++ b/internal/exercises/Catalog/Concepts/31_mutexes.yaml @@ -1,5 +1,5 @@ -- slug: 31_mutexes - title: Mutexes - test_regex: ".*" - hints: - - Use `sync.Mutex` to synchronize access to a shared resource. +slug: 31_mutexes +title: Mutexes +test_regex: ".*" +hints: + - Use `sync.Mutex` to synchronize access to a shared resource. diff --git a/internal/exercises/Catalog/Concepts/32_sorting.yaml b/internal/exercises/Catalog/Concepts/32_sorting.yaml index 747b00f..0e031f0 100644 --- a/internal/exercises/Catalog/Concepts/32_sorting.yaml +++ b/internal/exercises/Catalog/Concepts/32_sorting.yaml @@ -1,5 +1,5 @@ -- slug: 32_sorting - title: Sorting - test_regex: ".*" - hints: - - Use `slice.Sort` for sorting slices. +slug: 32_sorting +title: Sorting +test_regex: ".*" +hints: + - Use `slice.Sort` for sorting slices. diff --git a/internal/exercises/Catalog/Concepts/33_string_formatting.yaml b/internal/exercises/Catalog/Concepts/33_string_formatting.yaml index 4132064..09dd089 100644 --- a/internal/exercises/Catalog/Concepts/33_string_formatting.yaml +++ b/internal/exercises/Catalog/Concepts/33_string_formatting.yaml @@ -1,5 +1,5 @@ -- slug: 33_string_formatting - title: String Formatting - test_regex: ".*" - hints: - - Use `fmt.Sprintf` and `%s`, `%d`, `%f` formatting directives to format strings, integers, and floats. +slug: 33_string_formatting +title: String Formatting +test_regex: ".*" +hints: + - Use `fmt.Sprintf` and `%s`, `%d`, `%f` formatting directives to format strings, integers, and floats. diff --git a/internal/exercises/Catalog/Concepts/34_channel_buffering.yaml b/internal/exercises/Catalog/Concepts/34_channel_buffering.yaml index ae6fd80..ddaea60 100644 --- a/internal/exercises/Catalog/Concepts/34_channel_buffering.yaml +++ b/internal/exercises/Catalog/Concepts/34_channel_buffering.yaml @@ -1,5 +1,5 @@ -- slug: 34_channel_buffering - title: Channel Buffering - test_regex: ".*" - hints: - - Use `make(chan T, N)` to create a buffered channel and `<-` to send and receive on it. +slug: 34_channel_buffering +title: Channel Buffering +test_regex: ".*" +hints: + - Use `make(chan T, N)` to create a buffered channel and `<-` to send and receive on it. diff --git a/internal/exercises/Catalog/Concepts/35_channel_sync.yaml b/internal/exercises/Catalog/Concepts/35_channel_sync.yaml index 2ee4376..9d5db2c 100644 --- a/internal/exercises/Catalog/Concepts/35_channel_sync.yaml +++ b/internal/exercises/Catalog/Concepts/35_channel_sync.yaml @@ -1,5 +1,5 @@ -- slug: 35_channel_sync - title: Channel Synchronization - test_regex: ".*" - hints: - - Use a buffered boolean channel and wait till go routine completes. +slug: 35_channel_sync +title: Channel Synchronization +test_regex: ".*" +hints: + - Use a buffered boolean channel and wait till go routine completes. diff --git a/internal/exercises/Catalog/Concepts/36_json.yaml b/internal/exercises/Catalog/Concepts/36_json.yaml index 99357c8..70242cc 100644 --- a/internal/exercises/Catalog/Concepts/36_json.yaml +++ b/internal/exercises/Catalog/Concepts/36_json.yaml @@ -1,9 +1,9 @@ -- slug: 36_json - title: JSON - test_regex: ".*" - hints: - - Use the encoding/json package to work with JSON data. - - Implement MarshalPerson to convert a struct into JSON using json.Marshal. - - Implement UnmarshalPerson to convert a JSON string into a struct using json.Unmarshal. - - Handle and return errors properly in both functions. +slug: 36_json +title: JSON +test_regex: ".*" +hints: + - Use the encoding/json package to work with JSON data. + - Implement MarshalPerson to convert a struct into JSON using json.Marshal. + - Implement UnmarshalPerson to convert a JSON string into a struct using json.Unmarshal. + - Handle and return errors properly in both functions. diff --git a/internal/exercises/Catalog/Concepts/37_sorting_by_functions.yaml b/internal/exercises/Catalog/Concepts/37_sorting_by_functions.yaml index 594545c..765900c 100644 --- a/internal/exercises/Catalog/Concepts/37_sorting_by_functions.yaml +++ b/internal/exercises/Catalog/Concepts/37_sorting_by_functions.yaml @@ -1,7 +1,7 @@ -- slug: 37_sorting_by_functions - title: "Sorting by Functions" - hints: - - Implement sort.Interface with Len(), Less(), and Swap() methods - - Create custom slice types like ByName and ByAge (e.g., type ByName []Person) - - Use sort.Sort() to sort slices with your custom comparison logic - - Remember to make copies of slices to avoid modifying the original \ No newline at end of file +slug: 37_sorting_by_functions +title: "Sorting by Functions" +hints: + - Implement sort.Interface with Len(), Less(), and Swap() methods + - Create custom slice types like ByName and ByAge (e.g., type ByName []Person) + - Use sort.Sort() to sort slices with your custom comparison logic + - Remember to make copies of slices to avoid modifying the original \ No newline at end of file diff --git a/internal/exercises/Catalog/Concepts/37_xml.yaml b/internal/exercises/Catalog/Concepts/37_xml.yaml index 311e4af..6c97c6b 100644 --- a/internal/exercises/Catalog/Concepts/37_xml.yaml +++ b/internal/exercises/Catalog/Concepts/37_xml.yaml @@ -1,8 +1,8 @@ -- slug: 37_xml - title: XML Encoding and Decoding - test_regex: ".*" - hints: - - Use encoding/xml package for marshaling and unmarshaling XML data. - - Add XML struct tags using `xml:"fieldname"` to map struct fields to XML elements. - - Use xml.Marshal to convert structs to XML bytes. - - Use xml.Unmarshal to parse XML bytes into structs. +slug: 37_xml +title: XML Encoding and Decoding +test_regex: ".*" +hints: + - Use encoding/xml package for marshaling and unmarshaling XML data. + - Add XML struct tags using `xml:"fieldname"` to map struct fields to XML elements. + - Use xml.Marshal to convert structs to XML bytes. + - Use xml.Unmarshal to parse XML bytes into structs. diff --git a/internal/exercises/Catalog/Concepts/38_time_formatting.yaml b/internal/exercises/Catalog/Concepts/38_time_formatting.yaml index d69db97..80d611e 100644 --- a/internal/exercises/Catalog/Concepts/38_time_formatting.yaml +++ b/internal/exercises/Catalog/Concepts/38_time_formatting.yaml @@ -1,7 +1,7 @@ -- slug: 38_time_formatting - title: "Time Formatting" - hints: - - Use time.Now().Format() to format current time with a specific layout - - Use time.Parse() to parse time strings with known layouts - - Use time.LoadLocation() to work with different timezones - - Extract time components using .Date() and .Clock() methods \ No newline at end of file +slug: 38_time_formatting +title: "Time Formatting" +hints: + - Use time.Now().Format() to format current time with a specific layout + - Use time.Parse() to parse time strings with known layouts + - Use time.LoadLocation() to work with different timezones + - Extract time components using .Date() and .Clock() methods \ No newline at end of file diff --git a/internal/exercises/Catalog/Concepts/39_channel_directions.yaml b/internal/exercises/Catalog/Concepts/39_channel_directions.yaml index 3b1c8f8..83eeb5d 100644 --- a/internal/exercises/Catalog/Concepts/39_channel_directions.yaml +++ b/internal/exercises/Catalog/Concepts/39_channel_directions.yaml @@ -1,9 +1,9 @@ -- slug: 39_channel_directions - title: Channel Directions - test_regex: ".*" - hints: - - Create two channels, one for sending jobs and one for receiving results. - - Start 5 workers using go routines. - - Send 50 jobs to the worker pool. - - Receive results from the worker pool and store them in a `rs` slice. - - Enforce type safety by specifying the channel directions in worker function. +slug: 39_channel_directions +title: Channel Directions +test_regex: ".*" +hints: + - Create two channels, one for sending jobs and one for receiving results. + - Start 5 workers using go routines. + - Send 50 jobs to the worker pool. + - Receive results from the worker pool and store them in a `rs` slice. + - Enforce type safety by specifying the channel directions in worker function. diff --git a/internal/exercises/Catalog/Concepts/39_panic.yaml b/internal/exercises/Catalog/Concepts/39_panic.yaml index 13026ab..1971e39 100644 --- a/internal/exercises/Catalog/Concepts/39_panic.yaml +++ b/internal/exercises/Catalog/Concepts/39_panic.yaml @@ -1,8 +1,7 @@ -- slug: 39_panic - title: Panic and Recover - test_regex: ".*" - hints: - - Use `panic()` to simulate runtime errors when appropriate. - - Use `defer` with `recover()` to catch and handle panics. - - Recovering from panics allows graceful handling of unexpected situations. - - Remember: '`recover()` only works inside a deferred function.' \ No newline at end of file +slug: 39_panic +title: Panic and Recover +test_regex: ".*" +hints: + - Use `panic()` to simulate runtime errors when appropriate. + - Use `defer` with `recover()` to catch and handle panics. + - Recovering from panics allows graceful handling of unexpected situations. \ No newline at end of file diff --git a/internal/exercises/Catalog/Concepts/40_channel_select.yaml b/internal/exercises/Catalog/Concepts/40_channel_select.yaml index 0c63e99..276a4a2 100644 --- a/internal/exercises/Catalog/Concepts/40_channel_select.yaml +++ b/internal/exercises/Catalog/Concepts/40_channel_select.yaml @@ -1,6 +1,6 @@ -- slug: 40_channel_select - title: Channel Select - test_regex: ".*" - hints: - - Use `select` to handle multiple channel operations concurrently. - - Use `time.After` to simulate a 5 microsecond timeout. +slug: 40_channel_select +title: Channel Select +test_regex: ".*" +hints: + - Use `select` to handle multiple channel operations concurrently. + - Use `time.After` to simulate a 5 microsecond timeout. diff --git a/internal/exercises/Catalog/Concepts/41_time_delay.yaml b/internal/exercises/Catalog/Concepts/41_time_delay.yaml index a1a870d..78b4e3e 100644 --- a/internal/exercises/Catalog/Concepts/41_time_delay.yaml +++ b/internal/exercises/Catalog/Concepts/41_time_delay.yaml @@ -1,8 +1,8 @@ -- slug: 41_time_delay - title: "Delays and Timers in Go" - test_regex: ".*" - hints: - - Use time.Sleep() to pause execution for a duration. - - Convert milliseconds to time.Duration using time.Millisecond. - - Use a buffered channel to send a signal after waiting. - - Use time.After or time.Sleep to trigger events after delays. +slug: 41_time_delay +title: "Delays and Timers in Go" +test_regex: ".*" +hints: + - Use time.Sleep() to pause execution for a duration. + - Convert milliseconds to time.Duration using time.Millisecond. + - Use a buffered channel to send a signal after waiting. + - Use time.After or time.Sleep to trigger events after delays. diff --git a/internal/exercises/Catalog/Concepts/42_wait_group.yaml b/internal/exercises/Catalog/Concepts/42_wait_group.yaml index e166f4e..a7b473b 100644 --- a/internal/exercises/Catalog/Concepts/42_wait_group.yaml +++ b/internal/exercises/Catalog/Concepts/42_wait_group.yaml @@ -1,9 +1,9 @@ -- slug: 42_wait_group - title: WaitGroups - test_regex: ".*" - hints: - - Use sync.WaitGroup to wait for all launched goroutines. - - Call wg.Add(1) before starting a goroutine and wg.Done() when it finishes. - - Close result channels after wg.Wait() so collectors can range over them. - - Capture loop variables correctly inside goroutines (e.g., `n := n`). - - Return results as a slice — order does not need to match input order. \ No newline at end of file +slug: 42_wait_group +title: WaitGroups +test_regex: ".*" +hints: + - Use sync.WaitGroup to wait for all launched goroutines. + - Call wg.Add(1) before starting a goroutine and wg.Done() when it finishes. + - Close result channels after wg.Wait() so collectors can range over them. + - Capture loop variables correctly inside goroutines (e.g., `n := n`). + - Return results as a slice — order does not need to match input order. \ No newline at end of file diff --git a/internal/exercises/Catalog/Concepts/43_worker_pools.yaml b/internal/exercises/Catalog/Concepts/43_worker_pools.yaml index ac6ae8e..0557da8 100644 --- a/internal/exercises/Catalog/Concepts/43_worker_pools.yaml +++ b/internal/exercises/Catalog/Concepts/43_worker_pools.yaml @@ -1,10 +1,10 @@ -- slug: 43_worker_pools - title: Worker Pools - test_regex: ".*" - hints: - - Create separate channels for jobs (send work) and results (receive processed data). - - Launch multiple worker goroutines that read from jobs channel and write to results channel. - - Use channel directions (<-chan for receive-only, chan<- for send-only) in worker function signature. - - Close the jobs channel after sending all work to signal workers to finish. - - Use strings.TrimSpace() to clean message strings by removing leading/trailing whitespace. - - Collect exactly len(logs) results to ensure all work is processed. +slug: 43_worker_pools +title: Worker Pools +test_regex: ".*" +hints: + - Create separate channels for jobs (send work) and results (receive processed data). + - Launch multiple worker goroutines that read from jobs channel and write to results channel. + - Use channel directions (<-chan for receive-only, chan<- for send-only) in worker function signature. + - Close the jobs channel after sending all work to signal workers to finish. + - Use strings.TrimSpace() to clean message strings by removing leading/trailing whitespace. + - Collect exactly len(logs) results to ensure all work is processed. diff --git a/internal/exercises/Catalog/Concepts/44_atomic_counters.yaml b/internal/exercises/Catalog/Concepts/44_atomic_counters.yaml index d6d5772..1c39610 100644 --- a/internal/exercises/Catalog/Concepts/44_atomic_counters.yaml +++ b/internal/exercises/Catalog/Concepts/44_atomic_counters.yaml @@ -1,7 +1,7 @@ -- slug: 44_atomic_counters - title: Atomic Counters - test_regex: ".*" - hints: - - Use `sync/atomic` package to create and increment atomic counter. - - Launch 10,000 anonymous go routines that simultaneously increment the counter. - - Use WaitGroups to wait for all go routines to finish. +slug: 44_atomic_counters +title: Atomic Counters +test_regex: ".*" +hints: + - Use `sync/atomic` package to create and increment atomic counter. + - Launch 10,000 anonymous go routines that simultaneously increment the counter. + - Use WaitGroups to wait for all go routines to finish. diff --git a/internal/exercises/Catalog/Concepts/45_range_over_channels.yaml b/internal/exercises/Catalog/Concepts/45_range_over_channels.yaml index 3e85183..1ac2e11 100644 --- a/internal/exercises/Catalog/Concepts/45_range_over_channels.yaml +++ b/internal/exercises/Catalog/Concepts/45_range_over_channels.yaml @@ -1,5 +1,5 @@ -- slug: 45_range_over_channels - title: Range over channels - test_regex: ".*" - hints: - - Use `for i := range ch {...}` syntax to range over a channels. +slug: 45_range_over_channels +title: Range over channels +test_regex: ".*" +hints: + - Use `for i := range ch {...}` syntax to range over a channels. diff --git a/internal/exercises/Catalog/Concepts/46_non_blocking_channel_operations.yaml b/internal/exercises/Catalog/Concepts/46_non_blocking_channel_operations.yaml index 34932ae..3394cb2 100644 --- a/internal/exercises/Catalog/Concepts/46_non_blocking_channel_operations.yaml +++ b/internal/exercises/Catalog/Concepts/46_non_blocking_channel_operations.yaml @@ -1,7 +1,7 @@ -- slug: 46_non_blocking_channel_operations - title: Non-blocking channel operations - test_regex: ".*" - hints: - - Use `default` case in select to handle non-blocking channel operations. - - Safely increment `dropped` using mutex `Lock()` and `Unlock()`. +slug: 46_non_blocking_channel_operations +title: Non-blocking channel operations +test_regex: ".*" +hints: + - Use `default` case in select to handle non-blocking channel operations. + - Safely increment `dropped` using mutex `Lock()` and `Unlock()`. diff --git a/internal/exercises/Catalog/Concepts/64_timers.yaml b/internal/exercises/Catalog/Concepts/64_timers.yaml index 3c989e0..9ab1914 100644 --- a/internal/exercises/Catalog/Concepts/64_timers.yaml +++ b/internal/exercises/Catalog/Concepts/64_timers.yaml @@ -1,13 +1,13 @@ -- slug: 64_timers - title: "Timers" - test_regex: ".*" - hints: - - Use a map[string]*time.Timer to track active timers by key. - - Use a mutex to safely access the timers map concurrently. - - Start a timer with time.AfterFunc(d, fn) to run a callback after duration d. - - If Start is called again for an existing key, stop and replace the old timer. - - Stop should remove the timer from the map and prevent the callback from firing. - - Reset can call timer.Reset(d) to reschedule the same callback. - - Remember to remove timers from the map after they fire to avoid memory leaks. - - Write tests to verify Start, Stop, and Reset behavior, including concurrency scenarios. - +slug: 64_timers +title: "Timers" +test_regex: ".*" +hints: + - Use a map[string]*time.Timer to track active timers by key. + - Use a mutex to safely access the timers map concurrently. + - Start a timer with time.AfterFunc(d, fn) to run a callback after duration d. + - If Start is called again for an existing key, stop and replace the old timer. + - Stop should remove the timer from the map and prevent the callback from firing. + - Reset can call timer.Reset(d) to reschedule the same callback. + - Remember to remove timers from the map after they fire to avoid memory leaks. + - Write tests to verify Start, Stop, and Reset behavior, including concurrency scenarios. + diff --git a/internal/exercises/Catalog/Concepts/68_rate_limiting.yaml b/internal/exercises/Catalog/Concepts/68_rate_limiting.yaml index f21cc8c..9975b1a 100644 --- a/internal/exercises/Catalog/Concepts/68_rate_limiting.yaml +++ b/internal/exercises/Catalog/Concepts/68_rate_limiting.yaml @@ -1,11 +1,11 @@ -- slug: 68_rate_limiting - title: Rate Limiting - test_regex: ".*" - hints: - - Implement a RateLimiter struct that tracks request timestamps per key using a map[string][]time.Time. - - Use a mutex to safely handle concurrent access to the map. - - In the Allow method, remove timestamps older than the interval and check if the number of recent requests exceeds the limit. - - Reset should clear the request history for a key; if the key does not exist, do nothing. - - Consider edge cases: multiple keys, concurrent access, and requests exactly at the interval boundary. - - In tests, use time.Sleep with a small buffer above the interval to avoid flakiness. - - Initialize the timestamps map in NewRateLimiter to prevent nil pointer errors. +slug: 68_rate_limiting +title: Rate Limiting +test_regex: ".*" +hints: + - Implement a RateLimiter struct that tracks request timestamps per key using a map[string][]time.Time. + - Use a mutex to safely handle concurrent access to the map. + - In the Allow method, remove timestamps older than the interval and check if the number of recent requests exceeds the limit. + - Reset should clear the request history for a key; if the key does not exist, do nothing. + - Consider edge cases:` multiple keys, concurrent access, and requests exactly at the interval boundary. + - In tests, use time.Sleep with a small buffer above the interval to avoid flakiness. + - Initialize the timestamps map in NewRateLimiter to prevent nil pointer errors. diff --git a/internal/exercises/Catalog/Projects/01_text_analyzer.yaml b/internal/exercises/Catalog/Projects/01_text_analyzer.yaml index 5c8a4da..7220644 100644 --- a/internal/exercises/Catalog/Projects/01_text_analyzer.yaml +++ b/internal/exercises/Catalog/Projects/01_text_analyzer.yaml @@ -1,5 +1,6 @@ -- slug: 01_text_analyzer - title: Text Analyzer (Easy) - test_regex: ".*" - hints: - - Implement functions to count characters, words, and unique words in a given text. +slug: 01_text_analyzer +dir: 101_text_analyzer +title: Text Analyzer (Easy) +test_regex: ".*" +hints: + - Implement functions to count characters, words, and unique words in a given text. diff --git a/internal/exercises/Catalog/Projects/02_shape_calculator.yaml b/internal/exercises/Catalog/Projects/02_shape_calculator.yaml index fafb0b9..5ecd111 100644 --- a/internal/exercises/Catalog/Projects/02_shape_calculator.yaml +++ b/internal/exercises/Catalog/Projects/02_shape_calculator.yaml @@ -1,5 +1,6 @@ -- slug: 02_shape_calculator - title: Shape Area Calculator (Medium) - test_regex: ".*" - hints: - - Define structs for different shapes, implement methods to calculate their areas, and use an interface for common shape behavior. +slug: 02_shape_calculator +dir: 102_shape_calculator +title: Shape Area Calculator (Medium) +test_regex: ".*" +hints: + - Define structs for different shapes, implement methods to calculate their areas, and use an interface for common shape behavior. diff --git a/internal/exercises/Catalog/Projects/03_task_scheduler.yaml b/internal/exercises/Catalog/Projects/03_task_scheduler.yaml index adc0802..63bd470 100644 --- a/internal/exercises/Catalog/Projects/03_task_scheduler.yaml +++ b/internal/exercises/Catalog/Projects/03_task_scheduler.yaml @@ -1,5 +1,6 @@ -- slug: 03_task_scheduler - title: Task Scheduler (Hard) - test_regex: ".*" - hints: - - Create a task scheduler with features like adding/removing tasks, a custom iterator, closures for task execution, and custom error handling. +slug: 03_task_scheduler +dir: 103_task_scheduler +title: Task Scheduler (Hard) +test_regex: ".*" +hints: + - Create a task scheduler with features like adding/removing tasks, a custom iterator, closures for task execution, and custom error handling. diff --git a/internal/exercises/Catalog/Projects/04_http_server.yaml b/internal/exercises/Catalog/Projects/04_http_server.yaml index cd5727a..fff04d1 100644 --- a/internal/exercises/Catalog/Projects/04_http_server.yaml +++ b/internal/exercises/Catalog/Projects/04_http_server.yaml @@ -1,5 +1,6 @@ -- slug: 04_http_server - title: HTTP Server (Easy) - test_regex: ".*" - hints: - - Implement a basic HTTP server that responds to GET requests. +slug: 04_http_server +dir: 104_http_server +title: HTTP Server (Easy) +test_regex: ".*" +hints: + - Implement a basic HTTP server that responds to GET requests. diff --git a/internal/exercises/Catalog/Projects/05_cli_todo_list.yaml b/internal/exercises/Catalog/Projects/05_cli_todo_list.yaml index 654b7bf..7c46c4c 100644 --- a/internal/exercises/Catalog/Projects/05_cli_todo_list.yaml +++ b/internal/exercises/Catalog/Projects/05_cli_todo_list.yaml @@ -1,5 +1,6 @@ -- slug: 05_cli_todo_list - title: CLI Todo List (Medium) - test_regex: ".*" - hints: - - Build a command-line tool to manage a todo list, including adding, listing, and completing tasks. +slug: 05_cli_todo_list +dir: 105_cli_todo_list +title: CLI Todo List (Medium) +test_regex: ".*" +hints: + - Build a command-line tool to manage a todo list, including adding, listing, and completing tasks. diff --git a/internal/exercises/Catalog/Projects/06_simple_chat_app.yaml b/internal/exercises/Catalog/Projects/06_simple_chat_app.yaml index 672a141..a2bf5f9 100644 --- a/internal/exercises/Catalog/Projects/06_simple_chat_app.yaml +++ b/internal/exercises/Catalog/Projects/06_simple_chat_app.yaml @@ -1,5 +1,6 @@ -- slug: 06_simple_chat_app - title: Simple Chat Application (Medium) - test_regex: ".*" - hints: - - Create a basic client-server chat application. +slug: 06_simple_chat_app +dir: 106_simple_chat_app +title: Simple Chat Application (Medium) +test_regex: ".*" +hints: + - Create a basic client-server chat application. diff --git a/internal/exercises/Catalog/Projects/07_image_processing_utility.yaml b/internal/exercises/Catalog/Projects/07_image_processing_utility.yaml index 05e2fbd..d722391 100644 --- a/internal/exercises/Catalog/Projects/07_image_processing_utility.yaml +++ b/internal/exercises/Catalog/Projects/07_image_processing_utility.yaml @@ -1,5 +1,6 @@ -- slug: 07_image_processing_utility - title: Image Processing Utility (Hard) - test_regex: ".*" - hints: - - Develop a command-line utility for basic image transformations like resizing or converting to grayscale. +slug: 07_image_processing_utility +dir: 107_image_processing_utility +title: Image Processing Utility (Hard) +test_regex: ".*" +hints: + - Develop a command-line utility for basic image transformations like resizing or converting to grayscale. diff --git a/internal/exercises/Catalog/Projects/08_basic_key_value_store.yaml b/internal/exercises/Catalog/Projects/08_basic_key_value_store.yaml index 84e672a..3758aed 100644 --- a/internal/exercises/Catalog/Projects/08_basic_key_value_store.yaml +++ b/internal/exercises/Catalog/Projects/08_basic_key_value_store.yaml @@ -1,5 +1,6 @@ -- slug: 08_basic_key_value_store - title: Basic Key-Value Store (Hard) - test_regex: ".*" - hints: - - Implement an in-memory key-value store with basic CRUD operations and optional persistence. +slug: 08_basic_key_value_store +dir: 108_basic_key_value_store +title: Basic Key-Value Store (Hard) +test_regex: ".*" +hints: + - Implement an in-memory key-value store with basic CRUD operations and optional persistence. diff --git a/internal/exercises/Catalog/Projects/09_epoch.yaml b/internal/exercises/Catalog/Projects/09_epoch.yaml index c608b55..1b129e4 100644 --- a/internal/exercises/Catalog/Projects/09_epoch.yaml +++ b/internal/exercises/Catalog/Projects/09_epoch.yaml @@ -1,6 +1,7 @@ -- slug: 09_epoch - title: "Epoch Conversion" - hints: - - Use Go's `time.Unix()` to convert an epoch to time. - - Use `t.Unix()` to convert time back to epoch. - - Remember Go’s `time.Parse` can help parse date strings. +slug: 09_epoch +dir: 109_epoch +title: "Epoch Conversion" +hints: + - Use Go's `time.Unix()` to convert an epoch to time. + - Use `t.Unix()` to convert time back to epoch. + - Remember Go’s `time.Parse` can help parse date strings. diff --git a/internal/exercises/exercises.go b/internal/exercises/exercises.go index 54f8c5d..4ea1004 100644 --- a/internal/exercises/exercises.go +++ b/internal/exercises/exercises.go @@ -6,8 +6,10 @@ import ( "fmt" "io/fs" "os" + "path" "path/filepath" "sort" + "strings" "sync" "gopkg.in/yaml.v3" @@ -21,134 +23,137 @@ var catalogFS embed.FS type Exercise struct { Slug string `yaml:"slug"` + Dir string `yaml:"dir,omitempty"` // Maps to folder name if different from slug Title string `yaml:"title"` TestRegex string `yaml:"test_regex"` Hints []string `yaml:"hints"` } -type Catalog struct { - Concepts []Exercise - Projects []Exercise +// Path returns the directory name in templates/solutions. +func (e Exercise) Path() string { + if e.Dir != "" { + return e.Dir + } + return e.Slug } -// --- Catalog Loader Infrastructure --- - -// defaultCatalogLoader loads the catalog from embedded FS. -// Tests override this to inject fake catalogs. -var defaultCatalogLoader = func() (Catalog, error) { - return loadCatalogFromFS(catalogFS) +type Catalog struct { + Concepts []Exercise `yaml:"concepts"` + Projects []Exercise `yaml:"projects"` } var ( catalogMu sync.Mutex - catalogOnce sync.Once - catalogData Catalog + catalogData *Catalog + + catalogLoader = func() (Catalog, error) { + return loadCatalogFromFS(catalogFS) + } ) -// withTestCatalogLoader temporarily overrides the catalog loader -// and resets internal singleton state for the duration of the test. -func withTestCatalogLoader(loader func() (Catalog, error), fn func()) { +// catalog returns the singleton Catalog instance. +func catalog() Catalog { catalogMu.Lock() + defer catalogMu.Unlock() - oldLoader := defaultCatalogLoader - - // override loader + reset singleton - defaultCatalogLoader = loader - catalogOnce = sync.Once{} - catalogData = Catalog{} - - catalogMu.Unlock() + if catalogData != nil { + return *catalogData + } - fn() + c, err := catalogLoader() + if err != nil { + // If loading fails, use fallback + f := fallbackCatalog() + catalogData = &f + return *catalogData + } - // restore loader, and reset the once/data again - catalogMu.Lock() - defaultCatalogLoader = oldLoader - catalogOnce = sync.Once{} - catalogData = Catalog{} - catalogMu.Unlock() + catalogData = &c + return *catalogData } -// Get the singleton catalog instance. -// Loads from embedded FS on first call, -// or falls back to default if loading fails. -func catalog() Catalog { - catalogOnce.Do(func() { - cat, err := defaultCatalogLoader() - if err != nil || (len(cat.Concepts) == 0 && len(cat.Projects) == 0) { - catalogData = fallbackCatalog() - return - } - catalogData = cat - }) - return catalogData -} - -// Load catalog from a given FS -// Returns a Catalog struct. +// loadCatalogFromFS scans the provided filesystem for exercise definitions. +// It accepts fs.FS to allow testing with fstest.MapFS. func loadCatalogFromFS(fsys fs.FS) (Catalog, error) { concepts, err := loadExercisesDir(fsys, "Catalog/Concepts") - if err != nil && !errors.Is(err, fs.ErrNotExist) { + if err != nil { return Catalog{}, err } - projects, err := loadExercisesDir(fsys, "Catalog/Projects") - if err != nil && !errors.Is(err, fs.ErrNotExist) { + if err != nil { return Catalog{}, err } + 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 + } + } - // Deterministic ordering - sort.Slice(concepts, func(i, j int) bool { return concepts[i].Slug < concepts[j].Slug }) - sort.Slice(projects, func(i, j int) bool { return projects[i].Slug < projects[j].Slug }) - - return Catalog{ - Concepts: concepts, - Projects: projects, - }, nil + return Catalog{Concepts: concepts, Projects: projects}, nil } -// Load all exercises from a given directory in the FS -// Returns a slice of Exercise structs. +// loadExercisesDir reads all .yaml/.yml files in a directory and returns sorted exercises. func loadExercisesDir(fsys fs.FS, dir string) ([]Exercise, error) { + var exercises []Exercise entries, err := fs.ReadDir(fsys, dir) + if errors.Is(err, fs.ErrNotExist) { + return []Exercise{}, nil + } if err != nil { return nil, err } - - var out []Exercise - for _, e := range entries { - if e.IsDir() { - continue - } - - name := e.Name() - if ext := filepath.Ext(name); ext != ".yaml" && ext != ".yml" { + if e.IsDir() || (!strings.HasSuffix(e.Name(), ".yaml") && !strings.HasSuffix(e.Name(), ".yml")) { continue } - - b, err := fs.ReadFile(fsys, filepath.Join(dir, name)) + data, err := fs.ReadFile(fsys, path.Join(dir, e.Name())) if err != nil { - return nil, err + return nil, fmt.Errorf("read %s: %w", e.Name(), err) } - var ex Exercise - if err := yaml.Unmarshal(b, &ex); err != nil { - return nil, fmt.Errorf("%s: %w", name, err) + 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("%s: missing slug", name) + return nil, fmt.Errorf("exercise in %s missing slug", e.Name()) } - - out = append(out, ex) + // 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) } - - return out, nil + sort.Slice(exercises, func(i, j int) bool { return exercises[i].Slug < exercises[j].Slug }) + return exercises, nil } -// Fallback catalog in case of errors -// or no embedded exercises found. +// fallbackCatalog provides a minimal hardcoded catalog if loading fails. func fallbackCatalog() Catalog { return Catalog{ Concepts: []Exercise{{ @@ -160,8 +165,7 @@ func fallbackCatalog() Catalog { } } -// Discover local exercises from "exercises" directory -// Returns a slice of Exercise structs. +// discoverLocal checks the local exercises/ directory for any user-added exercises. func discoverLocal() ([]Exercise, error) { var items []Exercise entries, err := os.ReadDir("exercises") @@ -171,50 +175,39 @@ func discoverLocal() ([]Exercise, error) { if err != nil { return nil, err } - for _, e := range entries { if e.IsDir() { slug := e.Name() - items = append(items, Exercise{ - Slug: slug, - Title: slug, - TestRegex: ".*", - Hints: nil, - }) + items = append(items, Exercise{Slug: slug, Title: slug, TestRegex: ".*"}) } } return items, nil } -// List all exercises from catalog or local exercises. +// ListAll returns all exercises from the catalog or local directory if present. func ListAll() (Catalog, error) { locals, err := discoverLocal() if err != nil { return Catalog{}, err } - if len(locals) > 0 { return Catalog{Concepts: locals}, nil } - return catalog(), nil } -// Get exercise by slug from catalog or local exercises. +// Get looks up an exercise by its slug or directory name. func Get(slug string) (Exercise, error) { - // Search catalog for _, ex := range catalog().Concepts { - if ex.Slug == slug { + if ex.Slug == slug || ex.Dir == slug { return ex, nil } } for _, ex := range catalog().Projects { - if ex.Slug == slug { + if ex.Slug == slug || ex.Dir == slug { return ex, nil } } - - // Search local locals, err := discoverLocal() if err != nil { return Exercise{}, err @@ -224,72 +217,64 @@ func Get(slug string) (Exercise, error) { return ex, nil } } - return Exercise{}, fmt.Errorf("exercise not found: %s", slug) } -// Reset exercise template for a given slug -// from embedded FS to local exercises dir. +// Reset restores the exercise files from the embedded template. func Reset(ex Exercise) error { - if !templateExists(ex.Slug) { + if !templateExists(ex.Path()) { return fmt.Errorf("reset unsupported for non-embedded exercise '%s'", ex.Slug) } - return copyExerciseTemplate(ex.Slug) + return copyExerciseTemplate(ex) } -func templateExists(slug string) bool { - root := filepath.Join("templates", slug) +// templateExists reports whether a template directory exists in the embedded templatesFS. +func templateExists(dirName string) bool { + root := path.Join("templates", dirName) _, err := fs.Stat(templatesFS, root) return err == nil } -// Initialize all exercises from embedded templates. +// InitAll initializes all exercises from the embedded templates. func InitAll() error { for _, ex := range catalog().Concepts { - if err := copyExerciseTemplate(ex.Slug); err != nil { + if err := copyExerciseTemplate(ex); err != nil { return err } } for _, ex := range catalog().Projects { - if err := copyExerciseTemplate(ex.Slug); err != nil { + if err := copyExerciseTemplate(ex); err != nil { return err } } return nil } -// Copy exercise template from embedded FS to local exercises dir. -func copyExerciseTemplate(slug string) error { - targetDir := filepath.Join("exercises", slug) - - // Clean slate +// copyExerciseTemplate copies the template files for an exercise into the local exercises/ directory. +func copyExerciseTemplate(ex Exercise) error { + targetDir := filepath.Join("exercises", ex.Slug) _ = os.RemoveAll(targetDir) if err := os.MkdirAll(targetDir, 0o755); err != nil { return err } - root := filepath.Join("templates", slug) - return fs.WalkDir(templatesFS, root, func(path string, d fs.DirEntry, err error) error { + root := path.Join("templates", ex.Path()) + return fs.WalkDir(templatesFS, root, func(fpath string, d fs.DirEntry, err error) error { if err != nil { return err } - - rel, err := filepath.Rel(root, path) + rel, err := filepath.Rel(root, filepath.FromSlash(fpath)) if err != nil { - return err + rel = strings.TrimPrefix(fpath, root+"/") } - 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, path) + data, err := fs.ReadFile(templatesFS, fpath) if err != nil { return err } diff --git a/internal/exercises/exercises_test.go b/internal/exercises/exercises_test.go index a3c4ac8..a982e1c 100644 --- a/internal/exercises/exercises_test.go +++ b/internal/exercises/exercises_test.go @@ -2,7 +2,6 @@ package exercises import ( "strings" - "sync" "testing" "testing/fstest" @@ -43,14 +42,9 @@ func TestLoadExercisesDir_LoadsValidYAML(t *testing.T) { t.Fatalf("expected 2 items, got %d", len(items)) } - // Unsorted read → sorted in loadCatalogFromFS, but loadExercisesDir just loads. - found := map[string]bool{ - items[0].Slug: true, - items[1].Slug: true, - } - - if !found["01_hello"] || !found["02_vars"] { - t.Fatalf("missing expected slugs: %v", found) + // Unsorted read → sorted in loadCatalogFromFS, but loadExercisesDir returns them sorted by slug. + if items[0].Slug != "01_hello" || items[1].Slug != "02_vars" { + t.Fatalf("items not sorted properly: %v", items) } } @@ -156,24 +150,22 @@ func TestFallbackCatalog(t *testing.T) { } func TestCatalog_UsesDirectoryLoader(t *testing.T) { - // Reset catalogOnce - catalogOnce = sync.Once{} - + // We verify that the singleton `catalog()` function actually calls our custom loader logic. fsys := fstest.MapFS{ "Catalog/Concepts/a.yaml": &fstest.MapFile{ Data: y(Exercise{Slug: "01_test"}), }, } - // Call loader directly - cat, err := loadCatalogFromFS(fsys) - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - - if len(cat.Concepts) != 1 || cat.Concepts[0].Slug != "01_test" { - t.Fatalf("loadCatalogFromFS did not correctly read from FS") - } + // Use the exported helper to swap the loader with one that uses our test filesystem + WithTestCatalogLoader(func() (Catalog, error) { + return loadCatalogFromFS(fsys) + }, func() { + cat := catalog() + if len(cat.Concepts) != 1 || cat.Concepts[0].Slug != "01_test" { + t.Fatalf("catalog() did not load from the injected FS") + } + }) } func TestDiscoverLocal_NoDir(t *testing.T) { @@ -184,14 +176,15 @@ func TestDiscoverLocal_NoDir(t *testing.T) { } func TestCatalogOverride(t *testing.T) { - withTestCatalogLoader(func() (Catalog, error) { + WithTestCatalogLoader(func() (Catalog, error) { return Catalog{ Concepts: []Exercise{{Slug: "01_mock"}}, }, nil }, func() { c := catalog() + if len(c.Concepts) != 1 || c.Concepts[0].Slug != "01_mock" { - t.Fatalf("expected mock catalog") + t.Fatalf("expected mock catalog, got: %+v", c) } }) } diff --git a/internal/exercises/export_test.go b/internal/exercises/export_test.go new file mode 100644 index 0000000..1e0efae --- /dev/null +++ b/internal/exercises/export_test.go @@ -0,0 +1,27 @@ +package exercises + +// WithTestCatalogLoader replaces the catalog loader temporarily for a test. +// This allows you to inject mock data and reset the singleton state. +func WithTestCatalogLoader(mockLoader func() (Catalog, error), testFunc func()) { + catalogMu.Lock() + + // Save old state + oldLoader := catalogLoader + oldData := catalogData + + // Swap in mock and force reload + catalogLoader = mockLoader + catalogData = nil + + catalogMu.Unlock() + + defer func() { + catalogMu.Lock() + // Restore old state + catalogLoader = oldLoader + catalogData = oldData + catalogMu.Unlock() + }() + + testFunc() +} diff --git a/internal/exercises/solutions.go b/internal/exercises/solutions.go index 7dd669a..5b8a882 100644 --- a/internal/exercises/solutions.go +++ b/internal/exercises/solutions.go @@ -5,30 +5,38 @@ import ( "errors" "io/fs" "os" + pathpkg "path" "path/filepath" "strings" ) -// Embed the canonical solutions alongside templates. We do not expose these via CLI directly. -// //go:embed all:solutions/** var solutionsFS embed.FS -// SolutionExists reports whether we have an embedded solution for the given slug. -func SolutionExists(slug string) bool { - root := filepath.Join("solutions", slug) +// SolutionExists reports whether a solution directory exists in the embedded solutionsFS. +// dirName is the directory name under solutions/ (usually the exercise's Path). +func SolutionExists(dirName string) bool { + root := pathpkg.Join("solutions", dirName) _, err := fs.Stat(solutionsFS, root) return err == nil } -// CreateSolutionSandbox creates a temporary module containing the solution implementation -// files and the original tests from templates for the given exercise slug. -// The returned directory can be used as the working directory for `go test`. +// CreateSolutionSandbox creates a temporary sandbox directory for the solution of the given exercise slug. +// It copies the solution implementation and test files into the sandbox. +// It returns the path to the sandbox, a cleanup function to remove it, and any error encountered. func CreateSolutionSandbox(slug string) (string, func(), error) { - if !templateExists(slug) { + // 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(slug) { + if !SolutionExists(srcDir) { return "", func() {}, errors.New("no embedded solution available") } @@ -38,7 +46,7 @@ func CreateSolutionSandbox(slug string) (string, func(), error) { } cleanup := func() { _ = os.RemoveAll(workDir) } - // 1) Create a minimal go.mod. We pin testify which is used by some tests. + // Create go.mod using the SLUG (01) for the module name goMod := "module golearn/tmp/" + strings.ReplaceAll(slug, "_", "-") + "\n\n" + "go 1.22.0\n\n" + "require github.com/stretchr/testify v1.11.0\n" @@ -47,8 +55,8 @@ func CreateSolutionSandbox(slug string) (string, func(), error) { return "", func() {}, err } - // 2) Copy tests from embedded templates - templateRoot := filepath.Join("templates", slug) + // 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 { if err != nil { return err @@ -59,26 +67,19 @@ func CreateSolutionSandbox(slug string) (string, func(), error) { if !strings.HasSuffix(path, "_test.go") { return nil } - rel, err := filepath.Rel(templateRoot, path) - if err != nil { - return err - } + + rel, _ := filepath.Rel(templateRoot, path) dest := filepath.Join(workDir, rel) - if err := os.MkdirAll(filepath.Dir(dest), 0o755); err != nil { - return err - } - data, err := fs.ReadFile(templatesFS, path) - if err != nil { - return err - } + _ = os.MkdirAll(filepath.Dir(dest), 0o755) + data, _ := fs.ReadFile(templatesFS, path) return os.WriteFile(dest, data, 0o644) }); err != nil { cleanup() return "", func() {}, err } - // 3) Copy solution implementation files (exclude *_test.go just in case) - solutionRoot := filepath.Join("solutions", slug) + // 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 { if err != nil { return err @@ -89,18 +90,11 @@ func CreateSolutionSandbox(slug string) (string, func(), error) { if strings.HasSuffix(path, "_test.go") { return nil } - rel, err := filepath.Rel(solutionRoot, path) - if err != nil { - return err - } + + rel, _ := filepath.Rel(solutionRoot, path) dest := filepath.Join(workDir, rel) - if err := os.MkdirAll(filepath.Dir(dest), 0o755); err != nil { - return err - } - data, err := fs.ReadFile(solutionsFS, path) - if err != nil { - return err - } + _ = os.MkdirAll(filepath.Dir(dest), 0o755) + data, _ := fs.ReadFile(solutionsFS, path) return os.WriteFile(dest, data, 0o644) }); err != nil { cleanup()