Skip to content

Commit

Permalink
customtype: add ability to register any custom type
Browse files Browse the repository at this point in the history
  • Loading branch information
bobheadxi committed Jul 24, 2024
1 parent 465506f commit 568f186
Show file tree
Hide file tree
Showing 4 changed files with 87 additions and 32 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ This can be useful for debugging and testing, you may think of it as a more comp
- Fully handles unexported fields, types, and values (optional.)
- Strong emphasis on being used for producing valid Go code that can be copy & pasted directly into e.g. tests.
- [Extensively tested](https://github.com/hexops/valast/tree/main/testdata), over 88 tests and handling numerous edge cases (such as pointers to unaddressable literal values like `&"foo"` properly, and even [finding bugs in alternative packages'](https://github.com/shurcooL/go-goon/issues/15)).
- Provide custom AST representations for your types with `customtypes.Register(...)`.

## Alternatives comparison

Expand Down
42 changes: 42 additions & 0 deletions customtype/customtype.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package customtype

import (
"fmt"
"go/ast"
"reflect"
"sync"
)

var (
customTypesMux sync.Mutex
customTypes = make(map[reflect.Type]func(any) ast.Expr)
)

// Register registers a type that for representation in a custom manner with
// valast. If valast encounters a value or pointer to a value of this type, it
// will use the given render func to generate the appropriate AST representation.
//
// This is useful if a type's fields are private, and can only be represented
// through a constructor - see stdtypes.go for examples.
//
// This mechanism currently only works with struct types.
func Register[T any](render func(value T) ast.Expr) {
customTypesMux.Lock()
var zero T
t := reflect.TypeOf(zero)
if _, exists := customTypes[t]; exists {
panic(fmt.Sprintf("%T already registered", zero))
}
customTypes[t] = func(value any) ast.Expr { return render(value.(T)) }
customTypesMux.Unlock()
}

// Is indicates if the given reflect.Type has a custom AST representation
// generator registered.
func Is(rt reflect.Type) (func(any) ast.Expr, bool) {
customTypesMux.Lock()
defer customTypesMux.Unlock()

t, ok := customTypes[rt]
return t, ok
}
39 changes: 39 additions & 0 deletions stdtypes.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package valast

import (
"fmt"
"go/ast"
"go/token"
"time"

"github.com/hexops/valast/customtype"
)

// Register custom reprsentations of common structs from stdlib that only
// contain unexported fields.
func init() {
// For time.Time, returns the AST expression equivalent of:
//
// time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC)
customtype.Register(func(t time.Time) ast.Expr {
return &ast.CallExpr{
Fun: &ast.SelectorExpr{
X: &ast.Ident{Name: "time"},
Sel: &ast.Ident{Name: "Date"},
},
Args: []ast.Expr{
&ast.BasicLit{Kind: token.INT, Value: fmt.Sprintf("%d", t.Year())},
&ast.BasicLit{Kind: token.INT, Value: fmt.Sprintf("%d", t.Month())},
&ast.BasicLit{Kind: token.INT, Value: fmt.Sprintf("%d", t.Day())},
&ast.BasicLit{Kind: token.INT, Value: fmt.Sprintf("%d", t.Hour())},
&ast.BasicLit{Kind: token.INT, Value: fmt.Sprintf("%d", t.Minute())},
&ast.BasicLit{Kind: token.INT, Value: fmt.Sprintf("%d", t.Second())},
&ast.BasicLit{Kind: token.INT, Value: fmt.Sprintf("%d", t.Nanosecond())},
&ast.SelectorExpr{
X: &ast.Ident{Name: "time"},
Sel: &ast.Ident{Name: t.Location().String()},
},
},
}
})
}
37 changes: 5 additions & 32 deletions valast.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"strings"
"time"

"github.com/hexops/valast/customtype"
"github.com/hexops/valast/internal/bypass"
"golang.org/x/tools/go/packages"
gofumpt "mvdan.cc/gofumpt/format"
Expand Down Expand Up @@ -559,8 +560,8 @@ func computeAST(v reflect.Value, opt *Options, cycleDetector *cycleDetector, pro
OmittedUnexported: elem.OmittedUnexported,
}, nil
}
switch vv.Elem().Type() {
case reflect.TypeOf(time.Time{}):
// Wrap custom type representations in generic pointer.
if _, ok := customtype.Is(vv.Elem().Type()); ok {
return Result{
AST: pointifyASTExpr(elem.AST),
}, nil
Expand Down Expand Up @@ -608,12 +609,9 @@ func computeAST(v reflect.Value, opt *Options, cycleDetector *cycleDetector, pro
}
return basicLit(vv, token.STRING, "string", strconv.Quote(v.String()), opt.withUnqualify(), typeExprCache)
case reflect.Struct:
// special handling for common structs from stdlib
// that only contain unexported fields
switch v.Type() {
case reflect.TypeOf(time.Time{}):
if render, ok := customtype.Is(v.Type()); ok {
return Result{
AST: timeTypeASTExpr(v.Interface().(time.Time)),
AST: render(v.Interface()),
}, nil
}

Expand Down Expand Up @@ -721,31 +719,6 @@ func unexported(v reflect.Value) reflect.Value {
return bypass.UnsafeReflectValue(v)
}

// timeTypeASTExpr returns the AST expression equivalent of
//
// time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC)
func timeTypeASTExpr(t time.Time) ast.Expr {
return &ast.CallExpr{
Fun: &ast.SelectorExpr{
X: &ast.Ident{Name: "time"},
Sel: &ast.Ident{Name: "Date"},
},
Args: []ast.Expr{
&ast.BasicLit{Kind: token.INT, Value: fmt.Sprintf("%d", t.Year())},
&ast.BasicLit{Kind: token.INT, Value: fmt.Sprintf("%d", t.Month())},
&ast.BasicLit{Kind: token.INT, Value: fmt.Sprintf("%d", t.Day())},
&ast.BasicLit{Kind: token.INT, Value: fmt.Sprintf("%d", t.Hour())},
&ast.BasicLit{Kind: token.INT, Value: fmt.Sprintf("%d", t.Minute())},
&ast.BasicLit{Kind: token.INT, Value: fmt.Sprintf("%d", t.Second())},
&ast.BasicLit{Kind: token.INT, Value: fmt.Sprintf("%d", t.Nanosecond())},
&ast.SelectorExpr{
X: &ast.Ident{Name: "time"},
Sel: &ast.Ident{Name: t.Location().String()},
},
},
}
}

// pointifyASTExpr wraps an expression in a call to the `Ptr` helper function.
//
// valast.Ptr(//...)
Expand Down

0 comments on commit 568f186

Please sign in to comment.