diff mbox series

[v1,5/9] qapi: golang: Generate qapi's union types in Go

Message ID 20230927112544.85011-6-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
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.

Not that @discriminator's enum might have more values than the union's
data struct. This is fine. The union does not need to handle all cases
of the enum, but it should accept them without error. For this
specific case, we keep the @discriminator field in every union type.

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 {
 |     Protocol  DisplayProtocol    `json:"protocol"`
 |     Password  string             `json:"password"`
 |     Connected *SetPasswordAction `json:"connected,omitempty"`
 |
 |     // Variants fields
 |     Vnc *SetPasswordOptionsVnc `json:"-"`
 | }

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

Comments

Daniel P. Berrangé Sept. 28, 2023, 2:21 p.m. UTC | #1
On Wed, Sep 27, 2023 at 01:25:40PM +0200, Victor Toso wrote:
> 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.
> 
> Not that @discriminator's enum might have more values than the union's
> data struct. This is fine. The union does not need to handle all cases
> of the enum, but it should accept them without error. For this
> specific case, we keep the @discriminator field in every union type.

I still tend think the @discriminator field should not be
present in the union structs. It feels like we're just trying
to directly copy the C code in Go and so smells wrong from a
Go POV.

For most of the unions the @discriminator field will be entirely
redundant, becasue the commonm case is that a @variant field
exists for every possible @discriminator value.

To take one example

  type SocketAddress struct {
        Type SocketAddressType `json:"type"`

        // Variants fields
        Inet  *InetSocketAddress  `json:"-"`
        Unix  *UnixSocketAddress  `json:"-"`
        Vsock *VsockSocketAddress `json:"-"`
        Fd    *String             `json:"-"`
  }

If one was just writing Go code without the pre-existing knowledge
of the QAPI C code, 'Type' is not something a Go programmer would
be inclined add IMHO.

And yet you are right that we need a way to represent a @discriminator
value that has no corresponding @variant, since QAPI allows for that
scenario. To deal with that I would suggest we just use an empty
interface type. eg


  type SocketAddress struct {
        Type SocketAddressType `json:"type"`

        // Variants fields
        Inet  *InetSocketAddress  `json:"-"`
        Unix  *UnixSocketAddress  `json:"-"`
        Vsock *VsockSocketAddress `json:"-"`
        Fd    *String             `json:"-"`
	Fish  *interface{}        `json:"-"`
	Food  *interface()        `json:"-"`
  }

the pointer value for 'Fish' and 'Food' fields here merely needs to
be non-NULL, it doesn't matter what the actual thing assigned is.


With regards,
Daniel
Victor Toso Sept. 29, 2023, 1:41 p.m. UTC | #2
Hi,

On Thu, Sep 28, 2023 at 03:21:59PM +0100, Daniel P. Berrangé wrote:
> On Wed, Sep 27, 2023 at 01:25:40PM +0200, Victor Toso wrote:
> > 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.
> > 
> > Not that @discriminator's enum might have more values than the union's
> > data struct. This is fine. The union does not need to handle all cases
> > of the enum, but it should accept them without error. For this
> > specific case, we keep the @discriminator field in every union type.
> 
> I still tend think the @discriminator field should not be
> present in the union structs. It feels like we're just trying
> to directly copy the C code in Go and so smells wrong from a
> Go POV.
> 
> For most of the unions the @discriminator field will be entirely
> redundant, becasue the commonm case is that a @variant field
> exists for every possible @discriminator value.

You are correct.

> To take one example
> 
>   type SocketAddress struct {
>         Type SocketAddressType `json:"type"`
> 
>         // Variants fields
>         Inet  *InetSocketAddress  `json:"-"`
>         Unix  *UnixSocketAddress  `json:"-"`
>         Vsock *VsockSocketAddress `json:"-"`
>         Fd    *String             `json:"-"`
>   }
> 
> If one was just writing Go code without the pre-existing knowledge
> of the QAPI C code, 'Type' is not something a Go programmer would
> be inclined add IMHO.

You don't need previous knowledge in the QAPI C code to see that
having optional field members and a discriminator field feels
very very suspicious. I wasn't too happy to add it.

> And yet you are right that we need a way to represent a
> @discriminator value that has no corresponding @variant, since
> QAPI allows for that scenario.

Thank Markus for that, really nice catch :)


> To deal with that I would suggest we just use an empty
> interface type. eg
> 
>   type SocketAddress struct {
>         Type SocketAddressType `json:"type"`
> 
>         // Variants fields
>         Inet  *InetSocketAddress  `json:"-"`
>         Unix  *UnixSocketAddress  `json:"-"`
>         Vsock *VsockSocketAddress `json:"-"`
>         Fd    *String             `json:"-"`
> 	Fish  *interface{}        `json:"-"`
> 	Food  *interface()        `json:"-"`
>   }
> 
> the pointer value for 'Fish' and 'Food' fields here merely needs to
> be non-NULL, it doesn't matter what the actual thing assigned is.

I like this idea. What happens if Fish becomes a handled in the
future?

Before:

    type SocketAddress struct {
        // Variants fields
        Inet  *InetSocketAddress  `json:"-"`
        Unix  *UnixSocketAddress  `json:"-"`
        Vsock *VsockSocketAddress `json:"-"`
        Fd    *String             `json:"-"`

        // Unhandled enum branches
        Fish  *interface{}        `json:"-"`
        Food  *interface{}        `json:"-"`
    }

to

    type SocketAddress struct {
        // Variants fields
        Inet  *InetSocketAddress  `json:"-"`
        Unix  *UnixSocketAddress  `json:"-"`
        Vsock *VsockSocketAddress `json:"-"`
        Fd    *String             `json:"-"`
        Fish  *FishSocketAddress  `json:"-"`

        // Unhandled enum branches
        Food  *interface{}        `json:"-"`
    }

An application that hat s.Fish = &something, will now error on
compile due something type not being FishSocketAddress. I think
this is acceptable. Very corner case scenario and the user
probably want to use the right struct now.

If you agree with above, I'd instead like to try a boolean
instead of *interface{}. s.Fish = true seems better and false is
simply ignored.

Cheers,
Victor
Victor Toso Oct. 11, 2023, 1:27 p.m. UTC | #3
Hi,

On Fri, Sep 29, 2023 at 03:41:27PM +0200, Victor Toso wrote:
> Hi,
> 
> On Thu, Sep 28, 2023 at 03:21:59PM +0100, Daniel P. Berrangé wrote:
> > On Wed, Sep 27, 2023 at 01:25:40PM +0200, Victor Toso wrote:
> > > 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.
> > > 
> > > Not that @discriminator's enum might have more values than the union's
> > > data struct. This is fine. The union does not need to handle all cases
> > > of the enum, but it should accept them without error. For this
> > > specific case, we keep the @discriminator field in every union type.
> > 
> > I still tend think the @discriminator field should not be
> > present in the union structs. It feels like we're just trying
> > to directly copy the C code in Go and so smells wrong from a
> > Go POV.
> > 
> > For most of the unions the @discriminator field will be entirely
> > redundant, becasue the commonm case is that a @variant field
> > exists for every possible @discriminator value.
> 
> You are correct.
> 
> > To take one example
> > 
> >   type SocketAddress struct {
> >         Type SocketAddressType `json:"type"`
> > 
> >         // Variants fields
> >         Inet  *InetSocketAddress  `json:"-"`
> >         Unix  *UnixSocketAddress  `json:"-"`
> >         Vsock *VsockSocketAddress `json:"-"`
> >         Fd    *String             `json:"-"`
> >   }
> > 
> > If one was just writing Go code without the pre-existing knowledge
> > of the QAPI C code, 'Type' is not something a Go programmer would
> > be inclined add IMHO.
> 
> You don't need previous knowledge in the QAPI C code to see that
> having optional field members and a discriminator field feels
> very very suspicious. I wasn't too happy to add it.
> 
> > And yet you are right that we need a way to represent a
> > @discriminator value that has no corresponding @variant, since
> > QAPI allows for that scenario.
> 
> Thank Markus for that, really nice catch :)
> 
> 
> > To deal with that I would suggest we just use an empty
> > interface type. eg
> > 
> >   type SocketAddress struct {
> >         Type SocketAddressType `json:"type"`
> > 
> >         // Variants fields
> >         Inet  *InetSocketAddress  `json:"-"`
> >         Unix  *UnixSocketAddress  `json:"-"`
> >         Vsock *VsockSocketAddress `json:"-"`
> >         Fd    *String             `json:"-"`
> > 	Fish  *interface{}        `json:"-"`
> > 	Food  *interface()        `json:"-"`
> >   }
> > 
> > the pointer value for 'Fish' and 'Food' fields here merely needs to
> > be non-NULL, it doesn't matter what the actual thing assigned is.
> 
> I like this idea. What happens if Fish becomes a handled in the
> future?
> 
> Before:
> 
>     type SocketAddress struct {
>         // Variants fields
>         Inet  *InetSocketAddress  `json:"-"`
>         Unix  *UnixSocketAddress  `json:"-"`
>         Vsock *VsockSocketAddress `json:"-"`
>         Fd    *String             `json:"-"`
> 
>         // Unhandled enum branches
>         Fish  *interface{}        `json:"-"`
>         Food  *interface{}        `json:"-"`
>     }
> 
> to
> 
>     type SocketAddress struct {
>         // Variants fields
>         Inet  *InetSocketAddress  `json:"-"`
>         Unix  *UnixSocketAddress  `json:"-"`
>         Vsock *VsockSocketAddress `json:"-"`
>         Fd    *String             `json:"-"`
>         Fish  *FishSocketAddress  `json:"-"`
> 
>         // Unhandled enum branches
>         Food  *interface{}        `json:"-"`
>     }
> 
> An application that hat s.Fish = &something, will now error on
> compile due something type not being FishSocketAddress. I think
> this is acceptable. Very corner case scenario and the user
> probably want to use the right struct now.
> 
> If you agree with above, I'd instead like to try a boolean
> instead of *interface{}. s.Fish = true seems better and false is
> simply ignored.

I was just double checking that the Union's value for each enum
branch needs to be a Struct. So I think it is fine to use boolean
for unhandled enum branches. I'll be doing that in the next
iteration.

docs/devel/qapi-code-gen.rst: 350
    The BRANCH's value defines the branch's properties, in particular its
    type.  The type must a struct type.  The form TYPE-REF_ is shorthand
    for :code:`{ 'type': TYPE-REF }`.

Cheers,
Victor
diff mbox series

Patch

diff --git a/scripts/qapi/golang.py b/scripts/qapi/golang.py
index 8320af99b6..343c9c9b95 100644
--- a/scripts/qapi/golang.py
+++ b/scripts/qapi/golang.py
@@ -52,6 +52,17 @@ 
     }
     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
+}
 '''
 
 TEMPLATE_ALTERNATE = '''
@@ -131,6 +142,62 @@ 
 }}
 '''
 
+TEMPLATE_UNION_CHECK_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 {{
+        s.{discriminator} = {go_enum_value}
+        m["{member_name}"] = {go_enum_value}
+        bytes, err = json.Marshal(m)
+    }}
+}}
+'''
+
+TEMPLATE_UNION_DRIVER_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_METHODS = '''
+func (s {type_name}) MarshalJSON() ([]byte, error) {{
+    var bytes []byte
+    var err error
+    type Alias {type_name}
+    v := Alias(s)
+    m := make(map[string]any)
+    unwrapToMap(m, &v)
+    {check_fields}
+    {check_non_fields_marshal}
+    if err != nil {{
+        return nil, fmt.Errorf("error: marshal: {type_name}: reason='%s', struct='%+v'", err, s)
+    }} else if len(bytes) == 0 {{
+        return nil, fmt.Errorf("error: 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}
+    {check_non_fields_unmarshal}
+    }}
+    return nil
+}}
+'''
+
+
 def gen_golang(schema: QAPISchema,
                output_dir: str,
                prefix: str) -> None:
@@ -428,6 +495,98 @@  def qapi_to_golang_struct(self: QAPISchemaGenGolangVisitor,
                                                          variants)
     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)
+    for member in base.local_members:
+        field = qapi_to_field_name(member.name)
+        base_type_assign_unmarshal += f'''s.{field} = tmp.{field}
+'''
+
+    driver_cases = ""
+    check_fields = ""
+    exists = {}
+    enum_name = variants.tag_member.type.name
+    discriminator = qapi_to_field_name(variants.tag_member.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_FIELD.format(field=field,
+                                                             discriminator=discriminator,
+                                                             member_name=variants.tag_member.name,
+                                                             go_enum_value=go_enum_value)
+            driver_cases += TEMPLATE_UNION_DRIVER_CASE.format(go_enum_value=go_enum_value,
+                                                             field=field,
+                                                             member_type=member_type)
+
+    check_non_fields_marshal = ""
+    check_non_fields_unmarshal = ""
+    enum_obj = self.schema.lookup_entity(enum_name)
+    if len(exists) != len(enum_obj.members):
+        driver_cases += '''\ndefault:'''
+        check_non_fields_marshal = '''
+    // Check for valid values without field members
+    if len(bytes) == 0 && err == nil &&
+        ('''
+        check_non_fields_unmarshal = '''
+    // Check for valid values without field members
+    if '''
+
+        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
+
+            check_non_fields_marshal += f'''s.{discriminator} == {go_enum_value} ||\n'''
+            check_non_fields_unmarshal += f'''tmp.{discriminator} != {go_enum_value} &&\n'''
+
+        check_non_fields_marshal = f'''{check_non_fields_marshal[:-3]}) {{
+        type Alias {type_name}
+        bytes, err = json.Marshal(Alias(s))
+    }}
+'''
+        check_non_fields_unmarshal = f'''{check_non_fields_unmarshal[1:-3]} {{
+        return fmt.Errorf("error: unmarshal: {type_name}: received unrecognized value: '%s'",
+            tmp.{discriminator})
+    }}
+'''
+
+    return TEMPLATE_UNION_METHODS.format(type_name=type_name,
+                                         check_fields=check_fields,
+                                         check_non_fields_marshal=check_non_fields_marshal,
+                                         base_type_def=base_type_def,
+                                         base_type_name=base_type_name,
+                                         base_type_assign_unmarshal=base_type_assign_unmarshal,
+                                         discriminator=discriminator,
+                                         driver_cases=driver_cases[1:],
+                                         check_non_fields_unmarshal=check_non_fields_unmarshal)
+
 def generate_template_alternate(self: QAPISchemaGenGolangVisitor,
                                 name: str,
                                 variants: Optional[QAPISchemaVariants]) -> str:
@@ -490,7 +649,7 @@  class QAPISchemaGenGolangVisitor(QAPISchemaVisitor):
 
     def __init__(self, _: str):
         super().__init__()
-        types = ["alternate", "enum", "helper", "struct"]
+        types = ["alternate", "enum", "helper", "struct", "union"]
         self.target = {name: "" for name in types}
         self.objects_seen = {}
         self.schema = None
@@ -530,10 +689,10 @@  def visit_object_type(self: QAPISchemaGenGolangVisitor,
                           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"]):
+                info.defn_meta not in ["struct", "union"]):
             return
 
         # Base structs are embed
@@ -566,6 +725,11 @@  def visit_object_type(self: QAPISchemaGenGolangVisitor,
                                                              base,
                                                              members,
                                                              variants)
+        if info.defn_meta == "union":
+            self.target[info.defn_meta] += qapi_to_golang_methods_union(self,
+                                                                        name,
+                                                                        base,
+                                                                        variants)
 
     def visit_alternate_type(self: QAPISchemaGenGolangVisitor,
                              name: str,