Response
Responses of the byte 0, 1 or 2 followed by one volume in the postcard wire format, added, changed or removed; the byte 3, once, when the listing is whole; the byte 4, alone and first, when the server keeps no volume; or the byte 5 followed by an error as one JSON value, last; and the response finish.
[0][volume, postcard …] per volume added
[1][volume, postcard …] per volume changed, after it was added
[2][volume, postcard …] per volume removed, after it was added
[3] once, when the listing is whole
[4] once, alone: the server keeps no volume
[5][error JSON …] at most once, last
- The sequence. Zero or more responses of the byte
0followed by one volume, one per volume available to the client when the scope opened; exactly one response of the byte3and nothing else; then zero or more responses of the byte0,1or2, each followed by one volume, as volumes are created, edited and deleted; the response finish follows the last of them, when the client stops or the client’s connection ends. Or: the responses sent so far, exactly one response of the byte5followed by an error, and the response finish. Or: exactly one response of the byte4and nothing else, and the response finish. No response follows the error. The server sends no response of the byte1or2before the byte3. - The volume. A payload whose first byte is
0,1or2carries one volume after that byte, running to the end of the payload. The volume is theVolumedefined below, encoded as the postcard reading of Notation provides: itsnameas a varint byte length followed by that many bytes of UTF-8, followed bybytesas a varint, followed bycreatedas a varint, followed bymodeas one byte. - Added. A response of the byte
0states that the volume is available to the client. The server sends it exactly once per name while the volume is listed: for every volume available when the scope opened, before the byte3; for a volume that becomes available after, when it does. - Changed. A response of the byte
1states that the volume’sbytesormodeare now what the response carries. The server sends it only for a volume it has sent as added on this scope and not since as removed, and only whenbytesormodediffer from what it last sent for that name. Thenameand thecreatedof a volume do not change. - Removed. A response of the byte
2states that the volume is no longer available to the client, and carries the volume as the server last sent it. The server sends it only for a volume it has sent as added on this scope and not since as removed, and exactly once for it. - Whole. A payload that is the byte
3and nothing else states that every volume available to the client when the scope opened has been sent as added. A client that wants the listing once reads to this byte and stops. A payload whose first byte is3is read by that byte alone; bytes after it are not read. name. The name of the volume, UTF-8. The name is the only handle of a volume: every request of this specification that names a volume names it by this string. No two volumes listed at once share a name.bytes. The size of the volume, in bytes. For a volume the client created,bytesis the size the client last stated for it, in its volumes::create request or in a later volumes::edit request. The number of bytes in use is reported by volumes::stat.created. The time at which the volume came into being, in seconds since1970-01-01T00:00:00Z, unsigned. What “came into being” marks is the server’s to determine.mode. The mode of the volume, as one byte, the postcard discriminant of the enum:0ispersistent,1isephemeral,2isread_only. For a volume the client created,modeis the mode the client last stated for it, in its volumes::create request or in a later volumes::edit request; for another volume, the mode is the server’s to determine. What each mode means for a container that mounts the volume, for a volumes::fuse_serve of it, and for how many may hold it at once at most, volumes::create states; how many this server lets hold it at once is the mode object the server holds out for the mode, and the listing contains no volume of a mode the server holds out no mode object for. Every container the client mounts the volume in and every FUSE serve of it is bound under this mode. A byte other than0,1and2does not decode.- Order. The order of the volumes is not prescribed. A client reads nothing into the position of a volume.
- A finish with nothing before it. The request was not served, as Endpoints provides, or the client stopped before the server had sent anything. The server does not say which.
- Unsupported. A payload that is the byte
4and nothing else states that the server holds out none of the three mode objects and keeps no volume, as Capabilities provides. It is the only response of the scope, sent before anything is looked up; bytes after it are not read. - The error. A payload whose first byte is
5carries an error after that byte, running to the end of the payload. The error is exactly one JSON value. This revision prescribes nothing about its content. - Malformed. A payload with no byte, a payload whose first byte
is none of
0to5, a payload whose bytes after0,1or2do not decode as one volume, and a payload whose bytes after5are not one JSON value are malformed.
The response is defined by diverge-sdk-rs/src/provider/endpoints/volumes/list/server/response/frame.rs:
//! What a server's response frame carries for a volume listing.
use std::fmt;
use super::Volume;
use crate::wire::decode::Decode;
use crate::wire::encode::{Encode, Writer};
use crate::shared::error::Error;
/// A volume the caller has, as it comes to be listed, changes, or
/// goes; the word that the listing is whole; or the news that the
/// provider could not say.
///
/// A listing is a stream kept open. First every volume the caller
/// has, each as [`Added`](Self::Added); then exactly one
/// [`Listed`](Self::Listed); then, for the scope's life, a volume the
/// caller creates as `Added`, one it edits as
/// [`Changed`](Self::Changed), one it deletes as
/// [`Removed`](Self::Removed). The finish follows the caller's stop,
/// the caller's connection ending, or exactly one
/// [`Error`](Self::Error), and nothing else: a caller with no volume
/// is listed as `Listed` at once, and watched. A payload leads with
/// one byte saying which — `0` for `Added`, `1` for `Changed`, `2`
/// for `Removed`, `3` for `Listed`, `4` for `Unsupported`, `5` for
/// `Error` — and the rest is that variant's own bytes; `Listed` and
/// `Unsupported` carry nothing. A provider that keeps no volumes
/// sends `Unsupported` first and alone, and the finish follows it.
///
/// # A finish with nothing is an answer
///
/// The caller stopped before anything was sent, or the request was
/// not served: a scope that finishes with no response before it is
/// that, not a failure. An [`Error`](Self::Error) is a failure: the
/// provider could not tell — a caller that confuses the two stops
/// asking when it should retry.
#[derive(Debug, Clone, PartialEq)]
pub enum Frame {
/// A volume the caller has, listed. Tag `0`.
///
/// Once per name while the volume is listed: at the opening for
/// every volume the caller has then, and after it for one the
/// caller creates. Nothing promises an order and nothing should
/// be read into one.
Added(Volume),
/// A volume listed already, whose `bytes` or `mode` changed. Tag
/// `1`.
///
/// The volume as it is now. Its `name` and its `created` are what
/// they were: neither changes.
Changed(Volume),
/// A volume listed already, gone. Tag `2`.
///
/// The volume as it was last listed.
Removed(Volume),
/// Every volume the caller had when the scope opened has been
/// listed. Tag `3`.
///
/// Carries nothing — the variant is bare. A caller that wants the
/// listing once stops here; one that watches reads on.
Listed,
/// The provider keeps no volumes: no mode is among its capabilities,
/// see [`info`](crate::provider::endpoints::info), and every volume
/// endpoint answers this. Tag `4`.
///
/// Carries nothing. Nothing is listed; a caller asks another
/// provider.
Unsupported,
/// A failure. Tag `5`.
///
/// See [`shared::error::Error`](crate::shared::error::Error) for
/// why it says so little.
Error(Error),
}
/// Tag for [`Frame::Added`].
const ADDED: u8 = 0;
/// Tag for [`Frame::Changed`].
const CHANGED: u8 = 1;
/// Tag for [`Frame::Removed`].
const REMOVED: u8 = 2;
/// Tag for [`Frame::Listed`].
const LISTED: u8 = 3;
/// Tag for [`Frame::Unsupported`].
const UNSUPPORTED: u8 = 4;
/// Tag for [`Frame::Error`].
const ERROR: u8 = 5;
/// Two formats, and the tag chooses between them.
///
/// A volume is **postcard**, matching
/// [`filetree`](crate::shared::filetree) rather than the JSON the rest
/// of the crate uses: this relays nothing, so no byte of it has to
/// survive a round trip unchanged, and nothing downstream reads it as
/// text.
///
/// The error is JSON, and has to be. A
/// [`serde_json::Value`] deserializes through `deserialize_any`, which
/// a format with no self-description cannot answer — so postcard can
/// carry a volume and cannot carry the failure, and each variant gets
/// the format it needs.
impl Encode for Frame {
/// One failure per half, and they are different libraries'.
type Error = FrameEncodeError;
// Spelled out rather than `Self::Error`: this enum has a variant
// called `Error`, so the associated type is ambiguous by that name.
fn encode(&self, out: &mut Writer<'_>) -> Result<(), FrameEncodeError> {
match self {
Frame::Added(volume) => {
out.extend_from_slice(&[ADDED]);
postcard::to_io(volume, &mut *out).map(|_| ()).map_err(FrameEncodeError::Volume)
}
Frame::Changed(volume) => {
out.extend_from_slice(&[CHANGED]);
postcard::to_io(volume, &mut *out).map(|_| ()).map_err(FrameEncodeError::Volume)
}
Frame::Removed(volume) => {
out.extend_from_slice(&[REMOVED]);
postcard::to_io(volume, &mut *out).map(|_| ()).map_err(FrameEncodeError::Volume)
}
Frame::Listed => {
out.extend_from_slice(&[LISTED]);
Ok(())
}
Frame::Unsupported => {
out.extend_from_slice(&[UNSUPPORTED]);
Ok(())
}
Frame::Error(error) => {
out.extend_from_slice(&[ERROR]);
error.encode(out).map_err(FrameEncodeError::Error)
}
}
}
}
impl Decode<'_> for Frame {
/// Four ways to fail, and each names which half failed.
type Error = FrameDecodeError;
// Spelled out for the same reason as `encode` above.
fn decode(bytes: &[u8]) -> Result<Self, FrameDecodeError> {
let (tag, rest) = bytes.split_first().ok_or(FrameDecodeError::Empty)?;
match *tag {
ADDED => postcard::from_bytes(rest).map(Frame::Added).map_err(FrameDecodeError::Volume),
CHANGED => postcard::from_bytes(rest).map(Frame::Changed).map_err(FrameDecodeError::Volume),
REMOVED => postcard::from_bytes(rest).map(Frame::Removed).map_err(FrameDecodeError::Volume),
LISTED => Ok(Frame::Listed),
UNSUPPORTED => Ok(Frame::Unsupported),
ERROR => Error::decode(rest).map(Frame::Error).map_err(FrameDecodeError::Error),
tag => Err(FrameDecodeError::UnknownTag(tag)),
}
}
}
/// A volume listing response that could not be written.
#[derive(Debug)]
pub enum FrameEncodeError {
/// The volume did not serialize.
Volume(postcard::Error),
/// The error did not serialize.
Error(serde_json::Error),
}
impl fmt::Display for FrameEncodeError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
FrameEncodeError::Volume(error) => write!(f, "volume listing volume did not serialize: {error}"),
FrameEncodeError::Error(error) => write!(f, "volume listing error did not serialize: {error}"),
}
}
}
impl std::error::Error for FrameEncodeError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
FrameEncodeError::Volume(error) => Some(error),
FrameEncodeError::Error(error) => Some(error),
}
}
}
/// A volume listing response that could not be read.
#[derive(Debug)]
pub enum FrameDecodeError {
/// No bytes at all, so not even a tag.
Empty,
/// A tag that is none of this frame's six.
UnknownTag(u8),
/// The volume did not parse.
Volume(postcard::Error),
/// The error did not parse.
Error(serde_json::Error),
}
impl fmt::Display for FrameDecodeError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
FrameDecodeError::Empty => f.write_str("volume listing response frame is empty"),
FrameDecodeError::UnknownTag(tag) => write!(f, "unknown volume listing response frame tag {tag}"),
FrameDecodeError::Volume(error) => write!(f, "volume listing volume did not parse: {error}"),
FrameDecodeError::Error(error) => write!(f, "volume listing error did not parse: {error}"),
}
}
}
impl std::error::Error for FrameDecodeError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
FrameDecodeError::Volume(error) => Some(error),
FrameDecodeError::Error(error) => Some(error),
FrameDecodeError::Empty | FrameDecodeError::UnknownTag(_) => None,
}
}
}
and by diverge-sdk-rs/src/provider/endpoints/volumes/list/server/response/volume.rs:
//! One volume a provider offers.
use serde::{Deserialize, Serialize};
use crate::provider::endpoints::volumes::Mode;
/// A directory a caller may mount, under the name a provider gave it.
///
/// [`name`](Self::name) is what to call it, [`bytes`](Self::bytes) is
/// how big it is, [`created`](Self::created) is how old it is, and
/// [`mode`](Self::mode) is what becomes of a change to it. That is
/// the whole of what a listing says about one, and
/// the omissions are the interesting part.
///
/// # What is inside it is a stat away
///
/// How much of it is used and the hash of its content are what a
/// [`stat`](crate::provider::endpoints::volumes::stat) reports, because each
/// costs a walk of the volume and a listing does not pay for one. A
/// listing is what a provider knows without looking.
///
/// # Where it is, is not here
///
/// A volume carries no path. Not a private one, not an opaque one —
/// none, because a caller has nothing to do with one.
///
/// Everything a caller does with a volume goes through its name: a
/// [`stat`](crate::provider::endpoints::volumes::stat) names it, a
/// [`delete`](crate::provider::endpoints::volumes::delete) names it, and a
/// [`VolumeMount`](crate::shared::containers::request::VolumeMount)
/// names it, and a provider looks the name up rather than resolving
/// anything. A path would be the one field nothing consumes, and a
/// field nothing consumes is one that gets consumed anyway — a caller
/// building strings out of it, a provider then unable to move a volume
/// without breaking someone.
///
/// The paths a mount's
/// [`volume_relative_path`](crate::shared::containers::request::VolumeMount::volume_relative_path)
/// names still mean what they meant. They are relative to the volume;
/// the volume is simply no longer described in terms of anywhere
/// else.
///
/// # Why a volume rather than a directory
///
/// Because the name is the point. A directory is a thing on a disk; a
/// volume is a thing a provider decided to OFFER, and the offering is
/// what a caller interacts with. It is also what a
/// [`VolumeMount`](crate::shared::containers::request::VolumeMount)
/// names, which is where the word already meant this.
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct Volume {
/// What to call this volume, and how to ask for it.
///
/// A label, and nothing derives it from anything: for a volume the
/// provider offers, the provider chose it; for one a
/// [`create`](crate::provider::endpoints::volumes::create) made, the caller
/// did. Nothing here says which, and nothing should.
///
/// It is also the HANDLE, and the only one. A
/// [`stat`](crate::provider::endpoints::volumes::stat) names a volume by
/// this and by nothing else, so two volumes in one listing sharing
/// a name would make one of them unreachable.
pub name: String,
/// How big it is, in BYTES.
///
/// For a volume a
/// [`create`](crate::provider::endpoints::volumes::create::client::request::Frame::bytes)
/// made, the number that was asked for.
pub bytes: u64,
/// When the volume came into being, in SECONDS since the Unix
/// epoch.
///
/// What "came into being" means is the provider's to decide and
/// the provider's alone — the directory's own creation time where
/// that is knowable, when it was first offered where it is not.
/// Nothing here can distinguish the two, and a caller that needs
/// to has asked a question this listing does not answer.
///
/// # Seconds, and an integer
///
/// Seconds because nothing sorts volumes at finer resolution than
/// that, and a field's precision is a promise about what varies
/// between two values rather than about how many digits fit.
///
/// An integer rather than a formatted timestamp because this rides
/// a binary format. A date written as text inside postcard would
/// be a text format smuggled into a binary one, carrying a
/// timezone offset that is always the same, at a width that varies
/// with the number it holds.
///
/// Unsigned, so a volume cannot predate 1970. Nothing a provider
/// offers does, and the alternative is a signed field whose
/// negative half exists to represent a state that never occurs.
pub created: u64,
/// The mode the volume is in: persistent, ephemeral, or read
/// only. See [`Mode`].
///
/// What a
/// [`create`](crate::provider::endpoints::volumes::create::client::request::Frame::mode)
/// stated, or the last
/// [`edit`](crate::provider::endpoints::volumes::edit::client::request::Change::Mode)
/// of it; for a volume the provider offers on its own, the
/// provider's word. One answer for every container that mounts
/// the volume and every FUSE serve of it — neither chooses.
///
/// One byte in postcard, `0`, `1` or `2`, after `created`.
pub mode: Mode,
}
and the error by diverge-sdk-rs/src/shared/error.rs:
//! What went wrong, as far as this specification describes it.
use serde::{Deserialize, Serialize};
use crate::wire::decode::Decode;
use crate::wire::encode::{Encode, Writer};
/// An error, carrying one JSON value and nothing else.
///
/// This specification does not say what is inside it. Not "not yet" —
/// it says nothing, and a reader should not expect a shape to appear
/// here later.
///
/// # Why a value rather than a vocabulary
///
/// Because the alternative is this crate tracking every failure every
/// provider can have. A typed enum would have to name what a container
/// runtime, a kernel, a filesystem, a registry and an upstream model
/// can each go wrong with, and would be wrong the first time any of
/// them added one. What a caller does about most of them is the same
/// thing anyway — stop, or try again later — and that decision does
/// not need the protocol to have a word for the cause.
///
/// So a provider puts in whatever it knows. A caller that understands
/// a particular provider reads it; one that does not still has
/// something to log, surface, or hand to a person, which is more than
/// a bare failure gives it.
///
/// # It is JSON, and that is not a free choice
///
/// [`serde_json::Value`] deserializes through `deserialize_any`, which
/// a format with no self-description cannot answer. So this decodes
/// from JSON and not from [`postcard`], whatever module it ends up
/// riding in — a postcard payload that wanted to carry one would have
/// to tunnel it through a string, and would be inventing a second
/// encoding for a value that already has one.
///
/// # It is a message, not a Rust error
///
/// Deliberately no [`std::error::Error`] impl. This is a thing that
/// arrives on a wire; the `Error` types elsewhere in this crate are
/// what a decoder returns when it cannot read one. Giving both the
/// same trait would blur two ideas that share only a name.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
#[derive(schemars::JsonSchema)]
pub struct Error(
/// Whatever the provider had to say.
///
/// [`Value::Null`](serde_json::Value::Null) is legal and is what
/// [`Default`] gives — an error whose sender had nothing to add.
pub serde_json::Value,
);
impl Encode for Error {
/// The ordinary JSON failure.
type Error = serde_json::Error;
fn encode(&self, out: &mut Writer<'_>) -> Result<(), Self::Error> {
serde_json::to_writer(out, self)
}
}
impl Decode<'_> for Error {
/// The ordinary JSON failure.
type Error = serde_json::Error;
fn decode(bytes: &[u8]) -> Result<Self, Self::Error> {
serde_json::from_slice(bytes)
}
}