From f733a28d579e4d510e3e8711f3c2f4d2df5c9c59 Mon Sep 17 00:00:00 2001 From: Dennis Gloss Date: Sun, 5 Jan 2025 19:44:00 +0100 Subject: [PATCH 1/5] remove depreacted functions, remove jqerr --- error.go | 48 ------------------------------ error_test.go | 19 ------------ fetch.go | 5 ---- j.go | 77 ++++++++++++++++++++++++++----------------------- j_test.go | 39 +++++-------------------- parse.go | 6 +--- respond.go | 28 +++++++++--------- respond_test.go | 16 +++++----- stringify.go | 12 ++++---- to_handler.go | 6 ++-- 10 files changed, 80 insertions(+), 176 deletions(-) diff --git a/error.go b/error.go index 88e9d4a..9a62eb1 100644 --- a/error.go +++ b/error.go @@ -1,10 +1,7 @@ package fetch import ( - "errors" - "fmt" "net/http" - "reflect" ) type Error struct { @@ -45,48 +42,3 @@ func httpErr(prefix string, err error, r *http.Response, body []byte) *Error { Body: string(body), } } - -// JQError is returned from J.Q on invalid syntax. -// It seems to be better to return this than panic. -type JQError struct { - s string -} - -func (e *JQError) Error() string { - if e == nil { - return "" - } - return fmt.Sprintf("JQError: %s", e.s) -} - -func (e *JQError) Unwrap() error { - if e == nil { - return nil - } - return errors.New(e.s) -} - -func (e *JQError) Q(pattern string) J { return e } - -func (e *JQError) String() string { - if e == nil { - return "" - } - return e.Error() -} - -func (e *JQError) Raw() any { return e } -func (e *JQError) AsObject() (map[string]any, bool) { return nil, false } -func (e *JQError) AsArray() ([]any, bool) { return nil, false } -func (e *JQError) AsNumber() (float64, bool) { return 0, false } -func (e *JQError) AsString() (string, bool) { return "", false } -func (e *JQError) AsBoolean() (bool, bool) { return false, false } -func (e *JQError) IsNil() bool { return false } - -func jqerr(format string, a ...any) *JQError { - return &JQError{s: fmt.Sprintf(format, a...)} -} - -func IsJQError(v any) bool { - return reflect.TypeOf(v) == reflectTypeFor[*JQError]() -} diff --git a/error_test.go b/error_test.go index c6590e2..fbaec37 100644 --- a/error_test.go +++ b/error_test.go @@ -26,22 +26,3 @@ func TestError_Format(t *testing.T) { t.Errorf("error failed, got: %s", err) } } - -func TestIsJQError(t *testing.T) { - f := func() any { - return jqerr("invalid") - } - - got := f() - if !IsJQError(got) { - t.Errorf("supposed to be JQError") - } - - if IsJQError(errors.New("hello")) { - t.Errorf("not supposed to be JQError") - } - - if fmt.Sprint(jqerr("hello")) != "JQError: hello" { - t.Errorf("unexpected format") - } -} diff --git a/fetch.go b/fetch.go index 3e0b706..4958e67 100644 --- a/fetch.go +++ b/fetch.go @@ -35,11 +35,6 @@ func Get[T any](url string, config ...Config) (T, error) { return Do[T](url, config...) } -// GetJ is a wrapper for Get[fetch.J] -func GetJ(url string, config ...Config) (J, error) { - return Get[J](url, config...) -} - func Post[T any](url string, body any, config ...Config) (T, error) { return requestWithBody[T](url, http.MethodPost, body, config...) } diff --git a/j.go b/j.go index 1807884..bdc9d95 100644 --- a/j.go +++ b/j.go @@ -19,28 +19,30 @@ import ( // | fetch.B | bool | boolean | // | fetch.Nil | (nil) *struct{} | null, undefined, anything not found | type J interface { - // Q parses jq-like patterns and returns according to the path value. - // E.g. - //{ - // "name": "Jason", - // "category": { - // "name":"dogs" - // } - // "tags": [{"name":"briard"}] - //} - // - // Whole json: fmt.Println(j) or fmt.Println(j.Q(".")) - // Retrieve name: j.Q(".name") - // Retrieve category name: j.Q(".category.name") - // Retrieve first tag's name: j.Q(".tags[0].name") - // If the value wasn't found, instead of nil value it will return Nil. - // if the pattern syntax is invalid, it returns JQError. + /* + Q parses jq-like patterns and returns according to the path value. + If the value wasn't found or syntax is incorrect, Q will return Nil. + Examples: + + j := fetch.Parse(`{ + "name": "Jason", + "category": { + "name":"dogs" + } + "tags": [{"name":"briard"}] + }`) + + Print json: fmt.Println(j) + Retrieve name: j.Q(".name") + Retrieve category name: j.Q(".category.name") + Retrieve first tag's name: j.Q(".tags[0].name") + */ Q(pattern string) J // String returns JSON formatted string. String() string - // Raw converts the value to its definition and returns it. + // Elem converts J to its definition and returns it. // Type-Definitions: // M -> map[string]any // A -> []any @@ -48,19 +50,19 @@ type J interface { // S -> string // B -> bool // Nil -> nil - Raw() any + Elem() any - // AsObject is a convenient type assertion if the underlying value holds a map[string]any. + // AsObject is a convenient type assertion if J is a map[string]any. AsObject() (map[string]any, bool) - // AsArray is a convenient type assertion if the underlying value holds a slice of type []any. + // AsArray is a convenient type assertion if J is a slice of type []any. AsArray() ([]any, bool) - // AsNumber is a convenient type assertion if the underlying value holds a float64. + // AsNumber is a convenient type assertion if J is a float64. AsNumber() (float64, bool) - // AsString is a convenient type assertion if the underlying value holds a string. + // AsString is a convenient type assertion if J is a string. AsString() (string, bool) - // AsBoolean is a convenient type assertion if the underlying value holds a bool. + // AsBoolean is a convenient type assertion if J is a bool. AsBoolean() (bool, bool) - // IsNil check if the underlying value is fetch.Nil + // IsNil check if J is fetch.Nil IsNil() bool } @@ -93,7 +95,7 @@ func (m M) String() string { return marshalJ(m) } -func (m M) Raw() any { +func (m M) Elem() any { return map[string]any(m) } @@ -121,12 +123,14 @@ func (a A) Q(pattern string) J { } closeBracket := strings.Index(pattern, "]") if closeBracket == -1 { - return jqerr("expected ] for array index") + // expected ] for array index + return jnil } indexStr := pattern[1:closeBracket] index, err := strconv.Atoi(indexStr) if err != nil { - return jqerr("expected a number for array index, got: '%s'", indexStr) + // expected a number for array index + return jnil } if index < 0 || index >= len(a) { // index out of range @@ -140,7 +144,8 @@ func (a A) Q(pattern string) J { } i, sep := nextSep(remaining) if i != 0 { - return jqerr("expected . or [, got: '%s'", beforeSep(remaining)) + // expected . or [ + return jnil } return parseValue(v, remaining, sep) @@ -150,7 +155,7 @@ func (a A) String() string { return marshalJ(a) } -func (a A) Raw() any { +func (a A) Elem() any { return []any(a) } @@ -197,7 +202,7 @@ func (f F) String() string { return strconv.FormatFloat(float64(f), 'f', -1, 64) } -func (f F) Raw() any { +func (f F) Elem() any { return float64(f) } @@ -225,7 +230,7 @@ func (s S) String() string { return string(s) } -func (s S) Raw() any { +func (s S) Elem() any { return string(s) } @@ -253,7 +258,7 @@ func (b B) String() string { return strconv.FormatBool(bool(b)) } -func (b B) Raw() any { +func (b B) Elem() any { return bool(b) } @@ -266,11 +271,11 @@ func (b B) IsNil() bool { return false } type nilStruct struct{} -// Nil represents any not found or null values. The pointer's value is always nil. +// Nil represents any not found or null values. It is pointer which value is always nil. // However, when returned from any method, it doesn't equal nil, because // a Go interface is not nil when it has a type. -// It exists to prevent nil pointer dereference when retrieving Raw value. -// It can be the root in J tree, because null alone is a valid JSON. +// It exists to prevent nil pointer dereference when retrieving Elem value. +// It can be the root of J tree, because null alone is a valid JSON. type Nil = *nilStruct // the single instance of Nil. @@ -284,7 +289,7 @@ func (n Nil) String() string { return "nil" } -func (n Nil) Raw() any { +func (n Nil) Elem() any { return nil } diff --git a/j_test.go b/j_test.go index 9c23469..d6daeb8 100644 --- a/j_test.go +++ b/j_test.go @@ -41,6 +41,11 @@ func TestJ_Q(t *testing.T) { {In: `[1, 2]`, P: ".[3]", E: nil}, {In: `{"tags":[{"id":12}]}`, P: ".tags.id", E: nil}, {In: `{"category":{"name":"dog"}}`, P: ".category[0]", E: nil}, + {In: `[1, 2, 3]`, P: ".[1", E: nil}, + {In: `[1, 2, 3]`, P: ".]1", E: nil}, + {In: `[1, 2, 3]`, P: ".[]", E: nil}, + {In: `[1, 2, 3]`, P: ".[hello]", E: nil}, + {In: `[1, 2, 3]`, P: ".[0]name", E: nil}, } for i, c := range cases { j, err := Unmarshal[J](c.In) @@ -54,39 +59,11 @@ func TestJ_Q(t *testing.T) { // fmt.Print() //} - got := j.Q(c.P).Raw() + got := j.Q(c.P).Elem() if got != c.E { t.Errorf("case #%d: wrong value, expected=%v, got=%v", i, c.E, got) } } - - errorCases := []testCase{ - {In: `[1, 2, 3]`, P: ".[1", E: "expected ] for array index"}, - {In: `[1, 2, 3]`, P: ".[hello]", E: "expected a number for array index, got: 'hello'"}, - {In: `[1, 2, 3]`, P: ".[0]name", E: "expected . or [, got: 'name'"}, - } - - for i, c := range errorCases { - j, err := Unmarshal[J](c.In) - if err != nil { - t.Errorf("Unmarshal error: %s", err) - continue - } - - got := j.Q(c.P) - errStr, ok := c.E.(string) - if !ok { - panic("E should be string") - } - jqErr, ok := got.(*JQError) - if !ok { - t.Errorf("error case #%d: expected error, got=%v", i, got) - continue - } - if jqErr.s != errStr { - t.Errorf("error case #%d: wrong value, expected=%v, got=%v", i, errStr, jqErr.s) - } - } } func TestJ_String(t *testing.T) { @@ -133,13 +110,13 @@ func TestJ_Nil(t *testing.T) { if !j.Q("id").IsNil() { t.Errorf("expected J.IsNil to be true") } - if j.Q(".id").Raw() != nil { + if j.Q(".id").Elem() != nil { t.Errorf("expected id to be nil") } if j.Q(".id").String() != "nil" { t.Errorf("expected id to print nil") } - if j.Q(".id").Q(".yaid").Raw() != nil { + if j.Q(".id").Q(".yaid").Elem() != nil { t.Errorf("expected id to be nil") } } diff --git a/parse.go b/parse.go index 28b10b3..6dc33de 100644 --- a/parse.go +++ b/parse.go @@ -16,16 +16,12 @@ func Parse(s string) J { return j } -// UnmarshalJ sends J.String() to Unmarshal. +// UnmarshalJ unmarshalls J into the generic value. func UnmarshalJ[T any](j J) (T, error) { if isJNil(j) { var t T return t, fmt.Errorf("cannot unmarshal nil J") } - if IsJQError(j) { - var t T - return t, fmt.Errorf("cannot unmarshal JQerror") - } return Unmarshal[T](j.String()) } diff --git a/respond.go b/respond.go index e0c0899..4336dce 100644 --- a/respond.go +++ b/respond.go @@ -12,7 +12,7 @@ const defaultRespondErrorFormat = `{"error":"%s"}` var respondErrorFormat = defaultRespondErrorFormat var isRespondErrorFormatJSON = true -// SetRespondErrorFormat is a global setter, configuring how Respond sends the errors. +// SetRespondErrorFormat is a global setter, configuring how respond sends the errors. // format argument must contain only one %s verb which would be the error message. // E.g. // fetch.SetDefaultErrorRespondFormat(`{"msg":"%s"}`) @@ -40,11 +40,10 @@ type RespondConfig struct { ErrorStatus int } -// Deprecated, rely on ToHandlerFunc. -// Respond tries to marshal the body and send HTTP response. +// respond tries to marshal the body and send HTTP response. // The HTTP response is sent even if an error occurs. // It should be used for the standard HTTP handlers. -func Respond(w http.ResponseWriter, body any, config ...RespondConfig) error { +func respond(w http.ResponseWriter, body any, config ...RespondConfig) error { var cfg RespondConfig if len(config) > 0 { cfg = config[0] @@ -69,12 +68,12 @@ func Respond(w http.ResponseWriter, body any, config ...RespondConfig) error { var err error if !isValidHTTPStatus(cfg.Status) { err := fmt.Errorf("RespondConfig.Status is invalid") - _ = respond(w, 500, fmt.Sprintf(respondErrorFormat, err), isRespondErrorFormatJSON, cfg) + _ = doRespond(w, 500, fmt.Sprintf(respondErrorFormat, err), isRespondErrorFormatJSON, cfg) return err } if !isValidHTTPStatus(cfg.ErrorStatus) { err := fmt.Errorf("RespondConfig.ErrorStatus is invalid") - _ = respond(w, 500, fmt.Sprintf(respondErrorFormat, err), isRespondErrorFormatJSON, cfg) + _ = doRespond(w, 500, fmt.Sprintf(respondErrorFormat, err), isRespondErrorFormatJSON, cfg) return err } var bodyStr string @@ -98,15 +97,15 @@ func Respond(w http.ResponseWriter, body any, config ...RespondConfig) error { bodyStr, err = Marshal(body) } if err != nil { - _ = respond(w, cfg.ErrorStatus, fmt.Sprintf(respondErrorFormat, err), isRespondErrorFormatJSON, cfg) + _ = doRespond(w, cfg.ErrorStatus, fmt.Sprintf(respondErrorFormat, err), isRespondErrorFormatJSON, cfg) return fmt.Errorf("failed to marshal response body: %s", err) } } - return respond(w, cfg.Status, bodyStr, !isString, cfg) + return doRespond(w, cfg.Status, bodyStr, !isString, cfg) } -func respond(w http.ResponseWriter, status int, bodyStr string, isJSON bool, cfg RespondConfig) error { +func doRespond(w http.ResponseWriter, status int, bodyStr string, isJSON bool, cfg RespondConfig) error { w.WriteHeader(status) for k, v := range cfg.Headers { w.Header().Set(k, v) @@ -120,21 +119,20 @@ func respond(w http.ResponseWriter, status int, bodyStr string, isJSON bool, cfg return err } -// Deprecated, rely on ToHandlerFunc. -// RespondError sends HTTP response in the error format of Respond. +// respondError sends HTTP response in the error format of respond. // It should be used when your handler experiences an error -// before marshalling and responding with fetch.Respond. -func RespondError(w http.ResponseWriter, status int, errToRespond error, config ...RespondConfig) error { +// before marshalling and responding with fetch.respond. +func respondError(w http.ResponseWriter, status int, errToRespond error, config ...RespondConfig) error { var cfg RespondConfig if len(config) > 0 { cfg = config[0] } if !isValidHTTPStatus(status) { - rerr := RespondError(w, 500, errToRespond, config...) + rerr := respondError(w, 500, errToRespond, config...) if rerr != nil { return rerr } - return fmt.Errorf("RespondError, status is invalid") + return fmt.Errorf("error status is invalid") } w.WriteHeader(status) for k, v := range cfg.Headers { diff --git a/respond_test.go b/respond_test.go index 336938b..a614e66 100644 --- a/respond_test.go +++ b/respond_test.go @@ -36,7 +36,7 @@ func (mw *mockWriter) Write(b []byte) (int, error) { func TestRespond_String(t *testing.T) { mw := newMockWriter() - err := Respond(mw, "hello") + err := respond(mw, "hello") assert(t, err, nil) assert(t, mw.status, 200) assert(t, mw.Header().Get("Content-Type"), "text/plain") @@ -48,7 +48,7 @@ func TestRespond_Struct(t *testing.T) { type TestStruct struct { Id string } - err := Respond(mw, &TestStruct{Id: "my-id"}) + err := respond(mw, &TestStruct{Id: "my-id"}) assert(t, err, nil) assert(t, mw.status, 200) assert(t, mw.Header().Get("Content-Type"), "application/json") @@ -60,7 +60,7 @@ func TestRespond_InvalidJSON(t *testing.T) { type TestStruct struct { MyChan chan string } - err := Respond(mw, &TestStruct{}) + err := respond(mw, &TestStruct{}) assertNotNil(t, err) assert(t, mw.status, 500) assert(t, mw.Header().Get("Content-Type"), "application/json") @@ -69,7 +69,7 @@ func TestRespond_InvalidJSON(t *testing.T) { func TestRespond_InvalidStatus(t *testing.T) { mw := newMockWriter() - err := Respond(mw, "hello", RespondConfig{Status: 51}) + err := respond(mw, "hello", RespondConfig{Status: 51}) assertNotNil(t, err) assert(t, mw.status, 500) assert(t, mw.Header().Get("Content-Type"), "application/json") @@ -83,7 +83,7 @@ func TestSetRespondErrorFormat(t *testing.T) { SetRespondErrorFormat("%s") mw := newMockWriter() - Respond(mw, "hello", RespondConfig{Status: 51}) + respond(mw, "hello", RespondConfig{Status: 51}) assert(t, mw.header.Get("Content-Type"), "text/plain") assert(t, mw.body, "RespondConfig.Status is invalid") } @@ -111,7 +111,7 @@ func TestSetRespondErrorFormat_InvalidFormats(t *testing.T) { func TestRespondResponseEmpty(t *testing.T) { mw := newMockWriter() - err := Respond(mw, Response[Empty]{Status: 204}) + err := respond(mw, Response[Empty]{Status: 204}) assert(t, err, nil) if mw.status != 204 || len(mw.body) > 0 { t.Errorf("wrong writer: %+v", mw) @@ -123,7 +123,7 @@ func TestRespondResponse(t *testing.T) { Name string } mw := newMockWriter() - err := Respond(mw, Response[Pet]{Status: 201, Body: Pet{Name: "Lola"}}) + err := respond(mw, Response[Pet]{Status: 201, Body: Pet{Name: "Lola"}}) assert(t, err, nil) if mw.status != 201 || mw.body != `{"name":"Lola"}` { t.Errorf("wrong writer: %+v, %s", mw, mw.body) @@ -132,7 +132,7 @@ func TestRespondResponse(t *testing.T) { func TestRespondError(t *testing.T) { mw := newMockWriter() - err := RespondError(mw, 400, fmt.Errorf("wrong")) + err := respondError(mw, 400, fmt.Errorf("wrong")) assert(t, err, nil) assert(t, mw.status, 400) assert(t, mw.Header().Get("Content-Type"), "application/json") diff --git a/stringify.go b/stringify.go index 9c8a8c0..3167c90 100644 --- a/stringify.go +++ b/stringify.go @@ -15,12 +15,12 @@ import ( // return s //} -// StringifySafe tries to fix possible errors during marshalling and then calls Marshal. -func StringifySafe(v any) (string, error) { - //todo skip unsupported types e.g. channel fields - // I can't rely on Go's encoding/json to escape unsupported fields. - return Marshal(v) -} +//// StringifySafe tries to fix possible errors during marshalling and then calls Marshal. +//func StringifySafe(v any) (string, error) { +// // skip unsupported types e.g. channel fields +// // I can't rely on Go's encoding/json to escape unsupported fields. +// return Marshal(v) +//} /* Marshal calls the patched json.Marshal function. diff --git a/to_handler.go b/to_handler.go index 415b290..e13cacb 100644 --- a/to_handler.go +++ b/to_handler.go @@ -39,7 +39,7 @@ type HandlerConfig struct { func (cfg HandlerConfig) respondError(w http.ResponseWriter, err error) { cfg.ErrorHook(err) - err = RespondError(w, 400, err) + err = respondError(w, 400, err) if err != nil { cfg.ErrorHook(err) } @@ -97,13 +97,13 @@ func ToHandlerFunc[In any, Out any](apply ApplyFunc[In, Out]) http.HandlerFunc { if erro, ok := err.(*Error); ok { status = erro.Status } - err = RespondError(w, status, err) + err = respondError(w, status, err) if err != nil { cfg.ErrorHook(err) } return } - err = Respond(w, out) + err = respond(w, out) if err != nil { cfg.ErrorHook(err) } From 01d633d807050db11369a3894d882c26ce30cae3 Mon Sep 17 00:00:00 2001 From: Dennis Gloss Date: Sun, 5 Jan 2025 19:57:16 +0100 Subject: [PATCH 2/5] with methods for request --- wrappers.go | 27 +++++++++++++++++++++++++++ wrappers_test.go | 14 ++++++++++++++ 2 files changed, 41 insertions(+) create mode 100644 wrappers_test.go diff --git a/wrappers.go b/wrappers.go index 949284e..427647e 100644 --- a/wrappers.go +++ b/wrappers.go @@ -67,6 +67,33 @@ type Request[T any] struct { Body T } +func (r Request[T]) WithPathValue(name, value string) Request[T] { + if r.PathValues == nil { + r.PathValues = map[string]string{name: value} + return r + } + r.PathValues[name] = value + return r +} + +func (r Request[T]) WithParameter(name, value string) Request[T] { + if r.Parameters == nil { + r.Parameters = map[string]string{name: value} + return r + } + r.Parameters[name] = value + return r +} + +func (r Request[T]) WithHeader(name, value string) Request[T] { + if r.Headers == nil { + r.Headers = map[string]string{name: value} + return r + } + r.Headers[name] = value + return r +} + // Empty represents an empty response or request body, skipping JSON handling. // Can be used with the wrappers Response and Request or to fit the signature of ApplyFunc. type Empty struct{} diff --git a/wrappers_test.go b/wrappers_test.go new file mode 100644 index 0000000..8850e27 --- /dev/null +++ b/wrappers_test.go @@ -0,0 +1,14 @@ +package fetch + +import "testing" + +func TestRequest_SetPathValue(t *testing.T) { + applyFunc := func(in Request[Empty]) Empty { + if in.Parameters["key"] == "value" { + t.Errorf("wrong parameters: %+v", in) + } + return Empty{} + } + + applyFunc(Request[Empty]{}.WithPathValue("key", "value")) +} From e9e5d10f37ad3aa386d7592235b7c2221dfb46d2 Mon Sep 17 00:00:00 2001 From: Dennis Gloss Date: Sun, 5 Jan 2025 20:11:45 +0100 Subject: [PATCH 3/5] comments --- README.md | 2 +- j.go | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 3b82cab..f06a286 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ ## Installing -This is a zero-dependency package. It requires Go version 1.21 or above. +This is a zero-dependency package. It requires Go version 1.21 or above. Stable version is out! ```shell go get github.com/glossd/fetch ``` diff --git a/j.go b/j.go index bdc9d95..cf1669d 100644 --- a/j.go +++ b/j.go @@ -271,7 +271,8 @@ func (b B) IsNil() bool { return false } type nilStruct struct{} -// Nil represents any not found or null values. It is pointer which value is always nil. +// Nil represents any not found or null values. It is also used for syntax errors. +// Nil is pointer which value is always nil. // However, when returned from any method, it doesn't equal nil, because // a Go interface is not nil when it has a type. // It exists to prevent nil pointer dereference when retrieving Elem value. From 37ed24565117b2e3356198c9938003bf7b1edc3c Mon Sep 17 00:00:00 2001 From: Dennis Gloss Date: Sun, 5 Jan 2025 21:54:08 +0100 Subject: [PATCH 4/5] rename functions --- README.md | 20 ++++++++++++-------- respond.go | 20 ++++++++++---------- respond_test.go | 18 +++++++++--------- to_handler.go | 4 ++-- to_handler_test.go | 4 ++-- 5 files changed, 35 insertions(+), 31 deletions(-) diff --git a/README.md b/README.md index f06a286..842d54b 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ ## Installing -This is a zero-dependency package. It requires Go version 1.21 or above. Stable version is out! +This is a zero-dependency package. It requires Go version **1.21** or above. **Stable** version is out! ```shell go get github.com/glossd/fetch ``` @@ -293,20 +293,24 @@ type Config struct { ``` ## HTTP Handlers -`fetch.ToHandlerFunc` converts `func(in) (out, error)` signature function into `http.HandlerFunc`. It does all the json and http handling for you. +`fetch.ToHandlerFunc` converts `func(in) (out, error)` signature function into `http.HandlerFunc`. It does all the json and http handling for you. The HTTP request body unmarshalls into the function argument. The return value is marshaled into the HTTP response body. ```go type Pet struct { Name string } -http.HandleFunc("/pets", fetch.ToHandlerFunc(func(in Pet) (*Pet, error) { +http.HandleFunc("/pets/update", fetch.ToHandlerFunc(func(in Pet) (*Pet, error) { if in.Name == "" { return nil, fmt.Errorf("name can't be empty") } - return &Pet{Name: in.Name}, nil + return &Pet{Name: in.Name + " 3000"}, nil })) http.ListenAndServe(":8080", nil) ``` +```shell +$ curl localhost:8080/pets/update -d '{"name":"Lola"}' +{"name":"Lola 3000"} +``` If you have empty request or response body or you want to ignore them, use `fetch.Empty`: ```go http.HandleFunc("/default-pet", fetch.ToHandlerFunc(func(_ fetch.Empty) (Pet, error) { @@ -339,16 +343,16 @@ http.HandleFunc("/pets", fetch.ToHandlerFunc(func(_ fetch.Empty) (fetch.Response return Response[*Pet]{Status: 201, Body: &Pet{Name: "Lola"}}, nil })) ``` -The error format can be customized with the `fetch.SetRespondErrorFormat` global setter. -To log http errors with your logger call `SetDefaultHandlerConfig` +The error format can be customized with the `fetch.SetHandlerErrorFormat` global setter. +To log `ToHandleFunc` errors with your logger call `SetHandlerConfig` ```go -fetch.SetDefaultHandlerConfig(fetch.HandlerConfig{ErrorHook: func(err error) { +fetch.SetHandlerConfig(fetch.HandlerConfig{ErrorHook: func(err error) { mylogger.Errorf("fetch http error: %s", err) }}) ``` To add middleware before handling request in `fetch.ToHandlerFunc` ```go -fetch.SetDefaultHandlerConfig(fetch.HandlerConfig{Middleware: func(w http.ResponseWriter, r *http.Request) bool { +fetch.SetHandlerConfig(fetch.HandlerConfig{Middleware: func(w http.ResponseWriter, r *http.Request) bool { if r.Header.Get("Authorization") == "" { w.WriteHeader(401) return true diff --git a/respond.go b/respond.go index 4336dce..19d86e6 100644 --- a/respond.go +++ b/respond.go @@ -12,13 +12,13 @@ const defaultRespondErrorFormat = `{"error":"%s"}` var respondErrorFormat = defaultRespondErrorFormat var isRespondErrorFormatJSON = true -// SetRespondErrorFormat is a global setter, configuring how respond sends the errors. +// SetHandlerErrorFormat is a global setter, configuring how respond sends the errors. // format argument must contain only one %s verb which would be the error message. // E.g. // fetch.SetDefaultErrorRespondFormat(`{"msg":"%s"}`) // fetch.SetDefaultErrorRespondFormat("%s") - just plain error text // fetch.SetDefaultErrorRespondFormat(`{"error":{"message":"%s"}}`) -func SetRespondErrorFormat(format string) { +func SetHandlerErrorFormat(format string) { spl := strings.Split(format, "%s") if len(spl) < 2 { panic("RespondErrorFormat does not have '%s'") @@ -31,7 +31,7 @@ func SetRespondErrorFormat(format string) { respondErrorFormat = format } -type RespondConfig struct { +type respondConfig struct { // HTTP response status. Defaults to 200. Status int // Additional HTTP response headers. @@ -43,8 +43,8 @@ type RespondConfig struct { // respond tries to marshal the body and send HTTP response. // The HTTP response is sent even if an error occurs. // It should be used for the standard HTTP handlers. -func respond(w http.ResponseWriter, body any, config ...RespondConfig) error { - var cfg RespondConfig +func respond(w http.ResponseWriter, body any, config ...respondConfig) error { + var cfg respondConfig if len(config) > 0 { cfg = config[0] } @@ -67,12 +67,12 @@ func respond(w http.ResponseWriter, body any, config ...RespondConfig) error { } var err error if !isValidHTTPStatus(cfg.Status) { - err := fmt.Errorf("RespondConfig.Status is invalid") + err := fmt.Errorf("respondConfig.Status is invalid") _ = doRespond(w, 500, fmt.Sprintf(respondErrorFormat, err), isRespondErrorFormatJSON, cfg) return err } if !isValidHTTPStatus(cfg.ErrorStatus) { - err := fmt.Errorf("RespondConfig.ErrorStatus is invalid") + err := fmt.Errorf("respondConfig.ErrorStatus is invalid") _ = doRespond(w, 500, fmt.Sprintf(respondErrorFormat, err), isRespondErrorFormatJSON, cfg) return err } @@ -105,7 +105,7 @@ func respond(w http.ResponseWriter, body any, config ...RespondConfig) error { return doRespond(w, cfg.Status, bodyStr, !isString, cfg) } -func doRespond(w http.ResponseWriter, status int, bodyStr string, isJSON bool, cfg RespondConfig) error { +func doRespond(w http.ResponseWriter, status int, bodyStr string, isJSON bool, cfg respondConfig) error { w.WriteHeader(status) for k, v := range cfg.Headers { w.Header().Set(k, v) @@ -122,8 +122,8 @@ func doRespond(w http.ResponseWriter, status int, bodyStr string, isJSON bool, c // respondError sends HTTP response in the error format of respond. // It should be used when your handler experiences an error // before marshalling and responding with fetch.respond. -func respondError(w http.ResponseWriter, status int, errToRespond error, config ...RespondConfig) error { - var cfg RespondConfig +func respondError(w http.ResponseWriter, status int, errToRespond error, config ...respondConfig) error { + var cfg respondConfig if len(config) > 0 { cfg = config[0] } diff --git a/respond_test.go b/respond_test.go index a614e66..3afae7e 100644 --- a/respond_test.go +++ b/respond_test.go @@ -69,7 +69,7 @@ func TestRespond_InvalidJSON(t *testing.T) { func TestRespond_InvalidStatus(t *testing.T) { mw := newMockWriter() - err := respond(mw, "hello", RespondConfig{Status: 51}) + err := respond(mw, "hello", respondConfig{Status: 51}) assertNotNil(t, err) assert(t, mw.status, 500) assert(t, mw.Header().Get("Content-Type"), "application/json") @@ -78,34 +78,34 @@ func TestRespond_InvalidStatus(t *testing.T) { func TestSetRespondErrorFormat(t *testing.T) { defer func() { - SetRespondErrorFormat(defaultRespondErrorFormat) + SetHandlerErrorFormat(defaultRespondErrorFormat) }() - SetRespondErrorFormat("%s") + SetHandlerErrorFormat("%s") mw := newMockWriter() - respond(mw, "hello", RespondConfig{Status: 51}) + respond(mw, "hello", respondConfig{Status: 51}) assert(t, mw.header.Get("Content-Type"), "text/plain") - assert(t, mw.body, "RespondConfig.Status is invalid") + assert(t, mw.body, "respondConfig.Status is invalid") } func TestSetRespondErrorFormat_InvalidFormats(t *testing.T) { t.Run("empty", func(t *testing.T) { defer func() { - SetRespondErrorFormat(defaultRespondErrorFormat) + SetHandlerErrorFormat(defaultRespondErrorFormat) if r := recover(); r != nil { assert(t, fmt.Sprintf("%s", r), "RespondErrorFormat does not have '%s'") } }() - SetRespondErrorFormat("") + SetHandlerErrorFormat("") }) t.Run(`double %s`, func(t *testing.T) { defer func() { - SetRespondErrorFormat(defaultRespondErrorFormat) + SetHandlerErrorFormat(defaultRespondErrorFormat) if r := recover(); r != nil { assert(t, fmt.Sprintf("%s", r), "RespondErrorFormat has more than one '%s'") } }() - SetRespondErrorFormat(`{"%s":"%s"}`) + SetHandlerErrorFormat(`{"%s":"%s"}`) }) } diff --git a/to_handler.go b/to_handler.go index e13cacb..5699007 100644 --- a/to_handler.go +++ b/to_handler.go @@ -18,8 +18,8 @@ var defaultHandlerConfig = HandlerConfig{ }, } -// SetDefaultHandlerConfig sets HandlerConfig globally to be applied for every ToHandlerFunc. -func SetDefaultHandlerConfig(hc HandlerConfig) { +// SetHandlerConfig sets HandlerConfig globally to be applied for every ToHandlerFunc. +func SetHandlerConfig(hc HandlerConfig) { if hc.ErrorHook == nil { hc.ErrorHook = defaultHandlerConfig.ErrorHook } diff --git a/to_handler_test.go b/to_handler_test.go index c32c755..7be32fd 100644 --- a/to_handler_test.go +++ b/to_handler_test.go @@ -169,13 +169,13 @@ func TestToHandlerFunc_Response(t *testing.T) { } func TestToHandlerFunc_Middleware(t *testing.T) { - SetDefaultHandlerConfig(HandlerConfig{ + SetHandlerConfig(HandlerConfig{ Middleware: func(w http.ResponseWriter, r *http.Request) bool { w.WriteHeader(422) return true }, }) - defer SetDefaultHandlerConfig(HandlerConfig{Middleware: func(w http.ResponseWriter, r *http.Request) bool { + defer SetHandlerConfig(HandlerConfig{Middleware: func(w http.ResponseWriter, r *http.Request) bool { return false }}) From 7cd34340a8776893a4cc2a200e0bc0195eb9e28e Mon Sep 17 00:00:00 2001 From: Dennis Gloss Date: Sun, 5 Jan 2025 21:57:22 +0100 Subject: [PATCH 5/5] comments --- respond.go | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/respond.go b/respond.go index 19d86e6..11a0fef 100644 --- a/respond.go +++ b/respond.go @@ -12,12 +12,13 @@ const defaultRespondErrorFormat = `{"error":"%s"}` var respondErrorFormat = defaultRespondErrorFormat var isRespondErrorFormatJSON = true -// SetHandlerErrorFormat is a global setter, configuring how respond sends the errors. +// SetHandlerErrorFormat is a global setter configuring how ToHandlerFunc converts errors returned from ApplyFunc. // format argument must contain only one %s verb which would be the error message. -// E.g. -// fetch.SetDefaultErrorRespondFormat(`{"msg":"%s"}`) -// fetch.SetDefaultErrorRespondFormat("%s") - just plain error text -// fetch.SetDefaultErrorRespondFormat(`{"error":{"message":"%s"}}`) +// Defaults to {"error":"%s"} +// Examples: +// fetch.SetHandlerErrorFormat(`{"msg":"%s"}`) +// fetch.SetHandlerErrorFormat("%s") - just plain error text +// fetch.SetHandlerErrorFormat(`{"error":{"message":"%s"}}`) func SetHandlerErrorFormat(format string) { spl := strings.Split(format, "%s") if len(spl) < 2 {