@@ -5,13 +5,14 @@ use std::{
5
5
6
6
use anyhow:: { anyhow, Context } ;
7
7
use indexmap:: IndexMap ;
8
+ use itertools:: Itertools ;
8
9
use regex:: Regex ;
9
10
10
11
use crate :: {
11
12
constraints:: StringConstraints ,
12
13
reader:: {
13
- RawExtraOutput , RawParameter , RawTemplateManifest , RawTemplateManifestV1 ,
14
- RawTemplateVariant ,
14
+ RawCondition , RawConditional , RawExtraOutput , RawParameter , RawTemplateManifest ,
15
+ RawTemplateManifestV1 , RawTemplateVariant ,
15
16
} ,
16
17
run:: { Run , RunOptions } ,
17
18
store:: TemplateLayout ,
@@ -46,7 +47,7 @@ enum TemplateVariantKind {
46
47
}
47
48
48
49
/// The variant mode in which a template should be run.
49
- #[ derive( Clone , Debug , Eq , PartialEq ) ]
50
+ #[ derive( Clone , Debug ) ]
50
51
pub enum TemplateVariantInfo {
51
52
/// Create a new application from the template.
52
53
NewApplication ,
@@ -96,6 +97,22 @@ pub(crate) struct TemplateVariant {
96
97
skip_files : Vec < String > ,
97
98
skip_parameters : Vec < String > ,
98
99
snippets : HashMap < String , String > ,
100
+ conditions : Vec < Conditional > ,
101
+ }
102
+
103
+ #[ derive( Clone , Debug ) ]
104
+ pub ( crate ) struct Conditional {
105
+ condition : Condition ,
106
+ skip_files : Vec < String > ,
107
+ skip_parameters : Vec < String > ,
108
+ skip_snippets : Vec < String > ,
109
+ }
110
+
111
+ #[ derive( Clone , Debug ) ]
112
+ pub ( crate ) enum Condition {
113
+ ManifestEntryExists ( Vec < String > ) ,
114
+ #[ cfg( test) ]
115
+ Always ( bool ) ,
99
116
}
100
117
101
118
#[ derive( Clone , Debug , Eq , PartialEq , Hash ) ]
@@ -241,9 +258,12 @@ impl Template {
241
258
}
242
259
}
243
260
244
- fn variant ( & self , variant_info : & TemplateVariantInfo ) -> Option < & TemplateVariant > {
261
+ // TODO: we should resolve this once at the start of Run and then use that forever
262
+ fn variant ( & self , variant_info : & TemplateVariantInfo ) -> Option < TemplateVariant > {
245
263
let kind = variant_info. kind ( ) ;
246
- self . variants . get ( & kind)
264
+ self . variants
265
+ . get ( & kind)
266
+ . map ( |vt| vt. resolve_conditions ( variant_info) )
247
267
}
248
268
249
269
pub ( crate ) fn parameters (
@@ -253,7 +273,7 @@ impl Template {
253
273
let variant = self . variant ( variant_kind) . unwrap ( ) ; // TODO: for now
254
274
self . parameters
255
275
. iter ( )
256
- . filter ( |p| !variant. skip_parameter ( p) )
276
+ . filter ( move |p| !variant. skip_parameter ( p) )
257
277
}
258
278
259
279
pub ( crate ) fn parameter ( & self , name : impl AsRef < str > ) -> Option < & TemplateParameter > {
@@ -277,9 +297,9 @@ impl Template {
277
297
self . variants . contains_key ( & variant. kind ( ) )
278
298
}
279
299
280
- pub ( crate ) fn snippets ( & self , variant_kind : & TemplateVariantInfo ) -> & HashMap < String , String > {
300
+ pub ( crate ) fn snippets ( & self , variant_kind : & TemplateVariantInfo ) -> HashMap < String , String > {
281
301
let variant = self . variant ( variant_kind) . unwrap ( ) ; // TODO: for now
282
- & variant. snippets
302
+ variant. snippets
283
303
}
284
304
285
305
/// Creates a runner for the template, governed by the given options. Call
@@ -355,6 +375,29 @@ impl Template {
355
375
skip_files : raw. skip_files . unwrap_or_default ( ) ,
356
376
skip_parameters : raw. skip_parameters . unwrap_or_default ( ) ,
357
377
snippets : raw. snippets . unwrap_or_default ( ) ,
378
+ conditions : raw
379
+ . conditions
380
+ . unwrap_or_default ( )
381
+ . into_values ( )
382
+ . map ( Self :: parse_conditional)
383
+ . collect ( ) ,
384
+ }
385
+ }
386
+
387
+ fn parse_conditional ( conditional : RawConditional ) -> Conditional {
388
+ Conditional {
389
+ condition : Self :: parse_condition ( conditional. condition ) ,
390
+ skip_files : conditional. skip_files . unwrap_or_default ( ) ,
391
+ skip_parameters : conditional. skip_parameters . unwrap_or_default ( ) ,
392
+ skip_snippets : conditional. skip_snippets . unwrap_or_default ( ) ,
393
+ }
394
+ }
395
+
396
+ fn parse_condition ( condition : RawCondition ) -> Condition {
397
+ match condition {
398
+ RawCondition :: ManifestEntryExists ( path) => {
399
+ Condition :: ManifestEntryExists ( path. split ( '.' ) . map ( |s| s. to_string ( ) ) . collect_vec ( ) )
400
+ }
358
401
}
359
402
}
360
403
@@ -528,6 +571,45 @@ impl TemplateVariant {
528
571
pub ( crate ) fn skip_parameter ( & self , parameter : & TemplateParameter ) -> bool {
529
572
self . skip_parameters . iter ( ) . any ( |p| & parameter. id == p)
530
573
}
574
+
575
+ fn resolve_conditions ( & self , variant_info : & TemplateVariantInfo ) -> Self {
576
+ let mut resolved = self . clone ( ) ;
577
+ for condition in & self . conditions {
578
+ if condition. condition . is_true ( variant_info) {
579
+ resolved
580
+ . skip_files
581
+ . append ( & mut condition. skip_files . clone ( ) ) ;
582
+ resolved
583
+ . skip_parameters
584
+ . append ( & mut condition. skip_parameters . clone ( ) ) ;
585
+ resolved
586
+ . snippets
587
+ . retain ( |id, _| !condition. skip_snippets . contains ( id) ) ;
588
+ }
589
+ }
590
+ resolved
591
+ }
592
+ }
593
+
594
+ impl Condition {
595
+ fn is_true ( & self , variant_info : & TemplateVariantInfo ) -> bool {
596
+ match self {
597
+ Self :: ManifestEntryExists ( path) => match variant_info {
598
+ TemplateVariantInfo :: NewApplication => false ,
599
+ TemplateVariantInfo :: AddComponent { manifest_path } => {
600
+ let Ok ( toml_text) = std:: fs:: read_to_string ( manifest_path) else {
601
+ return false ;
602
+ } ;
603
+ let Ok ( table) = toml:: from_str :: < toml:: Value > ( & toml_text) else {
604
+ return false ;
605
+ } ;
606
+ crate :: toml:: get_at ( table, path) . is_some ( )
607
+ }
608
+ } ,
609
+ #[ cfg( test) ]
610
+ Self :: Always ( b) => * b,
611
+ }
612
+ }
531
613
}
532
614
533
615
fn parse_string_constraints ( raw : & RawParameter ) -> anyhow:: Result < StringConstraints > {
@@ -559,3 +641,128 @@ fn validate_v1_manifest(raw: &RawTemplateManifestV1) -> anyhow::Result<()> {
559
641
}
560
642
Ok ( ( ) )
561
643
}
644
+
645
+ #[ cfg( test) ]
646
+ mod test {
647
+ use super :: * ;
648
+
649
+ struct TempFile ( tempfile:: TempDir , PathBuf ) ;
650
+
651
+ impl TempFile {
652
+ fn path ( & self ) -> PathBuf {
653
+ self . 1 . clone ( )
654
+ }
655
+ }
656
+
657
+ fn make_temp_manifest ( content : & str ) -> TempFile {
658
+ let temp_dir = tempfile:: tempdir ( ) . unwrap ( ) ;
659
+ let temp_file = temp_dir. path ( ) . join ( "spin.toml" ) ;
660
+ std:: fs:: write ( & temp_file, content) . unwrap ( ) ;
661
+ TempFile ( temp_dir, temp_file)
662
+ }
663
+
664
+ #[ test]
665
+ fn manifest_entry_exists_condition_is_false_for_new_app ( ) {
666
+ let condition = Template :: parse_condition ( RawCondition :: ManifestEntryExists (
667
+ "application.trigger.redis" . to_owned ( ) ,
668
+ ) ) ;
669
+ assert ! ( !condition. is_true( & TemplateVariantInfo :: NewApplication ) ) ;
670
+ }
671
+
672
+ #[ test]
673
+ fn manifest_entry_exists_condition_is_false_if_not_present_in_existing_manifest ( ) {
674
+ let temp_file =
675
+ make_temp_manifest ( "name = \" hello\" \n [application.trigger.http]\n base = \" /\" " ) ;
676
+ let condition = Template :: parse_condition ( RawCondition :: ManifestEntryExists (
677
+ "application.trigger.redis" . to_owned ( ) ,
678
+ ) ) ;
679
+ assert ! ( !condition. is_true( & TemplateVariantInfo :: AddComponent {
680
+ manifest_path: temp_file. path( )
681
+ } ) ) ;
682
+ }
683
+
684
+ #[ test]
685
+ fn manifest_entry_exists_condition_is_true_if_present_in_existing_manifest ( ) {
686
+ let temp_file = make_temp_manifest (
687
+ "name = \" hello\" \n [application.trigger.redis]\n channel = \" HELLO\" " ,
688
+ ) ;
689
+ let condition = Template :: parse_condition ( RawCondition :: ManifestEntryExists (
690
+ "application.trigger.redis" . to_owned ( ) ,
691
+ ) ) ;
692
+ assert ! ( condition. is_true( & TemplateVariantInfo :: AddComponent {
693
+ manifest_path: temp_file. path( )
694
+ } ) ) ;
695
+ }
696
+
697
+ #[ test]
698
+ fn manifest_entry_exists_condition_is_false_if_path_does_not_exist ( ) {
699
+ let condition = Template :: parse_condition ( RawCondition :: ManifestEntryExists (
700
+ "application.trigger.redis" . to_owned ( ) ,
701
+ ) ) ;
702
+ assert ! ( !condition. is_true( & TemplateVariantInfo :: AddComponent {
703
+ manifest_path: PathBuf :: from( "this/file/does/not.exist" )
704
+ } ) ) ;
705
+ }
706
+
707
+ #[ test]
708
+ fn selected_variant_respects_target ( ) {
709
+ let add_component_vt = TemplateVariant {
710
+ conditions : vec ! [ Conditional {
711
+ condition: Condition :: Always ( true ) ,
712
+ skip_files: vec![ "test2" . to_owned( ) ] ,
713
+ skip_parameters: vec![ "p1" . to_owned( ) ] ,
714
+ skip_snippets: vec![ "s1" . to_owned( ) ] ,
715
+ } ] ,
716
+ skip_files : vec ! [ "test1" . to_owned( ) ] ,
717
+ snippets : [
718
+ ( "s1" . to_owned ( ) , "s1val" . to_owned ( ) ) ,
719
+ ( "s2" . to_owned ( ) , "s2val" . to_owned ( ) ) ,
720
+ ]
721
+ . into_iter ( )
722
+ . collect ( ) ,
723
+ ..Default :: default ( )
724
+ } ;
725
+ let variants = [
726
+ (
727
+ TemplateVariantKind :: NewApplication ,
728
+ TemplateVariant :: default ( ) ,
729
+ ) ,
730
+ ( TemplateVariantKind :: AddComponent , add_component_vt) ,
731
+ ]
732
+ . into_iter ( )
733
+ . collect ( ) ;
734
+ let template = Template {
735
+ id : "test" . to_owned ( ) ,
736
+ tags : HashSet :: new ( ) ,
737
+ description : None ,
738
+ installed_from : InstalledFrom :: Unknown ,
739
+ trigger : TemplateTriggerCompatibility :: Any ,
740
+ variants,
741
+ parameters : vec ! [ ] ,
742
+ extra_outputs : vec ! [ ] ,
743
+ snippets_dir : None ,
744
+ content_dir : None ,
745
+ } ;
746
+
747
+ let variant_info = TemplateVariantInfo :: NewApplication ;
748
+ let variant = template. variant ( & variant_info) . unwrap ( ) ;
749
+ assert ! ( variant. skip_files. is_empty( ) ) ;
750
+ assert ! ( variant. skip_parameters. is_empty( ) ) ;
751
+ assert ! ( variant. snippets. is_empty( ) ) ;
752
+
753
+ let add_variant_info = TemplateVariantInfo :: AddComponent {
754
+ manifest_path : PathBuf :: from ( "dummy" ) ,
755
+ } ;
756
+ let add_variant = template. variant ( & add_variant_info) . unwrap ( ) ;
757
+ // the conditional skip_files and skip_parameters are added to the variant's skip lists
758
+ assert_eq ! ( 2 , add_variant. skip_files. len( ) ) ;
759
+ assert ! ( add_variant. skip_files. contains( & "test1" . to_owned( ) ) ) ;
760
+ assert ! ( add_variant. skip_files. contains( & "test2" . to_owned( ) ) ) ;
761
+ assert_eq ! ( 1 , add_variant. skip_parameters. len( ) ) ;
762
+ assert ! ( add_variant. skip_parameters. contains( & "p1" . to_owned( ) ) ) ;
763
+ // the conditional skip_snippets are *removed from* the variant's snippets list
764
+ assert_eq ! ( 1 , add_variant. snippets. len( ) ) ;
765
+ assert ! ( !add_variant. snippets. contains_key( "s1" ) ) ;
766
+ assert ! ( add_variant. snippets. contains_key( "s2" ) ) ;
767
+ }
768
+ }
0 commit comments