Skip to content

Commit

Permalink
Merge pull request #1 from pasqal-io/yoric/doc
Browse files Browse the repository at this point in the history
[Doc] Comparing with UnmarshalJSON
  • Loading branch information
David Teller authored Dec 18, 2023
2 parents 6b641f4 + 725a5d4 commit 6151294
Show file tree
Hide file tree
Showing 8 changed files with 137 additions and 17 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/go.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v4
with:
go-version: '1.20'
go-version: '1.21'

- name: Build
run: go build -v ./...
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/golangci-lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,4 @@ jobs:
# Require: The version of golangci-lint to use.
# When `install-mode` is `binary` (default) the value can be v1.2 or v1.2.3 or `latest` to use the latest version.
# When `install-mode` is `goinstall` the value can be v1.2.3, `latest`, or the hash of a commit.
version: v1.54
version: v1.55
1 change: 0 additions & 1 deletion .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ linters:
- gosmopolitan
- govet
- mirror
- musttag
- nolintlint
- nosprintfhostport
- perfsprint
Expand Down
125 changes: 122 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ fairly sophisticated, error-prone and need to be discovered manually, as they ap
undocumented.


By opposition, Gourde:
By opposition, Godasse:

- supports a simple mechanism to provide default values or constructor for `undefined` or private fields;
- rejects messages with `undefined` field if no default value or constructor has been provided.
Expand All @@ -47,7 +47,7 @@ library doesn't.
Again, there are patterns that let a developer work around such issues, but they're more complicated,
undocumented and error-prone than they seem.

By opposition, Gourde supports a simple mechanism to provide validation.
By opposition, Godasse supports a simple mechanism to provide validation.


# Usage
Expand Down Expand Up @@ -324,7 +324,126 @@ The rules for `CanValidate` are as follows:

## Implementing Unmarshaler

(To be Documented)
With a little elbow grease, Go supports initialization and validation with
`Unmarshal`.

Let us start with our example

```go
// Additional options for fetching.
type Options struct {
// Accept responses that have been generated since `MinDateMS`.
//
// Defaults to "now minus 10s".
MinDateMS uint64 `json:minDateMS`
}


type AdvancedFetchRequest struct {
Resource string `json:"resource"`
Number uint8 `json:"number"`
Options Options `json:"options"`

// The instant at which the request was received.
date Time
}
```

Now, we can implement `UnmarshalJSON` for `Options` to setup default values:

```go
// Critically, implement it on `*Options`, not on `Options`.
func (dest *Options) UnmarshalJSON(buf []byte) error {
// Prepare our result.
result := new(Options)
// Pre-initialize fields.
result.MinDateMS = time.Now().UnixMilli() - 10000

// Perform deserialization.
err := json.Unmarshal(result)
if err != nil {
// TODO: Presumably, add some context.
return err
}
return *result
}
```

That's... not too bad. A bit repetitive and error-prone but we can live with that.

Now, what about `AdvancedFetchRequest`? Ah, well, there it's a bit more complicated
because we want the ability to detect whether a field is left undefined:

```go
func (dest *AdvancedFetchRequest) UnmarshalJSON(buf []byte) error {
// The same type as `AdvancedFetchRequest`, except every field is a pointer.
type Aux struct {
Resource *string
Number *uint8
Options *Options
}
aux := new(Aux)

// First, unmarshal to the pointerized type.
err := json.Unmarshal(aux)
if err != nil {
// TODO: Presumably, add some context.
return err
}

// Reject nil fields.
if aux.Resource == nil {
return fmt.ErrorF("In AdvancedFetchRequest, field `resource` should be specified")
}
resource := *aux.Resource

if aux.Number == nil {
return fmt.ErrorF("In AdvancedFetchRequest, field `Number` should be specified")
}
number := *aux.Number

// Or inject default values.
if aux.Options == nil {
err = json.Unmarshal("{}", aux.Options)
if err != nil {
// Wait, how could his happen?
return err
}
}
options := *aux.Options

// Inject default values for private fields.
time := time.Now()

// Reconstruct destination field.
dest.Resource = resource
dest.Number = number
dest.Options = options
dest.time = time

// Perform validation.
if dest.Number > 100 {
return fmt.Errorf("Invalid number, expected a value in [0, 100], got %d", request.number)
}

// Finally, we should be good.
return nil
}
```

Pros of this approach:

- No dependency.

Cons of this approach:

- For some reason, I couldn't find any documentation on this approach.
- Only works with JSON.
- More verbose.
- More error-prone.
- No detection of errors at startup.
- Worse error messages.
- You don't get to use a module called "godasse".

## Json schema

Expand Down
12 changes: 7 additions & 5 deletions deserialize/deserialize.go
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ func (me mapDeserializer[T]) DeserializeBytes(source []byte) (*T, error) {
dict := new(Dict)
err := json.Unmarshal(source, &dict)
if err != nil {
return nil, err
return nil, err //nolint:wrapcheck
}
return me(*dict)
}
Expand All @@ -160,7 +160,7 @@ func (me mapDeserializer[T]) DeserializeString(source string) (*T, error) {
func MakeMapDeserializer[T any](options Options) (MapDeserializer[T], error) {
tagName := options.MainTagName
if tagName == "" {
tagName = "json"
tagName = JSON
}
return makeOuterStructDeserializer[T](options.RootPath, staticOptions{
renamingTagName: tagName,
Expand All @@ -181,7 +181,7 @@ func (me kvListDeserializer[T]) DeserializeBytes(source []byte) (*T, error) {
dict := new(kvList)
err := json.Unmarshal(source, &dict)
if err != nil {
return nil, err
return nil, err //nolint:wrapcheck
}
return me(*dict)
}
Expand All @@ -193,7 +193,7 @@ func (me kvListDeserializer[T]) DeserializeString(source string) (*T, error) {
func MakeKVListDeserializer[T any](options Options) (KVListDeserializer[T], error) {
tagName := options.MainTagName
if tagName == "" {
tagName = "json"
tagName = JSON
}
innerOptions := staticOptions{
renamingTagName: tagName,
Expand Down Expand Up @@ -297,6 +297,8 @@ var canUnmarshal = reflect.TypeOf((*json.Unmarshaler)(nil)).Elem()
// The interface `error`.
var errorInterface = reflect.TypeOf((*error)(nil)).Elem()

const JSON = "json"

// Construct a statically-typed deserializer.
//
// Under the hood, this uses the reflectDeserializer.
Expand Down Expand Up @@ -373,7 +375,7 @@ func makeStructDeserializerFromReflect(path string, typ reflect.Type, options st
if err != nil {
return nil, err
}
canUnmarshalSelf := options.renamingTagName == "json" && reflect.PointerTo(typ).Implements(canUnmarshal)
canUnmarshalSelf := options.renamingTagName == JSON && reflect.PointerTo(typ).Implements(canUnmarshal)
if canInitializeSelf && canUnmarshalSelf {
slog.Warn("At %s, type %s supports both CanInitialize and Unmarshaler, defaulting to Unmarshaler")
}
Expand Down
2 changes: 1 addition & 1 deletion deserialize/deserialize_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ func twoWaysGeneric[Input any, Output any](t *testing.T, sample Input) (*Output,
t.Error(err)
return nil, err //nolint:wrapcheck
}
return deserializer.DeserializeMap(dict)
return deserializer.DeserializeMap(dict) //nolint:wrapcheck
}
func twoWays[T any](t *testing.T, sample T) (*T, error) {
return twoWaysGeneric[T, T](t, sample)
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
module github.com/pasqal-io/godasse

go 1.21.1
go 1.21
8 changes: 4 additions & 4 deletions validation/validation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,9 @@ var _ validation.CanValidate = &ExampleCanValidate{} //nolint:exhaustruct
// See the tests for deserialize for more advanced checks.
func TestValidation(t *testing.T) {
// This should pass validation.
good := ExampleCanValidate{
good := ExampleCanValidate{ //nolint:exhaustruct
Kind: "one",
} //nolint:exhaustruct
}
err := good.Validate()
if err != nil {
t.Error(err)
Expand All @@ -82,9 +82,9 @@ func TestValidation(t *testing.T) {
testutils.AssertEqual(t, good.kindIndex, 1, "Field kindIndex should have been set")

// This shouldn't.
bad := ExampleCanValidate{
bad := ExampleCanValidate{ //nolint:exhaustruct
Kind: "three",
} //nolint:exhaustruct
}
err = bad.Validate()
testutils.AssertEqual(t, err.Error(), "Invalid schema kind three", "Validation should reject")
}

0 comments on commit 6151294

Please sign in to comment.