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

wip(keeper): implement json primitive return #2949

Draft
wants to merge 14 commits into
base: master
Choose a base branch
from
1 change: 1 addition & 0 deletions examples/gno.land/r/demo/users/users.gno
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ var (
func Register(inviter std.Address, name string, profile string) {
// assert CallTx call.
std.AssertOriginCall()

// assert invited or paid.
caller := std.GetCallerAt(2)
if caller != std.GetOrigCaller() {
Expand Down
8 changes: 3 additions & 5 deletions gno.land/pkg/gnoland/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -405,11 +405,9 @@ func EndBlocker(
}

// Run the VM to get the updates from the chain
response, err := vmk.QueryEval(
ctx,
valRealm,
fmt.Sprintf("%s(%d)", valChangesFn, app.LastBlockHeight()),
)
expr := fmt.Sprintf("%s(%d)", valChangesFn, app.LastBlockHeight())
msgEval := vm.NewMsgEval(vm.FormatDefault, valRealm, expr)
response, err := vmk.Eval(ctx, msgEval)
if err != nil {
app.Logger().Error("unable to call VM during EndBlocker", "err", err)

Expand Down
76 changes: 76 additions & 0 deletions gno.land/pkg/keyscli/query_json.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package keyscli

import (
"context"
"encoding/json"
"flag"
"fmt"

"github.com/gnolang/gno/tm2/pkg/amino"
"github.com/gnolang/gno/tm2/pkg/commands"
"github.com/gnolang/gno/tm2/pkg/crypto/keys/client"
)

func NewQueryJSONCmd(rootCfg *client.BaseCfg, io commands.IO) *commands.Command {
cfg := &client.QueryCfg{
RootCfg: rootCfg,
}

return commands.NewCommand(
commands.Metadata{
Name: "jquery",
ShortUsage: "jquery [flags] <path>",
ShortHelp: "EXPERIMENTAL: makes an ABCI query and return a result in json",
LongHelp: "EXPERIMENTAL: makes an ABCI query and return a result in json",
},
cfg,
func(_ context.Context, args []string) error {
return execQuery(cfg, args, io)
},
)
}

func execQuery(cfg *client.QueryCfg, args []string, io commands.IO) error {
if len(args) != 1 {
return flag.ErrHelp
}

cfg.Path = args[0]
if cfg.Path == "vm/qeval" {
// automatically add json suffix for qeval
cfg.Path = cfg.Path + "/json"
}

qres, err := client.QueryHandler(cfg)
if err != nil {
return err
}

var output struct {
Response json.RawMessage `json:"response"`
Returns json.RawMessage `json:"returns,omitempty"`
}

if output.Response, err = amino.MarshalJSONIndent(qres.Response, "", " "); err != nil {
io.ErrPrintfln("Unable to marshal response %+v\n", qres)
return fmt.Errorf("amino marshal json error: %w", err)
}

// XXX: this is probably too specific
if cfg.Path == "vm/qeval/json" {
if len(qres.Response.Data) > 0 {
output.Returns = qres.Response.Data
} else {
output.Returns = []byte("[]")
}
}

res, err := json.MarshalIndent(output, "", " ")
if err != nil {
io.ErrPrintfln("Unable to marshal output %+v\n", qres)
return fmt.Errorf("marshal json error: %w", err)
}

io.Println(string(res))
return nil
}
3 changes: 3 additions & 0 deletions gno.land/pkg/keyscli/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ func NewRootCmd(io commands.IO, base client.BaseOptions) *commands.Command {
client.NewQueryCmd(cfg, io),
client.NewBroadcastCmd(cfg, io),

// Custom query command
NewQueryJSONCmd(cfg, io),

// Custom MakeTX command
NewMakeTxCmd(cfg, io),
)
Expand Down
93 changes: 93 additions & 0 deletions gno.land/pkg/sdk/vm/convert.go
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,99 @@ func convertArgToGno(arg string, argT gno.Type) (tv gno.TypedValue) {
}
}

func JSONPrimitiveValues(m *gno.Machine, tvs []gno.TypedValue) string {
var str strings.Builder

str.WriteRune('[')
for i, tv := range tvs {
if i > 0 {
str.WriteRune(',')
}
str.WriteString(JSONPrimitiveValue(m, tv))
}
str.WriteRune(']')

return str.String()
}

func JSONPrimitiveValue(m *gno.Machine, tv gno.TypedValue) string {
if tv.T == nil {
return "null"
}

switch bt := gno.BaseOf(tv.T).(type) {
case gno.PrimitiveType:
switch bt {
case gno.IntType:
return fmt.Sprintf("%d", tv.GetInt())
case gno.Int8Type:
return fmt.Sprintf("%d", tv.GetInt8())
case gno.Int16Type:
return fmt.Sprintf("%d", tv.GetInt16())
case gno.UntypedRuneType, gno.Int32Type:
return fmt.Sprintf("%d", tv.GetInt32())
case gno.Int64Type:
return fmt.Sprintf("%d", tv.GetInt64())
case gno.UintType:
return fmt.Sprintf("%d", tv.GetUint())
case gno.Uint8Type:
return fmt.Sprintf("%d", tv.GetUint8())
case gno.DataByteType:
return fmt.Sprintf("%d", tv.GetDataByte())
case gno.Uint16Type:
return fmt.Sprintf("%d", tv.GetUint16())
case gno.Uint32Type:
return fmt.Sprintf("%d", tv.GetUint32())
case gno.Uint64Type:
return fmt.Sprintf("%d", tv.GetUint64())
case gno.Float32Type:
return fmt.Sprintf("%f", tv.GetFloat32())
case gno.Float64Type:
return fmt.Sprintf("%f", tv.GetFloat64())
case gno.UntypedBigintType, gno.BigintType:
return tv.V.(gno.BigintValue).V.String()
case gno.UntypedBigdecType, gno.BigdecType:
return tv.V.(gno.BigdecValue).V.String()
case gno.UntypedBoolType, gno.BoolType:
return fmt.Sprintf("%t", tv.GetBool())
case gno.UntypedStringType, gno.StringType:
return strconv.Quote(tv.GetString())
default:
panic("invalid primitive type - should not happen")
}
case *gno.PointerType:
// Check if Pointer we type implement Error
// If implements .Error(), return it.
if tv.IsError() {
res := m.Eval(gno.Call(gno.Sel(&gno.ConstExpr{TypedValue: tv}, "Error")))
return strconv.Quote(res[0].GetString())
}
default:
// Check if pointer wraped value can implement Error
ptv := gno.TypedValue{
T: &gno.PointerType{Elt: tv.T},
V: gno.PointerValue{TV: &tv, Base: tv.V},
}

// If implements .Error(), return it.
if ptv.IsError() {
res := m.Eval(gno.Call(gno.Sel(&gno.ConstExpr{TypedValue: ptv}, "Error")))
return strconv.Quote(res[0].GetString())
}
}

if tv.V == nil {
return "null"
}

var id string
if pv, ok := tv.V.(gno.PointerValue); ok {
id = pv.GetBase(m.Store).GetObjectID().String()
}

return strconv.Quote(fmt.Sprintf(`<%s:%s>`, tv.T.String(), id))
}

func convertFloat(value string, precision int) float64 {
assertNoPlusPrefix(value)
dec, _, err := apd.NewFromString(value)
Expand Down
161 changes: 161 additions & 0 deletions gno.land/pkg/sdk/vm/convert_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@ package vm

import (
"fmt"
"strconv"
"strings"
"testing"

"github.com/gnolang/gno/gnovm/pkg/gnolang"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestConvertEmptyNumbers(t *testing.T) {
Expand Down Expand Up @@ -37,3 +40,161 @@ func TestConvertEmptyNumbers(t *testing.T) {
})
}
}

func TestConvertJSONValuePrimtive(t *testing.T) {
cases := []struct {
ValueRep string // Go representation
Expected string // string representation
}{
// Boolean
{"nil", "null"},

// Boolean
{"true", "true"},
{"false", "false"},

// int types
{"int(42)", `42`}, // Needs to be quoted for amino
{"int8(42)", `42`},
{"int16(42)", `42`},
{"int32(42)", `42`},
{"int64(42)", `42`},

// uint types
{"uint(42)", `42`},
{"uint8(42)", `42`},
{"uint16(42)", `42`},
{"uint32(42)", `42`},
{"uint64(42)", `42`},

// Float types
{"float32(3.14)", "3.14"},
{"float64(3.14)", "3.14"},

// String type
{`"hello world"`, `"hello world"`},
}

for _, tc := range cases {
t.Run(tc.ValueRep, func(t *testing.T) {
m := gnolang.NewMachine("testdata", nil)
defer m.Release()

nn := gnolang.MustParseFile("testdata.gno",
fmt.Sprintf(`package testdata; var Value = %s`, tc.ValueRep))
m.RunFiles(nn)
m.RunDeclaration(gnolang.ImportD("testdata", "testdata"))

tps := m.Eval(gnolang.Sel(gnolang.Nx("testdata"), "Value"))
require.Len(t, tps, 1)

tv := tps[0]

rep := JSONPrimitiveValue(m, tv)
require.Equal(t, tc.Expected, rep)
})
}
}

func TestConvertJSONValueStruct(t *testing.T) {
const StructsFile = `
package testdata

// E struct, impement error
type E struct { S string }

func (e *E) Error() string { return e.S }
`

t.Run("null pointer", func(t *testing.T) {
m := gnolang.NewMachine("testdata", nil)
defer m.Release()

const expected = "Hello World"
nn := gnolang.MustParseFile("struct.gno", StructsFile)
m.RunFiles(nn)
nn = gnolang.MustParseFile("testdata.gno",
fmt.Sprintf(`package testdata; var Value *E = nil`, expected))
m.RunFiles(nn)
m.RunDeclaration(gnolang.ImportD("testdata", "testdata"))

tps := m.Eval(gnolang.Sel(gnolang.Nx("testdata"), "Value"))
require.Len(t, tps, 1)

tv := tps[0]
rep := JSONPrimitiveValue(m, tv)
require.Equal(t, strconv.Quote(expected), rep)
})

t.Run("without pointer", func(t *testing.T) {
m := gnolang.NewMachine("testdata", nil)
defer m.Release()

const expected = "Hello World"
nn := gnolang.MustParseFile("struct.gno", StructsFile)
m.RunFiles(nn)
nn = gnolang.MustParseFile("testdata.gno",
fmt.Sprintf(`package testdata; var Value = E{%q}`, expected))
m.RunFiles(nn)
m.RunDeclaration(gnolang.ImportD("testdata", "testdata"))

tps := m.Eval(gnolang.Sel(gnolang.Nx("testdata"), "Value"))
require.Len(t, tps, 1)

tv := tps[0]
rep := JSONPrimitiveValue(m, tv)
require.Equal(t, strconv.Quote(expected), rep)
})

t.Run("with pointer", func(t *testing.T) {
m := gnolang.NewMachine("testdata", nil)
defer m.Release()

const expected = "Hello World"
nn := gnolang.MustParseFile("struct.gno", StructsFile)
m.RunFiles(nn)
nn = gnolang.MustParseFile("testdata.gno",
fmt.Sprintf(`package testdata; var Value = &E{%q}`, expected))
m.RunFiles(nn)
m.RunDeclaration(gnolang.ImportD("testdata", "testdata"))

tps := m.Eval(gnolang.Sel(gnolang.Nx("testdata"), "Value"))
require.Len(t, tps, 1)

tv := tps[0]
rep := JSONPrimitiveValue(m, tv)
require.Equal(t, strconv.Quote(expected), rep)
})
}

func TestConvertJSONValuesList(t *testing.T) {
cases := []struct {
ValueRep []string // Go representation
Expected string // string representation
}{
// Boolean
{[]string{}, "[]"},
{[]string{"42"}, "[42]"},
{[]string{"42", `"hello world"`}, `[42,"hello world"]`},
{[]string{"42", `"hello world"`, "[]int{42}"}, `[42,"hello world"]`},
}

for _, tc := range cases {
t.Run(strings.Join(tc.ValueRep, "-"), func(t *testing.T) {
m := gnolang.NewMachine("testdata", nil)
defer m.Release()

nn := gnolang.MustParseFile("testdata.gno",
fmt.Sprintf(`package testdata; var Value = []interface{}{%s}`, strings.Join(tc.ValueRep, ",")))
m.RunFiles(nn)
m.RunDeclaration(gnolang.ImportD("testdata", "testdata"))

tps := m.Eval(gnolang.Sel(gnolang.Nx("testdata"), "Value"))
require.Len(t, tps, 1)
require.Equal(t, gnolang.SliceKind.String(), tps[0].T.Kind().String())
tpvs := tps[0].V.(*gnolang.SliceValue).Base.(*gnolang.ArrayValue).List
rep := JSONPrimitiveValues(m, tpvs)
require.Equal(t, tc.Expected, rep)
})
}
}
Loading
Loading