toss-api 0.1.0

A Vim-inspired TUI and CLI API client for exploring and testing endpoints
use serde::{Deserialize, Serialize};
use crate::cli::args::Method;

#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Collection {
    pub id: String,
    pub name: String,
    pub items: Vec<CollectionItem>,
    #[serde(default)]
    pub expanded: bool,
    #[serde(default)]
    pub env_vars: Vec<KVParam>,
}

#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(tag = "type")]
pub enum CollectionItem {
    Folder(Folder),
    Request(Request),
}

#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Folder {
    pub id: String,
    pub name: String,
    pub items: Vec<CollectionItem>,
    #[serde(default)]
    pub expanded: bool,
}

#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct KVParam {
    pub key: String,
    pub value: String,
    pub enabled: bool,
    pub description: Option<String>,
}

#[derive(Debug, Serialize, Deserialize, Clone)]
pub enum Auth {
    None,
    Bearer { token: String },
    Basic { username: String, password: String },
    ApiKey { key: String, value: String, in_header: bool },
}

#[derive(Debug, Serialize, Deserialize, Clone)]
pub enum RequestBody {
    None,
    Raw { content: String, content_type: String },
    FormData { items: Vec<KVParam> },
    XWwwFormUrlEncoded { items: Vec<KVParam> },
}

#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Request {
    pub id: String,
    pub name: String,
    pub method: Method,
    pub url: String,
    pub params: Vec<KVParam>,
    pub headers: Vec<KVParam>,
    pub auth: Auth,
    pub body: RequestBody,
    pub pre_request_script: Option<String>,
    pub post_response_script: Option<String>,
}

impl Collection {
    pub fn new(name: String) -> Self {
        Self {
            id: uuid::Uuid::new_v4().to_string(),
            name,
            items: Vec::new(),
            expanded: false,
            env_vars: Vec::new(),
        }
    }

    pub fn find_request_mut(&mut self, id: &str) -> Option<&mut Request> {
        for item in &mut self.items {
            if let Some(req) = item.find_request_mut(id) {
                return Some(req);
            }
        }
        None
    }

    pub fn find_request(&self, id: &str) -> Option<&Request> {
        for item in &self.items {
            if let Some(req) = item.find_request(id) {
                return Some(req);
            }
        }
        None
    }

    pub fn find_request_by_name(&self, name: &str) -> Option<&Request> {
        for item in &self.items {
            if let Some(req) = item.find_request_by_name(name) {
                return Some(req);
            }
        }
        None
    }

    pub fn replace_urls_with_placeholder(&mut self, base_url: &str, placeholder: &str) -> Vec<(String, String)> {
        let mut changed_ids = Vec::new();
        Self::recursive_replace(&mut self.items, base_url, placeholder, &mut changed_ids);
        changed_ids
    }

    fn recursive_replace(items: &mut [CollectionItem], base_url: &str, placeholder: &str, changed: &mut Vec<(String, String)>) {
        for item in items {
            match item {
                CollectionItem::Request(r) => {
                    if r.url.starts_with(base_url) {
                        r.url = r.url.replace(base_url, placeholder);
                        changed.push((r.id.clone(), r.url.clone()));
                    }
                }
                CollectionItem::Folder(f) => {
                    Self::recursive_replace(&mut f.items, base_url, placeholder, changed)
                }
            }
        }
    }
}

impl Folder {
    pub fn new(name: String) -> Self {
        Self {
            id: uuid::Uuid::new_v4().to_string(),
            name,
            items: Vec::new(),
            expanded: false,
        }
    }
}

impl CollectionItem {
    pub fn find_request_mut(&mut self, id: &str) -> Option<&mut Request> {
        match self {
            CollectionItem::Request(req) => {
                if req.id == id {
                    Some(req)
                } else {
                    None
                }
            }
            CollectionItem::Folder(f) => {
                for item in &mut f.items {
                    if let Some(req) = item.find_request_mut(id) {
                        return Some(req);
                    }
                }
                None
            }
        }
    }

    pub fn find_request(&self, id: &str) -> Option<&Request> {
        match self {
            CollectionItem::Request(req) => {
                if req.id == id {
                    Some(req)
                } else {
                    None
                }
            }
            CollectionItem::Folder(f) => {
                for item in &f.items {
                    if let Some(req) = item.find_request(id) {
                        return Some(req);
                    }
                }
                None
            }
        }
    }

    pub fn find_request_by_name(&self, name: &str) -> Option<&Request> {
        match self {
            CollectionItem::Request(req) => {
                if req.name == name {
                    Some(req)
                } else {
                    None
                }
            }
            CollectionItem::Folder(f) => {
                for item in &f.items {
                    if let Some(req) = item.find_request_by_name(name) {
                        return Some(req);
                    }
                }
                None
            }
        }
    }

    #[allow(dead_code)]
    pub fn name(&self) -> &str {
        match self {
            CollectionItem::Folder(f) => &f.name,
            CollectionItem::Request(r) => &r.name,
        }
    }

    #[allow(dead_code)]
    pub fn set_name(&mut self, name: String) {
        match self {
            CollectionItem::Folder(f) => f.name = name,
            CollectionItem::Request(r) => r.name = name,
        }
    }
}

impl Request {
    pub fn new(name: String, method: Method, url: String) -> Self {
        Self {
            id: uuid::Uuid::new_v4().to_string(),
            name,
            method,
            url,
            params: Vec::new(),
            headers: Vec::new(),
            auth: Auth::None,
            body: RequestBody::None,
            pre_request_script: None,
            post_response_script: None,
        }
    }
}