config_files 0.1.6 → 0.2.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.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +94 -0
  3. data/.gitignore +2 -3
  4. data/.rubocop.yml +81 -0
  5. data/CHANGELOG.md +154 -0
  6. data/Gemfile +12 -1
  7. data/MULTI_RUBY_SETUP.md +158 -0
  8. data/README.md +246 -23
  9. data/Rakefile +26 -3
  10. data/TESTING.md +226 -0
  11. data/config_files.gemspec +12 -9
  12. data/docker/Dockerfile.test +32 -0
  13. data/lib/config_files/file_factory.rb +7 -3
  14. data/lib/config_files/loader_factory.rb +20 -11
  15. data/lib/config_files/loaders/base_parser.rb +133 -0
  16. data/lib/config_files/loaders/conf.rb +61 -0
  17. data/lib/config_files/loaders/default.rb +3 -1
  18. data/lib/config_files/loaders/ini.rb +48 -0
  19. data/lib/config_files/loaders/json.rb +3 -1
  20. data/lib/config_files/loaders/xml.rb +67 -0
  21. data/lib/config_files/loaders/yaml.rb +2 -0
  22. data/lib/config_files/loaders.rb +6 -1
  23. data/lib/config_files/version.rb +3 -1
  24. data/lib/config_files.rb +34 -18
  25. data/lib/meta.rb +3 -1
  26. data/scripts/install_rubies_asdf.sh +187 -0
  27. data/scripts/test_docker.sh +91 -0
  28. data/scripts/test_multiple_rubies.sh +290 -0
  29. data/test/comprehensive_multi_directory_test.rb +168 -0
  30. data/test/config/dummy.json +10 -0
  31. data/test/config/dummy.yml +6 -0
  32. data/test/config_files_test.rb +10 -8
  33. data/test/etc/dummy.conf +14 -2
  34. data/test/etc/dummy.ini +12 -0
  35. data/test/etc/dummy.xml +13 -0
  36. data/test/etc/dummy.yml +2 -2
  37. data/test/loader_factory_test.rb +10 -10
  38. data/test/loaders_test.rb +362 -0
  39. data/test/local/dummy.json +10 -0
  40. data/test/local/dummy.yml +6 -0
  41. data/test/mixed_format_test.rb +152 -0
  42. data/test/multi_directory_test.rb +126 -0
  43. data/test/test_helper.rb +3 -0
  44. metadata +49 -26
  45. data/Gemfile.lock +0 -34
@@ -0,0 +1,133 @@
1
+ module ConfigFiles
2
+ module Loaders
3
+ # Base class providing common parsing functionality for configuration file loaders
4
+ class BaseParser
5
+ class << self
6
+ def call(file_name)
7
+ content = File.read(file_name)
8
+ parse(content)
9
+ end
10
+
11
+ private
12
+
13
+ # Template method to be implemented by subclasses
14
+ def parse(content)
15
+ raise NotImplementedError, "Subclasses must implement #parse"
16
+ end
17
+
18
+ # Common value parsing logic shared across parsers
19
+ def parse_value(value)
20
+ return nil if value.nil?
21
+ return value if value.empty?
22
+
23
+ # Try to parse as boolean
24
+ boolean_result = boolean_value?(value)
25
+ return boolean_result unless boolean_result.nil?
26
+
27
+ # Try to parse as number
28
+ number_value = parse_number(value)
29
+ return number_value unless number_value.nil?
30
+
31
+ # Return as string
32
+ value
33
+ end
34
+
35
+ # Parse boolean values with common representations
36
+ def boolean_value?(value)
37
+ case value.downcase
38
+ when 'true', 'yes', 'on', '1'
39
+ true
40
+ when 'false', 'no', 'off', '0'
41
+ false
42
+ end
43
+ end
44
+
45
+ # Parse numeric values (integers and floats)
46
+ def parse_number(value)
47
+ return value.to_i if integer?(value)
48
+ return value.to_f if float?(value)
49
+
50
+ nil
51
+ end
52
+
53
+ # Check if string represents an integer
54
+ def integer?(value)
55
+ value.match?(/^\d+$/)
56
+ end
57
+
58
+ # Check if string represents a float
59
+ def float?(value)
60
+ value.match?(/^\d+\.\d+$/)
61
+ end
62
+
63
+ # Remove surrounding quotes from a value
64
+ def unquote(value)
65
+ value.gsub(/^["']|["']$/, '')
66
+ end
67
+
68
+ # Check if line is a comment based on comment prefixes
69
+ def comment?(line, prefixes = ['#'])
70
+ prefixes.any? { |prefix| line.start_with?(prefix) }
71
+ end
72
+
73
+ # Check if line is empty or whitespace only
74
+ def empty_line?(line)
75
+ line.strip.empty?
76
+ end
77
+
78
+ # Skip processing for comments and empty lines
79
+ def skip_line?(line, comment_prefixes = ['#'])
80
+ empty_line?(line) || comment?(line, comment_prefixes)
81
+ end
82
+
83
+ # Parse section header like [section_name]
84
+ def parse_section_header(line)
85
+ match = line.match(/^\[(.+)\]$/)
86
+ match ? match[1].strip : nil
87
+ end
88
+
89
+ # Check if line is a section header
90
+ def section_header?(line)
91
+ line.match?(/^\[.+\]$/)
92
+ end
93
+
94
+ # Set nested value using dot notation (e.g., "server.host" -> {"server" => {"host" => value}})
95
+ def set_nested_value(hash, key_path, value, section = nil)
96
+ keys = key_path.split('.')
97
+ target = section ? (hash[section] ||= {}) : hash
98
+
99
+ # Navigate to the nested location
100
+ keys[0..-2].each do |key|
101
+ target = (target[key] ||= {})
102
+ end
103
+
104
+ # Set the final value
105
+ target[keys.last] = value
106
+ end
107
+
108
+ # Check if key contains dot notation for nesting
109
+ def nested_key?(key)
110
+ key.include?('.')
111
+ end
112
+
113
+ # Set value in hash, handling sections and nesting
114
+ def set_value(hash, key, value, current_section = nil)
115
+ parsed_value = parse_value(value)
116
+
117
+ if nested_key?(key)
118
+ set_nested_value(hash, key, parsed_value, current_section)
119
+ elsif current_section
120
+ hash[current_section][key] = parsed_value
121
+ else
122
+ hash[key] = parsed_value
123
+ end
124
+ end
125
+
126
+ # Initialize section in hash if it doesn't exist
127
+ def ensure_section(hash, section_name)
128
+ hash[section_name] ||= {}
129
+ end
130
+ end
131
+ end
132
+ end
133
+ end
@@ -0,0 +1,61 @@
1
+ require_relative 'base_parser'
2
+
3
+ module ConfigFiles
4
+ module Loaders
5
+ class Conf < BaseParser
6
+ # Supported key-value separators in order of preference
7
+ SEPARATORS = ['=', ':', ' '].freeze
8
+
9
+ class << self
10
+ private
11
+
12
+ def parse(content)
13
+ result = {}
14
+ current_section = nil
15
+
16
+ content.each_line do |line|
17
+ line = line.strip
18
+ next if skip_line?(line)
19
+
20
+ if section_header?(line)
21
+ current_section = parse_section_header(line)
22
+ ensure_section(result, current_section)
23
+ next
24
+ end
25
+
26
+ key, value = parse_conf_line(line)
27
+ next unless key && value
28
+
29
+ set_value(result, key, value, current_section)
30
+ end
31
+
32
+ result
33
+ end
34
+
35
+ # Parse CONF format line supporting multiple syntaxes:
36
+ # key=value, key: value, key value (space-separated)
37
+ def parse_conf_line(line)
38
+ key, value = extract_key_value_pair(line)
39
+ return [nil, nil] unless key && value
40
+
41
+ key = key.strip
42
+ value = unquote(value.strip)
43
+
44
+ [key, value]
45
+ end
46
+
47
+ # Extract key-value pair from line using different separators
48
+ def extract_key_value_pair(line)
49
+ SEPARATORS.each do |separator|
50
+ if line.include?(separator)
51
+ parts = line.split(separator, 2)
52
+ return parts if parts.length == 2
53
+ end
54
+ end
55
+
56
+ [nil, nil]
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
@@ -1 +1,3 @@
1
- ConfigFiles::Loaders::Default=ConfigFiles::Loaders::Yaml
1
+ # frozen_string_literal: true
2
+
3
+ ConfigFiles::Loaders::Default = ConfigFiles::Loaders::Yaml
@@ -0,0 +1,48 @@
1
+ require_relative 'base_parser'
2
+
3
+ module ConfigFiles
4
+ module Loaders
5
+ class Ini < BaseParser
6
+ # INI files support both # and ; for comments
7
+ COMMENT_PREFIXES = ['#', ';'].freeze
8
+
9
+ class << self
10
+ private
11
+
12
+ def parse(content)
13
+ result = {}
14
+ current_section = nil
15
+
16
+ content.each_line do |line|
17
+ line = line.strip
18
+ next if skip_line?(line, COMMENT_PREFIXES)
19
+
20
+ if section_header?(line)
21
+ current_section = parse_section_header(line)
22
+ ensure_section(result, current_section)
23
+ next
24
+ end
25
+
26
+ key, value = parse_ini_line(line)
27
+ next unless key && value
28
+
29
+ set_value(result, key, value, current_section)
30
+ end
31
+
32
+ result
33
+ end
34
+
35
+ # Parse INI format line (key=value only)
36
+ def parse_ini_line(line)
37
+ return [nil, nil] unless line.include?('=')
38
+
39
+ key, value = line.split('=', 2)
40
+ key = key.strip
41
+ value = unquote(value.strip)
42
+
43
+ [key, value]
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'json'
2
4
 
3
5
  module ConfigFiles
@@ -5,7 +7,7 @@ module ConfigFiles
5
7
  class Json
6
8
  class << self
7
9
  def call(file_name, object_class: ::Hash)
8
- ::JSON.load(::File.open(file_name), nil, {object_class: object_class, quirks_mode: true})
10
+ ::JSON.parse(::File.read(file_name), { object_class: object_class, quirks_mode: true })
9
11
  end
10
12
  end
11
13
  end
@@ -0,0 +1,67 @@
1
+ require 'rexml/document'
2
+ require_relative 'base_parser'
3
+
4
+ module ConfigFiles
5
+ module Loaders
6
+ class Xml < BaseParser
7
+ class << self
8
+ private
9
+
10
+ def parse(content)
11
+ doc = REXML::Document.new(content)
12
+ doc.root ? parse_element(doc.root) : {}
13
+ end
14
+
15
+ def parse_element(element)
16
+ result = parse_attributes(element)
17
+ parse_child_elements(element, result)
18
+
19
+ # Return text content if element has no children or attributes
20
+ return parse_text_content(element) if result.empty? && element.has_text?
21
+
22
+ result
23
+ end
24
+
25
+ # Parse element attributes, prefixing with @
26
+ def parse_attributes(element)
27
+ result = {}
28
+ element.attributes.each do |name, value|
29
+ result["@#{name}"] = parse_value(value)
30
+ end
31
+ result
32
+ end
33
+
34
+ # Parse child elements, handling duplicates and nesting
35
+ def parse_child_elements(element, result)
36
+ element.elements.each do |child|
37
+ key = child.name
38
+ child_value = child.has_elements? ? parse_element(child) : parse_leaf_element(child)
39
+
40
+ if result.key?(key)
41
+ result[key] = convert_to_array(result[key])
42
+ result[key] << child_value
43
+ else
44
+ result[key] = child_value
45
+ end
46
+ end
47
+ end
48
+
49
+ # Parse leaf element (no child elements)
50
+ def parse_leaf_element(element)
51
+ text = element.text
52
+ text ? parse_value(text.strip) : nil
53
+ end
54
+
55
+ # Parse text content of element
56
+ def parse_text_content(element)
57
+ parse_value(element.text.strip)
58
+ end
59
+
60
+ # Convert single value to array for handling multiple elements with same name
61
+ def convert_to_array(value)
62
+ value.is_a?(Array) ? value : [value]
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ConfigFiles
2
4
  module Loaders
3
5
  class Yaml
@@ -1,3 +1,8 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative 'loaders/json'
2
4
  require_relative 'loaders/yaml'
3
- require_relative 'loaders/default'
5
+ require_relative 'loaders/conf'
6
+ require_relative 'loaders/ini'
7
+ require_relative 'loaders/xml'
8
+ require_relative 'loaders/default'
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ConfigFiles
2
- VERSION='0.1.6'
4
+ VERSION = '0.2.1'
3
5
  end
data/lib/config_files.rb CHANGED
@@ -1,7 +1,10 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'config_files/file_factory'
2
4
  require 'config_files/loader_factory'
3
5
  require 'config_files/loaders'
4
6
  require 'config_files/version'
7
+ require 'active_support/core_ext/hash/indifferent_access'
5
8
  require 'active_support/core_ext/hash/deep_merge'
6
9
  require 'active_support/core_ext/object/blank'
7
10
 
@@ -9,8 +12,8 @@ require 'meta'
9
12
  require 'yaml'
10
13
 
11
14
  class NoDirectoryEntry < NoMethodError; end
12
- module ConfigFiles
13
15
 
16
+ module ConfigFiles
14
17
  class << self
15
18
  def included(base)
16
19
  base.class_eval do
@@ -25,7 +28,7 @@ module ConfigFiles
25
28
 
26
29
  def self.extended(base)
27
30
  base.instance_eval do
28
- self.directories=default_directories
31
+ self.directories = default_directories
29
32
  end
30
33
  end
31
34
 
@@ -38,21 +41,23 @@ module ConfigFiles
38
41
  end
39
42
 
40
43
  def default_directories
41
- { :etc => ['config', 'etc', '/etc'] }
44
+ { etc: ['config', 'etc', '/etc'] }
42
45
  end
43
46
 
44
47
  def config_directories(*arr)
45
- self.directories||=default_directories
48
+ self.directories ||= default_directories
46
49
  arr.each do |directory_list|
47
50
  directory_list.each do |key, value|
48
- self.directories[key]=value.map { |dir| ::File.expand_path(dir) }
51
+ self.directories[key] = value.map { |dir| ::File.expand_path(dir) }
49
52
  meta_def("#{key}_dir") { @directories[key] }
50
53
  end
51
54
  end
52
55
  end
53
56
 
54
57
  def merged_hash(file)
55
- config_files(file).inject({}) { |master, file| master.deep_merge(FileFactory.(file)) }
58
+ all_config_files(file).inject(::HashWithIndifferentAccess.new) do |master, file|
59
+ master.deep_merge(FileFactory.call(file))
60
+ end
56
61
  end
57
62
 
58
63
  def build_combined(file)
@@ -61,7 +66,7 @@ module ConfigFiles
61
66
 
62
67
  def static_config_files(*arr)
63
68
  arr.each do |file|
64
- content=build_combined(file)
69
+ content = build_combined(file)
65
70
  meta_def(file) { content }
66
71
  end
67
72
  end
@@ -72,27 +77,38 @@ module ConfigFiles
72
77
  end
73
78
  end
74
79
 
75
- alias_method :config_files, :dynamic_config_files
80
+ alias config_files dynamic_config_files
76
81
 
77
82
  private
83
+
78
84
  def directory_listing(directory, file)
79
- ::Dir.glob(::File.join(directory, "#{file}.*"))
85
+ ::Dir.glob(::File.join(directory, "#{file}.*")).sort
80
86
  end
81
87
 
82
- def first_directory(file, key=config_key)
83
- begin
84
- self.directories[key]&.detect { |directory| directory_listing(directory, file).presence } || ''
85
- rescue NoMethodError=>e
86
- raise NoDirectoryEntry, "Unable to find #{key} in #{self.directories}"
87
- end
88
+ def first_directory(file, key = config_key)
89
+ self.directories[key]&.detect { |directory| directory_listing(directory, file).presence } || ''
90
+ rescue NoMethodError
91
+ raise NoDirectoryEntry, "Unable to find #{key} in #{self.directories}"
88
92
  end
89
93
 
90
- def files(file, key=config_key)
94
+ def files(file, key = config_key)
91
95
  directory_listing(first_directory(file, key), file)
92
96
  end
93
97
 
94
- def config_files(file, key=config_key)
95
- files(file, key)
98
+ def all_config_files(file, key = config_key)
99
+ return [] unless self.directories && self.directories[key]
100
+
101
+ # Collect files by directory, maintaining alphabetical order within each directory
102
+ files_by_directory = []
103
+ self.directories[key].each do |directory|
104
+ if ::File.directory?(directory)
105
+ directory_files = directory_listing(directory, file)
106
+ files_by_directory << directory_files if directory_files.any?
107
+ end
108
+ end
109
+
110
+ # Reverse directory order but keep file order within each directory
111
+ files_by_directory.reverse.flatten
96
112
  end
97
113
  end
98
114
  end
data/lib/meta.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # @author Paul McKibbin based on meta definitions by _why_the_lucky_stiff_
2
4
  module Meta
3
5
  def meta_self
@@ -6,7 +8,7 @@ module Meta
6
8
  end
7
9
  end
8
10
 
9
- def meta_def name, &blk
11
+ def meta_def(name, &blk)
10
12
  meta_self.instance_eval { define_method name, &blk }
11
13
  end
12
14
  end
@@ -0,0 +1,187 @@
1
+ #!/bin/bash
2
+
3
+ # Script to install all Ruby versions needed for testing using asdf
4
+ # This is a specialized script for asdf users
5
+
6
+ set -e
7
+
8
+ # Colors for output
9
+ RED='\033[0;31m'
10
+ GREEN='\033[0;32m'
11
+ YELLOW='\033[1;33m'
12
+ BLUE='\033[0;34m'
13
+ NC='\033[0m' # No Color
14
+
15
+ # Ruby versions to install
16
+ RUBY_VERSIONS=("2.7.8" "3.0.6" "3.1.4" "3.2.2" "3.3.0" "3.4.1")
17
+
18
+ echo -e "${BLUE}ConfigFiles Ruby Installation Script (asdf)${NC}"
19
+ echo -e "${BLUE}===========================================${NC}"
20
+
21
+ # Function to check if asdf is installed and configured
22
+ check_asdf() {
23
+ if ! command -v asdf >/dev/null 2>&1; then
24
+ echo -e "${RED}asdf not found. Please install asdf first.${NC}"
25
+ echo -e "${BLUE}Installation instructions:${NC}"
26
+ echo -e " macOS: brew install asdf"
27
+ echo -e " Linux: git clone https://github.com/asdf-vm/asdf.git ~/.asdf --branch v0.14.0"
28
+ echo -e " More info: https://asdf-vm.com/guide/getting-started.html"
29
+ exit 1
30
+ fi
31
+
32
+ echo -e "${GREEN}✓ asdf found${NC}"
33
+
34
+ # Check if ruby plugin is installed
35
+ if ! asdf plugin list | grep -q "^ruby$"; then
36
+ echo -e "${YELLOW}Installing Ruby plugin...${NC}"
37
+ if asdf plugin add ruby https://github.com/asdf-vm/asdf-ruby.git; then
38
+ echo -e "${GREEN}✓ Ruby plugin installed${NC}"
39
+ else
40
+ echo -e "${RED}✗ Failed to install Ruby plugin${NC}"
41
+ exit 1
42
+ fi
43
+ else
44
+ echo -e "${GREEN}✓ Ruby plugin already installed${NC}"
45
+ fi
46
+ }
47
+
48
+ # Function to check if Ruby version is installed
49
+ check_ruby_version() {
50
+ local version=$1
51
+ asdf list ruby 2>/dev/null | grep -q "^\s*${version}$"
52
+ }
53
+
54
+ # Function to install Ruby version
55
+ install_ruby_version() {
56
+ local version=$1
57
+ echo -e "${YELLOW}Installing Ruby ${version}...${NC}"
58
+
59
+ # Show progress
60
+ if asdf install ruby "$version"; then
61
+ echo -e "${GREEN}✓ Ruby ${version} installed successfully${NC}"
62
+ return 0
63
+ else
64
+ echo -e "${RED}✗ Failed to install Ruby ${version}${NC}"
65
+ return 1
66
+ fi
67
+ }
68
+
69
+ # Function to show system requirements
70
+ show_requirements() {
71
+ echo -e "\n${BLUE}System Requirements:${NC}"
72
+ echo -e "Before installing Ruby versions, make sure you have the required dependencies:"
73
+ echo
74
+
75
+ if [[ "$OSTYPE" == "darwin"* ]]; then
76
+ echo -e "${YELLOW}macOS:${NC}"
77
+ echo -e " brew install openssl readline sqlite3 xz zlib"
78
+ echo -e " xcode-select --install"
79
+ elif [[ "$OSTYPE" == "linux-gnu"* ]]; then
80
+ echo -e "${YELLOW}Ubuntu/Debian:${NC}"
81
+ echo -e " sudo apt-get install -y build-essential libssl-dev libreadline-dev zlib1g-dev"
82
+ echo -e " sudo apt-get install -y libsqlite3-dev libxml2-dev libxslt1-dev libcurl4-openssl-dev"
83
+ echo -e " sudo apt-get install -y libffi-dev libyaml-dev"
84
+ echo
85
+ echo -e "${YELLOW}CentOS/RHEL/Fedora:${NC}"
86
+ echo -e " sudo yum groupinstall -y 'Development Tools'"
87
+ echo -e " sudo yum install -y openssl-devel readline-devel zlib-devel sqlite-devel"
88
+ fi
89
+
90
+ echo
91
+ read -p "Have you installed the required dependencies? (y/N): " -n 1 -r
92
+ echo
93
+ if [[ ! $REPLY =~ ^[Yy]$ ]]; then
94
+ echo -e "${YELLOW}Please install the dependencies first, then run this script again.${NC}"
95
+ exit 1
96
+ fi
97
+ }
98
+
99
+ # Main execution
100
+ echo -e "\n${YELLOW}This script will install the following Ruby versions using asdf:${NC}"
101
+ for version in "${RUBY_VERSIONS[@]}"; do
102
+ echo -e " - Ruby ${version}"
103
+ done
104
+
105
+ echo
106
+ read -p "Continue? (y/N): " -n 1 -r
107
+ echo
108
+ if [[ ! $REPLY =~ ^[Yy]$ ]]; then
109
+ echo -e "${YELLOW}Installation cancelled.${NC}"
110
+ exit 0
111
+ fi
112
+
113
+ # Check asdf installation
114
+ check_asdf
115
+
116
+ # Show system requirements
117
+ show_requirements
118
+
119
+ # Check current installations
120
+ echo -e "\n${BLUE}Checking current Ruby installations...${NC}"
121
+ already_installed=()
122
+ to_install=()
123
+
124
+ for ruby_version in "${RUBY_VERSIONS[@]}"; do
125
+ if check_ruby_version "$ruby_version"; then
126
+ echo -e "${GREEN}✓ Ruby ${ruby_version} is already installed${NC}"
127
+ already_installed+=("$ruby_version")
128
+ else
129
+ echo -e "${YELLOW}○ Ruby ${ruby_version} needs to be installed${NC}"
130
+ to_install+=("$ruby_version")
131
+ fi
132
+ done
133
+
134
+ if [ ${#to_install[@]} -eq 0 ]; then
135
+ echo -e "\n${GREEN}All Ruby versions are already installed! 🎉${NC}"
136
+ exit 0
137
+ fi
138
+
139
+ # Install missing versions
140
+ echo -e "\n${BLUE}Installing ${#to_install[@]} Ruby versions...${NC}"
141
+ failed_installations=()
142
+ successful_installations=()
143
+
144
+ for ruby_version in "${to_install[@]}"; do
145
+ echo -e "\n${YELLOW}[$(date '+%H:%M:%S')] Installing Ruby ${ruby_version}...${NC}"
146
+
147
+ if install_ruby_version "$ruby_version"; then
148
+ successful_installations+=("$ruby_version")
149
+ else
150
+ failed_installations+=("$ruby_version")
151
+ echo -e "${RED}Installation of Ruby ${ruby_version} failed. Continuing with others...${NC}"
152
+ fi
153
+ done
154
+
155
+ # Summary
156
+ echo -e "\n${BLUE}=== Installation Summary ===${NC}"
157
+ echo -e "Already installed: ${#already_installed[@]}"
158
+ echo -e "${GREEN}Successfully installed: ${#successful_installations[@]}${NC}"
159
+ echo -e "${RED}Failed installations: ${#failed_installations[@]}${NC}"
160
+
161
+ if [ ${#successful_installations[@]} -gt 0 ]; then
162
+ echo -e "\n${GREEN}Successfully installed:${NC}"
163
+ for version in "${successful_installations[@]}"; do
164
+ echo -e "${GREEN} ✓ Ruby ${version}${NC}"
165
+ done
166
+ fi
167
+
168
+ if [ ${#failed_installations[@]} -gt 0 ]; then
169
+ echo -e "\n${RED}Failed installations:${NC}"
170
+ for version in "${failed_installations[@]}"; do
171
+ echo -e "${RED} ✗ Ruby ${version}${NC}"
172
+ done
173
+ echo -e "\n${YELLOW}Troubleshooting tips:${NC}"
174
+ echo -e " 1. Make sure you have all system dependencies installed"
175
+ echo -e " 2. Check asdf ruby plugin: asdf plugin update ruby"
176
+ echo -e " 3. Try installing manually: asdf install ruby <version>"
177
+ echo -e " 4. Check logs in ~/.asdf/installs/ruby/<version>/install.log"
178
+ fi
179
+
180
+ # Show next steps
181
+ echo -e "\n${BLUE}Next Steps:${NC}"
182
+ echo -e " 1. Run the test suite: ./scripts/test_multiple_rubies.sh"
183
+ echo -e " 2. Set a global Ruby version: asdf global ruby <version>"
184
+ echo -e " 3. Set a local Ruby version: asdf local ruby <version>"
185
+
186
+ total_available=$((${#already_installed[@]} + ${#successful_installations[@]}))
187
+ echo -e "\n${GREEN}You now have ${total_available} Ruby versions available for testing! 🚀${NC}"