cfndsl 0.17.5 → 1.0.0.pre.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/.rubocop.yml +4 -4
- data/.travis.yml +6 -1
- data/CHANGELOG.md +32 -34
- data/README.md +68 -21
- data/Rakefile +25 -3
- data/TODO.md +18 -0
- data/UPGRADING.md +22 -0
- data/cfndsl.gemspec +3 -3
- data/exe/cfndsl +5 -0
- data/lib/cfndsl.rb +2 -116
- data/lib/cfndsl/aws/cloud_formation_template.rb +8 -1
- data/lib/cfndsl/aws/patches/000_sam.spec.json +574 -0
- data/lib/cfndsl/aws/patches/100_sam.spec_DeploymentPreference_patch.json +64 -0
- data/lib/cfndsl/aws/patches/500_Cognito_IdentityPoolRoleAttachment_patches.json +25 -0
- data/lib/cfndsl/aws/patches/500_IoT1Click_patch_PlacementTemplate_DeviceTemplates.json +20 -0
- data/lib/cfndsl/aws/patches/500_SAM_Serverless_Function_S3Event_Events_patch.json +16 -0
- data/lib/cfndsl/aws/patches/500_SAM_Serverless_Function_S3Location_Version_patch.json +16 -0
- data/lib/cfndsl/aws/patches/500_SSM_AssociationName_patch.json +16 -0
- data/lib/cfndsl/aws/patches/500_VPCEndpoint_patch.json +17 -0
- data/lib/cfndsl/aws/patches/510_ElasticSearch_Domain_patches.json +15 -0
- data/lib/cfndsl/aws/patches/600_RefKinds_patch.json +3654 -0
- data/lib/cfndsl/aws/patches/700_SAM_Serverless_Function_InlineCode_patch.json +20 -0
- data/lib/cfndsl/aws/patches/800_List_types_patch.json +115 -0
- data/lib/cfndsl/aws/resource_specification.json +35809 -11627
- data/lib/cfndsl/aws/types.rb +3 -3
- data/lib/cfndsl/cfnlego.rb +34 -0
- data/lib/{cfnlego → cfndsl/cfnlego}/cloudformation.erb +0 -0
- data/lib/{cfnlego → cfndsl/cfnlego}/cloudformation.rb +0 -0
- data/lib/{cfnlego → cfndsl/cfnlego}/resource.rb +3 -8
- data/lib/cfndsl/cloudformation.rb +107 -0
- data/lib/cfndsl/conditions.rb +11 -1
- data/lib/cfndsl/creation_policy.rb +1 -1
- data/lib/cfndsl/deep_merge.rb +4 -0
- data/lib/cfndsl/external_parameters.rb +4 -13
- data/lib/cfndsl/globals.rb +48 -9
- data/lib/cfndsl/jsonable.rb +22 -60
- data/lib/cfndsl/mappings.rb +1 -1
- data/lib/cfndsl/module.rb +16 -5
- data/lib/cfndsl/orchestration_template.rb +185 -83
- data/lib/cfndsl/outputs.rb +5 -1
- data/lib/cfndsl/parameters.rb +1 -1
- data/lib/cfndsl/plurals.rb +12 -1
- data/lib/cfndsl/properties.rb +1 -1
- data/lib/cfndsl/rake_task.rb +206 -12
- data/lib/cfndsl/ref_check.rb +19 -11
- data/lib/cfndsl/resources.rb +6 -19
- data/lib/cfndsl/rules.rb +1 -1
- data/lib/cfndsl/runner.rb +143 -0
- data/lib/cfndsl/specification.rb +80 -95
- data/lib/cfndsl/types.rb +205 -91
- data/lib/cfndsl/update_policy.rb +1 -1
- data/lib/cfndsl/version.rb +1 -1
- data/sample/autoscale.rb +0 -1
- data/sample/autoscale2.rb +0 -1
- data/sample/config_service.rb +2 -2
- data/sample/t1.rb +1 -1
- data/sample/vpc_example.rb +1 -1
- data/sample/vpc_with_vpn_example.rb +1 -1
- data/spec/aws/list_type_patches_spec.rb +35 -0
- data/spec/aws/nested_arrays_spec.rb +155 -3
- data/spec/aws/serverless_spec.rb +0 -2
- data/spec/cfndsl_spec.rb +94 -78
- data/spec/cli_spec.rb +16 -54
- data/spec/cloud_formation_template_spec.rb +233 -0
- data/spec/condition_spec.rb +24 -0
- data/spec/direct_ruby_spec.rb +19 -0
- data/spec/external_parameters_spec.rb +2 -15
- data/spec/fixtures/condition-assertion.json +1 -0
- data/spec/fixtures/test.rb +2 -1
- data/spec/generate_spec.rb +4 -2
- data/spec/resources_spec.rb +0 -7
- data/spec/spec_helper.rb +2 -7
- data/spec/support/shared_examples/orchestration_template.rb +15 -2
- data/spec/types_definition_spec.rb +3 -6
- metadata +52 -23
- data/bin/cfndsl +0 -160
- data/lib/cfndsl/errors.rb +0 -31
- data/lib/cfndsl/os/heat_template.rb +0 -18
- data/lib/cfndsl/os/types.rb +0 -14
- data/lib/cfndsl/os/types.yaml +0 -2423
- data/lib/cfndsl/patches.rb +0 -226
- data/lib/cfnlego.rb +0 -44
- data/spec/fixtures/heattest.rb +0 -24
- data/spec/heat_template_spec.rb +0 -7
data/lib/cfndsl/mappings.rb
CHANGED
data/lib/cfndsl/module.rb
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
4
|
-
|
3
|
+
require_relative 'plurals'
|
4
|
+
require_relative 'names'
|
5
5
|
|
6
6
|
# Adds some dsl module helpers
|
7
7
|
class Module
|
@@ -53,6 +53,7 @@ class Module
|
|
53
53
|
# on the main object, and the block is then evaluated in the context
|
54
54
|
# of the new object.
|
55
55
|
#
|
56
|
+
# rubocop:disable Metrics/MethodLength
|
56
57
|
def dsl_content_object(*symbols)
|
57
58
|
symbols.each do |symbol|
|
58
59
|
plural = CfnDsl::Plurals.pluralize(symbol) # @@plurals[symbol] || "#{symbol}s"
|
@@ -67,12 +68,22 @@ class Module
|
|
67
68
|
hash = {}
|
68
69
|
instance_variable_set(pluralvar, hash)
|
69
70
|
end
|
70
|
-
hash[name]
|
71
|
-
|
72
|
-
|
71
|
+
instance = hash[name]
|
72
|
+
|
73
|
+
if !instance
|
74
|
+
instance = definition_class.new(*values)
|
75
|
+
hash[name] = instance
|
76
|
+
elsif !instance.is_a?(definition_class)
|
77
|
+
raise ArgumentError, "#{method}(#{name}) already exists and is not a #{definition_class}"
|
78
|
+
elsif !values.empty?
|
79
|
+
raise ArgumentError, "wrong number of arguments (given #{values.size + 1}, expected 1) as #{method}(#{name}) already exists"
|
80
|
+
end
|
81
|
+
instance.instance_eval(&block) if block
|
82
|
+
return instance
|
73
83
|
end
|
74
84
|
end
|
75
85
|
end
|
76
86
|
end
|
87
|
+
# rubocop:enable Metrics/MethodLength
|
77
88
|
end
|
78
89
|
end
|
@@ -1,10 +1,22 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
3
|
+
require_relative 'globals'
|
4
|
+
require_relative 'module'
|
5
|
+
require_relative 'jsonable'
|
6
|
+
require_relative 'names'
|
7
|
+
require_relative 'plurals'
|
8
|
+
require_relative 'ref_check'
|
9
|
+
require_relative 'properties'
|
10
|
+
require_relative 'update_policy'
|
11
|
+
require_relative 'creation_policy'
|
12
|
+
require_relative 'conditions'
|
13
|
+
require_relative 'mappings'
|
14
|
+
require_relative 'resources'
|
15
|
+
require_relative 'rules'
|
16
|
+
require_relative 'parameters'
|
17
|
+
require_relative 'outputs'
|
18
|
+
|
19
|
+
require 'tsort'
|
8
20
|
|
9
21
|
module CfnDsl
|
10
22
|
# Handles the overall template object
|
@@ -34,7 +46,7 @@ module CfnDsl
|
|
34
46
|
|
35
47
|
abreve_name = parts.join('_')
|
36
48
|
if accessors.key? abreve_name
|
37
|
-
accessors
|
49
|
+
accessors[abreve_name] = :duplicate # Delete potentially ambiguous names
|
38
50
|
else
|
39
51
|
accessors[abreve_name] = type_module.const_get resource_name
|
40
52
|
types_mapping[abreve_name] = resource
|
@@ -42,17 +54,25 @@ module CfnDsl
|
|
42
54
|
parts.shift
|
43
55
|
end
|
44
56
|
end
|
45
|
-
accessors.each_pair { |acc, res| create_resource_accessor(acc, res, types_mapping[acc]) }
|
57
|
+
accessors.each_pair { |acc, res| create_resource_accessor(acc, res, types_mapping[acc]) unless res == :duplicate }
|
46
58
|
end
|
47
59
|
|
48
60
|
def create_resource_def(name, info)
|
49
|
-
resource = Class.new ResourceDefinition
|
61
|
+
resource = Class.new ResourceDefinition do
|
62
|
+
# do not allow Type to be respecified
|
63
|
+
def Type(type = nil)
|
64
|
+
return @Type unless type
|
65
|
+
raise CfnDsl::Error, "Cannot override previously defined Type #{@Type} with #{type}" unless type == @Type
|
66
|
+
|
67
|
+
super
|
68
|
+
end
|
69
|
+
end
|
50
70
|
resource_name = name.gsub(/::/, '_')
|
51
71
|
type_module.const_set(resource_name, resource)
|
52
72
|
info['Properties'].each_pair do |pname, ptype|
|
53
73
|
if ptype.is_a? Array
|
54
74
|
pclass = type_module.const_get ptype.first
|
55
|
-
create_array_property_def(resource, pname, pclass)
|
75
|
+
create_array_property_def(resource, pname, pclass, info)
|
56
76
|
else
|
57
77
|
pclass = type_module.const_get ptype
|
58
78
|
create_property_def(resource, pname, pclass)
|
@@ -61,9 +81,66 @@ module CfnDsl
|
|
61
81
|
resource_name
|
62
82
|
end
|
63
83
|
|
64
|
-
|
84
|
+
# rubocop:disable Metrics/PerceivedComplexity
|
85
|
+
def create_array_property_def(resource, pname, pclass, info)
|
86
|
+
singular_name = CfnDsl::Plurals.singularize pname
|
87
|
+
plural_name = singular_name == pname ? CfnDsl::Plurals.pluralize(pname) : pname
|
88
|
+
|
89
|
+
if singular_name == plural_name
|
90
|
+
# Generate the extended list concat method
|
91
|
+
plural_name = nil
|
92
|
+
elsif pname == plural_name && info['Properties'].include?(singular_name)
|
93
|
+
# The singlular name is a different property, do not redefine it here but rather use the extended form
|
94
|
+
# with the plural name. This allows construction of deep types, but no mechanism to overwrite a previous value
|
95
|
+
# (eg CodePipeline::Pipeline ArtifactStores vs ArtifactStore)
|
96
|
+
# Note is is also possible (but unlikely) for the spec to change in a way that triggers this condition where it did not
|
97
|
+
# before which will result in breaking behaviour for existing apps.
|
98
|
+
singular_name = plural_name
|
99
|
+
plural_name = nil
|
100
|
+
elsif pname == singular_name && info['Properties'].include?(plural_name)
|
101
|
+
# The plural name is a different property, do not redefine it here
|
102
|
+
# Note it is unlikely that a singular form is going to be a List property if the plural form also exists.
|
103
|
+
plural_name = singular_name
|
104
|
+
end
|
105
|
+
|
106
|
+
# Plural form just a normal property definition expecting an Array type
|
107
|
+
create_property_def(resource, pname, Array, plural_name) if plural_name
|
108
|
+
|
109
|
+
# Singular form understands concatenation and Fn::If property
|
110
|
+
create_singular_property_def(resource, pname, pclass, singular_name) if singular_name
|
111
|
+
end
|
112
|
+
# rubocop:enable Metrics/PerceivedComplexity
|
113
|
+
|
114
|
+
def create_resource_accessor(accessor, resource, type)
|
115
|
+
class_eval do
|
116
|
+
CfnDsl.method_names(accessor) do |method|
|
117
|
+
define_method(method) do |name, *values, &block|
|
118
|
+
name = name.to_s
|
119
|
+
@Resources ||= {}
|
120
|
+
instance = @Resources[name]
|
121
|
+
if !instance
|
122
|
+
instance = resource.new(*values)
|
123
|
+
# Previously the type was set after the block was evaled
|
124
|
+
# But now trying to reset Type on a specific subtype will raise exception
|
125
|
+
instance.instance_variable_set('@Type', type)
|
126
|
+
@Resources[name] = instance
|
127
|
+
elsif type != (other_type = instance.instance_variable_get('@Type'))
|
128
|
+
raise ArgumentError, "Resource #{name}<#{other_type}> exists, and is not a <#{type}>"
|
129
|
+
elsif !values.empty?
|
130
|
+
raise ArgumentError, "wrong number of arguments (given #{values.size + 1}, expected 1) as Resource #{name} already exists"
|
131
|
+
end
|
132
|
+
@Resources[name].instance_eval(&block) if block
|
133
|
+
instance
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
private
|
140
|
+
|
141
|
+
def create_property_def(resource, pname, pclass, method_name = pname)
|
65
142
|
resource.class_eval do
|
66
|
-
CfnDsl.method_names(
|
143
|
+
CfnDsl.method_names(method_name) do |method|
|
67
144
|
define_method(method) do |*values, &block|
|
68
145
|
values.push pclass.new if values.empty?
|
69
146
|
@Properties ||= {}
|
@@ -75,114 +152,139 @@ module CfnDsl
|
|
75
152
|
end
|
76
153
|
end
|
77
154
|
|
78
|
-
def
|
79
|
-
create_property_def(resource, pname, Array)
|
80
|
-
|
81
|
-
sname = CfnDsl::Plurals.singularize pname
|
82
|
-
|
83
|
-
return if sname == pname
|
84
|
-
|
155
|
+
def create_singular_property_def(resource, pname, pclass, singular_name)
|
85
156
|
resource.class_eval do
|
86
|
-
CfnDsl.method_names(
|
87
|
-
define_method(method) do |value = nil, &block|
|
157
|
+
CfnDsl.method_names(singular_name) do |method|
|
158
|
+
define_method(method) do |value = nil, fn_if: nil, **hash_value, &block|
|
159
|
+
value = hash_value unless value || hash_value.empty?
|
88
160
|
@Properties ||= {}
|
89
161
|
@Properties[pname] ||= PropertyDefinition.new([])
|
90
|
-
value
|
91
|
-
|
92
|
-
|
162
|
+
if value.is_a?(Array)
|
163
|
+
@Properties[pname].value.concat(value)
|
164
|
+
else
|
165
|
+
value ||= pclass.new
|
166
|
+
@Properties[pname].value.push fn_if ? FnIf(fn_if, value, Ref('AWS::NoValue')) : value
|
167
|
+
value.instance_eval(&block) if block
|
168
|
+
end
|
93
169
|
value
|
94
170
|
end
|
95
171
|
end
|
96
172
|
end
|
97
173
|
end
|
174
|
+
end
|
98
175
|
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
176
|
+
def initialize(description = nil, &block)
|
177
|
+
@AWSTemplateFormatVersion = '2010-09-09'
|
178
|
+
@Description = description if description
|
179
|
+
declare(&block) if block_given?
|
180
|
+
end
|
181
|
+
|
182
|
+
alias _Condition Condition
|
183
|
+
|
184
|
+
# Condition has two usages at this level
|
185
|
+
# @overload Condition(name,expression)
|
186
|
+
# @overload Condition(name) - referencing a condition in a condition expression
|
187
|
+
def Condition(name, expression = nil)
|
188
|
+
if expression
|
189
|
+
_Condition(name, expression)
|
190
|
+
else
|
191
|
+
{ Condition: ConditionDefinition.new(name) }
|
112
192
|
end
|
113
193
|
end
|
114
194
|
|
115
|
-
def
|
116
|
-
|
195
|
+
def check_refs
|
196
|
+
invalids = check_condition_refs + check_resource_refs + check_output_refs + check_rule_refs
|
197
|
+
invalids unless invalids.empty?
|
117
198
|
end
|
118
199
|
|
119
|
-
|
120
|
-
def valid_ref?(ref, origin = nil)
|
200
|
+
def valid_ref?(ref, ref_containers = [GLOBAL_REFS, @Resources, @Parameters])
|
121
201
|
ref = ref.to_s
|
122
|
-
|
202
|
+
ref_containers.any? { |c| c && c.key?(ref) }
|
203
|
+
end
|
123
204
|
|
124
|
-
|
205
|
+
def check_condition_refs
|
206
|
+
invalids = []
|
125
207
|
|
126
|
-
|
208
|
+
# Conditions can refer to other conditions in Fn::And, Fn::Or and Fn::Not
|
209
|
+
invalids.concat(_check_refs(:Condition, :condition_refs, [@Conditions]))
|
127
210
|
|
128
|
-
|
211
|
+
# They can also Ref Globals and Parameters (but not Resources))
|
212
|
+
invalids.concat(_check_refs(:Condition, :all_refs, [GLOBAL_REFS, @Parameters]))
|
213
|
+
end
|
129
214
|
|
130
|
-
|
215
|
+
def check_resource_refs
|
216
|
+
invalids = []
|
217
|
+
invalids.concat(_check_refs(:Resource, :all_refs, [@Resources, GLOBAL_REFS, @Parameters]))
|
218
|
+
|
219
|
+
# DependsOn and conditions in Fn::If expressions
|
220
|
+
invalids.concat(_check_refs(:Resource, :condition_refs, [@Conditions]))
|
131
221
|
end
|
132
|
-
# rubocop:enable Metrics/PerceivedComplexity
|
133
222
|
|
134
|
-
def
|
135
|
-
invalids =
|
136
|
-
invalids
|
223
|
+
def check_output_refs
|
224
|
+
invalids = []
|
225
|
+
invalids.concat(_check_refs(:Output, :all_refs, [@Resources, GLOBAL_REFS, @Parameters]))
|
226
|
+
invalids.concat(_check_refs(:Output, :condition_refs, [@Conditions]))
|
137
227
|
end
|
138
228
|
|
139
|
-
def
|
229
|
+
def check_rule_refs
|
140
230
|
invalids = []
|
141
|
-
@
|
142
|
-
|
143
|
-
@Resources.each_key do |resource|
|
144
|
-
@_resource_refs[resource.to_s] = @Resources[resource].build_references({})
|
145
|
-
end
|
146
|
-
@_resource_refs.each_key do |origin|
|
147
|
-
@_resource_refs[origin].each_key do |ref|
|
148
|
-
invalids.push "Invalid Reference: Resource #{origin} refers to #{ref}" unless valid_ref?(ref, origin)
|
149
|
-
end
|
150
|
-
end
|
151
|
-
end
|
231
|
+
invalids.concat(_check_refs(:Rule, :all_refs, [@Resources, GLOBAL_REFS, @Parameters]))
|
232
|
+
invalids.concat(_check_refs(:Rule, :condition_refs, [@Conditions]))
|
152
233
|
invalids
|
153
234
|
end
|
154
235
|
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
output_refs.each_key do |origin|
|
163
|
-
output_refs[origin].each_key do |ref|
|
164
|
-
invalids.push "Invalid Reference: Output #{origin} refers to #{ref}" unless valid_ref?(ref)
|
165
|
-
end
|
166
|
-
end
|
236
|
+
# For testing for cycles
|
237
|
+
class RefHash < Hash
|
238
|
+
include TSort
|
239
|
+
|
240
|
+
alias tsort_each_node each_key
|
241
|
+
def tsort_each_child(node, &block)
|
242
|
+
fetch(node, []).each(&block)
|
167
243
|
end
|
168
|
-
invalids
|
169
244
|
end
|
170
245
|
|
171
|
-
|
246
|
+
# rubocop:disable Metrics/PerceivedComplexity
|
247
|
+
def _check_refs(container_name, method, source_containers)
|
248
|
+
container = instance_variable_get("@#{container_name}s")
|
249
|
+
return [] unless container
|
250
|
+
|
172
251
|
invalids = []
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
252
|
+
referred_by = RefHash.new { |h, k| h[k] = [] }
|
253
|
+
self_check = source_containers.first.eql?(container)
|
254
|
+
|
255
|
+
container.each_pair do |name, entry|
|
256
|
+
name = name.to_s
|
257
|
+
begin
|
258
|
+
refs = entry.build_references([], self_check && name, method)
|
259
|
+
refs.each { |r| referred_by[r.to_s] << name }
|
260
|
+
rescue RefCheck::SelfReference, RefCheck::NullReference => e
|
261
|
+
# Topological sort will not detect self or null references
|
262
|
+
invalids.push("#{container_name} #{e.message}")
|
177
263
|
end
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
264
|
+
end
|
265
|
+
|
266
|
+
referred_by.each_pair do |ref, names|
|
267
|
+
unless valid_ref?(ref, source_containers)
|
268
|
+
invalids.push "Invalid Reference: #{container_name}s #{names} refer to unknown #{method == :condition_refs ? 'Condition' : 'Reference'} #{ref}"
|
182
269
|
end
|
183
270
|
end
|
271
|
+
|
272
|
+
begin
|
273
|
+
referred_by.tsort if self_check && invalids.empty? # Check for cycles
|
274
|
+
rescue TSort::Cyclic => e
|
275
|
+
invalids.push "Cyclic references found in #{container_name}s #{referred_by} - #{e.message}"
|
276
|
+
end
|
277
|
+
|
184
278
|
invalids
|
185
279
|
end
|
280
|
+
# rubocop:enable Metrics/PerceivedComplexity
|
281
|
+
|
282
|
+
def validate
|
283
|
+
errors = check_refs || []
|
284
|
+
raise CfnDsl::Error, "#{errors.size} errors in template\n#{errors.join("\n")}" unless errors.empty?
|
285
|
+
|
286
|
+
self
|
287
|
+
end
|
186
288
|
end
|
187
289
|
# rubocop:enable Metrics/ClassLength
|
188
290
|
end
|
data/lib/cfndsl/outputs.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
3
|
+
require_relative 'jsonable'
|
4
4
|
|
5
5
|
module CfnDsl
|
6
6
|
# Handles Output objects
|
@@ -14,5 +14,9 @@ module CfnDsl
|
|
14
14
|
def initialize(value = nil)
|
15
15
|
@Value = value if value
|
16
16
|
end
|
17
|
+
|
18
|
+
def condition_refs
|
19
|
+
[@Condition].flatten.compact.map(&:to_s)
|
20
|
+
end
|
17
21
|
end
|
18
22
|
end
|
data/lib/cfndsl/parameters.rb
CHANGED
data/lib/cfndsl/plurals.rb
CHANGED
@@ -24,7 +24,18 @@ module CfnDsl
|
|
24
24
|
end
|
25
25
|
|
26
26
|
def singularize(name)
|
27
|
-
@singles.fetch(name.to_s)
|
27
|
+
@singles.fetch(name.to_s) do |key|
|
28
|
+
case key
|
29
|
+
when /List$/
|
30
|
+
key[0..-5]
|
31
|
+
when /ies$/
|
32
|
+
key[0..-4] + 'y'
|
33
|
+
when /s$/
|
34
|
+
key[0..-2]
|
35
|
+
else
|
36
|
+
key
|
37
|
+
end
|
38
|
+
end
|
28
39
|
end
|
29
40
|
end
|
30
41
|
end
|