Skip to content
Draft
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: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,20 @@ req, err := httpsuite.ParseRequest[*CreateUserRequest](
)
```

### Problem type URLs

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.

```go
cfg := httpsuite.NewProblemConfig()
cfg.BaseURL = "https://api.example.com"
cfg.ErrorTypePaths["unauthorized"] = "/errors/unauthorized"
httpsuite.SetProblemConfig(cfg)

// restore the built-in default
httpsuite.SetProblemConfig(httpsuite.NewProblemConfig())
```

## Direct helpers

```go
Expand Down
28 changes: 26 additions & 2 deletions problem_config.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
package httpsuite

import "strings"
import (
"strings"
"sync"
)

var defaultProblemConfig = NewProblemConfig()
var (
defaultProblemConfigMu sync.RWMutex
defaultProblemConfig = NewProblemConfig()
)

// ProblemConfig controls how problem type URLs are generated.
type ProblemConfig struct {
Expand All @@ -24,9 +30,25 @@ func NewProblemConfig() ProblemConfig {

// DefaultProblemConfig returns a copy of the package default config.
func DefaultProblemConfig() ProblemConfig {
defaultProblemConfigMu.RLock()
defer defaultProblemConfigMu.RUnlock()
return defaultProblemConfig.Clone()
}

// SetProblemConfig replaces the package-level default config used by
// GetProblemTypeURL, DefaultProblemConfig and the helpers that resolve problem
// types. The config is copied, so later changes to cfg do not affect the
// default. A per-call ParseOptions.Problems still overrides it.
//
// Call it once at startup. To restore the built-in default, call
// SetProblemConfig(NewProblemConfig()).
func SetProblemConfig(cfg ProblemConfig) {
clone := cfg.Clone()
defaultProblemConfigMu.Lock()
defer defaultProblemConfigMu.Unlock()
defaultProblemConfig = clone
}

func mergeProblemConfig(config *ProblemConfig) ProblemConfig {
merged := DefaultProblemConfig()
if config == nil {
Expand Down Expand Up @@ -71,6 +93,8 @@ func (c ProblemConfig) TypeURL(errorType string) string {

// GetProblemTypeURL returns the default problem type URL for a known error type.
func GetProblemTypeURL(errorType string) string {
defaultProblemConfigMu.RLock()
defer defaultProblemConfigMu.RUnlock()
return defaultProblemConfig.TypeURL(errorType)
}

Expand Down
92 changes: 91 additions & 1 deletion problem_config_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,96 @@
package httpsuite

import "testing"
import (
"sync"
"testing"
)

func restoreProblemConfig(t *testing.T) {
t.Helper()
SetProblemConfig(NewProblemConfig())
t.Cleanup(func() { SetProblemConfig(NewProblemConfig()) })
}

func TestSetProblemConfig(t *testing.T) {
restoreProblemConfig(t)

cfg := NewProblemConfig()
cfg.BaseURL = "https://api.example.com/"
cfg.ErrorTypePaths["unauthorized"] = "errors/unauthorized"
SetProblemConfig(cfg)

if got, want := GetProblemTypeURL("unauthorized"), "https://api.example.com/errors/unauthorized"; got != want {
t.Fatalf("expected %q, got %q", want, got)
}
if got := DefaultProblemConfig().TypeURL("not_found_error"); got != "https://api.example.com/errors/not-found" {
t.Fatalf("unexpected default type URL %q", got)
}
if got := NewBadRequestProblem("x").Type; got != "https://api.example.com/errors/bad-request" {
t.Fatalf("helper did not use configured default, got %q", got)
}
}

func TestSetProblemConfigCopiesInput(t *testing.T) {
restoreProblemConfig(t)

cfg := NewProblemConfig()
SetProblemConfig(cfg)
cfg.ErrorTypePaths["validation_error"] = "/mutated"

if got := GetProblemTypeURL("validation_error"); got != "/errors/validation-error" {
t.Fatalf("expected default to be isolated from caller map, got %q", got)
}
}

func TestParseOptionsProblemsOverrideSetProblemConfig(t *testing.T) {
restoreProblemConfig(t)

cfg := NewProblemConfig()
cfg.BaseURL = "https://global.example.com"
SetProblemConfig(cfg)

merged := mergeProblemConfig(&ProblemConfig{BaseURL: "https://local.example.com"})
if got, want := merged.TypeURL("validation_error"), "https://local.example.com/errors/validation-error"; got != want {
t.Fatalf("expected %q, got %q", want, got)
}
merged = mergeProblemConfig(nil)
if got, want := merged.TypeURL("validation_error"), "https://global.example.com/errors/validation-error"; got != want {
t.Fatalf("expected %q, got %q", want, got)
}
}

func TestSetProblemConfigRestore(t *testing.T) {
restoreProblemConfig(t)

SetProblemConfig(ProblemConfig{BaseURL: "https://api.example.com"})
if got := GetProblemTypeURL("validation_error"); got != BlankURL {
t.Fatalf("expected %q, got %q", BlankURL, got)
}

SetProblemConfig(NewProblemConfig())
if got := GetProblemTypeURL("validation_error"); got != "/errors/validation-error" {
t.Fatalf("expected built-in default, got %q", got)
}
}

func TestSetProblemConfigConcurrent(t *testing.T) {
restoreProblemConfig(t)

var wg sync.WaitGroup
for i := 0; i < 8; i++ {
wg.Add(2)
go func() {
defer wg.Done()
SetProblemConfig(NewProblemConfig())
}()
go func() {
defer wg.Done()
_ = GetProblemTypeURL("validation_error")
_ = DefaultProblemConfig()
}()
}
wg.Wait()
}

func TestProblemConfigTypeURL(t *testing.T) {
t.Parallel()
Expand Down
Loading