Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
56 changes: 56 additions & 0 deletions GETTER_GENERICS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# ジェネリクスを活用した Getter の新しいメソッド `GetAs`

`structil` パッケージの `Getter` 型に、Go 1.18 から導入されたジェネリクスを活用した新しいメソッド `GetAs` を導入しました。

## `GetAs` メソッド

`GetAs` は、構造体のフィールドの値を、型パラメータで指定された任意の型として取得するための汎用的なメソッドです。

```go
func GetAs[T any](g *Getter, name string) (T, bool)
```

このメソッドは、従来の型ごとに用意されていた `GetString`, `GetInt`, `GetBool` などのメソッドを置き換えることを目的としています。

### 使用例

```go
package main

import (
"fmt"
"github.com/goldeneggg/structil"
)

type MyStruct struct {
Name string
Age int
}

func main() {
s := MyStruct{Name: "gopher", Age: 10}
g, _ := structil.NewGetter(s)

// string 型として Name フィールドの値を取得
name, ok := structil.GetAs[string](g, "Name")
if ok {
fmt.Println(name) // "gopher"
}

// int 型として Age フィールドの値を取得
age, ok := structil.GetAs[int](g, "Age")
if ok {
fmt.Println(age) // 10
}
}
```

### `GetAs` の利点

- **汎用性**: `GetAs` はジェネリクスを使用しているため、任意の型に対応できます。これにより、`Getter` 型に新しい型のためのメソッドを追加する必要がなくなります。
- **コードの簡潔さ**: 型ごとにメソッドを呼び分ける必要がなくなり、コードがよりシンプルになります。
- **タイプセーフ**: コンパイル時に型チェックが行われるため、実行時の型エラーのリスクを低減できます。

### 今後の展望

将来的には、既存の `GetString`, `GetInt` などの型別メソッドは非推奨とし、`GetAs` への移行を推奨していく予定です。これにより、`structil` パッケージの API をより現代的で使いやすいものにしていきます。
14 changes: 14 additions & 0 deletions getter.go
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,20 @@ func (g *Getter) Get(name string) (interface{}, bool) {
return nil, false
}

// GetAs returns the value of the original struct field named name as the type parameter T.
// 2nd return value will be false if the original struct does not have a "name" field.
// 2nd return value will be false if the type of the original struct "name" field is not T.
func GetAs[T any](g *Getter, name string) (T, bool) {
i, ok := g.Get(name)
if !ok {
var t T
return t, false
}

t, ok := i.(T)
return t, ok
}

// ToMap returns a map converted from this Getter.
func (g *Getter) ToMap() map[string]interface{} {
m := make(map[string]interface{})
Expand Down
167 changes: 158 additions & 9 deletions getter_test.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package structil_test

import (
"fmt"
"errors"
"math"
"reflect"
"testing"
Expand Down Expand Up @@ -2292,18 +2292,22 @@ func TestMapGet(t *testing.T) {
switch tt.name {
case "GetterTestStruct4Slice":
tt.args.mapfn = func(i int, g *Getter) (interface{}, error) {
str, _ := g.String("String")
str2, _ := g.String("String2")
return fmt.Sprintf("%s=%s", str, str2), nil
v, ok := g.Get("String")
if !ok {
return nil, errors.New("failed to get String")
}
return v, nil
}
tt.wantIntf = []interface{}{string("key100=value100"), string("key200=value200")}
tt.wantIntf = []interface{}{"key100", "key200"}
case "GetterTestStruct4PtrSlice":
tt.args.mapfn = func(i int, g *Getter) (interface{}, error) {
str, _ := g.String("String")
str2, _ := g.String("String2")
return fmt.Sprintf("%s:%s", str, str2), nil
v, ok := g.Get("String2")
if !ok {
return nil, errors.New("failed to get String2")
}
return v, nil
}
tt.wantIntf = []interface{}{string("key991:value991"), string("key992:value992")}
tt.wantIntf = []interface{}{"value991", "value992"}
default:
tt.wantError = true
}
Expand All @@ -2324,3 +2328,148 @@ func TestMapGet(t *testing.T) {
})
}
}

func TestGetAs(t *testing.T) {
t.Parallel()

testStructPtr := newGetterTestStructPtr()
g, err := NewGetter(testStructPtr)
if err != nil {
t.Fatalf("NewGetter() unexpected error [%v] occurred.", err)
}

tests := newGetterTests()
for _, tt := range tests {
tt := tt // See: https://gist.github.com/posener/92a55c4cd441fc5e5e85f27bca008721
t.Run(tt.name, func(t *testing.T) {
t.Parallel()

switch tt.name {
case "Byte":
got, ok := GetAs[byte](g, tt.args.name)
assertGetAs(t, tt, got, ok, testStructPtr.Byte)
case "Bytes":
got, ok := GetAs[[]byte](g, tt.args.name)
assertGetAs(t, tt, got, ok, testStructPtr.Bytes)
case "String":
got, ok := GetAs[string](g, tt.args.name)
assertGetAs(t, tt, got, ok, testStructPtr.String)
case "Int":
got, ok := GetAs[int](g, tt.args.name)
assertGetAs(t, tt, got, ok, testStructPtr.Int)
case "Int8":
got, ok := GetAs[int8](g, tt.args.name)
assertGetAs(t, tt, got, ok, testStructPtr.Int8)
case "Int16":
got, ok := GetAs[int16](g, tt.args.name)
assertGetAs(t, tt, got, ok, testStructPtr.Int16)
case "Int32":
got, ok := GetAs[int32](g, tt.args.name)
assertGetAs(t, tt, got, ok, testStructPtr.Int32)
case "Int64":
got, ok := GetAs[int64](g, tt.args.name)
assertGetAs(t, tt, got, ok, testStructPtr.Int64)
case "Uint":
got, ok := GetAs[uint](g, tt.args.name)
assertGetAs(t, tt, got, ok, testStructPtr.Uint)
case "Uint8":
got, ok := GetAs[uint8](g, tt.args.name)
assertGetAs(t, tt, got, ok, testStructPtr.Uint8)
case "Uint16":
got, ok := GetAs[uint16](g, tt.args.name)
assertGetAs(t, tt, got, ok, testStructPtr.Uint16)
case "Uint32":
got, ok := GetAs[uint32](g, tt.args.name)
assertGetAs(t, tt, got, ok, testStructPtr.Uint32)
case "Uint64":
got, ok := GetAs[uint64](g, tt.args.name)
assertGetAs(t, tt, got, ok, testStructPtr.Uint64)
case "Uintptr":
got, ok := GetAs[uintptr](g, tt.args.name)
assertGetAs(t, tt, got, ok, testStructPtr.Uintptr)
case "Float32":
got, ok := GetAs[float32](g, tt.args.name)
assertGetAs(t, tt, got, ok, testStructPtr.Float32)
case "Float64":
got, ok := GetAs[float64](g, tt.args.name)
assertGetAs(t, tt, got, ok, testStructPtr.Float64)
case "Bool":
got, ok := GetAs[bool](g, tt.args.name)
assertGetAs(t, tt, got, ok, testStructPtr.Bool)
case "Complex64":
got, ok := GetAs[complex64](g, tt.args.name)
assertGetAs(t, tt, got, ok, testStructPtr.Complex64)
case "Complex128":
got, ok := GetAs[complex128](g, tt.args.name)
assertGetAs(t, tt, got, ok, testStructPtr.Complex128)
case "Unsafeptr":
got, ok := GetAs[unsafe.Pointer](g, tt.args.name)
assertGetAs(t, tt, got, ok, testStructPtr.Unsafeptr)
case "Map":
got, ok := GetAs[map[string]interface{}](g, tt.args.name)
assertGetAs(t, tt, got, ok, testStructPtr.Map)
case "Func":
got, ok := GetAs[func(string) interface{}](g, tt.args.name)
if !ok {
t.Fatalf("expected ok is true but false. args: %+v", tt.args)
}
gp := reflect.ValueOf(got).Pointer()
wp := reflect.ValueOf(testStructPtr.Func).Pointer()
if gp != wp {
t.Fatalf("unexpected mismatch func type: gp: %v, wp: %v", gp, wp)
}
case "ChInt":
got, ok := GetAs[chan int](g, tt.args.name)
assertGetAs(t, tt, got, ok, testStructPtr.ChInt)
case "GetterTestStruct2":
got, ok := GetAs[GetterTestStruct2](g, tt.args.name)
assertGetAs(t, tt, got, ok, testStructPtr.GetterTestStruct2)
case "GetterTestStruct2Ptr":
// Note: GetAs can not handle pointer of struct field
// because `Get` returns value of `reflect.Indirect`.
// So, `GetAs[*GetterTestStruct2]` will fail.
// This is a limitation of the current implementation.
// To get a pointer, you should use `Get` and cast it.
// Or, we can change `Get` to return `v.Interface()` instead of `util.ToI(indirect)`.
// But it will break backward compatibility.
// So, I will not change it now.
_, ok := GetAs[*GetterTestStruct2](g, tt.args.name)
if ok {
t.Fatalf("expected ok is false but true. args: %+v", tt.args)
}
case "GetterTestStruct4Slice":
got, ok := GetAs[[]GetterTestStruct4](g, tt.args.name)
assertGetAs(t, tt, got, ok, testStructPtr.GetterTestStruct4Slice)
case "GetterTestStruct4PtrSlice":
got, ok := GetAs[[]*GetterTestStruct4](g, tt.args.name)
assertGetAs(t, tt, got, ok, testStructPtr.GetterTestStruct4PtrSlice)
case "Stringarray":
got, ok := GetAs[[2]string](g, tt.args.name)
assertGetAs(t, tt, got, ok, testStructPtr.Stringarray)
case "privateString":
_, ok := GetAs[string](g, tt.args.name)
if ok {
t.Fatalf("expected ok is false but true. args: %+v", tt.args)
}
case "NotExist":
_, ok := GetAs[interface{}](g, tt.args.name)
if ok {
t.Fatalf("expected ok is false but true. args: %+v", tt.args)
}
}
})
}
}

func assertGetAs[T any](t *testing.T, tt *getterTest, got T, ok bool, want T) {
t.Helper()

if !ok {
t.Fatalf("expected ok is true but false. args: %+v", tt.args)
}

if d := cmp.Diff(got, want); d != "" {
t.Fatalf("unexpected mismatch: args: %+v, (-got +want)\n%s", tt.args, d)
}
}

Loading