defmastership 1.2.0 → 1.3.1
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 +4 -4
- data/.gitlab-ci.yml +7 -44
- data/Gemfile +29 -57
- data/bin/defmastership +1 -109
- data/config/mutant.yml +27 -22
- data/config/rubocop.yml +5 -2
- data/defmastership.gemspec +9 -8
- data/features/changeref.feature +112 -1
- data/features/definition_version.feature +41 -1
- data/features/export.feature +18 -0
- data/features/step_definitions/git_steps.rb +1 -1
- data/lib/defmastership/app.rb +115 -0
- data/lib/defmastership/batch_modifier.rb +2 -0
- data/lib/defmastership/config_preserver.rb +45 -0
- data/lib/defmastership/definition.rb +4 -2
- data/lib/defmastership/document.rb +6 -1
- data/lib/defmastership/export/body_formatter.rb +0 -2
- data/lib/defmastership/export/header_formatter.rb +0 -2
- data/lib/defmastership/hash_merge_no_new.rb +24 -0
- data/lib/defmastership/modifier/change_ref.rb +72 -7
- data/lib/defmastership/modifier/factory.rb +5 -0
- data/lib/defmastership/modifier/rename_included_files.rb +1 -3
- data/lib/defmastership/modifier/update_def.rb +1 -3
- data/lib/defmastership/modifier/update_def_checksum.rb +1 -1
- data/lib/defmastership/modifier/update_def_version.rb +4 -5
- data/lib/defmastership/version.rb +1 -1
- data/spec/spec_helper.rb +1 -10
- data/spec/unit/defmastership/app_spec.rb +235 -0
- data/spec/unit/defmastership/config_preserver_spec.rb +127 -0
- data/spec/unit/defmastership/document_spec.rb +17 -0
- data/spec/unit/defmastership/export/csv/formatter_spec.rb +0 -2
- data/spec/unit/defmastership/hash_spec.rb +27 -0
- data/spec/unit/defmastership/modifier/change_ref_spec.rb +116 -1
- data/spec/unit/defmastership/modifier/modifier_common_spec.rb +0 -2
- data/spec/unit/defmastership/modifier/update_def_spec.rb +0 -2
- data/spec/unit/defmastership/modifier/update_def_version_spec.rb +4 -4
- metadata +38 -24
- data/lib/defmastership/set_join_hack.rb +0 -13
- data/lib/defmastership.rb +0 -19
@@ -0,0 +1,115 @@
|
|
1
|
+
# Copyright (c) 2020 Jerome Arbez-Gindre
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require('csv')
|
5
|
+
require('defmastership/batch_modifier')
|
6
|
+
require('defmastership/config_preserver')
|
7
|
+
require('defmastership/document')
|
8
|
+
require('defmastership/export/csv/formatter')
|
9
|
+
require('gli')
|
10
|
+
|
11
|
+
module Defmastership
|
12
|
+
# The start of everything !
|
13
|
+
class App
|
14
|
+
extend GLI::App
|
15
|
+
|
16
|
+
program_desc 'Tool to handle Asciidoctor definition extension'
|
17
|
+
subcommand_option_handling :normal
|
18
|
+
arguments :strict
|
19
|
+
|
20
|
+
version Defmastership::VERSION
|
21
|
+
|
22
|
+
# Helper method
|
23
|
+
def self.configure_export_flags(command)
|
24
|
+
command.flag(%i[separator sep s], default_value: ',', desc: 'CSV separator')
|
25
|
+
command.switch(%i[no-fail], desc: 'Exit success even in case of wrong explicit checksum')
|
26
|
+
end
|
27
|
+
|
28
|
+
desc 'Export the definition database in CSV'
|
29
|
+
arg 'asciidoctor_file'
|
30
|
+
command :export do |command|
|
31
|
+
configure_export_flags(command)
|
32
|
+
command.action do |_global_options, options, args|
|
33
|
+
@results = { number_of_args_give_to_action: args.size }
|
34
|
+
|
35
|
+
my_doc = Defmastership::Document.new
|
36
|
+
my_doc.parse_file_with_preprocessor(args.first)
|
37
|
+
|
38
|
+
output_file = args.first.sub(/\.adoc$/, '.csv')
|
39
|
+
|
40
|
+
Defmastership::Export::CSV::Formatter.new(my_doc, options['separator']).export_to(output_file)
|
41
|
+
|
42
|
+
if my_doc.wrong_explicit_checksum?
|
43
|
+
my_doc.definitions.each do |definition|
|
44
|
+
next if definition.wrong_explicit_checksum.nil?
|
45
|
+
|
46
|
+
warn(
|
47
|
+
"warning: #{definition.reference} has a wrong explicit " \
|
48
|
+
"checksum (should be #{definition.sha256_short})"
|
49
|
+
)
|
50
|
+
end
|
51
|
+
exit 1 unless options[:'no-fail']
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
# Helper method
|
57
|
+
def self.configure_modify_flags(command)
|
58
|
+
configure_modify_modifications_flag(command)
|
59
|
+
configure_modify_modifications_file_flag(command)
|
60
|
+
configure_modify_changes_summary_flag(command)
|
61
|
+
end
|
62
|
+
|
63
|
+
# Helper method
|
64
|
+
def self.configure_modify_modifications_flag(command)
|
65
|
+
command.flag(
|
66
|
+
%i[modifications mod m],
|
67
|
+
must_match: /(?:[\w-]+)(?:,[\w-]+)*/,
|
68
|
+
default_value: 'all',
|
69
|
+
desc: 'comma separated list of modifications to apply'
|
70
|
+
)
|
71
|
+
end
|
72
|
+
|
73
|
+
# Helper method
|
74
|
+
def self.configure_modify_modifications_file_flag(command)
|
75
|
+
command.flag(
|
76
|
+
%i[modifications-file mf],
|
77
|
+
default_value: 'modifications.yml',
|
78
|
+
desc: 'modifications description file'
|
79
|
+
)
|
80
|
+
end
|
81
|
+
|
82
|
+
# Helper method
|
83
|
+
def self.configure_modify_changes_summary_flag(command)
|
84
|
+
command.flag(
|
85
|
+
%i[changes-summary s],
|
86
|
+
desc: 'generates a change summary in a CSV file'
|
87
|
+
)
|
88
|
+
end
|
89
|
+
|
90
|
+
desc 'Apply one or more modifications'
|
91
|
+
arg 'asciidoctor_file'
|
92
|
+
arg 'asciidoctor_file', %i[optional multiple]
|
93
|
+
command :modify do |command|
|
94
|
+
configure_modify_flags(command)
|
95
|
+
|
96
|
+
command.action do |_global_options, options, args|
|
97
|
+
@results = { number_of_args_give_to_action: args.size }
|
98
|
+
config_preserver = Defmastership::ConfigPreserver.new(options[:'modifications-file'])
|
99
|
+
changer = BatchModifier.new(config_preserver.config, args.to_h { |afile| [afile, File.read(afile)] })
|
100
|
+
changer.apply(options[:modifications].split(/\s*,\s*/).map(&:to_sym))
|
101
|
+
changer.adoc_sources.each do |adoc_filename, adoc_text|
|
102
|
+
File.write(adoc_filename, adoc_text)
|
103
|
+
end
|
104
|
+
config_preserver.save(changer.config)
|
105
|
+
|
106
|
+
unless options['changes-summary'].nil?
|
107
|
+
CSV.open(options['changes-summary'], 'wb') do |csv|
|
108
|
+
csv << %w[Modifier Was Becomes]
|
109
|
+
changer.changes.each { |row| csv << row }
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# Copyright (c) 2025 Jerome Arbez-Gindre
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require('defmastership/hash_merge_no_new')
|
5
|
+
require('yaml')
|
6
|
+
|
7
|
+
module Defmastership
|
8
|
+
# The configuration can be modified by Mofifiers (e.g. ChangeRef).
|
9
|
+
# The configuration file need to include these modifications without:
|
10
|
+
# - saving default values
|
11
|
+
# - removing empty line
|
12
|
+
# - removing comments
|
13
|
+
class ConfigPreserver
|
14
|
+
def initialize(filename)
|
15
|
+
@filename = filename
|
16
|
+
end
|
17
|
+
|
18
|
+
# Get the yaml config from configuration file
|
19
|
+
#
|
20
|
+
# @return [Hash] the configuration
|
21
|
+
def config
|
22
|
+
# idiomatic way to perform a deep clone of a hash
|
23
|
+
Marshal.load(Marshal.dump(YAML.load_file(@filename)))
|
24
|
+
end
|
25
|
+
|
26
|
+
# Save the updated configuration taking into account blank lines and comments
|
27
|
+
#
|
28
|
+
# @return [Hash] the configuration
|
29
|
+
def save(new_config)
|
30
|
+
# we don't wan't to add new configuration items from default values
|
31
|
+
new_config_content = config.merge_no_new(new_config).to_yaml.split("\n")
|
32
|
+
|
33
|
+
# Put in there place the emty lines and comments
|
34
|
+
File.open(@filename, 'r').each_line(chomp: true).with_index do |line, line_num|
|
35
|
+
if /\A\s*(\#.*)?\z/.match?(line)
|
36
|
+
new_config_content.insert(line_num, line)
|
37
|
+
elsif line =~ /\A\s*[^\s].+?(\s*\#.*)\z/
|
38
|
+
new_config_content[line_num] += Regexp.last_match(1)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
File.write(@filename, "#{new_config_content.join("\n")}\n")
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -1,7 +1,8 @@
|
|
1
1
|
# Copyright (c) 2020 Jerome Arbez-Gindre
|
2
2
|
# frozen_string_literal: true
|
3
3
|
|
4
|
-
require
|
4
|
+
require('digest')
|
5
|
+
require('forwardable')
|
5
6
|
|
6
7
|
# Contains the content of a Defmastership definition
|
7
8
|
module Defmastership
|
@@ -15,7 +16,7 @@ module Defmastership
|
|
15
16
|
labels.merge(match[:labels].split(/\s*,\s*/).to_set) if match[:labels]
|
16
17
|
labels
|
17
18
|
end,
|
18
|
-
eref: ->(_) { Hash.new
|
19
|
+
eref: ->(_) { Hash.new { |hash, key| hash[key] = [] } },
|
19
20
|
iref: ->(_) { [] },
|
20
21
|
attributes: ->(_) { {} },
|
21
22
|
explicit_checksum: ->(match) { match[:explicit_checksum] },
|
@@ -43,6 +44,7 @@ module Defmastership
|
|
43
44
|
# Defmastership definition: contains all data of a definition
|
44
45
|
class Definition
|
45
46
|
extend Forwardable
|
47
|
+
|
46
48
|
def_delegators :@data,
|
47
49
|
:type,
|
48
50
|
:reference,
|
@@ -4,7 +4,11 @@
|
|
4
4
|
require('asciidoctor')
|
5
5
|
require('defmastership/core/constants')
|
6
6
|
require('defmastership/core/parsing_state')
|
7
|
+
require('defmastership/definition')
|
8
|
+
require('defmastership/definition_parser')
|
9
|
+
require('defmastership/filters')
|
7
10
|
require('defmastership/matching_line')
|
11
|
+
require('forwardable')
|
8
12
|
|
9
13
|
# Contains the content of a Defmastership document: mainly definitions
|
10
14
|
module Defmastership
|
@@ -75,6 +79,7 @@ module Defmastership
|
|
75
79
|
# Reflects document structure from a definition point of view
|
76
80
|
class Document
|
77
81
|
extend Forwardable
|
82
|
+
|
78
83
|
def_delegators :@data, :definitions, :labels, :eref, :iref, :attributes, :variables
|
79
84
|
|
80
85
|
def initialize
|
@@ -134,7 +139,7 @@ module Defmastership
|
|
134
139
|
# @param reference [String] the defintion from the reference
|
135
140
|
# @return [Definition] the defintion from the reference
|
136
141
|
def ref_to_def(reference)
|
137
|
-
definitions.find { |definition| definition.reference
|
142
|
+
definitions.find { |definition| definition.reference.eql?(reference) }
|
138
143
|
end
|
139
144
|
|
140
145
|
private
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# Copyright (c) 2025 Jerome Arbez-Gindre
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
# adding a method to hash to allow to merge two hases only by modifying aready existing entries
|
5
|
+
class Hash
|
6
|
+
# Merges another hash by updating only the values for keys that already exist.
|
7
|
+
# It does not add new keys from the other hash. The merge is recursive for nested hashes.
|
8
|
+
# @param other_hash [Hash] The hash to merge from.
|
9
|
+
# @return [Hash] The new, merged hash.
|
10
|
+
def merge_no_new(other_hash)
|
11
|
+
# We use #merge with a block to handle key "collisions".
|
12
|
+
# We only merge the keys from `other_hash` that already exist in `self`
|
13
|
+
# by using `other_hash.slice(*keys)`.
|
14
|
+
merge(other_hash.slice(*keys)) do |_key, old_val, new_val|
|
15
|
+
if old_val.is_a?(Hash) && new_val.is_a?(Hash)
|
16
|
+
# Recursive call if both values are hashes
|
17
|
+
old_val.merge_no_new(new_val)
|
18
|
+
else
|
19
|
+
# else, we simply take the new value
|
20
|
+
new_val
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -1,9 +1,9 @@
|
|
1
1
|
# Copyright (c) 2020 Jerome Arbez-Gindre
|
2
2
|
# frozen_string_literal: true
|
3
3
|
|
4
|
-
require
|
5
|
-
require('defmastership/
|
6
|
-
require
|
4
|
+
require('defmastership/core/constants')
|
5
|
+
require('defmastership/modifier/modifier_common')
|
6
|
+
require('facets/file/writelines')
|
7
7
|
|
8
8
|
module Defmastership
|
9
9
|
module Modifier
|
@@ -12,7 +12,7 @@ module Defmastership
|
|
12
12
|
include ModifierCommon
|
13
13
|
|
14
14
|
# [Regexp] match all text before the definition's reference
|
15
|
-
DEF_BEFORE_REF = <<~"BEF"
|
15
|
+
DEF_BEFORE_REF = <<~"BEF".freeze
|
16
16
|
^
|
17
17
|
\\s*
|
18
18
|
\\[
|
@@ -24,7 +24,7 @@ module Defmastership
|
|
24
24
|
private_constant :DEF_BEFORE_REF
|
25
25
|
|
26
26
|
# [Regexp] match all text after the definition's reference
|
27
|
-
DEF_AFTER_REF = <<~"AFT"
|
27
|
+
DEF_AFTER_REF = <<~"AFT".freeze
|
28
28
|
\\s*
|
29
29
|
#{Core::DMRegexp::DEF_SUMMARY}
|
30
30
|
#{Core::DMRegexp::DEF_LABELS}
|
@@ -57,7 +57,7 @@ module Defmastership
|
|
57
57
|
#
|
58
58
|
# @return [Array<Symbol>] the two symbols of replacement methods
|
59
59
|
def self.replacement_methods
|
60
|
-
%i[replace_refdef replace_irefs]
|
60
|
+
%i[replace_refdef replace_irefs replace_include_tags replace_tags_in_included_files]
|
61
61
|
end
|
62
62
|
|
63
63
|
# @return [Hash{Symbol => Object}] the default configuration
|
@@ -100,6 +100,29 @@ module Defmastership
|
|
100
100
|
end
|
101
101
|
end
|
102
102
|
|
103
|
+
# Replace the definition's refs in tags of include statements
|
104
|
+
#
|
105
|
+
# @param line [String] the current line
|
106
|
+
# @return [String] the modified line
|
107
|
+
def replace_include_tags(line)
|
108
|
+
return line unless line.match?(Core::DMRegexp::INCLUDE)
|
109
|
+
|
110
|
+
changes.reduce(line) do |res_line, (from, to)|
|
111
|
+
res_line.sub(/(?<before>tags?=!?([^;]*;)?)#{from}\b/) { "#{$LAST_MATCH_INFO[:before]}#{to}" }
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
# Replace the definition's refs in included files
|
116
|
+
#
|
117
|
+
# @param line [String] the current line
|
118
|
+
# @return [String] the (unmodified) line
|
119
|
+
def replace_tags_in_included_files(line)
|
120
|
+
if line.match(Core::DMRegexp::INCLUDE)
|
121
|
+
Helper.replace_tags_in_included_files(Helper::ParsedFilename.full_filename(Regexp.last_match), changes)
|
122
|
+
end
|
123
|
+
line
|
124
|
+
end
|
125
|
+
|
103
126
|
private
|
104
127
|
|
105
128
|
def do_replace_refdef(line)
|
@@ -134,12 +157,54 @@ module Defmastership
|
|
134
157
|
Regexp.new(regexp_str, Regexp::EXTENDED)
|
135
158
|
end
|
136
159
|
|
137
|
-
# @param match [MatchData] The match
|
160
|
+
# @param match [MatchData] The match from Regexp match
|
138
161
|
# @param replacement [String] the reference replacement text
|
139
162
|
# @return [String] the overall replaced text
|
140
163
|
def self.text_with(match, replacement)
|
141
164
|
match[:before] + replacement + (match[:version_and_checksum] || '') + match[:after]
|
142
165
|
end
|
166
|
+
|
167
|
+
# Replace the definition's refs in included files
|
168
|
+
#
|
169
|
+
# @param filename [String] the included file name
|
170
|
+
# @param changes [Array<Array<String>>] the changes to apply
|
171
|
+
# @return [void]
|
172
|
+
def self.replace_tags_in_included_files(filename, changes)
|
173
|
+
lines = File.readlines(filename)
|
174
|
+
|
175
|
+
new_lines =
|
176
|
+
lines.map do |included_line|
|
177
|
+
apply_changes_in_included_file_line(included_line, changes)
|
178
|
+
end
|
179
|
+
|
180
|
+
File.writelines(filename, new_lines)
|
181
|
+
end
|
182
|
+
|
183
|
+
# Do the concrete replacements in included files
|
184
|
+
#
|
185
|
+
# @param line [String] The line from the included file
|
186
|
+
# @param changes [Array<Array<String>>] the changes to apply
|
187
|
+
# @return [String] the modified line with all changes
|
188
|
+
def self.apply_changes_in_included_file_line(line, changes)
|
189
|
+
changes.reduce(line) do |res_line, (from, to)|
|
190
|
+
if res_line.match?(/(tag|end)::#{from}\[\]/)
|
191
|
+
res_line.sub(from, to)
|
192
|
+
else
|
193
|
+
res_line
|
194
|
+
end
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
# Build filename from match data
|
199
|
+
module ParsedFilename
|
200
|
+
# build the full filename from the match of an include statement
|
201
|
+
#
|
202
|
+
# @param match [MatchData] The match from Regexp match
|
203
|
+
# @return [String] the overall filename
|
204
|
+
def self.full_filename(match)
|
205
|
+
(match[:path] || '') + match[:filename]
|
206
|
+
end
|
207
|
+
end
|
143
208
|
end
|
144
209
|
private_constant :Helper
|
145
210
|
end
|
@@ -1,6 +1,11 @@
|
|
1
1
|
# Copyright (c) 2023 Jerome Arbez-Gindre
|
2
2
|
# frozen_string_literal: true
|
3
3
|
|
4
|
+
require('defmastership/modifier/change_ref')
|
5
|
+
require('defmastership/modifier/rename_included_files')
|
6
|
+
require('defmastership/modifier/update_def_checksum')
|
7
|
+
require('defmastership/modifier/update_def_version')
|
8
|
+
|
4
9
|
module Defmastership
|
5
10
|
module Modifier
|
6
11
|
# build modifiers from a piece of configuration
|
@@ -1,9 +1,7 @@
|
|
1
1
|
# Copyright (c) 2020 Jerome Arbez-Gindre
|
2
2
|
# frozen_string_literal: true
|
3
3
|
|
4
|
-
require('defmastership/
|
5
|
-
require('defmastership/matching_line')
|
6
|
-
require('defmastership/modifier/modifier_common')
|
4
|
+
require('defmastership/filters')
|
7
5
|
|
8
6
|
module Defmastership
|
9
7
|
# defintion of the Rename Included Files Modifier
|
@@ -1,8 +1,6 @@
|
|
1
1
|
# Copyright (c) 2024 Jerome Arbez-Gindre
|
2
2
|
# frozen_string_literal: true
|
3
3
|
|
4
|
-
require('defmastership/core/constants')
|
5
|
-
|
6
4
|
module Defmastership
|
7
5
|
module Modifier
|
8
6
|
# @abstract Subclass and define +reference_replacement+ to implement a
|
@@ -62,7 +60,7 @@ module Defmastership
|
|
62
60
|
match = line.match(Core::DMRegexp::DEFINITION)
|
63
61
|
|
64
62
|
return line unless match
|
65
|
-
return line unless match[:type]
|
63
|
+
return line unless match[:type].eql?(def_type)
|
66
64
|
|
67
65
|
reference = match[:reference]
|
68
66
|
line.sub(self.class.reference_regexp(reference), reference_replacement(reference, match))
|
@@ -1,9 +1,8 @@
|
|
1
1
|
# Copyright (c) 2020 Jerome Arbez-Gindre
|
2
2
|
# frozen_string_literal: true
|
3
3
|
|
4
|
-
require
|
5
|
-
require
|
6
|
-
require 'tmpdir'
|
4
|
+
require('defmastership/modifier/update_def')
|
5
|
+
require('git')
|
7
6
|
|
8
7
|
module Defmastership
|
9
8
|
module Modifier
|
@@ -36,7 +35,7 @@ module Defmastership
|
|
36
35
|
# * :key filename
|
37
36
|
# * :value file content
|
38
37
|
def do_modifications(adoc_sources)
|
39
|
-
if ref_tag
|
38
|
+
if ref_tag.eql?('')
|
40
39
|
ref_document.each { |ref_doc| @ref_document.parse_file_with_preprocessor(ref_doc) }
|
41
40
|
else
|
42
41
|
Dir.mktmpdir('defmastership') do |tmpdir|
|
@@ -92,7 +91,7 @@ module Defmastership
|
|
92
91
|
# @return [String] the current definition version
|
93
92
|
def self.ref_version(ref_definition, definition, first_version)
|
94
93
|
new_ref_version = ref_definition.explicit_version
|
95
|
-
return new_ref_version if definition.sha256_short
|
94
|
+
return new_ref_version if definition.sha256_short.eql?(ref_definition.sha256_short)
|
96
95
|
|
97
96
|
new_ref_version ? new_ref_version.next : first_version
|
98
97
|
end
|
data/spec/spec_helper.rb
CHANGED
@@ -2,7 +2,6 @@
|
|
2
2
|
# frozen_string_literal: true
|
3
3
|
|
4
4
|
require('aasm/rspec')
|
5
|
-
require('aruba/rspec')
|
6
5
|
|
7
6
|
# formatter = [SimpleCov::Formatter::HTMLFormatter]
|
8
7
|
# SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter.new(formatter)
|
@@ -17,15 +16,9 @@ SimpleCov.start do
|
|
17
16
|
|
18
17
|
add_filter 'config'
|
19
18
|
add_filter 'vendor'
|
20
|
-
add_filter 'set_join_hack'
|
21
|
-
|
22
|
-
minimum_coverage 100
|
23
19
|
|
24
20
|
enable_coverage :branch
|
25
|
-
|
26
|
-
|
27
|
-
RSpec.configure do |config|
|
28
|
-
config.include(Aruba::Api)
|
21
|
+
minimum_coverage line: 100, branch: 100
|
29
22
|
end
|
30
23
|
|
31
24
|
RSpec::Matchers.define(:matchdata_including) do |h|
|
@@ -35,5 +28,3 @@ RSpec::Matchers.define(:matchdata_including) do |h|
|
|
35
28
|
end
|
36
29
|
end
|
37
30
|
end
|
38
|
-
|
39
|
-
require('defmastership')
|