oas3-gen 0.20.0

A rust type generator for OpenAPI v3.1.x specification.
use std::collections::HashSet;

use http::Method;
use indexmap::IndexMap;
use oas3::Spec;

use crate::naming::identifiers::to_rust_field_name;

#[derive(Debug, Clone)]
pub struct OperationLocation {
  pub method: Method,
  pub path: String,
}

#[derive(Debug)]
pub struct OperationRegistry {
  id_to_location: IndexMap<String, OperationLocation>,
  spec: Spec,
}

impl OperationRegistry {
  pub fn from_spec(spec: &Spec) -> Self {
    Self::from_spec_filtered(spec, None, None)
  }

  pub fn from_spec_filtered(
    spec: &Spec,
    only_operations: Option<&HashSet<String>>,
    excluded_operations: Option<&HashSet<String>>,
  ) -> Self {
    let mut id_to_location = IndexMap::new();

    for (path, method, operation) in spec.operations() {
      let stable_id = compute_stable_id(method.as_str(), &path, operation);

      if let Some(included) = only_operations
        && !included.contains(&stable_id)
      {
        continue;
      }

      if let Some(excluded) = excluded_operations
        && excluded.contains(&stable_id)
      {
        continue;
      }

      let location = OperationLocation {
        method: method.clone(),
        path,
      };
      id_to_location.insert(stable_id, location);
    }

    Self {
      id_to_location,
      spec: spec.clone(),
    }
  }

  pub fn operations(&self) -> impl Iterator<Item = (&str, &OperationLocation)> {
    self.id_to_location.iter().map(|(id, loc)| (id.as_str(), loc))
  }

  pub fn operations_with_details(&self) -> impl Iterator<Item = (&str, &Method, &str, &oas3::spec::Operation)> + '_ {
    self.id_to_location.iter().filter_map(|(stable_id, location)| {
      let paths = self.spec.paths.as_ref()?;
      let path_item = paths.get(&location.path)?;

      let operation = match location.method {
        Method::GET => path_item.get.as_ref(),
        Method::POST => path_item.post.as_ref(),
        Method::PUT => path_item.put.as_ref(),
        Method::DELETE => path_item.delete.as_ref(),
        Method::PATCH => path_item.patch.as_ref(),
        Method::OPTIONS => path_item.options.as_ref(),
        Method::HEAD => path_item.head.as_ref(),
        Method::TRACE => path_item.trace.as_ref(),
        _ => None,
      }?;

      Some((stable_id.as_str(), &location.method, location.path.as_str(), operation))
    })
  }

  #[cfg(test)]
  pub fn len(&self) -> usize {
    self.id_to_location.len()
  }

  #[cfg(test)]
  #[must_use]
  pub fn is_empty(&self) -> bool {
    self.id_to_location.is_empty()
  }
}

pub fn compute_stable_id(method: &str, path: &str, operation: &oas3::spec::Operation) -> String {
  let id = operation
    .operation_id
    .clone()
    .unwrap_or_else(|| generate_operation_id(method, path));
  to_rust_field_name(&id)
}

pub(crate) fn generate_operation_id(method: &str, path: &str) -> String {
  let path_parts: Vec<&str> = path
    .split('/')
    .filter(|s| !s.is_empty())
    .map(|s| {
      if s.starts_with('{') && s.ends_with('}') {
        "by_id"
      } else {
        s
      }
    })
    .collect();

  let method_lower = method.to_lowercase();
  if path_parts.is_empty() {
    method_lower
  } else {
    format!("{}_{}", method_lower, path_parts.join("_"))
  }
}