diff --git a/internal/exercises/catalog.yaml b/internal/exercises/catalog.yaml index 8d0a808..0a708dd 100644 --- a/internal/exercises/catalog.yaml +++ b/internal/exercises/catalog.yaml @@ -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 }`" \ No newline at end of file diff --git a/internal/exercises/exercises.go b/internal/exercises/exercises.go index 7f588d6..6a4b44b 100644 --- a/internal/exercises/exercises.go +++ b/internal/exercises/exercises.go @@ -7,6 +7,7 @@ import ( "io/fs" "os" "path/filepath" + "strings" "sync" "gopkg.in/yaml.v3" @@ -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 + } return Exercise{}, fmt.Errorf("exercise not found: %s", slug) } @@ -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, " ") +} + var ErrNoTemplates = errors.New("no templates found") diff --git a/internal/exercises/solutions/110_recover/recover.go b/internal/exercises/solutions/110_recover/recover.go new file mode 100644 index 0000000..e6bd834 --- /dev/null +++ b/internal/exercises/solutions/110_recover/recover.go @@ -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 +} diff --git a/internal/exercises/templates/110_recover/recover.go b/internal/exercises/templates/110_recover/recover.go new file mode 100644 index 0000000..017cdb3 --- /dev/null +++ b/internal/exercises/templates/110_recover/recover.go @@ -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 +} diff --git a/internal/exercises/templates/110_recover/recover_test.go b/internal/exercises/templates/110_recover/recover_test.go new file mode 100644 index 0000000..8b5cf3e --- /dev/null +++ b/internal/exercises/templates/110_recover/recover_test.go @@ -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) + } +}