Skip to content

Commit 84bb764

Browse files
authored
feat: add SetProblemConfig for package-level problem config (#24)
Closes #15
1 parent bc30568 commit 84bb764

3 files changed

Lines changed: 131 additions & 3 deletions

File tree

‎README.md‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,20 @@ req, err := httpsuite.ParseRequest[*CreateUserRequest](
289289
)
290290
```
291291

292+
### Problem type URLs
293+
294+
Set the base URL and extra error type paths once at startup; `GetProblemTypeURL` and the problem helpers pick them up. `ParseOptions.Problems` still overrides per call.
295+
296+
```go
297+
cfg := httpsuite.NewProblemConfig()
298+
cfg.BaseURL = "https://api.example.com"
299+
cfg.ErrorTypePaths["payment_required_error"] = "/errors/payment-required"
300+
httpsuite.SetProblemConfig(cfg)
301+
302+
// restore the built-in default
303+
httpsuite.SetProblemConfig(httpsuite.NewProblemConfig())
304+
```
305+
292306
## Direct helpers
293307

294308
```go

‎problem_config.go‎

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,14 @@
11
package httpsuite
22

3-
import "strings"
3+
import (
4+
"strings"
5+
"sync"
6+
)
47

5-
var defaultProblemConfig = NewProblemConfig()
8+
var (
9+
defaultProblemConfigMu sync.RWMutex
10+
defaultProblemConfig = NewProblemConfig()
11+
)
612

713
// ProblemConfig controls how problem type URLs are generated.
814
type ProblemConfig struct {
@@ -28,9 +34,25 @@ func NewProblemConfig() ProblemConfig {
2834

2935
// DefaultProblemConfig returns a copy of the package default config.
3036
func DefaultProblemConfig() ProblemConfig {
37+
defaultProblemConfigMu.RLock()
38+
defer defaultProblemConfigMu.RUnlock()
3139
return defaultProblemConfig.Clone()
3240
}
3341

42+
// SetProblemConfig replaces the package-level default config used by
43+
// GetProblemTypeURL, DefaultProblemConfig and the helpers that resolve problem
44+
// types. The config is copied, so later changes to cfg do not affect the
45+
// default. A per-call ParseOptions.Problems still overrides it.
46+
//
47+
// Call it once at startup. To restore the built-in default, call
48+
// SetProblemConfig(NewProblemConfig()).
49+
func SetProblemConfig(cfg ProblemConfig) {
50+
clone := cfg.Clone()
51+
defaultProblemConfigMu.Lock()
52+
defer defaultProblemConfigMu.Unlock()
53+
defaultProblemConfig = clone
54+
}
55+
3456
func mergeProblemConfig(config *ProblemConfig) ProblemConfig {
3557
merged := DefaultProblemConfig()
3658
if config == nil {
@@ -75,6 +97,8 @@ func (c ProblemConfig) TypeURL(errorType string) string {
7597

7698
// GetProblemTypeURL returns the default problem type URL for a known error type.
7799
func GetProblemTypeURL(errorType string) string {
100+
defaultProblemConfigMu.RLock()
101+
defer defaultProblemConfigMu.RUnlock()
78102
return defaultProblemConfig.TypeURL(errorType)
79103
}
80104

‎problem_config_test.go‎

Lines changed: 91 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,96 @@
11
package httpsuite
22

3-
import "testing"
3+
import (
4+
"sync"
5+
"testing"
6+
)
7+
8+
func restoreProblemConfig(t *testing.T) {
9+
t.Helper()
10+
SetProblemConfig(NewProblemConfig())
11+
t.Cleanup(func() { SetProblemConfig(NewProblemConfig()) })
12+
}
13+
14+
func TestSetProblemConfig(t *testing.T) {
15+
restoreProblemConfig(t)
16+
17+
cfg := NewProblemConfig()
18+
cfg.BaseURL = "https://api.example.com/"
19+
cfg.ErrorTypePaths["unauthorized"] = "errors/unauthorized"
20+
SetProblemConfig(cfg)
21+
22+
if got, want := GetProblemTypeURL("unauthorized"), "https://api.example.com/errors/unauthorized"; got != want {
23+
t.Fatalf("expected %q, got %q", want, got)
24+
}
25+
if got := DefaultProblemConfig().TypeURL("not_found_error"); got != "https://api.example.com/errors/not-found" {
26+
t.Fatalf("unexpected default type URL %q", got)
27+
}
28+
if got := NewBadRequestProblem("x").Type; got != "https://api.example.com/errors/bad-request" {
29+
t.Fatalf("helper did not use configured default, got %q", got)
30+
}
31+
}
32+
33+
func TestSetProblemConfigCopiesInput(t *testing.T) {
34+
restoreProblemConfig(t)
35+
36+
cfg := NewProblemConfig()
37+
SetProblemConfig(cfg)
38+
cfg.ErrorTypePaths["validation_error"] = "/mutated"
39+
40+
if got := GetProblemTypeURL("validation_error"); got != "/errors/validation-error" {
41+
t.Fatalf("expected default to be isolated from caller map, got %q", got)
42+
}
43+
}
44+
45+
func TestParseOptionsProblemsOverrideSetProblemConfig(t *testing.T) {
46+
restoreProblemConfig(t)
47+
48+
cfg := NewProblemConfig()
49+
cfg.BaseURL = "https://global.example.com"
50+
SetProblemConfig(cfg)
51+
52+
merged := mergeProblemConfig(&ProblemConfig{BaseURL: "https://local.example.com"})
53+
if got, want := merged.TypeURL("validation_error"), "https://local.example.com/errors/validation-error"; got != want {
54+
t.Fatalf("expected %q, got %q", want, got)
55+
}
56+
merged = mergeProblemConfig(nil)
57+
if got, want := merged.TypeURL("validation_error"), "https://global.example.com/errors/validation-error"; got != want {
58+
t.Fatalf("expected %q, got %q", want, got)
59+
}
60+
}
61+
62+
func TestSetProblemConfigRestore(t *testing.T) {
63+
restoreProblemConfig(t)
64+
65+
SetProblemConfig(ProblemConfig{BaseURL: "https://api.example.com"})
66+
if got := GetProblemTypeURL("validation_error"); got != BlankURL {
67+
t.Fatalf("expected %q, got %q", BlankURL, got)
68+
}
69+
70+
SetProblemConfig(NewProblemConfig())
71+
if got := GetProblemTypeURL("validation_error"); got != "/errors/validation-error" {
72+
t.Fatalf("expected built-in default, got %q", got)
73+
}
74+
}
75+
76+
func TestSetProblemConfigConcurrent(t *testing.T) {
77+
restoreProblemConfig(t)
78+
79+
var wg sync.WaitGroup
80+
for i := 0; i < 8; i++ {
81+
wg.Add(2)
82+
go func() {
83+
defer wg.Done()
84+
SetProblemConfig(NewProblemConfig())
85+
}()
86+
go func() {
87+
defer wg.Done()
88+
_ = GetProblemTypeURL("validation_error")
89+
_ = DefaultProblemConfig()
90+
}()
91+
}
92+
wg.Wait()
93+
}
494

595
func TestProblemConfigTypeURL(t *testing.T) {
696
t.Parallel()

0 commit comments

Comments
 (0)