-
Notifications
You must be signed in to change notification settings - Fork 214
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #8703 from Agoric/mfig-vm-action-defaults
feat(cosmos): contextual and declarative VM action field defaults
- Loading branch information
Showing
13 changed files
with
405 additions
and
231 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,130 @@ | ||
package vm | ||
|
||
import ( | ||
"reflect" | ||
"strconv" | ||
"time" | ||
|
||
sdk "github.com/cosmos/cosmos-sdk/types" | ||
) | ||
|
||
var ( | ||
zeroTime = time.Time{} | ||
actionHeaderType = reflect.TypeOf(ActionHeader{}) | ||
_ Action = &ActionHeader{} | ||
) | ||
|
||
// Jsonable is a value, j, that can be passed through json.Marshal(j). | ||
type Jsonable interface{} | ||
|
||
type Action interface { | ||
GetActionHeader() ActionHeader | ||
} | ||
|
||
// ActionPusher enqueues data for later consumption by the controller. | ||
type ActionPusher func(ctx sdk.Context, action Action) error | ||
|
||
// ActionHeader should be embedded in all actions. It is populated by PopulateAction. | ||
type ActionHeader struct { | ||
// Type defaults to the `actionType:"..."` tag of the embedder's ActionHeader field. | ||
Type string `json:"type"` | ||
// BlockHeight defaults to sdk.Context.BlockHeight(). | ||
BlockHeight int64 `json:"blockHeight,omitempty"` | ||
// BlockTime defaults to sdk.Context.BlockTime().Unix(). | ||
BlockTime int64 `json:"blockTime,omitempty"` | ||
} | ||
|
||
func (ah ActionHeader) GetActionHeader() ActionHeader { | ||
return ah | ||
} | ||
|
||
// SetActionHeaderFromContext provides defaults to an ActionHeader. | ||
func SetActionHeaderFromContext(ctx sdk.Context, actionType string, ah *ActionHeader) { | ||
// Default the action type. | ||
if len(ah.Type) == 0 { | ||
ah.Type = actionType | ||
} | ||
|
||
// Default the block height. | ||
if ah.BlockHeight == 0 { | ||
ah.BlockHeight = ctx.BlockHeight() | ||
} | ||
|
||
if ah.BlockTime == 0 { | ||
// Only default to a non-zero block time. | ||
if blockTime := ctx.BlockTime(); blockTime != zeroTime { | ||
ah.BlockTime = blockTime.Unix() | ||
} | ||
} | ||
} | ||
|
||
// PopulateAction interprets `default:"..."` tags and specially handles an | ||
// embedded ActionHeader struct. | ||
func PopulateAction(ctx sdk.Context, action Action) Action { | ||
oldRv := reflect.Indirect(reflect.ValueOf(action)) | ||
if oldRv.Kind() != reflect.Struct { | ||
return action | ||
} | ||
|
||
// Shallow copy to a new value. | ||
rp := reflect.New(oldRv.Type()) | ||
rv := reflect.Indirect(rp) | ||
for i := 0; i < rv.NumField(); i++ { | ||
oldField := oldRv.Field(i) | ||
field := rv.Field(i) | ||
fieldType := rv.Type().Field(i) | ||
if !field.CanSet() { | ||
continue | ||
} | ||
|
||
// Copy from original. | ||
field.Set(oldField) | ||
|
||
// Populate any ActionHeader struct. | ||
var ahp *ActionHeader | ||
if fieldType.Type == actionHeaderType { | ||
ahp = field.Addr().Interface().(*ActionHeader) | ||
} else if fieldType.Type == reflect.PtrTo(actionHeaderType) { | ||
if field.IsNil() { | ||
ahp = &ActionHeader{} | ||
} else { | ||
ahp = field.Interface().(*ActionHeader) | ||
} | ||
} | ||
if ahp != nil { | ||
actionTypeTag, _ := fieldType.Tag.Lookup("actionType") | ||
ah := *ahp | ||
SetActionHeaderFromContext(ctx, actionTypeTag, &ah) | ||
if field.Kind() == reflect.Ptr { | ||
field.Set(reflect.ValueOf(&ah)) | ||
} else { | ||
field.Set(reflect.ValueOf(ah)) | ||
} | ||
continue | ||
} | ||
|
||
// Still zero value, try default struct field tag. | ||
defaultTag, _ := fieldType.Tag.Lookup("default") | ||
if !field.IsZero() || len(defaultTag) == 0 { | ||
continue | ||
} | ||
|
||
switch field.Kind() { | ||
case reflect.String: | ||
field.SetString(defaultTag) | ||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: | ||
if val, err := strconv.ParseInt(defaultTag, 0, 64); err == nil { | ||
field.SetInt(val) | ||
} | ||
case reflect.Bool: | ||
if val, err := strconv.ParseBool(defaultTag); err == nil { | ||
field.SetBool(val) | ||
} | ||
case reflect.Float32, reflect.Float64: | ||
if val, err := strconv.ParseFloat(defaultTag, 64); err == nil { | ||
field.SetFloat(val) | ||
} | ||
} | ||
} | ||
return rv.Interface().(Action) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,129 @@ | ||
package vm_test | ||
|
||
import ( | ||
"encoding/json" | ||
"testing" | ||
"time" | ||
|
||
"github.com/Agoric/agoric-sdk/golang/cosmos/vm" | ||
sdk "github.com/cosmos/cosmos-sdk/types" | ||
) | ||
|
||
var ( | ||
_ vm.Action = &Trivial{} | ||
_ vm.Action = &Defaults{} | ||
_ vm.Action = &dataAction{} | ||
) | ||
|
||
type Trivial struct { | ||
vm.ActionHeader | ||
Abc int | ||
def string | ||
} | ||
|
||
type Defaults struct { | ||
String string `default:"abc"` | ||
Int int `default:"123"` | ||
Float float64 `default:"4.56"` | ||
Bool bool `default:"true"` | ||
Any interface{} | ||
} | ||
|
||
func (d Defaults) GetActionHeader() vm.ActionHeader { | ||
return vm.ActionHeader{} | ||
} | ||
|
||
type dataAction struct { | ||
*vm.ActionHeader `actionType:"DATA_ACTION"` | ||
Data []byte | ||
} | ||
|
||
func TestActionContext(t *testing.T) { | ||
emptyCtx := sdk.Context{} | ||
|
||
testCases := []struct { | ||
name string | ||
ctx sdk.Context | ||
in vm.Action | ||
expectedOut interface{} | ||
}{ | ||
{"nil", emptyCtx, nil, nil}, | ||
{"no context", emptyCtx, | ||
&Trivial{Abc: 123, def: "zot"}, | ||
&Trivial{Abc: 123, def: "zot"}, | ||
}, | ||
{"block height", | ||
emptyCtx.WithBlockHeight(998), | ||
&Trivial{Abc: 123, def: "zot"}, | ||
&Trivial{ActionHeader: vm.ActionHeader{BlockHeight: 998}, Abc: 123, def: "zot"}, | ||
}, | ||
{"block time", | ||
emptyCtx.WithBlockTime(time.UnixMicro(1_000_000)), | ||
&Trivial{Abc: 123, def: "zot"}, | ||
&Trivial{Abc: 123, def: "zot", ActionHeader: vm.ActionHeader{BlockTime: 1}}, | ||
}, | ||
{"default tags", | ||
emptyCtx, | ||
&Defaults{}, | ||
&Defaults{"abc", 123, 4.56, true, nil}, | ||
}, | ||
{"data action defaults", | ||
emptyCtx.WithBlockHeight(998).WithBlockTime(time.UnixMicro(1_000_000)), | ||
&dataAction{Data: []byte("hello")}, | ||
&dataAction{Data: []byte("hello"), | ||
ActionHeader: &vm.ActionHeader{Type: "DATA_ACTION", BlockHeight: 998, BlockTime: 1}}, | ||
}, | ||
{"data action override Type", | ||
emptyCtx.WithBlockHeight(998).WithBlockTime(time.UnixMicro(1_000_000)), | ||
&dataAction{Data: []byte("hello2"), | ||
ActionHeader: &vm.ActionHeader{Type: "DATA_ACTION2"}}, | ||
&dataAction{Data: []byte("hello2"), | ||
ActionHeader: &vm.ActionHeader{Type: "DATA_ACTION2", BlockHeight: 998, BlockTime: 1}}, | ||
}, | ||
{"data action override BlockHeight", | ||
emptyCtx.WithBlockHeight(998).WithBlockTime(time.UnixMicro(1_000_000)), | ||
&dataAction{Data: []byte("hello2"), | ||
ActionHeader: &vm.ActionHeader{BlockHeight: 999}}, | ||
&dataAction{Data: []byte("hello2"), | ||
ActionHeader: &vm.ActionHeader{Type: "DATA_ACTION", BlockHeight: 999, BlockTime: 1}}, | ||
}, | ||
{"data action override BlockTime", | ||
emptyCtx.WithBlockHeight(998).WithBlockTime(time.UnixMicro(1_000_000)), | ||
&dataAction{Data: []byte("hello2"), | ||
ActionHeader: &vm.ActionHeader{BlockTime: 2}}, | ||
&dataAction{Data: []byte("hello2"), | ||
ActionHeader: &vm.ActionHeader{Type: "DATA_ACTION", BlockHeight: 998, BlockTime: 2}}, | ||
}, | ||
{"data action override all defaults", | ||
emptyCtx.WithBlockHeight(998).WithBlockTime(time.UnixMicro(1_000_000)), | ||
&dataAction{Data: []byte("hello2"), | ||
ActionHeader: &vm.ActionHeader{Type: "DATA_ACTION2", BlockHeight: 999, BlockTime: 2}}, | ||
&dataAction{Data: []byte("hello2"), | ||
ActionHeader: &vm.ActionHeader{Type: "DATA_ACTION2", BlockHeight: 999, BlockTime: 2}}, | ||
}, | ||
} | ||
|
||
for _, tc := range testCases { | ||
tc := tc | ||
t.Run(tc.name, func(t *testing.T) { | ||
jstr := func(in interface{}) string { | ||
bz, err := json.Marshal(in) | ||
if err != nil { | ||
t.Fatal(err) | ||
} | ||
return string(bz) | ||
} | ||
jsonIn := jstr(tc.in) | ||
out := vm.PopulateAction(tc.ctx, tc.in) | ||
jsonIn2 := jstr(tc.in) | ||
if jsonIn != jsonIn2 { | ||
t.Errorf("unexpected mutated input: %s to %s", jsonIn, jsonIn2) | ||
} | ||
jsonOut := jstr(out) | ||
jsonExpectedOut := jstr(tc.expectedOut) | ||
if jsonOut != jsonExpectedOut { | ||
t.Errorf("expected %s, got %s", jsonExpectedOut, jsonOut) | ||
} | ||
}) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.