cfndsl 0.4.4 → 0.5.0.pre
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +5 -13
- data/.rubocop.yml +23 -0
- data/Gemfile +4 -0
- data/Rakefile +19 -17
- data/bin/cfndsl +20 -20
- data/cfndsl.gemspec +16 -15
- data/lib/cfndsl.rb +62 -68
- data/lib/cfndsl/aws/cloud_formation_template.rb +16 -0
- data/lib/cfndsl/aws/types.rb +12 -0
- data/lib/cfndsl/{aws_types.yaml → aws/types.yaml} +0 -0
- data/lib/cfndsl/{Conditions.rb → conditions.rb} +5 -7
- data/lib/cfndsl/creation_policy.rb +21 -0
- data/lib/cfndsl/errors.rb +29 -0
- data/lib/cfndsl/generate_types.rb +154 -0
- data/lib/cfndsl/jsonable.rb +214 -0
- data/lib/cfndsl/mappings.rb +23 -0
- data/lib/cfndsl/metadata.rb +16 -0
- data/lib/cfndsl/module.rb +52 -51
- data/lib/cfndsl/names.rb +5 -5
- data/lib/cfndsl/orchestration_template.rb +173 -0
- data/lib/cfndsl/os/heat_template.rb +16 -0
- data/lib/cfndsl/os/types.rb +12 -0
- data/lib/cfndsl/{os_types.yaml → os/types.yaml} +11 -11
- data/lib/cfndsl/{Outputs.rb → outputs.rb} +3 -4
- data/lib/cfndsl/{Parameters.rb → parameters.rb} +12 -13
- data/lib/cfndsl/plurals.rb +34 -0
- data/lib/cfndsl/properties.rb +21 -0
- data/lib/cfndsl/rake_task.rb +9 -7
- data/lib/cfndsl/ref_check.rb +44 -0
- data/lib/cfndsl/{Resources.rb → resources.rb} +13 -15
- data/lib/cfndsl/types.rb +151 -0
- data/lib/cfndsl/update_policy.rb +25 -0
- data/lib/cfndsl/version.rb +1 -1
- data/sample/autoscale.rb +152 -158
- data/sample/autoscale2.rb +151 -155
- data/sample/circular.rb +30 -33
- data/sample/codedeploy.rb +35 -36
- data/sample/config_service.rb +120 -0
- data/sample/ecs.rb +39 -39
- data/sample/iam_policies.rb +82 -0
- data/sample/lambda.rb +20 -24
- data/sample/s3.rb +11 -11
- data/sample/t1.rb +7 -9
- data/sample/vpc_example.rb +50 -0
- data/sample/vpc_with_vpn_example.rb +97 -0
- data/spec/cfndsl_spec.rb +22 -11
- data/spec/fixtures/heattest.rb +13 -14
- data/spec/fixtures/test.rb +56 -53
- metadata +36 -30
- data/lib/cfndsl/CloudFormationTemplate.rb +0 -267
- data/lib/cfndsl/CreationPolicy.rb +0 -25
- data/lib/cfndsl/Errors.rb +0 -31
- data/lib/cfndsl/JSONable.rb +0 -235
- data/lib/cfndsl/Mappings.rb +0 -25
- data/lib/cfndsl/Metadata.rb +0 -22
- data/lib/cfndsl/Plurals.rb +0 -35
- data/lib/cfndsl/Properties.rb +0 -25
- data/lib/cfndsl/RefCheck.rb +0 -48
- data/lib/cfndsl/Types.rb +0 -309
- data/lib/cfndsl/UpdatePolicy.rb +0 -29
- data/sample/config-service.rb +0 -119
- data/sample/iam-policies.rb +0 -82
- data/sample/vpc-example.rb +0 -51
- data/sample/vpc-with-vpn-example.rb +0 -97
checksums.yaml
CHANGED
@@ -1,15 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
|
5
|
-
data.tar.gz: !binary |-
|
6
|
-
MmNiN2IyZjZiMTFhOWFmYmIxZWJkNjExZTFiZDdkNjhkNWIxYjMxYw==
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 0c3d2ac765697ae526e12354216d7c45845e07b0
|
4
|
+
data.tar.gz: 7ea0a5bb2eb5d16a611165bde335400d6fbdf29b
|
7
5
|
SHA512:
|
8
|
-
metadata.gz:
|
9
|
-
|
10
|
-
NjJmNWU3MDc5ZjZiOWRhY2I2OWVjN2U1NjMzOGVkNWZjNzY1ZDVkZjNlZWJk
|
11
|
-
ZTlkMDg3NjVlMDI4MmYzYzdlNjhjODNhNzA5Y2FjNGQ1NWVjZWE=
|
12
|
-
data.tar.gz: !binary |-
|
13
|
-
MzQyNDEzZDliYjEwZTc4ZWMwYjkzNjczZTkyNzBlYTk5MjM4YTIyNGI3NTEw
|
14
|
-
NTgwMjY2MTI4OWQ4MWVhZmNiMzNmMWYyMTk2ODZiOTY3OWNlNGE1MmZhODc3
|
15
|
-
YTc2YTY2MmI1ZDkyZWYzZmZlZDkxOTU2ZmQzNjZkMjVmNWRmMjA=
|
6
|
+
metadata.gz: efac56f41113e2ba016d176eeb23c5bf177d57c2ec22008eb029adea4f990d0ddcc975d13e87277efb07d192e227e76559b2bf567f04408e9641a432ba60c753
|
7
|
+
data.tar.gz: 27ed0952f74bf9841485d5236208b4315820bee91e737bbfa55d823281bc543245cb77e2ff6bd7f7a4fabb18f5472c6d13a0d023235da86d6677baa49958b340
|
data/.rubocop.yml
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
Metrics/LineLength:
|
2
|
+
Max: 160
|
3
|
+
|
4
|
+
Metrics/CyclomaticComplexity:
|
5
|
+
Max: 10
|
6
|
+
|
7
|
+
Metrics/AbcSize:
|
8
|
+
Max: 25
|
9
|
+
|
10
|
+
Metrics/MethodLength:
|
11
|
+
Max: 25
|
12
|
+
|
13
|
+
# Due to our @Properties style instance names
|
14
|
+
Style/VariableName:
|
15
|
+
Enabled: false
|
16
|
+
|
17
|
+
# We are a DSL
|
18
|
+
Style/MethodName:
|
19
|
+
Enabled: false
|
20
|
+
|
21
|
+
# Lone String
|
22
|
+
Lint/Void:
|
23
|
+
Enabled: false
|
data/Gemfile
CHANGED
data/Rakefile
CHANGED
@@ -1,24 +1,26 @@
|
|
1
|
-
require
|
2
|
-
require
|
3
|
-
require
|
1
|
+
require 'bundler/gem_tasks'
|
2
|
+
require 'rspec/core/rake_task'
|
3
|
+
require 'cfndsl/version'
|
4
|
+
require 'rubocop/rake_task'
|
4
5
|
|
5
6
|
RSpec::Core::RakeTask.new
|
7
|
+
RuboCop::RakeTask.new
|
6
8
|
|
7
|
-
task default: [:spec]
|
9
|
+
task default: [:spec, :rubocop]
|
8
10
|
|
9
|
-
task :bump, :type do |
|
11
|
+
task :bump, :type do |_, args|
|
10
12
|
type = args[:type].downcase
|
11
|
-
version_path =
|
13
|
+
version_path = 'lib/cfndsl/version.rb'
|
12
14
|
|
13
|
-
types = %w
|
15
|
+
types = %w(major minor patch)
|
14
16
|
|
15
|
-
|
17
|
+
raise unless types.include?(type)
|
16
18
|
|
17
|
-
if `git rev-parse --abbrev-ref HEAD`.strip !=
|
18
|
-
|
19
|
+
if `git rev-parse --abbrev-ref HEAD`.strip != 'master'
|
20
|
+
raise "Looks like you're trying to create a release in a branch, you can only create one in 'master'"
|
19
21
|
end
|
20
22
|
|
21
|
-
version_segments = CfnDsl::VERSION.split(
|
23
|
+
version_segments = CfnDsl::VERSION.split('.').map(&:to_i)
|
22
24
|
|
23
25
|
segment_index = types.find_index type
|
24
26
|
|
@@ -26,7 +28,7 @@ task :bump, :type do |t, args|
|
|
26
28
|
[version_segments.at(segment_index).succ] +
|
27
29
|
[0] * version_segments.drop(segment_index.succ).count
|
28
30
|
|
29
|
-
version = version_segments.join(
|
31
|
+
version = version_segments.join('.')
|
30
32
|
|
31
33
|
puts "Bumping gem from version #{CfnDsl::VERSION} to #{version} as a '#{type.capitalize}' release"
|
32
34
|
|
@@ -34,18 +36,18 @@ task :bump, :type do |t, args|
|
|
34
36
|
updated_contents = contents.gsub(/([0-9\.]+)/, version)
|
35
37
|
File.write(version_path, updated_contents)
|
36
38
|
|
37
|
-
puts
|
39
|
+
puts 'Commiting version update'
|
38
40
|
`git add #{version_path}`
|
39
41
|
`git commit --message='#{type.capitalize} release #{version}'`
|
40
42
|
|
41
|
-
puts
|
43
|
+
puts 'Tagging release'
|
42
44
|
`git tag -a v#{version} -m 'Version #{version}'`
|
43
45
|
|
44
|
-
puts
|
46
|
+
puts 'Pushing branch'
|
45
47
|
`git push origin master`
|
46
48
|
|
47
|
-
puts
|
49
|
+
puts 'Pushing tag'
|
48
50
|
`git push origin v#{version}`
|
49
51
|
|
50
|
-
puts
|
52
|
+
puts 'All done, travis should pick up and release the gem now!'
|
51
53
|
end
|
data/bin/cfndsl
CHANGED
@@ -6,51 +6,51 @@ require 'json'
|
|
6
6
|
|
7
7
|
options = {}
|
8
8
|
|
9
|
-
optparse = OptionParser.new do|opts|
|
10
|
-
opts.banner =
|
9
|
+
optparse = OptionParser.new do |opts|
|
10
|
+
opts.banner = 'Usage: cfndsl [options] FILE'
|
11
11
|
|
12
12
|
# Define the options, and what they do
|
13
13
|
options[:output] = '-'
|
14
|
-
opts.on(
|
14
|
+
opts.on('-o', '--output FILE', 'Write output to file') do |file|
|
15
15
|
options[:output] = file
|
16
16
|
end
|
17
17
|
|
18
18
|
options[:extras] = []
|
19
|
-
opts.on(
|
20
|
-
options[:extras].push([:yaml,File.expand_path(file)])
|
19
|
+
opts.on('-y', '--yaml FILE', 'Import yaml file as local variables') do |file|
|
20
|
+
options[:extras].push([:yaml, File.expand_path(file)])
|
21
21
|
end
|
22
22
|
|
23
|
-
opts.on(
|
24
|
-
options[:extras].push([:ruby,File.expand_path(file)])
|
23
|
+
opts.on('-r', '--ruby FILE', 'Evaluate ruby file before template') do |file|
|
24
|
+
options[:extras].push([:ruby, File.expand_path(file)])
|
25
25
|
end
|
26
26
|
|
27
|
-
opts.on(
|
28
|
-
options[:extras].push([:json,File.expand_path(file)])
|
27
|
+
opts.on('-j', '--json FILE', 'Import json file as local variables') do |file|
|
28
|
+
options[:extras].push([:json, File.expand_path(file)])
|
29
29
|
end
|
30
30
|
|
31
|
-
opts.on(
|
31
|
+
opts.on('-p', '--pretty', 'Pretty-format output JSON') do
|
32
32
|
options[:pretty] = true
|
33
33
|
end
|
34
34
|
|
35
|
-
opts.on(
|
36
|
-
options[:extras].push([:raw,file])
|
35
|
+
opts.on('-D', '--define "VARIABLE=VALUE"', 'Directly set local VARIABLE as VALUE') do |file|
|
36
|
+
options[:extras].push([:raw, file])
|
37
37
|
end
|
38
38
|
|
39
39
|
options[:verbose] = false
|
40
|
-
opts.on('-v', '--verbose',
|
40
|
+
opts.on('-v', '--verbose', 'Turn on verbose ouptut') do
|
41
41
|
options[:verbose] = true
|
42
42
|
end
|
43
43
|
|
44
44
|
# This displays the help screen, all programs are
|
45
45
|
# assumed to have this option.
|
46
|
-
opts.on(
|
46
|
+
opts.on('-h', '--help', 'Display this screen') do
|
47
47
|
puts opts
|
48
48
|
exit
|
49
49
|
end
|
50
50
|
end
|
51
51
|
|
52
52
|
optparse.parse!
|
53
|
-
unless ARGV[0]
|
53
|
+
unless ARGV[0]
|
54
54
|
puts optparse.help
|
55
55
|
exit(1)
|
56
56
|
end
|
@@ -58,14 +58,14 @@ end
|
|
58
58
|
filename = File.expand_path(ARGV[0])
|
59
59
|
verbose = options[:verbose] && STDERR
|
60
60
|
|
61
|
-
model = CfnDsl
|
61
|
+
model = CfnDsl.eval_file_with_extras(filename, options[:extras], verbose)
|
62
62
|
|
63
63
|
output = STDOUT
|
64
|
-
if options[:output] != '-'
|
64
|
+
if options[:output] != '-'
|
65
65
|
verbose.puts("Writing to #{options[:output]}") if verbose
|
66
|
-
output = File.open(
|
67
|
-
|
68
|
-
verbose.puts(
|
66
|
+
output = File.open(File.expand_path(options[:output]), 'w')
|
67
|
+
elsif verbose
|
68
|
+
verbose.puts('Writing to STDOUT')
|
69
69
|
end
|
70
70
|
|
71
71
|
output.puts options[:pretty] ? JSON.pretty_generate(model) : model.to_json
|
data/cfndsl.gemspec
CHANGED
@@ -1,21 +1,22 @@
|
|
1
1
|
# coding: utf-8
|
2
|
-
lib = File.expand_path(
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
3
|
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
-
require
|
4
|
+
require 'cfndsl/version'
|
5
5
|
|
6
6
|
Gem::Specification.new do |s|
|
7
|
-
s.name
|
8
|
-
s.version
|
9
|
-
s.summary
|
10
|
-
s.description
|
11
|
-
s.authors
|
12
|
-
s.email
|
13
|
-
s.files
|
14
|
-
s.
|
15
|
-
s.
|
16
|
-
s.
|
17
|
-
s.
|
18
|
-
s.require_paths = ["lib"]
|
7
|
+
s.name = 'cfndsl'
|
8
|
+
s.version = CfnDsl::VERSION
|
9
|
+
s.summary = 'AWS Cloudformation DSL'
|
10
|
+
s.description = 'DSL for creating AWS Cloudformation templates'
|
11
|
+
s.authors = ['Steven Jack', 'Chris Howe']
|
12
|
+
s.email = ['stevenmajack@gmail.com', 'chris@howeville.com']
|
13
|
+
s.files = `git ls-files`.split($INPUT_RECORD_SEPARATOR)
|
14
|
+
s.homepage = 'https://github.com/stevenjack/cfndsl'
|
15
|
+
s.license = 'MIT'
|
16
|
+
s.test_files = s.files.grep(%r{^(test|spec|features)/})
|
17
|
+
s.require_paths = ['lib']
|
19
18
|
|
20
|
-
s.
|
19
|
+
s.executables << 'cfndsl'
|
20
|
+
|
21
|
+
s.add_development_dependency 'bundler'
|
21
22
|
end
|
data/lib/cfndsl.rb
CHANGED
@@ -1,70 +1,64 @@
|
|
1
|
-
require 'json'
|
1
|
+
require 'json'
|
2
2
|
|
3
3
|
require 'cfndsl/module'
|
4
|
-
require 'cfndsl/
|
5
|
-
require 'cfndsl/
|
6
|
-
require 'cfndsl/
|
7
|
-
require 'cfndsl/
|
8
|
-
require 'cfndsl/
|
9
|
-
require 'cfndsl/
|
10
|
-
require 'cfndsl/
|
11
|
-
require 'cfndsl/
|
12
|
-
require 'cfndsl/
|
13
|
-
require 'cfndsl/
|
14
|
-
require 'cfndsl/
|
15
|
-
require 'cfndsl/
|
16
|
-
require 'cfndsl/
|
17
|
-
require 'cfndsl/
|
4
|
+
require 'cfndsl/errors'
|
5
|
+
require 'cfndsl/ref_check'
|
6
|
+
require 'cfndsl/jsonable'
|
7
|
+
require 'cfndsl/properties'
|
8
|
+
require 'cfndsl/update_policy'
|
9
|
+
require 'cfndsl/creation_policy'
|
10
|
+
require 'cfndsl/conditions'
|
11
|
+
require 'cfndsl/mappings'
|
12
|
+
require 'cfndsl/resources'
|
13
|
+
require 'cfndsl/metadata'
|
14
|
+
require 'cfndsl/parameters'
|
15
|
+
require 'cfndsl/outputs'
|
16
|
+
require 'cfndsl/aws/cloud_formation_template'
|
17
|
+
require 'cfndsl/os/heat_template'
|
18
18
|
|
19
|
+
# CfnDsl
|
19
20
|
module CfnDsl
|
21
|
+
# This function handles the eval of the template file and returns the
|
22
|
+
# results. It does this with a ruby "eval", but it builds up a customized
|
23
|
+
# binding environment before it calls eval. The environment can be
|
24
|
+
# customized by passing a list of customizations in the extras parameter.
|
25
|
+
#
|
26
|
+
# These customizations are expressed as an array of pairs of
|
27
|
+
# (type,filename). They are evaluated in the order they appear in the
|
28
|
+
# extras array. The types are as follows
|
29
|
+
#
|
30
|
+
# :yaml - the second element is treated as a file name, which is loaded
|
31
|
+
# as a yaml file. The yaml file should contain a top level
|
32
|
+
# dictionary. Each of the keys of the top level dictionary is
|
33
|
+
# used as a local variable in the evalation context.
|
34
|
+
#
|
35
|
+
# :json - the second element is treated as a file name, which is loaded
|
36
|
+
# as a json file. The yaml file should contain a top level
|
37
|
+
# dictionary. Each of the keys of the top level dictionary is
|
38
|
+
# used as a local variable in the evalation context.
|
39
|
+
#
|
40
|
+
# :ruby - the second element is treated as a file name which is evaluated
|
41
|
+
# as a ruby file. Any assigments (or other binding affecting
|
42
|
+
# side effects) will persist into the context where the template
|
43
|
+
# is evaluated
|
44
|
+
#
|
45
|
+
# :raw - the seccond elements is treated as a ruby statement and is
|
46
|
+
# evaluated in the binding context, similar to the contents of
|
47
|
+
# a ruby file.
|
48
|
+
#
|
49
|
+
# Note that the order is important, as later extra sections can overwrite
|
50
|
+
# or even undo things that were done by earlier sections.
|
20
51
|
def self.eval_file_with_extras(filename, extras = [], logstream = nil)
|
21
|
-
# This function handles the eval of the template file and returns the
|
22
|
-
# results. It does this with a ruby "eval", but it builds up a customized
|
23
|
-
# binding environment before it calls eval. The environment can be
|
24
|
-
# customized by passing a list of customizations in the extras parameter.
|
25
|
-
#
|
26
|
-
# These customizations are expressed as an array of pairs of
|
27
|
-
# (type,filename). They are evaluated in the order they appear in the
|
28
|
-
# extras array. The types are as follows
|
29
|
-
#
|
30
|
-
# :yaml - the second element is treated as a file name, which is loaded
|
31
|
-
# as a yaml file. The yaml file should contain a top level
|
32
|
-
# dictionary. Each of the keys of the top level dictionary is
|
33
|
-
# used as a local variable in the evalation context.
|
34
|
-
#
|
35
|
-
# :json - the second element is treated as a file name, which is loaded
|
36
|
-
# as a json file. The yaml file should contain a top level
|
37
|
-
# dictionary. Each of the keys of the top level dictionary is
|
38
|
-
# used as a local variable in the evalation context.
|
39
|
-
#
|
40
|
-
# :ruby - the second element is treated as a file name which is evaluated
|
41
|
-
# as a ruby file. Any assigments (or other binding affecting
|
42
|
-
# side effects) will persist into the context where the template
|
43
|
-
# is evaluated
|
44
|
-
#
|
45
|
-
# :raw - the seccond elements is treated as a ruby statement and is
|
46
|
-
# evaluated in the binding context, similar to the contents of
|
47
|
-
# a ruby file.
|
48
|
-
#
|
49
|
-
# Note that the order is important, as later extra sections can overwrite
|
50
|
-
# or even undo things that were done by earlier sections.
|
51
|
-
|
52
52
|
b = binding
|
53
53
|
extras.each do |pair|
|
54
|
-
type,file = pair
|
54
|
+
type, file = pair
|
55
55
|
case type
|
56
|
-
when :yaml
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
end
|
63
|
-
|
64
|
-
when :json
|
65
|
-
logstream.puts("Loading YAML file #{file}") if logstream
|
66
|
-
parameters = JSON.load(File.read(file))
|
67
|
-
parameters.each do |k,v|
|
56
|
+
when :yaml, :json
|
57
|
+
klass_name = type.to_s.upcase
|
58
|
+
klass = Object.const_get(klass_name)
|
59
|
+
logstream.puts("Loading #{klass_name} file #{file}") if logstream
|
60
|
+
parameters = klass.load(File.read(file))
|
61
|
+
parameters.each do |k, v|
|
68
62
|
logstream.puts("Setting local variable #{k} to #{v}") if logstream
|
69
63
|
b.eval("#{k} = #{v.inspect}")
|
70
64
|
end
|
@@ -75,23 +69,24 @@ module CfnDsl
|
|
75
69
|
|
76
70
|
when :raw
|
77
71
|
logstream.puts("Running raw ruby code #{file}") if logstream
|
78
|
-
b.eval(file,
|
72
|
+
b.eval(file, 'raw code')
|
79
73
|
end
|
80
74
|
end
|
81
75
|
|
82
76
|
logstream.puts("Loading template file #{filename}") if logstream
|
83
77
|
model = b.eval(File.read(filename), filename)
|
84
|
-
|
78
|
+
|
79
|
+
model
|
85
80
|
end
|
86
81
|
end
|
87
82
|
|
88
83
|
def CloudFormation(&block)
|
89
84
|
x = CfnDsl::CloudFormationTemplate.new
|
90
85
|
x.declare(&block)
|
91
|
-
invalid_references = x.
|
92
|
-
if
|
86
|
+
invalid_references = x.check_refs
|
87
|
+
if invalid_references
|
93
88
|
abort invalid_references.join("\n")
|
94
|
-
elsif
|
89
|
+
elsif CfnDsl::Errors.errors?
|
95
90
|
abort CfnDsl::Errors.errors.join("\n")
|
96
91
|
else
|
97
92
|
return x
|
@@ -101,13 +96,12 @@ end
|
|
101
96
|
def Heat(&block)
|
102
97
|
x = CfnDsl::HeatTemplate.new
|
103
98
|
x.declare(&block)
|
104
|
-
invalid_references = x.
|
105
|
-
if
|
99
|
+
invalid_references = x.check_refs
|
100
|
+
if invalid_references
|
106
101
|
abort invalid_references.join("\n")
|
107
|
-
elsif
|
102
|
+
elsif CfnDsl::Errors.errors?
|
108
103
|
abort CfnDsl::Errors.errors.join("\n")
|
109
104
|
else
|
110
105
|
return x
|
111
106
|
end
|
112
107
|
end
|
113
|
-
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'cfndsl/orchestration_template'
|
2
|
+
|
3
|
+
module CfnDsl
|
4
|
+
# Cloud Formation Templates
|
5
|
+
class CloudFormationTemplate < OrchestrationTemplate
|
6
|
+
def self.template_types
|
7
|
+
CfnDsl::AWS::Types::Types_Internal
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.type_module
|
11
|
+
CfnDsl::AWS::Types
|
12
|
+
end
|
13
|
+
|
14
|
+
create_types
|
15
|
+
end
|
16
|
+
end
|