power_stencil 0.5.1 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (60) hide show
  1. checksums.yaml +4 -4
  2. data/.gitlab-ci.yml +2 -0
  3. data/README.md +3 -7
  4. data/doc/builds.md +10 -1
  5. data/doc/entities.md +59 -50
  6. data/doc/images/power-stencil-entity-creation.svg +247 -114
  7. data/doc/plugins.md +49 -1
  8. data/doc/templates.md +23 -18
  9. data/etc/base_commands_definition.yml +28 -1
  10. data/etc/meta_templates/plugin_seed/psplugin_{entity}.gemspec +3 -1
  11. data/etc/power_stencil.yaml +7 -1
  12. data/etc/templates/plugin_definition/psplugin_{entity}.gemspec +3 -1
  13. data/etc/templates/project/.zzzgitignore.erb +8 -3
  14. data/lib/power_stencil.rb +1 -0
  15. data/lib/power_stencil/command_processors/build.rb +16 -3
  16. data/lib/power_stencil/command_processors/check.rb +12 -0
  17. data/lib/power_stencil/command_processors/create.rb +6 -5
  18. data/lib/power_stencil/command_processors/delete.rb +23 -15
  19. data/lib/power_stencil/command_processors/edit.rb +4 -2
  20. data/lib/power_stencil/command_processors/init.rb +2 -2
  21. data/lib/power_stencil/command_processors/plugin.rb +20 -6
  22. data/lib/power_stencil/command_processors/root.rb +1 -1
  23. data/lib/power_stencil/command_processors/shell.rb +10 -3
  24. data/lib/power_stencil/dsl/entities.rb +0 -16
  25. data/lib/power_stencil/engine/build_handling.rb +1 -2
  26. data/lib/power_stencil/engine/directory_processor.rb +6 -1
  27. data/lib/power_stencil/engine/entities_definitions.rb +16 -7
  28. data/lib/power_stencil/engine/entities_handling.rb +1 -1
  29. data/lib/power_stencil/engine/project_engine.rb +6 -10
  30. data/lib/power_stencil/initializer.rb +4 -7
  31. data/lib/power_stencil/plugins/base.rb +18 -9
  32. data/lib/power_stencil/plugins/capabilities.rb +10 -8
  33. data/lib/power_stencil/plugins/command_line.rb +1 -4
  34. data/lib/power_stencil/plugins/config.rb +1 -5
  35. data/lib/power_stencil/plugins/entity_definitions.rb +15 -0
  36. data/lib/power_stencil/plugins/gem.rb +14 -35
  37. data/lib/power_stencil/plugins/paths.rb +38 -0
  38. data/lib/power_stencil/plugins/require.rb +10 -16
  39. data/lib/power_stencil/plugins/templates.rb +3 -3
  40. data/lib/power_stencil/project/base.rb +24 -9
  41. data/lib/power_stencil/project/config.rb +3 -2
  42. data/lib/power_stencil/project/create.rb +23 -2
  43. data/lib/power_stencil/project/git.rb +75 -0
  44. data/lib/power_stencil/project/info.rb +14 -1
  45. data/lib/power_stencil/project/paths.rb +22 -26
  46. data/lib/power_stencil/project/plugins.rb +18 -23
  47. data/lib/power_stencil/project/templates.rb +15 -22
  48. data/lib/power_stencil/system_entity_definitions/all.rb +2 -1
  49. data/lib/power_stencil/system_entity_definitions/buildable.rb +1 -1
  50. data/lib/power_stencil/system_entity_definitions/entity_override.rb +6 -0
  51. data/lib/power_stencil/system_entity_definitions/entity_project_common.rb +13 -5
  52. data/lib/power_stencil/system_entity_definitions/{has_associated_files.rb → entity_templates.rb} +2 -2
  53. data/lib/power_stencil/system_entity_definitions/project_config.rb +1 -1
  54. data/lib/power_stencil/system_entity_definitions/project_entity.rb +7 -0
  55. data/lib/power_stencil/system_entity_definitions/simple_exec.rb +3 -3
  56. data/lib/power_stencil/system_entity_definitions/source_provider.rb +15 -0
  57. data/lib/power_stencil/utils/gem_utils.rb +22 -2
  58. data/lib/power_stencil/version.rb +1 -1
  59. data/power_stencil.gemspec +2 -1
  60. metadata +23 -6
@@ -0,0 +1,38 @@
1
+ module PowerStencil
2
+ module Plugins
3
+
4
+ module Paths
5
+
6
+ def plugin_path
7
+ case self.type
8
+ when :local
9
+ project.project_local_plugin_path self.name
10
+ when :gem
11
+ gem_spec.gem_dir
12
+ end
13
+ end
14
+
15
+ def plugin_command_line_definition_file
16
+ File.join plugin_path, 'etc', 'command_line.yaml'
17
+ end
18
+
19
+ def plugin_capabilities_definition_file
20
+ File.join plugin_path, 'etc', 'plugin_capabilities.yaml'
21
+ end
22
+
23
+ def plugin_config_specific_file
24
+ File.join plugin_path, 'etc', 'plugin_config.yaml'
25
+ end
26
+
27
+ def plugin_processors_dir
28
+ File.join plugin_path, 'lib', plugin_name, 'processors'
29
+ end
30
+
31
+ def plugin_entities_definitions_dir
32
+ File.join plugin_path, 'etc', plugin_name, 'entities_definitions'
33
+ end
34
+
35
+ end
36
+
37
+ end
38
+ end
@@ -6,7 +6,6 @@ module PowerStencil
6
6
  POST_BUILD_HOOK = :post_build_hook
7
7
 
8
8
  include PowerStencil::Utils::SecureRequire
9
- include PowerStencil::Utils::GemUtils
10
9
 
11
10
  private
12
11
 
@@ -15,21 +14,16 @@ module PowerStencil
15
14
  end
16
15
 
17
16
  def require_entry_point
18
- if is_available_gem? name
19
- logger.debug "Plugin '#{name}' is actually a Ruby Gem."
20
- raise "Plugin (#{name}) provided as a Ruby gem is not yet supported !"
21
- else
22
- @entry_point_path = File.join project.project_plugin_path(name), 'lib', "#{name.underscore}.rb"
23
- logger.debug "Plugin '#{name}' is provided locally: '#{entry_point_path}'"
24
- plugin_root_path = File.dirname(entry_point_path)
25
- begin
26
- $LOAD_PATH << plugin_root_path
27
- securely_require entry_point_path unless plugin_definition[:plugin_module].nil?
28
- rescue LoadError => e
29
- logger.warn "As plugin '#{name}' code is invalid, removing '#{plugin_root_path}' from LOAD_PATH"
30
- $LOAD_PATH.delete plugin_root_path
31
- end
32
-
17
+ @entry_point_path = File.join plugin_path, 'lib', "#{name.underscore}.rb"
18
+ logger.debug "Plugin '#{name}' entry point: '#{entry_point_path}'"
19
+ plugin_root_path = File.dirname(entry_point_path)
20
+ begin
21
+ $LOAD_PATH << plugin_root_path
22
+ securely_require entry_point_path unless plugin_definition[:plugin_module].nil?
23
+ rescue LoadError => e
24
+ @entry_point_path = nil
25
+ logger.warn "As plugin '#{name}' code is invalid, removing '#{plugin_root_path}' from LOAD_PATH"
26
+ $LOAD_PATH.delete plugin_root_path
33
27
  end
34
28
  end
35
29
 
@@ -3,15 +3,15 @@ module PowerStencil
3
3
 
4
4
  module Templates
5
5
 
6
- def register_plugin_templates
6
+ def register_plugin_templates_templates
7
7
  return unless capabilities[:templates]
8
8
  logger.info "Loading '#{self.name}' plugin templates..."
9
9
  plugin_definition[:templates].each do |templates_path|
10
- plugin_templates_path = File.join self.path, templates_path
10
+ plugin_templates_path = File.join self.plugin_path, templates_path
11
11
  Dir.entries(plugin_templates_path).reject { |e| %w(. ..).include? e }.each do |entry|
12
12
  template_path = File.join(plugin_templates_path, entry)
13
13
  if Dir.exist? template_path
14
- project.register_template_path_for_type entry.to_sym, template_path
14
+ project.register_template_template_path_for_type entry.to_sym, template_path
15
15
  end
16
16
  end
17
17
  end
@@ -5,6 +5,7 @@ require 'power_stencil/project/versioning'
5
5
  require 'power_stencil/project/info'
6
6
  require 'power_stencil/project/templates'
7
7
  require 'power_stencil/project/plugins'
8
+ require 'power_stencil/project/git'
8
9
 
9
10
  require 'power_stencil/engine/project_engine'
10
11
  require 'power_stencil/engine/entity_engine'
@@ -30,40 +31,54 @@ module PowerStencil
30
31
  include PowerStencil::Project::Templates
31
32
  include PowerStencil::Project::Plugins
32
33
  include PowerStencil::Project::Info
34
+ include PowerStencil::Project::Git
33
35
  extend PowerStencil::Project::Create
34
36
 
35
37
  attr_reader :engine, :entity_engine
36
38
 
39
+ def name
40
+ File.dirname project_config_root
41
+ end
42
+
37
43
  def initialize(search_from_path: Dir.pwd)
38
44
  initialize_paths search_from_path
39
45
  load_project_specific_config
40
46
  check_project_version
41
47
  bootstrap_plugins
42
48
  build_engines
43
- register_system_templates
44
- register_project_templates
49
+ register_system_templates_templates
50
+ register_plugins_templates_templates
51
+ register_project_templates_templates
52
+ setup_git_tracking
45
53
  end
46
54
 
47
55
  private
48
56
 
49
- def register_project_templates
50
- dir = project_templates_path
57
+ def register_plugins_templates_templates
58
+ plugins.each do |_, plugin|
59
+ plugin.register_plugin_templates_templates
60
+ end
61
+ end
62
+
63
+
64
+ def register_project_templates_templates
65
+ dir = project_templates_templates_path
51
66
  if Dir.exist? dir and File.readable? dir
52
67
  logger.info 'Registering project specific templates.'
53
68
  Dir.entries(dir).each do |potential_entity_type|
54
69
  next if potential_entity_type.match /^\./
55
70
  template_dir = File.join(dir, potential_entity_type)
56
71
  next unless File.directory? template_dir
57
- register_template_path_for_type potential_entity_type.to_sym, template_dir
72
+ register_template_template_path_for_type potential_entity_type.to_sym, template_dir
58
73
  end
59
74
  end
60
75
  end
61
76
 
62
- def register_system_templates
77
+ def register_system_templates_templates
63
78
  logger.debug 'Registering system templates'
64
79
  %i(plugin_definition simple_exec).each do |template_name|
65
- template_path = template_path template_name
66
- register_template_path_for_type template_name, template_path
80
+ template_path = system_template_template_path template_name
81
+ register_template_template_path_for_type template_name, template_path
67
82
  end
68
83
  end
69
84
 
@@ -76,4 +91,4 @@ module PowerStencil
76
91
  end
77
92
 
78
93
  end
79
- end
94
+ end
@@ -21,8 +21,9 @@ module PowerStencil
21
21
  end
22
22
 
23
23
 
24
- def add_plugin_config(plugin_name)
25
- yaml_file = plugin_config_specific_file plugin_name
24
+ def add_plugin_config(plugin)
25
+ plugin_name = plugin.name
26
+ yaml_file = plugin.plugin_config_specific_file
26
27
  priority = if priority.nil?
27
28
  PLUGIN_CONFIG_PRIORITY_MIN
28
29
  else
@@ -5,6 +5,8 @@ module PowerStencil
5
5
 
6
6
  module Create
7
7
 
8
+ INITIAL_REPOSITORY_COMMIT_MESSAGE = 'Initial commit for project "%s".'
9
+
8
10
  include Climatic::Proxy
9
11
 
10
12
  def create_project_tree(path, config_directory_name = PowerStencil.config[:default_config_directory_name])
@@ -14,14 +16,33 @@ module PowerStencil
14
16
  raise PowerStencil::Error, "The directory '#{path}' already contains a PowerStencil project !" unless config[:force]
15
17
  end
16
18
  logger.info "Creating project in '#{path}'..."
17
- render_project_template_in(path)
19
+ render_project_template_in path
20
+ initialize_git_repository path
18
21
  end
19
22
 
20
23
  private
21
24
 
25
+ def initialize_git_repository(repo_path)
26
+ if config[:'no-git']
27
+ puts_and_logs 'Do not initialize project git repository as per config request.', logs_as: :debug
28
+ return
29
+ end
30
+ if Dir.exists? File.join(repo_path, '.git')
31
+ puts_and_logs 'Git repository already exists. Skipping.', logs_as: :debug, check_verbose: false
32
+ return
33
+ end
34
+ puts_and_logs 'Initializing git repository...', logs_as: :debug
35
+ git = ::Git.init repo_path
36
+ logger.debug 'Adding all files.'
37
+ git.add repo_path
38
+ logger.debug 'Committing initial status'
39
+ commit_msg = INITIAL_REPOSITORY_COMMIT_MESSAGE % [File.basename(repo_path)]
40
+ git.commit_all commit_msg
41
+ end
42
+
22
43
  def render_project_template_in(new_project_config_path)
23
44
  engine = PowerStencil::Engine::InitEngine.new
24
- engine.render_source PowerStencil::Project::Paths.project_system_template_path, new_project_config_path
45
+ engine.render_source PowerStencil::Project::Paths.project_system_template_template_path, new_project_config_path
25
46
  end
26
47
 
27
48
  end
@@ -0,0 +1,75 @@
1
+ module PowerStencil
2
+ module Project
3
+
4
+ module Git
5
+
6
+ include Climatic::Utils::Input
7
+
8
+ def track_action_with_git(action_message,
9
+ user_validation_required: false,
10
+ validation_message: 'Commit changes ?',
11
+ show_files_to_commit: false,
12
+ &block)
13
+ return yield if git.nil?
14
+
15
+ status_before_action = git.status
16
+ yield
17
+ status_after_action = git.status
18
+
19
+ files_introduced_by_action = status_after_action.untracked.reject { |f| status_before_action.untracked.keys.include? f}
20
+ files_deleted_by_action = status_after_action.deleted.reject { |f| status_before_action.deleted.keys.include? f}
21
+ files_modified_by_action = status_after_action.changed.reject { |f| status_before_action.changed.keys.include? f}
22
+ files_to_commit = [files_introduced_by_action, files_deleted_by_action, files_modified_by_action].map(&:keys).flatten.sort.uniq
23
+
24
+ if files_to_commit.empty?
25
+ puts_and_logs 'Nothing to commit.'
26
+ return
27
+ end
28
+
29
+ if show_files_to_commit
30
+ header = 'Following file'
31
+ header << 's' if files_to_commit.count > 1
32
+ header << ' will be committed:'
33
+ puts header
34
+ puts files_to_commit.map { |filename| " - '#{filename}'" }
35
+ end
36
+
37
+ if user_validation_required
38
+ unless get_user_confirmation(default_choice: 'Yes', prompt: validation_message)
39
+ puts_and_logs 'Commit cancelled by user.', check_verbose: false
40
+ return
41
+ end
42
+ end
43
+
44
+ files_to_commit.each { |filename| git.add filename }
45
+
46
+ # Verify files to be committed really are
47
+ files_really_to_be_committed = git.diff.stats[:files].keys.select { |filename| files_to_commit.include? filename }
48
+ return if files_really_to_be_committed.empty?
49
+ files_really_to_be_committed.each { |filename| logger.info "File '#{filename}' will be committed" }
50
+
51
+ git.commit action_message
52
+ puts_and_logs 'All changes introduced have been committed.'
53
+ end
54
+
55
+ private
56
+
57
+ attr_reader :git
58
+
59
+ def setup_git_tracking
60
+ @git = nil
61
+ if config[:'no-git']
62
+ logger.debug "Won't track any changes with git as per config request!"
63
+ return
64
+ end
65
+ @git = ::Git.open(self.project_root, :log => PowerStencil.logger)
66
+ logger.debug 'Following project changes with git'
67
+ rescue => e
68
+ logger.debug PowerStencil::Error.report_error(e)
69
+ logger.warn 'This project is not managed by git'
70
+ end
71
+
72
+ end
73
+
74
+ end
75
+ end
@@ -60,7 +60,20 @@ module PowerStencil
60
60
  .sort{ |a,b| a.first <=> b.first }
61
61
  .each do |type, klass|
62
62
  msg = "Type '#{type}' --> #{klass}"
63
- msg << " (template-template path: '#{entity_type_templates[type]}')" unless entity_type_templates[type].nil?
63
+
64
+ source_provider = klass.entity_type_source_provider
65
+ source_provider_display = if source_provider == PowerStencil
66
+ "'#{source_provider.name}'"
67
+ elsif source_provider == self
68
+ "project '#{source_provider.name}'"
69
+ elsif source_provider.is_a? PowerStencil::Plugins::Base
70
+ "plugin '#{source_provider.name}'"
71
+ else
72
+ raise PowerStencil::Error, "Unidentified source provider for #{klass} !"
73
+ end
74
+
75
+ msg << " (provided by #{source_provider_display})"
76
+ msg << " template-template path: '#{entity_type_templates_templates[type]}'" unless entity_type_templates_templates[type].nil?
64
77
  report << msg
65
78
  end
66
79
  report
@@ -7,12 +7,12 @@ module PowerStencil
7
7
 
8
8
  attr_reader :project_config_root, :started_from
9
9
 
10
- def self.system_templates_path
10
+ def self.system_templates_templates_path
11
11
  File.expand_path File.join('..', '..', '..', '..', 'etc', 'templates'), __FILE__
12
12
  end
13
13
 
14
- def self.project_system_template_path
15
- File.join system_templates_path, 'project'
14
+ def self.project_system_template_template_path
15
+ File.join system_templates_templates_path, 'project'
16
16
  end
17
17
 
18
18
  def build_run_path(seed)
@@ -31,44 +31,40 @@ module PowerStencil
31
31
  File.join project_root, PowerStencil.config[:project_build_root_directory_name]
32
32
  end
33
33
 
34
- def template_path(entity_type)
35
- File.join PowerStencil::Project::Paths.system_templates_path, entity_type.to_s
34
+ def system_template_template_path(entity_type)
35
+ File.join PowerStencil::Project::Paths.system_templates_templates_path, entity_type.to_s
36
36
  end
37
37
 
38
- def project_plugins_path
38
+ def project_local_plugins_path
39
39
  File.join project_config_root, PowerStencil.config[:project_plugins_directory_name]
40
40
  end
41
41
 
42
- def project_templates_path
43
- File.join project_config_root, PowerStencil.config[:project_templates_directory_name]
44
- end
45
-
46
- def project_entity_definitions_path
47
- File.join project_config_root, PowerStencil.config[:project_entity_definitions_directory_name]
42
+ def project_local_plugin_path(plugin_name)
43
+ File.join project_local_plugins_path, plugin_name
48
44
  end
49
45
 
50
- def project_plugin_path(plugin_name)
51
- File.join project_plugins_path, plugin_name
52
- end
53
-
54
- def plugin_capabilities_definition_file(plugin_name)
55
- File.join project_plugin_path(plugin_name), 'etc', 'plugin_capabilities.yaml'
46
+ def project_templates_templates_path
47
+ File.join project_config_root, PowerStencil.config[:project_templates_directory_name]
56
48
  end
57
49
 
58
- def plugin_commands_line_definition_file(plugin_name)
59
- File.join project_plugin_path(plugin_name), 'etc', 'command_line.yaml'
50
+ def entities_template_path
51
+ File.join project_root, PowerStencil.config[:versioned_entities_templates_directory_name]
60
52
  end
61
53
 
62
- def plugin_config_specific_file(plugin_name)
63
- File.join project_plugin_path(plugin_name), 'etc', 'plugin_config.yaml'
54
+ def user_entities_template_path
55
+ File.join project_root, PowerStencil.config[:unversioned_user_entities_templates_directory_name]
64
56
  end
65
57
 
66
- def plugin_processors_dir(plugin_name)
67
- File.join project_plugin_path(plugin_name), 'lib', plugin_name, 'processors'
58
+ def entity_template_path(entity)
59
+ if entity.is_versioned_entity?
60
+ File.join entities_template_path, entity.type.to_s, entity.name
61
+ else
62
+ File.join user_entities_template_path, entity.type.to_s, entity.name
63
+ end
68
64
  end
69
65
 
70
- def plugin_entities_definitions_dir(plugin_name)
71
- File.join project_plugin_path(plugin_name), 'etc', plugin_name, 'entities_definitions'
66
+ def project_entity_definitions_path
67
+ File.join project_config_root, PowerStencil.config[:project_entity_definitions_directory_name]
72
68
  end
73
69
 
74
70
  def project_entity_path(entity)
@@ -7,11 +7,11 @@ module PowerStencil
7
7
  @plugins ||= {}
8
8
  end
9
9
 
10
- def create_plugin_tree(plugin_name, new_plugin_path, overwrite_files: false)
10
+ def create_new_local_plugin_tree(plugin_name, new_plugin_path, overwrite_files: false)
11
11
  raise PowerStencil::Error, "Plugin '#{plugin_name}' already exists !" if plugins.keys.include? plugin_name
12
12
  raise PowerStencil::Error, "Invalid plugin name '#{plugin_name}'" if (plugin_name.underscore =~ /^[_[:lower:]][_[:alnum:]]*$/).nil?
13
13
  entity_engine.dsl = PowerStencil::Dsl::PluginGeneration
14
- entity_engine.render_source entity_type_templates[:plugin_definition],
14
+ entity_engine.render_source entity_type_templates_templates[:plugin_definition],
15
15
  new_plugin_path,
16
16
  overwrite_files: overwrite_files,
17
17
  main_entry_point: plugin_name
@@ -21,6 +21,7 @@ module PowerStencil
21
21
  private
22
22
 
23
23
  def bootstrap_plugins
24
+ @plugins = {}
24
25
  initialize_gem_plugins
25
26
  initialize_local_plugins
26
27
  command_line_manager.definition_hash_to_commands
@@ -31,45 +32,39 @@ module PowerStencil
31
32
  end
32
33
  end
33
34
 
34
-
35
35
  def initialize_gem_plugins
36
- # PowerStencil::logger.warn 'Gem plugins not yet supported ! Skipping...'
37
36
  if config[:project_plugins].empty?
38
- PowerStencil.logger.info "No gem plugin found in '#{project_plugins_path}'"
37
+ PowerStencil.logger.info 'No gem plugin found in project'
39
38
  return
40
39
  end
41
40
  config[:project_plugins].each do |plugin_definition|
42
- plugin_name, plugin_requirements = case plugin_definition
43
- when String
44
- [plugin_definition, '']
45
- when Hash
46
- [plugin_definition.keys.first, plugin_definition.values.first]
47
- end
48
- unless PowerStencil::Plugins::Base.gem_locally_installed? plugin_name, plugin_requirements
49
- PowerStencil::Plugins::Base.install_gem plugin_name, plugin_requirements
50
- end
41
+
42
+ (gem_name, gem_req) = PowerStencil::Plugins::Base.plugin_definition_to_name_and_req plugin_definition
43
+ raise PowerStencil::Error, "Plugin '#{gem_name}' already exists !" unless plugins[gem_name].nil?
44
+
45
+ plugins[gem_name] = PowerStencil::Plugins::Base.new(gem_name, self, type: :gem, gem_req: gem_req)
51
46
  end
52
47
  end
53
48
 
54
49
  def initialize_local_plugins
55
- unless File.directory? project_plugins_path
56
- PowerStencil.logger.info "No local plugin found in '#{project_plugins_path}'"
50
+ unless File.directory? project_local_plugins_path
51
+ PowerStencil.logger.info "No local plugin found in '#{project_local_plugins_path}'"
57
52
  return
58
53
  end
59
54
 
60
- candidates = Dir.entries(project_plugins_path)
61
- .select { |e| File.directory? File.join(project_plugins_path, e) }
62
- .reject { |d| %w(. ..).include? d }
63
- @plugins = {}
64
- candidates.each do |candidate|
55
+ Dir.entries(project_local_plugins_path)
56
+ .select { |e| File.directory? File.join(project_local_plugins_path, e) }
57
+ .reject { |d| %w(. ..).include? d }
58
+ .each do |candidate|
65
59
  begin
66
60
  raise PowerStencil::Error, "Plugin '#{candidate}' already exists !" unless plugins[candidate].nil?
67
61
  plugins[candidate] = PowerStencil::Plugins::Base.new(candidate, self)
68
62
  rescue PowerStencil::Error => pse
69
- PowerStencil.logger.debug pse.message
70
- PowerStencil.logger.error "Discarding invalid plugin '#{candidate}'."
63
+ logger.puts_and_logs pse.message, logs_as: :error
64
+ logger.puts_and_logs "Discarding invalid plugin '#{candidate}'.", logs_as: :error
71
65
  end
72
66
  end
67
+
73
68
  end
74
69
 
75
70
  end