console_kit 1.4.0 → 1.5.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 (39) hide show
  1. checksums.yaml +4 -4
  2. data/lib/console_kit/configuration.rb +15 -21
  3. data/lib/console_kit/configuration_validator.rb +179 -0
  4. data/lib/console_kit/connections/base_connection_handler.rb +139 -25
  5. data/lib/console_kit/connections/connection_manager.rb +42 -8
  6. data/lib/console_kit/connections/dashboard.rb +3 -11
  7. data/lib/console_kit/connections/diagnostic_helpers.rb +24 -5
  8. data/lib/console_kit/connections/elasticsearch_connection_handler.rb +87 -32
  9. data/lib/console_kit/connections/elasticsearch_prefix_registry.rb +70 -0
  10. data/lib/console_kit/connections/mongo_connection_handler.rb +51 -46
  11. data/lib/console_kit/connections/mongoid_support.rb +56 -0
  12. data/lib/console_kit/connections/redis_client_adapter.rb +92 -0
  13. data/lib/console_kit/connections/redis_connection_handler.rb +97 -41
  14. data/lib/console_kit/connections/sql_connection_handler.rb +66 -30
  15. data/lib/console_kit/connections/sql_strategy.rb +248 -0
  16. data/lib/console_kit/connections/table_renderer.rb +30 -20
  17. data/lib/console_kit/console_helpers.rb +17 -31
  18. data/lib/console_kit/diagnostics.rb +143 -0
  19. data/lib/console_kit/errors.rb +93 -0
  20. data/lib/console_kit/instrumentation.rb +57 -0
  21. data/lib/console_kit/output.rb +5 -14
  22. data/lib/console_kit/prompt.rb +17 -29
  23. data/lib/console_kit/railtie.rb +2 -3
  24. data/lib/console_kit/setup.rb +66 -13
  25. data/lib/console_kit/setup_ui.rb +1 -4
  26. data/lib/console_kit/tenant_configurator/context_wrapper.rb +51 -23
  27. data/lib/console_kit/tenant_configurator.rb +40 -78
  28. data/lib/console_kit/tenant_orchestrator.rb +31 -27
  29. data/lib/console_kit/tenant_plan.rb +68 -0
  30. data/lib/console_kit/tenant_rollback.rb +77 -0
  31. data/lib/console_kit/tenant_selector.rb +3 -6
  32. data/lib/console_kit/tenant_state.rb +73 -0
  33. data/lib/console_kit/tenant_switch.rb +124 -0
  34. data/lib/console_kit/version.rb +1 -1
  35. data/lib/console_kit.rb +51 -24
  36. data/lib/generators/console_kit/install_generator.rb +1 -12
  37. data/lib/generators/console_kit/templates/console_kit.rb +4 -0
  38. metadata +15 -4
  39. data/lib/console_kit/connections/table_formatter.rb +0 -37
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'connections/diagnostic_helpers'
4
+
5
+ module ConsoleKit
6
+ PROGRAMMING_ERRORS = [NameError, ArgumentError, TypeError].freeze
7
+
8
+ class << self
9
+ def programming_error?(error) = PROGRAMMING_ERRORS.any? { |klass| error.is_a?(klass) }
10
+ end
11
+
12
+ class Error < StandardError; end
13
+
14
+ class ConfigurationError < Error; end
15
+
16
+ class TenantNotFoundError < ConfigurationError; end
17
+
18
+ class UnsupportedBackendError < Error; end
19
+
20
+ class ConnectionError < Error
21
+ attr_reader :backend, :tenant, :operation
22
+
23
+ def initialize(message = nil, backend: nil, tenant: nil, operation: nil)
24
+ @backend = backend
25
+ @tenant = tenant
26
+ @operation = operation
27
+ super(message || "#{backend} #{operation} failed for tenant #{tenant.inspect}")
28
+ end
29
+ end
30
+
31
+ class ConnectionVerificationError < ConnectionError
32
+ attr_reader :expected, :actual
33
+
34
+ def initialize(message = nil, expected: nil, actual: nil, **opts)
35
+ @expected = expected
36
+ @actual = actual
37
+ super(message || default_message(opts[:backend]), operation: :verify, **opts)
38
+ end
39
+
40
+ private
41
+
42
+ def default_message(backend)
43
+ "#{backend} verification failed. Expected #{expected.inspect}, got #{actual.inspect}"
44
+ end
45
+ end
46
+
47
+ class RollbackError < Error
48
+ attr_reader :failures
49
+
50
+ def initialize(failures)
51
+ @failures = failures
52
+ super("Rollback failed for: #{failures.map { |failure| failure[:backend] }.join(', ')}")
53
+ end
54
+ end
55
+
56
+ class TenantSwitchError < Error
57
+ attr_reader :from_tenant, :to_tenant, :backend, :original_error, :rollback_failures
58
+
59
+ def initialize(from_tenant:, to_tenant:, original_error:, backend: nil, rollback_failures: [])
60
+ @from_tenant = from_tenant
61
+ @to_tenant = to_tenant
62
+ @backend = backend
63
+ @original_error = original_error
64
+ @rollback_failures = rollback_failures
65
+ super(build_message)
66
+ end
67
+
68
+ def rollback_succeeded? = rollback_failures.empty?
69
+
70
+ private
71
+
72
+ def build_message
73
+ [headline, scrub(original_error.message), rollback_summary].compact.join("\n")
74
+ end
75
+
76
+ def scrub(message) = Connections::DiagnosticHelpers.scrub(message)
77
+
78
+ def headline
79
+ scope = backend ? " (#{backend})" : nil
80
+ "Failed to switch tenant from #{from_tenant.inspect} to #{to_tenant.inspect}#{scope}:"
81
+ end
82
+
83
+ def rollback_summary
84
+ return "\nPrevious tenant state was restored successfully." if rollback_succeeded?
85
+
86
+ "\nWARNING: rollback did not fully succeed:\n#{formatted_failures}"
87
+ end
88
+
89
+ def formatted_failures
90
+ rollback_failures.map { |failure| " - #{failure[:backend]}: #{scrub(failure[:error].message)}" }.join("\n")
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'connections/diagnostic_helpers'
4
+
5
+ module ConsoleKit
6
+ module Instrumentation
7
+ class << self
8
+ def subscribe(&block)
9
+ mutex.synchronize { subscribers << block }
10
+ block
11
+ end
12
+
13
+ def clear!
14
+ mutex.synchronize do
15
+ subscribers.clear
16
+ counters.clear
17
+ end
18
+ end
19
+
20
+ def counters = @counters ||= Hash.new(0)
21
+
22
+ def counts = mutex.synchronize { counters.dup }
23
+
24
+ def increment(name)
25
+ mutex.synchronize { counters[name] += 1 }
26
+ end
27
+
28
+ def instrument(name, payload = {})
29
+ start = Connections::DiagnosticHelpers.clock_time
30
+ yield.tap { publish(name, start, payload.merge(status: :ok)) }
31
+ rescue StandardError, NotImplementedError => e
32
+ publish(name, start, payload.merge(error: e.class.name, status: :error))
33
+ raise
34
+ end
35
+
36
+ private
37
+
38
+ def publish(name, start, payload)
39
+ increment(name)
40
+ duration_ms = elapsed_ms(start)
41
+ each_subscriber { |sub| sub.call(name, duration_ms, payload) }
42
+ end
43
+
44
+ def each_subscriber
45
+ mutex.synchronize { subscribers.dup }.each do |sub|
46
+ yield(sub)
47
+ rescue StandardError
48
+ nil
49
+ end
50
+ end
51
+
52
+ def elapsed_ms(start) = ((Connections::DiagnosticHelpers.clock_time - start) * 1000).round(2)
53
+ def subscribers = @subscribers ||= []
54
+ def mutex = @mutex ||= Mutex.new
55
+ end
56
+ end
57
+ end
@@ -1,15 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ConsoleKit
4
- # Handles Console outputs
5
4
  module Output
6
5
  PREFIX = '[ConsoleKit]'
7
6
  TYPES = {
8
7
  error: { symbol: '[✗]', color: '1;31' },
9
8
  success: { symbol: '[✓]', color: '1;32' },
10
9
  warning: { symbol: '[!]', color: '1;33' },
11
- prompt: { symbol: nil, color: '1;36' },
12
- header: { symbol: nil, color: '1;34' },
10
+ prompt: { symbol: nil, color: '1;36' },
11
+ header: { symbol: nil, color: '1;34' },
13
12
  trace: { symbol: nil, color: '0;90' },
14
13
  info: { symbol: nil, color: nil }
15
14
  }.freeze
@@ -34,7 +33,8 @@ module ConsoleKit
34
33
  return if silent
35
34
 
36
35
  formatted = (type == :header ? "\n--- #{text} ---" : text)
37
- print_with(type, formatted, timestamp: timestamp, newline: newline)
36
+ message = build_formatted_message(type, formatted, timestamp)
37
+ newline ? puts(message) : print(message)
38
38
  end
39
39
  end
40
40
 
@@ -54,20 +54,11 @@ module ConsoleKit
54
54
  def print_backtrace(exception)
55
55
  return if silent
56
56
 
57
- exception&.backtrace&.each do |line|
58
- print_with(:trace, " #{line}", timestamp: true)
59
- end
57
+ exception&.backtrace&.each { |line| print_trace(" #{line}", timestamp: true) }
60
58
  end
61
59
 
62
60
  private
63
61
 
64
- def print_with(type, text, options = {})
65
- opts = options.is_a?(Hash) ? options : { timestamp: options }
66
- message = build_formatted_message(type, text, opts[:timestamp])
67
-
68
- opts.fetch(:newline, true) ? puts(message) : print(message)
69
- end
70
-
71
62
  def build_formatted_message(type, text, timestamp)
72
63
  meta = TYPES.fetch(type)
73
64
  message = build_message(text, meta[:symbol], timestamp)
@@ -1,46 +1,34 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ConsoleKit
4
- # Sets the console prompt to show the current tenant
5
4
  module Prompt
5
+ module IrbLabel
6
+ def prompt_i = Prompt.labelled(super)
7
+ def prompt_s = Prompt.labelled(super)
8
+ def prompt_c = Prompt.labelled(super)
9
+ end
10
+
6
11
  class << self
7
12
  def apply
8
- apply_irb_prompt if defined?(IRB)
9
- apply_pry_prompt if defined?(Pry)
13
+ require 'irb' unless defined?(Pry)
14
+ IRB::Context.prepend(IrbLabel) if defined?(IRB::Context) && !IRB::Context.include?(IrbLabel)
15
+ Pry.config.prompt = pry_prompt if defined?(Pry)
10
16
  end
11
17
 
12
- private
18
+ def labelled(prompt) = prompt && "#{label.gsub('%', '%%')} #{prompt}"
13
19
 
14
- def tenant_label
15
- tenant = ConsoleKit::Setup.current_tenant
20
+ def label
21
+ tenant = StateStore.tenant_key
16
22
  tenant ? "[#{tenant}]" : '[no-tenant]'
17
23
  end
18
24
 
19
- def apply_irb_prompt
20
- conf = IRB.conf
21
- prompt = conf[:PROMPT] ||= {}
22
- prompt[:CONSOLE_KIT] = {
23
- PROMPT_I: "#{tenant_label} %N(%m):%03n> ",
24
- PROMPT_S: "#{tenant_label} %N(%m):%03n%l ",
25
- PROMPT_C: "#{tenant_label} %N(%m):%03n* ",
26
- RETURN: "=> %s\n"
27
- }
28
- conf[:PROMPT_MODE] = :CONSOLE_KIT
29
- end
30
-
31
- def apply_pry_prompt
32
- procs = pry_prompt_procs(tenant_label)
33
- Pry.config.prompt = build_pry_prompt(procs)
34
- end
25
+ private
35
26
 
36
- def pry_prompt_procs(label)
37
- [
38
- proc { |obj, nest, _| "#{label} (#{obj}):#{nest}> " },
39
- proc { |obj, nest, _| "#{label} (#{obj}):#{nest}* " }
27
+ def pry_prompt
28
+ procs = [
29
+ proc { |obj, nest, _| "#{ConsoleKit::Prompt.label} (#{obj}):#{nest}> " },
30
+ proc { |obj, nest, _| "#{ConsoleKit::Prompt.label} (#{obj}):#{nest}* " }
40
31
  ]
41
- end
42
-
43
- def build_pry_prompt(procs)
44
32
  return procs unless defined?(Pry::Prompt)
45
33
 
46
34
  Pry::Prompt.try(:new, 'console_kit', 'ConsoleKit tenant prompt', procs) || procs
@@ -1,10 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ConsoleKit
4
- # Railtie for integrating ConsoleKit with Rails console.
5
4
  class Railtie < Rails::Railtie
6
5
  console do
7
- ConsoleKit::Setup.setup
6
+ ConsoleKit::TenantOrchestrator.run
8
7
  ConsoleKit::Prompt.apply
9
8
  if defined?(IRB::ExtendCommandBundle) && !defined?(Pry)
10
9
  IRB::ExtendCommandBundle.include(ConsoleKit::ConsoleHelpers)
@@ -13,6 +12,6 @@ module ConsoleKit
13
12
  end
14
13
  end
15
14
 
16
- config.to_prepare { ConsoleKit::Setup.reapply if defined?(Rails::Console) }
15
+ config.to_prepare { ConsoleKit::TenantOrchestrator.reapply if defined?(Rails::Console) }
17
16
  end
18
17
  end
@@ -1,27 +1,80 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'tenant_selector'
4
- require_relative 'tenant_configurator'
5
- require_relative 'output'
6
- require_relative 'setup_ui'
3
+ require_relative 'configuration'
7
4
  require_relative 'tenant_orchestrator'
8
5
 
9
- # Core Logic for initial Setup
10
6
  module ConsoleKit
11
- # Does the initial setup
7
+ module Deprecation
8
+ class << self
9
+ def call(name, replacement)
10
+ warned = (@warned ||= {})
11
+ unless warned[name]
12
+ warned[name] = true
13
+ Kernel.warn("ConsoleKit: #{name} is deprecated and will be removed in 2.0. Use #{replacement} instead.")
14
+ end
15
+ yield
16
+ end
17
+ end
18
+ end
19
+
12
20
  module Setup
13
21
  class << self
14
- def current_tenant = Thread.current[:console_kit_current_tenant]
22
+ def setup = Deprecation.call('Setup.setup', 'TenantOrchestrator.run') { TenantOrchestrator.run }
23
+
24
+ def current_tenant
25
+ Deprecation.call('Setup.current_tenant', 'ConsoleKit.current_tenant') { ConsoleKit.current_tenant }
26
+ end
15
27
 
16
28
  def current_tenant=(val)
17
- Thread.current[:console_kit_current_tenant] = val
29
+ Deprecation.call('Setup.current_tenant=', 'ConsoleKit.switch_tenant') do
30
+ TenantOrchestrator.current_tenant = val
31
+ end
32
+ end
33
+
34
+ def tenant_setup_successful?
35
+ Deprecation.call('Setup.tenant_setup_successful?', 'ConsoleKit.current_tenant') do
36
+ !ConsoleKit.current_tenant.to_s.empty?
37
+ end
38
+ end
39
+
40
+ def reapply = Deprecation.call('Setup.reapply', 'TenantOrchestrator.reapply') { TenantOrchestrator.reapply }
41
+
42
+ def reset_current_tenant
43
+ Deprecation.call('Setup.reset_current_tenant', 'ConsoleKit.reset_current_tenant') do
44
+ ConsoleKit.reset_current_tenant
45
+ end
18
46
  end
19
47
 
20
- def setup = TenantOrchestrator.run
21
- def tenant_setup_successful? = !current_tenant.to_s.empty?
22
- def reapply = TenantOrchestrator.reapply
23
- def reset_current_tenant = TenantOrchestrator.reset
24
- def auto_select? = TenantOrchestrator.auto_select?
48
+ def auto_select?
49
+ Deprecation.call('Setup.auto_select?', 'TenantOrchestrator.auto_select?') { TenantOrchestrator.auto_select? }
50
+ end
51
+ end
52
+ end
53
+
54
+ class << self
55
+ %i[pretty_output context_class show_dashboard].each do |name|
56
+ define_method(name) do
57
+ Deprecation.call("ConsoleKit.#{name}", "ConsoleKit.configuration.#{name}") { configuration.public_send(name) }
58
+ end
59
+ end
60
+
61
+ %i[pretty_output tenants context_class show_dashboard].each do |name|
62
+ define_method(:"#{name}=") do |val|
63
+ Deprecation.call("ConsoleKit.#{name}=", "ConsoleKit.configuration.#{name}=") do
64
+ configuration.public_send(:"#{name}=", val)
65
+ end
66
+ end
67
+ end
68
+ end
69
+
70
+ class Configuration
71
+ def validate
72
+ Deprecation.call('Configuration#validate', 'Configuration#validate!') do
73
+ validate!
74
+ true
75
+ rescue Error
76
+ false
77
+ end
25
78
  end
26
79
  end
27
80
  end
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ConsoleKit
4
- # UI helpers for Setup
5
4
  module SetupUI
6
5
  ENVIRONMENT_WARNINGS = {
7
6
  'production' => -> { Output.print_error('!!! CAUTION: YOU ARE IN PRODUCTION ENVIRONMENT !!!') },
@@ -26,9 +25,7 @@ module ConsoleKit
26
25
 
27
26
  def print_active_connections
28
27
  ctx = ConsoleKit.configuration.context_class
29
- active = Connections::ConnectionManager.available_handlers(ctx).map do |handler|
30
- handler.class.name.demodulize.delete_suffix('ConnectionHandler')
31
- end
28
+ active = Connections::ConnectionManager.available_handlers(ctx).map(&:display_name)
32
29
 
33
30
  Output.print_info("Active connections: #{active.join(', ')}") if active.any?
34
31
  end
@@ -2,14 +2,9 @@
2
2
 
3
3
  module ConsoleKit
4
4
  module TenantConfigurator
5
- # Encapsulates context and attributes to resolve DataClump smells
6
5
  class ContextWrapper
7
- HANDLER_ATTRIBUTES = {
8
- Connections::SqlConnectionHandler => :tenant_shard,
9
- Connections::MongoConnectionHandler => :tenant_mongo_db,
10
- Connections::RedisConnectionHandler => :tenant_redis_db,
11
- Connections::ElasticsearchConnectionHandler => :tenant_elasticsearch_prefix
12
- }.freeze
6
+ UNREADABLE = :'#<console_kit unreadable>'
7
+ UNREADABLE_MESSAGE = 'Previous value of %s could not be read, so it was left as the new tenant set it.'
13
8
 
14
9
  attr_reader :ctx, :attributes
15
10
 
@@ -22,15 +17,13 @@ module ConsoleKit
22
17
 
23
18
  def detect_attributes(ctx)
24
19
  methods = ctx.public_methods
25
- partner_attrs(methods) + handler_attrs(methods)
26
- end
27
-
28
- def partner_attrs(methods)
29
- methods.include?(:partner_identifier=) ? [:partner_identifier] : []
20
+ partner = methods.include?(:partner_identifier=) ? [:partner_identifier] : []
21
+ partner + handler_attrs(methods)
30
22
  end
31
23
 
32
24
  def handler_attrs(methods)
33
- HANDLER_ATTRIBUTES.each_with_object([]) do |(handler, attr), list|
25
+ Connections::BaseConnectionHandler.registry.each_with_object([]) do |handler, list|
26
+ attr = handler.context_attribute
34
27
  next unless methods.include?(:"#{attr}=")
35
28
  next unless handler_available?(handler)
36
29
 
@@ -54,25 +47,60 @@ module ConsoleKit
54
47
  attributes.any? { |attr| ctx.public_send(attr).present? }
55
48
  end
56
49
 
57
- def reset
58
- attributes.each { |attr| ctx.public_send("#{attr}=", nil) }
50
+ def current_values = attributes.to_h { |attr| [attr, safe_read(attr)] }
51
+
52
+ def restore(values)
53
+ failures = values.filter_map { |attr, value| restore_attribute(attr, value) }
54
+ raise_restore_failure(failures) if failures.any?
55
+
56
+ values
59
57
  end
60
58
 
61
59
  def assign(constant, mapping)
62
- attributes.map do |attr|
63
- existing = safe_read(attr)
64
- new_value = constant[mapping[attr]]
65
- ctx.public_send("#{attr}=", new_value)
66
- [attr, existing, new_value]
67
- end
60
+ attributes.to_h { |attr| [attr, write_attribute(attr, constant[mapping[attr]])] }
68
61
  end
69
62
 
70
63
  private
71
64
 
65
+ def restore_attribute(attr, value)
66
+ return [attr, Error.new(format(UNREADABLE_MESSAGE, attr))] if value == UNREADABLE
67
+
68
+ ctx.public_send(:"#{attr}=", value)
69
+ nil
70
+ rescue StandardError, NotImplementedError => e
71
+ [attr, e]
72
+ end
73
+
74
+ def write_attribute(attr, new_value)
75
+ existing = safe_read(attr)
76
+ ctx.public_send(:"#{attr}=", new_value)
77
+ warn_case_mismatch(attr, existing, new_value)
78
+ new_value
79
+ end
80
+
81
+ def raise_restore_failure(failures)
82
+ detail = failures.map { |attr, error| "#{attr} (#{error.class})" }.join(', ')
83
+ raise Error, "Could not restore context attributes: #{detail}. " \
84
+ 'Those attributes are still set to the tenant the switch failed to reach.'
85
+ end
86
+
87
+ def warn_case_mismatch(attr, existing, configured)
88
+ return unless existing.is_a?(String) && configured.is_a?(String) &&
89
+ existing != configured && existing.casecmp(configured).zero?
90
+
91
+ Output.print_warning(
92
+ "#{attr} case mismatch: context had '#{existing}', config set '#{configured}'. " \
93
+ 'Check your ConsoleKit tenant configuration.'
94
+ )
95
+ end
96
+
72
97
  def safe_read(attr)
73
98
  ctx.public_send(attr)
74
- rescue StandardError
75
- nil
99
+ rescue StandardError, NotImplementedError => e
100
+ Output.print_warning(
101
+ "Could not read context attribute #{attr}: #{e.class}. Rollback will not be able to restore it."
102
+ )
103
+ UNREADABLE
76
104
  end
77
105
  end
78
106
  end
@@ -1,127 +1,89 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative 'output'
4
+ require_relative 'errors'
5
+ require_relative 'tenant_state'
4
6
  require_relative 'connections/connection_manager'
5
7
  require_relative 'connections/dashboard'
6
8
  require_relative 'tenant_configurator/context_wrapper'
9
+ require_relative 'tenant_switch'
7
10
 
8
11
  module ConsoleKit
9
- # For tenant configuration
10
12
  module TenantConfigurator
11
- CONTEXT_MAPPING = {
12
- partner_identifier: :partner_code,
13
- tenant_shard: :shard,
14
- tenant_mongo_db: :mongo_db,
15
- tenant_redis_db: :redis_db,
16
- tenant_elasticsearch_prefix: :elasticsearch_prefix
17
- }.freeze
18
-
19
13
  class << self
20
- def configuration_success = Thread.current[:console_kit_configuration_success]
14
+ def context_mapping
15
+ { partner_identifier: :partner_code }.merge(backend_context_mapping)
16
+ end
17
+
18
+ def configuration_success? = StateStore.configured?
19
+ alias configuration_success configuration_success?
21
20
 
22
21
  def configuration_success=(val)
23
- Thread.current[:console_kit_configuration_success] = val
22
+ StateStore.clear! unless val
24
23
  end
25
24
 
26
- def current_tenant_key = Thread.current[:console_kit_current_tenant_key]
25
+ def current_tenant_key = StateStore.tenant_key
27
26
 
28
27
  def current_tenant_key=(val)
29
- Thread.current[:console_kit_current_tenant_key] = val
28
+ StateStore.clear! if val.nil?
30
29
  end
31
30
 
32
31
  def configure_tenant(key)
33
- return true if key == current_tenant_key && configuration_success
34
-
35
- attempt_configuration(key)
36
- rescue StandardError => e
37
- handle_error?(e, key)
32
+ switch_unless_current(key)
33
+ true
34
+ rescue StandardError, NotImplementedError => e
35
+ report_failure(e, key)
36
+ false
38
37
  end
39
38
 
40
39
  def clear
41
40
  ctx = ConsoleKit.configuration.context_class
42
41
  return unless ctx
43
42
 
44
- perform_clear(ContextWrapper.for_context(ctx))
43
+ perform_clear(ctx, ContextWrapper.for_context(ctx))
45
44
  end
46
45
 
47
46
  private
48
47
 
49
- def attempt_configuration(key)
50
- constants = ConsoleKit.configuration.tenants[key]&.[](:constants)
51
- return missing_config_error?(key) unless constants
48
+ def backend_context_mapping
49
+ Connections::BaseConnectionHandler.registry.to_h { |handler| [handler.context_attribute, handler.constants_key] }
50
+ end
51
+
52
+ def switch_unless_current(key)
53
+ return if key == current_tenant_key && configuration_success
52
54
 
53
- execute_configuration(key, constants)
54
- configuration_success
55
+ TenantSwitch.call(key)
55
56
  end
56
57
 
57
- def perform_clear(wrapper)
58
+ def perform_clear(ctx, wrapper)
58
59
  return unless configuration_success || wrapper.any_set?
59
60
 
60
- reset_tenant(wrapper)
61
+ TenantSwitch.clear(context: ctx)
61
62
  Output.print_info('Tenant context has been cleared.')
62
63
  true
63
64
  end
64
65
 
65
- def reset_tenant(wrapper)
66
- self.configuration_success = false
67
- self.current_tenant_key = nil
68
- wrapper.reset
69
- setup_connections(wrapper.ctx)
70
- end
66
+ def report_failure(error, key)
67
+ return print_missing_config(key) if tenant_missing?(error)
71
68
 
72
- def validate_constants!(constants)
73
- missing = %i[shard partner_code] - constants.keys
74
- raise Error, "Tenant constants missing keys: #{missing.join(', ')}" unless missing.empty?
69
+ print_error_details(error, key)
75
70
  end
76
71
 
77
- def missing_config_error?(key)
78
- self.configuration_success = false
79
- Output.print_error("No configuration found for tenant: #{key}")
80
- false
72
+ def tenant_missing?(error)
73
+ error.is_a?(TenantNotFoundError) ||
74
+ (error.is_a?(TenantSwitchError) && error.original_error.is_a?(TenantNotFoundError))
81
75
  end
82
76
 
83
- def execute_configuration(key, constants)
84
- validate_constants!(constants)
85
- apply_context(constants)
86
- mark_success(key)
87
- end
88
-
89
- def apply_context(constant)
90
- wrapper = ContextWrapper.for_context(ConsoleKit.configuration.context_class)
91
- wrapper.assign(constant, CONTEXT_MAPPING).each do |attr, existing, configured|
92
- warn_case_mismatch(attr, existing, configured) if case_mismatch?(existing, configured)
93
- end
94
- setup_connections(wrapper.ctx)
95
- end
96
-
97
- def case_mismatch?(existing, new_value)
98
- existing.is_a?(String) && new_value.is_a?(String) &&
99
- existing != new_value &&
100
- existing.casecmp(new_value).zero?
101
- end
102
-
103
- def setup_connections(context)
104
- Connections::ConnectionManager.available_handlers(context).each(&:connect)
105
- end
106
-
107
- def mark_success(key)
108
- Output.print_success("Tenant set to: #{key}")
109
- self.configuration_success = true
110
- self.current_tenant_key = key
111
- end
112
-
113
- def warn_case_mismatch(attr, existing, configured)
114
- Output.print_warning(
115
- "#{attr} case mismatch: context had '#{existing}', config set '#{configured}'. " \
116
- 'Check your ConsoleKit tenant configuration.'
117
- )
77
+ def print_missing_config(key)
78
+ Output.print_error("No configuration found for tenant: #{key}")
79
+ nil
118
80
  end
119
81
 
120
- def handle_error?(error, key)
121
- self.configuration_success = false
122
- Output.print_error("Failed to configure tenant '#{key}': #{error.message}")
82
+ def print_error_details(error, key)
83
+ message = Connections::DiagnosticHelpers.scrub(error.message)
84
+ Output.print_error("Failed to configure tenant '#{key}': #{message}")
123
85
  Output.print_backtrace(error)
124
- false
86
+ nil
125
87
  end
126
88
  end
127
89
  end