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

Optional[T]を実装 #40

Merged
merged 7 commits into from
Jun 18, 2022
Merged
Show file tree
Hide file tree
Changes from 6 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
8 changes: 8 additions & 0 deletions server/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,11 @@ module github.com/dacq-trap/dacq/server
go 1.18

require github.com/google/uuid v1.3.0

require (
github.com/DATA-DOG/go-sqlmock v1.5.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/stretchr/testify v1.7.2 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
13 changes: 13 additions & 0 deletions server/go.sum
Original file line number Diff line number Diff line change
@@ -1,2 +1,15 @@
github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60=
github.com/DATA-DOG/go-sqlmock v1.5.0/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.2 h1:4jaiDzPyXQvSd7D0EjG45355tLlV3VOECpq10pLC+8s=
github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
108 changes: 108 additions & 0 deletions server/util/optional/optional.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package optional

import (
"bytes"
"database/sql"
"database/sql/driver"
"encoding/json"
"fmt"
)

type optionalType interface {
string | float64
}

type Of[T optionalType] struct {
value T
valid bool
}

func New[T optionalType](value T, valid bool) Of[T] {
return Of[T]{
value: value,
valid: valid,
}
}

func NewWithValue[T optionalType](value T) Of[T] {
return Of[T]{
value: value,
valid: true,
}
}

func (o Of[T]) ValueOrZero() T {
if o.valid {
return o.value
}

return defaultValue[T]()
}

func (o Of[T]) HasValue() bool {
return o.valid
}

func (o *Of[T]) UnmarshalJSON(data []byte) error {
if bytes.Equal(data, []byte("null")) {
o.value = defaultValue[T]()
o.valid = false
return nil
}

if err := json.Unmarshal(data, &o.value); err != nil {
return err
}

o.valid = true

return nil
}

func (o Of[T]) MarshalJSON() ([]byte, error) {
if o.valid {
return json.Marshal(o.value)
}

return json.Marshal(nil)
}

// Scan sql.Scannerを満たすためのメソッド
func (o *Of[T]) Scan(value any) error {
switch ((any)(o.value)).(type) {
case string:
nullString := sql.NullString{}
err := nullString.Scan(value)
if err != nil {
return err
}
o.value = any(nullString.String).(T)
o.valid = nullString.Valid
case float64:
nullFloat64 := sql.NullFloat64{}
err := nullFloat64.Scan(value)
if err != nil {
return err
}
o.value = any(nullFloat64.Float64).(T)
o.valid = nullFloat64.Valid
default:
return fmt.Errorf("unsupported type for Of[T].Scan: %T", o.value)
}

return nil
}

// Value driver.Valuerを満たすためのメソッド
func (o Of[T]) Value() (driver.Value, error) {
if !o.valid {
return nil, nil
}

return o.value, nil
}

func defaultValue[T optionalType]() T {
v := new(T)
return *v
}
221 changes: 221 additions & 0 deletions server/util/optional/optional_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
package optional

import (
"encoding/json"
"github.com/DATA-DOG/go-sqlmock"
"github.com/stretchr/testify/assert"
"testing"
)

func TestOf_MarshalJSON(t *testing.T) {
type S[T optionalType] struct {
Foo Of[T] `json:"foo"`
}

stringTests := []struct {
name string
foo Of[string]
expected string
}{
{
name: "ValidString",
foo: NewWithValue("bar"),
expected: `{"foo":"bar"}`,
},
{
name: "NullString",
foo: New("", false),
expected: `{"foo":null}`,
},
}

for _, tt := range stringTests {
t.Run(tt.name, func(t *testing.T) {
s := S[string]{
Foo: tt.foo,
}
got, err := json.Marshal(s)
if assert.NoError(t, err) {
assert.Equal(t, tt.expected, string(got))
}
})
}

floatTests := []struct {
name string
foo Of[float64]
expected string
}{
{
name: "ValidFloat",
foo: NewWithValue(1.05),
expected: `{"foo":1.05}`,
},
{
name: "NullFloat",
foo: New(0.0, false),
expected: `{"foo":null}`,
},
}

for _, tt := range floatTests {
t.Run(tt.name, func(t *testing.T) {
s := S[float64]{
Foo: tt.foo,
}
got, err := json.Marshal(s)
if assert.NoError(t, err) {
assert.Equal(t, tt.expected, string(got))
}
})
}

}

func TestOf_UnmarshalJSON(t *testing.T) {
type S[T optionalType] struct {
Foo Of[T] `json:"foo"`
}

stringTests := []struct {
name string
rawJson string
expectedFoo Of[string]
}{
{
name: "ValidString",
rawJson: `{"foo":"bar"}`,
expectedFoo: NewWithValue("bar"),
},
{
name: "NullString",
rawJson: `{"foo":null}`,
expectedFoo: New("", false),
},
}

for _, tt := range stringTests {
t.Run(tt.name, func(t *testing.T) {
var s S[string]
err := json.Unmarshal([]byte(tt.rawJson), &s)
expected := S[string]{
Foo: tt.expectedFoo,
}
if assert.NoError(t, err) {
assert.Equal(t, expected, s)
}
})
}

floatTests := []struct {
name string
rawJson string
expectedFoo Of[float64]
}{
{
name: "ValidFloat",
rawJson: `{"foo":1.05}`,
expectedFoo: NewWithValue(1.05),
},
{
name: "NullFloat",
rawJson: `{"foo":null}`,
expectedFoo: New(0.0, false),
},
}

for _, tt := range floatTests {
t.Run(tt.name, func(t *testing.T) {
var s S[float64]
err := json.Unmarshal([]byte(tt.rawJson), &s)
expected := S[float64]{
Foo: tt.expectedFoo,
}
if assert.NoError(t, err) {
assert.Equal(t, expected, s)
}
})
}
}

func TestOf_Scan(t *testing.T) {
stringTests := []struct {
name string
fooInDb any
expected Of[string]
}{
{
name: "ValidString",
fooInDb: "bar",
expected: NewWithValue("bar"),
},
{
name: "NullString",
fooInDb: nil,
expected: New("", false),
},
}

for _, tt := range stringTests {
t.Run(tt.name, func(t *testing.T) {
db, mock, err := sqlmock.New()
if err != nil {
t.Fatalf("an error '%s' was not expected when opening a stub database connection", err)
}

defer db.Close()

rows := sqlmock.NewRows([]string{"foo"}).AddRow(tt.fooInDb)
mock.ExpectQuery("SELECT").WillReturnRows(rows)

rs, _ := db.Query("SELECT")
defer rs.Close()

for rs.Next() {
var foo Of[string]
rs.Scan(&foo)
assert.Equal(t, tt.expected, foo)
}
})
}

floatTests := []struct {
name string
fooInDb any
expected Of[float64]
}{
{
name: "ValidFloat",
fooInDb: 1.05,
expected: NewWithValue(1.05),
},
{
name: "NullFloat",
fooInDb: nil,
expected: New(0.0, false),
},
}

for _, tt := range floatTests {
t.Run(tt.name, func(t *testing.T) {
db, mock, err := sqlmock.New()
if err != nil {
t.Fatalf("an error '%s' was not expected when opening a stub database connection", err)
}

defer db.Close()

rows := sqlmock.NewRows([]string{"foo"}).AddRow(tt.fooInDb)
mock.ExpectQuery("SELECT").WillReturnRows(rows)

rs, _ := db.Query("SELECT")
defer rs.Close()

for rs.Next() {
var foo Of[float64]
rs.Scan(&foo)
assert.Equal(t, tt.expected, foo)
}
})
}
}