#![allow(dead_code)]
use std::collections::BTreeMap;
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub enum HttpMethod {
Get,
Post,
Put,
Delete,
Patch,
}
impl HttpMethod {
pub fn as_str(&self) -> &'static str {
match self {
Self::Get => "get",
Self::Post => "post",
Self::Put => "put",
Self::Delete => "delete",
Self::Patch => "patch",
}
}
}
#[derive(Debug, Clone)]
pub struct ApiOperation {
pub method: HttpMethod,
pub summary: String,
pub operation_id: String,
pub tags: Vec<String>,
pub response_codes: Vec<u16>,
}
#[derive(Debug, Clone, Default)]
pub struct ApiPath {
pub operations: BTreeMap<String, ApiOperation>,
}
impl ApiPath {
pub fn add_operation(&mut self, method: HttpMethod, op: ApiOperation) {
self.operations.insert(method.as_str().to_string(), op);
}
pub fn operation_count(&self) -> usize {
self.operations.len()
}
}
#[derive(Debug, Clone)]
pub struct ApiInfo {
pub title: String,
pub version: String,
pub description: Option<String>,
}
#[derive(Debug, Clone, Default)]
pub struct OpenApiSpec {
pub info: Option<ApiInfo>,
pub paths: BTreeMap<String, ApiPath>,
pub servers: Vec<String>,
}
impl OpenApiSpec {
pub fn add_path(&mut self, path: impl Into<String>, item: ApiPath) {
self.paths.insert(path.into(), item);
}
pub fn path_count(&self) -> usize {
self.paths.len()
}
pub fn find_path(&self, path: &str) -> Option<&ApiPath> {
self.paths.get(path)
}
}
pub fn render_openapi_json(spec: &OpenApiSpec) -> String {
let title = spec
.info
.as_ref()
.map(|i| i.title.as_str())
.unwrap_or("API");
let version = spec
.info
.as_ref()
.map(|i| i.version.as_str())
.unwrap_or("1.0.0");
let paths_json: Vec<String> = spec
.paths
.iter()
.map(|(path, item)| {
let ops: Vec<String> = item
.operations
.iter()
.map(|(method, op)| {
format!(
r#""{method}":{{"summary":"{}","operationId":"{}"}}"#,
op.summary, op.operation_id
)
})
.collect();
format!(r#""{path}":{{{}}} "#, ops.join(","))
})
.collect();
format!(
r#"{{"openapi":"3.0.0","info":{{"title":"{title}","version":"{version}"}},"paths":{{{}}}}}"#,
paths_json.join(",")
)
}
pub fn validate_spec(spec: &OpenApiSpec) -> bool {
spec.info.is_some() && !spec.paths.is_empty()
}
pub fn total_operation_count(spec: &OpenApiSpec) -> usize {
spec.paths.values().map(|p| p.operation_count()).sum()
}
#[cfg(test)]
mod tests {
use super::*;
fn sample_spec() -> OpenApiSpec {
let mut spec = OpenApiSpec {
info: Some(ApiInfo {
title: "Test API".into(),
version: "1.0.0".into(),
description: None,
}),
..Default::default()
};
let mut path = ApiPath::default();
path.add_operation(
HttpMethod::Get,
ApiOperation {
method: HttpMethod::Get,
summary: "List things".into(),
operation_id: "listThings".into(),
tags: vec!["things".into()],
response_codes: vec![200],
},
);
spec.add_path("/things", path);
spec
}
#[test]
fn path_count() {
assert_eq!(sample_spec().path_count(), 1);
}
#[test]
fn find_path_found() {
assert!(sample_spec().find_path("/things").is_some());
}
#[test]
fn operation_count() {
let spec = sample_spec();
let p = spec.find_path("/things").expect("should succeed");
assert_eq!(p.operation_count(), 1);
}
#[test]
fn render_contains_openapi_version() {
assert!(render_openapi_json(&sample_spec()).contains("3.0.0"));
}
#[test]
fn render_contains_title() {
assert!(render_openapi_json(&sample_spec()).contains("Test API"));
}
#[test]
fn validate_ok() {
assert!(validate_spec(&sample_spec()));
}
#[test]
fn validate_no_info() {
let spec = OpenApiSpec::default();
assert!(!validate_spec(&spec));
}
#[test]
fn total_operations() {
assert_eq!(total_operation_count(&sample_spec()), 1);
}
#[test]
fn method_as_str() {
assert_eq!(HttpMethod::Post.as_str(), "post");
}
}