1use std::{collections::HashSet, path::PathBuf};
4
5use crate::schema::v2::{AppManifest, ComponentSpec, KebabId};
6use anyhow::Context;
7
8pub fn normalize_manifest(manifest: &mut AppManifest, profile: Option<&str>) -> anyhow::Result<()> {
13 normalize_trigger_ids(manifest);
14 normalize_inline_components(manifest);
15 apply_profile_overrides(manifest, profile);
16 normalize_dependency_inherit_configuration(manifest)?;
17 normalize_dependency_component_refs(manifest)?;
18 Ok(())
19}
20
21fn normalize_inline_components(manifest: &mut AppManifest) {
22 let components = &mut manifest.components;
24 for trigger in manifest.triggers.values_mut().flatten() {
25 let trigger_id = &trigger.id;
26
27 let component_specs = trigger
28 .component
29 .iter_mut()
30 .chain(
31 trigger
32 .components
33 .values_mut()
34 .flat_map(|specs| specs.0.iter_mut()),
35 )
36 .collect::<Vec<_>>();
37 let multiple_components = component_specs.len() > 1;
38
39 let mut counter = 1;
40 for spec in component_specs {
41 if !matches!(spec, ComponentSpec::Inline(_)) {
42 continue;
43 };
44
45 let inline_id = {
46 let mut id = KebabId::try_from(format!("{trigger_id}-component"));
48 if multiple_components
50 || id.is_err()
51 || components.contains_key(id.as_ref().unwrap())
52 {
53 id = Ok(loop {
54 let id = KebabId::try_from(format!("inline-component{counter}")).unwrap();
55 if !components.contains_key(&id) {
56 break id;
57 }
58 counter += 1;
59 });
60 }
61 id.unwrap()
62 };
63
64 let inline_spec = std::mem::replace(spec, ComponentSpec::Reference(inline_id.clone()));
66 let ComponentSpec::Inline(component) = inline_spec else {
67 unreachable!();
68 };
69 components.insert(inline_id.clone(), *component);
71 }
72 }
73}
74
75fn normalize_trigger_ids(manifest: &mut AppManifest) {
76 let mut trigger_ids = manifest
77 .triggers
78 .values()
79 .flatten()
80 .cloned()
81 .map(|t| t.id)
82 .collect::<HashSet<_>>();
83 for (trigger_type, triggers) in &mut manifest.triggers {
84 let mut counter = 1;
85 for trigger in triggers {
86 if !trigger.id.is_empty() {
87 continue;
88 }
89 if let Some(ComponentSpec::Reference(component_id)) = &trigger.component {
91 let candidate_id = format!("{component_id}-{trigger_type}-trigger");
92 if !trigger_ids.contains(&candidate_id) {
93 trigger.id.clone_from(&candidate_id);
94 trigger_ids.insert(candidate_id);
95 continue;
96 }
97 }
98 trigger.id = loop {
100 let id = format!("{trigger_type}-trigger{counter}");
101 if !trigger_ids.contains(&id) {
102 trigger_ids.insert(id.clone());
103 break id;
104 }
105 counter += 1;
106 }
107 }
108 }
109}
110
111fn apply_profile_overrides(manifest: &mut AppManifest, profile: Option<&str>) {
112 let Some(profile) = profile else {
113 return;
114 };
115
116 for (_, component) in &mut manifest.components {
117 let Some(overrides) = component.profile.get(profile) else {
118 continue;
119 };
120
121 if let Some(profile_build) = overrides.build.as_ref() {
122 match component.build.as_mut() {
123 None => {
124 component.build = Some(crate::schema::v2::ComponentBuildConfig {
125 command: profile_build.command.clone(),
126 workdir: None,
127 watch: vec![],
128 })
129 }
130 Some(build) => {
131 build.command = profile_build.command.clone();
132 }
133 }
134 }
135
136 if let Some(source) = overrides.source.as_ref() {
137 component.source = source.clone();
138 }
139
140 component.environment.extend(overrides.environment.clone());
141
142 component
143 .dependencies
144 .inner
145 .extend(overrides.dependencies.inner.clone());
146 }
147}
148
149use crate::schema::v2::{
150 Component, ComponentDependency, ComponentSource, InheritConfiguration, TriggerDependency,
151};
152
153fn normalize_dependency_inherit_configuration(manifest: &mut AppManifest) -> anyhow::Result<()> {
157 for (component_id, component) in &mut manifest.components {
158 let component_level = component.dependencies_inherit_configuration;
159
160 let has_per_dep = component
161 .dependencies
162 .inner
163 .values()
164 .any(|dep| dep.inherit_configuration().is_some());
165
166 if component_level.is_some() && has_per_dep {
167 anyhow::bail!(
168 "Component `{component_id}` specifies both `dependencies_inherit_configuration` \
169 and per-dependency `inherit_configuration`. These are mutually exclusive; \
170 use one or the other."
171 );
172 }
173
174 if component_level == Some(true) {
175 let inherit = InheritConfiguration::All(true);
176 for dep in component.dependencies.inner.values_mut() {
177 dep.set_inherit_configuration(inherit.clone());
178 }
179 component.dependencies_inherit_configuration = None;
180 }
181 }
182
183 Ok(())
184}
185
186fn normalize_dependency_component_refs(manifest: &mut AppManifest) -> anyhow::Result<()> {
187 let components = manifest.components.clone();
191
192 for (depender_id, component) in &mut manifest.components {
193 for dependency in component.dependencies.inner.values_mut() {
194 if let ComponentDependency::AppComponent {
195 component: depended_on_id,
196 export,
197 inherit_configuration,
198 } = dependency
199 {
200 let depended_on = components
201 .get(depended_on_id)
202 .with_context(|| format!("dependency ID {depended_on_id} does not exist"))?;
203 ensure_is_acceptable_dependency(depended_on, depended_on_id, depender_id.as_ref())?;
204 *dependency = component_source_to_dependency(
205 &depended_on.source,
206 export.clone(),
207 inherit_configuration.clone(),
208 );
209 }
210 }
211 }
212
213 for (_, triggers) in &mut manifest.triggers {
214 for trigger in triggers {
215 for (_, deps) in &mut trigger.dependencies {
216 for dependency in &mut deps.0 {
217 if let TriggerDependency::AppComponent {
218 component: depended_on_id,
219 inherit_configuration,
220 } = dependency
221 {
222 let depended_on = components.get(depended_on_id).with_context(|| {
223 format!("dependency ID {depended_on_id} does not exist")
224 })?;
225 ensure_is_acceptable_dependency(depended_on, depended_on_id, &trigger.id)?;
226 *dependency = component_source_to_trigger_dependency(
227 &depended_on.source,
228 inherit_configuration.clone(),
229 );
230 }
231 }
232 }
233 }
234 }
235
236 Ok(())
237}
238
239fn component_source_to_dependency(
240 source: &ComponentSource,
241 export: Option<String>,
242 inherit_configuration: Option<InheritConfiguration>,
243) -> ComponentDependency {
244 match source {
245 ComponentSource::Local(path) => ComponentDependency::Local {
246 path: PathBuf::from(path),
247 export,
248 inherit_configuration,
249 },
250 ComponentSource::Remote { url, digest } => ComponentDependency::HTTP {
251 url: url.clone(),
252 digest: digest.clone(),
253 export,
254 inherit_configuration,
255 },
256 ComponentSource::Registry {
257 registry,
258 package,
259 version,
260 } => ComponentDependency::Package {
261 version: version.clone(),
262 registry: registry.as_ref().map(|r| r.to_string()),
263 package: Some(package.to_string()),
264 export,
265 inherit_configuration,
266 },
267 }
268}
269
270fn component_source_to_trigger_dependency(
271 source: &ComponentSource,
272 inherit_configuration: Option<InheritConfiguration>,
273) -> TriggerDependency {
274 match source {
275 ComponentSource::Local(path) => TriggerDependency::Local {
276 path: PathBuf::from(path),
277 inherit_configuration,
278 },
279 ComponentSource::Remote { url, digest } => TriggerDependency::HTTP {
280 url: url.clone(),
281 digest: digest.clone(),
282 inherit_configuration,
283 },
284 ComponentSource::Registry {
285 registry,
286 package,
287 version,
288 } => TriggerDependency::Package {
289 version: version.clone(),
290 registry: registry.as_ref().map(|r| r.to_string()),
291 package: package.to_string(),
292 inherit_configuration,
293 },
294 }
295}
296
297fn ensure_is_acceptable_dependency(
301 component: &Component,
302 depended_on_id: &KebabId,
303 depender_id: &str,
304) -> anyhow::Result<()> {
305 let mut surprises = vec![];
306
307 #[allow(deprecated)]
311 let Component {
312 source: _,
313 description: _,
314 variables,
315 environment,
316 files,
317 exclude_files: _,
318 allowed_http_hosts,
319 allowed_outbound_hosts,
320 key_value_stores,
321 sqlite_databases,
322 ai_models,
323 targets: _,
324 build: _,
325 tool: _,
326 dependencies_inherit_configuration: _,
327 dependencies,
328 profile: _,
329 } = component;
330
331 if !ai_models.is_empty() {
332 surprises.push("ai_models");
333 }
334 if !allowed_http_hosts.is_empty() {
335 surprises.push("allowed_http_hosts");
336 }
337 if !allowed_outbound_hosts.is_empty() {
338 surprises.push("allowed_outbound_hosts");
339 }
340 if !dependencies.inner.is_empty() {
341 surprises.push("dependencies");
342 }
343 if !environment.is_empty() {
344 surprises.push("environment");
345 }
346 if !files.is_empty() {
347 surprises.push("files");
348 }
349 if !key_value_stores.is_empty() {
350 surprises.push("key_value_stores");
351 }
352 if !sqlite_databases.is_empty() {
353 surprises.push("sqlite_databases");
354 }
355 if !variables.is_empty() {
356 surprises.push("variables");
357 }
358
359 if surprises.is_empty() {
360 Ok(())
361 } else {
362 anyhow::bail!(
363 "Dependencies may not have their own resources or permissions. Component {depended_on_id} cannot be used as a dependency of {depender_id} because it specifies: {}",
364 surprises.join(", ")
365 );
366 }
367}
368
369#[cfg(test)]
370mod test {
371 use super::*;
372
373 use crate::schema::v2::InheritConfiguration;
374 use serde::Deserialize;
375 use toml::toml;
376
377 fn package_name(name: &str) -> spin_serde::DependencyName {
378 let dpn = spin_serde::DependencyPackageName::try_from(name.to_string()).unwrap();
379 spin_serde::DependencyName::Package(dpn)
380 }
381
382 #[test]
383 fn can_resolve_dependency_on_file_source() {
384 let mut manifest = AppManifest::deserialize(toml! {
385 spin_manifest_version = 2
386
387 [application]
388 name = "dummy"
389
390 [[trigger.dummy]]
391 component = "a"
392
393 [component.a]
394 source = "a.wasm"
395 [component.a.dependencies]
396 "b:b" = { component = "b" }
397
398 [component.b]
399 source = "b.wasm"
400 })
401 .unwrap();
402
403 normalize_manifest(&mut manifest, None).unwrap();
404
405 let dep = manifest
406 .components
407 .get("a")
408 .unwrap()
409 .dependencies
410 .inner
411 .get(&package_name("b:b"))
412 .unwrap();
413
414 let ComponentDependency::Local {
415 path,
416 export,
417 inherit_configuration,
418 } = dep
419 else {
420 panic!("should have normalised to local dep");
421 };
422
423 assert_eq!(&PathBuf::from("b.wasm"), path);
424 assert_eq!(&None, export);
425 assert!(inherit_configuration.is_none());
426 }
427
428 #[test]
429 fn can_resolve_dependency_on_http_source() {
430 let mut manifest = AppManifest::deserialize(toml! {
431 spin_manifest_version = 2
432
433 [application]
434 name = "dummy"
435
436 [[trigger.dummy]]
437 component = "a"
438
439 [component.a]
440 source = "a.wasm"
441 [component.a.dependencies]
442 "b:b" = { component = "b", export = "c:d/e" }
443
444 [component.b]
445 source = { url = "http://example.com/b.wasm", digest = "12345" }
446 })
447 .unwrap();
448
449 normalize_manifest(&mut manifest, None).unwrap();
450
451 let dep = manifest
452 .components
453 .get("a")
454 .unwrap()
455 .dependencies
456 .inner
457 .get(&package_name("b:b"))
458 .unwrap();
459
460 let ComponentDependency::HTTP {
461 url,
462 digest,
463 export,
464 inherit_configuration,
465 } = dep
466 else {
467 panic!("should have normalised to HTTP dep");
468 };
469
470 assert_eq!("http://example.com/b.wasm", url);
471 assert_eq!("12345", digest);
472 assert_eq!("c:d/e", export.as_ref().unwrap());
473 assert!(inherit_configuration.is_none());
474 }
475
476 #[test]
477 fn can_resolve_dependency_on_package() {
478 let mut manifest = AppManifest::deserialize(toml! {
479 spin_manifest_version = 2
480
481 [application]
482 name = "dummy"
483
484 [[trigger.dummy]]
485 component = "a"
486
487 [component.a]
488 source = "a.wasm"
489 [component.a.dependencies]
490 "b:b" = { component = "b" }
491
492 [component.b]
493 source = { package = "bb:bb", version = "1.2.3", registry = "reginalds-registry.reg" }
494 })
495 .unwrap();
496
497 normalize_manifest(&mut manifest, None).unwrap();
498
499 let dep = manifest
500 .components
501 .get("a")
502 .unwrap()
503 .dependencies
504 .inner
505 .get(&package_name("b:b"))
506 .unwrap();
507
508 let ComponentDependency::Package {
509 version,
510 registry,
511 package,
512 export,
513 inherit_configuration,
514 } = dep
515 else {
516 panic!("should have normalised to package dep");
517 };
518
519 assert_eq!("1.2.3", version);
520 assert_eq!("reginalds-registry.reg", registry.as_ref().unwrap());
521 assert_eq!("bb:bb", package.as_ref().unwrap());
522 assert_eq!(&None, export);
523 assert!(inherit_configuration.is_none());
524 }
525
526 #[test]
527 fn can_resolve_dependency_with_inherit() {
528 let mut manifest = AppManifest::deserialize(toml! {
529 spin_manifest_version = 2
530
531 [application]
532 name = "dummy"
533
534 [[trigger.dummy]]
535 component = "a"
536
537 [component.a]
538 source = "a.wasm"
539 [component.a.dependencies]
540 "b:b" = { component = "b", inherit_configuration = true }
541
542 [component.b]
543 source = "b.wasm"
544 })
545 .unwrap();
546
547 normalize_manifest(&mut manifest, None).unwrap();
548
549 let dep = manifest
550 .components
551 .get("a")
552 .unwrap()
553 .dependencies
554 .inner
555 .get(&package_name("b:b"))
556 .unwrap();
557
558 let ComponentDependency::Local {
559 path,
560 export,
561 inherit_configuration,
562 } = dep
563 else {
564 panic!("should have normalised to local dep");
565 };
566
567 assert_eq!(&PathBuf::from("b.wasm"), path);
568 assert_eq!(&None, export);
569 assert!(matches!(
570 inherit_configuration,
571 Some(InheritConfiguration::All(true))
572 ));
573 }
574
575 #[test]
576 fn can_resolve_dependency_with_inherit_some() {
577 let mut manifest = AppManifest::deserialize(toml! {
578 spin_manifest_version = 2
579
580 [application]
581 name = "dummy"
582
583 [[trigger.dummy]]
584 component = "a"
585
586 [component.a]
587 source = "a.wasm"
588 [component.a.dependencies]
589 "b:b" = { component = "b", inherit_configuration = ["ai_models", "allowed_outbound_hosts"] }
590
591 [component.b]
592 source = "b.wasm"
593 })
594 .unwrap();
595
596 normalize_manifest(&mut manifest, None).unwrap();
597
598 let dep = manifest
599 .components
600 .get("a")
601 .unwrap()
602 .dependencies
603 .inner
604 .get(&package_name("b:b"))
605 .unwrap();
606
607 let ComponentDependency::Local {
608 path,
609 export,
610 inherit_configuration,
611 } = dep
612 else {
613 panic!("should have normalised to local dep");
614 };
615
616 assert_eq!(&PathBuf::from("b.wasm"), path);
617 assert_eq!(&None, export);
618 let Some(InheritConfiguration::Some(keys)) = inherit_configuration else {
619 panic!("should have inherit_configuration = Some([...])");
620 };
621 assert_eq!(
622 &vec![
623 "ai_models".to_string(),
624 "allowed_outbound_hosts".to_string()
625 ],
626 keys
627 );
628 }
629}