New tests, refactor to be more modular, improve prompt ui, add test run parameter

CandyCorvid
Jul 13, 2023, 10:41 AM
FZIABKEV74PFFRFETMT5WVL625I76UYN2LXKIKRZNQE5GDGX3GUQC

Dependencies

  • [2] ARXVO5XA Make connection generic on the network
  • [3] JWLMAS7U Simplify tests with rstest
  • [4] 3SPNKI46 Improve parsing. Add modules that were missed
  • [5] AOJXTWBZ Refactor read_until
  • [6] WGRFJRTE Add tests, rename Status to Header, implement redirect and proper input handling
  • [7] XGDABCJN Add match_str macro
  • [8] DBKKKHC2 Initial commit
  • [9] AMTMTTJT Upgrade status parsing
  • [10] ONQEIR5B WIP mime-type handling
  • [11] HVFBGX2A Implement basic text/gemini
  • [12] OSV63NXN Implement contiguous preformat blocks with alt-text

Change contents

  • replacement in src/server.rs at line 16
    [2.215][2.215:247]()
    pub struct TcpNetwork<V>(V, !);
    [2.215]
    [2.247]
    pub struct TcpNetwork<V> {
    verifier: V,
    }
    impl<V> TcpNetwork<V> {
    pub fn new(ver: V) -> Self {
    Self { verifier: ver }
    }
    }
  • edit in src/media.rs at line 7
    [4.595][4.595:790]()
    }
    impl<'a> Media<'a> {
    pub fn display(&self) {
    match self {
    Media::Gemini(g) => println!("{g:#?}"), // TODO
    Media::Text(s) => println!("{s}"),
    }
    }
  • replacement in src/main.rs at line 6
    [4.31][4.6831:6915](),[4.40][4.6831:6915](),[4.3712][4.6831:6915](),[4.6915][2.941:1035]()
    use response::header::{CertRequired, FailPerm, FailTemp, Input, Redirect, Success};
    use server::{Network, TcpNetwork};
    use std::io::{BufReader, Read, Result as ioResult, Write};
    [4.40]
    [4.3845]
    use server::Network;
    use std::{
    fmt::Display,
    io::{BufReader, Read, Result as ioResult, Write},
    };
  • edit in src/main.rs at line 21
    [4.215]
    [4.215]
    use std::{
    fmt::Display,
    io::{stdout, Write},
    };
  • edit in src/main.rs at line 27
    [4.237]
    [4.237]
    use url::Url;
    use crate::{danger, media::Media, run, server::TcpNetwork, UI};
  • edit in src/main.rs at line 33
    [4.280]
    [4.280]
    #[arg(short, long)]
    pub test_prompt: bool,
  • edit in src/main.rs at line 37
    [4.319]
    [4.319]
    pub enum Error {}
    impl App {
    pub fn run(self) -> Result<(), Error> {
    let mut cli = Cli {};
    if self.test_prompt {
    let overt = cli.read("tell me something");
    cli.warn(format_args!("your response was: {overt}"));
    let secret = cli.read_secret("tell me your secrets");
    cli.warn(format_args!("your secret was: {secret}"));
    Ok(())
    } else {
    let url = self.url.unwrap_or_else(|| {
    "gemini://gemini.circumlunar.space/docs/specification.gmi".into()
    });
    let url: Url = url.parse().expect("Not a valid URL");
    run::<TcpNetwork<danger::Naive>>(&mut cli, url);
    Ok(())
    }
    }
    }
    pub struct Cli {}
    impl UI for Cli {
    fn show(&self, content: Media) {
    match content {
    Media::Gemini(g) => println!("{g:#?}"), // TODO
    Media::Text(s) => println!("{s}"),
    }
    }
    fn read(&self, prompt: impl Display) -> String {
    print!("{prompt}: ");
    stdout().flush().unwrap();
    std::io::stdin()
    .lines()
    .next()
    .expect("End of input")
    .expect("Failed to read input")
    }
    fn read_secret(&self, prompt: impl Display) -> String {
    let dim = ansi_term::Style::new().dimmed();
    print!("{prompt}: ");
    let hidden = "(input hidden)";
    let len = hidden.len() as u32;
    let back = ansi_control_codes::control_sequences::CUB(Some(len));
    print!("{}{back}", dim.paint(hidden));
    stdout().flush().unwrap();
    rpassword::read_password().expect("Failed to read input")
    }
    fn warn<D: std::fmt::Display>(&self, warning: D) {
    println!("Warning: {warning}")
    }
    }
    }
    pub trait UI {
    fn show(&self, content: media::Media);
    fn warn<D: Display>(&self, warning: D);
    fn read(&self, prompt: impl Display) -> String;
    fn read_secret(&self, prompt: impl Display) -> String;
  • replacement in src/main.rs at line 108
    [4.3977][4.3977:4103](),[4.4103][4.5116:5178](),[4.5178][2.1036:1110]()
    let url = cli
    .url
    .unwrap_or_else(|| "gemini://gemini.circumlunar.space/docs/specification.gmi".into());
    let mut url: Url = url.parse().expect("Not a valid URL");
    while let Some(new_url) = run_url::<TcpNetwork<danger::Naive>>(url) {
    [4.3977]
    [4.5223]
    match cli.run() {
    Ok(()) => println!("done"),
    Err(e) => match e {},
    }
    }
    fn run<N: Network>(ui: &mut impl UI, mut url: Url)
    where
    <N as Network>::Error: std::fmt::Debug,
    {
    while let Some(new_url) = run_url::<N>(ui, url) {
  • replacement in src/main.rs at line 123
    [4.4124][2.1111:1159]()
    fn run_url<N: Network>(url: Url) -> Option<Url>
    [4.4124]
    [2.1159]
    fn run_url<N: Network>(ui: &mut impl UI, url: Url) -> Option<Url>
  • replacement in src/main.rs at line 135
    [4.5339][4.5339:5385]()
    handle_response_header(head, url, stream)
    [4.5339]
    [4.5385]
    handle_response_header(ui, head, url, stream)
  • edit in src/main.rs at line 139
    [4.5415]
    [4.5415]
    ui: &mut impl UI,
  • replacement in src/main.rs at line 141
    [4.5443][4.5443:5488]()
    mut url: Url,
    mut stream: impl Read,
    [4.5443]
    [4.5488]
    url: Url,
    stream: impl Read,
  • replacement in src/main.rs at line 145
    [4.7108][4.5508:5548](),[4.5548][4.7148:7231](),[4.7148][4.7148:7231](),[4.7231][4.5549:5816](),[4.5816][4.7408:7593](),[4.7408][4.7408:7593]()
    response::Header::Input(s) => {
    let Input { prompt, sensitive } = s;
    println!("{prompt}");
    let input = if let Some(true) = sensitive {
    // Read sensitive input
    rpassword::read_password()
    } else {
    // Read plain input
    std::io::stdin().lines().next().expect("End of input")
    }
    .expect("Failed to read input");
    let input = urlencoding::encode(&input);
    url.set_query(Some(&input));
    Some(url)
    }
    [4.7108]
    [4.5817]
    response::Header::Input(s) => handle::input(s, ui, url),
  • replacement in src/main.rs at line 147
    [4.5859][4.7635:7673](),[4.7635][4.7635:7673](),[4.7673][4.81:156](),[4.156][4.5860:5904](),[4.5904][4.7803:7856](),[4.7803][4.7803:7856](),[4.7856][4.0:47](),[4.47][4.41:92](),[4.7856][4.41:92](),[4.92][4.168:197](),[4.168][4.168:197]()
    let Success { mime } = s;
    let body = stream.read_string().expect("failed to read body");
    //TODO remove this debug output
    println!("page is encoded as {mime:?}");
    println!("response body: {body}");
    let media = decode_media(mime, &body);
    media.display();
    [4.5859]
    [4.7888]
    handle::success(s, stream, ui);
  • edit in src/main.rs at line 149
    [4.7905][4.7905:7915](),[4.7915][4.5905:5948](),[4.5948][4.7958:8328](),[4.7958][4.7958:8328]()
    }
    response::Header::Redirect(s) => {
    let Redirect { url, temporary } = s;
    if let Some(false) = temporary {
    println!("Server has permanently moved. Redirecting to: {url}");
    println!("Please update your records!");
    } else {
    println!("Server has temporarily moved. Redirecting to: {url}");
    }
    Some(url)
  • edit in src/main.rs at line 150
    [4.8338]
    [4.5949]
    response::Header::Redirect(s) => Some(handle::redirect(s, ui)),
  • replacement in src/main.rs at line 152
    [4.5992][4.8381:8543](),[4.8381][4.8381:8543]()
    let FailTemp { message, typ } = s;
    println!("Temporary failure: {typ:?}! Please try again. Details:");
    println!("{message}");
    [4.5992]
    [4.8543]
    handle::fail_temp(s, ui);
  • replacement in src/main.rs at line 156
    [4.6036][4.8613:8757](),[4.8613][4.8613:8757]()
    let FailPerm { message, typ } = s;
    println!("Permanent failure: {typ:?}! Details:");
    println!("{message}");
    [4.6036]
    [4.8757]
    handle::fail_perm(s, ui);
  • replacement in src/main.rs at line 160
    [4.6084][4.8831:8982](),[4.8831][4.8831:8982]()
    let CertRequired { message, typ } = s;
    println!("Certificate required: {typ:?}! Details:");
    println!("{message}");
    [4.6084]
    [4.8982]
    handle::cert_required(s, ui);
  • edit in src/main.rs at line 162
    [4.8999]
    [4.8999]
    }
    }
    }
    mod handle {
    use crate::{
    decode_media,
    io::ReadExt,
    response::header::{CertRequired, FailPerm, FailTemp, Input, Redirect, Success},
    UI,
    };
    use std::io::Read;
    use url::Url;
    pub fn cert_required(s: CertRequired, ui: &mut impl UI) {
    let CertRequired { message, typ } = s;
    ui.warn(format_args!(
    "Certificate required: {typ:?}! Details:\n{message}"
    ));
    }
    pub fn fail_perm(s: FailPerm, ui: &mut impl UI) {
    let FailPerm { message, typ } = s;
    ui.warn(format_args!(
    "Permanent failure: {typ:?}! Details:\n{message}"
    ));
    }
    pub fn fail_temp(s: FailTemp, ui: &mut impl UI) {
    let FailTemp { message, typ } = s;
    ui.warn(format_args!(
    "Temporary failure: {typ:?}! Please try again. Details:\n{message}"
    ));
    }
    pub fn redirect(s: Redirect, ui: &mut impl UI) -> Url {
    let Redirect { url, temporary } = s;
    if let Some(false) = temporary {
    ui.warn(format_args!(
    "Server has permanently moved. Redirecting to: {url}\nPlease update your records!"
    ));
    } else {
    ui.warn(format_args!(
    "Server has temporarily moved. Redirecting to: {url}"
    ));
  • edit in src/main.rs at line 208
    [4.9009]
    [4.9009]
    url
    }
    pub fn success(s: Success, mut stream: impl Read, ui: &mut impl UI) {
    let Success { mime } = s;
    let body = stream.read_string().expect("failed to read body");
    //TODO remove this debug output
    println!("page is encoded as {mime:?}");
    println!("response body: {body}");
    let media = decode_media(mime, &body);
    ui.show(media);
    }
    pub fn input(s: Input, ui: &mut impl UI, mut url: Url) -> Option<Url> {
    let Input { prompt, sensitive } = s;
    let input = if let Some(true) = sensitive {
    ui.read_secret(&prompt)
    } else {
    ui.read(&prompt)
    };
    let input = urlencoding::encode(&input);
    url.set_query(Some(&input));
    Some(url)
  • replacement in src/main.rs at line 235
    [4.158][4.264:355](),[4.264][4.264:355](),[4.355][4.159:211](),[4.211][4.416:463](),[4.416][4.416:463]()
    let media = match mime.type_() {
    mime::TEXT => match mime.subtype().as_ref() {
    "gemini" => Media::Gemini(body.into()),
    _ => Media::Text(body),
    },
    [4.158]
    [4.463]
    let media = match (mime.type_(), mime.subtype().as_ref()) {
    (mime::TEXT, "gemini") => Media::Gemini(body.into()),
  • replacement in src/main.rs at line 280
    [4.6208][4.6208:6300]()
    use super::{
    handle_response_header,
    response::{header, Header},
    };
    [4.6208]
    [4.6300]
    use crate::{cl::Cli, read_response_header, send_request};
    use super::response::{header, Header};
  • replacement in src/main.rs at line 285
    [3.34][3.34:58]()
    fn handle_redirect(
    [3.34]
    [3.58]
    fn test_handle_redirect(
  • replacement in src/main.rs at line 287
    [3.111][3.111:180]()
    #[values(None, Some(true), Some(false))] temp: Option<bool>,
    [3.111]
    [3.180]
    #[values(None, Some(true), Some(false))] temporary: Option<bool>,
  • replacement in src/main.rs at line 291
    [4.6816][4.6816:6845]()
    temporary: temp,
    [4.6816]
    [4.6845]
    temporary,
  • replacement in src/main.rs at line 293
    [4.6857][4.6857:7088]()
    let url = "gopher://abc.de".parse().unwrap();
    let mut stream: &[u8] = b"";
    let Some(url) = handle_response_header(head, url, &mut stream)
    else {panic!("expected redirect handling to return a url")};
    [4.6857]
    [4.7088]
    let mut app = Cli {};
    let url = {
    let crate::response::Header::Redirect(s) = head
    else {
    panic!("expected redirect")
    };
    crate::handle::redirect(s, &mut app)
    };
  • edit in src/main.rs at line 302
    [4.7125]
    [4.7125]
    }
    #[test]
    fn test_request() {
    let url: Url = "gemini://some/url".try_into().unwrap();
    let req = b"gemini://some/url\r\n";
    let mut net_stream = [0; 1024];
    send_request(&mut net_stream.as_mut_slice(), &url).expect("error sending request");
    assert_eq!(req, &net_stream[..req.len()]);
    }
    #[rstest]
    #[case('0', Some(false))]
    #[case('1', Some(true))]
    #[case('3', None)]
    fn test_response_input(#[case] sensitive_code: char, #[case] sensitive: Option<bool>) {
    let exp_head = Header::Input(header::Input {
    prompt: "prompt text".to_string(),
    sensitive,
    });
    let resp_text = format!("1{sensitive_code} prompt text\r\n");
    let resp_head: &[u8] = resp_text.as_bytes();
    let head = read_response_header(&mut &*resp_head).expect("failed to read response");
    assert_eq!(exp_head, head);
  • edit in src/main.rs at line 330
    [4.7131]
    [4.7131]
    #[test]
    fn test_response_body() {
    let body: &[u8] = b"some text\r\n";
    }
  • edit in Cargo.toml at line 9
    [4.42105]
    [4.42105]
    ansi-control-codes = "0.0.2"
    ansi_term = "0.12.1"
  • edit in Cargo.lock at line 6
    [4.42429]
    [4.42429]
    name = "ansi-control-codes"
    version = "0.0.2"
    source = "registry+https://github.com/rust-lang/crates.io-index"
    checksum = "b7ca68a5142d31bc74840dc5520efedf65a3feb348fcca74d38ae1dadc0822f8"
    [[package]]
    name = "ansi_term"
    version = "0.12.1"
    source = "registry+https://github.com/rust-lang/crates.io-index"
    checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2"
    dependencies = [
    "winapi",
    ]
    [[package]]
  • edit in Cargo.lock at line 294
    [4.46837]
    [4.46837]
    "ansi-control-codes",
    "ansi_term",