[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]
[PATCH v1 4/9] qapi: golang: structs: Address 'null' members
From: |
Victor Toso |
Subject: |
[PATCH v1 4/9] qapi: golang: structs: Address 'null' members |
Date: |
Wed, 27 Sep 2023 13:25:39 +0200 |
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 --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,
--
2.41.0
[PATCH v1 4/9] qapi: golang: structs: Address 'null' members,
Victor Toso <=
[PATCH v1 7/9] qapi: golang: Generate qapi's command types in Go, Victor Toso, 2023/09/27
[PATCH v1 8/9] qapi: golang: Add CommandResult type to Go, Victor Toso, 2023/09/27
[PATCH v1 9/9] docs: add notes on Golang code generator, Victor Toso, 2023/09/27
Re: [PATCH v1 0/9] qapi-go: add generator for Golang interface, Victor Toso, 2023/09/27