New tests, refactor to be more modular, improve prompt ui, add test run parameter
Dependencies
- [2]
ARXVO5XAMake connection generic on the network - [3]
JWLMAS7USimplify tests with rstest - [4]
3SPNKI46Improve parsing. Add modules that were missed - [5]
AOJXTWBZRefactor read_until - [6]
WGRFJRTEAdd tests, rename Status to Header, implement redirect and proper input handling - [7]
XGDABCJNAdd match_str macro - [8]
DBKKKHC2Initial commit - [9]
AMTMTTJTUpgrade status parsing - [10]
ONQEIR5BWIP mime-type handling - [11]
HVFBGX2AImplement basic text/gemini - [12]
OSV63NXNImplement contiguous preformat blocks with alt-text
Change contents
- replacement in src/server.rs at line 16
pub struct TcpNetwork<V>(V, !);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
}impl<'a> Media<'a> {pub fn display(&self) {match self {Media::Gemini(g) => println!("{g:#?}"), // TODOMedia::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};use server::Network;use std::{fmt::Display,io::{BufReader, Read, Result as ioResult, Write},}; - edit in src/main.rs at line 21
use std::{fmt::Display,io::{stdout, Write},}; - edit in src/main.rs at line 27
use url::Url;use crate::{danger, media::Media, run, server::TcpNetwork, UI}; - edit in src/main.rs at line 33
#[arg(short, long)]pub test_prompt: bool, - edit in src/main.rs at line 37
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:#?}"), // TODOMedia::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
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) {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
fn run_url<N: Network>(url: Url) -> Option<Url>fn run_url<N: Network>(ui: &mut impl UI, url: Url) -> Option<Url> - replacement in src/main.rs at line 135
handle_response_header(head, url, stream)handle_response_header(ui, head, url, stream) - edit in src/main.rs at line 139
ui: &mut impl UI, - replacement in src/main.rs at line 141
mut url: Url,mut stream: impl Read,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 inputrpassword::read_password()} else {// Read plain inputstd::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)}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 outputprintln!("page is encoded as {mime:?}");println!("response body: {body}");let media = decode_media(mime, &body);media.display();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
response::Header::Redirect(s) => Some(handle::redirect(s, ui)), - replacement in src/main.rs at line 152
let FailTemp { message, typ } = s;println!("Temporary failure: {typ:?}! Please try again. Details:");println!("{message}");handle::fail_temp(s, ui); - replacement in src/main.rs at line 156
let FailPerm { message, typ } = s;println!("Permanent failure: {typ:?}! Details:");println!("{message}");handle::fail_perm(s, ui); - replacement in src/main.rs at line 160
let CertRequired { message, typ } = s;println!("Certificate required: {typ:?}! Details:");println!("{message}");handle::cert_required(s, ui); - edit in src/main.rs at line 162
}}}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
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 outputprintln!("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),},let media = match (mime.type_(), mime.subtype().as_ref()) {(mime::TEXT, "gemini") => Media::Gemini(body.into()), - replacement in src/main.rs at line 280
use super::{handle_response_header,response::{header, Header},};use crate::{cl::Cli, read_response_header, send_request};use super::response::{header, Header}; - replacement in src/main.rs at line 285
fn handle_redirect(fn test_handle_redirect( - replacement in src/main.rs at line 287
#[values(None, Some(true), Some(false))] temp: Option<bool>,#[values(None, Some(true), Some(false))] temporary: Option<bool>, - replacement in src/main.rs at line 291
temporary: temp,temporary, - replacement in src/main.rs at line 293
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")};let mut app = Cli {};let url = {let crate::response::Header::Redirect(s) = headelse {panic!("expected redirect")};crate::handle::redirect(s, &mut app)}; - edit in src/main.rs at line 302
}#[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
#[test]fn test_response_body() {let body: &[u8] = b"some text\r\n";} - edit in Cargo.toml at line 9
ansi-control-codes = "0.0.2"ansi_term = "0.12.1" - edit in Cargo.lock at line 6
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
"ansi-control-codes","ansi_term",