Add tests, rename Status to Header, implement redirect and proper input handling
Dependencies
- [2]
OSV63NXNImplement contiguous preformat blocks with alt-text - [3]
DBKKKHC2Initial commit - [4]
HVFBGX2AImplement basic text/gemini - [5]
3SPNKI46Improve parsing. Add modules that were missed - [6]
ONQEIR5BWIP mime-type handling - [7]
AOJXTWBZRefactor read_until - [8]
AMTMTTJTUpgrade status parsing
Change contents
- replacement in src/response.rs at line 3
pub enum Status {pub enum Header { - edit in src/response.rs at line 5
/// has a response body - replacement in src/response.rs at line 143
impl TryFrom<raw::Header> for Status {impl TryFrom<raw::Header> for Header { - replacement in src/response.rs at line 150
1 => Some(Status::Input((sub, meta).into())),2 => Some(Status::Success((sub, meta).into())),3 => Some(Status::Redirect((sub, meta).into())),4 => Some(Status::FailTemp((sub, meta).into())),5 => Some(Status::FailPerm((sub, meta).into())),6 => Some(Status::CertRequired((sub, meta).into())),1 => Some(Header::Input((sub, meta).into())),2 => Some(Header::Success((sub, meta).into())),3 => Some(Header::Redirect((sub, meta).into())),4 => Some(Header::FailTemp((sub, meta).into())),5 => Some(Header::FailPerm((sub, meta).into())),6 => Some(Header::CertRequired((sub, meta).into())), - file addition: media.rs[3.15]
use std::str::pattern::Pattern;use url::Url;#[derive(Clone, Debug)]pub enum Media<'a> {Gemini(Gemini<'a>),Text(&'a str),}impl<'a> Media<'a> {pub fn display(&self) {match self {Media::Gemini(g) => println!("{g:#?}"), // TODOMedia::Text(s) => println!("{s}"),}}}#[derive(Clone, Debug)]pub struct Gemini<'a> {// TODO include lang componentlines: Vec<Line<'a>>,}#[derive(Clone, Debug)]enum RawLine<'a> {Toggle { alt: TextLine<'a> },Line(Line<'a>),}#[derive(Clone, Debug)]pub enum Line<'a> {Heading {level: u8,title: TextLine<'a>,},Link {url: TextLine<'a>,description: Option<TextLine<'a>>,},Text(TextLine<'a>),Preformatted(Preformat<'a>),ListItem(TextLine<'a>),Quote(TextLine<'a>),}#[derive(Clone, Debug)]pub struct Preformat<'a> {alt: TextLine<'a>,lines: Vec<TextLine<'a>>,}fn split_trim_maybe_once<'a, P: Pattern<'a> + Copy>(s: &'a str,p: P,) -> (&'a str, Option<&'a str>) {match s.split_once(p) {Some((s, rem)) => (s, Some(rem.trim_start_matches(p))),None => (s, None),}}fn string_to_preformat(string: TextLine<'_>) -> Option<TextLine<'_>> {let line = string.0;if line.starts_with("```") {// ignore anything after the lead chars on preformat linesreturn None;} else {// ignore any other formatting between preformat toggle linesreturn Some(string);}}fn string_to_line(string: TextLine<'_>) -> RawLine {let line = string.0;RawLine::Line({if line.starts_with("```") {// ignore anything after the lead chars on preformat linesreturn RawLine::Toggle {alt: TextLine(&line[3..]),};} else if line.starts_with("=> ") {let line = &line[3..];match line.split_once(' ') {Some((url, desc)) => {let url = url.trim_start_matches(' ');Line::Link {url: TextLine(url),description: Some(TextLine(desc)),}}None => Line::Link {url: TextLine(line),description: None,},}} else if line.starts_with("* ") {Line::ListItem(TextLine(&line[2..]))} else if line.starts_with("# ") {Line::Heading {level: 1,title: TextLine(&line[2..]),}} else if line.starts_with("## ") {Line::Heading {level: 2,title: TextLine(&line[3..]),}} else if line.starts_with("### ") {Line::Heading {level: 3,title: TextLine(&line[4..]),}} else if line.starts_with(">") {Line::Quote(TextLine(&line[1..]))} else {Line::Text(string)}})}fn string_to_lines(value: &str) -> Vec<Line> {{let mut outer = vec![];let mut preformat_block: Option<Preformat> = None;let lines = value.lines().map(TextLine);for line in lines {if preformat_block.is_some() {match string_to_preformat(line) {// if we hit a toggle line, switch preformatting modeNone => {let Some(i) = preformat_block.take()else {unreachable!("This is within the is_some arm of the if")};outer.push(Line::Preformatted(i));}Some(p) => preformat_block.as_mut().unwrap().lines.push(p),}} else {match string_to_line(line) {// if we hit a toggle line, switch preformatting modeRawLine::Toggle { alt } => {preformat_block = Some(Preformat { alt, lines: vec![] });}RawLine::Line(l) => outer.push(l),}}}outer}}// guaranteed to be only a single line#[derive(Copy, Clone, Debug)]pub struct TextLine<'a>(&'a str);impl<'a> From<&'a str> for Gemini<'a> {fn from(value: &'a str) -> Self {Gemini {lines: string_to_lines(value),}}} - replacement in src/main.rs at line 9
io::{BufReader, Result as ioResult, Write},io::{BufReader, Read, Result as ioResult, Write}, - edit in src/main.rs at line 16
mod media; - edit in src/main.rs at line 27
- edit in src/main.rs at line 28
// TODO make tests using a fake connection to a local buffer - replacement in src/main.rs at line 36
run_url(url);let mut url: Url = url.parse().expect("Not a valid URL");while let Some(new_url) = run_url(url) {url = new_url;} - replacement in src/main.rs at line 42
fn run_url(url: String) -> Option<Url> {let mut url: Url = url.parse().expect("Not a valid URL");fn run_url(url: Url) -> Option<Url> {// must make a new connection per request - edit in src/main.rs at line 50
handle_response_header(head, url, stream)} - edit in src/main.rs at line 54
fn handle_response_header(head: response::Header,mut url: Url,mut stream: impl Read,) -> Option<Url> { - replacement in src/main.rs at line 60
response::Status::Input(s) => {response::Header::Input(s) => { - replacement in src/main.rs at line 63
let input = match sensitive {Some(true) => rpassword::read_password(),_ => std::io::stdin().lines().next().expect("End of input"),let input = if let Some(true) = sensitive {// Read sensitive inputrpassword::read_password()} else {// Read plain inputstd::io::stdin().lines().next().expect("End of input") - replacement in src/main.rs at line 75
response::Status::Success(s) => {response::Header::Success(s) => { - replacement in src/main.rs at line 78
// TODO use mime for anything//TODO remove this debug output - replacement in src/main.rs at line 85
response::Status::Redirect(s) => {response::Header::Redirect(s) => { - replacement in src/main.rs at line 95
response::Status::FailTemp(s) => {response::Header::FailTemp(s) => { - replacement in src/main.rs at line 101
response::Status::FailPerm(s) => {response::Header::FailPerm(s) => { - replacement in src/main.rs at line 107
response::Status::CertRequired(s) => {response::Header::CertRequired(s) => { - edit in src/main.rs at line 125[3.512]→[3.4501:4504](∅→∅),[3.9015]→[3.4501:4504](∅→∅),[3.4501]→[3.4501:4504](∅→∅),[3.4504]→[3.212:1018](∅→∅),[3.1018]→[2.48:79](∅→∅),[2.79]→[3.1040:1126](∅→∅),[3.1040]→[3.1040:1126](∅→∅),[3.1126]→[2.80:117](∅→∅),[2.117]→[3.1162:1223](∅→∅),[3.1162]→[3.1162:1223](∅→∅),[3.1223]→[2.118:244](∅→∅),[2.244]→[3.663:669](∅→∅),[3.1223]→[3.663:669](∅→∅),[3.663]→[3.663:669](∅→∅),[3.669]→[3.1224:1484](∅→∅),[3.1484]→[3.825:835](∅→∅),[3.825]→[3.825:835](∅→∅),[3.835]→[3.1485:1491](∅→∅),[3.1491]→[2.245:679](∅→∅),[2.679]→[3.1565:1618](∅→∅),[3.1565]→[3.1565:1618](∅→∅),[3.1618]→[2.680:721](∅→∅),[2.721]→[3.1643:1718](∅→∅),[3.1643]→[3.1643:1718](∅→∅),[3.1718]→[2.722:1215](∅→∅),[2.1215]→[3.2469:2495](∅→∅),[3.2469]→[3.2469:2495](∅→∅),[3.2688]→[3.2688:2710](∅→∅),[3.2710]→[2.1216:1368](∅→∅),[2.1368]→[3.2710:2728](∅→∅),[3.2710]→[3.2710:2728](∅→∅),[3.2728]→[2.1369:1548](∅→∅),[2.1548]→[3.2830:2909](∅→∅),[3.2830]→[3.2830:2909](∅→∅),[3.2909]→[2.1549:1647](∅→∅),[2.1647]→[3.2969:3048](∅→∅),[3.2969]→[3.2969:3048](∅→∅),[3.3048]→[2.1648:1747](∅→∅),[2.1747]→[3.3109:3188](∅→∅),[3.3109]→[3.3109:3188](∅→∅),[3.3188]→[2.1748:1918](∅→∅),[2.1918]→[3.3307:3389](∅→∅),[3.3307]→[3.3307:3389](∅→∅),[3.3389]→[2.1919:3059](∅→∅),[2.3059]→[3.3806:3828](∅→∅),[3.3806]→[3.3806:3828](∅→∅),[3.3926]→[3.3926:3944](∅→∅),[3.3944]→[2.3060:3102](∅→∅),[2.3102]→[3.3982:4282](∅→∅),[3.3982]→[3.3982:4282](∅→∅),[3.4282]→[3.851:857](∅→∅),[3.851]→[3.851:857](∅→∅)
}mod media {use std::str::pattern::Pattern;use url::Url;#[derive(Clone, Debug)]pub enum Media<'a> {Gemini(Gemini<'a>),Text(&'a str),}impl<'a> Media<'a> {pub fn display(&self) {match self {Media::Gemini(g) => println!("{g:#?}"), // TODOMedia::Text(s) => println!("{s}"),}}}#[derive(Clone, Debug)]pub struct Gemini<'a> {// TODO include lang componentlines: Vec<Line<'a>>,}#[derive(Clone, Debug)]enum RawLine<'a> {Toggle { alt: TextLine<'a> },Line(Line<'a>),}#[derive(Clone, Debug)]pub enum Line<'a> {Heading {level: u8,title: TextLine<'a>,},Link {url: TextLine<'a>,description: Option<TextLine<'a>>,},Text(TextLine<'a>),Preformatted(Preformat<'a>),ListItem(TextLine<'a>),Quote(TextLine<'a>),}#[derive(Clone, Debug)]pub struct Preformat<'a> {alt: TextLine<'a>,lines: Vec<TextLine<'a>>,}fn split_trim_maybe_once<'a, P: Pattern<'a> + Copy>(s: &'a str,p: P,) -> (&'a str, Option<&'a str>) {match s.split_once(p) {Some((s, rem)) => (s, Some(rem.trim_start_matches(p))),None => (s, None),}}fn string_to_preformat(string: TextLine<'_>) -> Option<TextLine<'_>> {let line = string.0;if line.starts_with("```") {// ignore anything after the lead chars on preformat linesreturn None;} else {// ignore any other formatting between preformat toggle linesreturn Some(string);}}fn string_to_line(string: TextLine<'_>) -> RawLine {let line = string.0;RawLine::Line({if line.starts_with("```") {// ignore anything after the lead chars on preformat linesreturn RawLine::Toggle {alt: TextLine(&line[3..]),};} else if line.starts_with("=> ") {let line = &line[3..];match line.split_once(' ') {Some((url, desc)) => {let url = url.trim_start_matches(' ');Line::Link {url: TextLine(url),description: Some(TextLine(desc)),}}None => Line::Link {url: TextLine(line),description: None,},}} else if line.starts_with("* ") {Line::ListItem(TextLine(&line[2..]))} else if line.starts_with("# ") {Line::Heading {level: 1,title: TextLine(&line[2..]),}} else if line.starts_with("## ") {Line::Heading {level: 2,title: TextLine(&line[3..]),}} else if line.starts_with("### ") {Line::Heading {level: 3,title: TextLine(&line[4..]),}} else if line.starts_with(">") {Line::Quote(TextLine(&line[1..]))} else {Line::Text(string)}})}fn string_to_lines(value: &str) -> Vec<Line> {{let mut outer = vec![];let mut preformat_block: Option<Preformat> = None;let lines = value.lines().map(TextLine);for line in lines {if preformat_block.is_some() {match string_to_preformat(line) {// if we hit a toggle line, switch preformatting modeNone => {let Some(i) = preformat_block.take()else {unreachable!("This is within the is_some arm of the if")};outer.push(Line::Preformatted(i));}Some(p) => preformat_block.as_mut().unwrap().lines.push(p),}} else {match string_to_line(line) {// if we hit a toggle line, switch preformatting modeRawLine::Toggle { alt } => {preformat_block = Some(Preformat { alt, lines: vec![] });}RawLine::Line(l) => outer.push(l),}}}outer}}// guaranteed to be only a single line#[derive(Copy, Clone, Debug)]pub struct TextLine<'a>(&'a str);impl<'a> From<&'a str> for Gemini<'a> {fn from(value: &'a str) -> Self {Gemini {lines: string_to_lines(value),}}} - replacement in src/main.rs at line 127
fn read_response_header(stream: &mut Stream) -> ioResult<response::Status> {fn read_response_header(stream: &mut Stream) -> ioResult<response::Header> { - edit in src/main.rs at line 159[3.5853]
#[cfg(test)]mod tests {use url::Url;use super::{handle_response_header,response::{header, Header},};#[test]fn handle_redirect_none() {handle_redirect("http://url.com".parse().unwrap(), None);}#[test]fn handle_redirect_perm() {handle_redirect("http://url.com".parse().unwrap(), Some(false));}#[test]fn handle_redirect_temp() {handle_redirect("http://url.com".parse().unwrap(), Some(true));}fn handle_redirect(expect_url: Url, temp: Option<bool>) {let head = Header::Redirect(header::Redirect {url: expect_url.clone(),temporary: temp,});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")};assert_eq!(url, expect_url);}}