Skip to main content

rusticity_term/
lambda.rs

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