und

package module
v1.0.0-alpha6 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Dec 11, 2024 License: Apache-2.0 Imports: 10 Imported by: 0

README

und - option and undefined-able types, mainly for JSON fields.

Types to interoperate with applications that make full use of JSON.

Note: dependency of github.com/go-json-experiment/json will be dropped when Go 1.24 is released

json:"omitzero" will be added to Go at Go 1.24. The dependency of github.com/go-json-experiment/json is no longer necessary since you can omit both the double-option und type and the slice-option und type just using encoding/json with json:",omitzero" option.

Example

run example by go run github.com/ngicks/und/example@v1.0.0-alpha4.

You'll see zero value fields whose type is defined under this module are omitted by jsonv2(github.com/go-json-experiment/json) with omitzero json option.

Also types defined under sliceund and sliceund/elastic are omitted by encoding/json v1 if zero, with omitempty struct tag option.

package main

import (
	"encoding/json"
	"fmt"

	"github.com/ngicks/und"
	"github.com/ngicks/und/elastic"
	"github.com/ngicks/und/option"

	jsonv2 "github.com/go-json-experiment/json"
	"github.com/go-json-experiment/json/jsontext"
	"github.com/ngicks/und/sliceund"
	sliceelastic "github.com/ngicks/und/sliceund/elastic"
)

type sample1 struct {
	Foo  string
	Bar  und.Und[nested1]              `json:",omitzero"`
	Baz  elastic.Elastic[nested1]      `json:",omitzero"`
	Qux  sliceund.Und[nested1]         `json:",omitzero"`
	Quux sliceelastic.Elastic[nested1] `json:",omitzero"`
}

type nested1 struct {
	Bar  und.Und[string]            `json:",omitzero"`
	Baz  elastic.Elastic[int]       `json:",omitzero"`
	Qux  sliceund.Und[float64]      `json:",omitzero"`
	Quux sliceelastic.Elastic[bool] `json:",omitzero"`
}

type sample2 struct {
	Foo  string
	Bar  und.Und[nested2]              `json:",omitempty"`
	Baz  elastic.Elastic[nested2]      `json:",omitempty"`
	Qux  sliceund.Und[nested2]         `json:",omitempty"`
	Quux sliceelastic.Elastic[nested2] `json:",omitempty"`
}

type nested2 struct {
	Bar  und.Und[string]            `json:",omitempty"`
	Baz  elastic.Elastic[int]       `json:",omitempty"`
	Qux  sliceund.Und[float64]      `json:",omitempty"`
	Quux sliceelastic.Elastic[bool] `json:",omitempty"`
}

func main() {
	s1 := sample1{
		Foo:  "foo",
		Bar:  und.Defined(nested1{Bar: und.Defined("foo")}),
		Baz:  elastic.FromValue(nested1{Baz: elastic.FromOptions([]option.Option[int]{option.Some(5), option.None[int](), option.Some(67)})}),
		Qux:  sliceund.Defined(nested1{Qux: sliceund.Defined(float64(1.223))}),
		Quux: sliceelastic.FromValue(nested1{Quux: sliceelastic.FromOptions([]option.Option[bool]{option.None[bool](), option.Some(true), option.Some(false)})}),
	}

	var (
		bin []byte
		err error
	)
	bin, err = jsonv2.Marshal(s1, jsontext.WithIndent("    "))
	if err != nil {
		panic(err)
	}
	fmt.Printf("marshaled by v2=\n%s\n", bin)
	// see? undefined (=zero value) fields are omitted.
	/*
	   marshaled by v2=
	   {
	       "Foo": "foo",
	       "Bar": {
	           "Bar": "foo"
	       },
	       "Baz": [
	           {
	               "Baz": [
	                   5,
	                   null,
	                   67
	               ]
	           }
	       ],
	       "Qux": {
	           "Qux": 1.223
	       },
	       "Quux": [
	           {
	               "Quux": [
	                   null,
	                   true,
	                   false
	               ]
	           }
	       ]
	   }
	*/

	s2 := sample2{
		Foo:  "foo",
		Bar:  und.Defined(nested2{Bar: und.Defined("foo")}),
		Baz:  elastic.FromValue(nested2{Baz: elastic.FromOptions([]option.Option[int]{option.Some(5), option.None[int](), option.Some(67)})}),
		Qux:  sliceund.Defined(nested2{Qux: sliceund.Defined(float64(1.223))}),
		Quux: sliceelastic.FromValue(nested2{Quux: sliceelastic.FromOptions([]option.Option[bool]{option.None[bool](), option.Some(true), option.Some(false)})}),
	}

	bin, err = json.MarshalIndent(s2, "", "    ")
	if err != nil {
		panic(err)
	}
	fmt.Printf("marshaled by v1=\n%s\n", bin)
	// You see. Types defined under ./sliceund/ can be omitted by encoding/json.
	// Types defined in ./ and ./elastic cannot be omitted by it.
	/*
	   marshaled by v1=
	   	{
	   	    "Foo": "foo",
	   	    "Bar": {
	   	        "Bar": "foo",
	   	        "Baz": null
	   	    },
	   	    "Baz": [
	   	        {
	   	            "Bar": null,
	   	            "Baz": [
	   	                5,
	   	                null,
	   	                67
	   	            ]
	   	        }
	   	    ],
	   	    "Qux": {
	   	        "Bar": null,
	   	        "Baz": null,
	   	        "Qux": 1.223
	   	    },
	   	    "Quux": [
	   	        {
	   	            "Bar": null,
	   	            "Baz": null,
	   	            "Quux": [
	   	                null,
	   	                true,
	   	                false
	   	            ]
	   	        }
	   	    ]
	   	}
	*/
}

being undefined is harder to express in Go.

Normal way to process JSON in Go.

When processing JSON values in GO, normally, at least I assume, you define a type that matches schema of JSON value, and use them with encoding/json. (Of course there are numbers of third party modules that process JSON nicely, but I place them out of scope.)

I think you'll normally specify *T as field type to allow it to be empty. This treats undefined and null fields equally (unless you use non-zero value for an unmarshale target.) This works fine in many cases. However sometimes its simplicity conflicts the concept of JSON.

The difference of null | undefined sometimes does matter

As you can see in Open API spec, JSON naturally has concept of absent fields(field is not specified in required section), and also nullable field(nullable attribute is set to true)

The difference of null and undefined does matter in some common practice. For example, Elasticsearch allows users to send partial document JSON to Update part of a document. The Elasticsearch treats all of undefined(absent), null, [] equally; nonexistent field. So the partial update API skips updating of undefined fields and clears the fields if corresponding field of input JSON is null.

How do you achieve this partial update in Go? I suspect simplest and most straightforward way is using map[string]any as a staging data.

Unmarshaling T | null | undefined is easy

If a field type implements json.Unmarshaler, encoding/json calls this method while unamrashaling incoming JSON value only if there's matching field in the data, even when the value is null literal. Therefore, T | null | undefined can be easily mapped from 3 state where: UnmarshalJSON was called with non-null data | was called with null literal | was not called, respectively.

Marshaling T | null | undefined using stating map[string]any

The problem arises when marshaling the struct if the field has distinct T | null | undefined state; encoding/json does not omit struct. This is why you end up always specifying *time.Time as field type instead of time.Time, if you want to omit zero value of the time.

Most simplest way to omit zero struct, I think, is using map[string]any as staging data.

You can use github.com/jeevatkm/go-model to map any arbitrary structs into map[string]any. Then you can remove any arbitrary field from that. (You can't use the popular mapstructure to achieve this because of #334). Finally you can marshal map[string]any via json.Marshal.

This should incur unnecessary overhead to marshaling, also feels clumsier and tiring.

Solution: use []Option[T] to achieve this

As you can see in here: https://github.com/golang/go/blob/go1.22.5/src/encoding/json/encode.go#L306-L318 , omitempty works on []T and map[K]V.

With generics introduced in Go1.18, you can define a []T based type with convenient methods. The only drawback is that you can't hide internal data structure for those type. Any change to that should be considered a breaking change. But it's ok because I suspect there's not a lot of chance of changing around it.

I've defined type like type Option[T any]{valid bool; t T} to express some or none. Then I combine this with []T to express T | null | undefined so that I can limit length of slice to 1. The capacity for the slice always stays 1, allocated only once and no chance of growth afterwards, no excess memory consumption(only a single bool flag field).

As a conclusion, this module defines []Option[T] based types to handle T | null | undefined easily.

types and variants

  • Option[T]: Rust-like optional value.
    • can be Some or None.
    • is comparable if T is comparable.
    • have Equal method in case T is not comparable or comparable but needs custom equality tests(e.g. time.Time)
    • has convenient methods stolen from rust's option<T>
    • can be used in place of *T
    • is copied by assign.

Other types are based on Option[T].

  • Und[T]: undefined, null or T
  • Elastic[T]: undefined, null, T or [](T | null)
    • mainly for consuming elasticsearch JSON documents.
    • or configuration files.

There are 2 variants

  • github.com/ngicks/und: Option[Option[T]] based types.
    • omitted only if encoding through
      • github.com/go-json-experiment/json(possibly a future encoding/json/v2) with the ,omitzero options
      • jsoniter with ,omitempty and custom encoder.
  • github.com/ngicks/und/sliceund: []Option[T] based types.
    • omitted with ,omitempty.

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func UndValidate

func UndValidate[T validate.UndValidator](u Und[T]) error

Types

type SqlNull

type SqlNull[T any] struct {
	Und[T]
}

SqlNull[T] adapts Und[T] to sql.Scanner and driver.Valuer.

func (*SqlNull[T]) Scan

func (n *SqlNull[T]) Scan(src any) error

Scan implements sql.Scanner.

If T or *T implements sql.Scanner, the implementation is used. Otherwise, SqlNull[T] falls back to sql.Null[T] as sql.Scanner.

func (SqlNull[T]) Value

func (n SqlNull[T]) Value() (driver.Value, error)

Value implements driver.Valuer.

If T or *T implements driver.Valuer, the implementation is used. In this respect, T should not be a pointer type or Und[T] should not store nil value. Otherwise, SqlNull[T] falls back to sql.Null[T] as driver.Valuer.

type Und

type Und[T any] struct {
	// contains filtered or unexported fields
}

Und[T] is a type that can express a value (`T`), empty (`null`), or absent (`undefined`). Und[T] is comparable if T is comparable. And it can be copied by assign.

Und[T] implements IsZero and is omitted when is a struct field of other structs and appropriate marshalers and appropriate struct tag on the field, e.g. "github.com/go-json-experiment/json/jsontext" with omitzero option set to the field, or "github.com/json-iterator/go" with omitempty option to the field and an appropriate extension.

If you need to stick with encoding/json v1, you can use github.com/ngicks/und/sliceund, a slice based version of Und[T] whish is already skppable by v1.

func Defined

func Defined[T any](t T) Und[T]

Defined returns a defined Und[T] whose internal value is t.

func FromOption

func FromOption[T any](opt option.Option[option.Option[T]]) Und[T]

FromOptions converts opt into an Und[T]. opt is retained by the returned value.

func FromPointer

func FromPointer[T any](v *T) Und[T]

FromPointer converts *T into Und[T]. If v is nil, it returns an undefined Und. Otherwise, it returns Defined[T] whose value is the dereferenced v.

If you need to keep t as pointer, use WrapPointer instead.

func FromSqlNull

func FromSqlNull[T any](v sql.Null[T]) Und[T]

FromSqlNull converts a valid sql.Null[T] to a defined Und[T] and invalid one into a null Und[].

func Map

func Map[T, U any](u Und[T], f func(t T) U) Und[U]

Map returns a new Und value whose internal value is mapped by f.

func Null

func Null[T any]() Und[T]

Null returns a null Und[T].

func Undefined

func Undefined[T any]() Und[T]

Undefined returns an undefined Und[T].

func WrapPointer

func WrapPointer[T any](t *T) Und[*T]

WrapPointer converts *T into Und[*T]. The und value is defined if t is non nil, undefined otherwise.

If you want t to be dereferenced, use FromPointer instead.

func (Und[T]) Clone

func (u Und[T]) Clone() Und[T]

Clone clones u. This is only a copy-by-assign unless T implements Cloner[T].

func (Und[T]) CloneFunc

func (u Und[T]) CloneFunc(cloneT func(T) T) Und[T]

func (Und[T]) DoublePointer

func (u Und[T]) DoublePointer() **T

DoublePointer returns nil if u is undefined, &(*T)(nil) if null, the internal value if defined.

func (Und[T]) Equal

func (u Und[T]) Equal(other Und[T]) bool

Equal implements Equality[Und[T]]. Equal panics if T is uncomparable and does not implement Equality[T].

func (Und[T]) EqualFunc

func (u Und[T]) EqualFunc(other Und[T], cmp func(i, j T) bool) bool

EqualFunc reports whether two Und values are equal. EqualFunc checks state of both. If both state does not match, it returns false. If both are "defined" state, then checks equality of their value by cmp, then returns true if they are equal.

func (Und[T]) IsDefined

func (u Und[T]) IsDefined() bool

IsDefined returns true if u is a defined value, otherwise false.

func (Und[T]) IsNull

func (u Und[T]) IsNull() bool

IsNull returns true if u is a null value, otherwise false.

func (Und[T]) IsUndefined

func (u Und[T]) IsUndefined() bool

IsUndefined returns true if u is an undefined value, otherwise false.

func (Und[T]) IsZero

func (u Und[T]) IsZero() bool

IsZero is an alias for IsUndefined.

func (Und[T]) Iter

func (u Und[T]) Iter() iter.Seq[option.Option[T]]

Iter returns an iterator over the internal option. If u is undefined, the iterator yields nothing, otherwise the internal option.

func (Und[T]) LogValue

func (u Und[T]) LogValue() slog.Value

LogValue implements slog.LogValuer.

func (Und[T]) Map

func (u Und[T]) Map(f func(option.Option[option.Option[T]]) option.Option[option.Option[T]]) Und[T]

Map returns a new Und[T] whose internal value is u's mapped by f.

func (Und[T]) MarshalJSON

func (u Und[T]) MarshalJSON() ([]byte, error)

MarshalJSON implements json.Marshaler.

func (Und[T]) MarshalJSONV2

func (u Und[T]) MarshalJSONV2(enc *jsontext.Encoder, opts jsonv2.Options) error

MarshalJSONV2 implements jsonv2.MarshalerV2.

func (Und[T]) MarshalXML

func (o Und[T]) MarshalXML(e *xml.Encoder, start xml.StartElement) error

MarshalXML implements xml.Marshaler.

func (Und[T]) Pointer

func (u Und[T]) Pointer() *T

Pointer returns u's internal value as a pointer.

func (Und[T]) SqlNull

func (u Und[T]) SqlNull() sql.Null[T]

SqlNull converts o into sql.Null[T].

func (Und[T]) UndCheck

func (u Und[T]) UndCheck() error

func (Und[T]) UndValidate

func (u Und[T]) UndValidate() error

func (*Und[T]) UnmarshalJSON

func (u *Und[T]) UnmarshalJSON(data []byte) error

UnmarshalJSON implements json.Unmarshaler.

func (*Und[T]) UnmarshalJSONV2

func (u *Und[T]) UnmarshalJSONV2(dec *jsontext.Decoder, opts jsonv2.Options) error

UnmarshalJSONV2 implements jsonv2.UnmarshalerV2.

func (*Und[T]) UnmarshalXML

func (o *Und[T]) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error

UnmarshalXML implements xml.Unmarshaler.

func (Und[T]) Unwrap

func (u Und[T]) Unwrap() option.Option[option.Option[T]]

Unwrap returns u's internal value.

func (Und[T]) Value

func (u Und[T]) Value() T

Value returns an internal value.

Directories

Path Synopsis
package conversion defines conversion helpers for code generated by github.com/ngicks/go-codegen/codegen
package conversion defines conversion helpers for code generated by github.com/ngicks/go-codegen/codegen
internal

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL