Wave 4: Full CLI command implementations - fetch, list, show, search, tags, aliases, doctor, cache lifecycle (bd-16o, bd-3km, bd-1dj, bd-acf, bd-3bl, bd-30a, bd-2s6, bd-1d4)
This commit is contained in:
666
src/cli/list.rs
666
src/cli/list.rs
@@ -1,6 +1,16 @@
|
||||
use clap::Args as ClapArgs;
|
||||
use std::collections::BTreeMap;
|
||||
use std::time::Instant;
|
||||
|
||||
use clap::Args as ClapArgs;
|
||||
use regex::Regex;
|
||||
use serde::Serialize;
|
||||
use tabled::Tabled;
|
||||
|
||||
use crate::core::cache::CacheManager;
|
||||
use crate::core::config::cache_dir;
|
||||
use crate::errors::SwaggerCliError;
|
||||
use crate::output::robot;
|
||||
use crate::output::table::render_table_or_empty;
|
||||
|
||||
/// List endpoints from a cached spec
|
||||
#[derive(Debug, ClapArgs)]
|
||||
@@ -8,19 +18,657 @@ pub struct Args {
|
||||
/// Alias of the cached spec
|
||||
pub alias: String,
|
||||
|
||||
/// Filter by HTTP method
|
||||
#[arg(long)]
|
||||
/// Filter by HTTP method (case-insensitive)
|
||||
#[arg(long, short = 'm')]
|
||||
pub method: Option<String>,
|
||||
|
||||
/// Filter by tag
|
||||
#[arg(long)]
|
||||
/// Filter by tag (endpoints containing this tag)
|
||||
#[arg(long, short = 't')]
|
||||
pub tag: Option<String>,
|
||||
|
||||
/// Filter by path pattern
|
||||
#[arg(long)]
|
||||
/// Filter by path pattern (regex)
|
||||
#[arg(long, short = 'p')]
|
||||
pub path: Option<String>,
|
||||
|
||||
/// Sort order: path, method, or tag
|
||||
#[arg(long, default_value = "path", value_parser = ["path", "method", "tag"])]
|
||||
pub sort: String,
|
||||
|
||||
/// Maximum number of endpoints to show
|
||||
#[arg(long, short = 'n', default_value = "50")]
|
||||
pub limit: usize,
|
||||
|
||||
/// Show all endpoints (no limit)
|
||||
#[arg(long, short = 'a')]
|
||||
pub all: bool,
|
||||
}
|
||||
|
||||
pub async fn execute(_args: &Args, _robot: bool) -> Result<(), SwaggerCliError> {
|
||||
Err(SwaggerCliError::Usage("list not yet implemented".into()))
|
||||
// ---------------------------------------------------------------------------
|
||||
// Robot output structs
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct ListOutput {
|
||||
endpoints: Vec<EndpointEntry>,
|
||||
total: usize,
|
||||
filtered: usize,
|
||||
applied_filters: BTreeMap<String, String>,
|
||||
meta: ListMeta,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct EndpointEntry {
|
||||
path: String,
|
||||
method: String,
|
||||
summary: Option<String>,
|
||||
operation_id: Option<String>,
|
||||
tags: Vec<String>,
|
||||
deprecated: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct ListMeta {
|
||||
alias: String,
|
||||
spec_version: String,
|
||||
cached_at: String,
|
||||
duration_ms: u64,
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Human output row
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[derive(Tabled)]
|
||||
struct EndpointRow {
|
||||
#[tabled(rename = "METHOD")]
|
||||
method: String,
|
||||
#[tabled(rename = "PATH")]
|
||||
path: String,
|
||||
#[tabled(rename = "SUMMARY")]
|
||||
summary: String,
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Map an HTTP method string to a sort rank.
|
||||
/// GET=0, POST=1, PUT=2, PATCH=3, DELETE=4, everything else=5.
|
||||
fn method_rank(method: &str) -> u8 {
|
||||
match method.to_uppercase().as_str() {
|
||||
"GET" => 0,
|
||||
"POST" => 1,
|
||||
"PUT" => 2,
|
||||
"PATCH" => 3,
|
||||
"DELETE" => 4,
|
||||
_ => 5,
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Execute
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
pub async fn execute(args: &Args, robot_mode: bool) -> Result<(), SwaggerCliError> {
|
||||
let start = Instant::now();
|
||||
|
||||
// Compile path regex early so we fail fast on invalid patterns
|
||||
let path_regex = match &args.path {
|
||||
Some(pattern) => {
|
||||
let re = Regex::new(pattern).map_err(|e| {
|
||||
SwaggerCliError::Usage(format!("Invalid path regex '{pattern}': {e}"))
|
||||
})?;
|
||||
Some(re)
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
|
||||
let cm = CacheManager::new(cache_dir());
|
||||
let (index, meta) = cm.load_index(&args.alias)?;
|
||||
|
||||
let total = index.endpoints.len();
|
||||
|
||||
// ---- Filter ----
|
||||
let method_upper = args.method.as_ref().map(|m| m.to_uppercase());
|
||||
let tag_lower = args.tag.as_ref().map(|t| t.to_lowercase());
|
||||
|
||||
let mut filtered: Vec<_> = index
|
||||
.endpoints
|
||||
.into_iter()
|
||||
.filter(|ep| {
|
||||
if let Some(ref m) = method_upper
|
||||
&& ep.method.to_uppercase() != *m
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if let Some(ref t) = tag_lower
|
||||
&& !ep
|
||||
.tags
|
||||
.iter()
|
||||
.any(|tag| tag.to_lowercase().contains(t.as_str()))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if let Some(ref re) = path_regex
|
||||
&& !re.is_match(&ep.path)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
true
|
||||
})
|
||||
.collect();
|
||||
|
||||
let filtered_count = filtered.len();
|
||||
|
||||
// ---- Sort ----
|
||||
match args.sort.as_str() {
|
||||
"method" => {
|
||||
filtered.sort_by(|a, b| {
|
||||
method_rank(&a.method)
|
||||
.cmp(&method_rank(&b.method))
|
||||
.then_with(|| a.path.cmp(&b.path))
|
||||
});
|
||||
}
|
||||
"tag" => {
|
||||
filtered.sort_by(|a, b| {
|
||||
let tag_a = a.tags.first().map(String::as_str).unwrap_or("");
|
||||
let tag_b = b.tags.first().map(String::as_str).unwrap_or("");
|
||||
tag_a
|
||||
.cmp(tag_b)
|
||||
.then_with(|| a.path.cmp(&b.path))
|
||||
.then_with(|| method_rank(&a.method).cmp(&method_rank(&b.method)))
|
||||
});
|
||||
}
|
||||
// "path" or anything else: default sort
|
||||
_ => {
|
||||
filtered.sort_by(|a, b| {
|
||||
a.path
|
||||
.cmp(&b.path)
|
||||
.then_with(|| method_rank(&a.method).cmp(&method_rank(&b.method)))
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Limit ----
|
||||
if !args.all {
|
||||
filtered.truncate(args.limit);
|
||||
}
|
||||
|
||||
let duration = start.elapsed();
|
||||
|
||||
// ---- Output ----
|
||||
if robot_mode {
|
||||
let mut applied_filters = BTreeMap::new();
|
||||
if let Some(ref m) = args.method {
|
||||
applied_filters.insert("method".into(), m.clone());
|
||||
}
|
||||
if let Some(ref t) = args.tag {
|
||||
applied_filters.insert("tag".into(), t.clone());
|
||||
}
|
||||
if let Some(ref p) = args.path {
|
||||
applied_filters.insert("path".into(), p.clone());
|
||||
}
|
||||
|
||||
let entries: Vec<EndpointEntry> = filtered
|
||||
.iter()
|
||||
.map(|ep| EndpointEntry {
|
||||
path: ep.path.clone(),
|
||||
method: ep.method.clone(),
|
||||
summary: ep.summary.clone(),
|
||||
operation_id: ep.operation_id.clone(),
|
||||
tags: ep.tags.clone(),
|
||||
deprecated: ep.deprecated,
|
||||
})
|
||||
.collect();
|
||||
|
||||
let output = ListOutput {
|
||||
endpoints: entries,
|
||||
total,
|
||||
filtered: filtered_count,
|
||||
applied_filters,
|
||||
meta: ListMeta {
|
||||
alias: args.alias.clone(),
|
||||
spec_version: meta.spec_version.clone(),
|
||||
cached_at: meta.fetched_at.to_rfc3339(),
|
||||
duration_ms: duration.as_millis().min(u64::MAX as u128) as u64,
|
||||
},
|
||||
};
|
||||
|
||||
robot::robot_success(output, "list", duration);
|
||||
} else {
|
||||
println!("API: {} ({} endpoints)", index.info.title, total);
|
||||
println!();
|
||||
|
||||
let rows: Vec<EndpointRow> = filtered
|
||||
.iter()
|
||||
.map(|ep| EndpointRow {
|
||||
method: ep.method.clone(),
|
||||
path: ep.path.clone(),
|
||||
summary: ep.summary.clone().unwrap_or_default(),
|
||||
})
|
||||
.collect();
|
||||
|
||||
let table = render_table_or_empty(&rows, "No endpoints match the given filters.");
|
||||
println!("{table}");
|
||||
|
||||
if !rows.is_empty() {
|
||||
println!();
|
||||
if filtered_count > rows.len() {
|
||||
println!(
|
||||
"Showing {} of {} (filtered from {}). Use --all to show everything.",
|
||||
rows.len(),
|
||||
filtered_count,
|
||||
total
|
||||
);
|
||||
} else {
|
||||
println!("Showing {} of {}", rows.len(), total);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tests
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::core::spec::{
|
||||
IndexInfo, IndexedEndpoint, IndexedParam, IndexedSchema, IndexedTag, SpecIndex,
|
||||
};
|
||||
|
||||
fn make_test_endpoint(
|
||||
path: &str,
|
||||
method: &str,
|
||||
summary: Option<&str>,
|
||||
tags: &[&str],
|
||||
deprecated: bool,
|
||||
) -> IndexedEndpoint {
|
||||
IndexedEndpoint {
|
||||
path: path.to_string(),
|
||||
method: method.to_string(),
|
||||
summary: summary.map(|s| s.to_string()),
|
||||
description: None,
|
||||
operation_id: Some(format!(
|
||||
"{}{}",
|
||||
method.to_lowercase(),
|
||||
path.replace('/', "_")
|
||||
)),
|
||||
tags: tags.iter().map(|t| t.to_string()).collect(),
|
||||
deprecated,
|
||||
parameters: vec![IndexedParam {
|
||||
name: "id".into(),
|
||||
location: "path".into(),
|
||||
required: true,
|
||||
description: None,
|
||||
}],
|
||||
request_body_required: method != "GET" && method != "DELETE",
|
||||
request_body_content_types: if method != "GET" && method != "DELETE" {
|
||||
vec!["application/json".into()]
|
||||
} else {
|
||||
vec![]
|
||||
},
|
||||
security_schemes: vec!["bearerAuth".into()],
|
||||
security_required: true,
|
||||
operation_ptr: format!(
|
||||
"#/paths/~1{}/{}",
|
||||
path.trim_start_matches('/'),
|
||||
method.to_lowercase()
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
fn make_test_index() -> SpecIndex {
|
||||
SpecIndex {
|
||||
index_version: 1,
|
||||
generation: 1,
|
||||
content_hash: "sha256:test".into(),
|
||||
openapi: "3.0.3".into(),
|
||||
info: IndexInfo {
|
||||
title: "Petstore API".into(),
|
||||
version: "1.0.0".into(),
|
||||
},
|
||||
endpoints: vec![
|
||||
make_test_endpoint("/pets", "GET", Some("List all pets"), &["pets"], false),
|
||||
make_test_endpoint("/pets", "POST", Some("Create a pet"), &["pets"], false),
|
||||
make_test_endpoint(
|
||||
"/pets/{petId}",
|
||||
"GET",
|
||||
Some("Get a pet by ID"),
|
||||
&["pets"],
|
||||
false,
|
||||
),
|
||||
make_test_endpoint(
|
||||
"/pets/{petId}",
|
||||
"DELETE",
|
||||
Some("Delete a pet"),
|
||||
&["pets"],
|
||||
true,
|
||||
),
|
||||
make_test_endpoint(
|
||||
"/store/inventory",
|
||||
"GET",
|
||||
Some("Get store inventory"),
|
||||
&["store"],
|
||||
false,
|
||||
),
|
||||
],
|
||||
schemas: vec![IndexedSchema {
|
||||
name: "Pet".into(),
|
||||
schema_ptr: "#/components/schemas/Pet".into(),
|
||||
}],
|
||||
tags: vec![
|
||||
IndexedTag {
|
||||
name: "pets".into(),
|
||||
description: Some("Pet operations".into()),
|
||||
endpoint_count: 4,
|
||||
},
|
||||
IndexedTag {
|
||||
name: "store".into(),
|
||||
description: Some("Store operations".into()),
|
||||
endpoint_count: 1,
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
/// Apply the same filtering logic used in execute() to the test index.
|
||||
fn filter_endpoints<'a>(
|
||||
index: &'a SpecIndex,
|
||||
method: Option<&str>,
|
||||
tag: Option<&str>,
|
||||
path_pattern: Option<&str>,
|
||||
) -> Result<Vec<&'a IndexedEndpoint>, SwaggerCliError> {
|
||||
let path_regex = match path_pattern {
|
||||
Some(p) => Some(
|
||||
Regex::new(p).map_err(|e| SwaggerCliError::Usage(format!("Invalid regex: {e}")))?,
|
||||
),
|
||||
None => None,
|
||||
};
|
||||
|
||||
let method_upper = method.map(|m| m.to_uppercase());
|
||||
let tag_lower = tag.map(|t| t.to_lowercase());
|
||||
|
||||
let results: Vec<&IndexedEndpoint> = index
|
||||
.endpoints
|
||||
.iter()
|
||||
.filter(|ep| {
|
||||
if let Some(ref m) = method_upper
|
||||
&& ep.method.to_uppercase() != *m
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if let Some(ref t) = tag_lower
|
||||
&& !ep
|
||||
.tags
|
||||
.iter()
|
||||
.any(|tag| tag.to_lowercase().contains(t.as_str()))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if let Some(ref re) = path_regex
|
||||
&& !re.is_match(&ep.path)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
true
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(results)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_filter_by_method() {
|
||||
let index = make_test_index();
|
||||
let results = filter_endpoints(&index, Some("GET"), None, None).unwrap();
|
||||
|
||||
assert_eq!(results.len(), 3);
|
||||
for ep in &results {
|
||||
assert_eq!(ep.method, "GET");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_filter_by_method_case_insensitive() {
|
||||
let index = make_test_index();
|
||||
let results = filter_endpoints(&index, Some("get"), None, None).unwrap();
|
||||
assert_eq!(results.len(), 3);
|
||||
|
||||
let results = filter_endpoints(&index, Some("Post"), None, None).unwrap();
|
||||
assert_eq!(results.len(), 1);
|
||||
assert_eq!(results[0].method, "POST");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_filter_by_tag() {
|
||||
let index = make_test_index();
|
||||
let results = filter_endpoints(&index, None, Some("store"), None).unwrap();
|
||||
|
||||
assert_eq!(results.len(), 1);
|
||||
assert_eq!(results[0].path, "/store/inventory");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_filter_by_tag_case_insensitive() {
|
||||
let index = make_test_index();
|
||||
let results = filter_endpoints(&index, None, Some("PETS"), None).unwrap();
|
||||
assert_eq!(results.len(), 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_filter_by_path_regex() {
|
||||
let index = make_test_index();
|
||||
let results = filter_endpoints(&index, None, None, Some(r"\{petId\}")).unwrap();
|
||||
|
||||
assert_eq!(results.len(), 2);
|
||||
for ep in &results {
|
||||
assert!(ep.path.contains("{petId}"));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_filter_by_path_regex_prefix() {
|
||||
let index = make_test_index();
|
||||
let results = filter_endpoints(&index, None, None, Some("^/pets")).unwrap();
|
||||
assert_eq!(results.len(), 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_regex_error() {
|
||||
let index = make_test_index();
|
||||
let result = filter_endpoints(&index, None, None, Some("[invalid"));
|
||||
|
||||
assert!(result.is_err());
|
||||
let err = result.unwrap_err();
|
||||
match err {
|
||||
SwaggerCliError::Usage(msg) => {
|
||||
assert!(msg.contains("Invalid regex"), "unexpected message: {msg}");
|
||||
}
|
||||
other => panic!("expected Usage error, got: {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_combined_filters() {
|
||||
let index = make_test_index();
|
||||
let results = filter_endpoints(&index, Some("GET"), Some("pets"), None).unwrap();
|
||||
|
||||
assert_eq!(results.len(), 2);
|
||||
for ep in &results {
|
||||
assert_eq!(ep.method, "GET");
|
||||
assert!(ep.tags.contains(&"pets".to_string()));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_no_matches() {
|
||||
let index = make_test_index();
|
||||
let results = filter_endpoints(&index, Some("PATCH"), None, None).unwrap();
|
||||
assert!(results.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sort_by_method() {
|
||||
let index = make_test_index();
|
||||
let mut endpoints: Vec<IndexedEndpoint> = index.endpoints.clone();
|
||||
|
||||
endpoints.sort_by(|a, b| {
|
||||
method_rank(&a.method)
|
||||
.cmp(&method_rank(&b.method))
|
||||
.then_with(|| a.path.cmp(&b.path))
|
||||
});
|
||||
|
||||
// All GETs come first, then POST, then DELETE
|
||||
assert_eq!(endpoints[0].method, "GET");
|
||||
assert_eq!(endpoints[1].method, "GET");
|
||||
assert_eq!(endpoints[2].method, "GET");
|
||||
assert_eq!(endpoints[3].method, "POST");
|
||||
assert_eq!(endpoints[4].method, "DELETE");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sort_by_path_default() {
|
||||
let index = make_test_index();
|
||||
let mut endpoints: Vec<IndexedEndpoint> = index.endpoints.clone();
|
||||
|
||||
endpoints.sort_by(|a, b| {
|
||||
a.path
|
||||
.cmp(&b.path)
|
||||
.then_with(|| method_rank(&a.method).cmp(&method_rank(&b.method)))
|
||||
});
|
||||
|
||||
assert_eq!(endpoints[0].path, "/pets");
|
||||
assert_eq!(endpoints[0].method, "GET");
|
||||
assert_eq!(endpoints[1].path, "/pets");
|
||||
assert_eq!(endpoints[1].method, "POST");
|
||||
assert_eq!(endpoints[2].path, "/pets/{petId}");
|
||||
assert_eq!(endpoints[2].method, "GET");
|
||||
assert_eq!(endpoints[3].path, "/pets/{petId}");
|
||||
assert_eq!(endpoints[3].method, "DELETE");
|
||||
assert_eq!(endpoints[4].path, "/store/inventory");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sort_by_tag() {
|
||||
let index = make_test_index();
|
||||
let mut endpoints: Vec<IndexedEndpoint> = index.endpoints.clone();
|
||||
|
||||
endpoints.sort_by(|a, b| {
|
||||
let tag_a = a.tags.first().map(String::as_str).unwrap_or("");
|
||||
let tag_b = b.tags.first().map(String::as_str).unwrap_or("");
|
||||
tag_a
|
||||
.cmp(tag_b)
|
||||
.then_with(|| a.path.cmp(&b.path))
|
||||
.then_with(|| method_rank(&a.method).cmp(&method_rank(&b.method)))
|
||||
});
|
||||
|
||||
// "pets" < "store" alphabetically
|
||||
assert!(endpoints[0].tags.contains(&"pets".to_string()));
|
||||
assert!(endpoints[1].tags.contains(&"pets".to_string()));
|
||||
assert!(endpoints[2].tags.contains(&"pets".to_string()));
|
||||
assert!(endpoints[3].tags.contains(&"pets".to_string()));
|
||||
assert!(endpoints[4].tags.contains(&"store".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_limit_applied() {
|
||||
let index = make_test_index();
|
||||
let mut endpoints = index.endpoints.clone();
|
||||
|
||||
let limit: usize = 2;
|
||||
endpoints.truncate(limit);
|
||||
|
||||
assert_eq!(endpoints.len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_limit_larger_than_count() {
|
||||
let index = make_test_index();
|
||||
let mut endpoints = index.endpoints.clone();
|
||||
|
||||
endpoints.truncate(100);
|
||||
assert_eq!(endpoints.len(), 5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_method_rank_ordering() {
|
||||
assert_eq!(method_rank("GET"), 0);
|
||||
assert_eq!(method_rank("POST"), 1);
|
||||
assert_eq!(method_rank("PUT"), 2);
|
||||
assert_eq!(method_rank("PATCH"), 3);
|
||||
assert_eq!(method_rank("DELETE"), 4);
|
||||
assert_eq!(method_rank("OPTIONS"), 5);
|
||||
assert_eq!(method_rank("HEAD"), 5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_method_rank_case_insensitive() {
|
||||
assert_eq!(method_rank("get"), 0);
|
||||
assert_eq!(method_rank("Post"), 1);
|
||||
assert_eq!(method_rank("delete"), 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_endpoint_entry_serialization() {
|
||||
let entry = EndpointEntry {
|
||||
path: "/pets".into(),
|
||||
method: "GET".into(),
|
||||
summary: Some("List pets".into()),
|
||||
operation_id: Some("listPets".into()),
|
||||
tags: vec!["pets".into()],
|
||||
deprecated: false,
|
||||
};
|
||||
|
||||
let json = serde_json::to_string(&entry).unwrap();
|
||||
assert!(json.contains("\"path\":\"/pets\""));
|
||||
assert!(json.contains("\"method\":\"GET\""));
|
||||
assert!(json.contains("\"deprecated\":false"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_output_serialization() {
|
||||
let output = ListOutput {
|
||||
endpoints: vec![],
|
||||
total: 5,
|
||||
filtered: 0,
|
||||
applied_filters: BTreeMap::new(),
|
||||
meta: ListMeta {
|
||||
alias: "petstore".into(),
|
||||
spec_version: "1.0.0".into(),
|
||||
cached_at: "2025-01-01T00:00:00+00:00".into(),
|
||||
duration_ms: 42,
|
||||
},
|
||||
};
|
||||
|
||||
let json = serde_json::to_string(&output).unwrap();
|
||||
assert!(json.contains("\"total\":5"));
|
||||
assert!(json.contains("\"filtered\":0"));
|
||||
assert!(json.contains("\"alias\":\"petstore\""));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_applied_filters_populated() {
|
||||
let mut filters: BTreeMap<String, String> = BTreeMap::new();
|
||||
filters.insert("method".into(), "GET".into());
|
||||
filters.insert("tag".into(), "pets".into());
|
||||
filters.insert("path".into(), "^/pets".into());
|
||||
|
||||
assert_eq!(filters.len(), 3);
|
||||
assert_eq!(filters.get("method").unwrap(), "GET");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_make_test_index_structure() {
|
||||
let index = make_test_index();
|
||||
|
||||
assert_eq!(index.info.title, "Petstore API");
|
||||
assert_eq!(index.info.version, "1.0.0");
|
||||
assert_eq!(index.endpoints.len(), 5);
|
||||
assert_eq!(index.schemas.len(), 1);
|
||||
assert_eq!(index.tags.len(), 2);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user