qemu-devel.nongnu.org archive mirror
 help / color / mirror / Atom feed
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 v4 06/11] qapi: golang: Generate union type
Date: Fri, 14 Feb 2025 21:29:39 +0100	[thread overview]
Message-ID: <20250214202944.69897-7-victortoso@redhat.com> (raw)
In-Reply-To: <20250214202944.69897-1-victortoso@redhat.com>

This patch handles QAPI union types and generates the equivalent data
structures and methods in Go to handle it.

The QAPI union type has two types of fields: The @base and the
@Variants members. The @base fields can be considered common members
for the union while only one field maximum is set for the @Variants.

In the QAPI specification, it defines a @discriminator field, which is
an Enum type. The purpose of the  @discriminator is to identify which
@variant type is being used.

For the @discriminator's enum that are not handled by the QAPI Union,
we add in the Go struct a separate block as "Unbranched enum fields".
The rationale for this extra block is to allow the user to pass that
enum value under the discriminator, without extra payload.

The union types implement the Marshaler and Unmarshaler interfaces to
seamless decode from JSON objects to Golang structs and vice versa.

qapi:
 | ##
 | # @SetPasswordOptions:
 | #
 | # Options for set_password.
 | #
 | # @protocol:
 | #     - 'vnc' to modify the VNC server password
 | #     - 'spice' to modify the Spice server password
 | #
 | # @password: the new password
 | #
 | # @connected: How to handle existing clients when changing the
 | #     password.  If nothing is specified, defaults to 'keep'.  For
 | #     VNC, only 'keep' is currently implemented.
 | #
 | # Since: 7.0
 | ##
 | { 'union': 'SetPasswordOptions',
 |   'base': { 'protocol': 'DisplayProtocol',
 |             'password': 'str',
 |             '*connected': 'SetPasswordAction' },
 |   'discriminator': 'protocol',
 |   'data': { 'vnc': 'SetPasswordOptionsVnc' } }

go:
 | // Options for set_password.
 | //
 | // Since: 7.0
 | type SetPasswordOptions struct {
 | 	// the new password
 | 	Password string `json:"password"`
 | 	// How to handle existing clients when changing the password. If
 | 	// nothing is specified, defaults to 'keep'. For VNC, only 'keep'
 | 	// is currently implemented.
 | 	Connected *SetPasswordAction `json:"connected,omitempty"`
 | 	// Variants fields
 | 	Vnc *SetPasswordOptionsVnc `json:"-"`
 | 	// Unbranched enum fields
 | 	Spice bool `json:"-"`
 | }
 |
 | func (s SetPasswordOptions) MarshalJSON() ([]byte, error) {
 |      ...
 | }

 |
 | func (s *SetPasswordOptions) UnmarshalJSON(data []byte) error {
 |      ...
 | }

Signed-off-by: Victor Toso <victortoso@redhat.com>
---
 scripts/qapi/golang/golang.py | 208 +++++++++++++++++++++++++++++++++-
 scripts/qapi/golang/utils.go  |  12 ++
 2 files changed, 217 insertions(+), 3 deletions(-)

diff --git a/scripts/qapi/golang/golang.py b/scripts/qapi/golang/golang.py
index 0637bb3e3e..59e9b1f644 100644
--- a/scripts/qapi/golang/golang.py
+++ b/scripts/qapi/golang/golang.py
@@ -155,6 +155,81 @@
 """
 
 
+TEMPLATE_UNION_CHECK_VARIANT_FIELD = """
+    if s.{field} != nil && err == nil {{
+        if len(bytes) != 0 {{
+            err = errors.New(`multiple variant fields set`)
+        }} else if err = unwrapToMap(m, s.{field}); err == nil {{
+            m["{discriminator}"] = {go_enum_value}
+            bytes, err = json.Marshal(m)
+        }}
+    }}
+"""
+
+TEMPLATE_UNION_CHECK_UNBRANCHED_FIELD = """
+    if s.{field} && err == nil {{
+        if len(bytes) != 0 {{
+            err = errors.New(`multiple variant fields set`)
+        }} else {{
+            m["{discriminator}"] = {go_enum_value}
+            bytes, err = json.Marshal(m)
+        }}
+    }}
+"""
+
+TEMPLATE_UNION_DRIVER_VARIANT_CASE = """
+    case {go_enum_value}:
+        s.{field} = new({member_type})
+        if err := json.Unmarshal(data, s.{field}); err != nil {{
+            s.{field} = nil
+            return err
+        }}"""
+
+TEMPLATE_UNION_DRIVER_UNBRANCHED_CASE = """
+    case {go_enum_value}:
+        s.{field} = true
+"""
+
+TEMPLATE_UNION_METHODS = """
+func (s {type_name}) MarshalJSON() ([]byte, error) {{
+    var bytes []byte
+    var err error
+    m := make(map[string]any)
+    {{
+        type Alias {type_name}
+        v := Alias(s)
+        unwrapToMap(m, &v)
+    }}
+{check_fields}
+    if err != nil {{
+        return nil, fmt.Errorf("marshal {type_name} due:'%s' struct='%+v'", err, s)
+    }} else if len(bytes) == 0 {{
+        return nil, fmt.Errorf("marshal {type_name} unsupported, struct='%+v'", s)
+    }}
+    return bytes, nil
+}}
+
+func (s *{type_name}) UnmarshalJSON(data []byte) error {{
+{base_type_def}
+    tmp := struct {{
+        {base_type_name}
+    }}{{}}
+
+    if err := json.Unmarshal(data, &tmp); err != nil {{
+        return err
+    }}
+{base_type_assign_unmarshal}
+    switch tmp.{discriminator} {{
+{driver_cases}
+    default:
+        return fmt.Errorf("unmarshal {type_name} received unrecognized value '%s'",
+            tmp.{discriminator})
+    }}
+    return nil
+}}
+"""
+
+
 # Takes the documentation object of a specific type and returns
 # that type's documentation and its member's docs.
 def qapi_to_golang_struct_docs(doc: QAPIDoc) -> (str, Dict[str, str]):
@@ -202,6 +277,12 @@ def qapi_name_is_object(name: str) -> bool:
     return name.startswith("q_obj_")
 
 
+def qapi_base_name_to_parent(name: str) -> str:
+    if qapi_name_is_base(name):
+        name = name[6:-5]
+    return name
+
+
 def qapi_to_field_name(name: str) -> str:
     return name.title().replace("_", "").replace("-", "")
 
@@ -639,7 +720,7 @@ def recursive_base(
         embed_base = self.schema.lookup_entity(base.base.name)
         fields, with_nullable = recursive_base(self, embed_base, discriminator)
 
-    doc = self.docmap.get(base.name, None)
+    doc = self.docmap.get(qapi_base_name_to_parent(base.name), None)
     _, docfields = qapi_to_golang_struct_docs(doc)
 
     for member in base.local_members:
@@ -719,6 +800,24 @@ def qapi_to_golang_struct(
             fields.append(field)
             with_nullable = True if nullable else with_nullable
 
+    if info.defn_meta == "union" and variants:
+        enum_name = variants.tag_member.type.name
+        enum_obj = self.schema.lookup_entity(enum_name)
+        if len(exists) != len(enum_obj.members):
+            fields.append({"comment": "Unbranched enum fields"})
+            for member in enum_obj.members:
+                if member.name in exists:
+                    continue
+
+                field_doc = (
+                    docfields.get(member.name, "") if doc_enabled else ""
+                )
+                field, nullable = get_struct_field(
+                    self, member.name, "bool", field_doc, False, False, True
+                )
+                fields.append(field)
+                with_nullable = True if nullable else with_nullable
+
     type_name = qapi_to_go_type_name(name)
     content = string_to_code(
         generate_struct_type(
@@ -732,6 +831,98 @@ def qapi_to_golang_struct(
     return content
 
 
+def qapi_to_golang_methods_union(
+    self: QAPISchemaGenGolangVisitor,
+    name: str,
+    base: Optional[QAPISchemaObjectType],
+    variants: Optional[QAPISchemaVariants],
+) -> str:
+    type_name = qapi_to_go_type_name(name)
+
+    assert base
+    base_type_assign_unmarshal = ""
+    base_type_name = qapi_to_go_type_name(base.name)
+    base_type_def = qapi_to_golang_struct(
+        self,
+        base.name,
+        base.info,
+        base.ifcond,
+        base.features,
+        base.base,
+        base.members,
+        base.branches,
+        indent=1,
+        doc_enabled=False,
+    )
+
+    discriminator = qapi_to_field_name(variants.tag_member.name)
+    for member in base.local_members:
+        field = qapi_to_field_name(member.name)
+        if field == discriminator:
+            continue
+        base_type_assign_unmarshal += f"""
+    s.{field} = tmp.{field}"""
+
+    driver_cases = ""
+    check_fields = ""
+    exists = {}
+    enum_name = variants.tag_member.type.name
+    if variants:
+        for var in variants.variants:
+            if var.type.is_implicit():
+                continue
+
+            field = qapi_to_field_name(var.name)
+            enum_value = qapi_to_field_name_enum(var.name)
+            member_type = qapi_schema_type_to_go_type(var.type.name)
+            go_enum_value = f"""{enum_name}{enum_value}"""
+            exists[go_enum_value] = True
+
+            check_fields += TEMPLATE_UNION_CHECK_VARIANT_FIELD.format(
+                field=field,
+                discriminator=variants.tag_member.name,
+                go_enum_value=go_enum_value,
+            )
+            driver_cases += TEMPLATE_UNION_DRIVER_VARIANT_CASE.format(
+                go_enum_value=go_enum_value,
+                field=field,
+                member_type=member_type,
+            )
+
+    enum_obj = self.schema.lookup_entity(enum_name)
+    if len(exists) != len(enum_obj.members):
+        for member in enum_obj.members:
+            value = qapi_to_field_name_enum(member.name)
+            go_enum_value = f"""{enum_name}{value}"""
+
+            if go_enum_value in exists:
+                continue
+
+            field = qapi_to_field_name(member.name)
+
+            check_fields += TEMPLATE_UNION_CHECK_UNBRANCHED_FIELD.format(
+                field=field,
+                discriminator=variants.tag_member.name,
+                go_enum_value=go_enum_value,
+            )
+            driver_cases += TEMPLATE_UNION_DRIVER_UNBRANCHED_CASE.format(
+                go_enum_value=go_enum_value,
+                field=field,
+            )
+
+    return string_to_code(
+        TEMPLATE_UNION_METHODS.format(
+            type_name=type_name,
+            check_fields=check_fields[1:],
+            base_type_def=base_type_def[1:],
+            base_type_name=base_type_name,
+            base_type_assign_unmarshal=base_type_assign_unmarshal,
+            discriminator=discriminator,
+            driver_cases=driver_cases[1:],
+        )
+    )
+
+
 def generate_template_alternate(
     self: QAPISchemaGenGolangVisitor,
     name: str,
@@ -855,6 +1046,7 @@ def __init__(self, _: str):
             "alternate": ["encoding/json", "errors", "fmt"],
             "enum": [],
             "struct": ["encoding/json"],
+            "union": ["encoding/json", "errors", "fmt"],
         }
 
         self.schema: QAPISchema
@@ -863,6 +1055,7 @@ def __init__(self, _: str):
         self.enums: dict[str, str] = {}
         self.alternates: dict[str, str] = {}
         self.structs: dict[str, str] = {}
+        self.unions: dict[str, str] = {}
         self.accept_null_types = []
         self.docmap = {}
 
@@ -902,6 +1095,7 @@ def visit_end(self) -> None:
         self.types["enum"] += generate_content_from_dict(self.enums)
         self.types["alternate"] += generate_content_from_dict(self.alternates)
         self.types["struct"] += generate_content_from_dict(self.structs)
+        self.types["union"] += generate_content_from_dict(self.unions)
 
     def visit_object_type(
         self,
@@ -913,11 +1107,11 @@ def visit_object_type(
         members: List[QAPISchemaObjectTypeMember],
         branches: Optional[QAPISchemaBranches],
     ) -> None:
-        # Do not handle anything besides struct.
+        # Do not handle anything besides struct and unions.
         if (
             name == self.schema.the_empty_object_type.name
             or not isinstance(name, str)
-            or info.defn_meta not in ["struct"]
+            or info.defn_meta not in ["struct", "union"]
         ):
             return
 
@@ -948,6 +1142,14 @@ def visit_object_type(
                     self, name, info, ifcond, features, base, members, branches
                 )
             )
+        else:
+            assert name not in self.unions
+            self.unions[name] = qapi_to_golang_struct(
+                self, name, info, ifcond, features, base, members, branches
+            )
+            self.unions[name] += qapi_to_golang_methods_union(
+                self, name, base, branches
+            )
 
     def visit_alternate_type(
         self,
diff --git a/scripts/qapi/golang/utils.go b/scripts/qapi/golang/utils.go
index f00c0a5d83..193e0c53bb 100644
--- a/scripts/qapi/golang/utils.go
+++ b/scripts/qapi/golang/utils.go
@@ -9,6 +9,7 @@ package qapi
 
 import (
 	"encoding/json"
+	"fmt"
 	"strings"
 )
 
@@ -24,3 +25,14 @@ func strictDecode(into interface{}, from []byte) error {
 	}
 	return nil
 }
+
+// This helper is used to move struct's fields into a map.
+// This function is useful to merge JSON objects.
+func unwrapToMap(m map[string]any, data any) error {
+	if bytes, err := json.Marshal(&data); err != nil {
+		return fmt.Errorf("unwrapToMap: %s", err)
+	} else if err := json.Unmarshal(bytes, &m); err != nil {
+		return fmt.Errorf("unwrapToMap: %s, data=%s", err, string(bytes))
+	}
+	return nil
+}
-- 
2.48.1



  parent reply	other threads:[~2025-02-14 20:33 UTC|newest]

Thread overview: 18+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2025-02-14 20:29 [PATCH v4 00/11] Victor Toso
2025-02-14 20:29 ` [PATCH v4 01/11] qapi: golang: first level unmarshalling type Victor Toso
2025-02-14 20:29 ` [PATCH v4 02/11] qapi: golang: Generate enum type Victor Toso
2025-02-14 20:29 ` [PATCH v4 03/11] qapi: golang: Generate alternate types Victor Toso
2025-02-14 20:29 ` [PATCH v4 04/11] qapi: golang: Generate struct types Victor Toso
2025-02-14 20:29 ` [PATCH v4 05/11] qapi: golang: structs: Address nullable members Victor Toso
2025-02-14 20:29 ` Victor Toso [this message]
2025-02-14 20:29 ` [PATCH v4 07/11] qapi: golang: Generate event type Victor Toso
2025-02-14 20:29 ` [PATCH v4 08/11] qapi: golang: Generate Event interface Victor Toso
2025-02-14 20:29 ` [PATCH v4 09/11] qapi: golang: Generate command type Victor Toso
2025-02-14 20:29 ` [PATCH v4 10/11] qapi: golang: Generate Command sync/async interfaces Victor Toso
2025-02-14 20:29 ` [PATCH v4 11/11] docs: add notes on Golang code generator Victor Toso
2025-02-17 13:13 ` [PATCH v4 00/11] Daniel P. Berrangé
2025-02-20  8:06   ` Markus Armbruster
2025-02-17 14:58 ` Daniel P. Berrangé
2025-02-17 16:52   ` Victor Toso
2025-03-07 11:25     ` Victor Toso
2025-03-07 11:33       ` Daniel P. Berrangé

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=20250214202944.69897-7-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).