1use std::{collections::HashMap, path::PathBuf};
2
3use crate::{
4 cargo::{
5 CargoMetadata, Metadata, PackageMetadata, binary_targets_from_metadata, build::Build,
6 deploy::Deploy, watch::Watch,
7 },
8 error::MetadataError,
9};
10use cargo_metadata::{Package, Target};
11use figment::{
12 Figment,
13 providers::{Env, Format, Serialized, Toml},
14};
15use miette::{IntoDiagnostic, Result};
16use serde::{Deserialize, Serialize};
17use tracing::trace;
18
19#[derive(Debug, Default)]
20pub struct ConfigOptions {
21 pub name: Option<String>,
22 pub context: Option<String>,
23 pub global: Option<PathBuf>,
24 pub admerge: bool,
25}
26
27#[derive(Debug, Default, Deserialize, Serialize)]
28pub struct Config {
29 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
30 pub env: HashMap<String, String>,
31 pub build: Build,
32 pub deploy: Deploy,
33 pub watch: Watch,
34}
35
36impl From<PackageMetadata> for Config {
37 fn from(meta: PackageMetadata) -> Self {
38 Config {
39 env: meta.env,
40 build: meta.build.unwrap_or_default(),
41 watch: meta.watch.unwrap_or_default(),
42 deploy: meta.deploy.unwrap_or_default(),
43 }
44 }
45}
46
47pub fn load_config(
48 args_config: &Config,
49 metadata: &CargoMetadata,
50 options: &ConfigOptions,
51) -> Result<Config> {
52 let mut figment = figment_from_metadata(metadata, options)?;
53
54 let mut args_serialized = Serialized::defaults(args_config);
55 if let Some(context) = &options.context {
56 args_serialized = args_serialized.profile(context);
57 }
58
59 figment = if options.admerge {
60 figment.admerge(args_serialized)
61 } else {
62 figment.merge(args_serialized)
63 };
64
65 figment.extract().into_diagnostic()
66}
67
68pub fn load_config_without_cli_flags(
69 metadata: &CargoMetadata,
70 options: &ConfigOptions,
71) -> Result<Config> {
72 let figment = figment_from_metadata(metadata, options)?;
73 figment.extract().into_diagnostic()
74}
75
76fn figment_from_metadata(metadata: &CargoMetadata, options: &ConfigOptions) -> Result<Figment> {
77 let (ws_metadata, bin_metadata) = workspace_metadata(metadata, options.name.as_deref())?;
78 let package_metadata = package_metadata(metadata, options.name.as_deref())?;
79
80 let mut config_file = options
81 .global
82 .as_ref()
83 .map(Toml::file)
84 .unwrap_or_else(|| Toml::file("CargoLambda.toml"));
85 if options.context.is_some() {
86 config_file = config_file.nested()
87 }
88
89 let mut figment = Figment::new();
90 if let Some(context) = &options.context {
91 figment = figment.select(context)
92 }
93
94 let mut env_serialized = Env::prefixed("CARGO_LAMBDA_");
95 if let Some(context) = &options.context {
96 env_serialized = env_serialized.profile(context);
97 }
98 figment = figment.merge(env_serialized);
99
100 figment = if options.admerge {
101 figment.admerge(config_file)
102 } else {
103 figment.merge(config_file)
104 };
105
106 let mut ws_serialized = Serialized::defaults(ws_metadata);
107 if let Some(context) = &options.context {
108 ws_serialized = ws_serialized.profile(context);
109 }
110 if options.admerge {
111 figment = figment.admerge(ws_serialized);
112 } else {
113 figment = figment.merge(ws_serialized);
114 }
115
116 if let Some(bin_metadata) = bin_metadata {
117 let mut bin_serialized = Serialized::defaults(bin_metadata);
118 if let Some(context) = &options.context {
119 bin_serialized = bin_serialized.profile(context);
120 }
121
122 if options.admerge {
123 figment = figment.admerge(bin_serialized);
124 } else {
125 figment = figment.merge(bin_serialized);
126 }
127 }
128
129 if let Some(package_metadata) = package_metadata {
130 let mut package_serialized = Serialized::defaults(package_metadata);
131 if let Some(context) = &options.context {
132 package_serialized = package_serialized.profile(context);
133 }
134
135 if options.admerge {
136 figment = figment.admerge(package_serialized);
137 } else {
138 figment = figment.merge(package_serialized);
139 }
140 }
141
142 Ok(figment)
143}
144
145fn workspace_metadata(
146 metadata: &CargoMetadata,
147 name: Option<&str>,
148) -> Result<(Config, Option<Config>)> {
149 if metadata.workspace_metadata.is_null() || !metadata.workspace_metadata.is_object() {
150 return Ok((Config::default(), None));
151 }
152
153 let meta: Metadata =
154 serde_json::from_value(metadata.workspace_metadata.clone()).into_diagnostic()?;
155
156 let ws_config = meta.lambda.package.into();
157 if let Some(name) = name {
158 if let Some(bin_config) = meta.lambda.bin.get(name) {
159 return Ok((ws_config, Some(bin_config.clone().into())));
160 }
161 }
162
163 Ok((ws_config, None))
164}
165
166fn package_metadata(metadata: &CargoMetadata, name: Option<&str>) -> Result<Option<Config>> {
167 let kind_condition = |pkg: &Package, target: &Target| {
168 target.kind.iter().any(|kind| kind == "bin") && pkg.metadata.is_object()
169 };
170
171 let Some(name) = name else {
172 if metadata.packages.len() == 1 {
173 return get_config_from_root(metadata);
174 }
175
176 let targets = binary_targets_from_metadata(metadata, false);
177 trace!(
178 ?targets,
179 "inspecting targets for a command without package name"
180 );
181 if targets.len() == 1 {
182 let name = targets
183 .into_iter()
184 .next()
185 .ok_or(MetadataError::MissingBinaryInProject)?;
186 return get_config_from_packages(metadata, kind_condition, &name);
187 }
188
189 return Ok(None);
190 };
191
192 get_config_from_packages(metadata, kind_condition, name)
193}
194
195fn get_config_from_packages(
196 metadata: &CargoMetadata,
197 kind_condition: impl Fn(&Package, &Target) -> bool,
198 name: &str,
199) -> Result<Option<Config>> {
200 for pkg in &metadata.packages {
201 for target in &pkg.targets {
202 if kind_condition(pkg, target) && target.name == name {
203 let meta: Metadata =
204 serde_json::from_value(pkg.metadata.clone()).into_diagnostic()?;
205
206 if let Some(bin_config) = meta.lambda.bin.get(name) {
207 return Ok(Some(bin_config.clone().into()));
208 }
209
210 return Ok(Some(meta.lambda.package.into()));
211 }
212 }
213 }
214
215 Ok(None)
216}
217
218fn get_config_from_root(metadata: &CargoMetadata) -> Result<Option<Config>> {
219 let Some(root) = metadata.root_package() else {
220 return Ok(None);
221 };
222
223 if root.metadata.is_null() || !root.metadata.is_object() {
224 return Ok(None);
225 }
226
227 let meta: Metadata = serde_json::from_value(root.metadata.clone()).into_diagnostic()?;
228 Ok(Some(meta.lambda.package.into()))
229}
230
231#[cfg(test)]
232mod tests {
233
234 use matchit::MatchError;
235
236 use super::*;
237 use crate::{
238 cargo::{build::CompilerOptions, load_metadata},
239 lambda::{Memory, Tracing},
240 tests::fixture_metadata,
241 };
242
243 #[test]
244 fn test_load_env_from_metadata() {
245 let metadata = load_metadata(fixture_metadata("single-binary-package")).unwrap();
246 let config = load_config_without_cli_flags(&metadata, &ConfigOptions::default()).unwrap();
247
248 assert_eq!(
249 config.deploy.lambda_tags(),
250 Some(HashMap::from([
251 ("organization".to_string(), "aws".to_string()),
252 ("team".to_string(), "lambda".to_string())
253 ]))
254 );
255
256 assert_eq!(config.env.get("FOO"), Some(&"BAR".to_string()));
257 assert_eq!(config.deploy.function_config.memory, Some(Memory(512)));
258 assert_eq!(config.deploy.function_config.timeout, Some(60.into()));
259
260 assert_eq!(
261 config.deploy.function_config.layer,
262 Some(vec![
263 "arn:aws:lambda:us-east-1:xxxxxxxx:layers:layer1".to_string(),
264 "arn:aws:lambda:us-east-1:xxxxxxxx:layers:layer2".to_string()
265 ])
266 );
267
268 let tracing = config.deploy.function_config.tracing.unwrap();
269 assert_eq!(tracing, Tracing::Active);
270 assert_eq!(
271 config.deploy.function_config.role,
272 Some("arn:aws:lambda:us-east-1:xxxxxxxx:iam:role1".to_string())
273 );
274
275 let env_options = config.deploy.function_config.env_options.unwrap();
276 assert_eq!(env_options.env_var, Some(vec!["VAR1=VAL1".to_string()]));
277 assert_eq!(env_options.env_file, Some(".env.production".into()));
278
279 let compiler = config.build.compiler.unwrap();
280
281 let cargo_compiler = match compiler {
282 CompilerOptions::Cargo(opts) => opts,
283 other => panic!("unexpected compiler: {:?}", other),
284 };
285 assert_eq!(
286 cargo_compiler.subcommand,
287 Some(vec!["brazil".to_string(), "build".to_string()])
288 );
289 assert_eq!(
290 cargo_compiler.extra_args,
291 Some(vec!["--release".to_string()])
292 );
293 }
294
295 #[test]
296 fn test_load_router_from_metadata_admerge() {
297 let options = ConfigOptions {
298 name: Some("crate-3".to_string()),
299 admerge: true,
300 ..Default::default()
301 };
302
303 let metadata = load_metadata(fixture_metadata("workspace-package")).unwrap();
304 let config = load_config_without_cli_flags(&metadata, &options).unwrap();
305
306 let router = config.watch.router.unwrap();
307 assert_eq!(
308 router.at("/foo", "GET"),
309 Ok(("crate-1".to_string(), HashMap::new()))
310 );
311 assert_eq!(
312 router.at("/bar", "GET"),
313 Ok(("crate-1".to_string(), HashMap::new()))
314 );
315 assert_eq!(
316 router.at("/bar", "POST"),
317 Ok(("crate-2".to_string(), HashMap::new()))
318 );
319 assert_eq!(router.at("/baz", "GET"), Err(MatchError::NotFound));
320 assert_eq!(
321 router.at("/qux", "GET"),
322 Ok(("crate-3".to_string(), HashMap::new()))
323 );
324 }
325
326 #[test]
327 fn test_load_router_from_metadata_strict() {
328 let options = ConfigOptions {
329 name: Some("crate-3".to_string()),
330 ..Default::default()
331 };
332
333 let metadata = load_metadata(fixture_metadata("workspace-package")).unwrap();
334 let config = load_config_without_cli_flags(&metadata, &options).unwrap();
335
336 let router = config.watch.router.unwrap();
337 assert_eq!(router.raw.len(), 1);
338 assert_eq!(router.at("/foo", "GET"), Err(MatchError::NotFound));
339 assert_eq!(router.at("/bar", "GET"), Err(MatchError::NotFound));
340 assert_eq!(router.at("/bar", "POST"), Err(MatchError::NotFound));
341 assert_eq!(router.at("/baz", "GET"), Err(MatchError::NotFound));
342 assert_eq!(
343 router.at("/qux", "GET"),
344 Ok(("crate-3".to_string(), HashMap::new()))
345 );
346 }
347
348 #[test]
349 fn test_extend_env_from_workspace() {
350 let options = ConfigOptions {
351 name: Some("basic-lambda-1".to_string()),
352 admerge: true,
353 ..Default::default()
354 };
355
356 let metadata = load_metadata(fixture_metadata("workspace-package")).unwrap();
357 let config = load_config_without_cli_flags(&metadata, &options).unwrap();
358
359 assert_eq!(config.env.get("FOO"), Some(&"BAR".to_string()));
360 assert_eq!(config.env.get("EXTRA"), Some(&"TRUE".to_string()));
361 assert_eq!(config.env.get("AWS_REGION"), Some(&"us-west-2".to_string()));
362 }
363
364 #[test]
365 fn test_config_with_context() {
366 let manifest = fixture_metadata("config-with-context");
367 let global = manifest.parent().unwrap().join("CargoLambda.toml");
368
369 let options = ConfigOptions {
370 context: Some("production".to_string()),
371 global: Some(global.clone()),
372 ..Default::default()
373 };
374
375 let metadata = load_metadata(manifest).unwrap();
376 let config = load_config_without_cli_flags(&metadata, &options).unwrap();
377 assert_eq!(config.deploy.function_config.memory, Some(Memory(1024)));
378
379 let options = ConfigOptions {
380 context: Some("development".to_string()),
381 global: Some(global.clone()),
382 ..Default::default()
383 };
384
385 let config = load_config_without_cli_flags(&metadata, &options).unwrap();
386 assert_eq!(config.deploy.function_config.memory, Some(Memory(512)));
387
388 let options = ConfigOptions {
389 global: Some(global),
390 ..Default::default()
391 };
392
393 let config = load_config_without_cli_flags(&metadata, &options).unwrap();
394 assert_eq!(config.deploy.function_config.memory, Some(Memory(256)));
395 }
396
397 #[test]
398 fn test_config_with_context_and_cli_flags() {
399 let manifest = fixture_metadata("config-with-context");
400 let global = manifest.parent().unwrap().join("CargoLambda.toml");
401
402 let options = ConfigOptions {
403 context: Some("production".to_string()),
404 global: Some(global.clone()),
405 ..Default::default()
406 };
407
408 let mut deploy = Deploy::default();
409 deploy.function_config.memory = Some(Memory(2048));
410
411 let args_config = Config {
412 deploy,
413 ..Default::default()
414 };
415
416 let metadata = load_metadata(manifest).unwrap();
417 let config = load_config(&args_config, &metadata, &options).unwrap();
418 assert_eq!(config.deploy.function_config.memory, Some(Memory(2048)));
419 }
420}