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
0is 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 asschema, the JSON Schema of the container’sarguments, a JSON object. Every byte after that line isbody, a string, verbatim. The server reads the file as UTF-8 and reads neither member further. - Missing. The byte
1states 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
2states 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))
}
}