From 5c32c8efab6c2e359281813b0175c6e65fa216a1 Mon Sep 17 00:00:00 2001 From: korya <148461+korya@users.noreply.github.com> Date: Sun, 30 Aug 2026 21:08:25 -0400 Subject: [PATCH] feat(api): Generalize Must for any value Previously, Must only unwrapped assertion constructors. Make it generic so callers can also inline standard Go constructors such as http.NewRequest while retaining the same panic-on-error contract for programmer-owned input. Co-Authored-By: OpenAI Codex (GPT-5) --- CHANGELOG.md | 5 +++-- README.md | 15 +++++---------- api_test.go | 8 ++++++-- assertions.go | 10 ---------- assertions_test.go | 22 ---------------------- must.go | 11 +++++++++++ must_test.go | 36 ++++++++++++++++++++++++++++++++++++ 7 files changed, 61 insertions(+), 46 deletions(-) create mode 100644 must.go create mode 100644 must_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 5674852..a84b674 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,8 +15,9 @@ are marked **Breaking** and listed first in their section. client, structured results and all response assertion constructors. The library invokes its configured HTTP client once and leaves retries, logging and presentation to its caller. -- `ha.Must(...)` keeps assertions built from static expressions inline while - preserving the constructors' explicit error returns for runtime input. +- Generic `ha.Must(...)` keeps values built from static input inline, including + HTTP requests and assertions, while preserving explicit errors for runtime + input. ### Changed diff --git a/README.md b/README.md index 0b4e275..5a51f30 100644 --- a/README.md +++ b/README.md @@ -138,13 +138,8 @@ import ( ) func Check(url string) (*ha.Result, error) { - req, err := http.NewRequest(http.MethodGet, url, nil) - if err != nil { - return nil, err - } - return (ha.Client{}).Do( - req, + ha.Must(http.NewRequest(http.MethodGet, url, nil)), ha.AssertStatusOK(), ha.AssertHeaderEqual("Content-Type", "application/json"), ha.Must(ha.AssertJQ(`.status == "healthy"`)), @@ -152,10 +147,10 @@ func Check(url string) (*ha.Result, error) { } ``` -Constructors that parse a status expression, regular expression or jq query -return `(ha.Assertion, error)`. `ha.Must(...)` keeps static, programmer-owned -expressions inline; it panics on invalid input, so runtime values should handle -the constructor error normally. +`ha.Must(...)` unwraps any `(T, error)` result, including requests and assertion +constructors. It keeps static, programmer-owned values inline and panics on an +error, so applications should handle errors from untrusted runtime input +normally. `Client.Do` calls the configured HTTP client once and never retries. A returned error means no complete response was available, such as a transport or diff --git a/api_test.go b/api_test.go index 6eb4949..0930b34 100644 --- a/api_test.go +++ b/api_test.go @@ -22,7 +22,7 @@ func (customAssertion) Check(*ha.Response) (*ha.Failure, error) { return nil, ni var _ ha.Assertion = customAssertion{} func ExampleClient_Do() { - req, _ := http.NewRequest(http.MethodGet, "https://example.test/health", nil) + req := ha.Must(http.NewRequest(http.MethodGet, "https://example.test/health", nil)) client := ha.Client{HTTPClient: &http.Client{Transport: exampleTransport(func(req *http.Request) (*http.Response, error) { return &http.Response{ StatusCode: http.StatusNoContent, @@ -43,8 +43,12 @@ func ExampleClient_Do() { } func ExampleMust() { + req := ha.Must(http.NewRequest(http.MethodGet, "https://example.test/health", nil)) assertion := ha.Must(ha.AssertJQ(`.status == "healthy"`)) + fmt.Println(req.Method) fmt.Println(assertion.Kind()) - // Output: jq + // Output: + // GET + // jq } diff --git a/assertions.go b/assertions.go index bd4bcbf..bea5520 100644 --- a/assertions.go +++ b/assertions.go @@ -42,16 +42,6 @@ type Assertion interface { Check(res *Response) (*Failure, error) } -// Must returns assertion when err is nil and panics otherwise. It makes -// assertions built from static, programmer-controlled expressions convenient -// to declare inline. Runtime input should handle the constructor error instead. -func Must(assertion Assertion, err error) Assertion { - if err != nil { - panic(err) - } - return assertion -} - // FailureCode identifies why an assertion did not hold without prescribing how // a caller presents that fact. type FailureCode string diff --git a/assertions_test.go b/assertions_test.go index f032d46..bb5c0e3 100644 --- a/assertions_test.go +++ b/assertions_test.go @@ -10,28 +10,6 @@ import ( "testing" ) -func TestMust(t *testing.T) { - t.Parallel() - - t.Run("returns a successfully constructed assertion", func(t *testing.T) { - assertion := Must(AssertJQ(`.status == "healthy"`)) - if assertion == nil || assertion.Kind() != "jq" { - t.Errorf("Must() = %v, want jq assertion", assertion) - } - }) - - t.Run("panics with the constructor error", func(t *testing.T) { - want := errors.New("invalid assertion") - defer func() { - if got := recover(); got != want { - t.Errorf("panic = %v, want %v", got, want) - } - }() - - Must(nil, want) - }) -} - func Test_AssertStatusOK(t *testing.T) { t.Parallel() diff --git a/must.go b/must.go new file mode 100644 index 0000000..feb7a11 --- /dev/null +++ b/must.go @@ -0,0 +1,11 @@ +package httpassert + +// Must returns value when err is nil and panics otherwise. It makes values +// built from static, programmer-controlled input convenient to declare inline. +// Runtime input should handle the constructor error instead. +func Must[T any](value T, err error) T { + if err != nil { + panic(err) + } + return value +} diff --git a/must_test.go b/must_test.go new file mode 100644 index 0000000..6ea7559 --- /dev/null +++ b/must_test.go @@ -0,0 +1,36 @@ +package httpassert + +import ( + "errors" + "net/http" + "testing" +) + +func TestMust(t *testing.T) { + t.Parallel() + + t.Run("returns a successfully constructed assertion", func(t *testing.T) { + assertion := Must(AssertJQ(`.status == "healthy"`)) + if assertion == nil || assertion.Kind() != KindJQ { + t.Errorf("Must() = %v, want jq assertion", assertion) + } + }) + + t.Run("returns an HTTP request", func(t *testing.T) { + request := Must(http.NewRequest(http.MethodGet, "https://example.test/health", nil)) + if request.Method != http.MethodGet || request.URL.String() != "https://example.test/health" { + t.Errorf("Must() = %s %s, want GET https://example.test/health", request.Method, request.URL) + } + }) + + t.Run("panics with the constructor error", func(t *testing.T) { + want := errors.New("invalid value") + defer func() { + if got := recover(); got != want { + t.Errorf("panic = %v, want %v", got, want) + } + }() + + Must("", want) + }) +}