diff mbox series

[v3,6/8] qapi: golang: Generate event type

Message ID 20250110104946.74960-7-victortoso@redhat.com (mailing list archive)
State New
Headers show
Series qapi-go: add generator for Golang interfaces | expand

Commit Message

Victor Toso Jan. 10, 2025, 10:49 a.m. UTC
This patch handles QAPI event types and generates data structures in
Go that handles it.

1. Naming: Every event type has an Event suffix.

2. Timestamp: Every event has a MessageTimestamp field with a
   reference to the Timestamp struct (not included in the QAPI spec
   but defined in docs/interop/qmp-spec.rst)

3. Every event implements the Event interface.

Example:
qapi:
 | ##
 | # @MEMORY_DEVICE_SIZE_CHANGE:
 | #
 | # Emitted when the size of a memory device changes.  Only emitted for
 | # memory devices that can actually change the size (e.g., virtio-mem
 | # due to guest action).
 | #
 | # @id: device's ID
 | #
 | # @size: the new size of memory that the device provides
 | #
 | # @qom-path: path to the device object in the QOM tree (since 6.2)
 | #
 | # .. note:: This event is rate-limited.
 | #
 | # Since: 5.1
 | #
 | # .. qmp-example::
 | #
 | #     <- { "event": "MEMORY_DEVICE_SIZE_CHANGE",
 | #          "data": { "id": "vm0", "size": 1073741824,
 | #                    "qom-path": "/machine/unattached/device[2]" },
 | #          "timestamp": { "seconds": 1588168529, "microseconds": 201316 } }
 | ##
 | { 'event': 'MEMORY_DEVICE_SIZE_CHANGE',
 |   'data': { '*id': 'str', 'size': 'size', 'qom-path' : 'str'} }

go:
 | // Emitted when the size of a memory device changes.  Only emitted for
 | // memory devices that can actually change the size (e.g., virtio-mem
 | // due to guest action).
 | //
 | // .. note:: This event is rate-limited.
 | //
 | // Since: 5.1
 | //
 | // .. qmp-example::    <- { "event": "MEMORY_DEVICE_SIZE_CHANGE",
 | // "data": { "id": "vm0", "size": 1073741824,           "qom-path":
 | // "/machine/unattached/device[2]" },      "timestamp": { "seconds":
 | // 1588168529, "microseconds": 201316 } }
 | type MemoryDeviceSizeChangeEvent struct {
 | 	MessageTimestamp Timestamp `json:"-"`
 | 	// device's ID
 | 	Id *string `json:"id,omitempty"`
 | 	// the new size of memory that the device provides
 | 	Size uint64 `json:"size"`
 | 	// path to the device object in the QOM tree (since 6.2)
 | 	QomPath string `json:"qom-path"`
 | }
 |
 | func (s MemoryDeviceSizeChangeEvent) MarshalJSON() ([]byte, error) {
 |      ...
 | }
 |
 | func (s *MemoryDeviceSizeChangeEvent) UnmarshalJSON(data []byte) error {
 |      ...
 | }

usage:
 | input := `{"event":"MEMORY_DEVICE_SIZE_CHANGE",` +
 | `"timestamp":{"seconds":1588168529,"microseconds":201316},` +
 | `"data":{"id":"vm0","size":1073741824,"qom-path":"/machine/unattached/device[2]"}}`
 |
 | // Straight forward if you know the event type
 | {
 |     mdsc := MemoryDeviceSizeChangeEvent{}
 |     err := json.Unmarshal([]byte(input), &mdsc)
 |     if err != nil {
 |         panic(err)
 |     }
 |     // mdsc.QomPath == "/machine/unattached/device[2]"
 | }
 |
 | // Generic way, using Event interface and helper function
 | if event, err := GetEventType(input); err != nil {
 |     // handle bad data or unknown event
 | }
 |
 | if err := json.Unmarshal(input, event); err != nil {
 |     // handle bad data or unknown event fields
 | }
 |
 | if mdsc, ok := event.(*MemoryDeviceSizeChangeEvent); ok {
 |     // mdsc.QomPath == "/machine/unattached/device[2]"
 | }

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

Patch

diff --git a/scripts/qapi/golang.py b/scripts/qapi/golang.py
index 330891ede9..6a8f5cf230 100644
--- a/scripts/qapi/golang.py
+++ b/scripts/qapi/golang.py
@@ -238,6 +238,73 @@ 
 }}
 """
 
+TEMPLATE_EVENT = """
+type Timestamp struct {{
+    Seconds      int64 `json:"seconds"`
+    Microseconds int64 `json:"microseconds"`
+}}
+
+type Event interface {{
+    json.Marshaler
+    json.Unmarshaler
+}}
+
+func marshalEvent(obj interface{{}}, name string, ts Timestamp) ([]byte, error) {{
+    m := make(map[string]any)
+    m["event"] = name
+    m["timestamp"] = ts
+    if bytes, err := json.Marshal(obj); err != nil {{
+        return []byte{{}}, err
+    }} else if len(bytes) > 2 {{
+        m["data"] = obj
+    }}
+    return json.Marshal(m)
+}}
+
+func GetEventType(data []byte) (Event, error) {{
+    tmp := struct {{
+        Name string `json:"event"`
+    }}{{}}
+
+    if err := json.Unmarshal(data, &tmp); err != nil {{
+        return nil, fmt.Errorf("Failed to unmarshal: %s", string(data))
+    }}
+
+    switch tmp.Name {{{cases}
+    default:
+        return nil, fmt.Errorf("Event %s not match to any type", tmp.Name)
+    }}
+}}
+"""
+
+TEMPLATE_EVENT_METHODS = """
+func (s {type_name}) MarshalJSON() ([]byte, error) {{
+	type Alias {type_name}
+	return marshalEvent(Alias(s), "{name}", s.MessageTimestamp)
+}}
+
+func (s *{type_name}) UnmarshalJSON(data []byte) error {{
+	type Alias {type_name}
+    tmp := struct {{
+        Name string    `json:"event"`
+        Time Timestamp `json:"timestamp"`
+        Data Alias     `json:"data"`
+    }}{{}}
+
+    if err := json.Unmarshal(data, &tmp); err != nil {{
+        return fmt.Errorf("Failed to unmarshal: %s", string(data))
+    }}
+
+    if !strings.EqualFold(tmp.Name, "{name}") {{
+        return fmt.Errorf("Event type does not match with %s", tmp.Name)
+    }}
+
+    *s = {type_name}(tmp.Data)
+    s.MessageTimestamp = tmp.Time
+    return nil
+}}
+"""
+
 
 # Takes the documentation object of a specific type and returns
 # that type's documentation and its member's docs.
@@ -300,7 +367,7 @@  def qapi_to_field_name_enum(name: str) -> str:
     return name.title().replace("-", "")
 
 
-def qapi_to_go_type_name(name: str) -> str:
+def qapi_to_go_type_name(name: str, meta: Optional[str] = None) -> str:
     # We want to keep CamelCase for Golang types. We want to avoid removing
     # already set CameCase names while fixing uppercase ones, eg:
     # 1) q_obj_SocketAddress_base -> SocketAddressBase
@@ -318,6 +385,12 @@  def qapi_to_go_type_name(name: str) -> str:
 
     name += "".join(word.title() for word in words[1:])
 
+    # Handle specific meta suffix
+    types = ["event"]
+    if meta in types:
+        name = name[:-3] if name.endswith("Arg") else name
+        name += meta.title().replace(" ", "")
+
     return name
 
 
@@ -773,6 +846,16 @@  def qapi_to_golang_struct(
     if not doc_enabled:
         type_doc = ""
 
+    if info.defn_meta == "event":
+        fields.insert(
+            0,
+            {
+                "name": "MessageTimestamp",
+                "type": "Timestamp",
+                "tag": """`json:"-"`""",
+            },
+        )
+
     if members:
         for member in members:
             field_doc = docfields.get(member.name, "") if doc_enabled else ""
@@ -827,7 +910,8 @@  def qapi_to_golang_struct(
                 fields.append(field)
                 with_nullable = True if nullable else with_nullable
 
-    type_name = qapi_to_go_type_name(name)
+    type_name = qapi_to_go_type_name(name, info.defn_meta)
+
     content = string_to_code(
         generate_struct_type(
             type_name, type_doc=type_doc, args=fields, indent=indent
@@ -1005,6 +1089,21 @@  def generate_template_alternate(
     return "\n" + content
 
 
+def generate_template_event(events: dict[str, Tuple[str, str]]) -> str:
+    content = ""
+    cases = ""
+    for name in sorted(events):
+        type_name, gocode = events[name]
+        content += gocode
+        cases += f"""
+    case "{name}":
+        return &{type_name}{{}}, nil
+"""
+
+    content += string_to_code(TEMPLATE_EVENT.format(cases=cases))
+    return content
+
+
 def generate_content_from_dict(data: dict[str, str]) -> str:
     content = ""
 
@@ -1040,12 +1139,14 @@  def __init__(self, _: str):
         types = (
             "alternate",
             "enum",
+            "event",
             "helper",
             "struct",
             "union",
         )
         self.target = dict.fromkeys(types, "")
         self.schema: QAPISchema
+        self.events: dict[str, Tuple[str, str]] = {}
         self.golang_package_name = "qapi"
         self.enums: dict[str, str] = {}
         self.alternates: dict[str, str] = {}
@@ -1084,7 +1185,7 @@  def visit_begin(self, schema: QAPISchema) -> None:
                 imports += """
 import "encoding/json"
 """
-            elif target == "helper":
+            elif target == "helper" or target == "event":
                 imports += """
 import (
     "encoding/json"
@@ -1112,6 +1213,7 @@  def visit_end(self) -> None:
         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)
+        self.target["event"] += generate_template_event(self.events)
 
     def visit_object_type(
         self,
@@ -1267,7 +1369,40 @@  def visit_event(
         arg_type: Optional[QAPISchemaObjectType],
         boxed: bool,
     ) -> None:
-        pass
+        assert name == info.defn_name
+        assert name not in self.events
+        type_name = qapi_to_go_type_name(name, info.defn_meta)
+
+        if isinstance(arg_type, QAPISchemaObjectType):
+            content = string_to_code(
+                qapi_to_golang_struct(
+                    self,
+                    name,
+                    info,
+                    arg_type.ifcond,
+                    arg_type.features,
+                    arg_type.base,
+                    arg_type.members,
+                    arg_type.branches,
+                )
+            )
+        else:
+            args: List[dict[str:str]] = []
+            args.append(
+                {
+                    "name": "MessageTimestamp",
+                    "type": "Timestamp",
+                    "tag": """`json:"-"`""",
+                }
+            )
+            content = string_to_code(
+                generate_struct_type(type_name, args=args)
+            )
+
+        content += string_to_code(
+            TEMPLATE_EVENT_METHODS.format(name=name, type_name=type_name)
+        )
+        self.events[name] = (type_name, content)
 
     def write(self, output_dir: str) -> None:
         for module_name, content in self.target.items():