lib/hash_delegator.rb in markdown_exec-1.7 vs lib/hash_delegator.rb in markdown_exec-1.8
- old
+ new
@@ -16,10 +16,11 @@
require_relative 'array_util'
require_relative 'block_label'
require_relative 'block_types'
require_relative 'cached_nested_file_reader'
require_relative 'constants'
+require_relative 'directory_searcher'
require_relative 'exceptions'
require_relative 'fcb'
require_relative 'filter'
require_relative 'fout'
require_relative 'hash'
@@ -32,10 +33,60 @@
def non_empty?
!empty?
end
end
+# This module provides methods for compacting and converting data structures.
+module CompactionHelpers
+ # Converts an array of key-value pairs into a hash, applying compaction to the values.
+ # Each value is processed by `compact_hash` to remove ineligible elements.
+ #
+ # @param array [Array] The array of key-value pairs to be converted.
+ # @return [Hash] A hash with keys from the array and compacted values.
+ def compact_and_convert_array_to_hash(array)
+ array.transform_values do |value|
+ compact_hash(value)
+ end
+ end
+
+ # Compacts a hash by removing ineligible elements.
+ # It filters out nil, empty arrays, empty hashes, and empty strings from its values.
+ # It also removes entries with :random as the key.
+ #
+ # @param hash [Hash] The hash to be compacted.
+ # @return [Hash] A compacted version of the input hash.
+ def compact_hash(hash)
+ hash.map do |key, value|
+ next if value_ineligible?(value) || key == :random
+
+ [key, value]
+ end.compact.to_h
+ end
+
+ # Converts a hash into another hash with indexed keys, applying compaction to the values.
+ # The keys are indexed, and the values are compacted using `compact_and_convert_array_to_hash`.
+ #
+ # @param hash [Hash] The hash to be converted and compacted.
+ # @return [Hash] A hash with indexed keys and the compacted original values.
+ def compact_and_index_hash(hash)
+ compact_and_convert_array_to_hash(hash.map.with_index do |value, index|
+ [index, value]
+ end.to_h)
+ end
+
+ private
+
+ # Determines if a value is ineligible for inclusion in a compacted hash.
+ # Ineligible values are nil, empty arrays, empty hashes, and empty strings.
+ #
+ # @param value [Object] The value to be checked.
+ # @return [Boolean] True if the value is ineligible, false otherwise.
+ def value_ineligible?(value)
+ [nil, [], {}, ''].include?(value)
+ end
+end
+
module MarkdownExec
class DebugHelper
# Class-level variable to store history of printed messages
@@printed_messages = Set.new
@@ -49,16 +100,20 @@
@@printed_messages.add(str)
end
end
class HashDelegator
- attr_accessor :run_state
+ attr_accessor :most_recent_loaded_filename, :pass_args, :run_state
+ include CompactionHelpers
+
def initialize(delegate_object = {})
@delegate_object = delegate_object
@prompt = tty_prompt_without_disabled_symbol
+ @most_recent_loaded_filename = nil
+ @pass_args = []
@run_state = OpenStruct.new(
link_history: []
)
@fout = FOut.new(@delegate_object) ### slice only relevant keys
@@ -265,24 +320,52 @@
return false
end
true
end
+ def runtime_exception(exception_sym, name, items)
+ if @delegate_object[exception_sym] != 0
+ data = { name: name, detail: items.join(', ') }
+ warn(
+ format(
+ @delegate_object.fetch(:exception_format_name, "\n%{name}"),
+ data
+ ).send(@delegate_object.fetch(:exception_color_name, :red)) +
+ format(
+ @delegate_object.fetch(:exception_format_detail, " - %{detail}\n"),
+ data
+ ).send(@delegate_object.fetch(:exception_color_detail, :yellow))
+ )
+ end
+ return unless (@delegate_object[exception_sym]).positive?
+
+ exit @delegate_object[exception_sym]
+ end
+
# Collects required code lines based on the selected block and the delegate object's configuration.
# If the block type is VARS, it also sets environment variables based on the block's content.
#
# @param mdoc [YourMDocClass] An instance of the MDoc class.
# @param selected [Hash] The selected block.
# @return [Array<String>] Required code blocks as an array of lines.
def collect_required_code_lines(mdoc, selected)
- if selected[:shell] == BlockType::VARS
- set_environment_variables(selected)
- end
+ set_environment_variables(selected) if selected[:shell] == BlockType::VARS
required = mdoc.collect_recursively_required_code(
- @delegate_object[:block_name], opts: @delegate_object
+ @delegate_object[:block_name],
+ label_format_above: @delegate_object[:shell_code_label_format_above],
+ label_format_below: @delegate_object[:shell_code_label_format_below]
)
+ if required[:unmet_dependencies].present?
+ warn format_and_highlight_dependencies(required[:dependencies],
+ highlight: required[:unmet_dependencies])
+ runtime_exception(:runtime_exception_error_level,
+ 'unmet_dependencies, flag: runtime_exception_error_level', required[:unmet_dependencies])
+ elsif true
+ warn format_and_highlight_dependencies(required[:dependencies],
+ highlight: [@delegate_object[:block_name]])
+ end
read_required_blocks_from_temp_file + required[:code]
end
# private
@@ -487,15 +570,15 @@
return unless fcb.title.nil? || fcb.title.empty?
fcb.derive_title_from_body
end
- def delete_blank_lines_next_to_chrome!(blocks_menu)
- blocks_menu.process_and_conditionally_delete! do |prev_item, current_item, next_item|
- (prev_item&.fetch(:chrome, nil) || next_item&.fetch(:chrome, nil)) &&
- current_item&.fetch(:chrome, nil) &&
- !current_item&.fetch(:oname).present?
+ # delete the current line if it is empty and the previous is also empty
+ def delete_consecutive_blank_lines!(blocks_menu)
+ blocks_menu.process_and_conditionally_delete! do |prev_item, current_item, _next_item|
+ prev_item&.fetch(:chrome, nil) && !prev_item&.fetch(:oname).present? &&
+ current_item&.fetch(:chrome, nil) && !current_item&.fetch(:oname).present?
end
end
# Deletes a temporary file specified by an environment variable.
# Checks if the file exists before attempting to delete it and clears the environment variable afterward.
@@ -573,12 +656,12 @@
# It sets the script block name, writes command files if required, and handles the execution
# including output formatting and summarization.
#
# @param required_lines [Array<String>] The lines of code to be executed.
# @param selected [FCB] The selected functional code block object.
- def execute_approved_block(required_lines = [], selected = FCB.new)
- set_script_block_name(selected)
+ def execute_approved_block(required_lines = [], _selected = FCB.new)
+ # set_script_block_name(selected)
write_command_file_if_needed(required_lines)
format_and_execute_command(required_lines)
post_execution_process
end
@@ -594,12 +677,11 @@
def format_and_execute_command(lines)
formatted_command = lines.flatten.join("\n")
@fout.fout fetch_color(data_sym: :script_execution_head,
color_sym: :script_execution_frame_color)
- command_execute(formatted_command,
- args: @delegate_object.fetch(:s_pass_args, []))
+ command_execute(formatted_command, args: @pass_args)
@fout.fout fetch_color(data_sym: :script_execution_tail,
color_sym: :script_execution_frame_color)
end
def post_execution_process
@@ -720,16 +802,12 @@
# @param selected [Hash] The selected item from the menu to be executed.
# @return [LoadFileNextBlock] An object indicating whether to load the next block or reuse the current one.
def handle_generic_block(mdoc, selected)
required_lines = collect_required_code_lines(mdoc, selected)
output_or_approval = @delegate_object[:output_script] || @delegate_object[:user_must_approve]
-
display_required_code(required_lines) if output_or_approval
-
allow_execution = @delegate_object[:user_must_approve] ? prompt_for_user_approval(required_lines) : true
-
- @delegate_object[:s_ir_approve] = allow_execution
execute_approved_block(required_lines, selected) if allow_execution
LoadFileNextBlock.new(LoadFile::Reuse, '')
end
@@ -767,11 +845,11 @@
# @param selected [Hash] Selected item from the menu containing a YAML body.
# @param tgt2 [Hash, nil] An optional target hash to update with YAML data.
# @return [LoadFileNextBlock] An instance indicating the next action for loading files.
def handle_opts_block(selected, tgt2 = nil)
data = YAML.load(selected[:body].join("\n"))
- data.each do |key, value|
+ (data || []).each do |key, value|
update_delegate_and_target(key, value, tgt2)
if @delegate_object[:menu_opts_set_format].present?
print_formatted_option(key,
value)
end
@@ -929,19 +1007,19 @@
# Executes a specified block once per filename.
# @param all_blocks [Array] Array of all block elements.
# @return [Boolean, nil] True if values were modified, nil otherwise.
def load_auto_blocks(all_blocks)
block_name = @delegate_object[:document_load_opts_block_name]
- unless block_name.present? && @delegate_object[:s_most_recent_filename] != @delegate_object[:filename]
+ unless block_name.present? && @most_recent_loaded_filename != @delegate_object[:filename]
return
end
block = block_find(all_blocks, :oname, block_name)
return unless block
handle_opts_block(block, @delegate_object)
- @delegate_object[:s_most_recent_filename] = @delegate_object[:filename]
+ @most_recent_loaded_filename = @delegate_object[:filename]
true
end
# DebugHelper.d ["HDmm method_name: #{method_name}", "#{first_n_caller_items 1}"]
def first_n_caller_items(n)
@@ -997,11 +1075,11 @@
all_blocks, mdoc = mdoc_and_blocks_from_nested_files
end
menu_blocks = mdoc.fcbs_per_options(@delegate_object)
add_menu_chrome_blocks!(menu_blocks)
- delete_blank_lines_next_to_chrome!(menu_blocks)
+ delete_consecutive_blank_lines!(menu_blocks) if true ### compress empty lines
[all_blocks, menu_blocks, mdoc]
end
# Formats and optionally colors a menu option based on delegate object's configuration.
# @param option_symbol [Symbol] The symbol key for the menu option in the delegate object.
@@ -1026,13 +1104,13 @@
option_value
end
end
def next_block_name_from_command_line_arguments
- return MenuControl::Repeat unless @delegate_object[:s_cli_rest].present?
+ return MenuControl::Repeat unless @delegate_object[:input_cli_rest].present?
- @delegate_object[:block_name] = @delegate_object[:s_cli_rest].pop
+ @delegate_object[:block_name] = @delegate_object[:input_cli_rest].pop
MenuControl::Fresh
end
def output_execution_result
@fout.fout fetch_color(data_sym: :execution_report_preview_head,
@@ -1207,11 +1285,11 @@
#
# This method allows the user to interactively select a code block from a
# Markdown document, obtain approval, and execute the chosen block of code.
#
# @return [Nil] Returns nil if no code block is selected or an error occurs.
- def select_approve_and_execute_block
+ def select_approve_and_execute_block(_execute: true)
@menu_base_options = @delegate_object
repeat_menu = @menu_base_options[:block_name].present? ? MenuControl::Fresh : MenuControl::Repeat
load_file_next_block = LoadFileNextBlock.new(LoadFile::Reuse)
default = nil
@@ -1223,23 +1301,39 @@
@delegate_object = @menu_base_options.dup
@menu_base_options[:filename] = @menu_state_filename
@menu_base_options[:block_name] = @menu_state_block_name
@menu_state_filename = nil
@menu_state_block_name = nil
-
@menu_user_clicked_back_link = false
+
blocks_in_file, menu_blocks, mdoc = mdoc_menu_and_blocks_from_nested_files
+ if @delegate_object[:dump_blocks_in_file]
+ warn format_and_highlight_dependencies(
+ compact_and_index_hash(blocks_in_file),
+ label: 'blocks_in_file'
+ )
+ end
+ if @delegate_object[:dump_menu_blocks]
+ warn format_and_highlight_dependencies(
+ compact_and_index_hash(menu_blocks),
+ label: 'menu_blocks'
+ )
+ end
block_state = command_or_user_selected_block(blocks_in_file,
menu_blocks, default)
return if block_state.state == MenuState::EXIT
if block_state.block.nil?
warn_format('select_approve_and_execute_block', "Block not found -- #{@delegate_object[:block_name]}",
{ abort: true })
# error_handler("Block not found -- #{opts[:block_name]}", { abort: true })
end
+ if @delegate_object[:dump_selected_block]
+ warn block_state.block.to_yaml.sub(/^(?:---\n)?/, "Block:\n")
+ end
+
load_file_next_block = approve_and_execute_block(block_state.block,
mdoc)
default = load_file_next_block.load_file == LoadFile::Load ? nil : @delegate_object[:block_name]
@menu_base_options[:block_name] =
@delegate_object[:block_name] = load_file_next_block.next_block
@@ -1467,15 +1561,19 @@
prompt_title = string_send_color(
@delegate_object[:prompt_select_block].to_s, :prompt_color_after_script_execution
)
block_menu = prepare_blocks_menu(menu_blocks)
- if block_menu.empty?
- return SelectedBlockMenuState.new(nil, MenuState::EXIT)
- end
+ return SelectedBlockMenuState.new(nil, MenuState::EXIT) if block_menu.empty?
- selection_opts = default ? @delegate_object.merge(default: default) : @delegate_object
+ # default value may not match if color is different from originating menu (opts changed while processing)
+ selection_opts = if default && menu_blocks.map(&:dname).include?(default)
+ @delegate_object.merge(default: default)
+ else
+ @delegate_object
+ end
+
selection_opts.merge!(per_page: @delegate_object[:select_page_height])
selected_option = select_option_with_metadata(prompt_title, block_menu,
selection_opts)
determine_block_state(selected_option)
@@ -1538,11 +1636,12 @@
# @param block_name [String] The name of the block to collect code for.
def write_required_blocks_to_temp_file(mdoc, block_name)
c1 = if mdoc
mdoc.collect_recursively_required_code(
block_name,
- opts: @delegate_object
+ label_format_above: @delegate_object[:shell_code_label_format_above],
+ label_format_below: @delegate_object[:shell_code_label_format_below]
)[:code]
else
[]
end
@@ -1595,20 +1694,18 @@
def test_calling_execute_approved_block_calls_command_execute_with_argument_args_value
pigeon = 'E'
obj = {
output_execution_label_format: '',
output_execution_label_name_color: 'plain',
- output_execution_label_value_color: 'plain',
- s_pass_args: pigeon
- # shell: 'bash'
+ output_execution_label_value_color: 'plain'
}
c = MarkdownExec::HashDelegator.new(obj)
+ c.pass_args = pigeon
# Expect that method opts_command_execute is called with argument args having value pigeon
c.expects(:command_execute).with(
- # obj,
'',
args: pigeon
)
# Call method opts_execute_approved_block
@@ -2277,12 +2374,10 @@
assert_instance_of LoadFileNextBlock, result
assert_equal 'value2',
@hd.instance_variable_get(:@delegate_object)[:option2]
end
-
- # Additional test cases can be added to cover more scenarios and edge cases.
end
# require 'stringio'
class TestHashDelegatorHandleStream < Minitest::Test
@@ -2346,12 +2441,10 @@
ENV[MDE_HISTORY_ENV_NAME] = ''
result = @hd.history_state_partition
assert_equal({ unit: '', rest: '' }, result)
end
-
- # Additional test cases can be added to cover more scenarios and edge cases.
end
class TestHashDelegatorHistoryStatePop < Minitest::Test
def setup
@hd = HashDelegator.new
@@ -2374,12 +2467,10 @@
@hd.instance_variable_get(:@delegate_object)[:filename]
assert_equal 'history_data',
ENV.fetch(MDE_HISTORY_ENV_NAME, nil)
assert_empty @hd.instance_variable_get(:@run_state).link_history
end
-
- # Additional test cases can be added to cover more scenarios and edge cases.
end
class TestHashDelegatorHistoryStatePush < Minitest::Test
def setup
@hd = HashDelegator.new
@@ -2408,12 +2499,10 @@
ENV.fetch(MDE_HISTORY_ENV_NAME, nil)
assert_includes @hd.instance_variable_get(:@run_state).link_history,
{ block_name: 'selected_block',
filename: 'data.md' }
end
-
- # Additional test cases can be added to cover more scenarios and edge cases.
end
class TestHashDelegatorIterBlocksFromNestedFiles < Minitest::Test
def setup
@hd = HashDelegator.new
@@ -2445,35 +2534,33 @@
end
class TestHashDelegatorLoadAutoBlocks < Minitest::Test
def setup
@hd = HashDelegator.new
- @hd.instance_variable_set(:@delegate_object, {
- document_load_opts_block_name: 'load_block',
- s_most_recent_filename: 'old_file',
- filename: 'new_file'
- })
- @hd.stubs(:block_find).returns({}) # Assuming it returns a block
+ @hd.stubs(:block_find).returns({})
@hd.stubs(:handle_opts_block)
end
def test_load_auto_blocks_with_new_filename
+ @hd.instance_variable_set(:@delegate_object, {
+ document_load_opts_block_name: 'load_block',
+ filename: 'new_file'
+ })
assert @hd.load_auto_blocks([])
end
def test_load_auto_blocks_with_same_filename
@hd.instance_variable_set(:@delegate_object, {
document_load_opts_block_name: 'load_block',
- s_most_recent_filename: 'new_file',
filename: 'new_file'
})
+ @hd.instance_variable_set(:@most_recent_loaded_filename, 'new_file')
assert_nil @hd.load_auto_blocks([])
end
def test_load_auto_blocks_without_block_name
@hd.instance_variable_set(:@delegate_object, {
document_load_opts_block_name: nil,
- s_most_recent_filename: 'old_file',
filename: 'new_file'
})
assert_nil @hd.load_auto_blocks([])
end
end