Skip to content

ngicks/und

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

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

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

Express T | null | undefined by only using the types in struct field.

Just use und.Und (for Go 1.24 or later) or sliceund.Und (for Go 1.23 or earlier) as struct field type then place ,omitzero, ,omitempty respectively.

type sample struct {
    Foo und.Und[string]      `json:",omitzero"`
    Bar sliceund.Und[string] `json:",omitempty"`
}

The zero value is undefined, encoding/json skips fields.

s := sample{}

bin, _ := json.MarshalIndent(s, "", "    ")
fmt.Printf("zero = %s\n", bin)
/*
    zero = {}
*/

Use Null functions to create null objects.

s.Foo = und.Null[string]()
s.Bar = sliceund.Null[string]()

bin, _ = json.MarshalIndent(s, "", "    ")
fmt.Printf("null = %s\n", bin)
/*
    null = {
        "Foo": null,
        "Bar": null
    }
*/

Use Defined functions to create defined objects.

s.Foo = und.Defined("foo")
s.Bar = sliceund.Defined("bar")

bin, _ = json.MarshalIndent(s, "", "    ")
fmt.Printf("defined = %s\n", bin)
/*
    defined = {
        "Foo": "foo",
        "Bar": "bar"
    }
*/

Use Undefined functions to create undefined objects.

s.Foo = und.Undefined[string]()
s.Bar = sliceund.Undefined[string]()

bin, _ = json.MarshalIndent(s, "", "    ")
fmt.Printf("undefined = %s\n", bin)
/*
    undefined = {}
*/

types and variants

  • Option[T]: Rust-like optional value.
    • can be Some or None.
    • zero is 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)
      • T should implement type Equality[T any] interface { Equal(T) bool } interface.
    • have EqualFunc method for cases where T is not comparable and does not implement Equality.
    • has convenient methods stolen from rust's core::option::Option<T>
    • can be used in some (not all) place of *T
    • is copied by assign.

Other types are based on Option[T].

  • Und[T]: undefined (empty or unspecified), null or T (any type you like)
  • Elastic[T]: undefined (empty or unspecified), null, T or [](T | null)
    • mainly for consuming elasticsearch JSON documents.
    • or maybe useful for user hand written configuration files.

There are 2 variants

  • github.com/ngicks/und: Option[Option[T]] based types.
    • most light-weighted.
    • comparable if T is comparable.
    • omitted with ,omitzero for Go 1.24 or later version.
  • github.com/ngicks/und/sliceund: []Option[T] based types.
    • omitted with ,omitempty.
    • For Go 1.23 or earlier version.

Example

run example by

Note: this will be fixed after Go 1.24 is released.

go install golang.org/dl/go1.24rc1@latest
go1.24rc1 download
go1.24rc1 run github.com/ngicks/und/[email protected]

As you can see, types defined in ./ (package und) and ./elastic (package elastic) can be omitted with json:",omitzero" option for Go 1.24 or later version.

For Go 1.23 or earlier version of it, you can also use types under ./sliceund (package sliceund) or ./sliceund/elastic (package elastic) with json:",omitempty" option.

package main

import (
    "encoding/json"
    "fmt"

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

    "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.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.None[bool](), option.Some(true), option.Some(false))}),
    }

    var (
        bin []byte
        err error
    )
    bin, err = json.MarshalIndent(s1, "", "    ")
    if err != nil {
        panic(err)
    }
    fmt.Printf("marshaled by with omitzero =\n%s\n", bin)
    // see? undefined (=zero value) fields are omitted with json:",omitzero" option.
    // ,omitzero is introduced in Go 1.24. For earlier version Go, see example of sample2 below.
    /*
        marshaled by with omitzero =
        {
            "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.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.None[bool](), option.Some(true), option.Some(false))}),
    }

    bin, err = json.MarshalIndent(s2, "", "    ")
    if err != nil {
        panic(err)
    }
    fmt.Printf("marshaled with omitempty =\n%s\n", bin)
    // You see. Types defined under ./sliceund/ can be omitted by encoding/[email protected] or earlier.
    /*
        marshaled with omitempty =
        {
            "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
                    ]
                }
            ]
        }
    */
}

generate Patcher, Validator, Plain types with github.com/ngicks/go-codegen/codegen

github.com/ngicks/go-codegen/codegen has the undgen sub command which generates methods to, types from the types that contains any und types(option.Option[T], und.Und[T], elastic.Elastic[T], sliceund.Und[T] and sliceund/elastic.Elastic[T]).

go run github.com/ngicks/go-codegen/codegen undgen patch     -v --dir /path/to/root/dir/of/target/package --pkg ./path/to/package ...
go run github.com/ngicks/go-codegen/codegen undgen validator -v --dir /path/to/root/dir/of/target/package --pkg ./...
go run github.com/ngicks/go-codegen/codegen undgen plain     -v --dir /path/to/root/dir/of/target/package --pkg ./...
  • The patch sub-sub commands generates patcher for any struct types.
    • It takes any struct types, then generates the type whose field is same as target's but the type is wrapped in sliceund.Und and json:",omitempty" added.
    • The generated patch type can be unmarshaled from partial JSON then can be used to patch(partially overwrite fields) the target struct.
  • The validator sub-sub commands generates validator method for any types containing any of und types.
    • The method only validates und state of the und fields.
    • It validates according to und:"" struct tag.
  • The plain sub-sub commands generates plain types and interconversion methods on types.
    • It takes any types containing any of und fields, then generates plain type whose fields is same as target's but the type is unwrapped according to und:"" struct tag.

Notable flags:

  • -v : verbose logs.
  • --dir: specify directory under which the target packages are placed.
  • --pkg: same package pattern that can be passed to go list. must be prefixed with ./. patch sub command only accept pattern that matches only a single package.
  • types...: the patch sub command needs types... arguments to specify target type names. Use ... to target all types found under --pkg.

Examples below assumes example.go is placed under ./pkg/example and it contains types described.

patch command

go run github.com/ngicks/go-codegen/codegen undgen patch --dir ./pkg/example --pkg ./ ...
// example.go
type PatchExample struct {
	Foo string
	Bar *int     `json:",omitempty"`
	Baz []string `json:"baz,omitempty"`
}

This emits the type and associated methods. Output filenames are name of the file in which the target type defined but with suffix .und_patch.

// example.und_patch.go

//codegen:generated
type PatchExamplePatch struct {
	Foo sliceund.Und[string]   `json:",omitempty"`
	Bar sliceund.Und[*int]     `json:",omitempty"`
	Baz sliceund.Und[[]string] `json:"baz,omitempty"`
}

//codegen:generated
func (p *PatchExamplePatch) FromValue(v PatchExample) {
	//nolint
	*p = PatchExamplePatch{
		Foo: sliceund.Defined(v.Foo),
		Bar: sliceund.Defined(v.Bar),
		Baz: sliceund.Defined(v.Baz),
	}
}

//codegen:generated
func (p PatchExamplePatch) ToValue() PatchExample {
	//nolint
	return PatchExample{
		Foo: p.Foo.Value(),
		Bar: p.Bar.Value(),
		Baz: p.Baz.Value(),
	}
}

//codegen:generated
func (p PatchExamplePatch) Merge(r PatchExamplePatch) PatchExamplePatch {
	//nolint
	return PatchExamplePatch{
		Foo: sliceund.FromOption(r.Foo.Unwrap().Or(p.Foo.Unwrap())),
		Bar: sliceund.FromOption(r.Bar.Unwrap().Or(p.Bar.Unwrap())),
		Baz: sliceund.FromOption(r.Baz.Unwrap().Or(p.Baz.Unwrap())),
	}
}

//codegen:generated
func (p PatchExamplePatch) ApplyPatch(v PatchExample) PatchExample {
	var orgP PatchExamplePatch
	orgP.FromValue(v)
	merged := orgP.Merge(p)
	return merged.ToValue()
}

validator command

validator sub command emits generated UndValidate method which validates its und-state.

To generate validator, you must specify its required states by struct tag und:"" before executing the command.

  • def requires value to be defined, null be null, und be undefined. These 3 can be combined.
  • required and nullish are shorthand for def, null,und respectively. Exclusive to each other and other def, null, und.
  • len and values are only applicable to Elastic types.
  • len specifies required length of field. This also has same effect specifying def.
    • comparison operator is placed right after len
    • len>n, len>=n, len==n, len<n and len<=n are allowed.
    • n is integer.
    • Operators have the same meaning as in Go.
    • Assume len will be replaced with your field length. len>n is valid when field length is greater than n.
  • values currently has only values:nonnull variant.
    • nonnull variant requires all values of Elastic field to be non-null. As mentioned in above, normally Elastic field is [](T | null).

Run command by

go run github.com/ngicks/go-codegen/codegen undgen validator --dir ./pkg/example --pkg ./ ...
// example.go
type Example struct {
	Foo    string
	Bar    option.Option[string]        // no tag
	Baz    option.Option[string]        `und:"def"`
	Qux    und.Und[string]              `und:"def,und"`
	Quux   elastic.Elastic[string]      `und:"null,len==3"`
	Corge  sliceund.Und[string]         `und:"nullish"`
	Grault sliceelastic.Elastic[string] `und:"und,len>=2,values:nonnull"`
}
// example.und_validator.go

//codegen:generated
func (v Example) UndValidate() (err error) {
	{
		validator := undtag.UndOptExport{
			States: &undtag.StateValidator{
				Def: true,
			},
		}.Into()

		if !validator.ValidOpt(v.Baz) {
			err = fmt.Errorf("%s: value is %s", validator.Describe(), validate.ReportState(v.Baz))
		}
		if err != nil {
			return validate.AppendValidationErrorDot(
				err,
				"Baz",
			)
		}
	}
	{
		validator := undtag.UndOptExport{
			States: &undtag.StateValidator{
				Def: true,
				Und: true,
			},
		}.Into()

		if !validator.ValidUnd(v.Qux) {
			err = fmt.Errorf("%s: value is %s", validator.Describe(), validate.ReportState(v.Qux))
		}
		if err != nil {
			return validate.AppendValidationErrorDot(
				err,
				"Qux",
			)
		}
	}
	{
		validator := undtag.UndOptExport{
			States: &undtag.StateValidator{
				Def:  true,
				Null: true,
			},
			Len: &undtag.LenValidator{
				Len: 3,
				Op:  undtag.LenOpEqEq,
			},
		}.Into()

		if !validator.ValidElastic(v.Quux) {
			err = fmt.Errorf("%s: value is %s", validator.Describe(), validate.ReportState(v.Quux))
		}
		if err != nil {
			return validate.AppendValidationErrorDot(
				err,
				"Quux",
			)
		}
	}
	{
		validator := undtag.UndOptExport{
			States: &undtag.StateValidator{
				Null: true,
				Und:  true,
			},
		}.Into()

		if !validator.ValidUnd(v.Corge) {
			err = fmt.Errorf("%s: value is %s", validator.Describe(), validate.ReportState(v.Corge))
		}
		if err != nil {
			return validate.AppendValidationErrorDot(
				err,
				"Corge",
			)
		}
	}
	{
		validator := undtag.UndOptExport{
			States: &undtag.StateValidator{
				Def: true,
				Und: true,
			},
			Len: &undtag.LenValidator{
				Len: 2,
				Op:  undtag.LenOpGrEq,
			},
			Values: &undtag.ValuesValidator{
				Nonnull: true,
			},
		}.Into()

		if !validator.ValidElastic(v.Grault) {
			err = fmt.Errorf("%s: value is %s", validator.Describe(), validate.ReportState(v.Grault))
		}
		if err != nil {
			return validate.AppendValidationErrorDot(
				err,
				"Grault",
			)
		}
	}
	return
}

plain command

plain sub command emits generated Plain types where all und-kind types are converted to normal Go types, and conversion methods between Plain and Raw(the original) types.

To generate plain, you must specify its required states by struct tag und:"" before executing the command as like validator command.

The meaning of each und struct tag is explained in the validator example.

Here's conversion rule for plain.

  • def strips Und[T] or Elastic[T] into T or []option.Option[T] respectively.
  • null, und replace type with special empty type conversion.Empty.
  • def,null,und is no-op. No conversion.
  • def,null or def,und strips types to Option[T]
    • If there's und, it should add ,omitzero or ,omitempty option.
    • Otherwise it removes the option.
  • len==n option strips Elastic type into und.Und[[n]option.Option[T]]
  • len==1 is special case where it strip []T to T, (und.Und[[]option.Option[T]] -> und.Und[option.Option[T]]).
  • len>n, len>=n, len<n and len<=n assures field length at conversion time.
    • For example, the field value of Plain type converted though UndPlain has at least n+1 length if len>n is specified.
    • In case input was shorter, conversion method extends slice with zero value.
  • values:nonnull unwraps und.Und[[]option.Option[T]] into und.Und[[]T]

Run command by

go run github.com/ngicks/go-codegen/codegen undgen plain --dir ./pkg/example --pkg ./ ...
// example.go
type Example struct {
	Foo    string
	Bar    option.Option[string]        // no tag
	Baz    option.Option[string]        `und:"def"`
	Qux    und.Und[string]              `und:"def,und"`
	Quux   elastic.Elastic[string]      `und:"null,len==3"`
	Corge  sliceund.Und[string]         `und:"nullish"`
	Grault sliceelastic.Elastic[string] `und:"und,len>=2,values:nonnull"`
}
//codegen:generated
type ExamplePlain struct {
	Foo    string
	Bar    option.Option[string]                   // no tag
	Baz    string                                  `und:"def"`
	Qux    option.Option[string]                   `und:"def,und"`
	Quux   option.Option[[3]option.Option[string]] `und:"null,len==3"`
	Corge  option.Option[conversion.Empty]         `und:"nullish"`
	Grault option.Option[[]string]                 `und:"und,len>=2,values:nonnull"`
}

//codegen:generated
func (v Example) UndPlain() ExamplePlain {
	return ExamplePlain{
		Foo: v.Foo,
		Bar: v.Bar,
		Baz: v.Baz.Value(),
		Qux: v.Qux.Unwrap().Value(),
		Quux: und.Map(
			conversion.UnwrapElastic(v.Quux),
			func(o []option.Option[string]) (out [3]option.Option[string]) {
				copy(out[:], o)
				return out
			},
		).Unwrap().Value(),
		Corge:  conversion.UndNullish(v.Corge),
		Grault: conversion.NonNullSlice(conversion.LenNAtLeastSlice(2, conversion.UnwrapElasticSlice(v.Grault))).Unwrap().Value(),
	}
}

//codegen:generated
func (v ExamplePlain) UndRaw() Example {
	return Example{
		Foo: v.Foo,
		Bar: v.Bar,
		Baz: option.Some(v.Baz),
		Qux: conversion.OptionUnd(false, v.Qux),
		Quux: elastic.FromUnd(und.Map(
			conversion.OptionUnd(true, v.Quux),
			func(s [3]option.Option[string]) []option.Option[string] {
				return s[:]
			},
		)),
		Corge:  conversion.NullishUndSlice[string](v.Corge),
		Grault: sliceelastic.FromUnd(conversion.NullifySlice(conversion.OptionUndSlice(false, v.Grault))),
	}
}

other examples

see sub packages under https://github.com/ngicks/go-codegen/tree/main/codegen/generator/undgen/internal/testtargets

About

A type that can be undefined or null or T.

Resources

License

Stars

Watchers

Forks

Packages

No packages published