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

feat(keeper): implement json primitive return #2949

Open
wants to merge 23 commits into
base: master
Choose a base branch
from

Conversation

gfanton
Copy link
Member

@gfanton gfanton commented Oct 14, 2024

Yet another JSON PR 🎉

This is a simpler implementation of the JSON keeper result that introduces JSON primitive results in keeper MsgCall and MsgQuery result. To maintain backward compatibility, it's not enabled by default, and you have to specify the format in the request.

The result will be returned in the form of a parsable JSON array.

Currently, it will resolve types as follows:

  • PrimitiveKind -> raw value
  • Error/Stringer -> execute and return result
  • Object -> <oid:type> <obj:TYPE:OID> as string
  • Other -> <type> as string

This PR also adds a small utility command in gnokey called jquery that outputs a parsable JSON response. This command is flagged as experimental, as are all JSON features present in the handler/keeper.

Screenshot 2025-01-09 at 10 31 49

TODOs

  • Probably more tests
  • Need approval on how we represent object
Contributors' checklist...
  • Added new tests, or not needed, or not feasible
  • Provided an example (e.g. screenshot) to aid review or the PR is self-explanatory
  • Updated the official documentation or not needed
  • No breaking changes were made, or a BREAKING CHANGE: xxx message was included in the description
  • Added references to related issues and PRs
  • Provided any useful hints for running manual tests
  • Added new benchmarks to generated graphs, if any. More info here.

Signed-off-by: gfanton <8671905+gfanton@users.noreply.github.com>
Signed-off-by: gfanton <8671905+gfanton@users.noreply.github.com>
Signed-off-by: gfanton <8671905+gfanton@users.noreply.github.com>
Signed-off-by: gfanton <8671905+gfanton@users.noreply.github.com>
@github-actions github-actions bot added 🧾 package/realm Tag used for new Realms or Packages. 📦 🤖 gnovm Issues or PRs gnovm related 📦 🌐 tendermint v2 Issues or PRs tm2 related 📦 ⛰️ gno.land Issues or PRs gno.land package related labels Oct 14, 2024
Copy link

codecov bot commented Oct 14, 2024

Copy link
Member

@thehowl thehowl left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

overall approach LGTM, let's roll with only supporting primitive types for now

gno.land/pkg/sdk/vm/keeper.go Outdated Show resolved Hide resolved
gno.land/pkg/sdk/vm/msgs.go Outdated Show resolved Hide resolved
gnovm/pkg/gnolang/values.go Outdated Show resolved Hide resolved
tm2/pkg/bft/abci/types/types.go Outdated Show resolved Hide resolved
Signed-off-by: gfanton <8671905+gfanton@users.noreply.github.com>
Signed-off-by: gfanton <8671905+gfanton@users.noreply.github.com>
Signed-off-by: gfanton <8671905+gfanton@users.noreply.github.com>
Signed-off-by: gfanton <8671905+gfanton@users.noreply.github.com>
QueryRender = "qrender"
QueryFuncs = "qfuncs"
QueryEval = "qeval"
QueryEvalJSON = "qeval/json" // EXPERIMENTAL
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I prefer a single call with a flag to specify the format.
Any other opinions?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A solution could be to have "qeval" data as a marshaled MsgEval, but that would break the current behavior of qeval until the current behavior is set as a fallback if unmarshal MsgEval fail.
I'm hesitating; it would probably give more flexibility, but qeval/json is really more straightforward.

wdyt?

Copy link
Member Author

@gfanton gfanton Jan 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok. After some attempts with url.Url using a query to parse the JSON flag or employing \n as a flag separator, I found it problematic and difficult to escape. This issue arises when users include ? or \n within the expression's arguments. In my opinion, we should use qeval/json as it looks like the most straightforward method to handle this for now, given the current implementation of the handler.

A possible compromise is to check the json part of vm/qeval/json within the queryEval function, allowing the right part of the path to serve as a flag.

Copy link
Member Author

@gfanton gfanton Jan 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@moul I've ended up going with the path query flag; see the queryEval function. We can probably set another separator than / to distinguish between the specific path and the flag. However, having this as a distinct sub-path does especially chock me.

gno.land/pkg/sdk/vm/keeper.go Outdated Show resolved Hide resolved
gno.land/pkg/sdk/vm/keeper.go Outdated Show resolved Hide resolved
@Kouteki Kouteki added the in focus Core team is prioritizing this work label Nov 15, 2024
@Kouteki Kouteki added this to the 🚀 Mainnet launch milestone Nov 22, 2024
Signed-off-by: gfanton <8671905+gfanton@users.noreply.github.com>
Signed-off-by: gfanton <8671905+gfanton@users.noreply.github.com>
Signed-off-by: gfanton <8671905+gfanton@users.noreply.github.com>
Signed-off-by: gfanton <8671905+gfanton@users.noreply.github.com>
Signed-off-by: gfanton <8671905+gfanton@users.noreply.github.com>
Signed-off-by: gfanton <8671905+gfanton@users.noreply.github.com>
Signed-off-by: gfanton <8671905+gfanton@users.noreply.github.com>
@gfanton gfanton changed the title wip(keeper): implement json primitive return feat(keeper): implement json primitive return Jan 26, 2025
Signed-off-by: gfanton <8671905+gfanton@users.noreply.github.com>
@gfanton gfanton marked this pull request as ready for review January 26, 2025 22:07
Signed-off-by: gfanton <8671905+gfanton@users.noreply.github.com>
Signed-off-by: gfanton <8671905+gfanton@users.noreply.github.com>
@moul
Copy link
Member

moul commented Jan 27, 2025

Can you add a small txtar to illustrate the expected UX?

Signed-off-by: gfanton <8671905+gfanton@users.noreply.github.com>
Signed-off-by: gfanton <8671905+gfanton@users.noreply.github.com>
fmt.Sprintf("%s(%d)", valChangesFn, app.LastBlockHeight()),
)
expr := fmt.Sprintf("%s(%d)", valChangesFn, app.LastBlockHeight())
msgEval := vm.NewMsgEval(vm.FormatDefault, valRealm, expr)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@zivkovicmilos, we don't need to port this module in this PR, but I'd like your opinion on what the diff will look like thanks to the new JSON built-in support.

)
}

func execQuery(cfg *client.QueryCfg, args []string, io commands.IO) error {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
func execQuery(cfg *client.QueryCfg, args []string, io commands.IO) error {
func execJSONQuery(cfg *client.QueryCfg, args []string, io commands.IO) error {

}

// XXX: this is probably too specific
if cfg.Path == "vm/qeval/json" {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this be a preliminary check raising an error at the beginning of the function?

return IsImplementedBy(gStringerType, tv.T)
}

func (tv *TypedValue) IsError() bool {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure that this check is universally seen as the way to indicate that a TypedValue is an error. It might make sense to rename it to ImplError(). Thoughts?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, while IsStringer makes more sense since Stringer is universal for the String() implementation, I agree that IsError is not obvious and can be confusing. I can rename it to ImplError, but if I do that, I should also rename IsStringer to ImplStringer for consistency.

Comment on lines +32 to +33
case MsgEval:
return vh.handleMsgEval(ctx, msg)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
case MsgEval:
return vh.handleMsgEval(ctx, msg)

Having a MsgEval struct instead of using arguments may be sensible. However, I believe we should not register Eval as a possible transaction type in that manner.


var _ std.Msg = MsgEval{}

func NewMsgEval(format Format, pkgPath, expr string) MsgEval {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
func NewMsgEval(format Format, pkgPath, expr string) MsgEval {
func NewMsgEval(pkgPath, expr string, format Format) MsgEval {

Personal preference for this order.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the more i think about it, the more i'm for having such struct, but unexported, without New constructor, not in the msgs.go file.

and probably centralizing everything about the query in query.go.

Comment on lines +135 to +136
func (msg MsgEval) GetSignBytes() []byte {
return std.MustSortJSON(amino.MustMarshalJSON(msg))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I support removing this and preventing MsgCall from being included in a transaction. It's just a query.

@@ -46,7 +47,7 @@ const (
type VMKeeperI interface {
AddPackage(ctx sdk.Context, msg MsgAddPackage) error
Call(ctx sdk.Context, msg MsgCall) (res string, err error)
QueryEval(ctx sdk.Context, pkgPath string, expr string) (res string, err error)
Eval(ctx sdk.Context, msg MsgEval) (res string, err error)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Eval(ctx sdk.Context, msg MsgEval) (res string, err error)
QueryEval(ctx sdk.Context, pkgPath, expr string, format Format) (res string, err error)

What about this?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Eval(ctx sdk.Context, msg MsgEval) (res string, err error)
QueryEval(ctx sdk.Context, cfg EvalCfg) (res string, err error)

To support optional flags, this is probably better.

}
return res, nil

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As only Call can handle multiple msgs, I don't think we should do that; we don't want Queries to end with a useless \n in their response.

const (
FormatMachine Format = "machine" // Default machine representation
FormatString = "string" // Single string representation
FormatJSON = "json" // XXX: EXPERIMENTAL, only supports primitive types for now
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
FormatJSON = "json" // XXX: EXPERIMENTAL, only supports primitive types for now
FormatJSON = "json" // XXX: EXPERIMENTAL, only supports primitive types for now
FormatJSONP = "jsonp" // XXX: EXPERIMENTAL, only supports primitive types for now

I'm for adding right now the jsonp alternative. Btw jsonp takes an extra argument (the callback name).

// XXX: This field is experimental, use with care as output is likely to change
// Default format is machine
Format Format `json:"format" yaml:"format"`
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
}
FormatOpts string `json:"format-opts" yaml:"format-opts"`
}

to support formatting options, such as JSONP' callback, and eventually other things later?

@@ -14,6 +14,7 @@ var Package = amino.RegisterPackage(amino.NewPackage(
std.Package,
gnovm.Package,
).WithTypes(
MsgEval{}, "m_eval",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
MsgEval{}, "m_eval",

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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks weird. what's the goal? maybe add a comment if there is a valid reason.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will do. The reason for that is this jquery is made to handle JSON, so you want your query to return JSON. Therefore, you should not have to specify the format for the query. The goal is to automatically pass the argument to produce a JSON output without having to specify it every time.
query vm/qeval abc.xy/r/toto -> machine
jquery vm/qeval abc.xy/r/toto -> JSON

slice := tv.V.(*gno.SliceValue)
if data := slice.GetBase(nil).Data; data != nil {
i := slice.GetLength()
return `"` + base64.StdEncoding.EncodeToString(data[:i]) + `"`
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can't we support slices and arrays just like this:

"[" + strings.Join(JSONPrimitiveValues(vals), ",") + "]"?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be cool, but not like this, as it would not handle cyclic dependencies. Setting a depth could be a solution, but I prefer handling this in a separate PR.

return res, nil
}

func stringifyResultValues(m *gno.Machine, format Format, values []gno.TypedValue) string {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

implement or add a comment to add gasmeter here.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As this method inherits the gno.Machine, I think it naturally handles gas here.
@piux2 can you confirm ?

Signed-off-by: gfanton <8671905+gfanton@users.noreply.github.com>

return commands.NewCommand(
commands.Metadata{
Name: "jquery",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This name gives me shivers. Could we not do gnokey query --json? Seems also easier to support in broadcast and maketx if it's just a flag.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What case do you see with broadcast/maketx and JSON?

I personally like "jquery"; it sounds like "anti-web 2.0." :)

}

// Fallback on generic object handler
if pv, ok := tv.V.(gno.PointerValue); ok {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we not also consider the case where tv.V is an Object?

	switch v := tv.V.(type) {
	case *PointerValue:
	case Object:
	default:
	}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
in focus Core team is prioritizing this work 📦 🌐 tendermint v2 Issues or PRs tm2 related 📦 ⛰️ gno.land Issues or PRs gno.land package related 📦 🤖 gnovm Issues or PRs gnovm related 🧾 package/realm Tag used for new Realms or Packages.
Projects
Status: No status
Status: No status
Status: In Review
Development

Successfully merging this pull request may close these issues.

5 participants