From b4b910d4682f7f4b2aeb0bcab678ea4696e5b23f Mon Sep 17 00:00:00 2001 From: Joonas Loppi Date: Mon, 27 Nov 2023 13:36:08 +0200 Subject: [PATCH] ezhttp: CURL equivalent command helper --- net/http/ezhttp/ezhttp.go | 39 +++++++++++++++++++++++++-------- net/http/ezhttp/helpers.go | 21 ++++++++++++++++++ net/http/ezhttp/helpers_test.go | 16 ++++++++++++++ 3 files changed, 67 insertions(+), 9 deletions(-) create mode 100644 net/http/ezhttp/helpers_test.go diff --git a/net/http/ezhttp/ezhttp.go b/net/http/ezhttp/ezhttp.go index 900c0b1..c40467c 100644 --- a/net/http/ezhttp/ezhttp.go +++ b/net/http/ezhttp/ezhttp.go @@ -1,10 +1,10 @@ // This package aims to wrap Go HTTP Client's request-response with sane defaults: // -// - You are forced to consider timeouts by having to specify Context -// - Instead of not considering non-2xx status codes as a failure, check that by default -// (unless explicitly asked to) -// - Sending and receiving JSON requires much less boilerplate, and on receiving JSON you -// are forced to think whether to "allowUnknownFields" +// - You are forced to consider timeouts by having to specify Context +// - Instead of not considering non-2xx status codes as a failure, check that by default +// (unless explicitly asked to) +// - Sending and receiving JSON requires much less boilerplate, and on receiving JSON you +// are forced to think whether to "allowUnknownFields" package ezhttp import ( @@ -59,29 +59,46 @@ func (e ResponseStatusError) StatusCode() int { return e.statusCode } +// returns *ResponseStatusError as error if non-2xx response (unless TolerateNon2xxResponse()). +// error is not *ResponseStatusError for transport-level errors, content (JSON) marshaling errors etc func Get(ctx context.Context, url string, confPieces ...ConfigPiece) (*http.Response, error) { return do(ctx, http.MethodGet, url, confPieces...) } +// returns *ResponseStatusError as error if non-2xx response (unless TolerateNon2xxResponse()). +// error is not *ResponseStatusError for transport-level errors, content (JSON) marshaling errors etc func Post(ctx context.Context, url string, confPieces ...ConfigPiece) (*http.Response, error) { return do(ctx, http.MethodPost, url, confPieces...) } +// returns *ResponseStatusError as error if non-2xx response (unless TolerateNon2xxResponse()). +// error is not *ResponseStatusError for transport-level errors, content (JSON) marshaling errors etc func Put(ctx context.Context, url string, confPieces ...ConfigPiece) (*http.Response, error) { return do(ctx, http.MethodPut, url, confPieces...) } +// returns *ResponseStatusError as error if non-2xx response (unless TolerateNon2xxResponse()). +// error is not *ResponseStatusError for transport-level errors, content (JSON) marshaling errors etc func Head(ctx context.Context, url string, confPieces ...ConfigPiece) (*http.Response, error) { return do(ctx, http.MethodHead, url, confPieces...) } +// returns *ResponseStatusError as error if non-2xx response (unless TolerateNon2xxResponse()). +// error is not *ResponseStatusError for transport-level errors, content (JSON) marshaling errors etc func Del(ctx context.Context, url string, confPieces ...ConfigPiece) (*http.Response, error) { return do(ctx, http.MethodDelete, url, confPieces...) } -// returns *ResponseStatusError as error if non-2xx response (unless TolerateNon2xxResponse()). -// error is not *ResponseStatusError for transport-level errors, content (JSON) marshaling errors etc func do(ctx context.Context, method string, url string, confPieces ...ConfigPiece) (*http.Response, error) { + conf, err := configure(ctx, method, url, confPieces...) + if err != nil { + return nil, err + } + + return doRequest(conf) +} + +func configure(ctx context.Context, method string, url string, confPieces ...ConfigPiece) (*Config, error) { conf := &Config{ Client: http.DefaultClient, } @@ -129,13 +146,17 @@ func do(ctx context.Context, method string, url string, confPieces ...ConfigPiec return nil, conf.Abort } - resp, err := conf.Client.Do(req) + return conf, nil +} + +func doRequest(conf *Config) (*http.Response, error) { + resp, err := conf.Client.Do(conf.Request) if err != nil { return resp, err // this is a transport-level error } // 304 is an error unless caller is expecting such response by sending caching headers - if resp.StatusCode == http.StatusNotModified && req.Header.Get("If-None-Match") != "" { + if resp.StatusCode == http.StatusNotModified && conf.Request.Header.Get("If-None-Match") != "" { return resp, nil } diff --git a/net/http/ezhttp/helpers.go b/net/http/ezhttp/helpers.go index f5e37e5..1aa4ec4 100644 --- a/net/http/ezhttp/helpers.go +++ b/net/http/ezhttp/helpers.go @@ -1,7 +1,9 @@ package ezhttp import ( + "context" "crypto/tls" + "fmt" "net/http" ) @@ -26,3 +28,22 @@ func ErrorIs(err error, statusCode int) bool { return false } } + +// for `method` please use `net/http` "enum" (quotes because it's not declared as such) +func CURLEquivalent(method string, url string, confPieces ...ConfigPiece) ([]string, error) { + conf, err := configure(context.Background(), http.MethodGet, url, confPieces...) + if err != nil { + return nil, err + } + + cmd := []string{"curl", "--request=" + method} + + for key, values := range conf.Request.Header { + // FIXME: doesn't take into account multiple values + cmd = append(cmd, fmt.Sprintf("--header=%s=%s", key, values[0])) + } + + cmd = append(cmd, url) + + return cmd, nil +} diff --git a/net/http/ezhttp/helpers_test.go b/net/http/ezhttp/helpers_test.go new file mode 100644 index 0000000..6d8477e --- /dev/null +++ b/net/http/ezhttp/helpers_test.go @@ -0,0 +1,16 @@ +package ezhttp + +import ( + "net/http" + "strings" + "testing" + + . "github.com/function61/gokit/builtin" + "github.com/function61/gokit/testing/assert" +) + +func TestCURLEquivalent(t *testing.T) { + curlCmd := Must(CURLEquivalent(http.MethodPost, "https://example.net/hello", Header("x-correlation-id", "123"))) + + assert.Equal(t, strings.Join(curlCmd, " "), "curl --request=POST --header=X-Correlation-Id=123 https://example.net/hello") +}