diff mbox series

[v1,7/9] qapi: golang: Generate qapi's command types in Go

Message ID 20230927112544.85011-8-victortoso@redhat.com
State New
Headers show
Series qapi-go: add generator for Golang interface | expand

Commit Message

Victor Toso Sept. 27, 2023, 11:25 a.m. UTC
This patch handles QAPI command types and generates data structures in
Go that decodes from QMP JSON Object to Go data structure and vice
versa.

Similar to Event, this patch adds a Command interface and two helper
functions MarshalCommand and UnmarshalCommand.

Example:
qapi:
 | { 'command': 'set_password',
 |   'boxed': true,
 |   'data': 'SetPasswordOptions' }

go:
 | type SetPasswordCommand struct {
 |     SetPasswordOptions
 |     CommandId string `json:"-"`
 | }

usage:
 | input := `{"execute":"set_password",` +
 |          `"arguments":{"protocol":"vnc",` +
 |          `"password":"secret"}}`
 |
 | c, err := UnmarshalCommand([]byte(input))
 | if err != nil {
 |     panic(err)
 | }
 |
 | if c.GetName() == `set_password` {
 |         m := c.(*SetPasswordCommand)
 |         // m.Password == "secret"
 | }

Signed-off-by: Victor Toso <victortoso@redhat.com>
---
 scripts/qapi/golang.py | 97 ++++++++++++++++++++++++++++++++++++++++--
 1 file changed, 94 insertions(+), 3 deletions(-)

Comments

Daniel P. Berrangé Sept. 28, 2023, 2:32 p.m. UTC | #1
On Wed, Sep 27, 2023 at 01:25:42PM +0200, Victor Toso wrote:
> This patch handles QAPI command types and generates data structures in
> Go that decodes from QMP JSON Object to Go data structure and vice
> versa.
> 
> Similar to Event, this patch adds a Command interface and two helper
> functions MarshalCommand and UnmarshalCommand.
> 
> Example:
> qapi:
>  | { 'command': 'set_password',
>  |   'boxed': true,
>  |   'data': 'SetPasswordOptions' }
> 
> go:
>  | type SetPasswordCommand struct {
>  |     SetPasswordOptions
>  |     CommandId string `json:"-"`

IIUC, you renamed that to MessageId in the code now.

>  | }

Overall, I'm not entirely convinced that we will want to
have the SetPasswordCommand struct wrappers, byut it is
hard to say, as what we're missing still is the eventual
application facing API.

eg something that ultimately looks more like this:

    qemu = qemu.QMPConnection()
    qemu.Dial("/path/to/unix/socket.sock")

    qemu.VncConnectedEvent(func(ev *VncConnectedEvent) {
         fmt.Printf("VNC client %s connected\n", ev.Client.Host)
    })

    resp, err := qemu.SetPassword(SetPasswordArguments{
        protocol: "vnc",
	password: "123456",
    })

    if err != nil {
        fmt.Fprintf(os.Stderr, "Cannot set passwd: %s", err)
    }

    ..do something wit resp....   (well SetPassword has no response, but other cmmands do)

It isn't clear that the SetPasswordCommand struct will be
needed internally for the impl if QMPCommand.

> 
> usage:
>  | input := `{"execute":"set_password",` +
>  |          `"arguments":{"protocol":"vnc",` +
>  |          `"password":"secret"}}`
>  |
>  | c, err := UnmarshalCommand([]byte(input))
>  | if err != nil {
>  |     panic(err)
>  | }
>  |
>  | if c.GetName() == `set_password` {
>  |         m := c.(*SetPasswordCommand)
>  |         // m.Password == "secret"
>  | }
> 
> Signed-off-by: Victor Toso <victortoso@redhat.com>
> ---
>  scripts/qapi/golang.py | 97 ++++++++++++++++++++++++++++++++++++++++--
>  1 file changed, 94 insertions(+), 3 deletions(-)
> 
> diff --git a/scripts/qapi/golang.py b/scripts/qapi/golang.py
> index ff3b1dd020..52a9124641 100644
> --- a/scripts/qapi/golang.py
> +++ b/scripts/qapi/golang.py
> @@ -246,6 +246,51 @@
>  }}
>  '''
>  
> +TEMPLATE_COMMAND_METHODS = '''
> +func (c *{type_name}) GetName() string {{
> +    return "{name}"
> +}}
> +
> +func (s *{type_name}) GetId() string {{
> +    return s.MessageId
> +}}
> +'''
> +
> +TEMPLATE_COMMAND = '''
> +type Command interface {{
> +    GetId()         string
> +    GetName()       string
> +}}
> +
> +func MarshalCommand(c Command) ([]byte, error) {{
> +    m := make(map[string]any)
> +    m["execute"] = c.GetName()
> +    if id := c.GetId(); len(id) > 0 {{
> +        m["id"] = id
> +    }}
> +    if bytes, err := json.Marshal(c); err != nil {{
> +        return []byte{{}}, err
> +    }} else if len(bytes) > 2 {{
> +        m["arguments"] = c
> +    }}
> +    return json.Marshal(m)
> +}}
> +
> +func UnmarshalCommand(data []byte) (Command, error) {{
> +    base := struct {{
> +        MessageId string `json:"id,omitempty"`
> +        Name      string `json:"execute"`
> +    }}{{}}
> +    if err := json.Unmarshal(data, &base); err != nil {{
> +        return nil, fmt.Errorf("Failed to decode command: %s", string(data))
> +    }}
> +
> +    switch base.Name {{
> +    {cases}
> +    }}
> +    return nil, errors.New("Failed to recognize command")
> +}}
> +'''
>  
>  def gen_golang(schema: QAPISchema,
>                 output_dir: str,
> @@ -282,7 +327,7 @@ def qapi_to_go_type_name(name: str,
>  
>      name += ''.join(word.title() for word in words[1:])
>  
> -    types = ["event"]
> +    types = ["event", "command"]
>      if meta in types:
>          name = name[:-3] if name.endswith("Arg") else name
>          name += meta.title().replace(" ", "")
> @@ -521,6 +566,8 @@ def qapi_to_golang_struct(self: QAPISchemaGenGolangVisitor,
>      fields, with_nullable = recursive_base(self, base)
>      if info.defn_meta == "event":
>          fields += f'''\tMessageTimestamp Timestamp `json:"-"`\n{fields}'''
> +    elif info.defn_meta == "command":
> +        fields += f'''\tMessageId string `json:"-"`\n{fields}'''
>  
>      if members:
>          for member in members:
> @@ -719,16 +766,36 @@ def generate_template_event(events: dict[str, str]) -> str:
>  '''
>      return TEMPLATE_EVENT.format(cases=cases)
>  
> +def generate_template_command(commands: dict[str, str]) -> str:
> +    cases = ""
> +    for name in sorted(commands):
> +        case_type = commands[name]
> +        cases += f'''
> +case "{name}":
> +    command := struct {{
> +        Args {case_type} `json:"arguments"`
> +    }}{{}}
> +
> +    if err := json.Unmarshal(data, &command); err != nil {{
> +        return nil, fmt.Errorf("Failed to unmarshal: %s", string(data))
> +    }}
> +    command.Args.MessageId = base.MessageId
> +    return &command.Args, nil
> +'''
> +    content = TEMPLATE_COMMAND.format(cases=cases)
> +    return content
> +
>  
>  class QAPISchemaGenGolangVisitor(QAPISchemaVisitor):
>  
>      def __init__(self, _: str):
>          super().__init__()
> -        types = ["alternate", "enum", "event", "helper", "struct", "union"]
> +        types = ["alternate", "command", "enum", "event", "helper", "struct", "union"]
>          self.target = {name: "" for name in types}
>          self.objects_seen = {}
>          self.schema = None
>          self.events = {}
> +        self.commands = {}
>          self.golang_package_name = "qapi"
>          self.accept_null_types = []
>  
> @@ -756,6 +823,7 @@ def visit_begin(self, schema):
>      def visit_end(self):
>          self.schema = None
>          self.target["event"] += generate_template_event(self.events)
> +        self.target["command"] += generate_template_command(self.commands)
>  
>      def visit_object_type(self: QAPISchemaGenGolangVisitor,
>                            name: str,
> @@ -853,7 +921,30 @@ def visit_command(self,
>                        allow_oob: bool,
>                        allow_preconfig: bool,
>                        coroutine: bool) -> None:
> -        pass
> +        assert name == info.defn_name
> +
> +        type_name = qapi_to_go_type_name(name, info.defn_meta)
> +        self.commands[name] = type_name
> +
> +        content = ""
> +        if boxed or not arg_type or not qapi_name_is_object(arg_type.name):
> +            args = "" if not arg_type else "\n" + arg_type.name
> +            args += '''\n\tMessageId   string `json:"-"`'''
> +            content = generate_struct_type(type_name, args)
> +        else:
> +            assert isinstance(arg_type, QAPISchemaObjectType)
> +            content = qapi_to_golang_struct(self,
> +                                            name,
> +                                            arg_type.info,
> +                                            arg_type.ifcond,
> +                                            arg_type.features,
> +                                            arg_type.base,
> +                                            arg_type.members,
> +                                            arg_type.variants)
> +
> +        content += TEMPLATE_COMMAND_METHODS.format(name=name,
> +                                                   type_name=type_name)
> +        self.target["command"] += content
>  
>      def visit_event(self, name, info, ifcond, features, arg_type, boxed):
>          assert name == info.defn_name
> -- 
> 2.41.0
> 

With regards,
Daniel
Victor Toso Sept. 29, 2023, 1:53 p.m. UTC | #2
Hi,

On Thu, Sep 28, 2023 at 03:32:54PM +0100, Daniel P. Berrangé wrote:
> On Wed, Sep 27, 2023 at 01:25:42PM +0200, Victor Toso wrote:
> > This patch handles QAPI command types and generates data structures in
> > Go that decodes from QMP JSON Object to Go data structure and vice
> > versa.
> > 
> > Similar to Event, this patch adds a Command interface and two helper
> > functions MarshalCommand and UnmarshalCommand.
> > 
> > Example:
> > qapi:
> >  | { 'command': 'set_password',
> >  |   'boxed': true,
> >  |   'data': 'SetPasswordOptions' }
> > 
> > go:
> >  | type SetPasswordCommand struct {
> >  |     SetPasswordOptions
> >  |     CommandId string `json:"-"`
> 
> IIUC, you renamed that to MessageId in the code now.

Thanks!

> 
> >  | }
> 
> Overall, I'm not entirely convinced that we will want to
> have the SetPasswordCommand struct wrappers, byut it is
> hard to say, as what we're missing still is the eventual
> application facing API.
> 
> eg something that ultimately looks more like this:
> 
>     qemu = qemu.QMPConnection()
>     qemu.Dial("/path/to/unix/socket.sock")
> 
>     qemu.VncConnectedEvent(func(ev *VncConnectedEvent) {
>          fmt.Printf("VNC client %s connected\n", ev.Client.Host)
>     })
> 
>     resp, err := qemu.SetPassword(SetPasswordArguments{
>         protocol: "vnc",
> 	password: "123456",
>     })

For the other structs, I've removed these embed struct (base).
For the commands, I wasn't sure so I left them. I think it is
worth to give another look as I do agree with you.

In the Go application is unlikely that the embed structs are
needed, the important part is to have all the fields in the
command struct.

My prior concern, if I recall correctly was:

 1. This would leave generated but unused quite a few structs.
    Should I remove them? Should I leave them?

 2. The struct might be something we might use elsewhere, so it
    would make sense to, for example:

    qemu.SetPassword.SetPasswordArguments = myArgs

    Instead of having to assign field by field, from myArgs to
    qemu.SetPassword as they would be different types.

Overall, I would prefer not having embed types so I'll give
another look to this.

Cheers,
Victor

> 
>     if err != nil {
>         fmt.Fprintf(os.Stderr, "Cannot set passwd: %s", err)
>     }
> 
>     ..do something wit resp....   (well SetPassword has no response, but other cmmands do)
> 
> It isn't clear that the SetPasswordCommand struct will be
> needed internally for the impl if QMPCommand.
> 
> > 
> > usage:
> >  | input := `{"execute":"set_password",` +
> >  |          `"arguments":{"protocol":"vnc",` +
> >  |          `"password":"secret"}}`
> >  |
> >  | c, err := UnmarshalCommand([]byte(input))
> >  | if err != nil {
> >  |     panic(err)
> >  | }
> >  |
> >  | if c.GetName() == `set_password` {
> >  |         m := c.(*SetPasswordCommand)
> >  |         // m.Password == "secret"
> >  | }
> > 
> > Signed-off-by: Victor Toso <victortoso@redhat.com>
> > ---
> >  scripts/qapi/golang.py | 97 ++++++++++++++++++++++++++++++++++++++++--
> >  1 file changed, 94 insertions(+), 3 deletions(-)
> > 
> > diff --git a/scripts/qapi/golang.py b/scripts/qapi/golang.py
> > index ff3b1dd020..52a9124641 100644
> > --- a/scripts/qapi/golang.py
> > +++ b/scripts/qapi/golang.py
> > @@ -246,6 +246,51 @@
> >  }}
> >  '''
> >  
> > +TEMPLATE_COMMAND_METHODS = '''
> > +func (c *{type_name}) GetName() string {{
> > +    return "{name}"
> > +}}
> > +
> > +func (s *{type_name}) GetId() string {{
> > +    return s.MessageId
> > +}}
> > +'''
> > +
> > +TEMPLATE_COMMAND = '''
> > +type Command interface {{
> > +    GetId()         string
> > +    GetName()       string
> > +}}
> > +
> > +func MarshalCommand(c Command) ([]byte, error) {{
> > +    m := make(map[string]any)
> > +    m["execute"] = c.GetName()
> > +    if id := c.GetId(); len(id) > 0 {{
> > +        m["id"] = id
> > +    }}
> > +    if bytes, err := json.Marshal(c); err != nil {{
> > +        return []byte{{}}, err
> > +    }} else if len(bytes) > 2 {{
> > +        m["arguments"] = c
> > +    }}
> > +    return json.Marshal(m)
> > +}}
> > +
> > +func UnmarshalCommand(data []byte) (Command, error) {{
> > +    base := struct {{
> > +        MessageId string `json:"id,omitempty"`
> > +        Name      string `json:"execute"`
> > +    }}{{}}
> > +    if err := json.Unmarshal(data, &base); err != nil {{
> > +        return nil, fmt.Errorf("Failed to decode command: %s", string(data))
> > +    }}
> > +
> > +    switch base.Name {{
> > +    {cases}
> > +    }}
> > +    return nil, errors.New("Failed to recognize command")
> > +}}
> > +'''
> >  
> >  def gen_golang(schema: QAPISchema,
> >                 output_dir: str,
> > @@ -282,7 +327,7 @@ def qapi_to_go_type_name(name: str,
> >  
> >      name += ''.join(word.title() for word in words[1:])
> >  
> > -    types = ["event"]
> > +    types = ["event", "command"]
> >      if meta in types:
> >          name = name[:-3] if name.endswith("Arg") else name
> >          name += meta.title().replace(" ", "")
> > @@ -521,6 +566,8 @@ def qapi_to_golang_struct(self: QAPISchemaGenGolangVisitor,
> >      fields, with_nullable = recursive_base(self, base)
> >      if info.defn_meta == "event":
> >          fields += f'''\tMessageTimestamp Timestamp `json:"-"`\n{fields}'''
> > +    elif info.defn_meta == "command":
> > +        fields += f'''\tMessageId string `json:"-"`\n{fields}'''
> >  
> >      if members:
> >          for member in members:
> > @@ -719,16 +766,36 @@ def generate_template_event(events: dict[str, str]) -> str:
> >  '''
> >      return TEMPLATE_EVENT.format(cases=cases)
> >  
> > +def generate_template_command(commands: dict[str, str]) -> str:
> > +    cases = ""
> > +    for name in sorted(commands):
> > +        case_type = commands[name]
> > +        cases += f'''
> > +case "{name}":
> > +    command := struct {{
> > +        Args {case_type} `json:"arguments"`
> > +    }}{{}}
> > +
> > +    if err := json.Unmarshal(data, &command); err != nil {{
> > +        return nil, fmt.Errorf("Failed to unmarshal: %s", string(data))
> > +    }}
> > +    command.Args.MessageId = base.MessageId
> > +    return &command.Args, nil
> > +'''
> > +    content = TEMPLATE_COMMAND.format(cases=cases)
> > +    return content
> > +
> >  
> >  class QAPISchemaGenGolangVisitor(QAPISchemaVisitor):
> >  
> >      def __init__(self, _: str):
> >          super().__init__()
> > -        types = ["alternate", "enum", "event", "helper", "struct", "union"]
> > +        types = ["alternate", "command", "enum", "event", "helper", "struct", "union"]
> >          self.target = {name: "" for name in types}
> >          self.objects_seen = {}
> >          self.schema = None
> >          self.events = {}
> > +        self.commands = {}
> >          self.golang_package_name = "qapi"
> >          self.accept_null_types = []
> >  
> > @@ -756,6 +823,7 @@ def visit_begin(self, schema):
> >      def visit_end(self):
> >          self.schema = None
> >          self.target["event"] += generate_template_event(self.events)
> > +        self.target["command"] += generate_template_command(self.commands)
> >  
> >      def visit_object_type(self: QAPISchemaGenGolangVisitor,
> >                            name: str,
> > @@ -853,7 +921,30 @@ def visit_command(self,
> >                        allow_oob: bool,
> >                        allow_preconfig: bool,
> >                        coroutine: bool) -> None:
> > -        pass
> > +        assert name == info.defn_name
> > +
> > +        type_name = qapi_to_go_type_name(name, info.defn_meta)
> > +        self.commands[name] = type_name
> > +
> > +        content = ""
> > +        if boxed or not arg_type or not qapi_name_is_object(arg_type.name):
> > +            args = "" if not arg_type else "\n" + arg_type.name
> > +            args += '''\n\tMessageId   string `json:"-"`'''
> > +            content = generate_struct_type(type_name, args)
> > +        else:
> > +            assert isinstance(arg_type, QAPISchemaObjectType)
> > +            content = qapi_to_golang_struct(self,
> > +                                            name,
> > +                                            arg_type.info,
> > +                                            arg_type.ifcond,
> > +                                            arg_type.features,
> > +                                            arg_type.base,
> > +                                            arg_type.members,
> > +                                            arg_type.variants)
> > +
> > +        content += TEMPLATE_COMMAND_METHODS.format(name=name,
> > +                                                   type_name=type_name)
> > +        self.target["command"] += content
> >  
> >      def visit_event(self, name, info, ifcond, features, arg_type, boxed):
> >          assert name == info.defn_name
> > -- 
> > 2.41.0
> > 
> 
> With regards,
> Daniel
> -- 
> |: https://berrange.com      -o-    https://www.flickr.com/photos/dberrange :|
> |: https://libvirt.org         -o-            https://fstop138.berrange.com :|
> |: https://entangle-photo.org    -o-    https://www.instagram.com/dberrange :|
>
Victor Toso Oct. 14, 2023, 2:26 p.m. UTC | #3
Hi,

On Thu, Sep 28, 2023 at 03:32:54PM +0100, Daniel P. Berrangé wrote:
> On Wed, Sep 27, 2023 at 01:25:42PM +0200, Victor Toso wrote:
> > This patch handles QAPI command types and generates data structures in
> > Go that decodes from QMP JSON Object to Go data structure and vice
> > versa.
> > 
> > Similar to Event, this patch adds a Command interface and two helper
> > functions MarshalCommand and UnmarshalCommand.
> > 
> > Example:
> > qapi:
> >  | { 'command': 'set_password',
> >  |   'boxed': true,
> >  |   'data': 'SetPasswordOptions' }
> > 
> > go:
> >  | type SetPasswordCommand struct {
> >  |     SetPasswordOptions
> >  |     CommandId string `json:"-"`
> 
> IIUC, you renamed that to MessageId in the code now.
> 
> >  | }
> 
> Overall, I'm not entirely convinced that we will want to
> have the SetPasswordCommand struct wrappers, byut it is
> hard to say,

I was playing with removing these embed structs now, similar to
how we did for all other base types. The extra complexity might
not be worthy to remove it, IMHO.

Actually, the SetPasswordCommand can be used as example.

No embed (proposed):
  type SetPasswordCommand struct {
      CommandId string `json:"-"`

      Password  string             `json:"password"`
      Connected *SetPasswordAction `json:"connected,omitempty"`

      Protocol  DisplayProtocol    `json:"protocol"`
      Password  string             `json:"password"`
      Connected *SetPasswordAction `json:"connected,omitempty"`

      // Variants fields
      Vnc *SetPasswordOptionsVnc `json:"-"`

      // Unbranched enum fields
      Spice bool `json:"-"`
  }

As SetPasswordOptions is a union, now we have to treat
SetPasswordCommand as union too, for Marshal/Unmarshal.

The data type could also be an Alternate, which has different
logic for Marshal/Unmarshal.

So, doing embed would add quite a bit of complexity to handling
Commands and Events too although no Event use boxed=true at the
moment in QEMU.

> as what we're missing still is the eventual application facing
> API.
> 
> eg something that ultimately looks more like this:
> 
>     qemu = qemu.QMPConnection()
>     qemu.Dial("/path/to/unix/socket.sock")
> 
>     qemu.VncConnectedEvent(func(ev *VncConnectedEvent) {
>          fmt.Printf("VNC client %s connected\n", ev.Client.Host)
>     })
> 
>     resp, err := qemu.SetPassword(SetPasswordArguments{
>         protocol: "vnc",
>         password: "123456",
>     })
> 
>     if err != nil {
>         fmt.Fprintf(os.Stderr, "Cannot set passwd: %s", err)
>     }
> 
>     ..do something wit resp....   (well SetPassword has no response, but other cmmands do)
> 
> It isn't clear that the SetPasswordCommand struct will be
> needed internally for the impl if QMPCommand.

Yes. Let's get this in so we can work on the next layer. We don't
need to declare this Go module as stable for now, till we get the
next few bits figure out.

Cheers,
Victor
 
> > usage:
> >  | input := `{"execute":"set_password",` +
> >  |          `"arguments":{"protocol":"vnc",` +
> >  |          `"password":"secret"}}`
> >  |
> >  | c, err := UnmarshalCommand([]byte(input))
> >  | if err != nil {
> >  |     panic(err)
> >  | }
> >  |
> >  | if c.GetName() == `set_password` {
> >  |         m := c.(*SetPasswordCommand)
> >  |         // m.Password == "secret"
> >  | }
> > 
> > Signed-off-by: Victor Toso <victortoso@redhat.com>
> > ---
> >  scripts/qapi/golang.py | 97 ++++++++++++++++++++++++++++++++++++++++--
> >  1 file changed, 94 insertions(+), 3 deletions(-)
> > 
> > diff --git a/scripts/qapi/golang.py b/scripts/qapi/golang.py
> > index ff3b1dd020..52a9124641 100644
> > --- a/scripts/qapi/golang.py
> > +++ b/scripts/qapi/golang.py
> > @@ -246,6 +246,51 @@
> >  }}
> >  '''
> >  
> > +TEMPLATE_COMMAND_METHODS = '''
> > +func (c *{type_name}) GetName() string {{
> > +    return "{name}"
> > +}}
> > +
> > +func (s *{type_name}) GetId() string {{
> > +    return s.MessageId
> > +}}
> > +'''
> > +
> > +TEMPLATE_COMMAND = '''
> > +type Command interface {{
> > +    GetId()         string
> > +    GetName()       string
> > +}}
> > +
> > +func MarshalCommand(c Command) ([]byte, error) {{
> > +    m := make(map[string]any)
> > +    m["execute"] = c.GetName()
> > +    if id := c.GetId(); len(id) > 0 {{
> > +        m["id"] = id
> > +    }}
> > +    if bytes, err := json.Marshal(c); err != nil {{
> > +        return []byte{{}}, err
> > +    }} else if len(bytes) > 2 {{
> > +        m["arguments"] = c
> > +    }}
> > +    return json.Marshal(m)
> > +}}
> > +
> > +func UnmarshalCommand(data []byte) (Command, error) {{
> > +    base := struct {{
> > +        MessageId string `json:"id,omitempty"`
> > +        Name      string `json:"execute"`
> > +    }}{{}}
> > +    if err := json.Unmarshal(data, &base); err != nil {{
> > +        return nil, fmt.Errorf("Failed to decode command: %s", string(data))
> > +    }}
> > +
> > +    switch base.Name {{
> > +    {cases}
> > +    }}
> > +    return nil, errors.New("Failed to recognize command")
> > +}}
> > +'''
> >  
> >  def gen_golang(schema: QAPISchema,
> >                 output_dir: str,
> > @@ -282,7 +327,7 @@ def qapi_to_go_type_name(name: str,
> >  
> >      name += ''.join(word.title() for word in words[1:])
> >  
> > -    types = ["event"]
> > +    types = ["event", "command"]
> >      if meta in types:
> >          name = name[:-3] if name.endswith("Arg") else name
> >          name += meta.title().replace(" ", "")
> > @@ -521,6 +566,8 @@ def qapi_to_golang_struct(self: QAPISchemaGenGolangVisitor,
> >      fields, with_nullable = recursive_base(self, base)
> >      if info.defn_meta == "event":
> >          fields += f'''\tMessageTimestamp Timestamp `json:"-"`\n{fields}'''
> > +    elif info.defn_meta == "command":
> > +        fields += f'''\tMessageId string `json:"-"`\n{fields}'''
> >  
> >      if members:
> >          for member in members:
> > @@ -719,16 +766,36 @@ def generate_template_event(events: dict[str, str]) -> str:
> >  '''
> >      return TEMPLATE_EVENT.format(cases=cases)
> >  
> > +def generate_template_command(commands: dict[str, str]) -> str:
> > +    cases = ""
> > +    for name in sorted(commands):
> > +        case_type = commands[name]
> > +        cases += f'''
> > +case "{name}":
> > +    command := struct {{
> > +        Args {case_type} `json:"arguments"`
> > +    }}{{}}
> > +
> > +    if err := json.Unmarshal(data, &command); err != nil {{
> > +        return nil, fmt.Errorf("Failed to unmarshal: %s", string(data))
> > +    }}
> > +    command.Args.MessageId = base.MessageId
> > +    return &command.Args, nil
> > +'''
> > +    content = TEMPLATE_COMMAND.format(cases=cases)
> > +    return content
> > +
> >  
> >  class QAPISchemaGenGolangVisitor(QAPISchemaVisitor):
> >  
> >      def __init__(self, _: str):
> >          super().__init__()
> > -        types = ["alternate", "enum", "event", "helper", "struct", "union"]
> > +        types = ["alternate", "command", "enum", "event", "helper", "struct", "union"]
> >          self.target = {name: "" for name in types}
> >          self.objects_seen = {}
> >          self.schema = None
> >          self.events = {}
> > +        self.commands = {}
> >          self.golang_package_name = "qapi"
> >          self.accept_null_types = []
> >  
> > @@ -756,6 +823,7 @@ def visit_begin(self, schema):
> >      def visit_end(self):
> >          self.schema = None
> >          self.target["event"] += generate_template_event(self.events)
> > +        self.target["command"] += generate_template_command(self.commands)
> >  
> >      def visit_object_type(self: QAPISchemaGenGolangVisitor,
> >                            name: str,
> > @@ -853,7 +921,30 @@ def visit_command(self,
> >                        allow_oob: bool,
> >                        allow_preconfig: bool,
> >                        coroutine: bool) -> None:
> > -        pass
> > +        assert name == info.defn_name
> > +
> > +        type_name = qapi_to_go_type_name(name, info.defn_meta)
> > +        self.commands[name] = type_name
> > +
> > +        content = ""
> > +        if boxed or not arg_type or not qapi_name_is_object(arg_type.name):
> > +            args = "" if not arg_type else "\n" + arg_type.name
> > +            args += '''\n\tMessageId   string `json:"-"`'''
> > +            content = generate_struct_type(type_name, args)
> > +        else:
> > +            assert isinstance(arg_type, QAPISchemaObjectType)
> > +            content = qapi_to_golang_struct(self,
> > +                                            name,
> > +                                            arg_type.info,
> > +                                            arg_type.ifcond,
> > +                                            arg_type.features,
> > +                                            arg_type.base,
> > +                                            arg_type.members,
> > +                                            arg_type.variants)
> > +
> > +        content += TEMPLATE_COMMAND_METHODS.format(name=name,
> > +                                                   type_name=type_name)
> > +        self.target["command"] += content
> >  
> >      def visit_event(self, name, info, ifcond, features, arg_type, boxed):
> >          assert name == info.defn_name
> > -- 
> > 2.41.0
> > 
> 
> With regards,
> Daniel
> -- 
> |: https://berrange.com      -o-    https://www.flickr.com/photos/dberrange :|
> |: https://libvirt.org         -o-            https://fstop138.berrange.com :|
> |: https://entangle-photo.org    -o-    https://www.instagram.com/dberrange :|
>
diff mbox series

Patch

diff --git a/scripts/qapi/golang.py b/scripts/qapi/golang.py
index ff3b1dd020..52a9124641 100644
--- a/scripts/qapi/golang.py
+++ b/scripts/qapi/golang.py
@@ -246,6 +246,51 @@ 
 }}
 '''
 
+TEMPLATE_COMMAND_METHODS = '''
+func (c *{type_name}) GetName() string {{
+    return "{name}"
+}}
+
+func (s *{type_name}) GetId() string {{
+    return s.MessageId
+}}
+'''
+
+TEMPLATE_COMMAND = '''
+type Command interface {{
+    GetId()         string
+    GetName()       string
+}}
+
+func MarshalCommand(c Command) ([]byte, error) {{
+    m := make(map[string]any)
+    m["execute"] = c.GetName()
+    if id := c.GetId(); len(id) > 0 {{
+        m["id"] = id
+    }}
+    if bytes, err := json.Marshal(c); err != nil {{
+        return []byte{{}}, err
+    }} else if len(bytes) > 2 {{
+        m["arguments"] = c
+    }}
+    return json.Marshal(m)
+}}
+
+func UnmarshalCommand(data []byte) (Command, error) {{
+    base := struct {{
+        MessageId string `json:"id,omitempty"`
+        Name      string `json:"execute"`
+    }}{{}}
+    if err := json.Unmarshal(data, &base); err != nil {{
+        return nil, fmt.Errorf("Failed to decode command: %s", string(data))
+    }}
+
+    switch base.Name {{
+    {cases}
+    }}
+    return nil, errors.New("Failed to recognize command")
+}}
+'''
 
 def gen_golang(schema: QAPISchema,
                output_dir: str,
@@ -282,7 +327,7 @@  def qapi_to_go_type_name(name: str,
 
     name += ''.join(word.title() for word in words[1:])
 
-    types = ["event"]
+    types = ["event", "command"]
     if meta in types:
         name = name[:-3] if name.endswith("Arg") else name
         name += meta.title().replace(" ", "")
@@ -521,6 +566,8 @@  def qapi_to_golang_struct(self: QAPISchemaGenGolangVisitor,
     fields, with_nullable = recursive_base(self, base)
     if info.defn_meta == "event":
         fields += f'''\tMessageTimestamp Timestamp `json:"-"`\n{fields}'''
+    elif info.defn_meta == "command":
+        fields += f'''\tMessageId string `json:"-"`\n{fields}'''
 
     if members:
         for member in members:
@@ -719,16 +766,36 @@  def generate_template_event(events: dict[str, str]) -> str:
 '''
     return TEMPLATE_EVENT.format(cases=cases)
 
+def generate_template_command(commands: dict[str, str]) -> str:
+    cases = ""
+    for name in sorted(commands):
+        case_type = commands[name]
+        cases += f'''
+case "{name}":
+    command := struct {{
+        Args {case_type} `json:"arguments"`
+    }}{{}}
+
+    if err := json.Unmarshal(data, &command); err != nil {{
+        return nil, fmt.Errorf("Failed to unmarshal: %s", string(data))
+    }}
+    command.Args.MessageId = base.MessageId
+    return &command.Args, nil
+'''
+    content = TEMPLATE_COMMAND.format(cases=cases)
+    return content
+
 
 class QAPISchemaGenGolangVisitor(QAPISchemaVisitor):
 
     def __init__(self, _: str):
         super().__init__()
-        types = ["alternate", "enum", "event", "helper", "struct", "union"]
+        types = ["alternate", "command", "enum", "event", "helper", "struct", "union"]
         self.target = {name: "" for name in types}
         self.objects_seen = {}
         self.schema = None
         self.events = {}
+        self.commands = {}
         self.golang_package_name = "qapi"
         self.accept_null_types = []
 
@@ -756,6 +823,7 @@  def visit_begin(self, schema):
     def visit_end(self):
         self.schema = None
         self.target["event"] += generate_template_event(self.events)
+        self.target["command"] += generate_template_command(self.commands)
 
     def visit_object_type(self: QAPISchemaGenGolangVisitor,
                           name: str,
@@ -853,7 +921,30 @@  def visit_command(self,
                       allow_oob: bool,
                       allow_preconfig: bool,
                       coroutine: bool) -> None:
-        pass
+        assert name == info.defn_name
+
+        type_name = qapi_to_go_type_name(name, info.defn_meta)
+        self.commands[name] = type_name
+
+        content = ""
+        if boxed or not arg_type or not qapi_name_is_object(arg_type.name):
+            args = "" if not arg_type else "\n" + arg_type.name
+            args += '''\n\tMessageId   string `json:"-"`'''
+            content = generate_struct_type(type_name, args)
+        else:
+            assert isinstance(arg_type, QAPISchemaObjectType)
+            content = qapi_to_golang_struct(self,
+                                            name,
+                                            arg_type.info,
+                                            arg_type.ifcond,
+                                            arg_type.features,
+                                            arg_type.base,
+                                            arg_type.members,
+                                            arg_type.variants)
+
+        content += TEMPLATE_COMMAND_METHODS.format(name=name,
+                                                   type_name=type_name)
+        self.target["command"] += content
 
     def visit_event(self, name, info, ifcond, features, arg_type, boxed):
         assert name == info.defn_name