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 v2 07/11] qapi: golang: Generate qapi's union types in Go
Date: Mon, 16 Oct 2023 17:27:00 +0200	[thread overview]
Message-ID: <20231016152704.221611-8-victortoso@redhat.com> (raw)
In-Reply-To: <20231016152704.221611-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:
 | { 'union': 'SetPasswordOptions',
 |   'base': { 'protocol': 'DisplayProtocol',
 |             'password': 'str',
 |             '*connected': 'SetPasswordAction' },
 |   'discriminator': 'protocol',
 |   'data': { 'vnc': 'SetPasswordOptionsVnc' } }

go:
 | type SetPasswordOptions struct {
 | 	Password  string             `json:"password"`
 | 	Connected *SetPasswordAction `json:"connected,omitempty"`
 | 	// Variants fields
 | 	Vnc *SetPasswordOptionsVnc `json:"-"`
 | 	// Unbranched enum fields
 | 	Spice bool `json:"-"`
 | }

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

diff --git a/scripts/qapi/golang.py b/scripts/qapi/golang.py
index 6a7e37dd90..bc6206797a 100644
--- a/scripts/qapi/golang.py
+++ b/scripts/qapi/golang.py
@@ -51,6 +51,17 @@
 \t}
 \treturn 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 {
+\tif bytes, err := json.Marshal(&data); err != nil {
+\t\treturn fmt.Errorf("unwrapToMap: %s", err)
+\t} else if err := json.Unmarshal(bytes, &m); err != nil {
+\t\treturn fmt.Errorf("unwrapToMap: %s, data=%s", err, string(bytes))
+\t}
+\treturn nil
+}
 """
 
 TEMPLATE_ALTERNATE = """
@@ -130,6 +141,81 @@
 """
 
 
+TEMPLATE_UNION_CHECK_VARIANT_FIELD = """
+\tif s.{field} != nil && err == nil {{
+\t\tif len(bytes) != 0 {{
+\t\t\terr = errors.New(`multiple variant fields set`)
+\t\t}} else if err = unwrapToMap(m, s.{field}); err == nil {{
+\t\t\tm["{discriminator}"] = {go_enum_value}
+\t\t\tbytes, err = json.Marshal(m)
+\t\t}}
+\t}}
+"""
+
+TEMPLATE_UNION_CHECK_UNBRANCHED_FIELD = """
+\tif s.{field} && err == nil {{
+\t\tif len(bytes) != 0 {{
+\t\t\terr = errors.New(`multiple variant fields set`)
+\t\t}} else {{
+\t\t\tm["{discriminator}"] = {go_enum_value}
+\t\t\tbytes, err = json.Marshal(m)
+\t\t}}
+\t}}
+"""
+
+TEMPLATE_UNION_DRIVER_VARIANT_CASE = """
+\tcase {go_enum_value}:
+\t\ts.{field} = new({member_type})
+\t\tif err := json.Unmarshal(data, s.{field}); err != nil {{
+\t\t\ts.{field} = nil
+\t\t\treturn err
+\t\t}}"""
+
+TEMPLATE_UNION_DRIVER_UNBRANCHED_CASE = """
+\tcase {go_enum_value}:
+\t\ts.{field} = true
+"""
+
+TEMPLATE_UNION_METHODS = """
+func (s {type_name}) MarshalJSON() ([]byte, error) {{
+\tvar bytes []byte
+\tvar err error
+\tm := make(map[string]any)
+\t{{
+\t\ttype Alias {type_name}
+\t\tv := Alias(s)
+\t\tunwrapToMap(m, &v)
+\t}}
+{check_fields}
+\tif err != nil {{
+\t\treturn nil, fmt.Errorf("marshal {type_name} due:'%s' struct='%+v'", err, s)
+\t}} else if len(bytes) == 0 {{
+\t\treturn nil, fmt.Errorf("marshal {type_name} unsupported, struct='%+v'", s)
+\t}}
+\treturn bytes, nil
+}}
+
+func (s *{type_name}) UnmarshalJSON(data []byte) error {{
+{base_type_def}
+\ttmp := struct {{
+\t\t{base_type_name}
+\t}}{{}}
+
+\tif err := json.Unmarshal(data, &tmp); err != nil {{
+\t\treturn err
+\t}}
+{base_type_assign_unmarshal}
+\tswitch tmp.{discriminator} {{
+{driver_cases}
+\tdefault:
+\t\treturn fmt.Errorf("unmarshal {type_name} received unrecognized value '%s'",
+\t\t\ttmp.{discriminator})
+\t}}
+\treturn nil
+}}
+"""
+
+
 def gen_golang(schema: QAPISchema, output_dir: str, prefix: str) -> None:
     vis = QAPISchemaGenGolangVisitor(prefix)
     schema.visit(vis)
@@ -511,15 +597,17 @@ def recursive_base(
 def qapi_to_golang_struct(
     self: QAPISchemaGenGolangVisitor,
     name: str,
-    _: Optional[QAPISourceInfo],
+    info: Optional[QAPISourceInfo],
     __: QAPISchemaIfCond,
     ___: List[QAPISchemaFeature],
     base: Optional[QAPISchemaObjectType],
     members: List[QAPISchemaObjectTypeMember],
     variants: Optional[QAPISchemaVariants],
+    ident: int = 0,
 ) -> str:
 
-    fields, with_nullable = recursive_base(self, base)
+    discriminator = None if not variants else variants.tag_member.name
+    fields, with_nullable = recursive_base(self, base, discriminator)
 
     if members:
         for member in members:
@@ -534,20 +622,37 @@ def qapi_to_golang_struct(
             fields.append(field)
             with_nullable = True if nullable else with_nullable
 
+    exists = {}
     if variants:
         fields.append({"comment": "Variants fields"})
         for variant in variants.variants:
             if variant.type.is_implicit():
                 continue
 
+            exists[variant.name] = True
             field, nullable = get_struct_field(
                 self, variant.name, variant.type.name, False, True, True
             )
             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, nullable = get_struct_field(
+                    self, member.name, "bool", False, False, True
+                )
+                fields.append(field)
+                with_nullable = True if nullable else with_nullable
+
     type_name = qapi_to_go_type_name(name)
-    content = generate_struct_type(type_name, fields)
+    content = generate_struct_type(type_name, fields, ident)
     if with_nullable:
         content += struct_with_nullable_generate_marshal(
             self, name, base, members, variants
@@ -555,6 +660,96 @@ 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.variants,
+        ident=1,
+    )
+
+    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"""
+\ts.{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 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,
@@ -648,6 +843,7 @@ def __init__(self, _: str):
             "enum",
             "helper",
             "struct",
+            "union",
         )
         self.target = dict.fromkeys(types, "")
         self.schema: QAPISchema
@@ -655,6 +851,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 = []
 
     def visit_begin(self, schema: QAPISchema) -> None:
@@ -681,6 +878,7 @@ def visit_begin(self, schema: QAPISchema) -> None:
             elif target == "helper":
                 imports = """\nimport (
 \t"encoding/json"
+\t"fmt"
 \t"strings"
 )
 """
@@ -702,6 +900,7 @@ def visit_end(self) -> None:
         self.target["enum"] += generate_content_from_dict(self.enums)
         self.target["alternate"] += generate_content_from_dict(self.alternates)
         self.target["struct"] += generate_content_from_dict(self.structs)
+        self.target["union"] += generate_content_from_dict(self.unions)
 
     def visit_object_type(
         self,
@@ -713,11 +912,11 @@ def visit_object_type(
         members: List[QAPISchemaObjectTypeMember],
         variants: Optional[QAPISchemaVariants],
     ) -> 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
 
@@ -725,9 +924,6 @@ def visit_object_type(
         if qapi_name_is_base(name):
             return
 
-        # Safety checks.
-        assert name not in self.structs
-
         # visit all inner objects as well, they are not going to be
         # called by python's generator.
         if variants:
@@ -744,9 +940,19 @@ def visit_object_type(
                 )
 
         # Save generated Go code to be written later
-        self.structs[name] = qapi_to_golang_struct(
-            self, name, info, ifcond, features, base, members, variants
-        )
+        if info.defn_meta == "struct":
+            assert name not in self.structs
+            self.structs[name] = qapi_to_golang_struct(
+                self, name, info, ifcond, features, base, members, variants
+            )
+        else:
+            assert name not in self.unions
+            self.unions[name] = qapi_to_golang_struct(
+                self, name, info, ifcond, features, base, members, variants
+            )
+            self.unions[name] += qapi_to_golang_methods_union(
+                self, name, base, variants
+            )
 
     def visit_alternate_type(
         self,
-- 
2.41.0



  parent reply	other threads:[~2023-10-16 15:28 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 ` [PATCH v2 04/11] qapi: golang: Generate qapi's alternate " Victor Toso
2023-11-06 15:28   ` 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 ` Victor Toso [this message]
2023-11-09 17:29   ` [PATCH v2 07/11] qapi: golang: Generate qapi's union types in Go 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-8-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).