use alfrusco::{config, AsyncRunnable, Item, Workflow, WorkflowError};
use clap::Parser;
use serde::{Deserialize, Serialize};
#[derive(Parser, Debug)]
struct RandomUserWorkflow {
pub keyword: Vec<String>,
#[arg(short, long, env)]
pub name: Option<String>,
#[arg(long, default_value = "https://randomuser.me")]
pub api_url: String,
}
#[tokio::main]
pub async fn main() {
env_logger::init();
let command = RandomUserWorkflow::parse();
alfrusco::execute_async(&config::AlfredEnvProvider, command, &mut std::io::stdout()).await;
}
#[async_trait::async_trait]
impl AsyncRunnable for RandomUserWorkflow {
type Error = RandomUserError;
async fn run_async(self, wf: &mut Workflow) -> Result<(), RandomUserError> {
if let Some(name) = self.name {
wf.append_item(Item::new(format!("NAME DEFINED AS: '{name}'")));
return Ok(());
}
let query = self.keyword.join(" ");
wf.set_filter_keyword(query.clone());
let url = format!(
"{}/api/?inc=gender,name&results=50&seed=alfrusco",
self.api_url
);
let response = reqwest::get(&url).await?;
let response: RandomUserResponse = response.json().await?;
wf.append_items(
response
.results
.into_iter()
.map(|r| {
let title = format!("{} {} {}", r.name.title, r.name.first, r.name.last);
Item::new(&title)
.valid(false)
.autocomplete("workflow:nonsense")
.var("NAME", title)
})
.collect(),
);
Ok(())
}
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RandomUserResponse {
pub results: Vec<RandomUser>,
pub info: serde_json::Value,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RandomUser {
pub name: RandomUserName,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RandomUserName {
pub title: String,
pub first: String,
pub last: String,
}
#[derive(Debug)]
pub enum RandomUserError {
Reqwest(reqwest::Error),
Json(serde_json::Error),
}
impl From<reqwest::Error> for RandomUserError {
fn from(e: reqwest::Error) -> Self {
Self::Reqwest(e)
}
}
impl From<serde_json::Error> for RandomUserError {
fn from(e: serde_json::Error) -> Self {
Self::Json(e)
}
}
impl WorkflowError for RandomUserError {}
impl std::fmt::Display for RandomUserError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
RandomUserError::Reqwest(e) => write!(f, "Reqwest error: {e}"),
RandomUserError::Json(e) => write!(f, "JSON error: {e}"),
}
}
}
impl std::error::Error for RandomUserError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
RandomUserError::Reqwest(e) => Some(e),
RandomUserError::Json(e) => Some(e),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use wiremock::matchers::{method, path};
use wiremock::{Mock, MockServer, ResponseTemplate};
const MOCK_RESPONSE: &str = r#"{
"results": [
{"gender": "male", "name": {"title": "Mr", "first": "Fletcher", "last": "Hall"}},
{"gender": "female", "name": {"title": "Ms", "first": "Jane", "last": "Doe"}}
],
"info": {"seed": "alfrusco", "results": 2, "page": 1, "version": "1.4"}
}"#;
#[test]
fn test_random_user_deserialization() {
let response: RandomUserResponse = serde_json::from_str(MOCK_RESPONSE).unwrap();
assert_eq!(response.results.len(), 2);
assert_eq!(response.results[0].name.title, "Mr");
assert_eq!(response.results[0].name.first, "Fletcher");
assert_eq!(response.results[0].name.last, "Hall");
}
#[tokio::test]
async fn test_random_user_response() {
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/api/"))
.respond_with(
ResponseTemplate::new(200).set_body_raw(MOCK_RESPONSE, "application/json"),
)
.mount(&server)
.await;
let command = RandomUserWorkflow {
keyword: vec![],
name: None,
api_url: server.uri(),
};
let mut buffer = Vec::new();
let dir = tempfile::tempdir().unwrap().keep();
alfrusco::execute_async(&config::TestingProvider(dir), command, &mut buffer).await;
let output = String::from_utf8(buffer).unwrap();
assert!(output.contains("\"title\":\"Mr Fletcher Hall\""));
assert!(output.contains("\"title\":\"Ms Jane Doe\""));
}
#[tokio::test]
async fn test_random_user_name_override() {
let command = RandomUserWorkflow {
keyword: vec![],
name: Some("Alice".to_string()),
api_url: String::new(),
};
let mut buffer = Vec::new();
let dir = tempfile::tempdir().unwrap().keep();
alfrusco::execute_async(&config::TestingProvider(dir), command, &mut buffer).await;
let output = String::from_utf8(buffer).unwrap();
assert!(output.contains("NAME DEFINED AS: 'Alice'"));
}
}