From: Victor Toso <victortoso@redhat.com>
To: qemu-devel@nongnu.org
Cc: "Markus Armbruster" <armbru@redhat.com>,
"John Snow" <jsnow@redhat.com>,
"Daniel P . Berrangé" <berrange@redhat.com>,
"Andrea Bolognani" <abologna@redhat.com>
Subject: [PATCH v2 04/11] qapi: golang: Generate qapi's alternate types in Go
Date: Mon, 16 Oct 2023 17:26:57 +0200 [thread overview]
Message-ID: <20231016152704.221611-5-victortoso@redhat.com> (raw)
In-Reply-To: <20231016152704.221611-1-victortoso@redhat.com>
This patch handles QAPI alternate types and generates data structures
in Go that handles it.
Alternate types are similar to Union but without a discriminator that
can be used to identify the underlying value on the wire. It is needed
to infer it. In Go, most of the types [*] are mapped as optional
fields and Marshal and Unmarshal methods will be handling the data
checks.
Example:
qapi:
| { 'alternate': 'BlockdevRef',
| 'data': { 'definition': 'BlockdevOptions',
| 'reference': 'str' } }
go:
| type BlockdevRef struct {
| Definition *BlockdevOptions
| Reference *string
| }
usage:
| input := `{"driver":"qcow2","data-file":"/some/place/my-image"}`
| k := BlockdevRef{}
| err := json.Unmarshal([]byte(input), &k)
| if err != nil {
| panic(err)
| }
| // *k.Definition.Qcow2.DataFile.Reference == "/some/place/my-image"
[*] The exception for optional fields as default is to Types that can
accept JSON Null as a value. For this case, we translate NULL to a
member type called IsNull, which is boolean in Go. This will be
explained better in the documentation patch of this series but the
main rationale is around Marshaling to and from JSON and Go data
structures.
Example:
qapi:
| { 'alternate': 'StrOrNull',
| 'data': { 's': 'str',
| 'n': 'null' } }
go:
| type StrOrNull struct {
| S *string
| IsNull bool
| }
Signed-off-by: Victor Toso <victortoso@redhat.com>
---
scripts/qapi/golang.py | 301 ++++++++++++++++++++++++++++++++++++++++-
1 file changed, 298 insertions(+), 3 deletions(-)
diff --git a/scripts/qapi/golang.py b/scripts/qapi/golang.py
index dc12be7b03..3f6692df4b 100644
--- a/scripts/qapi/golang.py
+++ b/scripts/qapi/golang.py
@@ -13,10 +13,11 @@
from __future__ import annotations
import os
-from typing import List, Optional
+from typing import List, Optional, Tuple
from .schema import (
QAPISchema,
+ QAPISchemaAlternateType,
QAPISchemaEnumMember,
QAPISchemaFeature,
QAPISchemaIfCond,
@@ -37,6 +38,77 @@
)
"""
+TEMPLATE_HELPER = """
+// 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
+func StrictDecode(into interface{}, from []byte) error {
+\tdec := json.NewDecoder(strings.NewReader(string(from)))
+\tdec.DisallowUnknownFields()
+
+\tif err := dec.Decode(into); err != nil {
+\t\treturn err
+\t}
+\treturn nil
+}
+"""
+
+TEMPLATE_ALTERNATE = """
+// Only implemented on Alternate types that can take JSON NULL as value.
+//
+// This is a helper for the marshalling code. It should return true only when
+// the Alternate is empty (no members are set), otherwise it returns false and
+// the member set to be Marshalled.
+type AbsentAlternate interface {
+\tToAnyOrAbsent() (any, bool)
+}
+"""
+
+TEMPLATE_ALTERNATE_NULLABLE_CHECK = """
+\t\t}} else if s.{var_name} != nil {{
+\t\t\treturn *s.{var_name}, false"""
+
+TEMPLATE_ALTERNATE_MARSHAL_CHECK = """
+\tif s.{var_name} != nil {{
+\t\treturn json.Marshal(s.{var_name})
+\t}} else """
+
+TEMPLATE_ALTERNATE_UNMARSHAL_CHECK = """
+\t// Check for {var_type}
+\t{{
+\t\ts.{var_name} = new({var_type})
+\t\tif err := StrictDecode(s.{var_name}, data); err == nil {{
+\t\t\treturn nil
+\t\t}}
+\t\ts.{var_name} = nil
+\t}}
+"""
+
+TEMPLATE_ALTERNATE_NULLABLE = """
+func (s *{name}) ToAnyOrAbsent() (any, bool) {{
+\tif s != nil {{
+\t\tif s.IsNull {{
+\t\t\treturn nil, false
+{absent_check_fields}
+\t\t}}
+\t}}
+
+\treturn nil, true
+}}
+"""
+
+TEMPLATE_ALTERNATE_METHODS = """
+func (s {name}) MarshalJSON() ([]byte, error) {{
+\t{marshal_check_fields}
+\treturn {marshal_return_default}
+}}
+
+func (s *{name}) UnmarshalJSON(data []byte) error {{
+{unmarshal_check_fields}
+\treturn fmt.Errorf("Can't convert to {name}: %s", string(data))
+}}
+"""
+
def gen_golang(schema: QAPISchema, output_dir: str, prefix: str) -> None:
vis = QAPISchemaGenGolangVisitor(prefix)
@@ -44,10 +116,191 @@ def gen_golang(schema: QAPISchema, output_dir: str, prefix: str) -> None:
vis.write(output_dir)
+def qapi_to_field_name(name: str) -> str:
+ return name.title().replace("_", "").replace("-", "")
+
+
def qapi_to_field_name_enum(name: str) -> str:
return name.title().replace("-", "")
+def qapi_schema_type_to_go_type(qapitype: str) -> str:
+ schema_types_to_go = {
+ "str": "string",
+ "null": "nil",
+ "bool": "bool",
+ "number": "float64",
+ "size": "uint64",
+ "int": "int64",
+ "int8": "int8",
+ "int16": "int16",
+ "int32": "int32",
+ "int64": "int64",
+ "uint8": "uint8",
+ "uint16": "uint16",
+ "uint32": "uint32",
+ "uint64": "uint64",
+ "any": "any",
+ "QType": "QType",
+ }
+
+ prefix = ""
+ if qapitype.endswith("List"):
+ prefix = "[]"
+ qapitype = qapitype[:-4]
+
+ qapitype = schema_types_to_go.get(qapitype, qapitype)
+ return prefix + qapitype
+
+
+def qapi_field_to_go_field(
+ member_name: str, type_name: str
+) -> Tuple[str, str, str]:
+ # Nothing to generate on null types. We update some
+ # variables to handle json-null on marshalling methods.
+ if type_name == "null":
+ return "IsNull", "bool", ""
+
+ # This function is called on Alternate, so fields should be ptrs
+ return (
+ qapi_to_field_name(member_name),
+ qapi_schema_type_to_go_type(type_name),
+ "*",
+ )
+
+
+# Helper function for boxed or self contained structures.
+def generate_struct_type(
+ type_name, args: List[dict[str:str]] = None, ident: int = 0
+) -> str:
+ members = "{}"
+ base_ident = "\t" * ident
+ if args is not None:
+ # Most of the logic below is to mimic the gofmt tool.
+ # We calculate spaces between member and type and between
+ # the type and tag. Note that gofmt considers comments as
+ # divider between ident blocks.
+ maxname, maxtype = 0, 0
+ blocks: tuple(int, int) = []
+ for arg in args:
+ if "comment" in arg:
+ blocks.append((maxname, maxtype))
+ maxname, maxtype = 0, 0
+ continue
+
+ if "type" not in arg:
+ continue
+
+ maxname = max(maxname, len(arg["name"]))
+ maxtype = max(maxtype, len(arg["type"]))
+
+ blocks.append((maxname, maxtype))
+ block = 0
+
+ maxname, maxtype = blocks[0]
+ members = " {\n"
+ for arg in args:
+ if "comment" in arg:
+ block += 1
+ maxname, maxtype = blocks[block]
+ members += f"""\t// {arg["comment"]}\n"""
+ continue
+
+ name2type = ""
+ if "type" in arg:
+ name2type = " " * (maxname - len(arg["name"]) + 1)
+ type2tag = ""
+ if "tag" in arg:
+ type2tag = " " * (maxtype - len(arg["type"]) + 1)
+
+ fident = "\t" * (ident + 1)
+ gotype = "" if "type" not in arg else arg["type"]
+ tag = "" if "tag" not in arg else arg["tag"]
+ name = arg["name"]
+ members += (
+ f"""{fident}{name}{name2type}{gotype}{type2tag}{tag}\n"""
+ )
+ members += f"{base_ident}}}\n"
+
+ with_type = f"\n{base_ident}type {type_name}" if len(type_name) > 0 else ""
+ return f"""{with_type} struct{members}"""
+
+
+def generate_template_alternate(
+ self: QAPISchemaGenGolangVisitor,
+ name: str,
+ variants: Optional[QAPISchemaVariants],
+) -> str:
+ absent_check_fields = ""
+ args: List[dict[str:str]] = []
+ # to avoid having to check accept_null_types
+ nullable = False
+ if name in self.accept_null_types:
+ # In QEMU QAPI schema, only StrOrNull and BlockdevRefOrNull.
+ nullable = True
+ marshal_return_default = """[]byte("{}"), nil"""
+ marshal_check_fields = """if s.IsNull {
+\t\treturn []byte("null"), nil
+\t} else """
+ unmarshal_check_fields = """
+\t// Check for json-null first
+\tif string(data) == "null" {
+\t\ts.IsNull = true
+\t\treturn nil
+\t}"""
+ else:
+ marshal_return_default = f'nil, errors.New("{name} has empty fields")'
+ marshal_check_fields = ""
+ unmarshal_check_fields = f"""
+\t// Check for json-null first
+\tif string(data) == "null" {{
+\t\treturn errors.New(`null not supported for {name}`)
+\t}}"""
+
+ if variants:
+ for var in variants.variants:
+ var_name, var_type, isptr = qapi_field_to_go_field(
+ var.name, var.type.name
+ )
+ args.append(
+ {
+ "name": f"{var_name}",
+ "type": f"{isptr}{var_type}",
+ }
+ )
+
+ # Null is special, handled first
+ if var.type.name == "null":
+ assert nullable
+ continue
+
+ if nullable:
+ absent_check_fields += (
+ TEMPLATE_ALTERNATE_NULLABLE_CHECK.format(var_name=var_name)
+ )
+ marshal_check_fields += TEMPLATE_ALTERNATE_MARSHAL_CHECK[
+ 2:
+ ].format(var_name=var_name)
+ unmarshal_check_fields += (
+ TEMPLATE_ALTERNATE_UNMARSHAL_CHECK.format(
+ var_name=var_name, var_type=var_type
+ )
+ )
+
+ content = generate_struct_type(name, args)
+ if nullable:
+ content += TEMPLATE_ALTERNATE_NULLABLE.format(
+ name=name, absent_check_fields=absent_check_fields
+ )
+ content += TEMPLATE_ALTERNATE_METHODS.format(
+ name=name,
+ marshal_check_fields=marshal_check_fields[:-6],
+ marshal_return_default=marshal_return_default,
+ unmarshal_check_fields=unmarshal_check_fields[1:],
+ )
+ return content
+
+
def generate_content_from_dict(data: dict[str, str]) -> str:
content = ""
@@ -61,22 +314,60 @@ class QAPISchemaGenGolangVisitor(QAPISchemaVisitor):
# pylint: disable=too-many-arguments
def __init__(self, _: str):
super().__init__()
- types = ("enum",)
+ types = (
+ "alternate",
+ "enum",
+ "helper",
+ )
self.target = dict.fromkeys(types, "")
self.schema: QAPISchema
self.golang_package_name = "qapi"
self.enums: dict[str, str] = {}
+ self.alternates: dict[str, str] = {}
+ self.accept_null_types = []
def visit_begin(self, schema: QAPISchema) -> None:
self.schema = schema
+ # We need to be aware of any types that accept JSON NULL
+ for name, entity in self.schema._entity_dict.items():
+ if not isinstance(entity, QAPISchemaAlternateType):
+ # Assume that only Alternate types accept JSON NULL
+ continue
+
+ for var in entity.variants.variants:
+ if var.type.name == "null":
+ self.accept_null_types.append(name)
+ break
+
# Every Go file needs to reference its package name
+ # and most have some imports too.
for target in self.target:
self.target[target] = f"package {self.golang_package_name}\n"
+ if target == "helper":
+ imports = """\nimport (
+\t"encoding/json"
+\t"strings"
+)
+"""
+ else:
+ imports = """\nimport (
+\t"encoding/json"
+\t"errors"
+\t"fmt"
+)
+"""
+ if target != "enum":
+ self.target[target] += imports
+
+ self.target["helper"] += TEMPLATE_HELPER
+ self.target["alternate"] += TEMPLATE_ALTERNATE
+
def visit_end(self) -> None:
del self.schema
self.target["enum"] += generate_content_from_dict(self.enums)
+ self.target["alternate"] += generate_content_from_dict(self.alternates)
def visit_object_type(
self,
@@ -98,7 +389,11 @@ def visit_alternate_type(
features: List[QAPISchemaFeature],
variants: QAPISchemaVariants,
) -> None:
- pass
+ assert name not in self.alternates
+
+ self.alternates[name] = generate_template_alternate(
+ self, name, variants
+ )
def visit_enum_type(
self,
--
2.41.0
next prev parent reply other threads:[~2023-10-16 15:27 UTC|newest]
Thread overview: 43+ messages / expand[flat|nested] mbox.gz Atom feed top
2023-10-16 15:26 [PATCH v2 00/11] qapi-go: add generator for Golang interface Victor Toso
2023-10-16 15:26 ` [PATCH v2 01/11] qapi: re-establish linting baseline Victor Toso
2023-10-16 15:26 ` [PATCH v2 02/11] scripts: qapi: black format main.py Victor Toso
2023-10-18 11:00 ` Markus Armbruster
2023-10-18 11:13 ` Daniel P. Berrangé
2023-10-18 15:23 ` Victor Toso
2023-10-19 5:42 ` Markus Armbruster
2023-10-19 7:30 ` Daniel P. Berrangé
2023-10-16 15:26 ` [PATCH v2 03/11] qapi: golang: Generate qapi's enum types in Go Victor Toso
2023-10-16 15:26 ` Victor Toso [this message]
2023-11-06 15:28 ` [PATCH v2 04/11] qapi: golang: Generate qapi's alternate " Andrea Bolognani
2023-11-06 15:52 ` Victor Toso
2023-11-06 16:12 ` Andrea Bolognani
2023-11-09 17:34 ` Andrea Bolognani
2023-11-09 19:01 ` Victor Toso
2023-11-10 9:58 ` Andrea Bolognani
2023-11-10 15:43 ` Victor Toso
2023-10-16 15:26 ` [PATCH v2 05/11] qapi: golang: Generate qapi's struct " Victor Toso
2023-10-16 15:26 ` [PATCH v2 06/11] qapi: golang: structs: Address 'null' members Victor Toso
2023-10-16 15:27 ` [PATCH v2 07/11] qapi: golang: Generate qapi's union types in Go Victor Toso
2023-11-09 17:29 ` Andrea Bolognani
2023-11-09 18:35 ` Victor Toso
2023-11-10 9:54 ` Andrea Bolognani
2023-11-10 15:45 ` Victor Toso
2023-10-16 15:27 ` [PATCH v2 08/11] qapi: golang: Generate qapi's event " Victor Toso
2023-11-09 17:59 ` Andrea Bolognani
2023-11-09 19:13 ` Victor Toso
2023-11-10 9:52 ` Andrea Bolognani
2023-10-16 15:27 ` [PATCH v2 09/11] qapi: golang: Generate qapi's command " Victor Toso
2023-10-16 15:27 ` [PATCH v2 10/11] qapi: golang: Add CommandResult type to Go Victor Toso
2023-11-09 18:24 ` Andrea Bolognani
2023-11-09 19:31 ` Victor Toso
2023-11-10 9:46 ` Andrea Bolognani
2023-10-16 15:27 ` [PATCH v2 11/11] docs: add notes on Golang code generator Victor Toso
2023-10-18 11:47 ` Markus Armbruster
2023-10-18 16:21 ` Victor Toso
2023-10-19 6:56 ` Markus Armbruster
2023-10-27 17:33 ` [PATCH v2 00/11] qapi-go: add generator for Golang interface Victor Toso
2023-10-31 16:42 ` Andrea Bolognani
2023-11-03 18:34 ` Andrea Bolognani
2023-11-06 12:00 ` Victor Toso
2023-11-09 18:35 ` Andrea Bolognani
2023-11-09 19:03 ` Victor Toso
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20231016152704.221611-5-victortoso@redhat.com \
--to=victortoso@redhat.com \
--cc=abologna@redhat.com \
--cc=armbru@redhat.com \
--cc=berrange@redhat.com \
--cc=jsnow@redhat.com \
--cc=qemu-devel@nongnu.org \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for NNTP newsgroup(s).