errand 0.1.1

Async small-web (smolweb) transport: gemini, gopher, finger, spartan, nex, guppy, and titan in one scheme-routed fetch, plus titan-upload and misfin-send write companions. Bytes in, bytes out, no HTTP, host-agnostic.
Documentation
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */

//! The Nex protocol (`nex://`, port 1900, <https://nex.nightfall.city>).
//!
//! Nex is the minimal smolweb protocol: plaintext TCP, no TLS, no status
//! line. The request is the URL path followed by `\r\n`; the response is
//! raw bytes (directory listing or file content). The server closes the
//! connection when done, so the body is whatever arrives before EOF.
//!
//! There is no header, so every reply is a [`Status::Success`] with no
//! MIME type. The caller (or the nematic `NexEngine`) distinguishes a
//! directory listing from a content response by inspecting the body.

use url::Url;

use crate::plain::exchange;
use crate::{Error, Response, Scheme, Status};

/// Fetch a `nex://` URL.
pub(crate) async fn fetch(url: &Url) -> Result<Response, Error> {
    let host = url
        .host_str()
        .ok_or_else(|| Error::BadUrl("nex URL has no host".into()))?;
    let port = url.port().unwrap_or_else(|| Scheme::Nex.default_port());
    let path = url.path();
    let path = if path.is_empty() { "/" } else { path };

    let request = format!("{path}\r\n");
    let body = exchange(host, port, request.as_bytes()).await?;
    Ok(Response {
        url: url.clone(),
        status: Status::Success,
        raw_status: None,
        meta: String::new(),
        body,
    })
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn nex_scheme_routes_to_port_1900() {
        assert_eq!(Scheme::Nex.default_port(), 1900);
    }

    #[tokio::test]
    #[ignore = "hits the live network; run with `cargo test -- --ignored`"]
    async fn live_nex_smoke() {
        let r = crate::fetch("nex://nightfall.city/")
            .await
            .expect("fetch nex root");
        assert_eq!(r.status, Status::Success);
        assert!(!r.body.is_empty());
    }
}