Skip to main content

rusticity_term/lambda/
mod.rs

1pub mod applications;
2pub mod functions;
3
4use crate::common::{format_bytes, translate_column, ColumnId, UTC_TIMESTAMP_WIDTH};
5use crate::ui::lambda::{ApplicationDetailTab, DetailTab};
6use crate::ui::table;
7use ratatui::prelude::*;
8use std::collections::HashMap;
9
10pub fn parse_layer_arn(arn: &str) -> (String, String) {
11    let parts: Vec<&str> = arn.split(':').collect();
12    let name = parts.get(6).unwrap_or(&"").to_string();
13    let version = parts.get(7).unwrap_or(&"").to_string();
14    (name, version)
15}
16
17pub fn init(i18n: &mut HashMap<String, String>) {
18    for col in FunctionColumn::all() {
19        i18n.entry(col.id().to_string())
20            .or_insert_with(|| col.default_name().to_string());
21    }
22    for col in ApplicationColumn::all() {
23        i18n.entry(col.id().to_string())
24            .or_insert_with(|| col.default_name().to_string());
25    }
26    for col in DeploymentColumn::all() {
27        i18n.entry(col.id().to_string())
28            .or_insert_with(|| col.default_name().to_string());
29    }
30    for col in ResourceColumn::all() {
31        i18n.entry(col.id().to_string())
32            .or_insert_with(|| col.default_name().to_string());
33    }
34}
35
36pub fn format_runtime(runtime: &str) -> String {
37    let lower = runtime.to_lowercase();
38
39    if let Some(rest) = lower.strip_prefix("python") {
40        let version = if rest.contains('.') {
41            rest.to_string()
42        } else if rest.len() >= 2 {
43            format!("{}.{}", &rest[0..1], &rest[1..])
44        } else {
45            rest.to_string()
46        };
47        format!("Python {}", version)
48    } else if let Some(rest) = lower.strip_prefix("nodejs") {
49        let formatted = rest.replace("x", ".x");
50        format!("Node.js {}", formatted)
51    } else if let Some(rest) = lower.strip_prefix("java") {
52        format!("Java {}", rest)
53    } else if let Some(rest) = lower.strip_prefix("dotnet") {
54        format!(".NET {}", rest)
55    } else if let Some(rest) = lower.strip_prefix("go") {
56        format!("Go {}", rest)
57    } else if let Some(rest) = lower.strip_prefix("ruby") {
58        format!("Ruby {}", rest)
59    } else {
60        runtime.to_string()
61    }
62}
63
64pub fn format_architecture(arch: &str) -> String {
65    match arch {
66        "X86_64" => "x86-64".to_string(),
67        "X8664" => "x86-64".to_string(),
68        "Arm64" => "arm64".to_string(),
69        _ => arch.replace("X86", "x86").replace("Arm", "arm"),
70    }
71}
72
73#[cfg(test)]
74mod tests {
75    use super::*;
76    use crate::common::InputFocus;
77    use crate::ui::lambda::FILTER_CONTROLS;
78    use crate::ui::table::Column as TableColumn;
79
80    #[test]
81    fn test_format_runtime() {
82        // AWS SDK format (e.g., Python39, Nodejs20x)
83        assert_eq!(format_runtime("Python39"), "Python 3.9");
84        assert_eq!(format_runtime("Python312"), "Python 3.12");
85        assert_eq!(format_runtime("Nodejs20x"), "Node.js 20.x");
86
87        // Lowercase format
88        assert_eq!(format_runtime("python3.12"), "Python 3.12");
89        assert_eq!(format_runtime("python3.11"), "Python 3.11");
90        assert_eq!(format_runtime("nodejs20x"), "Node.js 20.x");
91        assert_eq!(format_runtime("nodejs18x"), "Node.js 18.x");
92        assert_eq!(format_runtime("java21"), "Java 21");
93        assert_eq!(format_runtime("dotnet8"), ".NET 8");
94        assert_eq!(format_runtime("go1.x"), "Go 1.x");
95        assert_eq!(format_runtime("ruby3.3"), "Ruby 3.3");
96        assert_eq!(format_runtime("unknown"), "unknown");
97    }
98
99    #[test]
100    fn test_format_architecture() {
101        assert_eq!(format_architecture("X8664"), "x86-64");
102        assert_eq!(format_architecture("X86_64"), "x86-64");
103        assert_eq!(format_architecture("Arm64"), "arm64");
104        assert_eq!(format_architecture("arm64"), "arm64");
105    }
106
107    #[test]
108    fn test_runtime_formatter_in_table_column() {
109        let func = Function {
110            name: "test-func".to_string(),
111            arn: "arn".to_string(),
112            application: None,
113            description: "desc".to_string(),
114            package_type: "Zip".to_string(),
115            runtime: "python3.12".to_string(),
116            architecture: "X86_64".to_string(),
117            code_size: 1024,
118            code_sha256: "hash".to_string(),
119            memory_mb: 128,
120            timeout_seconds: 30,
121            last_modified: "2024-01-01".to_string(),
122            layers: vec![],
123        };
124
125        let runtime_col = FunctionColumn::Runtime;
126        let (text, _) = runtime_col.render(&func);
127        assert_eq!(text, "Python 3.12");
128    }
129
130    #[test]
131    fn test_architecture_formatter_in_table_column() {
132        let func = Function {
133            name: "test-func".to_string(),
134            arn: "arn".to_string(),
135            application: None,
136            description: "desc".to_string(),
137            package_type: "Zip".to_string(),
138            runtime: "python3.12".to_string(),
139            architecture: "X86_64".to_string(),
140            code_size: 1024,
141            code_sha256: "hash".to_string(),
142            memory_mb: 128,
143            timeout_seconds: 30,
144            last_modified: "2024-01-01".to_string(),
145            layers: vec![],
146        };
147
148        let arch_col = FunctionColumn::Architecture;
149        let (text, _) = arch_col.render(&func);
150        assert_eq!(text, "x86-64");
151    }
152
153    #[test]
154    fn test_nodejs_runtime_formatting() {
155        assert_eq!(format_runtime("nodejs16x"), "Node.js 16.x");
156        assert_eq!(format_runtime("nodejs18x"), "Node.js 18.x");
157        assert_eq!(format_runtime("nodejs20x"), "Node.js 20.x");
158    }
159
160    #[test]
161    fn test_python_runtime_formatting() {
162        // AWS SDK format
163        assert_eq!(format_runtime("Python38"), "Python 3.8");
164        assert_eq!(format_runtime("Python39"), "Python 3.9");
165        assert_eq!(format_runtime("Python310"), "Python 3.10");
166        assert_eq!(format_runtime("Python311"), "Python 3.11");
167        assert_eq!(format_runtime("Python312"), "Python 3.12");
168
169        // Lowercase with dots
170        assert_eq!(format_runtime("python3.8"), "Python 3.8");
171        assert_eq!(format_runtime("python3.9"), "Python 3.9");
172        assert_eq!(format_runtime("python3.10"), "Python 3.10");
173        assert_eq!(format_runtime("python3.11"), "Python 3.11");
174        assert_eq!(format_runtime("python3.12"), "Python 3.12");
175    }
176
177    #[test]
178    fn test_timeout_formatting() {
179        // Test timeout conversion to min/sec format
180        assert_eq!(300 / 60, 5); // 300 seconds = 5 minutes
181        assert_eq!(300 % 60, 0); // 0 seconds remainder
182        assert_eq!(900 / 60, 15); // 900 seconds = 15 minutes
183        assert_eq!(900 % 60, 0); // 0 seconds remainder
184        assert_eq!(330 / 60, 5); // 330 seconds = 5 minutes
185        assert_eq!(330 % 60, 30); // 30 seconds remainder
186    }
187
188    #[test]
189    fn test_version_column_architecture_formatter() {
190        let version = Version {
191            version: "1".to_string(),
192            aliases: "prod".to_string(),
193            description: "Production version".to_string(),
194            last_modified: "2024-01-01".to_string(),
195            architecture: "X86_64".to_string(),
196        };
197
198        let arch_col = VersionColumn::Architecture.to_column();
199        let (text, _) = arch_col.render(&version);
200        assert_eq!(text, "x86-64");
201    }
202
203    #[test]
204    fn test_version_architecture_formatter_arm() {
205        let version = Version {
206            version: "1".to_string(),
207            aliases: "".to_string(),
208            description: "".to_string(),
209            last_modified: "".to_string(),
210            architecture: "Arm64".to_string(),
211        };
212
213        let arch_col = VersionColumn::Architecture.to_column();
214        let (text, _) = arch_col.render(&version);
215        assert_eq!(text, "arm64");
216    }
217
218    #[test]
219    fn test_version_column_all() {
220        let all = VersionColumn::all();
221        assert_eq!(all.len(), 5);
222        assert!(all.contains(&VersionColumn::Version));
223        assert!(all.contains(&VersionColumn::Aliases));
224        assert!(all.contains(&VersionColumn::Description));
225        assert!(all.contains(&VersionColumn::LastModified));
226        assert!(all.contains(&VersionColumn::Architecture));
227    }
228
229    #[test]
230    fn test_version_column_names() {
231        assert_eq!(VersionColumn::Version.name(), "Version");
232        assert_eq!(VersionColumn::Aliases.name(), "Aliases");
233        assert_eq!(VersionColumn::Description.name(), "Description");
234        assert_eq!(VersionColumn::LastModified.name(), "Last modified");
235        assert_eq!(VersionColumn::Architecture.name(), "Architecture");
236    }
237
238    #[test]
239    fn test_versions_table_sort_config() {
240        // Versions table shows "Version ↓" header (DESC sort)
241        // This is configured in render_detail with:
242        // sort_column: "Version", sort_direction: "DESC"
243        let sort_column = "Version";
244        let sort_direction = "DESC";
245        assert_eq!(sort_column, "Version");
246        assert_eq!(sort_direction, "DESC");
247    }
248
249    #[test]
250    fn test_input_focus_cycling() {
251        let focus = InputFocus::Filter;
252        assert_eq!(focus.next(&FILTER_CONTROLS), InputFocus::Pagination);
253
254        let focus = InputFocus::Pagination;
255        assert_eq!(focus.next(&FILTER_CONTROLS), InputFocus::Filter);
256
257        let focus = InputFocus::Filter;
258        assert_eq!(focus.prev(&FILTER_CONTROLS), InputFocus::Pagination);
259
260        let focus = InputFocus::Pagination;
261        assert_eq!(focus.prev(&FILTER_CONTROLS), InputFocus::Filter);
262    }
263
264    #[test]
265    #[allow(clippy::useless_vec)]
266    fn test_version_sorting_desc() {
267        let mut versions = vec![
268            Version {
269                version: "1".to_string(),
270                aliases: "".to_string(),
271                description: "".to_string(),
272                last_modified: "".to_string(),
273                architecture: "".to_string(),
274            },
275            Version {
276                version: "10".to_string(),
277                aliases: "".to_string(),
278                description: "".to_string(),
279                last_modified: "".to_string(),
280                architecture: "".to_string(),
281            },
282            Version {
283                version: "2".to_string(),
284                aliases: "".to_string(),
285                description: "".to_string(),
286                last_modified: "".to_string(),
287                architecture: "".to_string(),
288            },
289        ];
290
291        // Sort DESC by version number
292        versions.sort_by(|a, b| {
293            let a_num = a.version.parse::<i32>().unwrap_or(0);
294            let b_num = b.version.parse::<i32>().unwrap_or(0);
295            b_num.cmp(&a_num)
296        });
297
298        assert_eq!(versions[0].version, "10");
299        assert_eq!(versions[1].version, "2");
300        assert_eq!(versions[2].version, "1");
301    }
302
303    #[test]
304    fn test_version_sorting_with_36_versions() {
305        let mut versions: Vec<Version> = (1..=36)
306            .map(|i| Version {
307                version: i.to_string(),
308                aliases: "".to_string(),
309                description: "".to_string(),
310                last_modified: "".to_string(),
311                architecture: "".to_string(),
312            })
313            .collect();
314
315        // Sort DESC by version number
316        versions.sort_by(|a, b| {
317            let a_num = a.version.parse::<i32>().unwrap_or(0);
318            let b_num = b.version.parse::<i32>().unwrap_or(0);
319            b_num.cmp(&a_num)
320        });
321
322        // Verify DESC order
323        assert_eq!(versions[0].version, "36");
324        assert_eq!(versions[1].version, "35");
325        assert_eq!(versions[35].version, "1");
326        assert_eq!(versions.len(), 36);
327    }
328
329    #[test]
330    fn test_column_id() {
331        assert_eq!(FunctionColumn::Name.id(), "column.lambda.function.name");
332        assert_eq!(
333            FunctionColumn::Description.id(),
334            "column.lambda.function.description"
335        );
336        assert_eq!(
337            FunctionColumn::PackageType.id(),
338            "column.lambda.function.package_type"
339        );
340        assert_eq!(
341            FunctionColumn::Runtime.id(),
342            "column.lambda.function.runtime"
343        );
344        assert_eq!(
345            FunctionColumn::Architecture.id(),
346            "column.lambda.function.architecture"
347        );
348        assert_eq!(
349            FunctionColumn::CodeSize.id(),
350            "column.lambda.function.code_size"
351        );
352        assert_eq!(
353            FunctionColumn::MemoryMb.id(),
354            "column.lambda.function.memory_mb"
355        );
356        assert_eq!(
357            FunctionColumn::TimeoutSeconds.id(),
358            "column.lambda.function.timeout_seconds"
359        );
360        assert_eq!(
361            FunctionColumn::LastModified.id(),
362            "column.lambda.function.last_modified"
363        );
364    }
365
366    #[test]
367    fn test_column_from_id() {
368        assert_eq!(
369            FunctionColumn::from_id("column.lambda.function.name"),
370            Some(FunctionColumn::Name)
371        );
372        assert_eq!(
373            FunctionColumn::from_id("column.lambda.function.runtime"),
374            Some(FunctionColumn::Runtime)
375        );
376        assert_eq!(FunctionColumn::from_id("invalid"), None);
377    }
378
379    #[test]
380    fn test_column_all_returns_ids() {
381        let all = FunctionColumn::ids();
382        assert_eq!(all.len(), 9);
383        assert!(all.contains(&"column.lambda.function.name"));
384        assert!(all.contains(&"column.lambda.function.runtime"));
385        assert!(all.contains(&"column.lambda.function.code_size"));
386    }
387
388    #[test]
389    fn test_column_visible_returns_ids() {
390        let visible = FunctionColumn::visible();
391        assert_eq!(visible.len(), 6);
392        assert!(visible.contains(&"column.lambda.function.name"));
393        assert!(visible.contains(&"column.lambda.function.runtime"));
394        assert!(!visible.contains(&"column.lambda.function.description"));
395    }
396
397    #[test]
398    fn test_application_column_id() {
399        assert_eq!(
400            ApplicationColumn::Name.id(),
401            "column.lambda.application.name"
402        );
403        assert_eq!(
404            ApplicationColumn::Description.id(),
405            "column.lambda.application.description"
406        );
407        assert_eq!(
408            ApplicationColumn::Status.id(),
409            "column.lambda.application.status"
410        );
411        assert_eq!(
412            ApplicationColumn::LastModified.id(),
413            "column.lambda.application.last_modified"
414        );
415    }
416
417    #[test]
418    fn test_application_column_from_id() {
419        assert_eq!(
420            ApplicationColumn::from_id("column.lambda.application.name"),
421            Some(ApplicationColumn::Name)
422        );
423        assert_eq!(
424            ApplicationColumn::from_id("column.lambda.application.status"),
425            Some(ApplicationColumn::Status)
426        );
427        assert_eq!(ApplicationColumn::from_id("invalid"), None);
428    }
429
430    #[test]
431    fn test_i18n_initialization() {
432        let mut i18n = std::collections::HashMap::new();
433        init(&mut i18n);
434        // Just verify that translation lookup works, don't assert specific values
435        // since user may have custom column names in config.toml
436        let name = crate::common::t("column.lambda.function.name");
437        assert!(!name.is_empty());
438        let nonexistent = crate::common::t("nonexistent.key");
439        assert_eq!(nonexistent, "nonexistent.key");
440    }
441
442    #[test]
443    fn test_column_width_uses_i18n() {
444        let mut i18n = std::collections::HashMap::new();
445        init(&mut i18n);
446        let col = FunctionColumn::Name;
447        let width = col.width();
448        assert!(width >= "Function name".len() as u16);
449    }
450}
451
452pub fn console_url_functions(region: &str) -> String {
453    format!(
454        "https://{}.console.aws.amazon.com/lambda/home?region={}#/functions",
455        region, region
456    )
457}
458
459pub fn console_url_function_detail(region: &str, function_name: &str) -> String {
460    format!(
461        "https://{}.console.aws.amazon.com/lambda/home?region={}#/functions/{}",
462        region, region, function_name
463    )
464}
465
466pub fn console_url_function_version(
467    region: &str,
468    function_name: &str,
469    version: &str,
470    detail_tab: &DetailTab,
471) -> String {
472    let tab = match detail_tab {
473        DetailTab::Code => "code",
474        DetailTab::Configuration => "configure",
475        _ => "code",
476    };
477    format!(
478        "https://{}.console.aws.amazon.com/lambda/home?region={}#/functions/{}/versions/{}?tab={}",
479        region, region, function_name, version, tab
480    )
481}
482
483pub fn console_url_applications(region: &str) -> String {
484    format!(
485        "https://{}.console.aws.amazon.com/lambda/home?region={}#/applications",
486        region, region
487    )
488}
489
490pub fn console_url_application_detail(
491    region: &str,
492    app_name: &str,
493    tab: &ApplicationDetailTab,
494) -> String {
495    let tab_param = match tab {
496        ApplicationDetailTab::Overview => "overview",
497        ApplicationDetailTab::Deployments => "deployments",
498    };
499    format!(
500        "https://{}.console.aws.amazon.com/lambda/home?region={}#/applications/{}?tab={}",
501        region, region, app_name, tab_param
502    )
503}
504
505#[derive(Debug, Clone)]
506pub struct Function {
507    pub name: String,
508    pub arn: String,
509    pub application: Option<String>,
510    pub description: String,
511    pub package_type: String,
512    pub runtime: String,
513    pub architecture: String,
514    pub code_size: i64,
515    pub code_sha256: String,
516    pub memory_mb: i32,
517    pub timeout_seconds: i32,
518    pub last_modified: String,
519    pub layers: Vec<Layer>,
520}
521
522#[derive(Debug, Clone)]
523pub struct Layer {
524    pub merge_order: String,
525    pub name: String,
526    pub layer_version: String,
527    pub compatible_runtimes: String,
528    pub compatible_architectures: String,
529    pub version_arn: String,
530}
531
532#[derive(Debug, Clone)]
533pub struct Version {
534    pub version: String,
535    pub aliases: String,
536    pub description: String,
537    pub last_modified: String,
538    pub architecture: String,
539}
540
541#[derive(Debug, Clone)]
542pub struct Alias {
543    pub name: String,
544    pub versions: String,
545    pub description: String,
546}
547
548#[derive(Debug, Clone)]
549pub struct Application {
550    pub name: String,
551    pub arn: String,
552    pub description: String,
553    pub status: String,
554    pub last_modified: String,
555}
556
557#[derive(Debug, Clone, Copy, PartialEq)]
558pub enum FunctionColumn {
559    Name,
560    Description,
561    PackageType,
562    Runtime,
563    Architecture,
564    CodeSize,
565    MemoryMb,
566    TimeoutSeconds,
567    LastModified,
568}
569
570impl FunctionColumn {
571    pub fn id(&self) -> ColumnId {
572        match self {
573            Self::Name => "column.lambda.function.name",
574            Self::Description => "column.lambda.function.description",
575            Self::PackageType => "column.lambda.function.package_type",
576            Self::Runtime => "column.lambda.function.runtime",
577            Self::Architecture => "column.lambda.function.architecture",
578            Self::CodeSize => "column.lambda.function.code_size",
579            Self::MemoryMb => "column.lambda.function.memory_mb",
580            Self::TimeoutSeconds => "column.lambda.function.timeout_seconds",
581            Self::LastModified => "column.lambda.function.last_modified",
582        }
583    }
584
585    pub fn default_name(&self) -> &'static str {
586        match self {
587            Self::Name => "Function name",
588            Self::Description => "Description",
589            Self::PackageType => "Package type",
590            Self::Runtime => "Runtime",
591            Self::Architecture => "Architecture",
592            Self::CodeSize => "Code size",
593            Self::MemoryMb => "Memory (MB)",
594            Self::TimeoutSeconds => "Timeout (s)",
595            Self::LastModified => "Last modified",
596        }
597    }
598
599    pub fn all() -> [Self; 9] {
600        [
601            Self::Name,
602            Self::Description,
603            Self::PackageType,
604            Self::Runtime,
605            Self::Architecture,
606            Self::CodeSize,
607            Self::MemoryMb,
608            Self::TimeoutSeconds,
609            Self::LastModified,
610        ]
611    }
612
613    pub fn ids() -> Vec<ColumnId> {
614        Self::all().iter().map(|c| c.id()).collect()
615    }
616
617    pub fn visible() -> Vec<ColumnId> {
618        [
619            Self::Name,
620            Self::Runtime,
621            Self::CodeSize,
622            Self::MemoryMb,
623            Self::TimeoutSeconds,
624            Self::LastModified,
625        ]
626        .iter()
627        .map(|c| c.id())
628        .collect()
629    }
630
631    pub fn from_id(id: ColumnId) -> Option<Self> {
632        match id {
633            "column.lambda.function.name" => Some(Self::Name),
634            "column.lambda.function.description" => Some(Self::Description),
635            "column.lambda.function.package_type" => Some(Self::PackageType),
636            "column.lambda.function.runtime" => Some(Self::Runtime),
637            "column.lambda.function.architecture" => Some(Self::Architecture),
638            "column.lambda.function.code_size" => Some(Self::CodeSize),
639            "column.lambda.function.memory_mb" => Some(Self::MemoryMb),
640            "column.lambda.function.timeout_seconds" => Some(Self::TimeoutSeconds),
641            "column.lambda.function.last_modified" => Some(Self::LastModified),
642            _ => None,
643        }
644    }
645}
646
647impl table::Column<Function> for FunctionColumn {
648    fn id(&self) -> &'static str {
649        Self::id(self)
650    }
651
652    fn default_name(&self) -> &'static str {
653        Self::default_name(self)
654    }
655
656    fn width(&self) -> u16 {
657        let translated = translate_column(self.id(), self.default_name());
658        translated.len().max(match self {
659            Self::Name => 30,
660            Self::Description => 40,
661            Self::Runtime => 20,
662            Self::LastModified => UTC_TIMESTAMP_WIDTH as usize,
663            _ => 0,
664        }) as u16
665    }
666
667    fn render(&self, item: &Function) -> (String, Style) {
668        let text = match self {
669            Self::Name => item.name.clone(),
670            Self::Description => item.description.clone(),
671            Self::PackageType => item.package_type.clone(),
672            Self::Runtime => format_runtime(&item.runtime),
673            Self::Architecture => format_architecture(&item.architecture),
674            Self::CodeSize => format_bytes(item.code_size),
675            Self::MemoryMb => item.memory_mb.to_string(),
676            Self::TimeoutSeconds => item.timeout_seconds.to_string(),
677            Self::LastModified => item.last_modified.clone(),
678        };
679        (text, Style::default())
680    }
681}
682
683#[derive(Debug, Clone, Copy, PartialEq)]
684pub enum ApplicationColumn {
685    Name,
686    Description,
687    Status,
688    LastModified,
689}
690
691impl ApplicationColumn {
692    pub fn id(&self) -> ColumnId {
693        match self {
694            Self::Name => "column.lambda.application.name",
695            Self::Description => "column.lambda.application.description",
696            Self::Status => "column.lambda.application.status",
697            Self::LastModified => "column.lambda.application.last_modified",
698        }
699    }
700
701    pub fn all() -> [Self; 4] {
702        [
703            Self::Name,
704            Self::Description,
705            Self::Status,
706            Self::LastModified,
707        ]
708    }
709
710    pub fn ids() -> Vec<ColumnId> {
711        Self::all().iter().map(|c| c.id()).collect()
712    }
713
714    pub fn visible() -> Vec<ColumnId> {
715        Self::ids()
716    }
717
718    pub fn from_id(id: ColumnId) -> Option<Self> {
719        match id {
720            "column.lambda.application.name" => Some(Self::Name),
721            "column.lambda.application.description" => Some(Self::Description),
722            "column.lambda.application.status" => Some(Self::Status),
723            "column.lambda.application.last_modified" => Some(Self::LastModified),
724            _ => None,
725        }
726    }
727
728    pub fn default_name(&self) -> &'static str {
729        match self {
730            Self::Name => "Name",
731            Self::Description => "Description",
732            Self::Status => "Status",
733            Self::LastModified => "Last modified",
734        }
735    }
736
737    pub fn name(&self) -> String {
738        translate_column(self.id(), self.default_name())
739    }
740}
741
742impl table::Column<Application> for ApplicationColumn {
743    fn id(&self) -> &'static str {
744        match self {
745            Self::Name => "column.lambda.application.name",
746            Self::Description => "column.lambda.application.description",
747            Self::Status => "column.lambda.application.status",
748            Self::LastModified => "column.lambda.application.last_modified",
749        }
750    }
751
752    fn default_name(&self) -> &'static str {
753        match self {
754            Self::Name => "Application name",
755            Self::Description => "Description",
756            Self::Status => "Status",
757            Self::LastModified => "Last modified",
758        }
759    }
760
761    fn width(&self) -> u16 {
762        self.name().len().max(match self {
763            Self::Name => 40,
764            Self::Description => 50,
765            Self::Status => 20,
766            Self::LastModified => UTC_TIMESTAMP_WIDTH as usize,
767        }) as u16
768    }
769
770    fn render(&self, item: &Application) -> (String, Style) {
771        match self {
772            Self::Name => (item.name.clone(), Style::default()),
773            Self::Description => (item.description.clone(), Style::default()),
774            Self::Status => {
775                let status_upper = item.status.to_uppercase();
776                let text = if status_upper.contains("COMPLETE") {
777                    format!("✅ {}", item.status)
778                } else if status_upper == "UPDATE_IN_PROGRESS" {
779                    format!("ℹ️  {}", item.status)
780                } else if status_upper.contains("ROLLBACK") || status_upper.contains("_FAILED") {
781                    format!("❌ {}", item.status)
782                } else {
783                    item.status.clone()
784                };
785                let color = if status_upper.contains("COMPLETE") {
786                    Color::Green
787                } else if status_upper == "UPDATE_IN_PROGRESS" {
788                    Color::LightBlue
789                } else if status_upper.contains("ROLLBACK") || status_upper.contains("_FAILED") {
790                    Color::Red
791                } else {
792                    Color::White
793                };
794                (text, Style::default().fg(color))
795            }
796            Self::LastModified => (item.last_modified.clone(), Style::default()),
797        }
798    }
799}
800
801#[derive(Debug, Clone, Copy, PartialEq)]
802pub enum VersionColumn {
803    Version,
804    Aliases,
805    Description,
806    LastModified,
807    Architecture,
808}
809
810impl VersionColumn {
811    pub fn name(&self) -> &'static str {
812        match self {
813            VersionColumn::Version => "Version",
814            VersionColumn::Aliases => "Aliases",
815            VersionColumn::Description => "Description",
816            VersionColumn::LastModified => "Last modified",
817            VersionColumn::Architecture => "Architecture",
818        }
819    }
820
821    pub fn all() -> [VersionColumn; 5] {
822        [
823            VersionColumn::Version,
824            VersionColumn::Aliases,
825            VersionColumn::Description,
826            VersionColumn::LastModified,
827            VersionColumn::Architecture,
828        ]
829    }
830
831    pub fn to_column(&self) -> Box<dyn table::Column<Version>> {
832        struct VersionCol {
833            variant: VersionColumn,
834        }
835
836        impl table::Column<Version> for VersionCol {
837            fn name(&self) -> &str {
838                self.variant.name()
839            }
840
841            fn width(&self) -> u16 {
842                match self.variant {
843                    VersionColumn::Version => 10,
844                    VersionColumn::Aliases => 20,
845                    VersionColumn::Description => 40,
846                    VersionColumn::LastModified => UTC_TIMESTAMP_WIDTH,
847                    VersionColumn::Architecture => 15,
848                }
849            }
850
851            fn render(&self, item: &Version) -> (String, Style) {
852                let text = match self.variant {
853                    VersionColumn::Version => item.version.clone(),
854                    VersionColumn::Aliases => item.aliases.clone(),
855                    VersionColumn::Description => item.description.clone(),
856                    VersionColumn::LastModified => item.last_modified.clone(),
857                    VersionColumn::Architecture => format_architecture(&item.architecture),
858                };
859                (text, Style::default())
860            }
861        }
862
863        Box::new(VersionCol { variant: *self })
864    }
865}
866
867#[derive(Debug, Clone, Copy, PartialEq)]
868pub enum AliasColumn {
869    Name,
870    Versions,
871    Description,
872}
873
874impl AliasColumn {
875    pub fn name(&self) -> &'static str {
876        match self {
877            AliasColumn::Name => "Name",
878            AliasColumn::Versions => "Versions",
879            AliasColumn::Description => "Description",
880        }
881    }
882
883    pub fn all() -> [AliasColumn; 3] {
884        [
885            AliasColumn::Name,
886            AliasColumn::Versions,
887            AliasColumn::Description,
888        ]
889    }
890
891    pub fn to_column(&self) -> Box<dyn table::Column<Alias>> {
892        struct AliasCol {
893            variant: AliasColumn,
894        }
895
896        impl table::Column<Alias> for AliasCol {
897            fn name(&self) -> &str {
898                self.variant.name()
899            }
900
901            fn width(&self) -> u16 {
902                match self.variant {
903                    AliasColumn::Name => 20,
904                    AliasColumn::Versions => 15,
905                    AliasColumn::Description => 50,
906                }
907            }
908
909            fn render(&self, item: &Alias) -> (String, Style) {
910                let text = match self.variant {
911                    AliasColumn::Name => item.name.clone(),
912                    AliasColumn::Versions => item.versions.clone(),
913                    AliasColumn::Description => item.description.clone(),
914                };
915                (text, Style::default())
916            }
917        }
918
919        Box::new(AliasCol { variant: *self })
920    }
921}
922
923#[derive(Debug, Clone, Copy, PartialEq)]
924pub enum LayerColumn {
925    MergeOrder,
926    Name,
927    LayerVersion,
928    CompatibleRuntimes,
929    CompatibleArchitectures,
930    VersionArn,
931}
932
933impl LayerColumn {
934    pub fn default_name(&self) -> &'static str {
935        match self {
936            LayerColumn::MergeOrder => "Merge order",
937            LayerColumn::Name => "Name",
938            LayerColumn::LayerVersion => "Layer version",
939            LayerColumn::CompatibleRuntimes => "Compatible runtimes",
940            LayerColumn::CompatibleArchitectures => "Compatible architectures",
941            LayerColumn::VersionArn => "Version ARN",
942        }
943    }
944
945    pub fn name(&self) -> String {
946        translate_column(self.id(), self.default_name())
947    }
948
949    pub fn id(&self) -> ColumnId {
950        match self {
951            Self::MergeOrder => "column.lambda.layer.merge_order",
952            Self::Name => "column.lambda.layer.name",
953            Self::LayerVersion => "column.lambda.layer.layer_version",
954            Self::CompatibleRuntimes => "column.lambda.layer.compatible_runtimes",
955            Self::CompatibleArchitectures => "column.lambda.layer.compatible_architectures",
956            Self::VersionArn => "column.lambda.layer.version_arn",
957        }
958    }
959
960    pub fn from_id(id: ColumnId) -> Option<Self> {
961        match id {
962            "column.lambda.layer.merge_order" => Some(Self::MergeOrder),
963            "column.lambda.layer.name" => Some(Self::Name),
964            "column.lambda.layer.layer_version" => Some(Self::LayerVersion),
965            "column.lambda.layer.compatible_runtimes" => Some(Self::CompatibleRuntimes),
966            "column.lambda.layer.compatible_architectures" => Some(Self::CompatibleArchitectures),
967            "column.lambda.layer.version_arn" => Some(Self::VersionArn),
968            _ => None,
969        }
970    }
971
972    pub fn all() -> [LayerColumn; 6] {
973        [
974            LayerColumn::MergeOrder,
975            LayerColumn::Name,
976            LayerColumn::LayerVersion,
977            LayerColumn::CompatibleRuntimes,
978            LayerColumn::CompatibleArchitectures,
979            LayerColumn::VersionArn,
980        ]
981    }
982
983    pub fn ids() -> Vec<ColumnId> {
984        Self::all().iter().map(|c| c.id()).collect()
985    }
986}
987
988impl table::Column<Layer> for LayerColumn {
989    fn id(&self) -> &'static str {
990        match self {
991            Self::MergeOrder => "column.lambda.layer.merge_order",
992            Self::Name => "column.lambda.layer.name",
993            Self::LayerVersion => "column.lambda.layer.layer_version",
994            Self::CompatibleRuntimes => "column.lambda.layer.compatible_runtimes",
995            Self::CompatibleArchitectures => "column.lambda.layer.compatible_architectures",
996            Self::VersionArn => "column.lambda.layer.version_arn",
997        }
998    }
999
1000    fn default_name(&self) -> &'static str {
1001        match self {
1002            Self::MergeOrder => "Merge order",
1003            Self::Name => "Layer name",
1004            Self::LayerVersion => "Version",
1005            Self::CompatibleRuntimes => "Compatible runtimes",
1006            Self::CompatibleArchitectures => "Compatible architectures",
1007            Self::VersionArn => "Version ARN",
1008        }
1009    }
1010
1011    fn width(&self) -> u16 {
1012        match self {
1013            Self::MergeOrder => 12,
1014            Self::Name => 20,
1015            Self::LayerVersion => 14,
1016            Self::CompatibleRuntimes => 20,
1017            Self::CompatibleArchitectures => 26,
1018            Self::VersionArn => 40,
1019        }
1020    }
1021
1022    fn render(&self, item: &Layer) -> (String, Style) {
1023        let text = match self {
1024            Self::MergeOrder => item.merge_order.clone(),
1025            Self::Name => item.name.clone(),
1026            Self::LayerVersion => item.layer_version.clone(),
1027            Self::CompatibleRuntimes => item.compatible_runtimes.clone(),
1028            Self::CompatibleArchitectures => item.compatible_architectures.clone(),
1029            Self::VersionArn => item.version_arn.clone(),
1030        };
1031        (text, Style::default())
1032    }
1033}
1034
1035#[derive(Debug, Clone, Copy, PartialEq)]
1036pub enum DeploymentColumn {
1037    Deployment,
1038    ResourceType,
1039    LastUpdated,
1040    Status,
1041}
1042
1043impl DeploymentColumn {
1044    pub fn id(&self) -> ColumnId {
1045        match self {
1046            Self::Deployment => "column.lambda.deployment.deployment",
1047            Self::ResourceType => "column.lambda.deployment.resource_type",
1048            Self::LastUpdated => "column.lambda.deployment.last_updated",
1049            Self::Status => "column.lambda.deployment.status",
1050        }
1051    }
1052
1053    pub fn default_name(&self) -> &'static str {
1054        match self {
1055            Self::Deployment => "Deployment",
1056            Self::ResourceType => "Resource type",
1057            Self::LastUpdated => "Last updated time",
1058            Self::Status => "Status",
1059        }
1060    }
1061
1062    pub fn name(&self) -> String {
1063        translate_column(self.id(), self.default_name())
1064    }
1065
1066    pub fn from_id(id: ColumnId) -> Option<Self> {
1067        match id {
1068            "column.lambda.deployment.deployment" => Some(Self::Deployment),
1069            "column.lambda.deployment.resource_type" => Some(Self::ResourceType),
1070            "column.lambda.deployment.last_updated" => Some(Self::LastUpdated),
1071            "column.lambda.deployment.status" => Some(Self::Status),
1072            _ => None,
1073        }
1074    }
1075
1076    pub fn all() -> [Self; 4] {
1077        [
1078            Self::Deployment,
1079            Self::ResourceType,
1080            Self::LastUpdated,
1081            Self::Status,
1082        ]
1083    }
1084
1085    pub fn ids() -> Vec<ColumnId> {
1086        Self::all().iter().map(|c| c.id()).collect()
1087    }
1088}
1089
1090impl table::Column<Deployment> for DeploymentColumn {
1091    fn id(&self) -> &'static str {
1092        match self {
1093            Self::Deployment => "column.lambda.deployment.deployment",
1094            Self::ResourceType => "column.lambda.deployment.resource_type",
1095            Self::LastUpdated => "column.lambda.deployment.last_updated",
1096            Self::Status => "column.lambda.deployment.status",
1097        }
1098    }
1099
1100    fn default_name(&self) -> &'static str {
1101        match self {
1102            Self::Deployment => "Deployment",
1103            Self::ResourceType => "Resource type",
1104            Self::LastUpdated => "Last updated",
1105            Self::Status => "Status",
1106        }
1107    }
1108
1109    fn width(&self) -> u16 {
1110        let translated = translate_column(self.id(), self.default_name());
1111        translated.len().max(match self {
1112            Self::Deployment => 30,
1113            Self::ResourceType => 20,
1114            Self::LastUpdated => UTC_TIMESTAMP_WIDTH as usize,
1115            Self::Status => 20,
1116        }) as u16
1117    }
1118
1119    fn render(&self, item: &Deployment) -> (String, Style) {
1120        match self {
1121            Self::Deployment => (item.deployment_id.clone(), Style::default()),
1122            Self::ResourceType => (item.resource_type.clone(), Style::default()),
1123            Self::LastUpdated => (item.last_updated.clone(), Style::default()),
1124            Self::Status => {
1125                if item.status == "Succeeded" {
1126                    (
1127                        format!("✅ {}", item.status),
1128                        Style::default().fg(Color::Green),
1129                    )
1130                } else {
1131                    (item.status.clone(), Style::default())
1132                }
1133            }
1134        }
1135    }
1136}
1137#[derive(Clone, Debug)]
1138pub struct Resource {
1139    pub logical_id: String,
1140    pub physical_id: String,
1141    pub resource_type: String,
1142    pub last_modified: String,
1143}
1144
1145#[derive(Clone, Debug)]
1146pub struct Deployment {
1147    pub deployment_id: String,
1148    pub resource_type: String,
1149    pub last_updated: String,
1150    pub status: String,
1151}
1152
1153#[derive(Debug, Clone, Copy, PartialEq)]
1154pub enum ResourceColumn {
1155    LogicalId,
1156    PhysicalId,
1157    Type,
1158    LastModified,
1159}
1160
1161impl ResourceColumn {
1162    pub fn id(&self) -> ColumnId {
1163        match self {
1164            Self::LogicalId => "column.lambda.resource.logical_id",
1165            Self::PhysicalId => "column.lambda.resource.physical_id",
1166            Self::Type => "column.lambda.resource.type",
1167            Self::LastModified => "column.lambda.resource.last_modified",
1168        }
1169    }
1170
1171    pub fn default_name(&self) -> &'static str {
1172        match self {
1173            Self::LogicalId => "Logical ID",
1174            Self::PhysicalId => "Physical ID",
1175            Self::Type => "Type",
1176            Self::LastModified => "Last modified",
1177        }
1178    }
1179
1180    pub fn name(&self) -> String {
1181        translate_column(self.id(), self.default_name())
1182    }
1183
1184    pub fn from_id(id: ColumnId) -> Option<Self> {
1185        match id {
1186            "column.lambda.resource.logical_id" => Some(Self::LogicalId),
1187            "column.lambda.resource.physical_id" => Some(Self::PhysicalId),
1188            "column.lambda.resource.type" => Some(Self::Type),
1189            "column.lambda.resource.last_modified" => Some(Self::LastModified),
1190            _ => None,
1191        }
1192    }
1193
1194    pub fn all() -> [ResourceColumn; 4] {
1195        [
1196            Self::LogicalId,
1197            Self::PhysicalId,
1198            Self::Type,
1199            Self::LastModified,
1200        ]
1201    }
1202
1203    pub fn ids() -> Vec<ColumnId> {
1204        Self::all().iter().map(|c| c.id()).collect()
1205    }
1206}
1207
1208impl table::Column<Resource> for ResourceColumn {
1209    fn id(&self) -> &'static str {
1210        match self {
1211            Self::LogicalId => "column.lambda.resource.logical_id",
1212            Self::PhysicalId => "column.lambda.resource.physical_id",
1213            Self::Type => "column.lambda.resource.type",
1214            Self::LastModified => "column.lambda.resource.last_modified",
1215        }
1216    }
1217
1218    fn default_name(&self) -> &'static str {
1219        match self {
1220            Self::LogicalId => "Logical ID",
1221            Self::PhysicalId => "Physical ID",
1222            Self::Type => "Type",
1223            Self::LastModified => "Last Modified",
1224        }
1225    }
1226
1227    fn width(&self) -> u16 {
1228        match self {
1229            Self::LogicalId => 30,
1230            Self::PhysicalId => 40,
1231            Self::Type => 30,
1232            Self::LastModified => 27,
1233        }
1234    }
1235
1236    fn render(&self, item: &Resource) -> (String, Style) {
1237        let text = match self {
1238            Self::LogicalId => item.logical_id.clone(),
1239            Self::PhysicalId => item.physical_id.clone(),
1240            Self::Type => item.resource_type.clone(),
1241            Self::LastModified => item.last_modified.clone(),
1242        };
1243        (text, Style::default())
1244    }
1245}
1246
1247#[cfg(test)]
1248mod column_tests {
1249    use super::*;
1250
1251    #[test]
1252    fn test_function_column_id_returns_full_key() {
1253        let id = FunctionColumn::Name.id();
1254        assert_eq!(id, "column.lambda.function.name");
1255        assert!(id.starts_with("column."));
1256    }
1257
1258    #[test]
1259    fn test_application_column_id_returns_full_key() {
1260        let id = ApplicationColumn::Status.id();
1261        assert_eq!(id, "column.lambda.application.status");
1262        assert!(id.starts_with("column."));
1263    }
1264
1265    #[test]
1266    fn test_layer_column_id_returns_full_key() {
1267        let id = LayerColumn::Name.id();
1268        assert_eq!(id, "column.lambda.layer.name");
1269        assert!(id.starts_with("column."));
1270    }
1271
1272    #[test]
1273    fn test_deployment_column_id_returns_full_key() {
1274        let id = DeploymentColumn::Deployment.id();
1275        assert_eq!(id, "column.lambda.deployment.deployment");
1276        assert!(id.starts_with("column."));
1277    }
1278
1279    #[test]
1280    fn test_resource_column_id_returns_full_key() {
1281        let id = ResourceColumn::LogicalId.id();
1282        assert_eq!(id, "column.lambda.resource.logical_id");
1283        assert!(id.starts_with("column."));
1284    }
1285
1286    #[test]
1287    fn test_function_column_ids_have_correct_prefix() {
1288        for col in FunctionColumn::all() {
1289            assert!(
1290                col.id().starts_with("column.lambda.function."),
1291                "FunctionColumn ID '{}' should start with 'column.lambda.function.'",
1292                col.id()
1293            );
1294        }
1295    }
1296
1297    #[test]
1298    fn test_application_column_ids_have_correct_prefix() {
1299        for col in ApplicationColumn::all() {
1300            assert!(
1301                col.id().starts_with("column.lambda.application."),
1302                "ApplicationColumn ID '{}' should start with 'column.lambda.application.'",
1303                col.id()
1304            );
1305        }
1306    }
1307
1308    #[test]
1309    fn test_deployment_column_ids_have_correct_prefix() {
1310        for col in DeploymentColumn::all() {
1311            assert!(
1312                col.id().starts_with("column.lambda.deployment."),
1313                "DeploymentColumn ID '{}' should start with 'column.lambda.deployment.'",
1314                col.id()
1315            );
1316        }
1317    }
1318
1319    #[test]
1320    fn test_resource_column_ids_have_correct_prefix() {
1321        for col in ResourceColumn::all() {
1322            assert!(
1323                col.id().starts_with("column.lambda.resource."),
1324                "ResourceColumn ID '{}' should start with 'column.lambda.resource.'",
1325                col.id()
1326            );
1327        }
1328    }
1329
1330    #[test]
1331    fn test_resource_column_trait_id_and_name_do_not_panic() {
1332        // Regression: expanded_from_columns calls col.name() which calls the Column trait's
1333        // default id() → panics if id()/default_name() are not implemented on the trait impl.
1334        use crate::ui::table::Column as TraitColumn;
1335        for col in ResourceColumn::all() {
1336            let _ = TraitColumn::id(&col);
1337            let _ = TraitColumn::default_name(&col);
1338            let _ = TraitColumn::name(&col);
1339        }
1340    }
1341
1342    #[test]
1343    fn test_deployment_column_trait_id_and_name_do_not_panic() {
1344        use crate::ui::table::Column as TraitColumn;
1345        for col in DeploymentColumn::all() {
1346            let _ = TraitColumn::id(&col);
1347            let _ = TraitColumn::default_name(&col);
1348            let _ = TraitColumn::name(&col);
1349        }
1350    }
1351}