1use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9
10#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)]
12pub struct Spec {
13 pub paths: HashMap<String, SpecPathItem>,
15}
16
17#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq, Eq)]
22pub struct SpecPathItem {
23 #[serde(default, skip_serializing_if = "Option::is_none")]
25 pub get: Option<SpecPathMethod>,
26 #[serde(default, skip_serializing_if = "Option::is_none")]
28 pub post: Option<SpecPathMethod>,
29 #[serde(default, skip_serializing_if = "Option::is_none")]
31 pub put: Option<SpecPathMethod>,
32 #[serde(default, skip_serializing_if = "Option::is_none")]
34 pub delete: Option<SpecPathMethod>,
35 #[serde(default, skip_serializing_if = "Option::is_none")]
37 pub patch: Option<SpecPathMethod>,
38}
39
40impl SpecPathItem {
41 pub fn methods(&self) -> impl Iterator<Item = &SpecPathMethod> {
43 [&self.get, &self.post, &self.put, &self.delete, &self.patch]
44 .into_iter()
45 .flatten()
46 }
47}
48
49#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq, Eq)]
51pub struct SpecPathMethod {
52 #[serde(
54 rename = "operationId",
55 default,
56 skip_serializing_if = "Option::is_none"
57 )]
58 pub operation_id: Option<String>,
59}
60
61impl Spec {
62 pub fn operation_index(&self) -> HashMap<String, String> {
65 let mut index = HashMap::new();
66 for (path, item) in &self.paths {
67 let path = path.strip_prefix('/').unwrap_or(path);
68 for op_id in item.methods().filter_map(|m| m.operation_id.as_ref()) {
69 index.insert(op_id.clone(), path.to_owned());
70 }
71 }
72 index
73 }
74}
75
76#[cfg(test)]
77mod tests {
78 use super::Spec;
79
80 const FIXTURE: &str = include_str!("../resources/test/openapi.json");
81
82 #[test]
83 fn test_parse_openapi_fixture() {
84 let spec: Spec = serde_json::from_str(FIXTURE).expect("fixture should parse");
85 let index = spec.operation_index();
86 assert!(index.len() > 200, "only {} operations", index.len());
87 assert_eq!(
88 index.get("GetMarketsRegionIdOrders").map(String::as_str),
89 Some("markets/{region_id}/orders")
90 );
91 assert_eq!(
92 index.get("GetCharactersDetail").map(String::as_str),
93 Some("characters/{character_id}")
94 );
95 }
96
97 #[test]
98 fn test_parse_ignores_unknown_keys() {
99 let source = r#"{
100 "openapi": "3.1.0",
101 "paths": {
102 "/status": {
103 "parameters": [{"name": "x", "in": "header"}],
104 "summary": "Server status",
105 "get": {"operationId": "GetStatus", "tags": ["Status"]}
106 },
107 "/no-op-id": {"get": {"summary": "missing id"}}
108 }
109 }"#;
110 let spec: Spec = serde_json::from_str(source).unwrap();
111 let index = spec.operation_index();
112 assert_eq!(index.len(), 1);
113 assert_eq!(index["GetStatus"], "status");
114 }
115}