openapi-tui 0.10.2

This TUI allows you to list and browse APIs described by the openapi specification.
use std::{collections::HashMap, env};

use color_eyre::eyre::Result;
use openapi_31::v31::{Openapi, Operation, Server};

use crate::response::Response;

#[derive(Default)]
pub struct State {
  pub openapi_input_source: String,
  pub openapi_spec: Openapi,
  pub openapi_operations: Vec<OperationItem>,
  pub active_operation_index: usize,
  pub active_tag_name: Option<String>,
  pub active_filter: String,
  pub input_mode: InputMode,
  pub responses: HashMap<String, Response>,
}

#[derive(Debug, Default, Clone)]
pub enum OperationItemType {
  #[default]
  Path,
  Webhook,
}

#[derive(Debug, Default, Clone)]
pub struct OperationItem {
  pub path: String,
  pub method: String,
  pub operation: Operation,
  pub r#type: OperationItemType,
}

#[derive(Default, PartialEq)]
pub enum InputMode {
  #[default]
  Normal,
  Insert,
  Command,
}

impl State {
  async fn from_path(openapi_path: String) -> Result<Self> {
    let openapi_spec = tokio::fs::read_to_string(&openapi_path)
      .await
      .map(|content| serde_yaml::from_str::<Openapi>(content.as_str()))??;

    let openapi_operations = openapi_spec
      .into_operations()
      .map(|(path, method, operation)| {
        if path.starts_with('/') {
          OperationItem { path, method, operation, r#type: OperationItemType::Path }
        } else {
          OperationItem { path, method, operation, r#type: OperationItemType::Webhook }
        }
      })
      .collect::<Vec<_>>();
    Ok(Self {
      openapi_spec,
      openapi_input_source: openapi_path,
      openapi_operations,
      active_operation_index: 0,
      active_tag_name: None,
      active_filter: String::default(),
      input_mode: InputMode::Normal,
      responses: HashMap::default(),
    })
  }

  async fn from_url(openapi_url: reqwest::Url) -> Result<Self> {
    let resp: String = reqwest::get(openapi_url.clone()).await?.text().await?;
    let mut openapi_spec = serde_yaml::from_str::<Openapi>(resp.as_str())?;
    if openapi_spec.servers.is_none() {
      let origin = openapi_url.origin().ascii_serialization();
      openapi_spec.servers = Some(vec![openapi_31::v31::Server::new(format!("{}/", origin))]);
    }

    let openapi_operations = openapi_spec
      .into_operations()
      .map(|(path, method, operation)| {
        if path.starts_with('/') {
          OperationItem { path, method, operation, r#type: OperationItemType::Path }
        } else {
          OperationItem { path, method, operation, r#type: OperationItemType::Webhook }
        }
      })
      .collect::<Vec<_>>();
    Ok(Self {
      openapi_spec,
      openapi_input_source: openapi_url.to_string(),
      openapi_operations,
      active_operation_index: 0,
      active_tag_name: None,
      active_filter: String::default(),
      input_mode: InputMode::Normal,
      responses: HashMap::default(),
    })
  }

  pub async fn from_input(input: String) -> Result<Self> {
    if let Ok(url) = reqwest::Url::parse(input.as_str()) {
      State::from_url(url).await
    } else {
      State::from_path(input).await
    }
  }

  pub fn get_operation(&self, operation_id: Option<String>) -> Option<&OperationItem> {
    self.openapi_operations.iter().find(|operation_item| operation_item.operation.operation_id.eq(&operation_id))
  }

  pub fn active_operation(&self) -> Option<&OperationItem> {
    if let Some(active_tag) = &self.active_tag_name {
      self
        .openapi_operations
        .iter()
        .filter(|flat_operation| {
          flat_operation.has_tag(active_tag) && flat_operation.path.contains(self.active_filter.as_str())
        })
        .nth(self.active_operation_index)
    } else {
      self
        .openapi_operations
        .iter()
        .filter(|flat_operation| flat_operation.path.contains(self.active_filter.as_str()))
        .nth(self.active_operation_index)
    }
  }

  pub fn operations_len(&self) -> usize {
    if let Some(active_tag) = &self.active_tag_name {
      self
        .openapi_operations
        .iter()
        .filter(|item| item.has_tag(active_tag) && item.path.contains(self.active_filter.as_str()))
        .count()
    } else {
      self
        .openapi_operations
        .iter()
        .filter(|flat_operation| flat_operation.path.contains(self.active_filter.as_str()))
        .count()
    }
  }

  fn default_url(server: &Server) -> String {
    let mut url = server.url.clone();
    if let Some(variables) = &server.variables {
      for (k, v) in variables {
        url = url.replace(format!("{{{}}}", k).as_str(), &v.default);
      }
    }
    url.trim_end_matches('/').to_string()
  }

  pub fn default_server_urls(&self, extra_servers: &Option<Vec<Server>>) -> Vec<String> {
    let mut result = vec![];
    if let Ok(url) = env::var("OPENAPI_TUI_DEFAULT_SERVER") {
      result.push(url.trim_end_matches('/').to_string());
    }

    extra_servers.iter().flatten().for_each(|server| {
      result.push(State::default_url(server));
    });

    self.openapi_spec.servers.iter().flatten().for_each(|server| {
      result.push(State::default_url(server));
    });

    if result.is_empty() {
      result.push("http://localhost".to_string());
    }
    result
  }
}

impl OperationItem {
  pub fn has_tag(&self, tag: &String) -> bool {
    self.operation.tags.as_ref().is_some_and(|tags| tags.contains(tag))
  }
}