Skip to main content

esi_openapi/
spec.rs

1//! Struct types for the ESI OpenAPI specification data.
2//!
3//! Only the parts of the specification needed to resolve an
4//! `operationId` to a URL path are modeled; every other key in
5//! the document is ignored during deserialization.
6
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9
10/// ESI OpenAPI spec type.
11#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)]
12pub struct Spec {
13    /// Map of URL path (e.g. `/markets/{region_id}/orders`) to its path item.
14    pub paths: HashMap<String, SpecPathItem>,
15}
16
17/// An OpenAPI path item: the operations available on a single URL path.
18///
19/// Path-level keys other than the HTTP methods (such as `parameters`
20/// or `summary`) are ignored.
21#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq, Eq)]
22pub struct SpecPathItem {
23    /// `GET` operation, if any.
24    #[serde(default, skip_serializing_if = "Option::is_none")]
25    pub get: Option<SpecPathMethod>,
26    /// `POST` operation, if any.
27    #[serde(default, skip_serializing_if = "Option::is_none")]
28    pub post: Option<SpecPathMethod>,
29    /// `PUT` operation, if any.
30    #[serde(default, skip_serializing_if = "Option::is_none")]
31    pub put: Option<SpecPathMethod>,
32    /// `DELETE` operation, if any.
33    #[serde(default, skip_serializing_if = "Option::is_none")]
34    pub delete: Option<SpecPathMethod>,
35    /// `PATCH` operation, if any.
36    #[serde(default, skip_serializing_if = "Option::is_none")]
37    pub patch: Option<SpecPathMethod>,
38}
39
40impl SpecPathItem {
41    /// Iterate over the operations defined on this path.
42    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/// A single OpenAPI operation.
50#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq, Eq)]
51pub struct SpecPathMethod {
52    /// The operation ID to use this endpoint, e.g. `GetMarketsRegionIdOrders`.
53    #[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    /// Build a map of `operationId` to URL path, with the path's leading
63    /// slash removed so it can be appended to the base API URL.
64    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}