@@ -117,6 +117,7 @@ class NodeTemplate(ChangeSetNode):
117
117
parameters : Final [NodeParameters ]
118
118
conditions : Final [NodeConditions ]
119
119
resources : Final [NodeResources ]
120
+ outputs : Final [NodeOutputs ]
120
121
121
122
def __init__ (
122
123
self ,
@@ -126,12 +127,14 @@ def __init__(
126
127
parameters : NodeParameters ,
127
128
conditions : NodeConditions ,
128
129
resources : NodeResources ,
130
+ outputs : NodeOutputs ,
129
131
):
130
132
super ().__init__ (scope = scope , change_type = change_type )
131
133
self .mappings = mappings
132
134
self .parameters = parameters
133
135
self .conditions = conditions
134
136
self .resources = resources
137
+ self .outputs = outputs
135
138
136
139
137
140
class NodeDivergence (ChangeSetNode ):
@@ -189,6 +192,36 @@ def __init__(self, scope: Scope, change_type: ChangeType, mappings: list[NodeMap
189
192
self .mappings = mappings
190
193
191
194
195
+ class NodeOutput (ChangeSetNode ):
196
+ name : Final [str ]
197
+ value : Final [ChangeSetEntity ]
198
+ export : Final [Optional [ChangeSetEntity ]]
199
+ condition_reference : Final [Optional [TerminalValue ]]
200
+
201
+ def __init__ (
202
+ self ,
203
+ scope : Scope ,
204
+ change_type : ChangeType ,
205
+ name : str ,
206
+ value : ChangeSetEntity ,
207
+ export : Optional [ChangeSetEntity ],
208
+ conditional_reference : Optional [TerminalValue ],
209
+ ):
210
+ super ().__init__ (scope = scope , change_type = change_type )
211
+ self .name = name
212
+ self .value = value
213
+ self .export = export
214
+ self .condition_reference = conditional_reference
215
+
216
+
217
+ class NodeOutputs (ChangeSetNode ):
218
+ outputs : Final [list [NodeOutput ]]
219
+
220
+ def __init__ (self , scope : Scope , change_type : ChangeType , outputs : list [NodeOutput ]):
221
+ super ().__init__ (scope = scope , change_type = change_type )
222
+ self .outputs = outputs
223
+
224
+
192
225
class NodeCondition (ChangeSetNode ):
193
226
name : Final [str ]
194
227
body : Final [ChangeSetEntity ]
@@ -218,7 +251,7 @@ def __init__(self, scope: Scope, change_type: ChangeType, resources: list[NodeRe
218
251
class NodeResource (ChangeSetNode ):
219
252
name : Final [str ]
220
253
type_ : Final [ChangeSetTerminal ]
221
- condition_reference : Final [TerminalValue ]
254
+ condition_reference : Final [Optional [ TerminalValue ] ]
222
255
properties : Final [NodeProperties ]
223
256
224
257
def __init__ (
@@ -325,6 +358,9 @@ def __init__(self, scope: Scope, value: Any):
325
358
ResourcesKey : Final [str ] = "Resources"
326
359
PropertiesKey : Final [str ] = "Properties"
327
360
ParametersKey : Final [str ] = "Parameters"
361
+ ValueKey : Final [str ] = "Value"
362
+ ExportKey : Final [str ] = "Export"
363
+ OutputsKey : Final [str ] = "Outputs"
328
364
# TODO: expand intrinsic functions set.
329
365
RefKey : Final [str ] = "Ref"
330
366
FnIf : Final [str ] = "Fn::If"
@@ -567,17 +603,9 @@ def _visit_object(
567
603
binding_scope , (before_value , after_value ) = self ._safe_access_in (
568
604
scope , binding_name , before_object , after_object
569
605
)
570
- if self ._is_intrinsic_function_name (function_name = binding_name ):
571
- value = self ._visit_intrinsic_function (
572
- scope = binding_scope ,
573
- intrinsic_function = binding_name ,
574
- before_arguments = before_value ,
575
- after_arguments = after_value ,
576
- )
577
- else :
578
- value = self ._visit_value (
579
- scope = binding_scope , before_value = before_value , after_value = after_value
580
- )
606
+ value = self ._visit_value (
607
+ scope = binding_scope , before_value = before_value , after_value = after_value
608
+ )
581
609
bindings [binding_name ] = value
582
610
change_type = change_type .for_child (value .change_type )
583
611
node_object = NodeObject (scope = scope , change_type = change_type , bindings = bindings )
@@ -601,8 +629,11 @@ def _visit_value(
601
629
value = self ._visited_scopes .get (scope )
602
630
if isinstance (value , ChangeSetEntity ):
603
631
return value
632
+
633
+ before_type_name = self ._type_name_of (before_value )
634
+ after_type_name = self ._type_name_of (after_value )
604
635
unset = object ()
605
- if type ( before_value ) is type ( after_value ) :
636
+ if before_type_name == after_type_name :
606
637
dominant_value = before_value
607
638
elif self ._is_created (before = before_value , after = after_value ):
608
639
dominant_value = after_value
@@ -611,6 +642,7 @@ def _visit_value(
611
642
else :
612
643
dominant_value = unset
613
644
if dominant_value is not unset :
645
+ dominant_type_name = self ._type_name_of (dominant_value )
614
646
if self ._is_terminal (value = dominant_value ):
615
647
value = self ._visit_terminal_value (
616
648
scope = scope , before_value = before_value , after_value = after_value
@@ -623,6 +655,16 @@ def _visit_value(
623
655
value = self ._visit_array (
624
656
scope = scope , before_array = before_value , after_array = after_value
625
657
)
658
+ elif self ._is_intrinsic_function_name (dominant_type_name ):
659
+ intrinsic_function_scope , (before_arguments , after_arguments ) = (
660
+ self ._safe_access_in (scope , dominant_type_name , before_value , after_value )
661
+ )
662
+ value = self ._visit_intrinsic_function (
663
+ scope = scope ,
664
+ intrinsic_function = dominant_type_name ,
665
+ before_arguments = before_arguments ,
666
+ after_arguments = after_arguments ,
667
+ )
626
668
else :
627
669
raise RuntimeError (f"Unsupported type { type (dominant_value )} " )
628
670
# Case: type divergence.
@@ -717,12 +759,15 @@ def _visit_resource(
717
759
# TODO: investigate behaviour with type changes, for now this is filler code.
718
760
_ , type_str = self ._safe_access_in (scope , TypeKey , before_resource )
719
761
762
+ condition_reference = None
720
763
scope_condition , (before_condition , after_condition ) = self ._safe_access_in (
721
764
scope , ConditionKey , before_resource , after_resource
722
765
)
723
- condition_reference = self ._visit_terminal_value (
724
- scope_condition , before_condition , after_condition
725
- )
766
+ # TODO: condition references should be resolved for the condition's change_type?
767
+ if before_condition or after_condition :
768
+ condition_reference = self ._visit_terminal_value (
769
+ scope_condition , before_condition , after_condition
770
+ )
726
771
727
772
scope_properties , (before_properties , after_properties ) = self ._safe_access_in (
728
773
scope , PropertiesKey , before_resource , after_resource
@@ -887,18 +932,9 @@ def _visit_condition(
887
932
node_condition = self ._visited_scopes .get (scope )
888
933
if isinstance (node_condition , NodeCondition ):
889
934
return node_condition
890
-
891
- # TODO: is schema validation/check necessary or can we trust the input at this point?
892
- function_names : list [str ] = self ._safe_keys_of (before_condition , after_condition )
893
- if len (function_names ) == 1 :
894
- body = self ._visit_object (
895
- scope = scope , before_object = before_condition , after_object = after_condition
896
- )
897
- else :
898
- body = self ._visit_divergence (
899
- scope = scope , before_value = before_condition , after_value = after_condition
900
- )
901
-
935
+ body = self ._visit_value (
936
+ scope = scope , before_value = before_condition , after_value = after_condition
937
+ )
902
938
node_condition = NodeCondition (
903
939
scope = scope , change_type = body .change_type , name = condition_name , body = body
904
940
)
@@ -932,6 +968,64 @@ def _visit_conditions(
932
968
self ._visited_scopes [scope ] = node_conditions
933
969
return node_conditions
934
970
971
+ def _visit_output (
972
+ self , scope : Scope , name : str , before_output : Maybe [dict ], after_output : Maybe [dict ]
973
+ ) -> NodeOutput :
974
+ change_type = ChangeType .UNCHANGED
975
+ scope_value , (before_value , after_value ) = self ._safe_access_in (
976
+ scope , ValueKey , before_output , after_output
977
+ )
978
+ value = self ._visit_value (scope_value , before_value , after_value )
979
+ change_type = change_type .for_child (value .change_type )
980
+
981
+ export : Optional [ChangeSetEntity ] = None
982
+ scope_export , (before_export , after_export ) = self ._safe_access_in (
983
+ scope , ExportKey , before_output , after_output
984
+ )
985
+ if before_export or after_export :
986
+ export = self ._visit_value (scope_export , before_export , after_export )
987
+ change_type = change_type .for_child (export .change_type )
988
+
989
+ # TODO: condition references should be resolved for the condition's change_type?
990
+ condition_reference : Optional [TerminalValue ] = None
991
+ scope_condition , (before_condition , after_condition ) = self ._safe_access_in (
992
+ scope , ConditionKey , before_output , after_output
993
+ )
994
+ if before_condition or after_condition :
995
+ condition_reference = self ._visit_terminal_value (
996
+ scope_condition , before_condition , after_condition
997
+ )
998
+ change_type = change_type .for_child (condition_reference .change_type )
999
+
1000
+ return NodeOutput (
1001
+ scope = scope ,
1002
+ change_type = change_type ,
1003
+ name = name ,
1004
+ value = value ,
1005
+ export = export ,
1006
+ conditional_reference = condition_reference ,
1007
+ )
1008
+
1009
+ def _visit_outputs (
1010
+ self , scope : Scope , before_outputs : Maybe [dict ], after_outputs : Maybe [dict ]
1011
+ ) -> NodeOutputs :
1012
+ change_type = ChangeType .UNCHANGED
1013
+ outputs : list [NodeOutput ] = list ()
1014
+ output_names : list [str ] = self ._safe_keys_of (before_outputs , after_outputs )
1015
+ for output_name in output_names :
1016
+ scope_output , (before_output , after_output ) = self ._safe_access_in (
1017
+ scope , output_name , before_outputs , after_outputs
1018
+ )
1019
+ output = self ._visit_output (
1020
+ scope = scope_output ,
1021
+ name = output_name ,
1022
+ before_output = before_output ,
1023
+ after_output = after_output ,
1024
+ )
1025
+ outputs .append (output )
1026
+ change_type = change_type .for_child (output .change_type )
1027
+ return NodeOutputs (scope = scope , change_type = change_type , outputs = outputs )
1028
+
935
1029
def _model (self , before_template : Maybe [dict ], after_template : Maybe [dict ]) -> NodeTemplate :
936
1030
root_scope = Scope ()
937
1031
# TODO: visit other child types
@@ -970,6 +1064,13 @@ def _model(self, before_template: Maybe[dict], after_template: Maybe[dict]) -> N
970
1064
after_resources = after_resources ,
971
1065
)
972
1066
1067
+ outputs_scope , (before_outputs , after_outputs ) = self ._safe_access_in (
1068
+ root_scope , OutputsKey , before_template , after_template
1069
+ )
1070
+ outputs = self ._visit_outputs (
1071
+ scope = outputs_scope , before_outputs = before_outputs , after_outputs = after_outputs
1072
+ )
1073
+
973
1074
# TODO: compute the change_type of the template properly.
974
1075
return NodeTemplate (
975
1076
scope = root_scope ,
@@ -978,6 +1079,7 @@ def _model(self, before_template: Maybe[dict], after_template: Maybe[dict]) -> N
978
1079
parameters = parameters ,
979
1080
conditions = conditions ,
980
1081
resources = resources ,
1082
+ outputs = outputs ,
981
1083
)
982
1084
983
1085
def _retrieve_condition_if_exists (self , condition_name : str ) -> Optional [NodeCondition ]:
@@ -1090,13 +1192,30 @@ def _change_type_for_parent_of(change_types: list[ChangeType]) -> ChangeType:
1090
1192
break
1091
1193
return parent_change_type
1092
1194
1195
+ @staticmethod
1196
+ def _name_if_intrinsic_function (value : Maybe [Any ]) -> Optional [str ]:
1197
+ if isinstance (value , dict ):
1198
+ keys = ChangeSetModel ._safe_keys_of (value )
1199
+ if len (keys ) == 1 :
1200
+ key_name = keys [0 ]
1201
+ if ChangeSetModel ._is_intrinsic_function_name (key_name ):
1202
+ return key_name
1203
+ return None
1204
+
1205
+ @staticmethod
1206
+ def _type_name_of (value : Maybe [Any ]) -> str :
1207
+ maybe_intrinsic_function_name = ChangeSetModel ._name_if_intrinsic_function (value )
1208
+ if maybe_intrinsic_function_name is not None :
1209
+ return maybe_intrinsic_function_name
1210
+ return type (value ).__name__
1211
+
1093
1212
@staticmethod
1094
1213
def _is_terminal (value : Any ) -> bool :
1095
1214
return type (value ) in {int , float , bool , str , None , NothingType }
1096
1215
1097
1216
@staticmethod
1098
1217
def _is_object (value : Any ) -> bool :
1099
- return isinstance (value , dict )
1218
+ return isinstance (value , dict ) and ChangeSetModel . _name_if_intrinsic_function ( value ) is None
1100
1219
1101
1220
@staticmethod
1102
1221
def _is_array (value : Any ) -> bool :
0 commit comments