Skip to main content

vantage_aws/models/lambda/
function.rs

1use ciborium::Value as CborValue;
2use serde::{Deserialize, Serialize};
3use vantage_expressions::Expression;
4use vantage_expressions::expr_any;
5use vantage_expressions::traits::expressive::{DeferredFn, ExpressiveEnum};
6use vantage_table::any::AnyTable;
7use vantage_table::table::Table;
8use vantage_table::traits::table_source::TableSource;
9
10use crate::condition::AwsCondition;
11use crate::types::{Arn, AwsDateTime};
12use crate::{AwsAccount, eq};
13
14use super::alias::{Alias, aliases_table};
15use super::version::{Version, versions_table};
16use crate::models::logs::group::{LogGroup, groups_table as log_groups_table};
17
18/// One Lambda function from `ListFunctions`. Lambda's response
19/// includes nested config blobs (`LoggingConfig`, `TracingConfig`,
20/// `VpcConfig`, …) that v0 leaves nested as wire-shape — we surface
21/// the scalars callers usually want at the top level.
22#[derive(Debug, Clone, Serialize, Deserialize)]
23pub struct Function {
24    #[serde(rename = "FunctionName")]
25    pub function_name: String,
26    #[serde(rename = "FunctionArn", default)]
27    pub function_arn: String,
28    #[serde(rename = "Runtime", default)]
29    pub runtime: String,
30    #[serde(rename = "Role", default)]
31    pub role: String,
32    #[serde(rename = "Handler", default)]
33    pub handler: String,
34    #[serde(rename = "Description", default)]
35    pub description: String,
36    #[serde(rename = "Timeout", default)]
37    pub timeout: i64,
38    #[serde(rename = "MemorySize", default)]
39    pub memory_size: i64,
40    #[serde(rename = "LastModified", default)]
41    pub last_modified: String,
42    #[serde(rename = "Version", default)]
43    pub version: String,
44    #[serde(rename = "PackageType", default)]
45    pub package_type: String,
46}
47
48/// `ListFunctions` table — every function in the configured region.
49/// `MasterRegion` and `FunctionVersion` are the only AWS-side filters;
50/// most callers leave them off and rely on post-hoc narrowing.
51///
52/// Relations:
53///   - `aliases` → `ListAliases` for this function
54///   - `versions` → `ListVersionsByFunction` for this function
55///   - `log_group` → CloudWatch Logs group at `/aws/lambda/<name>`
56///     (cross-service into [`crate::models::logs`])
57///
58/// ```no_run
59/// # use vantage_aws::AwsAccount;
60/// # use vantage_aws::models::lambda::functions_table;
61/// # async fn run() -> vantage_core::Result<()> {
62/// # let aws = AwsAccount::from_default()?;
63/// let functions = functions_table(aws);
64/// # Ok(()) }
65/// ```
66pub fn functions_table(aws: AwsAccount) -> Table<AwsAccount, Function> {
67    Table::new("restjson/Functions:lambda/GET /2015-03-31/functions/", aws)
68        .with_id_column("FunctionName")
69        .with_column_of::<Arn>("FunctionArn")
70        .with_title_column_of::<String>("Runtime")
71        .with_title_column_of::<String>("Handler")
72        .with_column_of::<String>("Description")
73        .with_column_of::<Arn>("Role")
74        .with_column_of::<i64>("Timeout")
75        .with_column_of::<i64>("MemorySize")
76        .with_column_of::<AwsDateTime>("LastModified")
77        .with_column_of::<String>("Version")
78        .with_column_of::<String>("PackageType")
79        .with_many("aliases", "FunctionName", aliases_table)
80        .with_many("versions", "FunctionName", versions_table)
81        .with_foreign(
82            "log_group",
83            std::any::type_name::<Table<AwsAccount, LogGroup>>(),
84            log_group_relation,
85        )
86}
87
88/// Build the `:log_group` traversal target for a Lambda function.
89///
90/// Standard `with_many` would project `FunctionName` straight into the
91/// log-group filter, which doesn't match — log groups carry the
92/// derived name `/aws/lambda/<FunctionName>`. So we splice that prefix
93/// in here: a deferred expression runs the source query, pulls
94/// `FunctionName` from each row, and prefixes it before handing it to
95/// `logGroupNamePrefix` for AWS-side narrowing. The single-value
96/// constraint that all `Deferred` conditions live under still applies
97/// — multi-row sources error at execute time, same as any other
98/// traversal.
99fn log_group_relation(functions: &Table<AwsAccount, Function>) -> vantage_core::Result<AnyTable> {
100    let aws = functions.data_source().clone();
101    let mut groups = log_groups_table(aws.clone());
102
103    let table = functions.clone();
104    let aws_for_fn = aws.clone();
105    let inner: DeferredFn<CborValue> = DeferredFn::new(move || {
106        let aws = aws_for_fn.clone();
107        let table = table.clone();
108        Box::pin(async move {
109            let records = aws.list_table_values(&table).await?;
110            let names: Vec<CborValue> = records
111                .values()
112                .filter_map(|r| match r.get("FunctionName") {
113                    Some(CborValue::Text(s)) => Some(CborValue::Text(format!("/aws/lambda/{s}"))),
114                    _ => None,
115                })
116                .collect();
117            Ok(ExpressiveEnum::Scalar(CborValue::Array(names)))
118        })
119    });
120
121    let source: Expression<CborValue> = expr_any!("{}", { inner });
122
123    groups.add_condition(AwsCondition::Deferred {
124        field: "logGroupNamePrefix".to_string(),
125        source,
126    });
127
128    Ok(AnyTable::from_table(groups))
129}
130
131impl Function {
132    /// Build a [`functions_table`] narrowed to the function named in
133    /// `arn`. Accepts ARNs of the shape
134    /// `arn:aws:lambda:<region>:<account>:function:<name>` (with or
135    /// without a trailing `:<qualifier>`).
136    pub fn from_arn(arn: &str, aws: AwsAccount) -> Option<Table<AwsAccount, Function>> {
137        let after = arn.split(":function:").nth(1)?;
138        // Strip optional version/alias qualifier.
139        let name = after.split(':').next().unwrap_or(after);
140        if name.is_empty() {
141            return None;
142        }
143        let mut t = functions_table(aws);
144        t.add_condition(eq("FunctionName", name.to_string()));
145        Some(t)
146    }
147
148    /// Aliases for *this* function.
149    pub fn ref_aliases(&self, aws: AwsAccount) -> Table<AwsAccount, Alias> {
150        let mut t = aliases_table(aws);
151        t.add_condition(eq("FunctionName", self.function_name.clone()));
152        t
153    }
154
155    /// Published versions for *this* function (always includes
156    /// `$LATEST`).
157    pub fn ref_versions(&self, aws: AwsAccount) -> Table<AwsAccount, Version> {
158        let mut t = versions_table(aws);
159        t.add_condition(eq("FunctionName", self.function_name.clone()));
160        t
161    }
162
163    /// CloudWatch Logs group for *this* function — `/aws/lambda/<name>`
164    /// by default. Returns a [`crate::models::logs::group::LogGroup`]
165    /// table pre-narrowed via `logGroupNamePrefix`.
166    pub fn ref_log_group(&self, aws: AwsAccount) -> Table<AwsAccount, LogGroup> {
167        let mut t = log_groups_table(aws);
168        t.add_condition(eq(
169            "logGroupNamePrefix",
170            format!("/aws/lambda/{}", self.function_name),
171        ));
172        t
173    }
174}