avm 0.6.1 → 0.10.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/lib/avm/data/package/dump.rb +3 -3
  3. data/lib/avm/data/rotate.rb +107 -0
  4. data/lib/avm/docker/runner.rb +8 -0
  5. data/lib/avm/instances/application.rb +25 -0
  6. data/lib/avm/instances/base/auto_values/access.rb +40 -0
  7. data/lib/avm/instances/base/auto_values/admin.rb +19 -0
  8. data/lib/avm/instances/base/auto_values/data.rb +26 -0
  9. data/lib/avm/instances/base/auto_values/database.rb +76 -0
  10. data/lib/avm/instances/base/auto_values/filesystem.rb +45 -0
  11. data/lib/avm/instances/base/auto_values/mailer.rb +34 -0
  12. data/lib/avm/instances/base/auto_values/ruby.rb +15 -0
  13. data/lib/avm/instances/base/auto_values/source.rb +15 -0
  14. data/lib/avm/instances/base/auto_values/system.rb +23 -0
  15. data/lib/avm/instances/base/auto_values/web.rb +46 -0
  16. data/lib/avm/instances/base/auto_values.rb +21 -0
  17. data/lib/avm/instances/base/dockerizable.rb +45 -0
  18. data/lib/avm/instances/base/entry_keys.rb +22 -0
  19. data/lib/avm/instances/base.rb +64 -0
  20. data/lib/avm/instances/entries.rb +43 -0
  21. data/lib/avm/instances/entry.rb +54 -0
  22. data/lib/avm/instances/entry_keys.rb +57 -0
  23. data/lib/avm/instances/runner.rb +38 -0
  24. data/lib/avm/instances.rb +8 -0
  25. data/lib/avm/registry/base.rb +12 -10
  26. data/lib/avm/registry.rb +1 -1
  27. data/lib/avm/runners/base.rb +31 -0
  28. data/lib/avm/scms/base.rb +5 -0
  29. data/lib/avm/scms/inflector.rb +22 -0
  30. data/lib/avm/self/docker_image.rb +14 -0
  31. data/lib/avm/self/instance/entry_keys.rb +12 -0
  32. data/lib/avm/self/instance.rb +24 -0
  33. data/lib/avm/sources/base/configuration.rb +37 -0
  34. data/lib/avm/sources/base/testing.rb +21 -0
  35. data/lib/avm/sources/base.rb +9 -25
  36. data/lib/avm/sources/tester.rb +24 -0
  37. data/lib/avm/sources/tests/builder.rb +83 -0
  38. data/lib/avm/sources/tests/performer.rb +35 -0
  39. data/lib/avm/sources/tests/result.rb +15 -0
  40. data/lib/avm/sources/tests/single.rb +56 -0
  41. data/lib/avm/sources/tests.rb +11 -0
  42. data/lib/avm/version.rb +1 -1
  43. metadata +58 -5
  44. data/lib/avm/source_stereotypes/base.rb +0 -21
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'eac_ruby_utils/require_sub'
4
+ require 'eac_ruby_utils/simple_cache'
5
+ require 'avm/instances/entries'
6
+
7
+ module Avm
8
+ module Instances
9
+ class Base
10
+ enable_listable
11
+ enable_simple_cache
12
+ require_sub __FILE__, include_modules: true
13
+ include ::Avm::Instances::Entries
14
+
15
+ lists.add_string :access, :local, :ssh
16
+
17
+ ID_PATTERN = /\A([a-z0-9]+(?:\-[a-z0-9]+)*)_(.+)\z/.freeze
18
+
19
+ class << self
20
+ def by_id(id)
21
+ application_id, suffix = parse_id(id)
22
+ require 'avm/instances/application'
23
+ new(::Avm::Instances::Application.new(application_id), suffix)
24
+ end
25
+
26
+ private
27
+
28
+ def parse_id(id)
29
+ m = ID_PATTERN.match(id)
30
+ return [m[1], m[2]] if m
31
+
32
+ raise "ID Pattern no matched: \"#{id}\""
33
+ end
34
+ end
35
+
36
+ common_constructor :application, :suffix do
37
+ self.suffix = suffix.to_s
38
+ end
39
+
40
+ def id
41
+ "#{application.id}_#{suffix}"
42
+ end
43
+
44
+ def to_s
45
+ id
46
+ end
47
+
48
+ def host_env_uncached
49
+ access = read_entry(:access, list: ::Avm::Instances::Base.lists.access.values)
50
+ case access
51
+ when 'local' then ::EacRubyUtils::Envs.local
52
+ when 'ssh' then ::EacRubyUtils::Envs.ssh(read_entry('ssh.url'))
53
+ else raise("Unmapped access value: \"#{access}\"")
54
+ end
55
+ end
56
+
57
+ private
58
+
59
+ def source_instance_uncached
60
+ ::Avm::Instances::Base.by_id(source_instance_id)
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'eac_ruby_utils/core_ext'
4
+ require 'avm/instances/entry'
5
+
6
+ module Avm
7
+ module Instances
8
+ module Entries
9
+ def entry(suffix, options = {})
10
+ ::Avm::Instances::Entry.new(self, suffix, options)
11
+ end
12
+
13
+ def path_prefix
14
+ @path_prefix ||= [id].freeze
15
+ end
16
+
17
+ def read_entry(entry_suffix, options = {})
18
+ entry(entry_suffix, options).value
19
+ end
20
+
21
+ def read_entry_optional(entry_suffix, options = {})
22
+ entry(entry_suffix, options).optional_value
23
+ end
24
+
25
+ def full_entry_path(entry_suffix)
26
+ unless entry_suffix.is_a?(::Array)
27
+ entry_suffix = ::EacConfig::PathsHash.parse_entry_key(entry_suffix.to_s)
28
+ end
29
+ (path_prefix + entry_suffix).join('.')
30
+ end
31
+
32
+ def inherited_entry_value(source_entry_suffix, target_entry_suffix, &block)
33
+ read_entry_optional(source_entry_suffix).if_present do |instance_id|
34
+ other_entry_value(instance_id, target_entry_suffix).if_present(&block)
35
+ end
36
+ end
37
+
38
+ def other_entry_value(instance_id, entry_suffix)
39
+ ::Avm::Instances::Base.by_id(instance_id).read_entry_optional(entry_suffix)
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'eac_config/node'
4
+ require 'eac_ruby_utils/core_ext'
5
+
6
+ module Avm
7
+ module Instances
8
+ class Entry
9
+ class << self
10
+ def auto_value_method_name(suffix)
11
+ "auto_#{suffix.to_s.gsub('.', '_')}"
12
+ end
13
+ end
14
+
15
+ common_constructor :parent, :suffix, :options
16
+
17
+ def auto_value
18
+ parent.respond_to?(auto_value_method, true) ? parent.send(auto_value_method) : nil
19
+ end
20
+
21
+ def auto_value_method
22
+ self.class.auto_value_method_name(suffix)
23
+ end
24
+
25
+ def full_path
26
+ (parent.path_prefix + suffix_as_array).join('.')
27
+ end
28
+
29
+ def optional_value
30
+ read(required: false, noinput: true) || auto_value
31
+ end
32
+
33
+ def read(extra_options = {})
34
+ ::EacConfig::Node.context.current.entry(full_path, options.merge(extra_options)).value
35
+ end
36
+
37
+ def suffix_as_array
38
+ if suffix.is_a?(::Array)
39
+ suffix.dup
40
+ else
41
+ ::EacConfig::PathsHash.parse_entry_key(suffix.to_s)
42
+ end
43
+ end
44
+
45
+ def value
46
+ optional_value || read
47
+ end
48
+
49
+ def write(value)
50
+ ::EacConfig::Node.context.current.entry(full_path).value = value
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'eac_ruby_utils/core_ext'
4
+
5
+ module Avm
6
+ module Instances
7
+ module EntryKeys
8
+ class << self
9
+ def all
10
+ all_keys.to_a
11
+ end
12
+
13
+ def keys_consts_set(prefix, suffixes)
14
+ if suffixes.is_a?(::Hash)
15
+ keys_consts_set_from_hash(prefix, suffixes)
16
+ elsif suffixes.is_a?(::Enumerable)
17
+ keys_consts_set_from_enum(prefix, suffixes)
18
+ else
19
+ raise "Unmapped suffixes class: #{suffixes.class}"
20
+ end
21
+ end
22
+
23
+ def key_const_set(prefix, suffix)
24
+ key = [prefix, suffix].reject(&:blank?).join('.')
25
+ const_set(key.gsub('.', '_').upcase, key)
26
+ all_keys << key
27
+ end
28
+
29
+ private
30
+
31
+ def all_keys
32
+ @all_keys ||= ::Set.new
33
+ end
34
+
35
+ def keys_consts_set_from_enum(prefix, suffixes)
36
+ suffixes.each { |suffix| key_const_set(prefix, suffix) }
37
+ end
38
+
39
+ def keys_consts_set_from_hash(prefix, suffixes)
40
+ suffixes.each { |k, v| keys_consts_set(prefix.to_s + (k.blank? ? '' : ".#{k}"), v) }
41
+ end
42
+ end
43
+
44
+ {
45
+ '' => %w[fs_path host_id source_instance_id],
46
+ database: %w[id hostname limit name password port system timeout username extra],
47
+ docker: %w[registry],
48
+ mailer: {
49
+ '' => %w[id from reply_to],
50
+ smtp: %w[address port domain username password authentication starttls_auto]
51
+ },
52
+ ssh: %w[hostname port url username],
53
+ web: %w[authority hostname path port scheme url userinfo]
54
+ }.each { |prefix, suffixes| keys_consts_set(prefix, suffixes) }
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'eac_cli/core_ext'
4
+
5
+ module Avm
6
+ module Instances
7
+ class Runner
8
+ class << self
9
+ def instance_class
10
+ ::Avm.const_get(stereotype_name).const_get('Instance')
11
+ end
12
+
13
+ def stereotype_module
14
+ ::Avm.const_get(stereotype_name)
15
+ end
16
+
17
+ def stereotype_name
18
+ name.demodulize
19
+ end
20
+ end
21
+
22
+ description = "Utilities for #{stereotype_name} instances."
23
+ runner_with :help, :subcommands do
24
+ desc description
25
+ pos_arg 'instance-id'
26
+ subcommands
27
+ end
28
+
29
+ delegate :instance_class, :stereotype_module, :stereotype_name, to: :class
30
+
31
+ private
32
+
33
+ def instance_uncached
34
+ self.class.instance_class.by_id(parsed.instance_id)
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Avm
4
+ module Instances
5
+ require 'avm/instances/application'
6
+ require 'avm/instances/entries'
7
+ end
8
+ end
@@ -12,11 +12,11 @@ module Avm
12
12
  def detect(*registered_initialize_args)
13
13
  detect_optional(*registered_initialize_args) ||
14
14
  raise("No registered module valid for #{registered_initialize_args}" \
15
- " (Module suffix: #{module_suffix})")
15
+ " (Module suffix: #{module_suffix}, Available: #{registered_modules.join(', ')})")
16
16
  end
17
17
 
18
18
  def detect_optional(*registered_initialize_args)
19
- registered_modules.lazy.map { |klass| klass.new(*registered_initialize_args) }
19
+ registered_modules.reverse.lazy.map { |klass| klass.new(*registered_initialize_args) }
20
20
  .find(&:valid?)
21
21
  end
22
22
 
@@ -39,18 +39,20 @@ module Avm
39
39
  private
40
40
 
41
41
  def registered_modules_uncached
42
- (single_registered_modules + provider_registered_modules)
43
- .select { |v| valid_registered_module?(v) }.uniq.sort_by { |s| [s.name] }
42
+ registered_gems.flat_map { |registry| modules_from_registry(registry) }
43
+ .select { |v| valid_registered_module?(v) }.uniq
44
44
  end
45
45
 
46
- def single_registered_modules
47
- single_instance_registry.registered.map(&:registered_module)
46
+ def modules_from_registry(registry)
47
+ if registry.registry.module_suffix == provider_module_suffix
48
+ registry.registered_module.new.all
49
+ else
50
+ [registry.registered_module]
51
+ end
48
52
  end
49
53
 
50
- def provider_registered_modules
51
- provider_registry.registered.map(&:registered_module).flat_map do |provider_class|
52
- provider_class.new.all
53
- end
54
+ def registered_gems
55
+ (single_instance_registry.registered + provider_registry.registered).sort
54
56
  end
55
57
 
56
58
  def single_instance_registry_uncached
data/lib/avm/registry.rb CHANGED
@@ -7,7 +7,7 @@ module Avm
7
7
  module Registry
8
8
  require_sub __FILE__
9
9
  enable_listable
10
- lists.add_symbol :category, :instance_stereotypes, :scms, :source_stereotypes
10
+ lists.add_symbol :category, :instance_stereotypes, :runners, :scms, :sources
11
11
 
12
12
  class << self
13
13
  enable_simple_cache
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'eac_cli/core_ext'
4
+
5
+ module Avm
6
+ module Runners
7
+ class Base
8
+ enable_abstract_methods
9
+
10
+ class << self
11
+ def command_argument
12
+ stereotype_name.underscore.dasherize
13
+ end
14
+
15
+ def stereotype_name
16
+ name.split('::')[-3]
17
+ end
18
+ end
19
+
20
+ delegate :command_argument, :stereotype_name, to: :class
21
+
22
+ runner_with :help, :subcommands do
23
+ subcommands
24
+ end
25
+
26
+ def to_s
27
+ stereotype_name
28
+ end
29
+ end
30
+ end
31
+ end
data/lib/avm/scms/base.rb CHANGED
@@ -20,6 +20,11 @@ module Avm
20
20
  self.class.name.demodulize
21
21
  end
22
22
 
23
+ # @return [Enumerable<Avm::Scms::Base>]
24
+ def subs
25
+ raise_abstract_method __method__
26
+ end
27
+
23
28
  def to_s
24
29
  name
25
30
  end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Avm
4
+ module Scms
5
+ class Inflector
6
+ class << self
7
+ def default_instance
8
+ @default_instance ||= new
9
+ end
10
+ end
11
+
12
+ ISSUE_POINTER_NAME_PREFIX = 'issue_'
13
+ POINTER_NAME_TO_ISSUE_PATTERN = /\A#{Regexp.quote(ISSUE_POINTER_NAME_PREFIX)}(\d+)\z/.freeze
14
+ POINTER_NAME_TO_ISSUE_PARSER = POINTER_NAME_TO_ISSUE_PATTERN.to_parser { |m| m[1] }
15
+
16
+ # @return [String, nil]
17
+ def pointer_name_to_issue_id(pointer_name)
18
+ POINTER_NAME_TO_ISSUE_PARSER.parse(pointer_name)
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'eac_ruby_utils/core_ext'
4
+ require 'avm/docker/image'
5
+
6
+ module Avm
7
+ module Self
8
+ class DockerImage < ::Avm::Docker::Image
9
+ def stereotype_tag
10
+ nil
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Avm
4
+ module Self
5
+ class Instance < ::Avm::Instances::Base
6
+ module EntryKeys
7
+ DATA_DEFAULT_PATH = 'data.default_path'
8
+ DOCKER_REGISTRY_NAME = 'docker.registry.name'
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'avm/instances/base'
4
+ require 'avm/self/docker_image'
5
+ require 'avm/self/instance/entry_keys'
6
+ require 'avm/eac_ubuntu_base0/docker_image'
7
+
8
+ module Avm
9
+ module Self
10
+ class Instance < ::Avm::Instances::Base
11
+ def docker_image_class
12
+ ::Avm::Self::DockerImage
13
+ end
14
+
15
+ def docker_registry
16
+ read_entry(::Avm::Self::Instance::EntryKeys::DOCKER_REGISTRY_NAME)
17
+ end
18
+
19
+ def docker_run_arguments
20
+ ['-e', "LOCAL_USER_ID=#{::Process.uid}"]
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'eac_ruby_utils/core_ext'
4
+ require 'eac_ruby_utils/yaml'
5
+ require 'shellwords'
6
+
7
+ module Avm
8
+ module Sources
9
+ class Base
10
+ module Configuration
11
+ # @return [Array<String>, nil]
12
+ def read_configuration_as_shell_words(key)
13
+ configuration[key].if_present do |v|
14
+ v.is_a?(::Enumerable) ? v.map(&:to_s) : ::Shellwords.split(v.to_s)
15
+ end
16
+ end
17
+
18
+ private
19
+
20
+ # @return [Hash]
21
+ def configuration_uncached
22
+ ::Avm::Sources::Configuration::FILENAMES.each do |filename|
23
+ file_path = path.join(filename)
24
+ return ::EacRubyUtils::Yaml.load_file(file_path).with_indifferent_access if
25
+ file_path.exist?
26
+ end
27
+ {}
28
+ end
29
+
30
+ # @return [Avm::Sources::Configuration]
31
+ def old_configuration_uncached
32
+ ::Avm::Sources::Configuration.find_in_path(path)
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'eac_ruby_utils/core_ext'
4
+
5
+ module Avm
6
+ module Sources
7
+ class Base
8
+ module Testing
9
+ # @return [Avm::Sources::Tester]
10
+ def tester
11
+ tester_class.new(self)
12
+ end
13
+
14
+ # @return [Class<Avm::Sources::Tester>
15
+ def tester_class
16
+ Avm::Sources::Tester
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -7,16 +7,20 @@ require 'eac_ruby_utils/core_ext'
7
7
  module Avm
8
8
  module Sources
9
9
  class Base
10
- require_sub __FILE__
10
+ require_sub __FILE__, include_modules: true
11
+ compare_by :path
12
+ enable_abstract_methods
11
13
  enable_simple_cache
12
14
  enable_listable
13
15
  lists.add_symbol :option, :parent
14
16
  common_constructor :path, :options, default: [{}] do
15
- self.path = path.to_pathname
16
- self.options = self.class.lists.option.hash_keys_validate!(options)
17
+ self.path = path.to_pathname.expand_path
18
+ self.options = ::Avm::Sources::Base.lists.option.hash_keys_validate!(options)
17
19
  end
18
20
 
19
- delegate :locale, to: :configuration
21
+ abstract_methods :update, :valid?
22
+
23
+ delegate :locale, to: :old_configuration
20
24
  delegate :to_s, to: :path
21
25
 
22
26
  # @return [Avm::Sources::Base]
@@ -33,35 +37,15 @@ module Avm
33
37
 
34
38
  # @return [Enumerable<Avm::Sources::Base>]
35
39
  def subs
36
- git_repo.subrepos
37
- .map { |subrepo| self.class.new(subrepo.subpath.expand_path(path), parent: self) }
38
- end
39
-
40
- def update
41
- stereotype.update_source(self)
40
+ scm.subs.map { |subrepo| self.class.new(subrepo.path, parent: self) }
42
41
  end
43
42
 
44
43
  private
45
44
 
46
- # @return [Avm::Sources::Configuration]
47
- def configuration_uncached
48
- ::Avm::Sources::Configuration.find_in_path(path) || ::Avm::Sources::Configuration.new
49
- end
50
-
51
- # @return [EacGit::Local]
52
- def git_repo_uncached
53
- ::EacGit::Local.new(path)
54
- end
55
-
56
45
  # @return [Avm::Scms::Base]
57
46
  def scm_uncached
58
47
  ::Avm::Registry.scms.detect(path)
59
48
  end
60
-
61
- # @return [Avm::SourceStereotypes::Base]
62
- def stereotype_uncached
63
- ::Avm::Registry.source_stereotypes.detect(self)
64
- end
65
49
  end
66
50
  end
67
51
  end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'eac_ruby_utils/core_ext'
4
+
5
+ module Avm
6
+ module Sources
7
+ class Tester
8
+ enable_abstract_methods
9
+ common_constructor :source
10
+
11
+ abstract_methods :result, :logs
12
+
13
+ # @return [EacRubyUtils::Fs::Logs]
14
+ def logs
15
+ raise_abstract_method __method__
16
+ end
17
+
18
+ # @return [Avm::Sources::Tests::Result]
19
+ def result
20
+ raise_abstract_method __method__
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'avm/sources/tests/performer'
4
+ require 'avm/sources/tests/single'
5
+ require 'eac_ruby_utils/core_ext'
6
+
7
+ module Avm
8
+ module Sources
9
+ module Tests
10
+ class Builder
11
+ require_sub __FILE__
12
+ enable_immutable
13
+
14
+ immutable_accessor :include_main, :include_subs, type: :boolean
15
+ immutable_accessor :include_id, type: :array
16
+ common_constructor :main_source
17
+
18
+ def immutable_constructor_args
19
+ [main_source]
20
+ end
21
+
22
+ # @return [Avm::Sources::Tests::Performer]
23
+ def performer
24
+ ::Avm::Sources::Tests::Performer.new(self)
25
+ end
26
+
27
+ def selected_units
28
+ (select_units_from_subs + select_units_from_main + select_units_from_ids).sort.uniq
29
+ end
30
+
31
+ private
32
+
33
+ # @return [Array<Avm::Sources::Tests::Single>]
34
+ def available_units
35
+ @available_units ||= ([main_source] + main_source.subs)
36
+ .map { |a_source| create_unit(a_source) }
37
+ end
38
+
39
+ def available_units_from_main
40
+ create_units([main_source])
41
+ end
42
+
43
+ def available_units_from_subs
44
+ create_units(main_source.subs)
45
+ end
46
+
47
+ # @return [Avm::Sources::Tests::Single]
48
+ def create_unit(source)
49
+ ::Avm::Sources::Tests::Single.new(self, source)
50
+ end
51
+
52
+ # @return [Array<Avm::Sources::Tests::Single>]
53
+ def create_units(sources)
54
+ sources.map { |a_source| create_unit(a_source) }
55
+ end
56
+
57
+ # @return [Avm::Sources::Tests::Single]
58
+ def create_unit_by_id(source_id)
59
+ r = available_units.find { |unit| unit.id == source_id }
60
+ return r if r
61
+
62
+ raise ::ArgumentError, "Source not found with ID=#{source_id}" \
63
+ "(Available: #{available_units.map(&:id).join(', ')})"
64
+ end
65
+
66
+ # @return [Array<Avm::Sources::Tests::Single>]
67
+ def select_units_from_ids
68
+ include_ids.map { |source_id| create_unit_by_id(source_id) }
69
+ end
70
+
71
+ # @return [Array<Avm::Sources::Tests::Single>]
72
+ def select_units_from_main
73
+ include_main? ? available_units_from_main : []
74
+ end
75
+
76
+ # @return [Array<Avm::Sources::Tests::Single>]
77
+ def select_units_from_subs
78
+ include_subs? ? available_units_from_subs : []
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end