From e7ccd89cad2e09f1fe7ff590dbdc8a2d6e350fbb Mon Sep 17 00:00:00 2001 From: Joel Nordell Date: Wed, 30 Aug 2023 14:58:26 -0500 Subject: [PATCH] Add MarshalJSON and UnmarshalJSON for Result[T] --- result.go | 53 +++++++++++++++++++++++++++- result_test.go | 93 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 145 insertions(+), 1 deletion(-) diff --git a/result.go b/result.go index cba761c..a9f58da 100644 --- a/result.go +++ b/result.go @@ -1,6 +1,10 @@ package mo -import "fmt" +import ( + "encoding/json" + "errors" + "fmt" +) // Ok builds a Result when value is valid. // Play: https://go.dev/play/p/PDwADdzNoyZ @@ -162,3 +166,50 @@ func (r Result[T]) FlatMap(mapper func(value T) Result[T]) Result[T] { return Err[T](r.err) } + +// MarshalJSON encodes Result into json, following the JSON-RPC specification for results, +// with one exception: when the result is an error, the "code" field is not included. +// Reference: https://www.jsonrpc.org/specification +func (o Result[T]) MarshalJSON() ([]byte, error) { + if o.isErr { + return json.Marshal(map[string]any{ + "error": map[string]any{ + "message": o.err.Error(), + }, + }) + } + + return json.Marshal(map[string]any{ + "result": o.value, + }) +} + +// UnmarshalJSON decodes json into Result. If "error" is set, the result is an +// Err containing the error message as a generic error object. Otherwise, the +// result is an Ok containing the result. If the JSON object contains netiher +// an error nor a result, the result is an Ok containing an empty value. If the +// JSON object contains both an error and a result, the result is an Err. Finally, +// if the JSON object contains an error but is not structured correctly (no message +// field), the unmarshaling fails. +func (o *Result[T]) UnmarshalJSON(data []byte) error { + var result struct { + Result T `json:"result"` + Error struct { + Message string `json:"message"` + } `json:"error"` + } + + if err := json.Unmarshal(data, &result); err != nil { + return err + } + + if result.Error.Message != "" { + o.err = errors.New(result.Error.Message) + o.isErr = true + return nil + } + + o.value = result.Result + o.isErr = false + return nil +} diff --git a/result_test.go b/result_test.go index 4a4a1ab..0130d64 100644 --- a/result_test.go +++ b/result_test.go @@ -1,6 +1,8 @@ package mo import ( + "encoding/json" + "fmt" "testing" "github.com/stretchr/testify/assert" @@ -201,3 +203,94 @@ func TestResultFlatMap(t *testing.T) { is.Equal(Result[int]{value: 42, isErr: false, err: nil}, opt1) is.Equal(Result[int]{value: 0, isErr: true, err: assert.AnError}, opt2) } + +func TestResultMarshalJSON(t *testing.T) { + is := assert.New(t) + + result1 := Ok("foo") + result2 := Err[string](fmt.Errorf("an error")) + result3 := Ok("") + + value, err := result1.MarshalJSON() + is.NoError(err) + is.Equal(`{"result":"foo"}`, string(value)) + + value, err = result2.MarshalJSON() + is.NoError(err) + is.Equal(`{"error":{"message":"an error"}}`, string(value)) + + value, err = result3.MarshalJSON() + is.NoError(err) + is.Equal(`{"result":""}`, string(value)) + + type testStruct struct { + Field Result[string] + } + + resultInStruct := testStruct{ + Field: result1, + } + var marshalled []byte + marshalled, err = json.Marshal(resultInStruct) + is.NoError(err) + is.Equal(`{"Field":{"result":"foo"}}`, string(marshalled)) +} + +func TestResultUnmarshalJSON(t *testing.T) { + is := assert.New(t) + + result1 := Ok("foo") + result2 := Err[string](fmt.Errorf("an error")) + result3 := Ok("") + + err := result1.UnmarshalJSON([]byte(`{"result":"foo"}`)) + is.NoError(err) + is.Equal(Ok("foo"), result1) + + var res Result[string] + err = json.Unmarshal([]byte(`{"result":"foo"}`), &res) + is.NoError(err) + is.Equal(res, result1) + + err = result2.UnmarshalJSON([]byte(`{"error":{"message":"an error"}}`)) + is.NoError(err) + is.Equal(Err[string](fmt.Errorf("an error")), result2) + + err = result3.UnmarshalJSON([]byte(`{"result":""}`)) + is.NoError(err) + is.Equal(Ok(""), result3) + + type testStruct struct { + Field Result[string] + } + + unmarshal := testStruct{} + err = json.Unmarshal([]byte(`{"Field":{"result":"foo"}}`), &unmarshal) + is.NoError(err) + is.Equal(testStruct{Field: Ok("foo")}, unmarshal) + + unmarshal = testStruct{} + err = json.Unmarshal([]byte(`{"Field":{"error":{"message":"an error"}}}`), &unmarshal) + is.NoError(err) + is.Equal(testStruct{Field: Err[string](fmt.Errorf("an error"))}, unmarshal) + + unmarshal = testStruct{} + err = json.Unmarshal([]byte(`{}`), &unmarshal) + is.NoError(err) + is.Equal(testStruct{Field: Ok("")}, unmarshal) + + // Both result and error are set; unmarshal to Err + unmarshal = testStruct{} + err = json.Unmarshal([]byte(`{"Field":{"result":"foo","error":{"message":"an error"}}}`), &unmarshal) + is.NoError(err) + is.Equal(testStruct{Field: Err[string](fmt.Errorf("an error"))}, unmarshal) + + // Bad structure for error; cannot unmarshal + unmarshal = testStruct{} + err = json.Unmarshal([]byte(`{"Field":{"result":"foo","error":true}}`), &unmarshal) + is.Error(err) + + unmarshal = testStruct{} + err = json.Unmarshal([]byte(`{"Field": "}`), &unmarshal) + is.Error(err) +}