nugrant 2.0.0.dev2 → 2.0.0.pre1
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 +6 -14
- data/.gitignore +2 -1
- data/.travis.yml +2 -2
- data/CHANGELOG.md +148 -3
- data/Gemfile +8 -20
- data/README.md +266 -72
- data/Rakefile +1 -0
- data/lib/nugrant.rb +14 -6
- data/lib/nugrant/bag.rb +116 -62
- data/lib/nugrant/helper/bag.rb +19 -19
- data/lib/nugrant/helper/env/exporter.rb +208 -0
- data/lib/nugrant/helper/env/namer.rb +47 -0
- data/lib/nugrant/helper/parameters.rb +12 -0
- data/lib/nugrant/helper/stack.rb +86 -0
- data/lib/nugrant/mixin/parameters.rb +98 -0
- data/lib/nugrant/parameters.rb +14 -68
- data/lib/nugrant/vagrant/errors.rb +27 -0
- data/lib/nugrant/vagrant/v2/command/env.rb +101 -0
- data/lib/nugrant/vagrant/v2/command/helper.rb +30 -0
- data/lib/nugrant/vagrant/v2/command/parameters.rb +16 -4
- data/lib/nugrant/vagrant/v2/command/restricted_keys.rb +60 -0
- data/lib/nugrant/vagrant/v2/command/root.rb +12 -2
- data/lib/nugrant/vagrant/v2/config/user.rb +9 -21
- data/lib/nugrant/vagrant/v2/plugin.rb +0 -1
- data/lib/nugrant/version.rb +1 -1
- data/locales/en.yml +13 -0
- data/nugrant.gemspec +3 -7
- data/test/lib/nugrant/helper/env/test_exporter.rb +238 -0
- data/test/lib/nugrant/helper/test_bag.rb +16 -0
- data/test/lib/nugrant/helper/test_parameters.rb +17 -0
- data/test/lib/nugrant/helper/test_stack.rb +152 -0
- data/test/lib/nugrant/test_bag.rb +132 -22
- data/test/lib/nugrant/test_config.rb +95 -92
- data/test/lib/nugrant/test_parameters.rb +232 -177
- data/test/lib/test_helper.rb +3 -0
- data/test/resources/json/params_user_nil_values.json +9 -0
- data/test/resources/vagrantfiles/v2.defaults_mixed_string_symbols +18 -0
- data/test/resources/vagrantfiles/v2.defaults_null_values_in_vagrantuser +23 -0
- data/test/resources/vagrantfiles/v2.defaults_using_string +18 -0
- data/test/resources/vagrantfiles/v2.defaults_using_symbol +18 -0
- data/test/resources/{Vagrantfile.v2.empty → vagrantfiles/v2.empty} +0 -2
- data/test/resources/{Vagrantfile.v2.fake → vagrantfiles/v2.fake} +4 -3
- data/test/resources/vagrantfiles/v2.missing_parameter +3 -0
- data/test/resources/{Vagrantfile.v2.real → vagrantfiles/v2.real} +0 -2
- data/test/resources/yaml/params_user_nil_values.yml +5 -0
- metadata +55 -88
- data/lib/nugrant/vagrant/v1/command/parameters.rb +0 -134
- data/lib/nugrant/vagrant/v1/command/root.rb +0 -81
- data/lib/nugrant/vagrant/v1/config/user.rb +0 -37
- data/lib/nugrant/vagrant/v1/plugin.rb +0 -6
- data/lib/vagrant_init.rb +0 -2
- data/test/resources/Vagrantfile.v1.empty +0 -2
- data/test/resources/Vagrantfile.v1.fake +0 -10
- data/test/resources/Vagrantfile.v1.real +0 -19
data/Rakefile
CHANGED
data/lib/nugrant.rb
CHANGED
@@ -8,16 +8,24 @@ unless defined?(KeyError)
|
|
8
8
|
end
|
9
9
|
end
|
10
10
|
|
11
|
+
module Nugrant
|
12
|
+
def self.setup_i18n()
|
13
|
+
I18n.load_path << File.expand_path("locales/en.yml", Nugrant.source_root)
|
14
|
+
I18n.reload!
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.source_root
|
18
|
+
@source_root ||= Pathname.new(File.expand_path("../../", __FILE__))
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
11
22
|
if defined?(Vagrant)
|
23
|
+
Nugrant.setup_i18n()
|
24
|
+
|
12
25
|
case
|
13
26
|
when defined?(Vagrant::Plugin::V2)
|
14
27
|
require 'nugrant/vagrant/v2/plugin'
|
15
|
-
when Vagrant::VERSION =~ /1\.0\..*/
|
16
|
-
# Nothing to do, v1 plugins are picked by the vagrant_init.rb file
|
17
28
|
else
|
18
|
-
|
29
|
+
raise RuntimeError, "Vagrant [#{Vagrant::VERSION}] is not supported by Nugrant."
|
19
30
|
end
|
20
31
|
end
|
21
|
-
|
22
|
-
module Nugrant
|
23
|
-
end
|
data/lib/nugrant/bag.rb
CHANGED
@@ -1,96 +1,150 @@
|
|
1
1
|
module Nugrant
|
2
|
-
class Bag
|
3
|
-
attr_reader :__elements
|
2
|
+
class Bag < Hash
|
4
3
|
|
5
|
-
|
6
|
-
|
7
|
-
|
4
|
+
##
|
5
|
+
# Create a new Bag object which holds key/value pairs.
|
6
|
+
# The Bag object inherits from the Hash object, the main
|
7
|
+
# differences with a normal Hash are indifferent access
|
8
|
+
# (symbol or string) and method access (via method call).
|
9
|
+
#
|
10
|
+
# =| Arguments
|
11
|
+
# * `elements`
|
12
|
+
# The initial elements the bag should be built with it.'
|
13
|
+
# Must be an object responding to `each` and accepting
|
14
|
+
# a block with two arguments: `key, value`.]. Defaults to
|
15
|
+
# the empty hash.
|
16
|
+
#
|
17
|
+
# * `options`
|
18
|
+
# An options hash where some customization option can be passed.
|
19
|
+
# Defaults to an empty hash, see options for specific option default
|
20
|
+
# values.
|
21
|
+
#
|
22
|
+
# =| Options
|
23
|
+
# * `:key_error`
|
24
|
+
# A callable object receiving a single parameter `key` that is
|
25
|
+
# called when a key cannot be found in the Bag. The received key
|
26
|
+
# is already converted to a symbol. If the callable does not
|
27
|
+
# raise an exception, the result of it's execution is returned.
|
28
|
+
# The default value is a callable that throws a KeyError exception.
|
29
|
+
#
|
30
|
+
def initialize(elements = {}, options = {})
|
31
|
+
super()
|
32
|
+
|
33
|
+
@__key_error = options[:key_error] || Proc.new do |key|
|
34
|
+
raise KeyError, "Undefined parameter '#{key}'" if not key?(key)
|
8
35
|
end
|
9
36
|
|
10
|
-
|
37
|
+
(elements || {}).each do |key, value|
|
38
|
+
self[key] = value.kind_of?(Hash) ? Bag.new(value, options) : value
|
39
|
+
end
|
11
40
|
end
|
12
41
|
|
13
|
-
def
|
14
|
-
return
|
42
|
+
def method_missing(method, *args, &block)
|
43
|
+
return self[method]
|
15
44
|
end
|
16
45
|
|
17
|
-
|
18
|
-
|
46
|
+
##
|
47
|
+
### Hash Overriden Methods (for string & symbol indifferent access)
|
48
|
+
##
|
49
|
+
|
50
|
+
def [](input)
|
51
|
+
key = __convert_key(input)
|
52
|
+
return @__key_error.call(key) if not key?(key)
|
53
|
+
|
54
|
+
super(key)
|
19
55
|
end
|
20
56
|
|
21
|
-
def
|
22
|
-
|
57
|
+
def []=(input, value)
|
58
|
+
super(__convert_key(input), value)
|
23
59
|
end
|
24
60
|
|
25
|
-
def
|
26
|
-
|
61
|
+
def key?(key)
|
62
|
+
super(__convert_key(key))
|
27
63
|
end
|
28
64
|
|
29
65
|
##
|
30
|
-
# This method
|
31
|
-
#
|
32
|
-
#
|
66
|
+
# This method first start by converting the `input` parameter
|
67
|
+
# into a bag. It will then *deep* merge current values with
|
68
|
+
# the new ones coming from the `input`.
|
33
69
|
#
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
@__elements[key] = value
|
47
|
-
end
|
48
|
-
|
49
|
-
next
|
50
|
-
end
|
70
|
+
# The array merge strategy is by default to replace current
|
71
|
+
# values with new ones. You can use option `:array_strategy`
|
72
|
+
# to change this default behavior.
|
73
|
+
#
|
74
|
+
# +Options+
|
75
|
+
# * :array_strategy
|
76
|
+
# * :replace (Default) => Replace current values by new ones
|
77
|
+
# * :extend => Merge current values with new ones
|
78
|
+
# * :concat => Append new values to current ones
|
79
|
+
#
|
80
|
+
def merge!(input, options = {})
|
81
|
+
options = {:array_strategy => :replace}.merge(options)
|
51
82
|
|
52
|
-
|
53
|
-
|
54
|
-
|
83
|
+
array_strategy = options[:array_strategy]
|
84
|
+
input.each do |key, value|
|
85
|
+
current = __get(key)
|
86
|
+
case
|
87
|
+
when current == nil
|
88
|
+
self[key] = value
|
89
|
+
|
90
|
+
when current.kind_of?(Hash) && value.kind_of?(Hash)
|
91
|
+
current.merge!(value, options)
|
55
92
|
|
56
|
-
|
57
|
-
|
58
|
-
|
93
|
+
when current.kind_of?(Array) && value.kind_of?(Array)
|
94
|
+
self[key] = send("__#{array_strategy}_array_merge", current, value)
|
95
|
+
|
96
|
+
when value != nil
|
97
|
+
self[key] = value
|
98
|
+
end
|
59
99
|
end
|
60
100
|
end
|
61
101
|
|
62
|
-
def
|
102
|
+
def to_hash(options = {})
|
63
103
|
return {} if empty?()
|
64
104
|
|
65
|
-
|
66
|
-
each do |key, value|
|
67
|
-
hash[key.to_sym()] = value.kind_of?(Bag) ? value.__to_hash() : value
|
68
|
-
end
|
105
|
+
use_string_key = options[:use_string_key]
|
69
106
|
|
70
|
-
|
107
|
+
Hash[map do |key, value|
|
108
|
+
key = use_string_key ? key.to_s() : key
|
109
|
+
value = value.kind_of?(Bag) ? value.to_hash(options) : value
|
110
|
+
|
111
|
+
[key, value]
|
112
|
+
end]
|
71
113
|
end
|
72
114
|
|
73
|
-
|
74
|
-
|
75
|
-
|
115
|
+
##
|
116
|
+
### Aliases
|
117
|
+
##
|
76
118
|
|
77
|
-
|
78
|
-
if not value.kind_of?(Hash)
|
79
|
-
@__elements[key.to_sym()] = value
|
80
|
-
next
|
81
|
-
end
|
119
|
+
alias_method :to_ary, :to_a
|
82
120
|
|
83
|
-
|
84
|
-
|
85
|
-
|
121
|
+
##
|
122
|
+
### Private Methods
|
123
|
+
##
|
124
|
+
|
125
|
+
private
|
126
|
+
|
127
|
+
def __convert_key(key)
|
128
|
+
return key.to_sym() if key.respond_to?(:to_sym)
|
129
|
+
|
130
|
+
raise ArgumentError, "Key cannot be converted to symbol, current value [#{key}] (#{key.class.name})"
|
86
131
|
end
|
87
132
|
|
88
|
-
def
|
89
|
-
|
90
|
-
|
91
|
-
|
133
|
+
def __get(key)
|
134
|
+
# Calls Hash method [__convert_key(key)], used internally to retrieve value without raising Undefined parameter
|
135
|
+
self.class.superclass.instance_method(:[]).bind(self).call(__convert_key(key))
|
136
|
+
end
|
137
|
+
|
138
|
+
def __concat_array_merge(current_array, new_array)
|
139
|
+
current_array + new_array
|
140
|
+
end
|
141
|
+
|
142
|
+
def __extend_array_merge(current_array, new_array)
|
143
|
+
current_array | new_array
|
144
|
+
end
|
92
145
|
|
93
|
-
|
146
|
+
def __replace_array_merge(current_array, new_array)
|
147
|
+
new_array
|
94
148
|
end
|
95
149
|
end
|
96
150
|
end
|
data/lib/nugrant/helper/bag.rb
CHANGED
@@ -6,36 +6,36 @@ require 'nugrant/bag'
|
|
6
6
|
module Nugrant
|
7
7
|
module Helper
|
8
8
|
module Bag
|
9
|
-
def self.read(filepath,
|
10
|
-
data = parse_data(filepath,
|
9
|
+
def self.read(filepath, filetype, options = {})
|
10
|
+
data = parse_data(filepath, filetype, options)
|
11
11
|
|
12
|
-
return Nugrant::Bag.new(data)
|
12
|
+
return Nugrant::Bag.new(data, options)
|
13
13
|
end
|
14
14
|
|
15
|
-
def self.
|
15
|
+
def self.restricted_keys()
|
16
|
+
Nugrant::Bag.instance_methods()
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.parse_data(filepath, filetype, options = {})
|
16
20
|
return if not File.exists?(filepath)
|
17
21
|
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
if error_handler
|
25
|
-
# TODO: Implements error handler logic
|
26
|
-
error_handler.handle("Could not parse the user #{format.to_s} parameters file '#{filepath}': #{error}")
|
27
|
-
end
|
22
|
+
File.open(filepath, "rb") do |file|
|
23
|
+
return send("parse_#{filetype}", file)
|
24
|
+
end
|
25
|
+
rescue => error
|
26
|
+
if options[:error_handler]
|
27
|
+
options[:error_handler].handle("Could not parse the user #{filetype} parameters file '#{filepath}': #{error}")
|
28
28
|
end
|
29
29
|
end
|
30
30
|
|
31
|
-
def self.parse_json(
|
32
|
-
|
31
|
+
def self.parse_json(io)
|
32
|
+
MultiJson.load(io.read())
|
33
33
|
end
|
34
34
|
|
35
|
-
def self.parse_yaml(
|
36
|
-
YAML::ENGINE.yamler= 'syck' if defined?(YAML::ENGINE)
|
35
|
+
def self.parse_yaml(io)
|
36
|
+
YAML::ENGINE.yamler = 'syck' if (defined?(Syck) || defined?(YAML::Syck)) && defined?(YAML::ENGINE)
|
37
37
|
|
38
|
-
YAML.load(
|
38
|
+
YAML.load(io.read())
|
39
39
|
end
|
40
40
|
end
|
41
41
|
end
|
@@ -0,0 +1,208 @@
|
|
1
|
+
require 'shellwords'
|
2
|
+
|
3
|
+
require 'nugrant/bag'
|
4
|
+
require 'nugrant/helper/env/namer'
|
5
|
+
|
6
|
+
module Nugrant
|
7
|
+
module Helper
|
8
|
+
module Env
|
9
|
+
module Exporter
|
10
|
+
@@DEFAULT_AUTOENV_PATH = "./.env"
|
11
|
+
@@DEFAULT_SCRIPT_PATH = "./nugrant2env.sh"
|
12
|
+
|
13
|
+
@@VALID_EXPORTERS = [:autoenv, :script, :terminal]
|
14
|
+
|
15
|
+
##
|
16
|
+
# Returns true if the exporter name received is a valid
|
17
|
+
# valid export, false otherwise.
|
18
|
+
#
|
19
|
+
# @param exporter The exporter name to check validity
|
20
|
+
#
|
21
|
+
# @return true if exporter is valid, false otherwise.
|
22
|
+
def self.valid?(exporter)
|
23
|
+
@@VALID_EXPORTERS.include?(exporter)
|
24
|
+
end
|
25
|
+
|
26
|
+
##
|
27
|
+
# Creates an autoenv script containing the commands that are required
|
28
|
+
# to export or unset a bunch of environment variables taken from the
|
29
|
+
# bag.
|
30
|
+
#
|
31
|
+
# @param bag The bag to create the script for.
|
32
|
+
#
|
33
|
+
# @return (side-effect) Creates a script file containing commands
|
34
|
+
# to export or unset environment variables for
|
35
|
+
# bag.
|
36
|
+
#
|
37
|
+
# Options:
|
38
|
+
# * :autoenv_path => The path where to write the script, defaults to `./.env`.
|
39
|
+
# * :escape_value => If true, escape the value to export (or unset), default to true.
|
40
|
+
# * :io => The io where the command should be written, default to nil which create the autoenv on disk.
|
41
|
+
# * :namer => The namer used to transform bag segments into variable name, default to Namer::default().
|
42
|
+
# * :override => If true, variable a exported even when the override an existing env key, default to true.
|
43
|
+
# * :type => The type of command, default to :export.
|
44
|
+
#
|
45
|
+
def self.autoenv_exporter(bag, options = {})
|
46
|
+
io = options[:io] || (File.open(File.expand_path(options[:autoenv_path] || @@DEFAULT_AUTOENV_PATH), "w"))
|
47
|
+
|
48
|
+
terminal_exporter(bag, options.merge({:io => io}))
|
49
|
+
ensure
|
50
|
+
io.close() if io
|
51
|
+
end
|
52
|
+
|
53
|
+
##
|
54
|
+
# Creates a bash script containing the commands that are required
|
55
|
+
# to export or unset a bunch of environment variables taken from the
|
56
|
+
# bag.
|
57
|
+
#
|
58
|
+
# @param bag The bag to create the script for.
|
59
|
+
#
|
60
|
+
# @return (side-effect) Creates a script file containing commands
|
61
|
+
# to export or unset environment variables for
|
62
|
+
# bag.
|
63
|
+
#
|
64
|
+
# Options:
|
65
|
+
# * :escape_value => If true, escape the value to export (or unset), default to true.
|
66
|
+
# * :io => The io where the command should be written, default to nil which create the script on disk.
|
67
|
+
# * :namer => The namer used to transform bag segments into variable name, default to Namer::default().
|
68
|
+
# * :override => If true, variable a exported even when the override an existing env key, default to true.
|
69
|
+
# * :script_path => The path where to write the script, defaults to `./nugrant2env.sh`.
|
70
|
+
# * :type => The type of command, default to :export.
|
71
|
+
#
|
72
|
+
def self.script_exporter(bag, options = {})
|
73
|
+
io = options[:io] || (File.open(File.expand_path(options[:script_path] || @@DEFAULT_SCRIPT_PATH), "w"))
|
74
|
+
|
75
|
+
io.puts("#!/bin/env sh")
|
76
|
+
io.puts()
|
77
|
+
|
78
|
+
terminal_exporter(bag, options.merge({:io => io}))
|
79
|
+
ensure
|
80
|
+
io.close() if io
|
81
|
+
end
|
82
|
+
|
83
|
+
##
|
84
|
+
# Export to terminal the commands that are required
|
85
|
+
# to export or unset a bunch of environment variables taken from the
|
86
|
+
# bag.
|
87
|
+
#
|
88
|
+
# @param bag The bag to create the script for.
|
89
|
+
#
|
90
|
+
# @return (side-effect) Outputs to io the commands generated.
|
91
|
+
#
|
92
|
+
# Options:
|
93
|
+
# * :escape_value => If true, escape the value to export (or unset), default to true.
|
94
|
+
# * :io => The io where the command should be displayed, default to $stdout.
|
95
|
+
# * :namer => The namer used to transform bag segments into variable name, default to Namer::default().
|
96
|
+
# * :override => If true, variable a exported even when the override an existing env key, default to true.
|
97
|
+
# * :type => The type of command, default to :export.
|
98
|
+
#
|
99
|
+
def self.terminal_exporter(bag, options = {})
|
100
|
+
io = options[:io] || $stdout
|
101
|
+
type = options[:type] || :export
|
102
|
+
|
103
|
+
export(bag, options) do |key, value|
|
104
|
+
io.puts(command(type, key, value, options))
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
##
|
109
|
+
# Generic function to export a bag. This walk the bag,
|
110
|
+
# for each element, it creates the key using the namer
|
111
|
+
# and then forward the key and value to the block if
|
112
|
+
# the variable does not override an existing environment
|
113
|
+
# variable or if options :override is set to true.
|
114
|
+
#
|
115
|
+
# @param bag The bag to export.
|
116
|
+
#
|
117
|
+
# @return (side-effect) Yields each key and value to a block
|
118
|
+
#
|
119
|
+
# Options:
|
120
|
+
# * :namer => The namer used to transform bag segments into variable name, default to Namer::default().
|
121
|
+
# * :override => If true, variable a exported even when the override an existing env key, default to true.
|
122
|
+
#
|
123
|
+
def self.export(bag, options = {})
|
124
|
+
namer = options[:namer] || Env::Namer.default()
|
125
|
+
override = options.fetch(:override, true)
|
126
|
+
|
127
|
+
variables = {}
|
128
|
+
walk_bag(bag) do |segments, key, value|
|
129
|
+
key = namer.call(segments)
|
130
|
+
|
131
|
+
variables[key] = value if override or not ENV[key]
|
132
|
+
end
|
133
|
+
|
134
|
+
variables.sort().each do |key, value|
|
135
|
+
yield key, value
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
##
|
140
|
+
# Given a key and a value, return a string representation
|
141
|
+
# of the command type requested. Available types:
|
142
|
+
#
|
143
|
+
# * :export => A bash compatible export command
|
144
|
+
# * :unset => A bash compatible export command
|
145
|
+
#
|
146
|
+
def self.command(type, key, value, options = {})
|
147
|
+
# TODO: Replace by a map type => function name
|
148
|
+
case
|
149
|
+
when type == :export
|
150
|
+
export_command(key, value, options)
|
151
|
+
when type == :unset
|
152
|
+
unset_command(key, value, options)
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
##
|
157
|
+
# Returns a string representation of the command
|
158
|
+
# that needs to be used on the current platform
|
159
|
+
# to export an environment variable.
|
160
|
+
#
|
161
|
+
# @param key The key of the environment variable to export.
|
162
|
+
# It cannot be nil.
|
163
|
+
# @param value The value of the environment variable to export
|
164
|
+
#
|
165
|
+
# @return The export command, as a string
|
166
|
+
#
|
167
|
+
# Options:
|
168
|
+
# * :escape_value (true) => If true, escape the value to export.
|
169
|
+
#
|
170
|
+
def self.export_command(key, value, options = {})
|
171
|
+
value = value.to_s()
|
172
|
+
value = Shellwords.escape(value) if options[:escape_value] == nil || options[:escape_value]
|
173
|
+
|
174
|
+
# TODO: Handle platform differently
|
175
|
+
"export #{key}=#{value}"
|
176
|
+
end
|
177
|
+
|
178
|
+
##
|
179
|
+
# Returns a string representation of the command
|
180
|
+
# that needs to be used on the current platform
|
181
|
+
# to unset an environment variable.
|
182
|
+
#
|
183
|
+
# @param key The key of the environment variable to export.
|
184
|
+
# It cannot be nil.
|
185
|
+
#
|
186
|
+
# @return The unset command, as a string
|
187
|
+
#
|
188
|
+
def self.unset_command(key, value, options = {})
|
189
|
+
# TODO: Handle platform differently
|
190
|
+
"unset #{key}"
|
191
|
+
end
|
192
|
+
|
193
|
+
# FIXME: Move this directly into bag class
|
194
|
+
def self.walk_bag(bag, parents = [], &block)
|
195
|
+
commands = []
|
196
|
+
|
197
|
+
bag.each do |key, value|
|
198
|
+
segments = parents + [key]
|
199
|
+
nested_bag = value.kind_of?(Nugrant::Bag)
|
200
|
+
|
201
|
+
walk_bag(value, segments, &block) if nested_bag
|
202
|
+
yield segments, key, value if not nested_bag
|
203
|
+
end
|
204
|
+
end
|
205
|
+
end
|
206
|
+
end
|
207
|
+
end
|
208
|
+
end
|