dre-protocol 0.7.1

The DRE plugin protocol: frames, messages, host side, plugin SDK and conformance suite
Documentation
//! A fixture destination plugin for protocol and run tests.
//!
//! Every delivery appends one JSON line to `<dir>/deliveries.jsonl` (`dir` from the connection):
//! `{"files": [{"local": ..., "remote": ..., "content": ...}], "options": {...}}`. The reply's
//! location is `fixture:` followed by the remote paths (or file names), comma-separated.
//!
//! - It advertises `multi_file` unless `DRE_FIXTURE_SINGLE_FILE` is set.
//! - It takes messages (`message`, limit 3,000 characters), recorded with `"message"` and the
//!   attached `files`, unless `DRE_FIXTURE_FILES_ONLY` is set. `DRE_FIXTURE_MESSAGE_ONLY` makes it
//!   `message_only`: it refuses files.
//! - A connection with `fail: true` makes every delivery fail; `temporary_failures: n` fails the
//!   first `n` tries with a temporary error, which the SDK's retry tries again at once.
//! - Its options are `to`, `subject` and `message`, recorded as given.

use std::io::Write;

use dre_protocol::delivery::Retry;
use dre_protocol::msg::ConnectionField;
use dre_protocol::msg::Message;
use dre_protocol::options::{OptionField, OptionType};
use dre_protocol::plugin::{
    About, Delivery, Destination, Result, conn_bool, conn_required, serve_destination,
};
use dre_protocol::{CAP_MESSAGE, CAP_MESSAGE_ONLY, CAP_MULTI_FILE};
use serde_json::json;

struct Fixture;

impl Destination for Fixture {
    fn connection_fields(&self) -> Vec<ConnectionField> {
        vec![
            ConnectionField::new("dir", "where deliveries are recorded").required(),
            ConnectionField::new("temporary_failures", "how many tries fail with a temporary error"),
        ]
    }

    fn options(&self) -> Vec<OptionField> {
        vec![
            OptionField::new("to", OptionType::Strings, "recorded"),
            OptionField::new("subject", OptionType::String, "recorded"),
            OptionField::new("message", OptionType::String, "recorded"),
        ]
    }

    fn message_limit(&self) -> Option<u64> {
        Some(3_000)
    }

    fn deliver_message(&mut self, d: &Delivery, m: &Message) -> Result<String> {
        let names = record(d, Some(m))?;
        Ok(match names.is_empty() {
            true => "fixture:message".to_string(),
            false => format!("fixture:message+{}", names.join(",")),
        })
    }

    fn deliver_files(&mut self, d: &Delivery) -> Result<String> {
        if std::env::var_os("DRE_FIXTURE_MESSAGE_ONLY").is_some() {
            return Err("the fixture destination only takes messages".into());
        }
        let failures = d
            .connection
            .get("temporary_failures")
            .and_then(|v| v.as_u64())
            .unwrap_or(0);
        let mut tries = 0;
        dre_protocol::delivery::retry(3, "fixture", || {
            tries += 1;
            if tries <= failures {
                return Err(Retry::Temporary(
                    "temporary failure".to_string(),
                    Some(std::time::Duration::ZERO),
                ));
            }
            Ok(())
        })?;
        let names = record(d, None)?;
        Ok(format!("fixture:{}", names.join(",")))
    }
}

/// Append the delivery to `deliveries.jsonl`; return the files' remote paths (or names).
fn record(d: &Delivery, message: Option<&Message>) -> Result<Vec<String>> {
    if conn_bool(&d.connection, "fail") == Some(true) {
        return Err("fixture destination told to fail".into());
    }
    let dir = conn_required(&d.connection, "dir")?;
    let mut files = Vec::new();
    let mut names = Vec::new();
    for f in &d.files {
        // Binary files (a zip) are recorded lossily: the test reads them from `local`.
        let content = std::fs::read(&f.local)
            .map(|b| String::from_utf8_lossy(&b).into_owned())
            .map_err(|e| format!("can't read {}: {e}", f.local.display()))?;
        names.push(f.remote.clone().unwrap_or_else(|| {
            f.local
                .file_name()
                .unwrap_or_default()
                .to_string_lossy()
                .to_string()
        }));
        files.push(json!({"local": f.local, "remote": f.remote, "content": content}));
    }
    std::fs::create_dir_all(dir)?;
    let mut log = std::fs::OpenOptions::new()
        .create(true)
        .append(true)
        .open(std::path::Path::new(dir).join("deliveries.jsonl"))?;
    let mut line = json!({"files": files, "options": d.options});
    if let Some(m) = message {
        line["message"] = json!(m);
    }
    writeln!(log, "{line}")?;
    Ok(names)
}

fn main() {
    let on = |v: &str| std::env::var_os(v).is_some();
    let mut caps: Vec<&'static str> = Vec::new();
    if !on("DRE_FIXTURE_SINGLE_FILE") {
        caps.push(CAP_MULTI_FILE);
    }
    if !on("DRE_FIXTURE_FILES_ONLY") {
        caps.push(CAP_MESSAGE);
    }
    if on("DRE_FIXTURE_MESSAGE_ONLY") {
        caps.push(CAP_MESSAGE_ONLY);
    }
    let caps: &'static [&'static str] = caps.leak();
    serve_destination(
        About::new("fixture", env!("CARGO_PKG_VERSION")).capabilities(caps),
        Fixture,
    )
}