Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 11 additions & 3 deletions internal/exercises/catalog.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -192,12 +192,20 @@ projects:
- 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"]
test_regex: ".*"
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: 110_recover
title: Safe Panic Recovery
test_regex: ".*"
hints:
- hint_type: concept
text: The `recover()` function is ONLY useful inside a function that is executed by a `defer` statement. Outside of a deferred function, `recover` will return `nil` and have no effect.
- hint_type: structure
text: You must place a `defer` statement at the beginning of the `Run` function. The function passed to `defer` is where you should call `recover()`.
- hint_type: solution
text: "The core logic of the deferred function should look like this: `if r := recover(); r != nil { recoveredValue = r }`"
Comment on lines +206 to +211
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Restore string hints to keep catalog schema valid

Every existing catalog entry provides hints as plain strings. The loader expects a []string; supplying maps (hint_type, text) will fail to unmarshal and break catalog loading. Please keep hints as simple strings (or extend the loader before changing the schema).

-    - hint_type: concept
-      text: The `recover()` function is ONLY useful inside a function that is executed by a `defer` statement. Outside of a deferred function, `recover` will return `nil` and have no effect.
-    - hint_type: structure
-      text: You must place a `defer` statement at the beginning of the `Run` function. The function passed to `defer` is where you should call `recover()`.
-    - hint_type: solution
-      text: "The core logic of the deferred function should look like this: `if r := recover(); r != nil { recoveredValue = r }`"
+    - The `recover()` function is ONLY useful inside a function that is executed by a `defer` statement. Outside of a deferred function, `recover` will return `nil` and have no effect.
+    - Place a `defer` statement at the beginning of `Run`; call `recover()` inside the deferred function.
+    - The deferred function can follow: `if r := recover(); r != nil { recoveredValue = r }`
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- hint_type: concept
text: The `recover()` function is ONLY useful inside a function that is executed by a `defer` statement. Outside of a deferred function, `recover` will return `nil` and have no effect.
- hint_type: structure
text: You must place a `defer` statement at the beginning of the `Run` function. The function passed to `defer` is where you should call `recover()`.
- hint_type: solution
text: "The core logic of the deferred function should look like this: `if r := recover(); r != nil { recoveredValue = r }`"
- The `recover()` function is ONLY useful inside a function that is executed by a `defer` statement. Outside of a deferred function, `recover` will return `nil` and have no effect.
- Place a `defer` statement at the beginning of `Run`; call `recover()` inside the deferred function.
- The deferred function can follow: `if r := recover(); r != nil { recoveredValue = r }`
🤖 Prompt for AI Agents
In internal/exercises/catalog.yaml around lines 206 to 211, the hints are
defined as maps with keys like hint_type/text which breaks the loader expecting
a []string; replace those three map entries with simple string items containing
the hint text (i.e., convert each map to a plain string entry using the existing
text values) so the hints remain a list of strings and the catalog loader can
unmarshal them correctly.

47 changes: 47 additions & 0 deletions internal/exercises/exercises.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"io/fs"
"os"
"path/filepath"
"strings"
"sync"

"gopkg.in/yaml.v3"
Expand Down Expand Up @@ -124,6 +125,19 @@ func Get(slug string) (Exercise, error) {
return ex, nil
}
}
// Fallback: if an embedded template or solution exists, synthesize an Exercise entry
if templateExists(slug) || SolutionExists(slug) {
fmt.Fprintf(os.Stderr,
"Warning: exercise '%s' found in templates/solutions but missing from catalog.yaml\n",
slug,
)
return Exercise{
Slug: slug,
Title: formatSlugAsTitle(slug), // e.g., "110 Recover"
TestRegex: ".*",
Hints: []string{"This exercise is missing proper catalog metadata. Check documentation."},
}, nil
Comment on lines +128 to +139
Copy link
Owner

Choose a reason for hiding this comment

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

@Omesh2004 : Why do we need support this?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The formatSlugAsTitle helper is only used in the fallback path when the slug isn’t in catalog.yaml (and not in local ./exercises) but exists in embedded templates/solutions..

Copy link
Owner

Choose a reason for hiding this comment

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

This is a nice edition, could you add this as separate PR?

}
return Exercise{}, fmt.Errorf("exercise not found: %s", slug)
}

Expand Down Expand Up @@ -187,4 +201,37 @@ func copyExerciseTemplate(slug string) error {
})
}

func formatSlugAsTitle(slug string) string {
s := strings.TrimSpace(slug)
if s == "" {
return "Exercise"
}
parts := strings.Split(s, "_")
for i, p := range parts {
if p == "" {
continue
}
// Keep purely numeric segments as-is (e.g., "110")
isDigits := true
for _, r := range p {
if r < '0' || r > '9' {
isDigits = false
break
}
}
if isDigits {
parts[i] = p
continue
}
upper := strings.ToUpper(p)
switch upper {
case "JSON", "XML", "HTTP", "CLI", "KV", "ID", "URL", "IO":
parts[i] = upper
default:
parts[i] = strings.ToUpper(p[:1]) + strings.ToLower(p[1:])
}
}
return strings.Join(parts, " ")
}
Comment on lines +204 to +235
Copy link
Owner

Choose a reason for hiding this comment

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

Could you elaborate what are we trying to achieve here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

basically this helper function is alloting titles based on the slugs in case the same arent present in catalog.yaml file

110_recover -> 110 Recover
36_json -> 36 JSON
104_http_server -> 104 HTTP Server
105_cli_todo_list -> 105 CLI Todo List are some of the examples for human readable titles


var ErrNoTemplates = errors.New("no templates found")
33 changes: 33 additions & 0 deletions internal/exercises/solutions/110_recover/recover.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package recover_exercise

// DoWork simulates a function that might panic if the input is negative.
//
// It is designed to be called by Run, which should demonstrate
// how to safely handle the panic using recover().
func DoWork(n int) {
if n < 0 {
// A panic occurs if n is negative
panic("input cannot be negative")
}
// Normal, non-panicking code path...
}

// Run calls DoWork and uses defer/recover to safely handle any panic.
// It returns the recovered panic value or nil if no panic occurred.
func Run(n int) (recoveredValue interface{}) {
// The deferred function is executed just before Run returns.
defer func() {
// Call recover() to check if a panic has occurred.
if r := recover(); r != nil {
// If recover() returns a non-nil value (a panic occurred),
// assign it to the named return variable 'recoveredValue'.
recoveredValue = r
}
}()

DoWork(n)

// 'recoveredValue' is returned. It will be the panic value if
// a panic occurred and was recovered, or nil otherwise.
return recoveredValue
}
25 changes: 25 additions & 0 deletions internal/exercises/templates/110_recover/recover.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package recover_exercise

// DoWork simulates a function that might panic if the input is negative.
func DoWork(n int) {
if n < 0 {
// A panic occurs if n is negative
panic("input cannot be negative")
}
// Normal, non-panicking code path...
}

// Run should call DoWork and safely recover from any panic
// that occurs during its execution, returning the recovered value.
// It should return nil if no panic occurred.
func Run(n int) (recoveredValue interface{}) {
// 1. Add your 'defer' statement here.
// 2. The deferred function should call recover() and assign the result
// to 'recoveredValue' if it is not nil.

// Your code here:

DoWork(n)

return recoveredValue
}
28 changes: 28 additions & 0 deletions internal/exercises/templates/110_recover/recover_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package recover_exercise

import (
"testing"
)

func TestRun_NoPanic(t *testing.T) {
t.Parallel()
result := Run(10)
if result != nil {
t.Errorf("Run(10) should not panic and should return nil. Got: %v", result)
}
}

func TestRun_WithPanic(t *testing.T) {
t.Parallel()
expectedPanicValue := "input cannot be negative"
result := Run(-5)

if result == nil {
t.Errorf("Run(-5) should panic and recover, returning the panic value. Got nil")
}

// Check if the recovered value is what we expected
if resultAsString, ok := result.(string); !ok || resultAsString != expectedPanicValue {
t.Errorf("Run(-5) recovered with unexpected value. Expected: %q, Got: %v (%T)", expectedPanicValue, result, result)
}
}
Loading