tax_generator 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (39) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +21 -0
  3. data/.inch.yml +10 -0
  4. data/.reek +10 -0
  5. data/.rspec +1 -0
  6. data/.rubocop.yml +72 -0
  7. data/Gemfile +3 -0
  8. data/Gemfile.lock +154 -0
  9. data/LICENSE +20 -0
  10. data/README.md +131 -0
  11. data/Rakefile +26 -0
  12. data/bin/tax_generator +7 -0
  13. data/data/input/.gitignore +25 -0
  14. data/data/input/destinations.xml +1073 -0
  15. data/data/input/taxonomy.xml +78 -0
  16. data/data/output/.gitignore +4 -0
  17. data/init.rb +1 -0
  18. data/lib/tax_generator/all.rb +26 -0
  19. data/lib/tax_generator/application.rb +125 -0
  20. data/lib/tax_generator/classes/destination.rb +103 -0
  21. data/lib/tax_generator/classes/file_creator.rb +100 -0
  22. data/lib/tax_generator/classes/processor.rb +270 -0
  23. data/lib/tax_generator/classes/taxonomy_tree.rb +97 -0
  24. data/lib/tax_generator/cli.rb +14 -0
  25. data/lib/tax_generator/helpers/application_helper.rb +154 -0
  26. data/lib/tax_generator/version.rb +27 -0
  27. data/lib/tax_generator.rb +1 -0
  28. data/spec/lib/tax_generator/application_spec.rb +0 -0
  29. data/spec/lib/tax_generator/classes/destination_spec.rb +62 -0
  30. data/spec/lib/tax_generator/classes/file_creator_spec.rb +96 -0
  31. data/spec/lib/tax_generator/classes/processor_spec.rb +30 -0
  32. data/spec/lib/tax_generator/classes/taxonomy_tree_spec.rb +0 -0
  33. data/spec/lib/tax_generator/cli_spec.rb +0 -0
  34. data/spec/lib/tax_generator/helpers/application_helper_spec.rb +0 -0
  35. data/spec/spec_helper.rb +60 -0
  36. data/tax_generator.gemspec +40 -0
  37. data/templates/static/all.css +586 -0
  38. data/templates/template.html.erb +91 -0
  39. metadata +452 -0
@@ -0,0 +1,270 @@
1
+ require_relative '../helpers/application_helper'
2
+ module TaxGenerator
3
+ # class used to process xml files and create html files
4
+ #
5
+ # @!attribute options
6
+ # @return [Hash] the options that can determine the input and output files and folders
7
+ #
8
+ # @!attribute worker_supervisor
9
+ # @return [Celluloid::SupervisionGroup] the supervision group that supervises workers
10
+ # @!attribute workers
11
+ # @return [Celluloid::Actor] the actors that will work on the jobs
12
+ # @!attribute taxonomy
13
+ # @return [TaxGenerator::TaxonomyTree] the taxonomy tree that holds the nodes from the taxonomy xml document
14
+ # @!attribute jobs
15
+ # @return [Hash] each key from the job list is the job id, and the value is the job itself
16
+ # @!attribute job_to_worker
17
+ # @return [Hash] each key from the list is the job id, and the value is the worker that will handle the job
18
+ # @!attribute worker_to_job
19
+ # @return [Hash] each key from the list is the workers mailbox address, and the value is the job being handled by the worker
20
+ # @!attribute condition
21
+ # @return [Celluloid::Condition] the supervision group that supervises workers
22
+ class Processor
23
+ include Celluloid
24
+ include Celluloid::Logger
25
+ include Celluloid::Notifications
26
+ include TaxGenerator::ApplicationHelper
27
+
28
+ attr_reader :options, :worker_supervisor, :workers, :taxonomy, :jobs, :job_to_worker, :worker_to_job, :condition
29
+
30
+ trap_exit :worker_died
31
+
32
+ # receives a list of options that are used to determine the input files and output and input folders
33
+ #
34
+ # @param [Hash] options the options that can determine the input and output files and folders
35
+ # @option options [String] :input_dir The input directory
36
+ # @option options [String]:output_dir The output directory
37
+ # @option options [String] :taxonomy_file_name The taxonomy file name
38
+ # @option options [String] :destinations_file_name The destinations file name
39
+ #
40
+ # @see #work
41
+ #
42
+ # @return [void]
43
+ #
44
+ # @api public
45
+ def initialize(options = {})
46
+ Celluloid.boot
47
+ @options = options.is_a?(Hash) ? options.symbolize_keys : {}
48
+ @worker_supervisor = Celluloid::SupervisionGroup.run!
49
+ @workers = @worker_supervisor.pool(TaxGenerator::FileCreator, as: :workers, size: 50)
50
+ Actor.current.link @workers
51
+ @jobs = {}
52
+ @job_to_worker = {}
53
+ @worker_to_job = {}
54
+ end
55
+
56
+ # returns the input folder from the options list
57
+ # otherwise the default path
58
+ #
59
+ # @return [String]
60
+ #
61
+ # @api public
62
+ def input_folder
63
+ @options.fetch(:input_dir, "#{root}/data/input")
64
+ end
65
+
66
+ # returns the taxonomy filename from the option list
67
+ # otherwise the default filename
68
+ #
69
+ # @return [String]
70
+ #
71
+ # @api public
72
+ def taxonomy_file_name
73
+ @options.fetch(:taxonomy_filename, 'taxonomy.xml')
74
+ end
75
+
76
+ # returns the destinations filename from the option list
77
+ # otherwise the default filename
78
+ #
79
+ # @return [String]
80
+ #
81
+ # @api public
82
+ def destinations_file_name
83
+ @options.fetch(:destinations_filename, 'destinations.xml')
84
+ end
85
+
86
+ # returns the output folder path from the option list
87
+ # otherwise the default path
88
+ #
89
+ # @return [String]
90
+ #
91
+ # @api public
92
+ def output_folder
93
+ @options.fetch(:output_dir, "#{root}/data/output")
94
+ end
95
+
96
+ # returns the full path to the taxonomy file
97
+ #
98
+ # @return [String]
99
+ #
100
+ # @api public
101
+ def taxonomy_file_path
102
+ File.join(input_folder, taxonomy_file_name)
103
+ end
104
+
105
+ # returns the full path to the destinations file
106
+ #
107
+ # @return [String]
108
+ #
109
+ # @api public
110
+ def destinations_file_path
111
+ File.join(input_folder, destinations_file_name)
112
+ end
113
+
114
+ # returns the full path to the static folder
115
+ #
116
+ # @return [String]
117
+ #
118
+ # @api public
119
+ def static_output_dir
120
+ File.join(output_folder, 'static')
121
+ end
122
+
123
+ # cleans the output folder and re-creates it and the static folder
124
+ #
125
+ # @return [void]
126
+ #
127
+ # @api public
128
+ def prepare_output_dirs
129
+ FileUtils.rm_rf Dir["#{output_folder}/**/*"]
130
+ create_directories(output_folder, static_output_dir)
131
+ FileUtils.cp_r(Dir["#{File.join(root, 'templates', 'static')}/*"], static_output_dir)
132
+ end
133
+
134
+ # checks if all workers finished and returns true or false
135
+ #
136
+ # @return [Boolean]
137
+ #
138
+ # @api public
139
+ def all_workers_finished?
140
+ @jobs.all? { |_job_id, job| job['status'] == 'finished' }
141
+ end
142
+
143
+ # registers all the jobs so that the managers can have access to them at any time
144
+ #
145
+ # @param [Array] jobs the jobs that will be registered
146
+ #
147
+ # @return [void]
148
+ #
149
+ # @api public
150
+ def register_jobs(*jobs)
151
+ jobs.pmap do |job|
152
+ job = job.stringify_keys
153
+ @jobs[job['atlas_id']] = job
154
+ end
155
+ end
156
+
157
+ # registers all the jobs, and then delegates them to workers
158
+ # @see #register_jobs
159
+ # @see TaxGenerator::FileCreator#work
160
+ #
161
+ # @param [Array] jobs the jobs that will be delegated to the workers
162
+ #
163
+ # @return [void]
164
+ #
165
+ # @api public
166
+ def delegate_job(*jobs)
167
+ # jobs need to be added into the manager before starting task to avoid adding new key while iterating
168
+ register_jobs(*jobs)
169
+ current_actor = Actor.current
170
+ @jobs.pmap do |_job_id, job|
171
+ @workers.async.work(job, current_actor) if @workers.alive?
172
+ end
173
+ end
174
+
175
+ # parses the destinations xml document, gets each destination and adds a new job for that
176
+ # destination in the job list and then returns it
177
+ # @see #nokogiri_xml
178
+ #
179
+ # @return [Array<Hash>]
180
+ #
181
+ # @api public
182
+ def fetch_file_jobs
183
+ jobs = [{ atlas_id: 0, taxonomy: @taxonomy, destination: nil, output_folder: output_folder }]
184
+ nokogiri_xml(destinations_file_path).xpath('//destination').pmap do |destination|
185
+ atlas_id = destination.attributes['atlas_id']
186
+ jobs << { atlas_id: atlas_id.value, taxonomy: @taxonomy, destination: destination, output_folder: output_folder }
187
+ end
188
+ jobs
189
+ end
190
+
191
+ # fetches the jobs for file generation, then delegates the jobs to workers and waits untill workers finish
192
+ # @see #fetch_file_jobs
193
+ # @see #delegate_job
194
+ # @see #wait_jobs_termination
195
+ #
196
+ # @return [void]
197
+ #
198
+ # @api public
199
+ def generate_files
200
+ @condition = Celluloid::Condition.new
201
+ jobs = fetch_file_jobs
202
+ delegate_job(*jobs)
203
+ wait_jobs_termination
204
+ end
205
+
206
+ # retrieves the information about the node from the tree and generates for each destination a new File
207
+ # @see #create_file
208
+ #
209
+ # @param [TaxGenerator::TaxonomyTree] taxonomy the taxonomy tree that will be used for fetching node information
210
+ #
211
+ # @return [void]
212
+ #
213
+ # @api public
214
+ def wait_jobs_termination
215
+ result = @condition.wait
216
+ return unless result.present?
217
+ terminate
218
+ end
219
+
220
+ # registers the worker so that the current actor has access to it at any given time and starts the worker
221
+ # @see TaxGenerator::FileCreator#start_work
222
+ #
223
+ # @param [Hash] job the job that the worker will work
224
+ # @param [TaxGenerator::FileCreator] worker the worker that will create the file
225
+ #
226
+ # @return [void]
227
+ #
228
+ # @api public
229
+ def register_worker_for_job(job, worker)
230
+ @job_to_worker[job['atlas_id']] = worker
231
+ @worker_to_job[worker.mailbox.address] = job
232
+ log_message("worker #{worker.job_id} registed into manager")
233
+ Actor.current.link worker
234
+ worker.async.start_work
235
+ end
236
+
237
+ # generates the taxonomy tree , prints it and generates the files
238
+ # @see TaxGenerator::TaxonomyTree#new
239
+ # @see Tree::TreeNode#print_tree
240
+ # @see #generate_files
241
+ #
242
+ # @return [void]
243
+ #
244
+ # @api public
245
+ def work
246
+ prepare_output_dirs
247
+ if File.directory?(input_folder) && File.file?(taxonomy_file_path) && File.file?(destinations_file_path)
248
+ @taxonomy = TaxGenerator::TaxonomyTree.new(taxonomy_file_path)
249
+ @taxonomy.print_tree
250
+ generate_files
251
+ else
252
+ log_message('Please provide valid options', log_method: 'fatal')
253
+ end
254
+ end
255
+
256
+ # logs the message about working being dead if a worker crashes
257
+ # @param [TaxGenerator::FileCreator] worker the worker that died
258
+ # @param [String] reason the reason for which the worker died
259
+ #
260
+ # @return [void]
261
+ #
262
+ # @api public
263
+ def worker_died(worker, reason)
264
+ mailbox_address = worker.mailbox.address
265
+ job = @worker_to_job.delete(mailbox_address)
266
+ return if reason.blank? || job.blank?
267
+ log_message("worker job #{job['atlas_id']} with mailbox #{mailbox_address.inspect} died for reason: #{log_error(reason)}", log_method: 'fatal')
268
+ end
269
+ end
270
+ end
@@ -0,0 +1,97 @@
1
+ require_relative '../helpers/application_helper'
2
+ module TaxGenerator
3
+ # class used to create the Taxonomy Tree
4
+ #
5
+ # @!attribute root_node
6
+ # @return [Tree::TreeNode] the root node of the tree
7
+ #
8
+ # @!attribute document
9
+ # @return [Nokogiri::XML] the xml document used to build the tree
10
+ class TaxonomyTree
11
+ include TaxGenerator::ApplicationHelper
12
+ attr_reader :root_node, :document
13
+
14
+ # receives a file path that will be parsed and used to build the tree
15
+ # @see Tree::TreeNode#new
16
+ # @see #add_node
17
+ #
18
+ # @param [String] file_path the path to the xml file that will be parsed and used to build the tree
19
+ #
20
+ # @return [void]
21
+ #
22
+ # @api public
23
+ def initialize(file_path)
24
+ @document = nokogiri_xml(file_path)
25
+ taxonomy_root = @document.at_xpath('//taxonomy_name')
26
+ @root_node = Tree::TreeNode.new(taxonomy_root.content, nil)
27
+ @document.xpath('//node').pmap do |taxonomy_node|
28
+ add_node(taxonomy_node, @root_node)
29
+ end
30
+ end
31
+
32
+ # gets the atlas_id from the nokogiri element and then searches first child whose name is 'node_name'
33
+ # and uses this to insert the node
34
+ # @see #insert_node
35
+ #
36
+ # @param [Nokogiri::Element] taxonomy_node the nokogiri element that wants to be added to the tree
37
+ # @param [Tree::TreeNode] node the parent node to which the element needs to be added
38
+ #
39
+ # @return [void]
40
+ #
41
+ # @api public
42
+ def add_taxonomy_node(taxonomy_node, node)
43
+ atlas_node_id = taxonomy_node.attributes['atlas_node_id']
44
+ node_name = taxonomy_node.children.find { |child| child.name == 'node_name' }
45
+ insert_node(atlas_node_id, node_name, node)
46
+ end
47
+
48
+ # inserts a new node in the tree by checking first if atlas_id and node_name are present
49
+ # and then adds the node as child to the node passed as third argument
50
+ # @see Tree::TreeNode#new
51
+ #
52
+ # @param [Nokogiri::Element] atlas_node_id the element that holds the value of the atlas_id attribute
53
+ # @param [Nokogiri::Element] node_name the the element that holds the node name of the element
54
+ # @param [Tree::TreeNode] node the parent node to which the element needs to be added
55
+ #
56
+ # @return [void]
57
+ #
58
+ # @api public
59
+ def insert_node(atlas_node_id, node_name, node)
60
+ return if atlas_node_id.blank? || node_name.blank?
61
+ current_node = Tree::TreeNode.new(atlas_node_id.value, node_name.content)
62
+ node << current_node
63
+ current_node
64
+ end
65
+
66
+ # checks to see if the nokogiri element has any childrens, if it has , will add it to the tree and iterates over the
67
+ # children and adds them as child to the newly added node
68
+ # @see #add_taxonomy_node
69
+ #
70
+ # @param [Nokogiri::Element] taxonomy_node the nokogiri element that wants to be added to the tree
71
+ # @param [Tree::TreeNode] node the parent node to which the element needs to be added
72
+ #
73
+ # @return [void]
74
+ #
75
+ # @api public
76
+ def add_node(taxonomy_node, node)
77
+ return unless taxonomy_node.children.any?
78
+ tax_node = add_taxonomy_node(taxonomy_node, node)
79
+ taxonomy_node.xpath('./node').pmap do |child_node|
80
+ add_taxonomy_node(child_node, tax_node) if tax_node.present?
81
+ end
82
+ end
83
+
84
+ # receives a file path that will be parsed and used to build the tree
85
+ #
86
+ # @param [String] name the name of the method that is invoked against the tree
87
+ # @param [Array] args the arguments to the method
88
+ # @param [Proc] block the block that will be passed to the method
89
+ #
90
+ # @return [void]
91
+ #
92
+ # @api public
93
+ def method_missing(name, *args, &block)
94
+ @root_node.send name, *args, &block
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,14 @@
1
+ require_relative './all'
2
+ module TaxGenerator
3
+ # this is the class that will be invoked from terminal , and willl use the invoke task as the primary function.
4
+ class CLI
5
+ # method used to start
6
+ #
7
+ # @return [void]
8
+ #
9
+ # @api public
10
+ def self.start
11
+ TaxGenerator::Application.new.run
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,154 @@
1
+ module TaxGenerator
2
+ # class that holds the helper methods used in the classes
3
+ module ApplicationHelper
4
+ delegate :app_logger,
5
+ to: :'TaxGenerator::Application'
6
+
7
+ module_function
8
+
9
+ # returns the text from a nokogiri element by rejecting blank elements
10
+ #
11
+ # @param [Nokogiri::Element] element the nokogiri element that will select only children with content and returns their text
12
+ #
13
+ # @return [String]
14
+ #
15
+ # @api public
16
+ def elements_with_content(element)
17
+ if element.present?
18
+ element.select { |elem| elem.content.present? }.join(&:text)
19
+ else
20
+ element
21
+ end
22
+ end
23
+
24
+ # returns the root path of the gem
25
+ #
26
+ # @return [void]
27
+ #
28
+ # @api public
29
+ def root
30
+ File.expand_path(File.dirname(File.dirname(File.dirname(__dir__))))
31
+ end
32
+
33
+ # returns a Nokogiri XML document from a file
34
+ #
35
+ # @param [String] file_path the path to the xml file that will be parsed
36
+ #
37
+ # @return [void]
38
+ #
39
+ # @api public
40
+ def nokogiri_xml(file_path)
41
+ Nokogiri::XML(File.open(file_path), nil, 'UTF-8')
42
+ end
43
+
44
+ # creates directories from a list of arguments
45
+ #
46
+ # @param [Array] args the arguments that will be used as directory names and will create them
47
+ #
48
+ # @return [void]
49
+ #
50
+ # @api public
51
+ def create_directories(*args)
52
+ args.pmap do |argument|
53
+ FileUtils.mkdir_p(argument) unless File.directory?(argument)
54
+ end
55
+ end
56
+
57
+ # sets the exception handler for celluloid actors
58
+ #
59
+ #
60
+ # @return [void]
61
+ #
62
+ # @api public
63
+ def set_celluloid_exception_handling
64
+ Celluloid.logger = app_logger
65
+ Celluloid.task_class = Celluloid::TaskThread
66
+ Celluloid.exception_handler do |ex|
67
+ unless ex.is_a?(Interrupt)
68
+ log_error(ex)
69
+ end
70
+ end
71
+ end
72
+
73
+ # Reads a file and interpretes it as ERB
74
+ #
75
+ # @param [String] file_path the file that will be read and interpreted as ERB
76
+ #
77
+ # @return [String]
78
+ #
79
+ # @api public
80
+ def erb_template(file_path)
81
+ template = ERB.new(File.read(file_path))
82
+ template.filename = file_path
83
+ template
84
+ end
85
+
86
+ # Displays a error with fatal log level
87
+ # @see #format_error
88
+ # @see #log_message
89
+ #
90
+ # @param [Exception] exception the exception that will be formatted and printed on screen
91
+ #
92
+ # @return [String]
93
+ #
94
+ # @api public
95
+ def log_error(exception)
96
+ message = format_error(exception)
97
+ log_message(message, log_method: 'fatal')
98
+ end
99
+
100
+ # formats a exception to be displayed on screen
101
+ #
102
+ # @param [String] message the message that will be printed to the log file
103
+ # @param [Hash] options the options used to determine how to log the message
104
+ # @option options [String] :log_method The log method , by default debug
105
+ #
106
+ # @return [String]
107
+ #
108
+ # @api public
109
+ def log_message(message, options = {})
110
+ app_logger.send(options.fetch(:log_method, 'debug'), message)
111
+ end
112
+
113
+ # formats a exception to be displayed on screen
114
+ #
115
+ # @param [Exception] exception the exception that will be formatted and printed on screen
116
+ #
117
+ # @return [String]
118
+ #
119
+ # @api public
120
+ def format_error(exception)
121
+ message = "#{exception.class} (#{exception.respond_to?(:message) ? exception.message : exception.inspect}):\n"
122
+ message << exception.annoted_source_code.to_s if exception.respond_to?(:annoted_source_code)
123
+ message << ' ' << exception.backtrace.join("\n ") if exception.respond_to?(:backtrace)
124
+ message
125
+ end
126
+
127
+ # wrapper to execute a block and rescue from exception
128
+ # @see #set_celluloid_exception_handling
129
+ # @see #rescue_interrupt
130
+ # @see #log_error
131
+ #
132
+ # @return [void]
133
+ #
134
+ # @api public
135
+ def execute_with_rescue
136
+ set_celluloid_exception_handling
137
+ yield if block_given?
138
+ rescue Interrupt
139
+ rescue_interrupt
140
+ rescue => error
141
+ log_error(error)
142
+ end
143
+
144
+ # rescues from a interrupt error and shows a message
145
+ #
146
+ # @return [void]
147
+ #
148
+ # @api public
149
+ def rescue_interrupt
150
+ `stty icanon echo`
151
+ puts "\n Command was cancelled due to an Interrupt error."
152
+ end
153
+ end
154
+ end
@@ -0,0 +1,27 @@
1
+ # Returns the version of the gem as a <tt>Gem::Version</tt>
2
+ module TaxGenerator
3
+ # it prints the gem version as a string
4
+ #
5
+ # @return [String]
6
+ #
7
+ # @api public
8
+ def self.gem_version
9
+ Gem::Version.new VERSION::STRING
10
+ end
11
+
12
+ # module used to generate the version string
13
+ # provides a easy way of getting the major, minor and tiny
14
+ module VERSION
15
+ # major release version
16
+ MAJOR = 0
17
+ # minor release version
18
+ MINOR = 0
19
+ # tiny release version
20
+ TINY = 1
21
+ # prelease version ( set this only if it is a prelease)
22
+ PRE = nil
23
+
24
+ # generates the version string
25
+ STRING = [MAJOR, MINOR, TINY, PRE].compact.join('.')
26
+ end
27
+ end
@@ -0,0 +1 @@
1
+ require 'tax_generator/all'
File without changes
@@ -0,0 +1,62 @@
1
+ # encoding:utf-8
2
+ require 'spec_helper'
3
+ describe TaxGenerator::Destination do
4
+ let(:default_processor) {TaxGenerator::Processor.new}
5
+ let(:str) { default_processor.destinations_file_path }
6
+
7
+ let(:destination_xml) { @xml ||= Nokogiri::XML(File.open(str), nil, 'UTF-8') }
8
+ let(:info_base) { './/practical_information/health_and_safety' }
9
+ before(:each) do
10
+ @destination = TaxGenerator::Destination.new(destination_xml)
11
+ end
12
+
13
+ context 'xpaths' do
14
+ it 'fetches the introduction' do
15
+ destination_xml.expects(:xpath).with('.//introductory/introduction/overview').returns(true)
16
+ @destination.introduction
17
+ end
18
+
19
+ it 'fetches the history' do
20
+ destination_xml.expects(:xpath).with('./history/history/history').returns(true)
21
+ @destination.history
22
+ end
23
+
24
+ ['dangers_and_annoyances', 'while_youre_there', 'before_you_go', 'money_and_costs/money'].each do |name|
25
+ it "fetches the practical_information with #{name}" do
26
+ destination_xml.stubs(:xpath).returns([])
27
+ destination_xml.expects(:xpath).with(".//practical_information/health_and_safety/#{name}").returns([])
28
+ @destination.practical_information
29
+ end
30
+ end
31
+
32
+ it 'fetches the transport' do
33
+ destination_xml.expects(:xpath).with('.//transport/getting_around').returns(true)
34
+ @destination.transport
35
+ end
36
+
37
+ it 'fetches the weather' do
38
+ destination_xml.expects(:xpath).with('.//weather').returns(true)
39
+ @destination.weather
40
+ end
41
+
42
+ it 'fetches the work_live_study' do
43
+ destination_xml.expects(:xpath).with('.//work_live_study').returns(true)
44
+ @destination.work_live_study
45
+ end
46
+ end
47
+
48
+ it 'returns the hash' do
49
+ expect(@destination.to_hash).to eq(
50
+ {
51
+ introduction: @destination.introduction,
52
+ history: @destination.history,
53
+ practical_information: @destination.practical_information,
54
+ transport: @destination.transport,
55
+ weather: @destination.weather,
56
+ work_live_study: @destination.work_live_study
57
+ }.each_with_object({}) do |(key, value), hsh|
58
+ hsh[key] = elements_with_content(value)
59
+ hsh
60
+ end)
61
+ end
62
+ end