diff mbox series

[v1,4/9] qapi: golang: structs: Address 'null' members

Message ID 20230927112544.85011-5-victortoso@redhat.com (mailing list archive)
State New, archived
Headers show
Series qapi-go: add generator for Golang interface | expand

Commit Message

Victor Toso Sept. 27, 2023, 11:25 a.m. UTC
Explaining why this is needed needs some context, so taking the
example of StrOrNull alternate type and considering a simplified
struct that has two fields:

qapi:
 | { 'struct': 'MigrationExample',
 |   'data': { '*label': 'StrOrNull',
 |             'target': 'StrOrNull' } }

We have a optional member 'label' which can have three JSON values:
 1. A string: { "label": "happy" }
 2. A null  : { "label": null }
 3. Absent  : {}

The member 'target' is not optional, hence it can't be absent.

A Go struct that contains a optional type that can be JSON Null like
'label' in the example above, will need extra care when Marshaling
and Unmarshaling from JSON.

This patch handles this very specific case:
 - It implements the Marshaler interface for these structs to properly
   handle these values.
 - It adds the interface AbsentAlternate() and implement it for any
   Alternate that can be JSON Null

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

Patch

diff --git a/scripts/qapi/golang.py b/scripts/qapi/golang.py
index 1b19e4b232..8320af99b6 100644
--- a/scripts/qapi/golang.py
+++ b/scripts/qapi/golang.py
@@ -110,6 +110,27 @@ 
 }}
 '''
 
+TEMPLATE_STRUCT_WITH_NULLABLE_MARSHAL = '''
+func (s {type_name}) MarshalJSON() ([]byte, error) {{
+    m := make(map[string]any)
+    {map_members}
+    {map_special}
+    return json.Marshal(&m)
+}}
+
+func (s *{type_name}) UnmarshalJSON(data []byte) error {{
+    tmp := {struct}{{}}
+
+    if err := json.Unmarshal(data, &tmp); err != nil {{
+        return err
+    }}
+
+    {set_members}
+    {set_special}
+    return nil
+}}
+'''
+
 def gen_golang(schema: QAPISchema,
                output_dir: str,
                prefix: str) -> None:
@@ -182,45 +203,187 @@  def generate_struct_type(type_name, args="") -> str:
 def get_struct_field(self: QAPISchemaGenGolangVisitor,
                      qapi_name: str,
                      qapi_type_name: str,
+                     within_nullable_struct: bool,
                      is_optional: bool,
-                     is_variant: bool) -> str:
+                     is_variant: bool) -> Tuple[str, bool]:
 
     field = qapi_to_field_name(qapi_name)
     member_type = qapi_schema_type_to_go_type(qapi_type_name)
+    is_nullable = False
 
     optional = ""
     if is_optional:
-        if member_type not in self.accept_null_types:
+        if member_type in self.accept_null_types:
+            is_nullable = True
+        else:
             optional = ",omitempty"
 
     # Use pointer to type when field is optional
     isptr = "*" if is_optional and member_type[0] not in "*[" else ""
 
+    if within_nullable_struct:
+        # Within a struct which has a field of type that can hold JSON NULL,
+        # we have to _not_ use a pointer, otherwise the Marshal methods are
+        # not called.
+        isptr = "" if member_type in self.accept_null_types else isptr
+
     fieldtag = '`json:"-"`' if is_variant else f'`json:"{qapi_name}{optional}"`'
-    return f"\t{field} {isptr}{member_type}{fieldtag}\n"
+    return f"\t{field} {isptr}{member_type}{fieldtag}\n", is_nullable
+
+# This helper is used whithin a struct that has members that accept JSON NULL.
+def map_and_set(is_nullable: bool,
+                field: str,
+                field_is_optional: bool,
+                name: str) -> Tuple[str, str]:
+
+    mapstr = ""
+    setstr = ""
+    if is_nullable:
+        mapstr = f'''
+    if val, absent := s.{field}.ToAnyOrAbsent(); !absent {{
+        m["{name}"] = val
+    }}
+'''
+        setstr += f'''
+    if _, absent := (&tmp.{field}).ToAnyOrAbsent(); !absent {{
+        s.{field} = &tmp.{field}
+    }}
+'''
+    elif field_is_optional:
+        mapstr = f'''
+    if s.{field} != nil {{
+        m["{name}"] = s.{field}
+    }}
+'''
+        setstr = f'''\ts.{field} = tmp.{field}\n'''
+    else:
+        mapstr = f'''\tm["{name}"] = s.{field}\n'''
+        setstr = f'''\ts.{field} = tmp.{field}\n'''
+
+    return mapstr, setstr
+
+def recursive_base_nullable(self: QAPISchemaGenGolangVisitor,
+                            base: Optional[QAPISchemaObjectType]) -> Tuple[str, str, str, str, str]:
+    fields = ""
+    map_members = ""
+    set_members = ""
+    map_special = ""
+    set_special = ""
+
+    if not base:
+        return fields, map_members, set_members, map_special, set_special
+
+    if base.base is not None:
+        embed_base = self.schema.lookup_entity(base.base.name)
+        fields, map_members, set_members, map_special, set_special = recursive_base_nullable(self, embed_base)
+
+    for member in base.local_members:
+        field, _ = get_struct_field(self, member.name, member.type.name,
+                                    True, member.optional, False)
+        fields += field
+
+        member_type = qapi_schema_type_to_go_type(member.type.name)
+        nullable = member_type in self.accept_null_types
+        field_name = qapi_to_field_name(member.name)
+        tomap, toset = map_and_set(nullable, field_name, member.optional, member.name)
+        if nullable:
+            map_special += tomap
+            set_special += toset
+        else:
+            map_members += tomap
+            set_members += toset
+
+    return fields, map_members, set_members, map_special, set_special
+
+# Helper function. This is executed when the QAPI schema has members
+# that could accept JSON NULL (e.g: StrOrNull in QEMU"s QAPI schema).
+# This struct will need to be extended with Marshal/Unmarshal methods to
+# properly handle such atypical members.
+#
+# Only the Marshallaing methods are generated but we do need to iterate over
+# all the members to properly set/check them in those methods.
+def struct_with_nullable_generate_marshal(self: QAPISchemaGenGolangVisitor,
+                                          name: str,
+                                          base: Optional[QAPISchemaObjectType],
+                                          members: List[QAPISchemaObjectTypeMember],
+                                          variants: Optional[QAPISchemaVariants]) -> str:
+
+
+    fields, map_members, set_members, map_special, set_special = recursive_base_nullable(self, base)
+
+    if members:
+        for member in members:
+            field, _ = get_struct_field(self, member.name, member.type.name,
+                                        True, member.optional, False)
+            fields += field
+
+            member_type = qapi_schema_type_to_go_type(member.type.name)
+            nullable = member_type in self.accept_null_types
+            tomap, toset = map_and_set(nullable, qapi_to_field_name(member.name),
+                                       member.optional, member.name)
+            if nullable:
+                map_special += tomap
+                set_special += toset
+            else:
+                map_members += tomap
+                set_members += toset
+
+        fields += "\n"
+
+    if variants:
+        for variant in variants.variants:
+            if variant.type.is_implicit():
+                continue
+
+            field, _ = get_struct_field(self, variant.name, variant.type.name,
+                                        True, variant.optional, True)
+            fields += field
+
+            member_type = qapi_schema_type_to_go_type(variant.type.name)
+            nullable = member_type in self.accept_null_types
+            tomap, toset = map_and_set(nullable, qapi_to_field_name(variant.name),
+                                       variant.optional, variant.name)
+            if nullable:
+                map_special += tomap
+                set_special += toset
+            else:
+                map_members += tomap
+                set_members += toset
+
+    type_name = qapi_to_go_type_name(name)
+    struct = generate_struct_type("", fields)[:-1]
+    return TEMPLATE_STRUCT_WITH_NULLABLE_MARSHAL.format(struct=struct,
+                                                        type_name=type_name,
+                                                        map_members=map_members,
+                                                        map_special=map_special,
+                                                        set_members=set_members,
+                                                        set_special=set_special)
 
 def recursive_base(self: QAPISchemaGenGolangVisitor,
-                   base: Optional[QAPISchemaObjectType]) -> str:
+                   base: Optional[QAPISchemaObjectType]) -> Tuple[str, bool]:
     fields = ""
+    with_nullable = False
 
     if not base:
-        return fields
+        return fields, with_nullable
 
     if base.base is not None:
         embed_base = self.schema.lookup_entity(base.base.name)
-        fields = recursive_base(self, embed_base)
+        fields, with_nullable = recursive_base(self, embed_base)
 
     for member in base.local_members:
         if base.variants and base.variants.tag_member.name == member.name:
             fields += '''// Discriminator\n'''
 
-        field = get_struct_field(self, member.name, member.type.name, member.optional, False)
+        field, nullable = get_struct_field(self, member.name, member.type.name,
+                                           False, member.optional, False)
         fields += field
+        with_nullable = True if nullable else with_nullable
 
     if len(fields) > 0:
         fields += "\n"
 
-    return fields
+    return fields, with_nullable
 
 # Helper function that is used for most of QAPI types
 def qapi_to_golang_struct(self: QAPISchemaGenGolangVisitor,
@@ -233,12 +396,14 @@  def qapi_to_golang_struct(self: QAPISchemaGenGolangVisitor,
                           variants: Optional[QAPISchemaVariants]) -> str:
 
 
-    fields = recursive_base(self, base)
+    fields, with_nullable = recursive_base(self, base)
 
     if members:
         for member in members:
-            field = get_struct_field(self, member.name, member.type.name, member.optional, False)
+            field, nullable = get_struct_field(self, member.name, member.type.name,
+                                               False, member.optional, False)
             fields += field
+            with_nullable = True if nullable else with_nullable
 
         fields += "\n"
 
@@ -248,11 +413,19 @@  def qapi_to_golang_struct(self: QAPISchemaGenGolangVisitor,
             if variant.type.is_implicit():
                 continue
 
-            field = get_struct_field(self, variant.name, variant.type.name, True, True)
+            field, nullable = get_struct_field(self, variant.name, variant.type.name,
+                                               False, True, True)
             fields += field
+            with_nullable = True if nullable else with_nullable
 
     type_name = qapi_to_go_type_name(name)
     content = generate_struct_type(type_name, fields)
+    if with_nullable:
+        content += struct_with_nullable_generate_marshal(self,
+                                                         name,
+                                                         base,
+                                                         members,
+                                                         variants)
     return content
 
 def generate_template_alternate(self: QAPISchemaGenGolangVisitor,