Skip to content

Commit

Permalink
feat(fetch): create the fetch handler and stabilize the Handler int…
Browse files Browse the repository at this point in the history
…erface
  • Loading branch information
0x61nas committed Aug 24, 2023
1 parent 1646c14 commit 0ed22dd
Show file tree
Hide file tree
Showing 2 changed files with 121 additions and 5 deletions.
104 changes: 104 additions & 0 deletions src/helper/docs/handlers/fetch.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
use std::time::Duration;
use ureq::{AgentBuilder, Proxy};

use crate::error::{Error, Result};
use crate::helper::docs::handlers::{Handler, parse_args};

/// Fetch pages from an external source by http.
///
/// # Examples
///
/// ```
/// # use halp::helper::docs::handlers::fetch::FetchHandler;
/// # use halp::error::Result;
/// # use halp::helper::docs::handlers::Handler;
///
/// let git_cheat_page = FetchHandler.handle(&["https://cheat.sh/git".to_string()]);
/// assert!(git_cheat_page.is_ok());
/// let git_cheat_page = git_cheat_page.unwrap();
/// assert!(git_cheat_page.is_some());
/// println!("{}", git_cheat_page.unwrap());
/// ```
pub struct FetchHandler;

impl Handler for FetchHandler {
/// Fetch the help page from an external source by http.
///
/// The first argument is the URL to fetch, the rest of the arguments is used to configure the request.
/// The first argument is required, the rest is optional.
///
/// The possible arguments are:
/// - `method`: The HTTP method to use, default is `GET`.
/// - `body`: The request body, default is empty.
/// - `headers`: The request headers, default is empty.
/// - `timeout`: The request timeout in seconds, default is 10 seconds.
/// - `user-agent`: The request user agent, default is `help me plz - <Halp version>`.
/// - `proxy`: The request proxy, default is empty.
fn handle(&self, args: &[String]) -> Result<Option<String>> {
let Some(url) = args.get(0) else {
return Err(Error::InvalidArgument("url".to_string()));
};
let args = parse_args(&args[1..]);
// build request
let mut agent_builder = AgentBuilder::new().user_agent(args.get("user-agent")
.unwrap_or(&format!("help me plz - {}", env!("CARGO_PKG_VERSION")).to_string()));
if let Some(proxy) = args.get("proxy") {
agent_builder = agent_builder.proxy(Proxy::new(proxy)
.map_err(|_| Error::InvalidArgument("proxy".to_string()))?)
}
let agent = agent_builder.build();
let mut request = agent.request(args.get("method").unwrap_or(&"GET".to_string()), url)
.timeout(if let Some(timeout) = args.get("timeout") {
Duration::from_secs(timeout.parse::<u64>().map_err(|_| Error::InvalidArgument("timeout".to_string()))?)
} else {
Duration::from_secs(10)
});
// add headers if any
if let Some(headers) = args.get("headers") {
for header in headers.split(',') {
let mut header = header.split(':');
request = request.set(header.next().unwrap_or("").trim(),
header.next().unwrap_or("").trim());
}
}
let request = if let Some(body) = args.get("body") {
request.send_string(body)
} else {
request.call()
}.map_err(|e| Error::from(Box::new(e)))?;
let response = request.into_string().map_err(|e| Error::ProviderError(e.to_string()))?;
// handle potential errors
if response.is_empty() || response.contains("Unknown topic") || response.contains("No manual entry") {
return Err(Error::ProviderError("Unknown topic, This topic/command might has no page in this provider yet.".to_string()));
}
Ok(Some(response))
}
}

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

#[test]
fn test_fetch_cheat_sheet() -> Result<()> {
let output = FetchHandler.handle(&["https://cheat.sh/ls".to_string(), "user-agent:fetch".to_string()])?;
let output = output.expect("output is empty");
assert!(output.contains(
"# To display all files, along with the size (with unit suffixes) and timestamp:"
));
assert!(output.contains(
"# Long format list with size displayed using human-readable units (KiB, MiB, GiB):"
));
Ok(())
}

#[test]
fn test_fetch_unknown_topic() -> Result<()> {
let output = FetchHandler.handle(&["https://cheat.sh/unknown".to_string(), "user-agent:fetch".to_string()]);
assert!(output.is_err());
assert_eq!(output.expect_err("Unreachable").to_string(),
"External help provider error: `Unknown topic, This topic/command might has no page in this provider yet.`");
Ok(())
}
}
22 changes: 17 additions & 5 deletions src/helper/docs/handlers/mod.rs
Original file line number Diff line number Diff line change
@@ -1,17 +1,29 @@
use crate::error::Result;
/// The fetch operation handler.
pub mod fetch;

pub type Formatter = fn(&str) -> String;
use std::collections::HashMap;
use crate::error::Result;

/// The operation handler trait.
pub trait Handler {
/// Handles the operation.
///
/// # Arguments
/// - `cmd`: The command that wanted to get help for.
/// - `op`: The operation to handle + its arguments. the first element is the operation specific string.
/// - `args`: The operation arguments.
///
/// # Returns
/// This method returns a Result type. On successful, it contains a `String` with the content of the fetched page.
/// Or `None` if the operation don't have an output.
fn handle(&self, cmd: &str, op: &[String]) -> Option<Result<String>>;
fn handle(&self, args: &[String]) -> Result<Option<String>>;
}

fn parse_args(args: &[String]) -> HashMap<String, String> {
let mut map = HashMap::with_capacity(args.len());
for arg in args {
let mut parts = arg.splitn(2, ':');
let key = parts.next().expect("Unreachable");
let value = parts.next().unwrap_or("");
map.insert(key.to_string(), value.to_string());
}
map
}

0 comments on commit 0ed22dd

Please sign in to comment.