bugsage 0.2.0

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 (69) hide show
  1. checksums.yaml +7 -0
  2. data/ARCHITECTURE.md +442 -0
  3. data/CHANGELOG.md +28 -0
  4. data/CODE_OF_CONDUCT.md +10 -0
  5. data/CONTRIBUTING.md +301 -0
  6. data/LICENSE.txt +21 -0
  7. data/README.md +344 -0
  8. data/ROADMAP.md +217 -0
  9. data/SECURITY.md +83 -0
  10. data/docs/AI.md +167 -0
  11. data/docs/Configuration.md +168 -0
  12. data/docs/GettingStarted.md +146 -0
  13. data/docs/RELEASE_CHECKLIST.md +346 -0
  14. data/docs/Troubleshooting.md +181 -0
  15. data/docs/images/BadRequest.png +0 -0
  16. data/docs/images/BadRequest2.png +0 -0
  17. data/docs/images/BugSage_Logo.png +0 -0
  18. data/docs/images/BugSage_Social_Preview.png +0 -0
  19. data/docs/images/NoMethodError.png +0 -0
  20. data/docs/images/NoMethodError2.png +0 -0
  21. data/docs/images/README.md +38 -0
  22. data/docs/releases/README.md +21 -0
  23. data/docs/releases/v0.2.0.md +40 -0
  24. data/exe/bugsage +7 -0
  25. data/lib/bugsage/ai_analyzer.rb +145 -0
  26. data/lib/bugsage/ai_chat.rb +388 -0
  27. data/lib/bugsage/ai_context.rb +69 -0
  28. data/lib/bugsage/ai_panel.rb +708 -0
  29. data/lib/bugsage/ai_support.rb +36 -0
  30. data/lib/bugsage/auto_configurator.rb +97 -0
  31. data/lib/bugsage/cli.rb +59 -0
  32. data/lib/bugsage/code_context.rb +81 -0
  33. data/lib/bugsage/code_patch.rb +147 -0
  34. data/lib/bugsage/configuration.rb +149 -0
  35. data/lib/bugsage/console_context.rb +51 -0
  36. data/lib/bugsage/cursor_client.rb +165 -0
  37. data/lib/bugsage/dashboard.rb +627 -0
  38. data/lib/bugsage/editor_links.rb +34 -0
  39. data/lib/bugsage/error_page.rb +298 -0
  40. data/lib/bugsage/exception_handler.rb +66 -0
  41. data/lib/bugsage/exception_support.rb +95 -0
  42. data/lib/bugsage/exceptions_app.rb +31 -0
  43. data/lib/bugsage/fix_applicator.rb +107 -0
  44. data/lib/bugsage/formatter.rb +37 -0
  45. data/lib/bugsage/http_error_capture.rb +104 -0
  46. data/lib/bugsage/http_response_error.rb +14 -0
  47. data/lib/bugsage/inline_console.rb +226 -0
  48. data/lib/bugsage/installation.rb +94 -0
  49. data/lib/bugsage/installer.rb +66 -0
  50. data/lib/bugsage/json_endpoint.rb +34 -0
  51. data/lib/bugsage/locales/en.yml +439 -0
  52. data/lib/bugsage/middleware.rb +152 -0
  53. data/lib/bugsage/openai_client.rb +103 -0
  54. data/lib/bugsage/page_actions.rb +255 -0
  55. data/lib/bugsage/railtie.rb +49 -0
  56. data/lib/bugsage/request_context.rb +56 -0
  57. data/lib/bugsage/rule.rb +271 -0
  58. data/lib/bugsage/session_clear.rb +35 -0
  59. data/lib/bugsage/store.rb +60 -0
  60. data/lib/bugsage/suggestion.rb +58 -0
  61. data/lib/bugsage/trace_cleaner.rb +24 -0
  62. data/lib/bugsage/translations.rb +85 -0
  63. data/lib/bugsage/version.rb +5 -0
  64. data/lib/bugsage.rb +65 -0
  65. data/lib/generators/bugsage/install/install_generator.rb +21 -0
  66. data/lib/generators/bugsage/install/templates/bugsage.rb +22 -0
  67. data/scripts/publish-github-release.sh +38 -0
  68. data/sig/bugsage.rbs +4 -0
  69. metadata +157 -0
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bugsage
4
+ # Shared helpers for AI provider responses.
5
+ module AiSupport
6
+ class << self
7
+ def extract_json(response)
8
+ stripped = response.to_s.strip
9
+ stripped = stripped.sub(/\A.*?```(?:json)?\s*/im, "").sub(/\s*```.*\z/m, "") if stripped.include?("```")
10
+
11
+ match = stripped.match(/\{.*\}/m)
12
+ match ? match[0] : stripped
13
+ end
14
+
15
+ def log_failure(label, error)
16
+ message = "[BugSage] #{label}: #{error.class}: #{error.message}"
17
+ if defined?(Rails) && Rails.respond_to?(:logger) && Rails.logger
18
+ Rails.logger.warn(message)
19
+ else
20
+ warn(message)
21
+ end
22
+ end
23
+
24
+ def build_client(config = Bugsage.configuration)
25
+ return config.ai_client if config.ai_client
26
+
27
+ case config.resolved_ai_provider
28
+ when :cursor
29
+ CursorClient.new(config: config)
30
+ else
31
+ OpenAiClient.new(config: config)
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bugsage
4
+ module AutoConfigurator
5
+ API_KEY_ENV_VARS = {
6
+ cursor: %w[CURSOR_API_KEY BUGSAGE_CURSOR_API_KEY],
7
+ openai: %w[OPENAI_API_KEY BUGSAGE_OPENAI_API_KEY]
8
+ }.freeze
9
+
10
+ module_function
11
+
12
+ def apply!(config = Bugsage.configuration, env: ENV)
13
+ apply_environment_settings!(config, env)
14
+ apply_ai_settings!(config, env)
15
+ config
16
+ end
17
+
18
+ def apply_environment_settings!(config, env)
19
+ environments = parse_enabled_environments(env["BUGSAGE_ENABLED_ENVIRONMENTS"])
20
+ return if environments.empty?
21
+
22
+ config.enabled_environments = environments
23
+ end
24
+
25
+ def apply_ai_settings!(config, env)
26
+ return if config.ai_enabled == false
27
+
28
+ credentials = detect_api_credentials(env)
29
+ return if credentials.nil?
30
+
31
+ config.ai_enabled = true if config.ai_enabled.nil?
32
+ config.ai_provider ||= credentials[:provider]
33
+
34
+ assign_api_key!(config, credentials)
35
+ end
36
+
37
+ def detect_api_credentials(env = ENV)
38
+ API_KEY_ENV_VARS.each do |provider, names|
39
+ names.each do |name|
40
+ key = env[name].to_s.strip
41
+ next if key.empty?
42
+
43
+ return { provider: normalize_provider(key, provider), key: key, source: name }
44
+ end
45
+ end
46
+
47
+ nil
48
+ end
49
+
50
+ def summary(config = Bugsage.configuration, env: ENV)
51
+ credentials = detect_api_credentials(env)
52
+ environments = Array(config.enabled_environments).join(", ")
53
+
54
+ lines = [
55
+ "BugSage is ready to use.",
56
+ "Enabled environments: #{environments}",
57
+ "Error page: #{enabled_label(config.show_error_page?("development"))}",
58
+ "Dashboard (/bugsage): #{enabled_label(config.show_dashboard?("development"))}",
59
+ "Inline console: #{enabled_label(config.show_inline_console?("development"))}"
60
+ ]
61
+
62
+ if credentials
63
+ lines << "AI provider: #{config.resolved_ai_provider} (from #{credentials[:source]})"
64
+ lines << "AI suggestions: #{enabled_label(config.ai_enabled?("development"))}"
65
+ else
66
+ lines << "AI suggestions: off (set OPENAI_API_KEY or CURSOR_API_KEY to enable)"
67
+ end
68
+
69
+ lines
70
+ end
71
+
72
+ def enabled_label(value)
73
+ value ? "enabled" : "disabled"
74
+ end
75
+
76
+ def parse_enabled_environments(raw)
77
+ return [] if raw.to_s.strip.empty?
78
+
79
+ raw.split(",").map { |name| name.strip.downcase.to_sym }.reject(&:empty?)
80
+ end
81
+
82
+ def normalize_provider(key, provider)
83
+ return :cursor if key.start_with?("crsr_")
84
+
85
+ provider
86
+ end
87
+
88
+ def assign_api_key!(config, credentials)
89
+ case credentials[:provider]
90
+ when :cursor
91
+ config.cursor_api_key ||= credentials[:key]
92
+ else
93
+ config.openai_api_key ||= credentials[:key]
94
+ end
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "thor"
4
+ require "fileutils"
5
+
6
+ module Bugsage
7
+ class CLI < Thor
8
+ desc "version", "Print BugSage version"
9
+ def version
10
+ puts "BugSage v#{Bugsage::VERSION}"
11
+ end
12
+
13
+ desc "hello", "Sanity check command"
14
+ def hello
15
+ puts "BugSage is alive."
16
+ end
17
+
18
+ desc "install", "Print install steps and generate config/initializers/bugsage.rb"
19
+ option :destination, aliases: "-d", desc: "Rails app root directory"
20
+ option :force, type: :boolean, default: false, desc: "Overwrite an existing initializer"
21
+ option :guide_only, type: :boolean, default: false, desc: "Print installation steps without writing files"
22
+ def install
23
+ if options[:guide_only]
24
+ Installation.print_guide
25
+ return
26
+ end
27
+
28
+ Installation.print_guide
29
+ say ""
30
+
31
+ result = Installer.run(destination: options[:destination] || Dir.pwd, force: options[:force])
32
+
33
+ if result[:created_initializer]
34
+ say "Created #{result[:initializer_path]}", :green
35
+ else
36
+ say "Skipped #{result[:initializer_path]} (already exists). Use --force to overwrite.", :yellow
37
+ end
38
+
39
+ say "\n#{result[:summary].join("\n")}\n"
40
+ end
41
+
42
+ desc "explain", "Run a fake exception through the rule engine (demo)"
43
+ def explain
44
+ fake_exception = begin
45
+ nil.foo
46
+ rescue NoMethodError => e
47
+ e
48
+ end
49
+
50
+ suggestion = Rule.match(fake_exception)
51
+
52
+ if suggestion
53
+ Formatter.print(suggestion)
54
+ else
55
+ puts "No matching rule found."
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "cgi"
4
+
5
+ module Bugsage
6
+ module CodeContext
7
+ module_function
8
+
9
+ def extract_location(location)
10
+ return [nil, nil] unless location
11
+
12
+ match = location.match(/^(.+):(\d+)/)
13
+ return [match[1], match[2].to_i] if match
14
+
15
+ [location, nil]
16
+ end
17
+
18
+ def render_code_context(file_path, line_number)
19
+ return "" unless file_path && line_number
20
+
21
+ lines = read_file_lines(file_path)
22
+ return "" unless lines
23
+
24
+ context_range = 5
25
+ start_line = [line_number - context_range, 1].max
26
+ end_line = [line_number + context_range, lines.length].min
27
+
28
+ code_html = ""
29
+ (start_line..end_line).each do |num|
30
+ is_error_line = num == line_number
31
+ line_content = lines[num - 1] || ""
32
+ error_class = is_error_line ? " error" : ""
33
+
34
+ code_html += "<div class=\"code-line#{error_class}\">"
35
+ code_html += "<div class=\"code-line-number\">#{num}</div>"
36
+ code_html += "<div class=\"code-line-content\">#{escape_html(line_content)}</div>"
37
+ code_html += "</div>"
38
+ end
39
+
40
+ <<~HTML
41
+ <div class="code-section">
42
+ <div class="code-header">#{escape_html(file_path)}</div>
43
+ <div class="code-block">
44
+ #{code_html}
45
+ </div>
46
+ </div>
47
+ HTML
48
+ end
49
+
50
+ def numbered_source(file_path, line_number, context_range: 20)
51
+ lines = read_file_lines(file_path)
52
+ return nil unless lines
53
+
54
+ start_line = [line_number - context_range, 1].max
55
+ end_line = [line_number + context_range, lines.length].min
56
+ source = (start_line..end_line).map do |num|
57
+ marker = num == line_number ? ">>" : " "
58
+ "#{marker} #{num.to_s.rjust(4)} | #{lines[num - 1]}"
59
+ end.join("\n")
60
+
61
+ {
62
+ start_line: start_line,
63
+ end_line: end_line,
64
+ error_line: line_number,
65
+ source: source
66
+ }
67
+ end
68
+
69
+ def read_file_lines(file_path)
70
+ return nil unless file_path && File.exist?(file_path)
71
+
72
+ File.readlines(file_path, chomp: true)
73
+ rescue StandardError
74
+ nil
75
+ end
76
+
77
+ def escape_html(value)
78
+ CGI.escapeHTML(value.to_s)
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,147 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bugsage
4
+ class CodePatch
5
+ ACTIONS = %w[replace_lines delete_lines insert_before no_change].freeze
6
+
7
+ attr_reader :action, :start_line, :end_line, :replacement
8
+
9
+ def initialize(action:, start_line:, end_line: nil, replacement: "")
10
+ @action = normalize_action(action)
11
+ @start_line = start_line.to_i
12
+ @end_line = (end_line || start_line).to_i
13
+ @replacement = replacement.to_s
14
+ end
15
+
16
+ def self.from_ai(payload, error_line:)
17
+ patch = payload["code_patch"]
18
+ return from_legacy(payload["code_fix"], error_line) if patch.nil? || patch.empty?
19
+
20
+ new(
21
+ action: patch["action"],
22
+ start_line: patch["start_line"] || error_line,
23
+ end_line: patch["end_line"] || patch["start_line"] || error_line,
24
+ replacement: patch["replacement"]
25
+ )
26
+ end
27
+
28
+ def self.from_legacy(code_fix, error_line)
29
+ text = code_fix.to_s.strip
30
+ return nil if text.empty?
31
+
32
+ new(action: "replace_lines", start_line: error_line, end_line: error_line, replacement: text)
33
+ end
34
+
35
+ def self.preview_for(patch)
36
+ return nil if patch.nil?
37
+
38
+ instance = patch.is_a?(self) ? patch : from_hash(patch)
39
+ instance&.preview
40
+ end
41
+
42
+ def self.from_hash(hash)
43
+ return nil if hash.nil? || hash.empty?
44
+
45
+ new(
46
+ action: hash[:action] || hash["action"],
47
+ start_line: hash[:start_line] || hash["start_line"],
48
+ end_line: hash[:end_line] || hash["end_line"],
49
+ replacement: hash[:replacement] || hash["replacement"]
50
+ )
51
+ end
52
+
53
+ def apply!(lines)
54
+ validate_range!(lines)
55
+
56
+ case action
57
+ when "no_change"
58
+ nil
59
+ when "delete_lines"
60
+ delete_lines!(lines)
61
+ when "insert_before"
62
+ insert_before!(lines)
63
+ else
64
+ replace_lines!(lines)
65
+ end
66
+ end
67
+
68
+ def preview
69
+ case action
70
+ when "no_change"
71
+ Bugsage.t("code_patch.no_change")
72
+ when "delete_lines"
73
+ if start_line == end_line
74
+ Bugsage.t("code_patch.delete_line", line: start_line)
75
+ else
76
+ Bugsage.t("code_patch.delete_lines", start_line: start_line, end_line: end_line)
77
+ end
78
+ when "insert_before"
79
+ Bugsage.t("code_patch.insert_before", line: start_line, replacement: replacement)
80
+ else
81
+ if start_line == end_line
82
+ Bugsage.t("code_patch.replace_line", line: start_line, replacement: replacement)
83
+ else
84
+ Bugsage.t("code_patch.replace_lines", start_line: start_line, end_line: end_line, replacement: replacement)
85
+ end
86
+ end
87
+ end
88
+
89
+ def duplicates_existing?(lines)
90
+ return false if %w[delete_lines no_change].include?(action)
91
+ return true if replacement.strip.empty?
92
+
93
+ normalized = normalize_line(replacement)
94
+ lines.any? { |line| normalize_line(line) == normalized }
95
+ end
96
+
97
+ def to_h
98
+ {
99
+ action: action,
100
+ start_line: start_line,
101
+ end_line: end_line,
102
+ replacement: replacement
103
+ }
104
+ end
105
+
106
+ private
107
+
108
+ def normalize_action(value)
109
+ action = value.to_s.strip
110
+ ACTIONS.include?(action) ? action : "replace_lines"
111
+ end
112
+
113
+ def validate_range!(lines)
114
+ raise Bugsage::Error, Bugsage.t("code_patch.invalid_line_range") if start_line < 1 || end_line < start_line
115
+ raise Bugsage::Error, Bugsage.t("code_patch.patch_ends_after_file_end") if end_line > lines.length
116
+ end
117
+
118
+ def replace_lines!(lines)
119
+ indent = lines[start_line - 1].to_s[/\A(\s*)/, 1] || ""
120
+ delete_lines!(lines)
121
+ return if replacement.strip.empty?
122
+
123
+ lines.insert(start_line - 1, *indented_replacement_lines(indent))
124
+ end
125
+
126
+ def insert_before!(lines)
127
+ indent = lines[start_line - 1].to_s[/\A(\s*)/, 1] || ""
128
+ lines.insert(start_line - 1, *indented_replacement_lines(indent))
129
+ end
130
+
131
+ def delete_lines!(lines)
132
+ lines.slice!(start_line - 1, end_line - start_line + 1)
133
+ end
134
+
135
+ def replacement_lines
136
+ replacement.lines(chomp: true)
137
+ end
138
+
139
+ def indented_replacement_lines(indent)
140
+ replacement_lines.map { |line| line.empty? ? line : "#{indent}#{line.lstrip}" }
141
+ end
142
+
143
+ def normalize_line(line)
144
+ line.to_s.strip.gsub(/\s+/, " ")
145
+ end
146
+ end
147
+ end
@@ -0,0 +1,149 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bugsage
4
+ class Configuration
5
+ AI_PROVIDERS = %i[openai cursor].freeze
6
+
7
+ attr_accessor :enabled_environments,
8
+ :show_error_page,
9
+ :show_dashboard,
10
+ :show_inline_console,
11
+ :capture_errors,
12
+ :capture_http_errors,
13
+ :ai_enabled,
14
+ :ai_provider,
15
+ :openai_api_key,
16
+ :openai_model,
17
+ :openai_api_base,
18
+ :cursor_api_key,
19
+ :cursor_model,
20
+ :cursor_api_base,
21
+ :ai_timeout,
22
+ :ai_client,
23
+ :fallback_exceptions_app
24
+
25
+ def initialize
26
+ @enabled_environments = %i[development test]
27
+ @show_error_page = nil
28
+ @show_dashboard = nil
29
+ @show_inline_console = nil
30
+ @capture_errors = true
31
+ @capture_http_errors = true
32
+ @ai_enabled = nil
33
+ @ai_provider = nil
34
+ @openai_api_key = nil
35
+ @openai_model = "gpt-4o-mini"
36
+ @openai_api_base = "https://api.openai.com/v1"
37
+ @cursor_api_key = nil
38
+ @cursor_model = nil
39
+ @cursor_api_base = "https://api.cursor.com"
40
+ @ai_timeout = 15
41
+ @ai_client = nil
42
+ @fallback_exceptions_app = nil
43
+ end
44
+
45
+ def enabled?(environment = current_environment)
46
+ environment_names.include?(environment.to_s)
47
+ end
48
+
49
+ def show_error_page?(environment = current_environment)
50
+ return show_error_page unless show_error_page.nil?
51
+
52
+ environment.to_s == "development"
53
+ end
54
+
55
+ def show_dashboard?(environment = current_environment)
56
+ return show_dashboard unless show_dashboard.nil?
57
+
58
+ environment.to_s == "development"
59
+ end
60
+
61
+ def show_inline_console?(environment = current_environment)
62
+ return show_inline_console unless show_inline_console.nil?
63
+
64
+ environment.to_s == "development"
65
+ end
66
+
67
+ def capture_errors?(environment = current_environment)
68
+ return false unless enabled?(environment)
69
+
70
+ capture_errors
71
+ end
72
+
73
+ def capture_http_errors?(environment = current_environment)
74
+ return false unless capture_errors?(environment)
75
+
76
+ capture_http_errors != false
77
+ end
78
+
79
+ def ai_enabled?(environment = current_environment)
80
+ return false unless enabled?(environment)
81
+ return ai_enabled unless ai_enabled.nil?
82
+
83
+ credential_available?
84
+ end
85
+
86
+ def credential_available?
87
+ !resolved_openai_api_key.to_s.strip.empty? || !resolved_cursor_api_key.to_s.strip.empty?
88
+ end
89
+
90
+ def ai_configured?(environment = current_environment)
91
+ return false unless ai_enabled?(environment)
92
+
93
+ return true unless ai_client.nil?
94
+
95
+ case resolved_ai_provider
96
+ when :cursor
97
+ !resolved_cursor_api_key.to_s.strip.empty?
98
+ else
99
+ !resolved_openai_api_key.to_s.strip.empty?
100
+ end
101
+ end
102
+
103
+ def resolved_ai_provider
104
+ provider = ai_provider&.to_sym
105
+ return provider if provider && AI_PROVIDERS.include?(provider)
106
+
107
+ return :cursor if resolved_cursor_api_key.to_s.start_with?("crsr_")
108
+
109
+ :openai
110
+ end
111
+
112
+ def resolved_openai_api_key
113
+ key = openai_api_key || ENV["OPENAI_API_KEY"] || ENV.fetch("BUGSAGE_OPENAI_API_KEY", nil)
114
+ return nil if key.to_s.start_with?("crsr_")
115
+
116
+ key
117
+ end
118
+
119
+ def resolved_cursor_api_key
120
+ explicit = cursor_api_key || ENV["CURSOR_API_KEY"] || ENV.fetch("BUGSAGE_CURSOR_API_KEY", nil)
121
+ return explicit if explicit.to_s.start_with?("crsr_")
122
+
123
+ misrouted = openai_api_key || ENV["OPENAI_API_KEY"] || ENV.fetch("BUGSAGE_OPENAI_API_KEY", nil)
124
+ return misrouted if misrouted.to_s.start_with?("crsr_")
125
+
126
+ explicit
127
+ end
128
+
129
+ def effective_ai_timeout
130
+ return [ai_timeout, 90].max if resolved_ai_provider == :cursor
131
+
132
+ ai_timeout
133
+ end
134
+
135
+ def current_environment
136
+ if defined?(Rails) && Rails.respond_to?(:env)
137
+ Rails.env.to_s
138
+ else
139
+ ENV.fetch("RAILS_ENV", ENV.fetch("RACK_ENV", "development"))
140
+ end
141
+ end
142
+
143
+ private
144
+
145
+ def environment_names
146
+ Array(enabled_environments).map(&:to_s)
147
+ end
148
+ end
149
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bugsage
4
+ module ConsoleContext
5
+ module_function
6
+
7
+ def set(exception:, context:)
8
+ @exception = exception
9
+ @context = context || {}
10
+ end
11
+
12
+ def load_from_event(event)
13
+ return false unless event
14
+
15
+ klass = safe_constantize(event[:exception_class])
16
+ message = event[:exception_message].to_s
17
+ message = event[:root_cause].to_s if message.empty?
18
+
19
+ set(
20
+ exception: klass.new(message),
21
+ context: event[:context] || {}
22
+ )
23
+ true
24
+ end
25
+
26
+ def clear!
27
+ @exception = nil
28
+ @context = {}
29
+ end
30
+
31
+ def binding_for_eval
32
+ exception = @exception
33
+ request_context = @context || {}
34
+ params = request_context["Request parameters"] || request_context["Path parameters"] || {}
35
+
36
+ binding
37
+ end
38
+
39
+ def available?
40
+ !@exception.nil?
41
+ end
42
+
43
+ def safe_constantize(name)
44
+ return StandardError if name.to_s.strip.empty?
45
+
46
+ Object.const_get(name)
47
+ rescue NameError
48
+ StandardError
49
+ end
50
+ end
51
+ end