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 v4 02/11] qapi: golang: Generate enum type
Date: Fri, 14 Feb 2025 21:29:35 +0100 [thread overview]
Message-ID: <20250214202944.69897-3-victortoso@redhat.com> (raw)
In-Reply-To: <20250214202944.69897-1-victortoso@redhat.com>
This patch handles QAPI enum types and generates its equivalent in Go.
We sort the output based on enum's type name.
Enums are being handled as strings in Golang.
1. For each QAPI enum, we will define a string type in Go to be the
assigned type of this specific enum.
2. Naming: CamelCase will be used in any identifier that we want to
export, which is everything.
Example:
qapi:
| ##
| # @DisplayProtocol:
| #
| # Display protocols which support changing password options.
| #
| # Since: 7.0
| ##
| { 'enum': 'DisplayProtocol',
| 'data': [ 'vnc', 'spice' ] }
go:
| // Display protocols which support changing password options.
| //
| // Since: 7.0
| type DisplayProtocol string
|
| const (
| DisplayProtocolVnc DisplayProtocol = "vnc"
| DisplayProtocolSpice DisplayProtocol = "spice"
| )
Signed-off-by: Victor Toso <victortoso@redhat.com>
---
scripts/qapi/golang/golang.py | 185 +++++++++++++++++++++++++++++++++-
1 file changed, 183 insertions(+), 2 deletions(-)
diff --git a/scripts/qapi/golang/golang.py b/scripts/qapi/golang/golang.py
index 333741b47b..f074ec1f6f 100644
--- a/scripts/qapi/golang/golang.py
+++ b/scripts/qapi/golang/golang.py
@@ -13,7 +13,7 @@
# Just for type hint on self
from __future__ import annotations
-import os, shutil
+import os, shutil, textwrap
from typing import List, Optional
from ..schema import (
@@ -30,6 +30,65 @@
)
from ..source import QAPISourceInfo
+TEMPLATE_GENERATED_HEADER = """
+/*
+ * Copyright 2025 Red Hat, Inc.
+ * SPDX-License-Identifier: (MIT-0 and GPL-2.0-or-later)
+ */
+
+/****************************************************************************
+ * THIS CODE HAS BEEN GENERATED. DO NOT CHANGE IT DIRECTLY *
+ ****************************************************************************/
+package {package_name}
+"""
+
+TEMPLATE_GO_IMPORTS = """
+import (
+{imports}
+)
+"""
+
+TEMPLATE_ENUM = """
+type {name} string
+
+const (
+{fields}
+)
+"""
+
+
+# Takes the documentation object of a specific type and returns
+# that type's documentation and its member's docs.
+def qapi_to_golang_struct_docs(doc: QAPIDoc) -> (str, Dict[str, str]):
+ if doc is None:
+ return "", {}
+
+ cmt = "// "
+ fmt = textwrap.TextWrapper(
+ width=70, initial_indent=cmt, subsequent_indent=cmt
+ )
+ main = fmt.fill(doc.body.text)
+
+ for section in doc.sections:
+ # TODO is not a relevant section to Go applications
+ if section.tag in ["TODO"]:
+ continue
+
+ if main != "":
+ # Give empty line as space for the tag.
+ main += "\n//\n"
+
+ tag = "" if section.tag is None else f"{section.tag}: "
+ text = section.text.replace(" ", " ")
+ main += fmt.fill(f"{tag}{text}")
+
+ fields = {}
+ for key, value in doc.args.items():
+ if len(value.text) > 0:
+ fields[key] = " ".join(value.text.replace("\n", " ").split())
+
+ return main, fields
+
def gen_golang(schema: QAPISchema, output_dir: str, prefix: str) -> None:
vis = QAPISchemaGenGolangVisitor(prefix)
@@ -37,20 +96,90 @@ def gen_golang(schema: QAPISchema, output_dir: str, prefix: str) -> None:
vis.write(output_dir)
+def qapi_to_field_name_enum(name: str) -> str:
+ return name.title().replace("-", "")
+
+
+def fetch_indent_blocks_over_enum_with_docs(
+ name: str, members: List[QAPISchemaEnumMember], docfields: Dict[str, str]
+) -> Tuple[int]:
+ maxname = 0
+ blocks: List[int] = [0]
+ for member in members:
+ # For simplicity, every time we have doc, we add a new indent block
+ hasdoc = member.name is not None and member.name in docfields
+
+ enum_name = f"{name}{qapi_to_field_name_enum(member.name)}"
+ maxname = (
+ max(maxname, len(enum_name)) if not hasdoc else len(enum_name)
+ )
+
+ if hasdoc:
+ blocks.append(maxname)
+ else:
+ blocks[-1] = maxname
+
+ return blocks
+
+
+def generate_content_from_dict(data: dict[str, str]) -> str:
+ content = ""
+
+ for name in sorted(data):
+ content += data[name]
+
+ return content.replace("\n\n\n", "\n\n")
+
+
+def generate_template_imports(words: List[str]) -> str:
+ if len(words) == 0:
+ return ""
+
+ if len(words) == 1:
+ return '\nimport "{words[0]}"\n'
+
+ return TEMPLATE_GO_IMPORTS.format(
+ imports="\n".join(f'\t"{w}"' for w in words)
+ )
+
+
class QAPISchemaGenGolangVisitor(QAPISchemaVisitor):
# pylint: disable=too-many-arguments
def __init__(self, _: str):
super().__init__()
gofiles = ("protocol.go",)
+ # Map each qapi type to the necessary Go imports
+ types = {
+ "enum": [],
+ }
+
self.schema: QAPISchema
self.golang_package_name = "qapi"
self.duplicate = list(gofiles)
+ self.enums: dict[str, str] = {}
+ self.docmap = {}
+
+ self.types = dict.fromkeys(types, "")
+ self.types_import = types
def visit_begin(self, schema: QAPISchema) -> None:
self.schema = schema
+ # iterate once in schema.docs to map doc objects to its name
+ for doc in schema.docs:
+ if doc.symbol is None:
+ continue
+ self.docmap[doc.symbol] = doc
+
+ for qapitype, imports in self.types_import.items():
+ self.types[qapitype] = TEMPLATE_GENERATED_HEADER[1:].format(
+ package_name=self.golang_package_name
+ )
+ self.types[qapitype] += generate_template_imports(imports)
+
def visit_end(self) -> None:
del self.schema
+ self.types["enum"] += generate_content_from_dict(self.enums)
def visit_object_type(
self,
@@ -83,7 +212,51 @@ def visit_enum_type(
members: List[QAPISchemaEnumMember],
prefix: Optional[str],
) -> None:
- pass
+ assert name not in self.enums
+ doc = self.docmap.get(name, None)
+ maindoc, docfields = qapi_to_golang_struct_docs(doc)
+
+ # The logic below is to generate QAPI enums as blocks of Go consts
+ # each with its own type for type safety inside Go applications.
+ #
+ # Block of const() blocks are vertically indented so we have to
+ # first iterate over all names to calculate space between
+ # $var_name and $var_type. This is achieved by helper function
+ # @fetch_indent_blocks_over_enum_with_docs()
+ #
+ # A new indentation block is defined by empty line or a comment.
+
+ indent_block = iter(
+ fetch_indent_blocks_over_enum_with_docs(name, members, docfields)
+ )
+ maxname = next(indent_block)
+ fields = ""
+ for index, member in enumerate(members):
+ # For simplicity, every time we have doc, we go to next indent block
+ hasdoc = member.name is not None and member.name in docfields
+
+ if hasdoc:
+ maxname = next(indent_block)
+
+ enum_name = f"{name}{qapi_to_field_name_enum(member.name)}"
+ name2type = " " * (maxname - len(enum_name) + 1)
+
+ if hasdoc:
+ docstr = (
+ textwrap.TextWrapper(width=80)
+ .fill(docfields[member.name])
+ .replace("\n", "\n\t// ")
+ )
+ fields += f"""\t// {docstr}\n"""
+
+ fields += f"""\t{enum_name}{name2type}{name} = "{member.name}"\n"""
+
+ if maindoc != "":
+ maindoc = f"\n{maindoc}"
+
+ self.enums[name] = maindoc + TEMPLATE_ENUM.format(
+ name=name, fields=fields[:-1]
+ )
def visit_array_type(
self,
@@ -133,3 +306,11 @@ def write(self, outdir: str) -> None:
srcpath = os.path.join(srcdir, filename)
dstpath = os.path.join(targetpath, filename)
shutil.copyfile(srcpath, dstpath)
+
+ # Types to be generated
+ for qapitype, content in self.types.items():
+ gofile = f"gen_type_{qapitype}.go"
+ pathname = os.path.join(targetpath, gofile)
+
+ with open(pathname, "w", encoding="utf8") as outfile:
+ outfile.write(content)
--
2.48.1
next prev parent reply other threads:[~2025-02-14 20:31 UTC|newest]
Thread overview: 18+ messages / expand[flat|nested] mbox.gz Atom feed top
2025-02-14 20:29 [PATCH v4 00/11] Victor Toso
2025-02-14 20:29 ` [PATCH v4 01/11] qapi: golang: first level unmarshalling type Victor Toso
2025-02-14 20:29 ` Victor Toso [this message]
2025-02-14 20:29 ` [PATCH v4 03/11] qapi: golang: Generate alternate types Victor Toso
2025-02-14 20:29 ` [PATCH v4 04/11] qapi: golang: Generate struct types Victor Toso
2025-02-14 20:29 ` [PATCH v4 05/11] qapi: golang: structs: Address nullable members Victor Toso
2025-02-14 20:29 ` [PATCH v4 06/11] qapi: golang: Generate union type Victor Toso
2025-02-14 20:29 ` [PATCH v4 07/11] qapi: golang: Generate event type Victor Toso
2025-02-14 20:29 ` [PATCH v4 08/11] qapi: golang: Generate Event interface Victor Toso
2025-02-14 20:29 ` [PATCH v4 09/11] qapi: golang: Generate command type Victor Toso
2025-02-14 20:29 ` [PATCH v4 10/11] qapi: golang: Generate Command sync/async interfaces Victor Toso
2025-02-14 20:29 ` [PATCH v4 11/11] docs: add notes on Golang code generator Victor Toso
2025-02-17 13:13 ` [PATCH v4 00/11] Daniel P. Berrangé
2025-02-20 8:06 ` Markus Armbruster
2025-02-17 14:58 ` Daniel P. Berrangé
2025-02-17 16:52 ` Victor Toso
2025-03-07 11:25 ` Victor Toso
2025-03-07 11:33 ` Daniel P. Berrangé
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=20250214202944.69897-3-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).