Skip to content

Commit 17cec4b

Browse files
committed
initial commit
1 parent 2b89817 commit 17cec4b

9 files changed

Lines changed: 428 additions & 2 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
.idea

README.md

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,41 @@
1-
# nullify
2-
Return the pointer version of any input (including changing the fields of structs to pointer versions)
1+
# Nullify
2+
3+
Returns the pointer version of any input recursively including e.g. field structs (while retaining tags). This is especially
4+
useful in e.g. JSON serialization/deserialization in combination with a struct validator to check if a field was sent or not
5+
without changing the struct.
6+
7+
## Install
8+
9+
Use `github.com/Emptyless/nullify` to download the latest version.
10+
11+
12+
## Example
13+
14+
To quickly get up and running, run e.g. the following:
15+
16+
```go
17+
package main
18+
19+
import (
20+
"encoding/json"
21+
"fmt"
22+
"github.com/go-playground/validator/v10"
23+
)
24+
25+
type Person struct {
26+
Name string `json:"name" validate:"required"`
27+
}
28+
29+
func main() {
30+
input := []byte("")
31+
person := Person{}
32+
p := Nullify(person)
33+
_ = json.Unmarshal(input, p)
34+
err := validator.New().Struct(p)
35+
fmt.Println(err)
36+
// Output:
37+
// Key: 'Name' Error:Field validation for 'Name' failed on the 'required' tag
38+
}
39+
```
40+
41+
For the full example, see `/example` for an example with [go-playground/validator](https://github.com/go-playground/validator).

example/go.mod

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
module github.com/Emptyless/nullify/example
2+
3+
go 1.21.7
4+
5+
replace github.com/Emptyless/nullify => ../
6+
7+
require (
8+
github.com/Emptyless/nullify v0.0.0-00010101000000-000000000000
9+
github.com/go-playground/validator/v10 v10.19.0
10+
github.com/stretchr/testify v1.9.0
11+
)
12+
13+
require (
14+
github.com/davecgh/go-spew v1.1.1 // indirect
15+
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
16+
github.com/go-playground/locales v0.14.1 // indirect
17+
github.com/go-playground/universal-translator v0.18.1 // indirect
18+
github.com/leodido/go-urn v1.4.0 // indirect
19+
github.com/pmezard/go-difflib v1.0.0 // indirect
20+
golang.org/x/crypto v0.19.0 // indirect
21+
golang.org/x/net v0.21.0 // indirect
22+
golang.org/x/sys v0.17.0 // indirect
23+
golang.org/x/text v0.14.0 // indirect
24+
gopkg.in/yaml.v3 v3.0.1 // indirect
25+
)

example/go.sum

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
2+
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
3+
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
4+
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
5+
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
6+
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
7+
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
8+
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
9+
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
10+
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
11+
github.com/go-playground/validator/v10 v10.19.0 h1:ol+5Fu+cSq9JD7SoSqe04GMI92cbn0+wvQ3bZ8b/AU4=
12+
github.com/go-playground/validator/v10 v10.19.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
13+
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
14+
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
15+
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
16+
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
17+
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
18+
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
19+
golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo=
20+
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
21+
golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
22+
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
23+
golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
24+
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
25+
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
26+
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
27+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
28+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
29+
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
30+
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

example/validate_test.go

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
package example
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"github.com/Emptyless/nullify"
7+
"github.com/go-playground/validator/v10"
8+
"github.com/stretchr/testify/assert"
9+
"testing"
10+
)
11+
12+
type Some struct {
13+
Optional string `json:"optional" validate:"omitnil,email"`
14+
Required string `json:"required" validate:"required,uuid"`
15+
}
16+
17+
func TestNullify_JsonUnmarshal(t *testing.T) {
18+
tests := map[string]struct {
19+
Payload string
20+
Required string
21+
Optional string
22+
ErrorMessage string
23+
}{
24+
"missing all": {
25+
Payload: `{}`,
26+
ErrorMessage: "Key: 'Required' Error:Field validation for 'Required' failed on the 'required' tag",
27+
},
28+
"missing optional": {
29+
Payload: `{"required": "89ec270d-8256-4b0e-b25c-39564b10f29e"}`,
30+
Required: "89ec270d-8256-4b0e-b25c-39564b10f29e",
31+
Optional: "",
32+
},
33+
"invalid format required": {
34+
Payload: `{"required": "invalid"}`,
35+
ErrorMessage: "Key: 'Required' Error:Field validation for 'Required' failed on the 'uuid' tag",
36+
},
37+
"invalid format optional": {
38+
Payload: `{"required": "89ec270d-8256-4b0e-b25c-39564b10f29e", "optional": "notanemail"}`,
39+
ErrorMessage: "Key: 'Optional' Error:Field validation for 'Optional' failed on the 'email' tag",
40+
},
41+
"valid": {
42+
Payload: `{"required": "89ec270d-8256-4b0e-b25c-39564b10f29e", "optional": "test@example.com"}`,
43+
Required: "89ec270d-8256-4b0e-b25c-39564b10f29e",
44+
Optional: "test@example.com",
45+
},
46+
}
47+
48+
for name, testData := range tests {
49+
testData := testData
50+
t.Run(name, func(t *testing.T) {
51+
// Arrange
52+
validate := validator.New()
53+
var some Some
54+
ptrSome := nullify.Nullify(&some)
55+
if err := json.Unmarshal([]byte(testData.Payload), ptrSome); err != nil {
56+
t.Fatal(err)
57+
}
58+
if err := json.Unmarshal([]byte(testData.Payload), &some); err != nil {
59+
t.Fatal(err)
60+
}
61+
62+
// Act
63+
err := validate.Struct(ptrSome)
64+
65+
// Assert
66+
if testData.ErrorMessage == "" {
67+
assert.Nil(t, err)
68+
assert.Equal(t, testData.Required, some.Required)
69+
assert.Equal(t, testData.Optional, some.Optional)
70+
} else {
71+
assert.ErrorContains(t, err, testData.ErrorMessage)
72+
}
73+
})
74+
}
75+
}
76+
77+
type Person struct {
78+
Name string `json:"name" validate:"required"`
79+
}
80+
81+
func ExampleNullify() {
82+
input := []byte("")
83+
person := Person{}
84+
p := nullify.Nullify(person)
85+
_ = json.Unmarshal(input, p)
86+
err := validator.New().Struct(p)
87+
fmt.Println(err)
88+
// Output:
89+
// Key: 'Name' Error:Field validation for 'Name' failed on the 'required' tag
90+
}

go.mod

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
module github.com/Emptyless/nullify
2+
3+
go 1.21.7
4+
5+
require github.com/stretchr/testify v1.9.0
6+
7+
require (
8+
github.com/davecgh/go-spew v1.1.1 // indirect
9+
github.com/pmezard/go-difflib v1.0.0 // indirect
10+
gopkg.in/yaml.v3 v3.0.1 // indirect
11+
)

go.sum

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
2+
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
3+
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
4+
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
5+
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
6+
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
7+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
8+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
9+
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
10+
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

nullify.go

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
package nullify
2+
3+
import (
4+
"reflect"
5+
)
6+
7+
// Nullify returns the pointer version of any input, e.g. string becomes *string, int becomes *int
8+
// and for a struct, the pointer version of the fields is returned as well. E.g.
9+
//
10+
// type Person struct {
11+
// Name string
12+
// }
13+
//
14+
// will be returned as
15+
//
16+
// type Person struct {
17+
// Name *string
18+
// }
19+
//
20+
// with `p := Person{}`, Nullify(p) returns a pointer to Person.
21+
//
22+
// This is especially useful in e.g. validating JSON input, see example.
23+
func Nullify(obj any) any {
24+
typeOf := reflect.TypeOf(obj)
25+
if typeOf == nil {
26+
return nil // guard for nil interface{}
27+
}
28+
29+
val := ptr(typeOf)
30+
return reflect.New(val.Elem()).Interface()
31+
}
32+
33+
// ptr recursively transforms the `reflect.Type` to a pointer kind.
34+
func ptr(t reflect.Type) reflect.Type {
35+
switch t.Kind() {
36+
case reflect.Struct:
37+
structFields := make([]reflect.StructField, t.NumField())
38+
for i := range structFields {
39+
structFields[i] = t.Field(i)
40+
structFields[i].Type = ptr(structFields[i].Type)
41+
}
42+
return reflect.PointerTo(reflect.StructOf(structFields))
43+
case reflect.Array:
44+
return reflect.PointerTo(reflect.ArrayOf(t.Len(), ptr(t.Elem())))
45+
case reflect.Slice:
46+
return reflect.PointerTo(reflect.SliceOf(ptr(t.Elem())))
47+
case reflect.Map:
48+
return reflect.PointerTo(reflect.MapOf(t.Key(), ptr(t.Elem())))
49+
// primitive types, just return the pointer value
50+
case reflect.Bool, reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Float32, reflect.Float64, reflect.Complex64, reflect.Complex128, reflect.String:
51+
return reflect.PointerTo(t)
52+
// recursively follow pointer and return the non-pointer version, then call ptr on that to resolve to a 1-depth pointer
53+
case reflect.Pointer:
54+
for ok := t.Kind() == reflect.Pointer; ok; ok = t.Kind() == reflect.Pointer {
55+
t = t.Elem()
56+
}
57+
return ptr(t)
58+
default:
59+
return reflect.PointerTo(t)
60+
}
61+
}

0 commit comments

Comments
 (0)