Merge pull request #14 from panicbit/11_gemini_documents
implement document API
This commit is contained in:
commit
d3f65f2fc2
|
@ -5,6 +5,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [Unreleased]
|
||||
### Added
|
||||
- `document` API for creating Gemini documents
|
||||
|
||||
## [0.3.0] - 2020-11-14
|
||||
### Added
|
||||
|
|
50
examples/document.rs
Normal file
50
examples/document.rs
Normal file
|
@ -0,0 +1,50 @@
|
|||
use anyhow::*;
|
||||
use futures::{future::BoxFuture, FutureExt};
|
||||
use log::LevelFilter;
|
||||
use northstar::{Server, Request, Response, GEMINI_PORT, Document};
|
||||
use northstar::document::HeadingLevel::*;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
env_logger::builder()
|
||||
.filter_module("northstar", LevelFilter::Debug)
|
||||
.init();
|
||||
|
||||
Server::bind(("localhost", GEMINI_PORT))
|
||||
.serve(handle_request)
|
||||
.await
|
||||
}
|
||||
|
||||
fn handle_request(_request: Request) -> BoxFuture<'static, Result<Response>> {
|
||||
async move {
|
||||
let mut document = Document::new();
|
||||
|
||||
document
|
||||
.add_preformatted(include_str!("northstar_logo.txt"))
|
||||
.add_blank_line()
|
||||
.add_link("https://docs.rs/northstar", "Documentation")
|
||||
.add_link("https://github.com/panicbit/northstar", "GitHub")
|
||||
.add_blank_line()
|
||||
.add_heading(H1, "Usage")
|
||||
.add_blank_line()
|
||||
.add_text("Add the latest version of northstar to your `Cargo.toml`.")
|
||||
.add_blank_line()
|
||||
.add_heading(H2, "Manually")
|
||||
.add_blank_line()
|
||||
.add_preformatted_with_alt("toml", r#"northstar = "0.3.0" # check crates.io for the latest version"#)
|
||||
.add_blank_line()
|
||||
.add_heading(H2, "Automatically")
|
||||
.add_blank_line()
|
||||
.add_preformatted_with_alt("sh", "cargo add northstar")
|
||||
.add_blank_line()
|
||||
.add_heading(H1, "Generating a key & certificate")
|
||||
.add_blank_line()
|
||||
.add_preformatted_with_alt("sh", concat!(
|
||||
"mkdir cert && cd cert\n",
|
||||
"openssl req -x509 -nodes -newkey rsa:4096 -keyout key.pem -out cert.pem -days 365",
|
||||
));
|
||||
|
||||
Ok(Response::document(document))
|
||||
}
|
||||
.boxed()
|
||||
}
|
5
examples/northstar_logo.txt
Normal file
5
examples/northstar_logo.txt
Normal file
|
@ -0,0 +1,5 @@
|
|||
__ __ __
|
||||
____ ____ _____/ /_/ /_ _____/ /_____ ______
|
||||
/ __ \/ __ \/ ___/ __/ __ \/ ___/ __/ __ `/ ___/
|
||||
/ / / / /_/ / / / /_/ / / (__ ) /_/ /_/ / /
|
||||
/_/ /_/\____/_/ \__/_/ /_/____/\__/\__,_/_/
|
|
@ -1,5 +1,6 @@
|
|||
pub use ::mime::Mime;
|
||||
pub use rustls::Certificate;
|
||||
pub use uriparse::URIReference;
|
||||
|
||||
mod meta;
|
||||
pub use self::meta::Meta;
|
||||
|
@ -18,3 +19,6 @@ pub use response::Response;
|
|||
|
||||
mod body;
|
||||
pub use body::Body;
|
||||
|
||||
pub mod document;
|
||||
pub use document::Document;
|
||||
|
|
|
@ -1,10 +1,18 @@
|
|||
use tokio::{io::AsyncRead, fs::File};
|
||||
|
||||
use crate::types::Document;
|
||||
|
||||
pub enum Body {
|
||||
Bytes(Vec<u8>),
|
||||
Reader(Box<dyn AsyncRead + Send + Sync + Unpin>),
|
||||
}
|
||||
|
||||
impl From<Document> for Body {
|
||||
fn from(document: Document) -> Self {
|
||||
Body::from(document.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Vec<u8>> for Body {
|
||||
fn from(bytes: Vec<u8>) -> Self {
|
||||
Self::Bytes(bytes)
|
||||
|
|
554
src/types/document.rs
Normal file
554
src/types/document.rs
Normal file
|
@ -0,0 +1,554 @@
|
|||
//! Provides types for creating Gemini Documents.
|
||||
//!
|
||||
//! The module is centered around the `Document` type,
|
||||
//! which provides all the necessary methods for programatically
|
||||
//! creation of Gemini documents.
|
||||
//!
|
||||
//! # Examples
|
||||
//!
|
||||
//! ```
|
||||
//! use northstar::document::HeadingLevel::*;
|
||||
//!
|
||||
//! let mut document = northstar::Document::new();
|
||||
//!
|
||||
//! document.add_heading(H1, "Heading 1");
|
||||
//! document.add_heading(H2, "Heading 2");
|
||||
//! document.add_heading(H3, "Heading 3");
|
||||
//! document.add_blank_line();
|
||||
//! document.add_text("text");
|
||||
//! document.add_link("gemini://gemini.circumlunar.space", "Project Gemini");
|
||||
//! document.add_unordered_list_item("list item");
|
||||
//! document.add_quote("quote");
|
||||
//! document.add_preformatted("preformatted");
|
||||
//!
|
||||
//! assert_eq!(document.to_string(), "\
|
||||
//! ## Heading 1\n\
|
||||
//! ### Heading 2\n\
|
||||
//! #### Heading 3\n\
|
||||
//! \n\
|
||||
//! text\n\
|
||||
//! => gemini://gemini.circumlunar.space/ Project Gemini\n\
|
||||
//! * list item\n\
|
||||
//! > quote\n\
|
||||
//! ```\n\
|
||||
//! preformatted\n\
|
||||
//! ```\n\
|
||||
//! ");
|
||||
//! ```
|
||||
#![warn(missing_docs)]
|
||||
use std::convert::TryInto;
|
||||
use std::fmt;
|
||||
|
||||
use itertools::Itertools;
|
||||
use crate::types::URIReference;
|
||||
use crate::util::Cowy;
|
||||
|
||||
#[derive(Default)]
|
||||
/// Represents a Gemini document.
|
||||
///
|
||||
/// Provides convenient methods for programatically
|
||||
/// creation of Gemini documents.
|
||||
pub struct Document {
|
||||
items: Vec<Item>,
|
||||
}
|
||||
|
||||
impl Document {
|
||||
/// Creates an empty Gemini `Document`.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// let document = northstar::Document::new();
|
||||
///
|
||||
/// assert_eq!(document.to_string(), "");
|
||||
/// ```
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// Adds an `item` to the document.
|
||||
///
|
||||
/// An `item` usually corresponds to a single line,
|
||||
/// except in the case of preformatted text.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```compile_fail
|
||||
/// use northstar::document::{Document, Item, Text};
|
||||
///
|
||||
/// let mut document = Document::new();
|
||||
/// let text = Text::new_lossy("foo");
|
||||
/// let item = Item::Text(text);
|
||||
///
|
||||
/// document.add_item(item);
|
||||
///
|
||||
/// assert_eq!(document.to_string(), "foo\n");
|
||||
/// ```
|
||||
fn add_item(&mut self, item: Item) -> &mut Self {
|
||||
self.items.push(item);
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds multiple `items` to the document.
|
||||
///
|
||||
/// This is a convenience wrapper around `add_item`.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```compile_fail
|
||||
/// use northstar::document::{Document, Item, Text};
|
||||
///
|
||||
/// let mut document = Document::new();
|
||||
/// let items = vec!["foo", "bar", "baz"]
|
||||
/// .into_iter()
|
||||
/// .map(Text::new_lossy)
|
||||
/// .map(Item::Text);
|
||||
///
|
||||
/// document.add_items(items);
|
||||
///
|
||||
/// assert_eq!(document.to_string(), "foo\nbar\nbaz\n");
|
||||
/// ```
|
||||
fn add_items<I>(&mut self, items: I) -> &mut Self
|
||||
where
|
||||
I: IntoIterator<Item = Item>,
|
||||
{
|
||||
self.items.extend(items);
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds a blank line to the document.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// let mut document = northstar::Document::new();
|
||||
///
|
||||
/// document.add_blank_line();
|
||||
///
|
||||
/// assert_eq!(document.to_string(), "\n");
|
||||
/// ```
|
||||
pub fn add_blank_line(&mut self) -> &mut Self {
|
||||
self.add_item(Item::Text(Text::blank()))
|
||||
}
|
||||
|
||||
/// Adds plain text to the document.
|
||||
///
|
||||
/// This function allows adding multiple lines at once.
|
||||
///
|
||||
/// It inserts a whitespace at the beginning of a line
|
||||
/// if it starts with a character sequence that
|
||||
/// would make it a non-plain text line (e.g. link, heading etc).
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// let mut document = northstar::Document::new();
|
||||
///
|
||||
/// document.add_text("hello\n* world!");
|
||||
///
|
||||
/// assert_eq!(document.to_string(), "hello\n * world!\n");
|
||||
/// ```
|
||||
pub fn add_text(&mut self, text: &str) -> &mut Self {
|
||||
let text = text
|
||||
.lines()
|
||||
.map(Text::new_lossy)
|
||||
.map(Item::Text);
|
||||
|
||||
self.add_items(text);
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds a link to the document.
|
||||
///
|
||||
/// `uri`s that fail to parse are substituted with `.`.
|
||||
///
|
||||
/// Consecutive newlines in `label` will be replaced
|
||||
/// with a single whitespace.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// let mut document = northstar::Document::new();
|
||||
///
|
||||
/// document.add_link("https://wikipedia.org", "Wiki\n\nWiki");
|
||||
///
|
||||
/// assert_eq!(document.to_string(), "=> https://wikipedia.org/ Wiki Wiki\n");
|
||||
/// ```
|
||||
pub fn add_link<'a, U>(&mut self, uri: U, label: impl Cowy<str>) -> &mut Self
|
||||
where
|
||||
U: TryInto<URIReference<'a>>,
|
||||
{
|
||||
let uri = uri
|
||||
.try_into()
|
||||
.map(URIReference::into_owned)
|
||||
.or_else(|_| ".".try_into()).expect("Northstar BUG");
|
||||
let label = LinkLabel::from_lossy(label);
|
||||
let link = Link { uri, label: Some(label) };
|
||||
let link = Item::Link(link);
|
||||
|
||||
self.add_item(link);
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds a link to the document, but without a label.
|
||||
///
|
||||
/// See `add_link` for details.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// let mut document = northstar::Document::new();
|
||||
///
|
||||
/// document.add_link_without_label("https://wikipedia.org");
|
||||
///
|
||||
/// assert_eq!(document.to_string(), "=> https://wikipedia.org/\n");
|
||||
/// ```
|
||||
pub fn add_link_without_label<'a, U>(&mut self, uri: U) -> &mut Self
|
||||
where
|
||||
U: TryInto<URIReference<'a>>,
|
||||
{
|
||||
let uri = uri
|
||||
.try_into()
|
||||
.map(URIReference::into_owned)
|
||||
.or_else(|_| ".".try_into()).expect("Northstar BUG");
|
||||
let link = Link {
|
||||
uri,
|
||||
label: None,
|
||||
};
|
||||
let link = Item::Link(link);
|
||||
|
||||
self.add_item(link);
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds a block of preformatted text.
|
||||
///
|
||||
/// Lines that start with ` ``` ` will be prependend with a whitespace.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// let mut document = northstar::Document::new();
|
||||
///
|
||||
/// document.add_preformatted("a\n b\n c");
|
||||
///
|
||||
/// assert_eq!(document.to_string(), "```\na\n b\n c\n```\n");
|
||||
/// ```
|
||||
pub fn add_preformatted(&mut self, preformatted_text: &str) -> &mut Self {
|
||||
self.add_preformatted_with_alt("", preformatted_text)
|
||||
}
|
||||
|
||||
/// Adds a block of preformatted text with an alt text.
|
||||
///
|
||||
/// Consecutive newlines in `alt` will be replaced
|
||||
/// with a single whitespace.
|
||||
///
|
||||
/// `preformatted_text` lines that start with ` ``` `
|
||||
/// will be prependend with a whitespace.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// let mut document = northstar::Document::new();
|
||||
///
|
||||
/// document.add_preformatted_with_alt("rust", "fn main() {\n}\n");
|
||||
///
|
||||
/// assert_eq!(document.to_string(), "```rust\nfn main() {\n}\n```\n");
|
||||
/// ```
|
||||
pub fn add_preformatted_with_alt(&mut self, alt: &str, preformatted_text: &str) -> &mut Self {
|
||||
let alt = AltText::new_lossy(alt);
|
||||
let lines = preformatted_text
|
||||
.lines()
|
||||
.map(PreformattedText::new_lossy)
|
||||
.collect();
|
||||
let preformatted = Preformatted {
|
||||
alt,
|
||||
lines,
|
||||
};
|
||||
let preformatted = Item::Preformatted(preformatted);
|
||||
|
||||
self.add_item(preformatted);
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds a heading.
|
||||
///
|
||||
/// Consecutive newlines in `text` will be replaced
|
||||
/// with a single whitespace.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use northstar::document::HeadingLevel::H1;
|
||||
///
|
||||
/// let mut document = northstar::Document::new();
|
||||
///
|
||||
/// document.add_heading(H1, "Welcome!");
|
||||
///
|
||||
/// assert_eq!(document.to_string(), "# Welcome!\n");
|
||||
/// ```
|
||||
pub fn add_heading(&mut self, level: HeadingLevel, text: impl Cowy<str>) -> &mut Self {
|
||||
let text = HeadingText::new_lossy(text);
|
||||
let heading = Heading {
|
||||
level,
|
||||
text,
|
||||
};
|
||||
let heading = Item::Heading(heading);
|
||||
|
||||
self.add_item(heading);
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds an unordered list item.
|
||||
///
|
||||
/// Consecutive newlines in `text` will be replaced
|
||||
/// with a single whitespace.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// let mut document = northstar::Document::new();
|
||||
///
|
||||
/// document.add_unordered_list_item("milk");
|
||||
/// document.add_unordered_list_item("eggs");
|
||||
///
|
||||
/// assert_eq!(document.to_string(), "* milk\n* eggs\n");
|
||||
/// ```
|
||||
pub fn add_unordered_list_item(&mut self, text: &str) -> &mut Self {
|
||||
let item = UnorderedListItem::new_lossy(text);
|
||||
let item = Item::UnorderedListItem(item);
|
||||
|
||||
self.add_item(item);
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds a quote.
|
||||
///
|
||||
/// This function allows adding multiple quote lines at once.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// let mut document = northstar::Document::new();
|
||||
///
|
||||
/// document.add_quote("I think,\ntherefore I am");
|
||||
///
|
||||
/// assert_eq!(document.to_string(), "> I think,\n> therefore I am\n");
|
||||
/// ```
|
||||
pub fn add_quote(&mut self, text: &str) -> &mut Self {
|
||||
let quote = text
|
||||
.lines()
|
||||
.map(Quote::new_lossy)
|
||||
.map(Item::Quote);
|
||||
|
||||
self.add_items(quote);
|
||||
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Document {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
for item in &self.items {
|
||||
match item {
|
||||
Item::Text(text) => writeln!(f, "{}", text.0)?,
|
||||
Item::Link(link) => {
|
||||
let separator = if link.label.is_some() {" "} else {""};
|
||||
let label = link.label.as_ref().map(|label| label.0.as_str())
|
||||
.unwrap_or("");
|
||||
|
||||
writeln!(f, "=> {}{}{}", link.uri, separator, label)?;
|
||||
}
|
||||
Item::Preformatted(preformatted) => {
|
||||
writeln!(f, "```{}", preformatted.alt.0)?;
|
||||
|
||||
for line in &preformatted.lines {
|
||||
writeln!(f, "{}", line.0)?;
|
||||
}
|
||||
|
||||
writeln!(f, "```")?
|
||||
}
|
||||
Item::Heading(heading) => {
|
||||
let level = match heading.level {
|
||||
HeadingLevel::H1 => "#",
|
||||
HeadingLevel::H2 => "##",
|
||||
HeadingLevel::H3 => "###",
|
||||
};
|
||||
|
||||
writeln!(f, "{} {}", level, heading.text.0)?;
|
||||
}
|
||||
Item::UnorderedListItem(item) => writeln!(f, "* {}", item.0)?,
|
||||
Item::Quote(quote) => writeln!(f, "> {}", quote.0)?,
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
enum Item {
|
||||
Text(Text),
|
||||
Link(Link),
|
||||
Preformatted(Preformatted),
|
||||
Heading(Heading),
|
||||
UnorderedListItem(UnorderedListItem),
|
||||
Quote(Quote),
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct Text(String);
|
||||
|
||||
impl Text {
|
||||
fn blank() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
fn new_lossy(line: impl Cowy<str>) -> Self {
|
||||
Self(lossy_escaped_line(line, SPECIAL_STARTS))
|
||||
}
|
||||
}
|
||||
|
||||
struct Link {
|
||||
uri: URIReference<'static>,
|
||||
label: Option<LinkLabel>,
|
||||
}
|
||||
|
||||
struct LinkLabel(String);
|
||||
|
||||
impl LinkLabel {
|
||||
fn from_lossy(line: impl Cowy<str>) -> Self {
|
||||
let line = strip_newlines(line);
|
||||
|
||||
LinkLabel(line)
|
||||
}
|
||||
}
|
||||
|
||||
struct Preformatted {
|
||||
alt: AltText,
|
||||
lines: Vec<PreformattedText>,
|
||||
}
|
||||
|
||||
struct PreformattedText(String);
|
||||
|
||||
impl PreformattedText {
|
||||
fn new_lossy(line: impl Cowy<str>) -> Self {
|
||||
Self(lossy_escaped_line(line, &[PREFORMATTED_TOGGLE_START]))
|
||||
}
|
||||
}
|
||||
|
||||
struct AltText(String);
|
||||
|
||||
impl AltText {
|
||||
fn new_lossy(alt: &str) -> Self {
|
||||
let alt = strip_newlines(alt);
|
||||
|
||||
Self(alt)
|
||||
}
|
||||
}
|
||||
|
||||
struct Heading {
|
||||
level: HeadingLevel,
|
||||
text: HeadingText,
|
||||
}
|
||||
|
||||
/// The level of a heading.
|
||||
pub enum HeadingLevel {
|
||||
/// Heading level 1 (`#`)
|
||||
H1,
|
||||
/// Heading level 2 (`##`)
|
||||
H2,
|
||||
/// Heading level 3 (`###`)
|
||||
H3,
|
||||
}
|
||||
|
||||
struct HeadingText(String);
|
||||
|
||||
impl HeadingText {
|
||||
fn new_lossy(line: impl Cowy<str>) -> Self {
|
||||
let line = strip_newlines(line);
|
||||
|
||||
Self(line)
|
||||
}
|
||||
}
|
||||
|
||||
struct UnorderedListItem(String);
|
||||
|
||||
impl UnorderedListItem {
|
||||
fn new_lossy(text: &str) -> Self {
|
||||
let text = strip_newlines(text);
|
||||
|
||||
Self(text)
|
||||
}
|
||||
}
|
||||
|
||||
struct Quote(String);
|
||||
|
||||
impl Quote {
|
||||
fn new_lossy(text: &str) -> Self {
|
||||
Self(lossy_escaped_line(text, &[QUOTE_START]))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const LINK_START: &str = "=>";
|
||||
const PREFORMATTED_TOGGLE_START: &str = "```";
|
||||
const HEADING_START: &str = "#";
|
||||
const UNORDERED_LIST_ITEM_START: &str = "*";
|
||||
const QUOTE_START: &str = ">";
|
||||
|
||||
const SPECIAL_STARTS: &[&str] = &[
|
||||
LINK_START,
|
||||
PREFORMATTED_TOGGLE_START,
|
||||
HEADING_START,
|
||||
UNORDERED_LIST_ITEM_START,
|
||||
QUOTE_START,
|
||||
];
|
||||
|
||||
fn starts_with_any(s: &str, starts: &[&str]) -> bool {
|
||||
for start in starts {
|
||||
if s.starts_with(start) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
fn lossy_escaped_line(line: impl Cowy<str>, escape_starts: &[&str]) -> String {
|
||||
let line_ref = line.as_ref();
|
||||
let contains_newline = line_ref.contains('\n');
|
||||
let has_special_start = starts_with_any(line_ref, escape_starts);
|
||||
|
||||
if !contains_newline && !has_special_start {
|
||||
return line.into();
|
||||
}
|
||||
|
||||
let mut line = String::new();
|
||||
|
||||
if has_special_start {
|
||||
line.push(' ');
|
||||
}
|
||||
|
||||
if let Some(line_ref) = line_ref.split('\n').next() {
|
||||
line.push_str(line_ref);
|
||||
}
|
||||
|
||||
line
|
||||
}
|
||||
|
||||
fn strip_newlines(text: impl Cowy<str>) -> String {
|
||||
if !text.as_ref().contains(&['\r', '\n'][..]) {
|
||||
return text.into();
|
||||
}
|
||||
|
||||
text.as_ref()
|
||||
.lines()
|
||||
.filter(|part| !part.is_empty())
|
||||
.join(" ")
|
||||
}
|
|
@ -1,5 +1,7 @@
|
|||
use anyhow::*;
|
||||
use mime::Mime;
|
||||
use crate::Mime;
|
||||
use crate::util::Cowy;
|
||||
|
||||
|
||||
#[derive(Debug,Clone,PartialEq,Eq,Default)]
|
||||
pub struct Meta(String);
|
||||
|
@ -9,7 +11,7 @@ impl Meta {
|
|||
|
||||
/// Creates a new "Meta" string.
|
||||
/// Fails if `meta` contains `\n`.
|
||||
pub fn new(meta: impl AsRef<str> + Into<String>) -> Result<Self> {
|
||||
pub fn new(meta: impl Cowy<str>) -> Result<Self> {
|
||||
ensure!(!meta.as_ref().contains("\n"), "Meta must not contain newlines");
|
||||
ensure!(meta.as_ref().len() <= Self::MAX_LEN, "Meta must not exceed {} bytes", Self::MAX_LEN);
|
||||
|
||||
|
@ -20,7 +22,7 @@ impl Meta {
|
|||
/// Truncates `meta` to before:
|
||||
/// - the first occurrence of `\n`
|
||||
/// - the character that makes `meta` exceed `Meta::MAX_LEN`
|
||||
pub fn new_lossy(meta: impl AsRef<str> + Into<String>) -> Self {
|
||||
pub fn new_lossy(meta: impl Cowy<str>) -> Self {
|
||||
let meta = meta.as_ref();
|
||||
let truncate_pos = meta.char_indices().position(|(i, ch)| {
|
||||
let is_newline = ch == '\n';
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
use anyhow::*;
|
||||
use crate::types::{ResponseHeader, Body, Mime};
|
||||
use crate::types::{ResponseHeader, Body, Mime, Document};
|
||||
use crate::util::Cowy;
|
||||
use crate::GEMINI_MIME;
|
||||
|
||||
pub struct Response {
|
||||
header: ResponseHeader,
|
||||
|
@ -14,12 +16,16 @@ impl Response {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn input(prompt: impl AsRef<str> + Into<String>) -> Result<Self> {
|
||||
pub fn document(document: Document) -> Self {
|
||||
Self::success(&GEMINI_MIME).with_body(document)
|
||||
}
|
||||
|
||||
pub fn input(prompt: impl Cowy<str>) -> Result<Self> {
|
||||
let header = ResponseHeader::input(prompt)?;
|
||||
Ok(Self::new(header))
|
||||
}
|
||||
|
||||
pub fn input_lossy(prompt: impl AsRef<str> + Into<String>) -> Self {
|
||||
pub fn input_lossy(prompt: impl Cowy<str>) -> Self {
|
||||
let header = ResponseHeader::input_lossy(prompt);
|
||||
Self::new(header)
|
||||
}
|
||||
|
@ -29,7 +35,7 @@ impl Response {
|
|||
Self::new(header)
|
||||
}
|
||||
|
||||
pub fn server_error(reason: impl AsRef<str> + Into<String>) -> Result<Self> {
|
||||
pub fn server_error(reason: impl Cowy<str>) -> Result<Self> {
|
||||
let header = ResponseHeader::server_error(reason)?;
|
||||
Ok(Self::new(header))
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
use anyhow::*;
|
||||
use mime::Mime;
|
||||
use crate::Mime;
|
||||
use crate::util::Cowy;
|
||||
use crate::types::{Status, Meta};
|
||||
|
||||
#[derive(Debug,Clone)]
|
||||
|
@ -9,14 +10,14 @@ pub struct ResponseHeader {
|
|||
}
|
||||
|
||||
impl ResponseHeader {
|
||||
pub fn input(prompt: impl AsRef<str> + Into<String>) -> Result<Self> {
|
||||
pub fn input(prompt: impl Cowy<str>) -> Result<Self> {
|
||||
Ok(Self {
|
||||
status: Status::INPUT,
|
||||
meta: Meta::new(prompt).context("Invalid input prompt")?,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn input_lossy(prompt: impl AsRef<str> + Into<String>) -> Self {
|
||||
pub fn input_lossy(prompt: impl Cowy<str>) -> Self {
|
||||
Self {
|
||||
status: Status::INPUT,
|
||||
meta: Meta::new_lossy(prompt),
|
||||
|
@ -30,14 +31,14 @@ impl ResponseHeader {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn server_error(reason: impl AsRef<str> + Into<String>) -> Result<Self> {
|
||||
pub fn server_error(reason: impl Cowy<str>) -> Result<Self> {
|
||||
Ok(Self {
|
||||
status: Status::PERMANENT_FAILURE,
|
||||
meta: Meta::new(reason).context("Invalid server error reason")?,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn server_error_lossy(reason: impl AsRef<str> + Into<String>) -> Self {
|
||||
pub fn server_error_lossy(reason: impl Cowy<str>) -> Self {
|
||||
Self {
|
||||
status: Status::PERMANENT_FAILURE,
|
||||
meta: Meta::new_lossy(reason),
|
||||
|
|
47
src/util.rs
47
src/util.rs
|
@ -1,12 +1,12 @@
|
|||
use std::path::Path;
|
||||
use mime::Mime;
|
||||
use percent_encoding::utf8_percent_encode;
|
||||
use anyhow::*;
|
||||
use tokio::{
|
||||
fs::{self, File},
|
||||
io,
|
||||
};
|
||||
use crate::{GEMINI_MIME, GEMINI_MIME_STR, Response};
|
||||
use crate::GEMINI_MIME_STR;
|
||||
use crate::types::{Response, Document, document::HeadingLevel::*};
|
||||
use itertools::Itertools;
|
||||
|
||||
pub async fn serve_file<P: AsRef<Path>>(path: P, mime: &Mime) -> Result<Response> {
|
||||
|
@ -49,8 +49,6 @@ pub async fn serve_dir<D: AsRef<Path>, P: AsRef<Path>>(dir: D, virtual_path: &[P
|
|||
}
|
||||
|
||||
async fn serve_dir_listing<P: AsRef<Path>, B: AsRef<Path>>(path: P, virtual_path: &[B]) -> Result<Response> {
|
||||
use std::fmt::Write;
|
||||
|
||||
let mut dir = match fs::read_dir(path).await {
|
||||
Ok(dir) => dir,
|
||||
Err(err) => match err.kind() {
|
||||
|
@ -60,13 +58,13 @@ async fn serve_dir_listing<P: AsRef<Path>, B: AsRef<Path>>(path: P, virtual_path
|
|||
};
|
||||
|
||||
let breadcrumbs = virtual_path.iter().map(|segment| segment.as_ref().display()).join("/");
|
||||
let mut listing = String::new();
|
||||
|
||||
writeln!(listing, "# Index of /{}", breadcrumbs)?;
|
||||
writeln!(listing)?;
|
||||
let mut document = Document::new();
|
||||
|
||||
document.add_heading(H1, format!("Index of /{}", breadcrumbs));
|
||||
document.add_blank_line();
|
||||
|
||||
if virtual_path.get(0).map(<_>::as_ref) != Some(Path::new("")) {
|
||||
writeln!(listing, "=> .. 📁 ../")?;
|
||||
document.add_link("..", "📁 ../");
|
||||
}
|
||||
|
||||
while let Some(entry) = dir.next_entry().await.context("Failed to list directory")? {
|
||||
|
@ -75,18 +73,17 @@ async fn serve_dir_listing<P: AsRef<Path>, B: AsRef<Path>>(path: P, virtual_path
|
|||
let is_dir = entry.file_type().await
|
||||
.with_context(|| format!("Failed to get file type of `{}`", entry.path().display()))?
|
||||
.is_dir();
|
||||
let trailing_slash = if is_dir { "/" } else { "" };
|
||||
let uri = format!("./{}{}", file_name, trailing_slash);
|
||||
|
||||
writeln!(
|
||||
listing,
|
||||
"=> {link}{trailing_slash} {icon} {name}{trailing_slash}",
|
||||
document.add_link(uri.as_str(), format!("{icon} {name}{trailing_slash}",
|
||||
icon = if is_dir { '📁' } else { '📄' },
|
||||
link = utf8_percent_encode(&file_name, percent_encoding::NON_ALPHANUMERIC),
|
||||
trailing_slash = if is_dir { "/" } else { "" },
|
||||
name = file_name,
|
||||
)?;
|
||||
trailing_slash = trailing_slash
|
||||
));
|
||||
}
|
||||
|
||||
Ok(Response::success(&GEMINI_MIME).with_body(listing))
|
||||
Ok(Response::document(document))
|
||||
}
|
||||
|
||||
pub fn guess_mime_from_path<P: AsRef<Path>>(path: P) -> Mime {
|
||||
|
@ -102,6 +99,22 @@ pub fn guess_mime_from_path<P: AsRef<Path>>(path: P) -> Mime {
|
|||
},
|
||||
None => "application/octet-stream",
|
||||
};
|
||||
|
||||
|
||||
mime.parse::<Mime>().unwrap_or(mime::APPLICATION_OCTET_STREAM)
|
||||
}
|
||||
|
||||
/// A convenience trait alias for `AsRef<T> + Into<T::Owned>`,
|
||||
/// most commonly used to accept `&str` or `String`:
|
||||
///
|
||||
/// `Cowy<str>` ⇔ `AsRef<str> + Into<String>`
|
||||
pub trait Cowy<T>
|
||||
where
|
||||
Self: AsRef<T> + Into<T::Owned>,
|
||||
T: ToOwned + ?Sized,
|
||||
{}
|
||||
|
||||
impl<C, T> Cowy<T> for C
|
||||
where
|
||||
C: AsRef<T> + Into<T::Owned>,
|
||||
T: ToOwned + ?Sized,
|
||||
{}
|
||||
|
|
Loading…
Reference in a new issue