Skip to main content

spin_manifest/schema/
v2.rs

1use anyhow::{Context, anyhow};
2use schemars::JsonSchema;
3use serde::{Deserialize, Serialize};
4use spin_serde::{DependencyName, DependencyPackageName, FixedVersion, LowerSnakeId};
5pub use spin_serde::{KebabId, SnakeId};
6
7pub use super::common::{ComponentBuildConfig, ComponentSource, Variable, WasiFilesMount};
8use super::{json_schema, kebab_or_snake_case, one_or_many};
9
10pub(crate) type Map<K, V> = indexmap::IndexMap<K, V>;
11
12mod app_details;
13mod component;
14mod dependency;
15mod target_env;
16mod trigger;
17
18pub use app_details::AppDetails;
19pub use component::{
20    Component, ComponentDependencies, ComponentProfileBuildOverride, ComponentProfileOverride,
21};
22pub use dependency::{ComponentDependency, InheritConfiguration, TriggerDependency};
23pub use target_env::TargetEnvironmentRef;
24pub use trigger::{ComponentSpec, OneOrManyComponentSpecs, Trigger, TriggerDependencies};
25
26/// App manifest
27#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
28#[serde(deny_unknown_fields)]
29pub struct AppManifest {
30    /// `spin_manifest_version = 2`
31    #[schemars(with = "usize", range(min = 2, max = 2))]
32    pub spin_manifest_version: FixedVersion<2>,
33    /// `[application]`
34    pub application: AppDetails,
35    /// Application configuration variables. These can be set via environment variables, or
36    /// from sources such as Hashicorp Vault or Azure KeyVault by using a runtime config file.
37    /// They are not available directly to components: use a component variable to ingest them.
38    ///
39    /// Learn more: https://spinframework.dev/variables, https://spinframework.dev/dynamic-configuration#application-variables-runtime-configuration
40    #[serde(default, skip_serializing_if = "Map::is_empty")]
41    pub variables: Map<LowerSnakeId, Variable>,
42    /// The triggers to which the application responds. Most triggers can appear
43    /// multiple times with different parameters: for example, the `http` trigger may
44    /// appear multiple times with different routes, or the `redis` trigger with
45    /// different channels.
46    ///
47    /// Example: `[[trigger.http]]`
48    #[serde(rename = "trigger")]
49    #[serde(default)]
50    #[schemars(with = "json_schema::TriggerSchema")]
51    pub triggers: Map<String, Vec<Trigger>>,
52    /// `[component.<id>]`
53    #[serde(rename = "component")]
54    #[serde(default, skip_serializing_if = "Map::is_empty")]
55    pub components: Map<KebabId, Component>,
56}
57
58impl AppManifest {
59    /// Ensures that required fields (where not enforced in the schema) are populated.
60    pub fn validate_required_fields(&self) -> anyhow::Result<()> {
61        if self.triggers.is_empty() {
62            anyhow::bail!("The application must have at least one trigger");
63        }
64
65        Ok(())
66    }
67
68    /// This method ensures that the dependencies of each component are valid.
69    pub fn validate_dependencies(&self) -> anyhow::Result<()> {
70        for (component_id, component) in &self.components {
71            component
72                .dependencies
73                .validate()
74                .with_context(|| format!("component {component_id:?} has invalid dependencies"))?;
75        }
76        Ok(())
77    }
78
79    /// Whether any component in the application defines the given profile.
80    /// Not every component defines every profile, and components intentionally
81    /// fall back to the anonymouse profile if they are asked for a profile
82    /// they don't define. So this can be used to detect that a user might have
83    /// mistyped a profile (e.g. `spin up --profile deugb`).
84    pub fn ensure_profile(&self, profile: Option<&str>) -> anyhow::Result<()> {
85        let Some(p) = profile else {
86            return Ok(());
87        };
88
89        let is_defined = self.components.values().any(|c| c.profile.contains_key(p));
90
91        if is_defined {
92            Ok(())
93        } else {
94            Err(anyhow!("Profile {p} is not defined in this application"))
95        }
96    }
97}
98
99#[cfg(test)]
100mod tests {
101    use std::path::PathBuf;
102    use toml::toml;
103
104    use super::*;
105
106    #[derive(Deserialize)]
107    #[allow(dead_code)]
108    struct FakeGlobalTriggerConfig {
109        global_option: bool,
110    }
111
112    #[derive(Deserialize)]
113    #[allow(dead_code)]
114    struct FakeTriggerConfig {
115        option: Option<bool>,
116    }
117
118    fn as_reference(spec: &ComponentSpec) -> Option<&str> {
119        match spec {
120            ComponentSpec::Reference(id) => Some(id.as_ref()),
121            ComponentSpec::Inline(_) => None,
122        }
123    }
124
125    fn as_inline(spec: &ComponentSpec) -> Option<&Component> {
126        match spec {
127            ComponentSpec::Reference(_) => None,
128            ComponentSpec::Inline(c) => Some(c),
129        }
130    }
131
132    fn as_local(source: &ComponentSource) -> Option<&str> {
133        match source {
134            ComponentSource::Local(path) => Some(path),
135            _ => None,
136        }
137    }
138
139    #[test]
140    fn deserializing_trigger_configs() {
141        let manifest = AppManifest::deserialize(toml! {
142            spin_manifest_version = 2
143            [application]
144            name = "trigger-configs"
145            [application.trigger.fake]
146            global_option = true
147            [[trigger.fake]]
148            component = { source = "inline.wasm" }
149            option = true
150        })
151        .unwrap();
152
153        FakeGlobalTriggerConfig::deserialize(
154            manifest.application.trigger_global_configs["fake"].clone(),
155        )
156        .unwrap();
157
158        FakeTriggerConfig::deserialize(manifest.triggers["fake"][0].config.clone()).unwrap();
159    }
160
161    #[derive(Deserialize)]
162    #[allow(dead_code)]
163    struct FakeGlobalToolConfig {
164        lint_level: String,
165    }
166
167    #[derive(Deserialize)]
168    #[allow(dead_code)]
169    struct FakeComponentToolConfig {
170        command: String,
171    }
172
173    #[test]
174    fn deserialising_custom_tool_settings() {
175        let manifest = AppManifest::deserialize(toml! {
176            spin_manifest_version = 2
177            [application]
178            name = "trigger-configs"
179            [application.tool.lint]
180            lint_level = "savage"
181            [[trigger.fake]]
182            something = "something else"
183            [component.fake]
184            source = "dummy"
185            [component.fake.tool.clean]
186            command = "cargo clean"
187        })
188        .unwrap();
189
190        FakeGlobalToolConfig::deserialize(manifest.application.tool["lint"].clone()).unwrap();
191        let fake_id: KebabId = "fake".to_owned().try_into().unwrap();
192        FakeComponentToolConfig::deserialize(manifest.components[&fake_id].tool["clean"].clone())
193            .unwrap();
194    }
195
196    #[test]
197    fn deserializing_labels() {
198        AppManifest::deserialize(toml! {
199            spin_manifest_version = 2
200            [application]
201            name = "trigger-configs"
202            [[trigger.fake]]
203            something = "something else"
204            [component.fake]
205            source = "dummy"
206            key_value_stores = ["default", "snake_case", "kebab-case"]
207            sqlite_databases = ["default", "snake_case", "kebab-case"]
208        })
209        .unwrap();
210    }
211
212    #[test]
213    fn deserializing_labels_fails_for_non_kebab_or_snake() {
214        assert!(
215            AppManifest::deserialize(toml! {
216                spin_manifest_version = 2
217                [application]
218                name = "trigger-configs"
219                [[trigger.fake]]
220                something = "something else"
221                [component.fake]
222                source = "dummy"
223                key_value_stores = ["b@dlabel"]
224            })
225            .is_err()
226        );
227    }
228
229    fn get_test_component_with_labels(labels: Vec<String>) -> Component {
230        #[allow(deprecated)]
231        Component {
232            source: ComponentSource::Local("dummy".to_string()),
233            description: "".to_string(),
234            variables: Map::new(),
235            environment: Map::new(),
236            files: vec![],
237            exclude_files: vec![],
238            allowed_http_hosts: vec![],
239            allowed_outbound_hosts: vec![],
240            key_value_stores: labels.clone(),
241            sqlite_databases: labels,
242            ai_models: vec![],
243            targets: None,
244            build: None,
245            tool: Map::new(),
246            dependencies_inherit_configuration: None,
247            dependencies: Default::default(),
248            profile: Default::default(),
249        }
250    }
251
252    #[test]
253    fn serialize_labels() {
254        let stores = vec![
255            "default".to_string(),
256            "snake_case".to_string(),
257            "kebab-case".to_string(),
258        ];
259        let component = get_test_component_with_labels(stores.clone());
260        let serialized = toml::to_string(&component).unwrap();
261        let deserialized = toml::from_str::<Component>(&serialized).unwrap();
262        assert_eq!(deserialized.key_value_stores, stores);
263    }
264
265    #[test]
266    fn serialize_labels_fails_for_non_kebab_or_snake() {
267        let component = get_test_component_with_labels(vec!["camelCase".to_string()]);
268        assert!(toml::to_string(&component).is_err());
269    }
270
271    #[test]
272    fn test_valid_snake_ids() {
273        for valid in ["default", "mixed_CASE_words", "letters1_then2_numbers345"] {
274            if let Err(err) = SnakeId::try_from(valid.to_string()) {
275                panic!("{valid:?} should be value: {err:?}");
276            }
277        }
278    }
279
280    #[test]
281    fn test_invalid_snake_ids() {
282        for invalid in [
283            "",
284            "kebab-case",
285            "_leading_underscore",
286            "trailing_underscore_",
287            "double__underscore",
288            "1initial_number",
289            "unicode_snowpeople☃☃☃",
290            "mIxEd_case",
291            "MiXeD_case",
292        ] {
293            if SnakeId::try_from(invalid.to_string()).is_ok() {
294                panic!("{invalid:?} should not be a valid SnakeId");
295            }
296        }
297    }
298
299    #[test]
300    fn test_check_disjoint() {
301        for (a, b) in [
302            ("foo:bar@0.1.0", "foo:bar@0.2.0"),
303            ("foo:bar/baz@0.1.0", "foo:bar/baz@0.2.0"),
304            ("foo:bar/baz@0.1.0", "foo:bar/bub@0.1.0"),
305            ("foo:bar@0.1.0", "foo:bar/bub@0.2.0"),
306            ("foo:bar@1.0.0", "foo:bar@2.0.0"),
307            ("foo:bar@0.1.0", "foo:bar@1.0.0"),
308            ("foo:bar/baz", "foo:bar/bub"),
309            ("foo:bar/baz@0.1.0-alpha", "foo:bar/baz@0.1.0-beta"),
310        ] {
311            let a: DependencyPackageName = a.parse().expect(a);
312            let b: DependencyPackageName = b.parse().expect(b);
313            ComponentDependencies::check_disjoint(&a, &b).unwrap();
314        }
315
316        for (a, b) in [
317            ("foo:bar@0.1.0", "foo:bar@0.1.1"),
318            ("foo:bar/baz@0.1.0", "foo:bar@0.1.0"),
319            ("foo:bar/baz@0.1.0", "foo:bar@0.1.0"),
320            ("foo:bar", "foo:bar@0.1.0"),
321            ("foo:bar@0.1.0-pre", "foo:bar@0.1.0-pre"),
322        ] {
323            let a: DependencyPackageName = a.parse().expect(a);
324            let b: DependencyPackageName = b.parse().expect(b);
325            assert!(
326                ComponentDependencies::check_disjoint(&a, &b).is_err(),
327                "{a} should conflict with {b}",
328            );
329        }
330    }
331
332    #[test]
333    fn test_validate_dependencies() {
334        // Specifying a dependency name as a plain-name without a package is an error
335        assert!(
336            ComponentDependencies::deserialize(toml! {
337                "plain-name" = "0.1.0"
338            })
339            .unwrap()
340            .validate()
341            .is_err()
342        );
343
344        // Specifying a dependency name as a plain-name without a package is an error
345        assert!(
346            ComponentDependencies::deserialize(toml! {
347                "plain-name" = { version = "0.1.0" }
348            })
349            .unwrap()
350            .validate()
351            .is_err()
352        );
353
354        // Specifying an export to satisfy a package dependency name is an error
355        assert!(
356            ComponentDependencies::deserialize(toml! {
357                "foo:baz@0.1.0" = { path = "foo.wasm", export = "foo"}
358            })
359            .unwrap()
360            .validate()
361            .is_err()
362        );
363
364        // Two compatible versions of the same package is an error
365        assert!(
366            ComponentDependencies::deserialize(toml! {
367                "foo:baz@0.1.0" = "0.1.0"
368                "foo:bar@0.2.1" = "0.2.1"
369                "foo:bar@0.2.2" = "0.2.2"
370            })
371            .unwrap()
372            .validate()
373            .is_err()
374        );
375
376        // Two disjoint versions of the same package is ok
377        assert!(
378            ComponentDependencies::deserialize(toml! {
379                "foo:bar@0.1.0" = "0.1.0"
380                "foo:bar@0.2.0" = "0.2.0"
381                "foo:baz@0.2.0" = "0.1.0"
382            })
383            .unwrap()
384            .validate()
385            .is_ok()
386        );
387
388        // Unversioned and versioned dependencies of the same package is an error
389        assert!(
390            ComponentDependencies::deserialize(toml! {
391                "foo:bar@0.1.0" = "0.1.0"
392                "foo:bar" = ">= 0.2.0"
393            })
394            .unwrap()
395            .validate()
396            .is_err()
397        );
398
399        // Two interfaces of two disjoint versions of a package is ok
400        assert!(
401            ComponentDependencies::deserialize(toml! {
402                "foo:bar/baz@0.1.0" = "0.1.0"
403                "foo:bar/baz@0.2.0" = "0.2.0"
404            })
405            .unwrap()
406            .validate()
407            .is_ok()
408        );
409
410        // A versioned interface and a different versioned package is ok
411        assert!(
412            ComponentDependencies::deserialize(toml! {
413                "foo:bar/baz@0.1.0" = "0.1.0"
414                "foo:bar@0.2.0" = "0.2.0"
415            })
416            .unwrap()
417            .validate()
418            .is_ok()
419        );
420
421        // A versioned interface and package of the same version is an error
422        assert!(
423            ComponentDependencies::deserialize(toml! {
424                "foo:bar/baz@0.1.0" = "0.1.0"
425                "foo:bar@0.1.0" = "0.1.0"
426            })
427            .unwrap()
428            .validate()
429            .is_err()
430        );
431
432        // A versioned interface and unversioned package is an error
433        assert!(
434            ComponentDependencies::deserialize(toml! {
435                "foo:bar/baz@0.1.0" = "0.1.0"
436                "foo:bar" = "0.1.0"
437            })
438            .unwrap()
439            .validate()
440            .is_err()
441        );
442
443        // An unversioned interface and versioned package is an error
444        assert!(
445            ComponentDependencies::deserialize(toml! {
446                "foo:bar/baz" = "0.1.0"
447                "foo:bar@0.1.0" = "0.1.0"
448            })
449            .unwrap()
450            .validate()
451            .is_err()
452        );
453
454        // An unversioned interface and unversioned package is an error
455        assert!(
456            ComponentDependencies::deserialize(toml! {
457                "foo:bar/baz" = "0.1.0"
458                "foo:bar" = "0.1.0"
459            })
460            .unwrap()
461            .validate()
462            .is_err()
463        );
464    }
465
466    fn normalized_component(
467        manifest: &AppManifest,
468        component: &str,
469        profile: Option<&str>,
470    ) -> Component {
471        use crate::normalize::normalize_manifest;
472
473        let id =
474            KebabId::try_from(component.to_owned()).expect("component ID should have been kebab");
475
476        let mut manifest = manifest.clone();
477        normalize_manifest(&mut manifest, profile).expect("should have normalised");
478        manifest
479            .components
480            .get(&id)
481            .expect("should have compopnent with id profile-test")
482            .clone()
483    }
484
485    #[test]
486    fn profiles_override_source() {
487        let manifest = AppManifest::deserialize(toml! {
488            spin_manifest_version = 2
489            [application]
490            name = "trigger-configs"
491            [[trigger.fake]]
492            component = "profile-test"
493            [component.profile-test]
494            source = "original"
495            [component.profile-test.profile.fancy]
496            source = "fancy-schmancy"
497        })
498        .expect("manifest should be valid");
499
500        let id = "profile-test";
501
502        let component = normalized_component(&manifest, id, None);
503        assert!(matches!(&component.source, ComponentSource::Local(p) if p == "original"));
504
505        let component = normalized_component(&manifest, id, Some("fancy"));
506        assert!(matches!(&component.source, ComponentSource::Local(p) if p == "fancy-schmancy"));
507
508        let component = normalized_component(&manifest, id, Some("non-existent"));
509        assert!(matches!(&component.source, ComponentSource::Local(p) if p == "original"));
510    }
511
512    #[test]
513    fn profiles_override_build_command() {
514        let manifest = AppManifest::deserialize(toml! {
515            spin_manifest_version = 2
516            [application]
517            name = "trigger-configs"
518            [[trigger.fake]]
519            component = "profile-test"
520            [component.profile-test]
521            source = "original"
522            build.command = "buildme --release"
523            [component.profile-test.profile.fancy]
524            source = "fancy-schmancy"
525            build.command = ["buildme --fancy", "lintme"]
526        })
527        .expect("manifest should be valid");
528
529        let id = "profile-test";
530
531        let build = normalized_component(&manifest, id, None)
532            .build
533            .expect("should have default build");
534        assert_eq!(1, build.commands().len());
535        assert_eq!("buildme --release", build.commands().next().unwrap());
536
537        let build = normalized_component(&manifest, id, Some("fancy"))
538            .build
539            .expect("should have fancy build");
540        assert_eq!(2, build.commands().len());
541        assert_eq!("buildme --fancy", build.commands().next().unwrap());
542        assert_eq!("lintme", build.commands().nth(1).unwrap());
543
544        let build = normalized_component(&manifest, id, Some("non-existent"))
545            .build
546            .expect("should fall back to default build");
547        assert_eq!(1, build.commands().len());
548        assert_eq!("buildme --release", build.commands().next().unwrap());
549    }
550
551    #[test]
552    fn profiles_can_have_build_command_when_default_doesnt() {
553        let manifest = AppManifest::deserialize(toml! {
554            spin_manifest_version = 2
555            [application]
556            name = "trigger-configs"
557            [[trigger.fake]]
558            component = "profile-test"
559            [component.profile-test]
560            source = "original"
561            [component.profile-test.profile.fancy]
562            source = "fancy-schmancy"
563            build.command = ["buildme --fancy", "lintme"]
564        })
565        .expect("manifest should be valid");
566
567        let component = normalized_component(&manifest, "profile-test", None);
568        assert!(component.build.is_none(), "shouldn't have default build");
569
570        let component = normalized_component(&manifest, "profile-test", Some("fancy"));
571        assert!(component.build.is_some(), "should have fancy build");
572
573        let build = component.build.expect("should have fancy build");
574
575        assert_eq!(2, build.commands().len());
576        assert_eq!("buildme --fancy", build.commands().next().unwrap());
577        assert_eq!("lintme", build.commands().nth(1).unwrap());
578    }
579
580    #[test]
581    fn profiles_override_env_vars() {
582        let manifest = AppManifest::deserialize(toml! {
583            spin_manifest_version = 2
584            [application]
585            name = "trigger-configs"
586            [[trigger.fake]]
587            component = "profile-test"
588            [component.profile-test]
589            source = "original"
590            environment = { DB_URL = "pg://production" }
591            [component.profile-test.profile.fancy]
592            environment = { DB_URL = "pg://fancy", FANCINESS = "1" }
593        })
594        .expect("manifest should be valid");
595
596        let id = "profile-test";
597
598        let component = normalized_component(&manifest, id, None);
599
600        assert_eq!(1, component.environment.len());
601        assert_eq!(
602            "pg://production",
603            component
604                .environment
605                .get("DB_URL")
606                .expect("DB_URL should have been set")
607        );
608
609        let component = normalized_component(&manifest, id, Some("fancy"));
610
611        assert_eq!(2, component.environment.len());
612        assert_eq!(
613            "pg://fancy",
614            component
615                .environment
616                .get("DB_URL")
617                .expect("DB_URL should have been set")
618        );
619        assert_eq!(
620            "1",
621            component
622                .environment
623                .get("FANCINESS")
624                .expect("FANCINESS should have been set")
625        );
626    }
627
628    #[test]
629    fn profiles_dependencies() {
630        let manifest = AppManifest::deserialize(toml! {
631            spin_manifest_version = 2
632            [application]
633            name = "trigger-configs"
634            [[trigger.fake]]
635            component = "profile-test"
636            [component.profile-test]
637            source = "original"
638            [component.profile-test.dependencies]
639            "foo-bar" = "1.0.0"
640            [component.profile-test.profile.fancy]
641            dependencies = { "foo-bar" = { path = "local.wasm" }, "fancy-thing" = "1.2.3" }
642        })
643        .expect("manifest should be valid");
644
645        let id = "profile-test";
646
647        let component = normalized_component(&manifest, id, None);
648
649        assert_eq!(1, component.dependencies.inner.len());
650        assert!(matches!(
651            component
652                .dependencies
653                .inner
654                .get(&DependencyName::Plain(KebabId::try_from("foo-bar".to_owned()).unwrap()))
655                .expect("foo-bar dep should have been set"),
656            ComponentDependency::Version(v) if v == "1.0.0",
657        ));
658
659        let component = normalized_component(&manifest, id, Some("fancy"));
660
661        assert_eq!(2, component.dependencies.inner.len());
662        assert!(matches!(
663            component
664                .dependencies
665                .inner
666                .get(&DependencyName::Plain(KebabId::try_from("foo-bar".to_owned()).unwrap()))
667                .expect("foo-bar dep should have been set"),
668            ComponentDependency::Local { path, .. } if path == &PathBuf::from("local.wasm"),
669        ));
670        assert!(matches!(
671            component
672                .dependencies
673                .inner
674                .get(&DependencyName::Plain(KebabId::try_from("fancy-thing".to_owned()).unwrap()))
675                .expect("fancy-thing dep should have been set"),
676            ComponentDependency::Version(v) if v == "1.2.3",
677        ));
678    }
679
680    #[test]
681    fn can_deserialise_one_or_many_one_ref() {
682        let manifest = AppManifest::deserialize(toml! {
683            spin_manifest_version = 2
684            [application]
685            name = "test"
686            [[trigger.fake]]
687            component = "test1"
688            components = { babble = "test2" }
689        })
690        .expect("manifest should be valid");
691
692        let trigger = manifest.triggers.get("fake").unwrap()[0].clone();
693
694        assert_eq!(
695            Some("test1"),
696            as_reference(trigger.component.as_ref().unwrap())
697        );
698        assert_eq!(1, trigger.components.len());
699        let babble_comps = &trigger.components.get("babble").as_ref().unwrap().0;
700        assert_eq!(1, babble_comps.len());
701        assert_eq!(Some("test2"), as_reference(&babble_comps[0]));
702    }
703
704    #[test]
705    fn can_deserialise_one_or_many_one_inline() {
706        let manifest = AppManifest::deserialize(toml! {
707            spin_manifest_version = 2
708            [application]
709            name = "test"
710            [[trigger.fake]]
711            component = "test1"
712            components = { babble = { source = "fie.wasm", allowed_outbound_hosts = ["http://example.com"] } }
713        })
714        .expect("manifest should be valid");
715
716        let trigger = manifest.triggers.get("fake").unwrap()[0].clone();
717
718        assert_eq!(1, trigger.components.len());
719        let babble_comps = &trigger.components.get("babble").as_ref().unwrap().0;
720        assert_eq!(1, babble_comps.len());
721        let single = as_inline(&babble_comps[0]).expect("should have deserialised to inline");
722        assert_eq!(Some("fie.wasm"), as_local(&single.source));
723        assert_eq!(1, single.allowed_outbound_hosts.len());
724    }
725
726    #[test]
727    fn can_deserialise_one_or_many_many() {
728        let manifest = AppManifest::deserialize(toml! {
729            spin_manifest_version = 2
730            [application]
731            name = "test"
732            [[trigger.fake]]
733            component = "test1"
734            components = { babble = ["test2", { source = "fie.wasm", allowed_outbound_hosts = ["http://example.com"] }, "test3"] }
735        })
736        .expect("manifest should be valid");
737
738        let trigger = manifest.triggers.get("fake").unwrap()[0].clone();
739
740        assert_eq!(1, trigger.components.len());
741        let babble_comps = &trigger.components.get("babble").as_ref().unwrap().0;
742        assert_eq!(3, babble_comps.len());
743
744        assert_eq!(Some("test2"), as_reference(&babble_comps[0]));
745
746        let inline = as_inline(&babble_comps[1]).expect("should have deserialised to inline");
747        assert_eq!(Some("fie.wasm"), as_local(&inline.source));
748        assert_eq!(1, inline.allowed_outbound_hosts.len());
749
750        assert_eq!(Some("test3"), as_reference(&babble_comps[2]));
751    }
752}