From nobody Mon Apr 7 00:45:28 2025 Delivered-To: importer@patchew.org Authentication-Results: mx.zohomail.com; dkim=pass; spf=pass (zohomail.com: domain of gnu.org designates 209.51.188.17 as permitted sender) smtp.mailfrom=qemu-devel-bounces+importer=patchew.org@nongnu.org; dmarc=pass(p=none dis=none) header.from=redhat.com ARC-Seal: i=1; a=rsa-sha256; t=1736506304; cv=none; d=zohomail.com; s=zohoarc; b=F9HBeFja+Kp1WDWm8sslJYDMMPyPFEDX3tDLymyuoqzmyiOiwg6I4t2X7f3TvOw6IGs/WpvxcD2pruCcLJhHvr0YiXGUHpmRhkwix5jiQOS/N6yKKuKHiVlfFRgr688LcasTqiWTTXxdxicYFaqfHYBm6GT99RI4ED9mDgjhTVM= ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed; d=zohomail.com; s=zohoarc; t=1736506304; h=Content-Transfer-Encoding:Cc:Cc:Date:Date:From:From:In-Reply-To:List-Subscribe:List-Post:List-Id:List-Archive:List-Help:List-Unsubscribe:MIME-Version:Message-ID:References:Sender:Subject:Subject:To:To:Message-Id:Reply-To; bh=UbyH8v5IB+LF0JpFVIwtVr25iA2MVfBysA/oCZIvUvQ=; b=nYK9f+4CSQeCxuNjTJCoIAh3jxD+CX5PpeAiOuEQnEZDF5zZnAIfkk1NcpOpzLgB3DdVHleChYhk/NtKLVRtFy09yxgarGKwdZ1gVPTbltOJKwwPFRB77y2DiwUy3pHGYZyhNyApmAoZoYJyVo/OGSKBEK/UKZotF62bZOeiu+A= ARC-Authentication-Results: i=1; mx.zohomail.com; dkim=pass; spf=pass (zohomail.com: domain of gnu.org designates 209.51.188.17 as permitted sender) smtp.mailfrom=qemu-devel-bounces+importer=patchew.org@nongnu.org; dmarc=pass header.from= (p=none dis=none) Return-Path: Received: from lists.gnu.org (lists.gnu.org [209.51.188.17]) by mx.zohomail.com with SMTPS id 1736506304719192.61485137558145; Fri, 10 Jan 2025 02:51:44 -0800 (PST) Received: from localhost ([::1] helo=lists1p.gnu.org) by lists.gnu.org with esmtp (Exim 4.90_1) (envelope-from ) id 1tWCb3-0005K7-7n; Fri, 10 Jan 2025 05:50:33 -0500 Received: from eggs.gnu.org ([2001:470:142:3::10]) by lists.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.90_1) (envelope-from ) id 1tWCam-0005E5-Us for qemu-devel@nongnu.org; Fri, 10 Jan 2025 05:50:18 -0500 Received: from us-smtp-delivery-124.mimecast.com ([170.10.133.124]) by eggs.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.90_1) (envelope-from ) id 1tWCak-0001HB-1M for qemu-devel@nongnu.org; Fri, 10 Jan 2025 05:50:16 -0500 Received: from mx-prod-mc-03.mail-002.prod.us-west-2.aws.redhat.com (ec2-54-186-198-63.us-west-2.compute.amazonaws.com [54.186.198.63]) by relay.mimecast.com with ESMTP with STARTTLS (version=TLSv1.3, cipher=TLS_AES_256_GCM_SHA384) id us-mta-6-uzdwYg6JMZKQR3SCjL0q2A-1; Fri, 10 Jan 2025 05:50:10 -0500 Received: from mx-prod-int-05.mail-002.prod.us-west-2.aws.redhat.com (mx-prod-int-05.mail-002.prod.us-west-2.aws.redhat.com [10.30.177.17]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits) server-digest SHA256) (No client certificate requested) by mx-prod-mc-03.mail-002.prod.us-west-2.aws.redhat.com (Postfix) with ESMTPS id 3FCC819560B1 for ; Fri, 10 Jan 2025 10:50:09 +0000 (UTC) Received: from tapioca.redhat.com (unknown [10.45.225.126]) by mx-prod-int-05.mail-002.prod.us-west-2.aws.redhat.com (Postfix) with ESMTP id 63FB21955BE3; Fri, 10 Jan 2025 10:50:07 +0000 (UTC) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=redhat.com; s=mimecast20190719; t=1736506213; h=from:from:reply-to:subject:subject:date:date:message-id:message-id: to:to:cc:cc:mime-version:mime-version: content-transfer-encoding:content-transfer-encoding: in-reply-to:in-reply-to:references:references; bh=UbyH8v5IB+LF0JpFVIwtVr25iA2MVfBysA/oCZIvUvQ=; b=ebHY5VI/p9FZmo5M7TBbe8DxPHtR+StFyH92Oq6vKL0LnzumlCPKWJDLkcmwtEeiTMPjG/ VYwfzDJbdsuqEatl/T7ECzpsgzRPEBKSRQFr/0eTwEV7dHnnAVVOGS3k2Ed0fsux1op/Qt Uu1aKWhcGNUcNdQdHqVpz23yuCgxpjo= X-MC-Unique: uzdwYg6JMZKQR3SCjL0q2A-1 X-Mimecast-MFC-AGG-ID: uzdwYg6JMZKQR3SCjL0q2A From: Victor Toso To: qemu-devel@nongnu.org Cc: Markus Armbruster , John Snow , =?UTF-8?q?Daniel=20P=20=2E=20Berrang=C3=A9?= , Andrea Bolognani Subject: [PATCH v3 7/8] qapi: golang: Generate command type Date: Fri, 10 Jan 2025 11:49:45 +0100 Message-ID: <20250110104946.74960-8-victortoso@redhat.com> In-Reply-To: <20250110104946.74960-1-victortoso@redhat.com> References: <20250110104946.74960-1-victortoso@redhat.com> MIME-Version: 1.0 Content-Transfer-Encoding: quoted-printable X-Scanned-By: MIMEDefang 3.0 on 10.30.177.17 Received-SPF: pass (zohomail.com: domain of gnu.org designates 209.51.188.17 as permitted sender) client-ip=209.51.188.17; envelope-from=qemu-devel-bounces+importer=patchew.org@nongnu.org; helo=lists.gnu.org; Received-SPF: pass client-ip=170.10.133.124; envelope-from=victortoso@redhat.com; helo=us-smtp-delivery-124.mimecast.com X-Spam_score_int: -24 X-Spam_score: -2.5 X-Spam_bar: -- X-Spam_report: (-2.5 / 5.0 requ) BAYES_00=-1.9, DKIMWL_WL_HIGH=-0.436, DKIM_SIGNED=0.1, DKIM_VALID=-0.1, DKIM_VALID_AU=-0.1, DKIM_VALID_EF=-0.1, RCVD_IN_DNSWL_NONE=-0.0001, RCVD_IN_MSPIKE_H2=-0.001, RCVD_IN_VALIDITY_CERTIFIED_BLOCKED=0.001, RCVD_IN_VALIDITY_RPBL_BLOCKED=0.001, SPF_HELO_NONE=0.001, SPF_PASS=-0.001, WEIRD_QUOTING=0.001 autolearn=ham autolearn_force=no X-Spam_action: no action X-BeenThere: qemu-devel@nongnu.org X-Mailman-Version: 2.1.29 Precedence: list List-Id: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: qemu-devel-bounces+importer=patchew.org@nongnu.org Sender: qemu-devel-bounces+importer=patchew.org@nongnu.org X-ZohoMail-DKIM: pass (identity @redhat.com) X-ZM-MESSAGEID: 1736506306550116600 Content-Type: text/plain; charset="utf-8" This patch handles QAPI command types and generates data structures in Go that handles it. This patch also generates the Command's return type. Each command has a specific type for its expected return value. 1. Command: i. Naming: Every command type has a Command suffix. ii. Id: Every command has a MessageId field of string type. iii. Every command implements the Command interface. iv. The Command interface includes GetReturnType() which returns the expected return type for that Command 2. CommandReturn: i. Naming: Every command return type has a CommandReturn suffix ii. Id: Every command return has a MessageId field of string type. iii. Every command return implements the CommandReturn interface. * Example: qapi: | ## | # @set_password: | # | # Set the password of a remote display server. | # | # Errors: | # - If Spice is not enabled, DeviceNotFound | # | # Since: 0.14 | # | # .. qmp-example:: | # | # -> { "execute": "set_password", "arguments": { "protocol": "vnc", | # "password": "secret= " } } | # <- { "return": {} } | ## | { 'command': 'set_password', 'boxed': true, 'data': 'SetPasswordOptions'= } go: | // Set the password of a remote display server. | // | // Errors: - If Spice is not enabled, DeviceNotFound | // | // Since: 0.14 | // | // .. qmp-example:: -> { "execute": "set_password", "arguments": { | // "protocol": "vnc", "password": "secret" } | // } <- { "return": {} } | type SetPasswordCommand struct { | SetPasswordOptions | MessageId string `json:"-"` | } | | type SetPasswordCommandReturn struct { | MessageId string `json:"id,omitempty"` | Error *QAPIError `json:"error,omitempty"` | } usage: | input :=3D `{"execute":"set_password",` + | `"arguments":{"protocol":"vnc",` + | `"password":"secret"}}` | | // Straight forward if you know the event type | { | c :=3D SetPasswordCommand{} | err :=3D json.Unmarshal([]byte(input), &c) | if err !=3D nil { | panic(err) | } | // c.Password =3D=3D "secret" | } | | // Generic way, using Command interface and helper function | if cmd, err :=3D GetCommandType(input); err !=3D nil { | // handle bad data or unknown event | } | | if err :=3D json.Unmarshal(input, cmd); err !=3D nil { | // handle bad data or unknown event fields | } | | if c, ok :=3D cmd.(*SetPasswordCommand); ok { | // c.Password =3D=3D "secret" | } Signed-off-by: Victor Toso --- scripts/qapi/golang.py | 233 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 231 insertions(+), 2 deletions(-) diff --git a/scripts/qapi/golang.py b/scripts/qapi/golang.py index 6a8f5cf230..085cdd89f6 100644 --- a/scripts/qapi/golang.py +++ b/scripts/qapi/golang.py @@ -43,6 +43,15 @@ """ =20 TEMPLATE_HELPER =3D """ +type QAPIError struct { + Class string `json:"class"` + Description string `json:"desc"` +} + +func (err *QAPIError) Error() string { + return err.Description +} + // Creates a decoder that errors on unknown Fields // Returns nil if successfully decoded @from payload to @into type // Returns error if failed to decode @from payload to @into type @@ -305,6 +314,111 @@ }} """ =20 +TEMPLATE_COMMAND_METHODS =3D """ +func (s {type_name}) MarshalJSON() ([]byte, error) {{ + type Alias {type_name} + return marshalCommand(Alias(s), "{name}", s.MessageId) +}} + +func (s *{type_name}) UnmarshalJSON(data []byte) error {{ + type Alias {type_name} + tmp :=3D struct {{ + MessageId string `json:"id,omitempty"` + Name string `json:"execute"` + Args Alias `json:"arguments"` + }}{{}} + + if err :=3D json.Unmarshal(data, &tmp); err !=3D nil {{ + return fmt.Errorf("Failed to unmarshal: %s", string(data)) + }} + + if !strings.EqualFold(tmp.Name, "{name}") {{ + return fmt.Errorf("Command type does not match with %s", tmp.Name) + }} + + *s =3D {type_name}(tmp.Args) + s.MessageId =3D tmp.MessageId + return nil +}} + +func (s *{type_name}) GetReturnType() CommandReturn {{ + return &{cmd_ret_type_name}{{}} +}} +""" + +TEMPLATE_COMMAND =3D """ +type Command interface {{ + json.Marshaler + json.Unmarshaler + GetReturnType() CommandReturn +}} + +func marshalCommand(obj interface{{}}, name, id string) ([]byte, error) {{ + m :=3D make(map[string]any) + m["execute"] =3D name + if len(id) > 0 {{ + m["id"] =3D id + }} + if bytes, err :=3D json.Marshal(obj); err !=3D nil {{ + return []byte{{}}, err + }} else if len(bytes) > 2 {{ + m["arguments"] =3D obj + }} + return json.Marshal(m) +}} + +func GetCommandType(data []byte) (Command, error) {{ + tmp :=3D struct {{ + Name string `json:"execute"` + }}{{}} + + if err :=3D json.Unmarshal(data, &tmp); err !=3D nil {{ + return nil, fmt.Errorf("Failed to decode command: %s", string(data= )) + }} + + switch tmp.Name {{{cases} + }} + return nil, errors.New("Failed to recognize command") +}} +""" + +TEMPLATE_COMMAND_RETURN =3D """ +type CommandReturn interface { + json.Marshaler +} + +func marshalCommandReturn(result, qerror any, id string) ([]byte, error) { + m :=3D make(map[string]any) + if len(id) > 0 { + m["id"] =3D id + } + if qerror !=3D nil && qerror.(*QAPIError) !=3D nil { + m["error"] =3D qerror + } else if result !=3D nil { + m["return"] =3D result + } else { + m["return"] =3D struct{}{} + } + return json.Marshal(m) +} +""" + +TEMPLATE_COMMAND_RETURN_METHODS =3D """ +func (r {cmd_ret_type_name}) MarshalJSON() ([]byte, error) {{ + return marshalCommandReturn({cmd_ret_field}, r.Error, r.MessageId) +}} +""" + +TEMPLATE_COMMAND_RETURN_MARSHAL_EMPTY =3D """ +func (r {cmd_ret_ype_name}) MarshalJSON() ([]byte, error) {{ + if r.Error !=3D nil {{ + type Alias {cmd_ret_type_name} + return json.Marshal(Alias(r)) + }} + return []byte(`{{"return":{{}}}}`), nil +}} +""" + =20 # Takes the documentation object of a specific type and returns # that type's documentation and its member's docs. @@ -386,7 +500,7 @@ def qapi_to_go_type_name(name: str, meta: Optional[str]= =3D None) -> str: name +=3D "".join(word.title() for word in words[1:]) =20 # Handle specific meta suffix - types =3D ["event"] + types =3D ["event", "command", "command return"] if meta in types: name =3D name[:-3] if name.endswith("Arg") else name name +=3D meta.title().replace(" ", "") @@ -855,6 +969,10 @@ def qapi_to_golang_struct( "tag": """`json:"-"`""", }, ) + elif info.defn_meta =3D=3D "command": + fields.insert( + 0, {"name": "MessageId", "type": "string", "tag": """`json:"-"= `"""} + ) =20 if members: for member in members: @@ -1089,6 +1207,21 @@ def generate_template_alternate( return "\n" + content =20 =20 +def generate_template_command(commands: dict[str, Tuple[str, str]]) -> str: + cases =3D "" + content =3D "" + for name in sorted(commands): + type_name, gocode =3D commands[name] + content +=3D gocode + cases +=3D f""" + case "{name}": + return &{type_name}{{}}, nil +""" + content +=3D string_to_code(TEMPLATE_COMMAND.format(cases=3Dcases)) + content +=3D string_to_code(TEMPLATE_COMMAND_RETURN) + return content + + def generate_template_event(events: dict[str, Tuple[str, str]]) -> str: content =3D "" cases =3D "" @@ -1138,6 +1271,7 @@ def __init__(self, _: str): super().__init__() types =3D ( "alternate", + "command", "enum", "event", "helper", @@ -1147,6 +1281,7 @@ def __init__(self, _: str): self.target =3D dict.fromkeys(types, "") self.schema: QAPISchema self.events: dict[str, Tuple[str, str]] =3D {} + self.commands: dict[str, Tuple[str, str]] =3D {} self.golang_package_name =3D "qapi" self.enums: dict[str, str] =3D {} self.alternates: dict[str, str] =3D {} @@ -1192,6 +1327,15 @@ def visit_begin(self, schema: QAPISchema) -> None: "fmt" "strings" ) +""" + elif target =3D=3D "command": + imports +=3D """ +import ( + "encoding/json" + "errors" + "fmt" + "strings" +) """ else: imports +=3D """ @@ -1214,6 +1358,7 @@ def visit_end(self) -> None: self.target["struct"] +=3D generate_content_from_dict(self.structs) self.target["union"] +=3D generate_content_from_dict(self.unions) self.target["event"] +=3D generate_template_event(self.events) + self.target["command"] +=3D generate_template_command(self.command= s) =20 def visit_object_type( self, @@ -1358,7 +1503,91 @@ def visit_command( allow_preconfig: bool, coroutine: bool, ) -> None: - pass + assert name =3D=3D info.defn_name + assert name not in self.commands + + type_name =3D qapi_to_go_type_name(name, info.defn_meta) + + doc =3D self.docmap.get(name, None) + type_doc, _ =3D qapi_to_golang_struct_docs(doc) + + cmd_ret_type_name =3D qapi_to_go_type_name(name, "command return") + cmd_ret_field =3D "nil" + retargs: List[dict[str:str]] =3D [ + { + "name": "MessageId", + "type": "string", + "tag": """`json:"id,omitempty"`""", + }, + { + "name": "Error", + "type": "*QAPIError", + "tag": """`json:"error,omitempty"`""", + }, + ] + if ret_type: + cmd_ret_field =3D "r.Result" + ret_type_name =3D qapi_schema_type_to_go_type(ret_type.name) + isptr =3D "*" if ret_type_name[0] not in "*[" else "" + retargs.append( + { + "name": "Result", + "type": f"{isptr}{ret_type_name}", + "tag": """`json:"return"`""", + } + ) + + content =3D "" + if boxed or not arg_type or not qapi_name_is_object(arg_type.name): + args: List[dict[str:str]] =3D [] + if arg_type: + args.append( + { + "name": f"{arg_type.name}", + } + ) + args.append( + { + "name": "MessageId", + "type": "string", + "tag": """`json:"-"`""", + } + ) + content +=3D string_to_code( + generate_struct_type(type_name, type_doc=3Dtype_doc, args= =3Dargs) + ) + else: + assert isinstance(arg_type, QAPISchemaObjectType) + content +=3D string_to_code( + qapi_to_golang_struct( + self, + name, + arg_type.info, + arg_type.ifcond, + arg_type.features, + arg_type.base, + arg_type.members, + arg_type.branches, + ) + ) + + content +=3D string_to_code( + TEMPLATE_COMMAND_METHODS.format( + name=3Dname, + type_name=3Dtype_name, + cmd_ret_type_name=3Dcmd_ret_type_name, + ) + ) + content +=3D string_to_code( + generate_struct_type(cmd_ret_type_name, args=3Dretargs) + ) + content +=3D string_to_code( + TEMPLATE_COMMAND_RETURN_METHODS.format( + cmd_ret_type_name=3Dcmd_ret_type_name, + cmd_ret_field=3Dcmd_ret_field, + ) + ) + self.commands[name] =3D (type_name, content) =20 def visit_event( self, --=20 2.47.1