[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]
[PATCH v3 7/8] qapi: golang: Generate command type
From: |
Victor Toso |
Subject: |
[PATCH v3 7/8] qapi: golang: Generate command type |
Date: |
Fri, 10 Jan 2025 11:49:45 +0100 |
This patch handles QAPI command types and generates data structures in
Go that handles it.
This patch also generates the Command's return type. Each command has a
specific type for its expected return value.
1. Command:
i. Naming: Every command type has a Command suffix.
ii. Id: Every command has a MessageId field of string type.
iii. Every command implements the Command interface.
iv. The Command interface includes GetReturnType() which returns the
expected return type for that Command
2. CommandReturn:
i. Naming: Every command return type has a CommandReturn suffix
ii. Id: Every command return has a MessageId field of string type.
iii. Every command return implements the CommandReturn interface.
* Example:
qapi:
| ##
| # @set_password:
| #
| # Set the password of a remote display server.
| #
| # Errors:
| # - If Spice is not enabled, DeviceNotFound
| #
| # Since: 0.14
| #
| # .. qmp-example::
| #
| # -> { "execute": "set_password", "arguments": { "protocol": "vnc",
| # "password": "secret" } }
| # <- { "return": {} }
| ##
| { 'command': 'set_password', 'boxed': true, 'data': 'SetPasswordOptions' }
go:
| // Set the password of a remote display server.
| //
| // Errors: - If Spice is not enabled, DeviceNotFound
| //
| // Since: 0.14
| //
| // .. qmp-example:: -> { "execute": "set_password", "arguments": {
| // "protocol": "vnc", "password": "secret" }
| // } <- { "return": {} }
| type SetPasswordCommand struct {
| SetPasswordOptions
| MessageId string `json:"-"`
| }
|
| type SetPasswordCommandReturn struct {
| MessageId string `json:"id,omitempty"`
| Error *QAPIError `json:"error,omitempty"`
| }
usage:
| input := `{"execute":"set_password",` +
| `"arguments":{"protocol":"vnc",` +
| `"password":"secret"}}`
|
| // Straight forward if you know the event type
| {
| c := SetPasswordCommand{}
| err := json.Unmarshal([]byte(input), &c)
| if err != nil {
| panic(err)
| }
| // c.Password == "secret"
| }
|
| // Generic way, using Command interface and helper function
| if cmd, err := GetCommandType(input); err != nil {
| // handle bad data or unknown event
| }
|
| if err := json.Unmarshal(input, cmd); err != nil {
| // handle bad data or unknown event fields
| }
|
| if c, ok := cmd.(*SetPasswordCommand); ok {
| // c.Password == "secret"
| }
Signed-off-by: Victor Toso <victortoso@redhat.com>
---
scripts/qapi/golang.py | 233 ++++++++++++++++++++++++++++++++++++++++-
1 file changed, 231 insertions(+), 2 deletions(-)
diff --git a/scripts/qapi/golang.py b/scripts/qapi/golang.py
index 6a8f5cf230..085cdd89f6 100644
--- a/scripts/qapi/golang.py
+++ b/scripts/qapi/golang.py
@@ -43,6 +43,15 @@
"""
TEMPLATE_HELPER = """
+type QAPIError struct {
+ Class string `json:"class"`
+ Description string `json:"desc"`
+}
+
+func (err *QAPIError) Error() string {
+ return err.Description
+}
+
// Creates a decoder that errors on unknown Fields
// Returns nil if successfully decoded @from payload to @into type
// Returns error if failed to decode @from payload to @into type
@@ -305,6 +314,111 @@
}}
"""
+TEMPLATE_COMMAND_METHODS = """
+func (s {type_name}) MarshalJSON() ([]byte, error) {{
+ type Alias {type_name}
+ return marshalCommand(Alias(s), "{name}", s.MessageId)
+}}
+
+func (s *{type_name}) UnmarshalJSON(data []byte) error {{
+ type Alias {type_name}
+ tmp := struct {{
+ MessageId string `json:"id,omitempty"`
+ Name string `json:"execute"`
+ Args Alias `json:"arguments"`
+ }}{{}}
+
+ if err := json.Unmarshal(data, &tmp); err != nil {{
+ return fmt.Errorf("Failed to unmarshal: %s", string(data))
+ }}
+
+ if !strings.EqualFold(tmp.Name, "{name}") {{
+ return fmt.Errorf("Command type does not match with %s", tmp.Name)
+ }}
+
+ *s = {type_name}(tmp.Args)
+ s.MessageId = tmp.MessageId
+ return nil
+}}
+
+func (s *{type_name}) GetReturnType() CommandReturn {{
+ return &{cmd_ret_type_name}{{}}
+}}
+"""
+
+TEMPLATE_COMMAND = """
+type Command interface {{
+ json.Marshaler
+ json.Unmarshaler
+ GetReturnType() CommandReturn
+}}
+
+func marshalCommand(obj interface{{}}, name, id string) ([]byte, error) {{
+ m := make(map[string]any)
+ m["execute"] = name
+ if len(id) > 0 {{
+ m["id"] = id
+ }}
+ if bytes, err := json.Marshal(obj); err != nil {{
+ return []byte{{}}, err
+ }} else if len(bytes) > 2 {{
+ m["arguments"] = obj
+ }}
+ return json.Marshal(m)
+}}
+
+func GetCommandType(data []byte) (Command, error) {{
+ tmp := struct {{
+ Name string `json:"execute"`
+ }}{{}}
+
+ if err := json.Unmarshal(data, &tmp); err != nil {{
+ return nil, fmt.Errorf("Failed to decode command: %s", string(data))
+ }}
+
+ switch tmp.Name {{{cases}
+ }}
+ return nil, errors.New("Failed to recognize command")
+}}
+"""
+
+TEMPLATE_COMMAND_RETURN = """
+type CommandReturn interface {
+ json.Marshaler
+}
+
+func marshalCommandReturn(result, qerror any, id string) ([]byte, error) {
+ m := make(map[string]any)
+ if len(id) > 0 {
+ m["id"] = id
+ }
+ if qerror != nil && qerror.(*QAPIError) != nil {
+ m["error"] = qerror
+ } else if result != nil {
+ m["return"] = result
+ } else {
+ m["return"] = struct{}{}
+ }
+ return json.Marshal(m)
+}
+"""
+
+TEMPLATE_COMMAND_RETURN_METHODS = """
+func (r {cmd_ret_type_name}) MarshalJSON() ([]byte, error) {{
+ return marshalCommandReturn({cmd_ret_field}, r.Error, r.MessageId)
+}}
+"""
+
+TEMPLATE_COMMAND_RETURN_MARSHAL_EMPTY = """
+func (r {cmd_ret_ype_name}) MarshalJSON() ([]byte, error) {{
+ if r.Error != nil {{
+ type Alias {cmd_ret_type_name}
+ return json.Marshal(Alias(r))
+ }}
+ return []byte(`{{"return":{{}}}}`), nil
+}}
+"""
+
# Takes the documentation object of a specific type and returns
# that type's documentation and its member's docs.
@@ -386,7 +500,7 @@ def qapi_to_go_type_name(name: str, meta: Optional[str] =
None) -> str:
name += "".join(word.title() for word in words[1:])
# Handle specific meta suffix
- types = ["event"]
+ types = ["event", "command", "command return"]
if meta in types:
name = name[:-3] if name.endswith("Arg") else name
name += meta.title().replace(" ", "")
@@ -855,6 +969,10 @@ def qapi_to_golang_struct(
"tag": """`json:"-"`""",
},
)
+ elif info.defn_meta == "command":
+ fields.insert(
+ 0, {"name": "MessageId", "type": "string", "tag": """`json:"-"`"""}
+ )
if members:
for member in members:
@@ -1089,6 +1207,21 @@ def generate_template_alternate(
return "\n" + content
+def generate_template_command(commands: dict[str, Tuple[str, str]]) -> str:
+ cases = ""
+ content = ""
+ for name in sorted(commands):
+ type_name, gocode = commands[name]
+ content += gocode
+ cases += f"""
+ case "{name}":
+ return &{type_name}{{}}, nil
+"""
+ content += string_to_code(TEMPLATE_COMMAND.format(cases=cases))
+ content += string_to_code(TEMPLATE_COMMAND_RETURN)
+ return content
+
+
def generate_template_event(events: dict[str, Tuple[str, str]]) -> str:
content = ""
cases = ""
@@ -1138,6 +1271,7 @@ def __init__(self, _: str):
super().__init__()
types = (
"alternate",
+ "command",
"enum",
"event",
"helper",
@@ -1147,6 +1281,7 @@ def __init__(self, _: str):
self.target = dict.fromkeys(types, "")
self.schema: QAPISchema
self.events: dict[str, Tuple[str, str]] = {}
+ self.commands: dict[str, Tuple[str, str]] = {}
self.golang_package_name = "qapi"
self.enums: dict[str, str] = {}
self.alternates: dict[str, str] = {}
@@ -1192,6 +1327,15 @@ def visit_begin(self, schema: QAPISchema) -> None:
"fmt"
"strings"
)
+"""
+ elif target == "command":
+ imports += """
+import (
+ "encoding/json"
+ "errors"
+ "fmt"
+ "strings"
+)
"""
else:
imports += """
@@ -1214,6 +1358,7 @@ def visit_end(self) -> None:
self.target["struct"] += generate_content_from_dict(self.structs)
self.target["union"] += generate_content_from_dict(self.unions)
self.target["event"] += generate_template_event(self.events)
+ self.target["command"] += generate_template_command(self.commands)
def visit_object_type(
self,
@@ -1358,7 +1503,91 @@ def visit_command(
allow_preconfig: bool,
coroutine: bool,
) -> None:
- pass
+ assert name == info.defn_name
+ assert name not in self.commands
+
+ type_name = qapi_to_go_type_name(name, info.defn_meta)
+
+ doc = self.docmap.get(name, None)
+ type_doc, _ = qapi_to_golang_struct_docs(doc)
+
+ cmd_ret_type_name = qapi_to_go_type_name(name, "command return")
+ cmd_ret_field = "nil"
+ retargs: List[dict[str:str]] = [
+ {
+ "name": "MessageId",
+ "type": "string",
+ "tag": """`json:"id,omitempty"`""",
+ },
+ {
+ "name": "Error",
+ "type": "*QAPIError",
+ "tag": """`json:"error,omitempty"`""",
+ },
+ ]
+ if ret_type:
+ cmd_ret_field = "r.Result"
+ ret_type_name = qapi_schema_type_to_go_type(ret_type.name)
+ isptr = "*" if ret_type_name[0] not in "*[" else ""
+ retargs.append(
+ {
+ "name": "Result",
+ "type": f"{isptr}{ret_type_name}",
+ "tag": """`json:"return"`""",
+ }
+ )
+
+ content = ""
+ if boxed or not arg_type or not qapi_name_is_object(arg_type.name):
+ args: List[dict[str:str]] = []
+ if arg_type:
+ args.append(
+ {
+ "name": f"{arg_type.name}",
+ }
+ )
+ args.append(
+ {
+ "name": "MessageId",
+ "type": "string",
+ "tag": """`json:"-"`""",
+ }
+ )
+ content += string_to_code(
+ generate_struct_type(type_name, type_doc=type_doc, args=args)
+ )
+ else:
+ assert isinstance(arg_type, QAPISchemaObjectType)
+ content += string_to_code(
+ qapi_to_golang_struct(
+ self,
+ name,
+ arg_type.info,
+ arg_type.ifcond,
+ arg_type.features,
+ arg_type.base,
+ arg_type.members,
+ arg_type.branches,
+ )
+ )
+
+ content += string_to_code(
+ TEMPLATE_COMMAND_METHODS.format(
+ name=name,
+ type_name=type_name,
+ cmd_ret_type_name=cmd_ret_type_name,
+ )
+ )
+ content += string_to_code(
+ generate_struct_type(cmd_ret_type_name, args=retargs)
+ )
+ content += string_to_code(
+ TEMPLATE_COMMAND_RETURN_METHODS.format(
+ cmd_ret_type_name=cmd_ret_type_name,
+ cmd_ret_field=cmd_ret_field,
+ )
+ )
+ self.commands[name] = (type_name, content)
def visit_event(
self,
--
2.47.1
- [PATCH v3 0/8] qapi-go: add generator for Golang interfaces, Victor Toso, 2025/01/10
- [PATCH v3 1/8] qapi: golang: Generate enum type, Victor Toso, 2025/01/10
- [PATCH v3 2/8] qapi: golang: Generate alternate types, Victor Toso, 2025/01/10
- [PATCH v3 3/8] qapi: golang: Generate struct types, Victor Toso, 2025/01/10
- [PATCH v3 4/8] qapi: golang: structs: Address nullable members, Victor Toso, 2025/01/10
- [PATCH v3 5/8] qapi: golang: Generate union type, Victor Toso, 2025/01/10
- [PATCH v3 6/8] qapi: golang: Generate event type, Victor Toso, 2025/01/10
- [PATCH v3 7/8] qapi: golang: Generate command type,
Victor Toso <=
- [PATCH v3 8/8] docs: add notes on Golang code generator, Victor Toso, 2025/01/10