Skip to content

Commit

Permalink
ezhttp: CURL equivalent command helper
Browse files Browse the repository at this point in the history
  • Loading branch information
joonas-fi committed Nov 27, 2023
1 parent 61081ad commit b4b910d
Show file tree
Hide file tree
Showing 3 changed files with 67 additions and 9 deletions.
39 changes: 30 additions & 9 deletions net/http/ezhttp/ezhttp.go
Original file line number Diff line number Diff line change
@@ -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 (
Expand Down Expand Up @@ -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,
}
Expand Down Expand Up @@ -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
}

Expand Down
21 changes: 21 additions & 0 deletions net/http/ezhttp/helpers.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package ezhttp

import (
"context"
"crypto/tls"
"fmt"
"net/http"
)

Expand All @@ -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
}
16 changes: 16 additions & 0 deletions net/http/ezhttp/helpers_test.go
Original file line number Diff line number Diff line change
@@ -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")
}

0 comments on commit b4b910d

Please sign in to comment.