Response

The sequence of channel responses that answers the channel, what ends it, and what a finish with nothing before it means.

[0][document JSON …]    the image's /DIVERGE.md, parsed
[1]                     the image ships no /DIVERGE.md
[2][error JSON …]       the file could not be read, or is not a document
  • The sequence. Exactly one channel response, followed by the channel response finish. A channel response finish that no channel response precedes states that the server did not serve the question.
  • The document. The byte 0 is followed by exactly one JSON object of two members, which the server reads from the file. The file begins with a line that is exactly ---; the lines after it, up to the next line that is exactly ---, are the frontmatter, which the server parses as JSON and sends as schema, the JSON Schema of the container’s arguments, a JSON object. Every byte after that line is body, a string, verbatim. The server reads the file as UTF-8 and reads neither member further.
  • Missing. The byte 1 states that the image ships no /DIVERGE.md. Nothing follows the byte. That is a fact about the image and not a failure.
  • The error. The byte 2 states that the file is at the root and the server could not read it, or that the file is not a document: it does not begin with a line that is exactly ---, no later line is exactly ---, the frontmatter is not JSON, or the frontmatter is JSON and not an object. The bytes after it are exactly one JSON value in the form defined on the volumes::list response page. No error of this channel is the container’s program’s: the server asks it nothing.

The channel response is defined by diverge-sdk-rs/src/container_proxy/outside/endpoints/tools/begin/server/channel_response/info/frame.rs:

//! What a server's response frame carries on an info channel.

/// The image's document, that it has none, or why not. See [`info::response::Frame`](crate::shared::containers::info::response::Frame).
///
/// An alias rather than a re-export because this module is real: the
/// path says this scope's answer lives here, and it does, rather than
/// naming somewhere else and hoping a reader follows. What the alias
/// points at is right there in the signature.
pub type Frame = crate::shared::containers::info::response::Frame;

and by diverge-sdk-rs/src/shared/containers/info/response/frame.rs:

//! What an info channel answers.

use std::fmt;

use super::super::Document;
use crate::wire::decode::Decode;
use crate::wire::encode::{Encode, Writer};
use crate::shared::error::Error;

/// The image's document, that it has none, or why it could not be
/// read.
///
/// One frame is all there is — this is not a stream, and a channel
/// carrying one of these finishes immediately after.
///
/// # The layout
///
/// ```text
/// [0][document JSON …]   the image's /DIVERGE.md, parsed: its schema and its body
/// [1]                    the image ships no /DIVERGE.md
/// [2][error JSON …]      the file could not be read, or is not a document, and why
/// ```
#[derive(Debug, Clone, PartialEq)]
pub enum Frame {
    /// The image's `/DIVERGE.md`, parsed: see [`Document`]. Tag `0`.
    ///
    /// Its `schema` is the JSON Schema of the
    /// [`arguments`](crate::shared::containers::request::Container::arguments)
    /// the image's program accepts, as the image states it; its
    /// `body` is the prose beneath.
    Info(Document),
    /// The image ships no `/DIVERGE.md`. Tag `1`.
    ///
    /// Bare: there is nothing to carry. This is not a failure of
    /// anything but a fact about the image — the proxy looked at the
    /// root and the file was not there — and a caller that asked
    /// learns that the image does not say what it takes.
    Missing,
    /// A failure. Tag `2`.
    ///
    /// The file is there and could not be read, or is there and is
    /// not a document — it breaks a rule of
    /// [`Document::parse`](crate::shared::containers::info::Document::parse)
    /// — or the container could not be asked. See
    /// [`shared::error::Error`](crate::shared::error::Error) for why
    /// it says so little.
    Error(Error),
}

/// Tag for [`Frame::Info`].
const INFO: u8 = 0;

/// Tag for [`Frame::Missing`].
const MISSING: u8 = 1;

/// Tag for [`Frame::Error`].
const ERROR: u8 = 2;

impl Encode for Frame {
    /// The ordinary JSON failure, from either half that has one.
    type Error = serde_json::Error;

    // 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<(), serde_json::Error> {
        match self {
            Frame::Info(document) => {
                out.extend_from_slice(&[INFO]);
                serde_json::to_writer(out, document)
            }
            Frame::Missing => {
                out.extend_from_slice(&[MISSING]);
                Ok(())
            }
            Frame::Error(error) => {
                out.extend_from_slice(&[ERROR]);
                error.encode(out)
            }
        }
    }
}

impl Decode<'_> for Frame {
    /// Four ways to fail, and two of them are JSON.
    type Error = FrameError;

    // Spelled out for the same reason as `encode` above.
    fn decode(bytes: &[u8]) -> Result<Self, FrameError> {
        let (tag, rest) = bytes.split_first().ok_or(FrameError::Empty)?;
        match *tag {
            INFO => serde_json::from_slice(rest).map(Frame::Info).map_err(FrameError::Info),
            MISSING => Ok(Frame::Missing),
            ERROR => Error::decode(rest).map(Frame::Error).map_err(FrameError::Error),
            tag => Err(FrameError::UnknownTag(tag)),
        }
    }
}

/// An info answer that could not be read.
#[derive(Debug)]
pub enum FrameError {
    /// No bytes at all, so not even a tag.
    Empty,
    /// A tag that is none of this frame's three.
    UnknownTag(u8),
    /// The document did not parse. The message is the parser's, which
    /// quotes no value.
    Info(serde_json::Error),
    /// The error did not parse.
    Error(serde_json::Error),
}

impl fmt::Display for FrameError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            FrameError::Empty => f.write_str("info answer frame is empty"),
            FrameError::UnknownTag(tag) => write!(f, "unknown info answer frame tag {tag}"),
            FrameError::Info(error) => write!(f, "info document did not parse: {error}"),
            FrameError::Error(error) => write!(f, "info error did not parse: {error}"),
        }
    }
}

impl std::error::Error for FrameError {
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
        match self {
            FrameError::Info(error) | FrameError::Error(error) => Some(error),
            FrameError::Empty | FrameError::UnknownTag(_) => None,
        }
    }
}

and the document by diverge-sdk-rs/src/shared/containers/info/document.rs:

//! The image's document, parsed and written.

use serde::{Deserialize, Serialize};
use serde_json::Value;

use super::ParseError;

/// `/DIVERGE.md`, parsed: the arguments' JSON Schema and the text
/// beneath it.
///
/// The frontmatter is the schema and nothing else — one required
/// field that is the whole document, as raw JSON, which is YAML's flow
/// style and so reads as a frontmatter anywhere one is expected. The
/// body is the image's prose: what it runs and the secrets it
/// declares, kept as the author left it. The provider carries and
/// does not read either; a caller reads the schema to learn what an
/// image's program takes without knowing the image.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Document {
    /// The JSON Schema of the image's
    /// [`arguments`](crate::shared::containers::request::Container::arguments),
    /// as the frontmatter states it: a JSON object, since a schema is
    /// one and [`parse`](Self::parse) refuses anything else. A
    /// [`Value`] rather than a typed schema, because a schema is a
    /// document in its own vocabulary and this crate has no business
    /// restating it.
    pub schema: Value,
    /// Everything after the closing `---` line, verbatim: leading
    /// blank lines, trailing newline and all.
    pub body: String,
}

/// Whether one line, as [`str::split_inclusive`] on `\n` yields it, is
/// a fence: exactly `---`, with or without the newline that ends it.
fn is_fence(line: &str) -> bool {
    line.strip_suffix('\n').unwrap_or(line) == "---"
}

impl Document {
    /// Read a `/DIVERGE.md` from its text.
    ///
    /// The rules, in the order they are checked:
    ///
    /// 1. The text begins with a line that is exactly `---`. A line
    ///    ends at `\n`, so a fence followed by anything else — a
    ///    space, a carriage return — is not one. Otherwise
    ///    [`ParseError::NoFrontmatter`].
    /// 2. The frontmatter is every line after it up to the next line
    ///    that is exactly `---`, which closes it. No such line is
    ///    [`ParseError::Unterminated`].
    /// 3. The frontmatter parses as JSON — otherwise
    ///    [`ParseError::Json`] — and is an object, since a JSON Schema
    ///    is one; any other value is [`ParseError::NotObject`].
    /// 4. The body is everything after the closing line, as the author
    ///    left it: nothing is trimmed, and a document whose closing
    ///    line is its last has an empty body.
    pub fn parse(text: &str) -> Result<Document, ParseError> {
        let mut lines = text.split_inclusive('\n');
        let opening = lines.next().ok_or(ParseError::NoFrontmatter)?;
        if !is_fence(opening) {
            return Err(ParseError::NoFrontmatter);
        }
        let start = opening.len();
        let mut end = start;
        loop {
            let line = lines.next().ok_or(ParseError::Unterminated)?;
            if is_fence(line) {
                let schema: Value = serde_json::from_str(&text[start..end]).map_err(ParseError::Json)?;
                if !schema.is_object() {
                    return Err(ParseError::NotObject);
                }
                let body = text[end + line.len()..].to_owned();
                return Ok(Document { schema, body });
            }
            end += line.len();
        }
    }

    /// Write the document back as its text: `---`, the schema
    /// pretty-printed, `---`, and the body, each fence on a line of its
    /// own. One shape is written and read: the assembler that makes an
    /// image's file and any later writer use this, and
    /// [`parse`](Self::parse) of what it writes is this document
    /// again, member for member, for any document whose schema is an
    /// object — which every parsed one is: a pretty-printed object has
    /// no line that is exactly `---`, and the body is copied verbatim.
    ///
    /// The `Result` is serde's: serializing a [`Value`] does not fail,
    /// and the signature carries what the serializer says rather than
    /// asserting it.
    pub fn render(&self) -> Result<String, serde_json::Error> {
        let schema = serde_json::to_string_pretty(&self.schema)?;
        Ok(format!("---\n{schema}\n---\n{}", self.body))
    }
}