Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Doc] Clarifications #2

Merged
merged 4 commits into from
Dec 21, 2023
Merged
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
110 changes: 94 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,19 +1,18 @@
# About

Go Deserializer for Application System Safety Engine (or Godasse) is an alternative deserialization mechanism for Go.
Go Deserializer for Acceptable Safety Side-Engine (or Godasse) is an alternative deserialization mechanism for Go.

# Why?

Go provides the core of a mechanism to deserialize data, but it has several shortcomings.

## No support for `undefined`/missing fields
## No support for missing fields

Most serialization formats (JSON, YAML, XML, etc.) differentiate between a
value that is `undefined` (i.e. literally not part of the message) and a value that is
specified as empty (e.g. `null` or `""`).
value that is not part of the message (or fields specified as `undefined`
in JavaScript) and a value that is specified as empty (e.g. `null` or `""`).


By opposition and by design, Go's `encoding/json` does not make the difference between `undefined` and a
By opposition and by design, Go's `encoding/json` does not make the difference between a missing field and a
zero value. While this is _often_ an acceptable choice, there are many cases in which
the default value specified by a protocol is not 0. For instance:

Expand All @@ -27,11 +26,10 @@ While there are patterns that let a developer work around such issues, these pat
fairly sophisticated, error-prone and need to be discovered manually, as they appear wholly
undocumented.


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.
- supports a simple mechanism to provide default values or constructor for missing or private fields;
- rejects messages with missing field if no default value or constructor has been provided.

## No support for validation

Expand Down Expand Up @@ -189,7 +187,7 @@ type AdvancedFetchRequest struct {
}
```

In this case, if `options` is undefined, it will default to `{}`
In this case, if `options` is missing, it will default to `{}`
*and* its contents are initialized using the default values
specified in `Options`.

Expand Down Expand Up @@ -322,6 +320,82 @@ The rules for `CanValidate` are as follows:

# Alternatives

## Making every field a pointer

The recommended workaround to detect missing fields is to:

1. Define every field as a pointer.
2. Write a pass that checks every `nil` field and replaces it with an adequate default value.

This means rewriting our example as follows:

```go:
Yoric marked this conversation as resolved.
Show resolved Hide resolved
type Options struct {
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
}

func deserialize(buf []byte) (*AdvancedFetchRequest, error) {
result := new(AdvancedFetchRequest)

err := json.Unmarshal(buf, result)
if err != nil {
// FIXME: Presumably add some context
return nil, err
}

// Check for missing fields.
if result.Resource == nil {
return nil, fmt.ErrorF("in AdvancedFetchRequest, field `resource` should be specified")
}
if result.Number == nil {
return nil, fmt.ErrorF("in AdvancedFetchRequest, field `number` should be specified")
}
if result.Options == nil {
result.Options = &Options {}
}

// Check for missing fields within fields.
if result.Options.MinDateMS == nil {
result.Options.MinDateMS = time.Now().UnixMilli() - 10000
}

result.date = &time.Time()

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

// Perform validation within fields.
// (in this case, nothing to do)

return result, nil
}
```

Pros of this approach:

- No dependency.

Cons of this approach:

- Very easy to miss checking one field and end up with a crash in production.
- Downstream code now needs to deals with pointers, even if pointers are not the appropriate data structure.
- More verbose.
- No detection of errors at startup.
- Less precise error messages.
- You don't get to use a module called "godasse".


## Implementing Unmarshaler

With a little elbow grease, Go supports initialization and validation with
Expand Down Expand Up @@ -360,7 +434,7 @@ func (dest *Options) UnmarshalJSON(buf []byte) error {
result.MinDateMS = time.Now().UnixMilli() - 10000

// Perform deserialization.
err := json.Unmarshal(result)
err := json.Unmarshal(buf, result)
Yoric marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
// TODO: Presumably, add some context.
return err
Yoric marked this conversation as resolved.
Show resolved Hide resolved
Expand All @@ -372,7 +446,7 @@ func (dest *Options) UnmarshalJSON(buf []byte) error {
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:
because we want the ability to detect whether a field is missing:

```go
func (dest *AdvancedFetchRequest) UnmarshalJSON(buf []byte) error {
Expand All @@ -393,12 +467,12 @@ func (dest *AdvancedFetchRequest) UnmarshalJSON(buf []byte) error {

// Reject nil fields.
if aux.Resource == nil {
return fmt.ErrorF("In AdvancedFetchRequest, field `resource` should be specified")
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")
return fmt.ErrorF("in AdvancedFetchRequest, field `Number` should be specified")
}
number := *aux.Number

Expand All @@ -423,7 +497,7 @@ func (dest *AdvancedFetchRequest) UnmarshalJSON(buf []byte) error {

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

// Finally, we should be good.
Expand All @@ -434,6 +508,10 @@ func (dest *AdvancedFetchRequest) UnmarshalJSON(buf []byte) error {
Pros of this approach:

- No dependency.
- You get to write more Go code!
- You get to spend more time debugging Go code!
- You get to spend more time debugging Go code *in production*!
- You get to write more tests!
Yoric marked this conversation as resolved.
Show resolved Hide resolved

Cons of this approach:

Expand All @@ -442,7 +520,7 @@ Cons of this approach:
- More verbose.
- More error-prone.
- No detection of errors at startup.
- Worse error messages.
- Less precise error messages.
- You don't get to use a module called "godasse".

## Json schema
Expand Down
Loading