Skip to main content

rusticity_term/apig/
api.rs

1use crate::common::translate_column;
2use crate::common::{format_iso_timestamp, ColumnId, UTC_TIMESTAMP_WIDTH};
3use crate::ui::table::Column as TableColumn;
4use ratatui::prelude::*;
5use std::collections::HashMap;
6
7pub fn init(i18n: &mut HashMap<String, String>) {
8    for col in Column::all() {
9        i18n.entry(col.id().to_string())
10            .or_insert_with(|| col.default_name().to_string());
11    }
12}
13
14#[derive(Debug, Clone)]
15pub struct RestApi {
16    pub id: String,
17    pub name: String,
18    pub description: String,
19    pub created_date: String,
20    pub api_key_source: String,
21    pub endpoint_configuration: String,
22    pub protocol_type: String,
23    pub disable_execute_api_endpoint: bool,
24    pub status: String,
25}
26
27#[derive(Debug, Clone, Copy, PartialEq)]
28pub enum Column {
29    Name,
30    Description,
31    Id,
32    Protocol,
33    EndpointType,
34    Created,
35    SecurityPolicy,
36    ApiStatus,
37}
38
39impl Column {
40    const ID_NAME: &'static str = "column.apig.api.name";
41    const ID_DESCRIPTION: &'static str = "column.apig.api.description";
42    const ID_ID: &'static str = "column.apig.api.id";
43    const ID_PROTOCOL: &'static str = "column.apig.api.protocol";
44    const ID_ENDPOINT_TYPE: &'static str = "column.apig.api.endpoint_type";
45    const ID_CREATED: &'static str = "column.apig.api.created";
46    const ID_SECURITY_POLICY: &'static str = "column.apig.api.security_policy";
47    const ID_API_STATUS: &'static str = "column.apig.api.api_status";
48
49    pub const fn id(&self) -> &'static str {
50        match self {
51            Column::Name => Self::ID_NAME,
52            Column::Description => Self::ID_DESCRIPTION,
53            Column::Id => Self::ID_ID,
54            Column::Protocol => Self::ID_PROTOCOL,
55            Column::EndpointType => Self::ID_ENDPOINT_TYPE,
56            Column::Created => Self::ID_CREATED,
57            Column::SecurityPolicy => Self::ID_SECURITY_POLICY,
58            Column::ApiStatus => Self::ID_API_STATUS,
59        }
60    }
61
62    pub const fn default_name(&self) -> &'static str {
63        match self {
64            Column::Name => "Name",
65            Column::Description => "Description",
66            Column::Id => "ID",
67            Column::Protocol => "Protocol",
68            Column::EndpointType => "API endpoint type",
69            Column::Created => "Created",
70            Column::SecurityPolicy => "Security policy",
71            Column::ApiStatus => "API status",
72        }
73    }
74
75    pub const fn all() -> [Column; 8] {
76        [
77            Column::Name,
78            Column::Description,
79            Column::Id,
80            Column::Protocol,
81            Column::EndpointType,
82            Column::Created,
83            Column::SecurityPolicy,
84            Column::ApiStatus,
85        ]
86    }
87
88    pub fn ids() -> Vec<ColumnId> {
89        Self::all().iter().map(|c| c.id()).collect()
90    }
91
92    pub fn from_id(id: &str) -> Option<Self> {
93        match id {
94            Self::ID_NAME => Some(Column::Name),
95            Self::ID_DESCRIPTION => Some(Column::Description),
96            Self::ID_ID => Some(Column::Id),
97            Self::ID_PROTOCOL => Some(Column::Protocol),
98            Self::ID_ENDPOINT_TYPE => Some(Column::EndpointType),
99            Self::ID_CREATED => Some(Column::Created),
100            Self::ID_SECURITY_POLICY => Some(Column::SecurityPolicy),
101            Self::ID_API_STATUS => Some(Column::ApiStatus),
102            _ => None,
103        }
104    }
105
106    pub fn name(&self) -> String {
107        translate_column(self.id(), self.default_name())
108    }
109}
110
111impl TableColumn<RestApi> for Column {
112    fn name(&self) -> &str {
113        Box::leak(translate_column(self.id(), self.default_name()).into_boxed_str())
114    }
115
116    fn width(&self) -> u16 {
117        let translated = translate_column(self.id(), self.default_name());
118        translated.len().max(match self {
119            Column::Name => 30,
120            Column::Description => 40,
121            Column::Id => 15,
122            Column::Protocol => 10,
123            Column::EndpointType => 20,
124            Column::Created => UTC_TIMESTAMP_WIDTH as usize,
125            Column::SecurityPolicy => 18,
126            Column::ApiStatus => 15,
127        }) as u16
128    }
129
130    fn render(&self, item: &RestApi) -> (String, Style) {
131        match self {
132            Column::Name => (item.name.clone(), Style::default()),
133            Column::Description => (item.description.clone(), Style::default()),
134            Column::Id => (item.id.clone(), Style::default()),
135            Column::Protocol => {
136                // Capitalize protocol: Http -> HTTP, Websocket -> WEBSOCKET
137                let protocol = match item.protocol_type.to_uppercase().as_str() {
138                    "HTTP" => "HTTP",
139                    "WEBSOCKET" => "WEBSOCKET",
140                    "REST" => "REST",
141                    _ => &item.protocol_type,
142                };
143                (protocol.to_string(), Style::default())
144            }
145            Column::EndpointType => {
146                // Format endpoint type: REGIONAL -> Regional, EDGE -> Edge-optimized
147                let endpoint = match item.endpoint_configuration.to_uppercase().as_str() {
148                    "REGIONAL" => "Regional",
149                    "EDGE" => "Edge-optimized",
150                    "PRIVATE" => "Private",
151                    _ => &item.endpoint_configuration,
152                };
153                (endpoint.to_string(), Style::default())
154            }
155            Column::Created => (format_iso_timestamp(&item.created_date), Style::default()),
156            Column::SecurityPolicy => {
157                // Security policy only applies to REST APIs with custom domains
158                // Regional APIs don't have a security policy in the API itself
159                let policy = if item.protocol_type.to_uppercase() == "REST" {
160                    "-" // REST APIs show security policy on custom domain, not API
161                } else {
162                    "TLS 1.2"
163                };
164                (policy.to_string(), Style::default())
165            }
166            Column::ApiStatus => {
167                // Show Available with green checkmark emoji
168                if item.disable_execute_api_endpoint {
169                    ("Disabled".to_string(), Style::default())
170                } else {
171                    (
172                        "✅ Available".to_string(),
173                        Style::default().fg(Color::Green),
174                    )
175                }
176            }
177        }
178    }
179}
180
181#[cfg(test)]
182mod tests {
183    use super::*;
184
185    #[test]
186    fn test_column_ids_have_correct_prefix() {
187        for col in Column::all() {
188            assert!(
189                col.id().starts_with("column.apig.api."),
190                "Column ID '{}' should start with 'column.apig.api.'",
191                col.id()
192            );
193        }
194    }
195
196    #[test]
197    fn test_all_columns_count() {
198        assert_eq!(Column::all().len(), 8);
199    }
200
201    #[test]
202    fn test_column_from_id() {
203        assert_eq!(Column::from_id("column.apig.api.name"), Some(Column::Name));
204    }
205
206    #[test]
207    fn test_protocol_capitalization() {
208        use crate::ui::table::Column as TableColumn;
209
210        let api = RestApi {
211            id: "test".to_string(),
212            name: "Test".to_string(),
213            description: "".to_string(),
214            created_date: "".to_string(),
215            api_key_source: "".to_string(),
216            endpoint_configuration: "".to_string(),
217            protocol_type: "Http".to_string(),
218            disable_execute_api_endpoint: false,
219            status: "".to_string(),
220        };
221
222        let (text, _) = Column::Protocol.render(&api);
223        assert_eq!(text, "HTTP");
224    }
225
226    #[test]
227    fn test_endpoint_type_formatting() {
228        use crate::ui::table::Column as TableColumn;
229
230        let mut api = RestApi {
231            id: "test".to_string(),
232            name: "Test".to_string(),
233            description: "".to_string(),
234            created_date: "".to_string(),
235            api_key_source: "".to_string(),
236            endpoint_configuration: "REGIONAL".to_string(),
237            protocol_type: "HTTP".to_string(),
238            disable_execute_api_endpoint: false,
239            status: "".to_string(),
240        };
241
242        let (text, _) = Column::EndpointType.render(&api);
243        assert_eq!(text, "Regional");
244
245        api.endpoint_configuration = "EDGE".to_string();
246        let (text, _) = Column::EndpointType.render(&api);
247        assert_eq!(text, "Edge-optimized");
248    }
249
250    #[test]
251    fn test_api_status_available_green() {
252        use crate::ui::table::Column as TableColumn;
253
254        let api = RestApi {
255            id: "test".to_string(),
256            name: "Test".to_string(),
257            description: "".to_string(),
258            created_date: "".to_string(),
259            api_key_source: "".to_string(),
260            endpoint_configuration: "".to_string(),
261            protocol_type: "HTTP".to_string(),
262            disable_execute_api_endpoint: false,
263            status: "".to_string(),
264        };
265
266        let (text, style) = Column::ApiStatus.render(&api);
267        assert_eq!(text, "✅ Available");
268        assert_eq!(style.fg, Some(Color::Green));
269    }
270
271    #[test]
272    fn test_api_status_disabled() {
273        use crate::ui::table::Column as TableColumn;
274
275        let api = RestApi {
276            id: "test".to_string(),
277            name: "Test".to_string(),
278            description: "".to_string(),
279            created_date: "".to_string(),
280            api_key_source: "".to_string(),
281            endpoint_configuration: "".to_string(),
282            protocol_type: "HTTP".to_string(),
283            disable_execute_api_endpoint: true,
284            status: "".to_string(),
285        };
286
287        let (text, _) = Column::ApiStatus.render(&api);
288        assert_eq!(text, "Disabled");
289    }
290
291    #[test]
292    fn test_security_policy_rest_api() {
293        use crate::ui::table::Column as TableColumn;
294
295        let api = RestApi {
296            id: "test".to_string(),
297            name: "Test".to_string(),
298            description: "".to_string(),
299            created_date: "".to_string(),
300            api_key_source: "".to_string(),
301            endpoint_configuration: "".to_string(),
302            protocol_type: "REST".to_string(),
303            disable_execute_api_endpoint: false,
304            status: "".to_string(),
305        };
306
307        let (text, _) = Column::SecurityPolicy.render(&api);
308        assert_eq!(text, "-");
309    }
310
311    #[test]
312    fn test_security_policy_http_api() {
313        use crate::ui::table::Column as TableColumn;
314
315        let api = RestApi {
316            id: "test".to_string(),
317            name: "Test".to_string(),
318            description: "".to_string(),
319            created_date: "".to_string(),
320            api_key_source: "".to_string(),
321            endpoint_configuration: "".to_string(),
322            protocol_type: "HTTP".to_string(),
323            disable_execute_api_endpoint: false,
324            status: "".to_string(),
325        };
326
327        let (text, _) = Column::SecurityPolicy.render(&api);
328        assert_eq!(text, "TLS 1.2");
329    }
330}