Skip to main content

rusticity_term/apig/
route.rs

1use crate::ui::table::Column as TableColumn;
2use crate::ui::tree::TreeItem;
3use ratatui::prelude::*;
4
5#[derive(Debug, Clone)]
6pub struct Route {
7    pub route_id: String,
8    pub route_key: String,
9    pub target: String,
10    pub authorization_type: String,
11    pub api_key_required: bool,
12    pub display_name: String,
13    pub arn: String,
14}
15
16impl TreeItem for Route {
17    fn id(&self) -> &str {
18        &self.route_key
19    }
20
21    fn display_name(&self) -> &str {
22        &self.display_name
23    }
24
25    fn is_expandable(&self) -> bool {
26        // Virtual parent nodes (empty target) are expandable
27        // They're created specifically to hold children
28        self.target.is_empty()
29    }
30
31    fn icon(&self) -> &str {
32        if self.route_key == "$default"
33            || self.route_key == "$connect"
34            || self.route_key == "$disconnect"
35        {
36            "🔌" // WebSocket special routes
37        } else {
38            "" // No icon, use tree expand/collapse indicators
39        }
40    }
41}
42
43#[derive(Debug, Clone, Copy, PartialEq)]
44pub enum Column {
45    RouteKey,
46    RouteId,
47    Arn,
48    AuthorizationType,
49    Target,
50}
51
52impl Column {
53    const ID_ROUTE_KEY: &'static str = "route_key";
54    const ID_ROUTE_ID: &'static str = "route_id";
55    const ID_ARN: &'static str = "arn";
56    const ID_AUTHORIZATION_TYPE: &'static str = "authorization_type";
57    const ID_TARGET: &'static str = "target";
58
59    pub const fn id(&self) -> &'static str {
60        match self {
61            Column::RouteKey => Self::ID_ROUTE_KEY,
62            Column::RouteId => Self::ID_ROUTE_ID,
63            Column::Arn => Self::ID_ARN,
64            Column::AuthorizationType => Self::ID_AUTHORIZATION_TYPE,
65            Column::Target => Self::ID_TARGET,
66        }
67    }
68
69    pub fn from_id(id: &str) -> Option<Self> {
70        match id {
71            Self::ID_ROUTE_KEY => Some(Column::RouteKey),
72            Self::ID_ROUTE_ID => Some(Column::RouteId),
73            Self::ID_ARN => Some(Column::Arn),
74            Self::ID_AUTHORIZATION_TYPE => Some(Column::AuthorizationType),
75            Self::ID_TARGET => Some(Column::Target),
76            _ => None,
77        }
78    }
79
80    pub const fn all() -> [Column; 5] {
81        [
82            Column::RouteKey,
83            Column::RouteId,
84            Column::Arn,
85            Column::AuthorizationType,
86            Column::Target,
87        ]
88    }
89}
90
91impl TableColumn<Route> for Column {
92    fn name(&self) -> &str {
93        match self {
94            Column::RouteKey => "Route",
95            Column::RouteId => "ID",
96            Column::Arn => "ARN",
97            Column::AuthorizationType => "Authorization",
98            Column::Target => "Integration",
99        }
100    }
101
102    fn width(&self) -> u16 {
103        match self {
104            Column::RouteKey => 30,
105            Column::RouteId => 15,
106            Column::Arn => 50,
107            Column::AuthorizationType => 15,
108            Column::Target => 30,
109        }
110    }
111
112    fn render(&self, item: &Route) -> (String, Style) {
113        let text = match self {
114            Column::RouteKey => item.route_key.clone(),
115            Column::RouteId => item.route_id.clone(),
116            Column::Arn => item.arn.clone(),
117            Column::AuthorizationType => item.authorization_type.clone(),
118            Column::Target => item.target.clone(),
119        };
120        (text, Style::default())
121    }
122}
123
124#[cfg(test)]
125mod tests {
126    use super::*;
127
128    #[test]
129    fn test_route_tree_item_id() {
130        let route = Route {
131            route_id: "abc123".to_string(),
132            route_key: "/api/users".to_string(),
133            target: "integration1".to_string(),
134            authorization_type: "NONE".to_string(),
135            api_key_required: false,
136            display_name: String::new(),
137            arn: String::new(),
138        };
139        assert_eq!(route.id(), "/api/users");
140    }
141
142    #[test]
143    fn test_route_tree_item_display_name() {
144        let route = Route {
145            route_id: "abc123".to_string(),
146            route_key: "/api/users/{id}".to_string(),
147            target: "integration1".to_string(),
148            authorization_type: "NONE".to_string(),
149            api_key_required: false,
150            display_name: "/api/users/{id}".to_string(),
151            arn: String::new(),
152        };
153        assert_eq!(route.display_name(), "/api/users/{id}");
154    }
155
156    #[test]
157    fn test_route_is_expandable() {
158        // Virtual parent (empty target) is expandable
159        let virtual_parent = Route {
160            route_id: "virtual_/v1".to_string(),
161            route_key: "/v1".to_string(),
162            target: String::new(),
163            authorization_type: String::new(),
164            api_key_required: false,
165            display_name: String::new(),
166            arn: String::new(),
167        };
168        assert!(virtual_parent.is_expandable());
169
170        // Real routes (with target) are not expandable
171        let real_route = Route {
172            route_id: "1".to_string(),
173            route_key: "/api/users".to_string(),
174            target: "int1".to_string(),
175            authorization_type: "NONE".to_string(),
176            api_key_required: false,
177            display_name: String::new(),
178            arn: String::new(),
179        };
180        assert!(!real_route.is_expandable());
181    }
182
183    #[test]
184    fn test_route_icons() {
185        let websocket_default = Route {
186            route_id: "3".to_string(),
187            route_key: "$default".to_string(),
188            target: "int3".to_string(),
189            authorization_type: "NONE".to_string(),
190            api_key_required: false,
191            display_name: String::new(),
192            arn: String::new(),
193        };
194        assert_eq!(websocket_default.icon(), "🔌");
195
196        let websocket_connect = Route {
197            route_id: "4".to_string(),
198            route_key: "$connect".to_string(),
199            target: "int4".to_string(),
200            authorization_type: "NONE".to_string(),
201            api_key_required: false,
202            display_name: String::new(),
203            arn: String::new(),
204        };
205        assert_eq!(websocket_connect.icon(), "🔌");
206
207        // Regular routes have no icon
208        let regular_route = Route {
209            route_id: "1".to_string(),
210            route_key: "/api/users".to_string(),
211            target: "int1".to_string(),
212            authorization_type: "NONE".to_string(),
213            api_key_required: false,
214            display_name: String::new(),
215            arn: String::new(),
216        };
217        assert_eq!(regular_route.icon(), "");
218    }
219
220    #[test]
221    fn test_build_route_hierarchy_flat() {
222        use crate::ui::apig::build_route_hierarchy;
223
224        let routes = vec![
225            Route {
226                route_id: "1".to_string(),
227                route_key: "/users".to_string(),
228                target: "int1".to_string(),
229                authorization_type: "NONE".to_string(),
230                api_key_required: false,
231                display_name: String::new(),
232                arn: String::new(),
233            },
234            Route {
235                route_id: "2".to_string(),
236                route_key: "/health".to_string(),
237                target: "int2".to_string(),
238                authorization_type: "NONE".to_string(),
239                api_key_required: false,
240                display_name: String::new(),
241                arn: String::new(),
242            },
243        ];
244
245        let (root, children) = build_route_hierarchy(routes);
246        assert_eq!(root.len(), 2);
247        assert!(children.is_empty());
248    }
249
250    #[test]
251    fn test_build_route_hierarchy_nested() {
252        use crate::ui::apig::build_route_hierarchy;
253
254        let routes = vec![
255            Route {
256                route_id: "1".to_string(),
257                route_key: "/api/users".to_string(),
258                target: "int1".to_string(),
259                authorization_type: "NONE".to_string(),
260                api_key_required: false,
261                display_name: String::new(),
262                arn: String::new(),
263            },
264            Route {
265                route_id: "2".to_string(),
266                route_key: "/api/users/{id}".to_string(),
267                target: "int2".to_string(),
268                authorization_type: "NONE".to_string(),
269                api_key_required: false,
270                display_name: String::new(),
271                arn: String::new(),
272            },
273        ];
274
275        let (root, children) = build_route_hierarchy(routes);
276        // Virtual /api parent created at root
277        assert_eq!(root.len(), 1);
278        assert_eq!(root[0].route_key, "/api");
279
280        // /api has /api/users as child
281        assert!(children.contains_key("/api"));
282        assert_eq!(children.get("/api").unwrap().len(), 1);
283        assert_eq!(children.get("/api").unwrap()[0].route_key, "/api/users");
284
285        // /api/users has /api/users/{id} as child
286        assert!(children.contains_key("/api/users"));
287        assert_eq!(children.get("/api/users").unwrap().len(), 1);
288    }
289
290    #[test]
291    fn test_build_route_hierarchy_websocket() {
292        use crate::ui::apig::build_route_hierarchy;
293
294        let routes = vec![
295            Route {
296                route_id: "1".to_string(),
297                route_key: "$default".to_string(),
298                target: "int1".to_string(),
299                authorization_type: "NONE".to_string(),
300                api_key_required: false,
301                display_name: String::new(),
302                arn: String::new(),
303            },
304            Route {
305                route_id: "2".to_string(),
306                route_key: "$connect".to_string(),
307                target: "int2".to_string(),
308                authorization_type: "NONE".to_string(),
309                api_key_required: false,
310                display_name: String::new(),
311                arn: String::new(),
312            },
313            Route {
314                route_id: "3".to_string(),
315                route_key: "/users".to_string(),
316                target: "int3".to_string(),
317                authorization_type: "NONE".to_string(),
318                api_key_required: false,
319                display_name: String::new(),
320                arn: String::new(),
321            },
322        ];
323
324        let (root, children) = build_route_hierarchy(routes);
325        assert_eq!(root.len(), 3);
326        assert!(children.is_empty());
327    }
328}