Response
The server sends exactly one response, whose payload begins with the byte 0 followed by the listing in the postcard wire format or with the byte 1 followed by an error as one JSON value, and the response finish follows it.
The server sends exactly one response on the scope. The response finish follows the response, and no frame follows the finish.
[0][listing, postcard …]
[1][error JSON …]
- The sequence. Exactly one response precedes the response finish, and nothing follows the finish. A response finish that no response precedes states that the request was not served, as Endpoints provides.
- The listing. A payload whose first byte is
0carries the listing after that byte, running to the end of the payload. The listing is theVec<Volume>defined below, encoded as the postcard reading of Notation provides: a varint count, followed by that many volumes; each volume is itsnameas a varint byte length followed by that many bytes of UTF-8, followed bybytesas a varint, followed bycreatedas a varint. A count of0is the empty listing, and the payload is then exactly the one byte0. 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 in one listing 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.- Order. The order of the volumes in the listing is not prescribed. A client reads nothing into the position of a volume.
- The error. A payload whose first byte is
1carries 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 neither
0nor1, a payload whose bytes after0do not decode as the listing, and a payload whose bytes after1are not one JSON value are malformed.
The response frame is defined by
diverge-provider-sdk/src/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::decode::Decode;
use crate::encode::{Encode, Writer};
use crate::shared::error::Error;
/// Every volume a provider will let this caller watch, or the news
/// that it could not say.
///
/// One of these on channel `0`, then the scope finishes. A payload
/// leads with one byte saying which — `0` for
/// [`Volumes`](Self::Volumes), `1` for [`Error`](Self::Error) — and
/// the rest is that variant's own bytes.
///
/// A listing is not a stream: a provider knows what it offers before
/// it is asked, so there is nothing to discover incrementally and
/// nothing to hold a channel open for.
///
/// An empty list is an ANSWER and means the provider offers nothing.
/// It is not an [`Error`](Self::Error), which means the provider could
/// not tell — a caller that confuses them stops asking when it should
/// retry.
#[derive(Debug, Clone, PartialEq)]
pub enum Frame {
/// The volumes, in whatever order the provider chose. Tag `0`.
///
/// Nothing promises an order and nothing should be read into one.
Volumes(Vec<Volume>),
/// A failure. Tag `1`.
///
/// See [`shared::error::Error`](crate::shared::error::Error) for
/// why it says so little.
Error(Error),
}
/// Tag for [`Frame::Volumes`].
const VOLUMES: u8 = 0;
/// Tag for [`Frame::Error`].
const ERROR: u8 = 1;
/// Two variants, two formats, and the tag chooses between them.
///
/// The volumes are **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 the listing 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::Volumes(volumes) => {
out.extend_from_slice(&[VOLUMES]);
postcard::to_io(volumes, &mut *out)
.map(|_| ())
.map_err(FrameEncodeError::Volumes)
}
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 {
VOLUMES => postcard::from_bytes(rest)
.map(Frame::Volumes)
.map_err(FrameDecodeError::Volumes),
ERROR => Error::decode(rest)
.map(Frame::Error)
.map_err(FrameDecodeError::Error),
tag => Err(FrameDecodeError::UnknownTag(tag)),
}
}
}
/// A volume listing that could not be written.
#[derive(Debug)]
pub enum FrameEncodeError {
/// The listing did not serialize.
Volumes(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::Volumes(error) => {
write!(f, "volume listing 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::Volumes(error) => Some(error),
FrameEncodeError::Error(error) => Some(error),
}
}
}
/// A volume listing that could not be read.
#[derive(Debug)]
pub enum FrameDecodeError {
/// No bytes at all, so not even a tag.
Empty,
/// A tag that is neither of this frame's two.
UnknownTag(u8),
/// The listing did not parse.
Volumes(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::Volumes(error) => {
write!(f, "volume listing 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::Volumes(error) => Some(error),
FrameDecodeError::Error(error) => Some(error),
FrameDecodeError::Empty | FrameDecodeError::UnknownTag(_) => None,
}
}
}
A volume is defined by diverge-provider-sdk/src/endpoints/volumes/list/server/response/volume.rs:
//! One volume a provider offers.
use serde::{Deserialize, Serialize};
/// A directory a caller may watch, under the name a provider gave it.
///
/// [`name`](Self::name) is what to call it, [`bytes`](Self::bytes) is
/// how big it is, and [`created`](Self::created) is how old it is.
/// 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::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
/// [`watch`](crate::endpoints::volumes::watch) names it, a
/// [`delete`](crate::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 inside a watch 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::endpoints::volumes::create) made, the caller
/// did. Nothing here says which, and nothing should.
///
/// It is also the HANDLE, and the only one. A
/// [`watch`](crate::endpoints::volumes::watch) 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::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 error is defined by diverge-provider-sdk/src/shared/error.rs:
//! What went wrong, as far as this specification describes it.
use serde::{Deserialize, Serialize};
use crate::decode::Decode;
use crate::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)]
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)
}
}