From: John Snow <jsnow@redhat.com>
To: qemu-devel@nongnu.org
Cc: crosa@redhat.com, John Snow <jsnow@redhat.com>,
ehabkost@redhat.com, stefanha@redhat.com, armbru@redhat.com
Subject: [PATCH RFC 4/7] message: add QMP Message type
Date: Tue, 13 Apr 2021 11:55:50 -0400 [thread overview]
Message-ID: <20210413155553.2660523-5-jsnow@redhat.com> (raw)
In-Reply-To: <20210413155553.2660523-1-jsnow@redhat.com>
This is an abstraction that represents a single message either sent to
or received from the server. It is used to subclass the
AsyncProtocol(Generic[T]) type.
It was written such that it can be populated by either raw data or by a
dict, with the other form being generated on-demand, as-needed.
It behaves almost exactly like a dict, but has some extra methods and a
special constructor. (It should quack fairly convincingly.)
Signed-off-by: John Snow <jsnow@redhat.com>
---
message.py | 196 +++++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 196 insertions(+)
create mode 100644 message.py
diff --git a/message.py b/message.py
new file mode 100644
index 0000000..5c7e828
--- /dev/null
+++ b/message.py
@@ -0,0 +1,196 @@
+"""
+QMP Message format and errors.
+
+This module provides the `Message` class, which represents a single QMP
+message sent to or from the server. Several error-classes that depend on
+knowing the format of this message are also included here.
+"""
+
+import json
+from json import JSONDecodeError
+from typing import (
+ Dict,
+ ItemsView,
+ Iterable,
+ KeysView,
+ Optional,
+ Union,
+ ValuesView,
+)
+
+from error import (
+ DeserializationError,
+ ProtocolError,
+ UnexpectedTypeError,
+)
+
+
+class Message:
+ """
+ Represents a single QMP protocol message.
+
+ QMP uses JSON objects as its basic communicative unit; so this
+ object behaves like a MutableMapping. It may be instantiated from
+ either another mapping (like a dict), or from raw bytes that still
+ need to be deserialized.
+
+ :param value: Initial value, if any.
+ :param eager: When true, attempt to serialize (or deserialize) the
+ initial value immediately, such that conversion exceptions
+ are raised during the call to the initialization method.
+ """
+ # TODO: make Message properly a MutableMapping so it can be typed as such?
+ def __init__(self,
+ value: Union[bytes, Dict[str, object]] = b'', *,
+ eager: bool = True):
+ self._data: Optional[bytes] = None
+ self._obj: Optional[Dict[str, object]] = None
+
+ if isinstance(value, bytes):
+ self._data = value
+ if eager:
+ self._obj = self._deserialize(self._data)
+ else:
+ self._obj = value
+ if eager:
+ self._data = self._serialize(self._obj)
+
+ @classmethod
+ def _serialize(cls, value: object) -> bytes:
+ """
+ Serialize a JSON object as bytes.
+
+ :raises: ValueError, TypeError from the json library.
+ """
+ return json.dumps(value, separators=(',', ':')).encode('utf-8')
+
+ @classmethod
+ def _deserialize(cls, data: bytes) -> Dict[str, object]:
+ """
+ Deserialize JSON bytes into a native python dict.
+
+ :raises: DeserializationError if JSON deserialization
+ fails for any reason.
+ :raises: UnexpectedTypeError if data does not represent
+ a JSON object.
+ """
+ try:
+ obj = json.loads(data)
+ except JSONDecodeError as err:
+ emsg = "Failed to deserialize QMP message."
+ raise DeserializationError(emsg, data) from err
+ if not isinstance(obj, dict):
+ raise UnexpectedTypeError(
+ "Incoming QMP message is not a JSON object.",
+ data
+ )
+ return obj
+
+ @property
+ def data(self) -> bytes:
+ """
+ bytes representing this QMP message.
+
+ Generated on-demand if required.
+ """
+ if self._data is None:
+ self._data = self._serialize(self._obj or {})
+ return self._data
+
+ @property
+ def _object(self) -> Dict[str, object]:
+ """
+ dict representing this QMP message.
+
+ Generated on-demand if required; Private because it returns an
+ object that could be used to validate the internal state of the
+ Message object.
+ """
+ if self._obj is None:
+ self._obj = self._deserialize(self._data or b'')
+ return self._obj
+
+ def __str__(self) -> str:
+ """Pretty-printed representation of this QMP message."""
+ return json.dumps(self._object, indent=2)
+
+ def __bytes__(self) -> bytes:
+ return self.data
+
+ def __contains__(self, item: str) -> bool: # Container, Collection
+ return item in self._object
+
+ def __iter__(self) -> Iterable[str]: # Iterable, Collection, Mapping
+ return iter(self._object)
+
+ def __len__(self) -> int: # Sized, Collection, Mapping
+ return len(self._object)
+
+ def __getitem__(self, key: str) -> object: # Mapping
+ return self._object[key]
+
+ def __setitem__(self, key: str, value: object) -> None: # MutableMapping
+ self._object[key] = value
+ self._data = None
+
+ def __delitem__(self, key: str) -> None: # MutableMapping
+ del self._object[key]
+ self._data = None
+
+ def keys(self) -> KeysView[str]:
+ """Return a KeysView object containing all field names."""
+ return self._object.keys()
+
+ def items(self) -> ItemsView[str, object]:
+ """Return an ItemsView object containing all key:value pairs."""
+ return self._object.items()
+
+ def values(self) -> ValuesView[object]:
+ """Return a ValuesView object containing all field values."""
+ return self._object.values()
+
+ def get(self, key: str,
+ default: Optional[object] = None) -> Optional[object]:
+ """Get the value for a single key."""
+ return self._object.get(key, default)
+
+
+class MsgProtocolError(ProtocolError):
+ """Abstract error class for protocol errors that have a JSON object."""
+ def __init__(self, error_message: str, msg: Message):
+ super().__init__(error_message)
+ self.msg = msg
+
+ def __str__(self) -> str:
+ return "\n".join([
+ super().__str__(),
+ f" Message was: {str(self.msg)}\n",
+ ])
+
+
+class ObjectTypeError(MsgProtocolError):
+ """
+ Incoming message was a JSON object, but has an unexpected data shape.
+
+ e.g.: A malformed greeting may cause this error.
+ """
+
+
+# FIXME: Remove this? Current draft simply trashes these replies.
+
+# class OrphanedError(MsgProtocolError):
+# """
+# Received message, but had no queue to deliver it to.
+#
+# e.g.: A reply arrives from the server, but the ID does not match any
+# pending execution requests we are aware of.
+# """
+
+
+class ServerParseError(MsgProtocolError):
+ """
+ Server sent a `ParsingError` message.
+
+ e.g. A reply arrives from the server, but it is missing the "ID"
+ field, which indicates a parsing error on behalf of the server.
+ """
--
2.30.2
next prev parent reply other threads:[~2021-04-13 16:05 UTC|newest]
Thread overview: 21+ messages / expand[flat|nested] mbox.gz Atom feed top
2021-04-13 15:55 [PATCH RFC 0/7] RFC: Asynchronous QMP Draft John Snow
2021-04-13 15:55 ` [PATCH RFC 1/7] util: asyncio-related helpers John Snow
2021-04-13 15:55 ` [PATCH RFC 2/7] error: Error classes and so on John Snow
2021-04-13 15:55 ` [PATCH RFC 3/7] protocol: generic async message-based protocol loop John Snow
2021-04-13 20:00 ` Stefan Hajnoczi
2021-04-14 17:29 ` John Snow
2021-04-15 9:14 ` Stefan Hajnoczi
2021-04-13 15:55 ` John Snow [this message]
2021-04-13 20:07 ` [PATCH RFC 4/7] message: add QMP Message type Stefan Hajnoczi
2021-04-14 17:39 ` John Snow
2021-04-13 15:55 ` [PATCH RFC 5/7] models: Add well-known QMP objects John Snow
2021-04-13 15:55 ` [PATCH RFC 6/7] qmp_protocol: add QMP client implementation John Snow
2021-04-14 5:44 ` Stefan Hajnoczi
2021-04-14 17:50 ` John Snow
2021-04-15 9:23 ` Stefan Hajnoczi
2021-04-13 15:55 ` [PATCH RFC 7/7] linter config John Snow
2021-04-14 6:38 ` [PATCH RFC 0/7] RFC: Asynchronous QMP Draft Stefan Hajnoczi
2021-04-14 19:17 ` John Snow
2021-04-15 9:52 ` Stefan Hajnoczi
2021-04-20 2:26 ` John Snow
2021-04-20 2:47 ` John Snow
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=20210413155553.2660523-5-jsnow@redhat.com \
--to=jsnow@redhat.com \
--cc=armbru@redhat.com \
--cc=crosa@redhat.com \
--cc=ehabkost@redhat.com \
--cc=qemu-devel@nongnu.org \
--cc=stefanha@redhat.com \
/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).