console_kit 0.1.5 → 1.1.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6519b1ab2c0164341a30cf828095d91fe8e3872f7456abf0041a4bc58401ec54
4
- data.tar.gz: 33df556d5e20599248763f074a0f8379171bcacddb3996dede936980a1fbe6ae
3
+ metadata.gz: acd47ead1008fc3bcdc25b0d995b735f69d1e6f5a8d5f8442c67dc5ee86bb9a3
4
+ data.tar.gz: 55bd88158a92c6741eab85bda17d4b61828dd101e94cd5ab02097688f8994302
5
5
  SHA512:
6
- metadata.gz: 5d5041a668f6c03adbd7cd41fd902d2a9ff420c0b3da700ae1b56e3b9d5773dfb009bce276ef970eb4e0b09db6aedf41a7404a1a9c8d8c1a8a8a98e5269235ce
7
- data.tar.gz: d6db97c81c56974ff3652dca1fceded6d917556def258670b1710aa8603ef5f9a593b5e426b74ab87b3a380c5288eb2f6688346674a3add582de1e6d09502e45
6
+ metadata.gz: 9d1214289e7859342493f2930557554e05ad3ccb3200e041d3ade9b2f95a710b6f111eaea9bfc40f0192e98cabd1010f7be6c71469066d2fd702a74d70a33132
7
+ data.tar.gz: 4564b76383af8ab6d60c6ee79644fc78a1be8633520ffe1c74a26a69936ebbacb7db8b20dd84c28c2433f532198c46c1c140ff7d31cd323d613559624fabb713
data/CHANGELOG.md CHANGED
@@ -6,6 +6,47 @@ This project adheres to [Semantic Versioning](https://semver.org/).
6
6
 
7
7
  ---
8
8
 
9
+ ## [1.1.0] - 2026-03-14
10
+ ### Added
11
+ - **Redis Connection Handler:** Automatic Redis DB selection per tenant via `Redis.current.select`, with graceful fallback for Redis v5+ where `Redis.current` is deprecated.
12
+ - **Elasticsearch Connection Handler:** Sets a per-tenant Elasticsearch index name prefix via thread-local storage and `Elasticsearch::Model.index_name_prefix=` (when available).
13
+ - **Console Helpers:** New `switch_tenant`, `tenant_info`, and `tenants` methods available in the Rails console for quick tenant management.
14
+ - **Custom Console Prompt:** IRB and Pry prompts now display the active tenant name (e.g., `[acme] main:001>`).
15
+ - **Tenant Banner:** On successful tenant initialization, a banner now shows the tenant name, environment safety warnings (production in red, staging in yellow), and a summary of active connections.
16
+ - **Environment Safety Warnings:** Production and staging environments are flagged with color-coded warnings at tenant setup time.
17
+ - New tenant configuration keys: `redis_db`, `elasticsearch_prefix`, and `environment`.
18
+
19
+ ### Changed
20
+ - `TenantConfigurator` now manages `tenant_redis_db` and `tenant_elasticsearch_prefix` context attributes alongside existing ones.
21
+ - Generator template updated with examples for the new configuration keys.
22
+
23
+ ---
24
+
25
+ ## [1.0.0] - 2026-03-01
26
+ ### Added
27
+ - **Global Configuration Persistence:** ConsoleKit settings now persist across the entire session and across multiple threads.
28
+ - **Isolated Tenant Selection:** Each thread maintains its own tenant selection for safety, while sharing the global configuration.
29
+ - **Seamless Rails Reloading:** Full support for Rails `reload!`; your selected tenant and context are now automatically preserved after code reloads.
30
+ - **Reliable Tenant Switching:** Switching or clearing tenants now correctly resets all database connections (SQL and MongoDB) to their default state.
31
+ - **Flexible Tenant Selection:** Users can now select tenants by typing their names (case-insensitive) in addition to index numbers.
32
+ - **Session Control:** Added support for `exit` or `quit` commands directly at the selection prompt to terminate the console session.
33
+ - **Safe Mode:** Added a "Skip" option (0) to load the console without any tenant configuration.
34
+ - **Improved Configuration Validation:** Enhanced startup checks to provide clearer feedback if the configuration or context class is incorrectly defined.
35
+ - **Custom SQL Base Class:** New configuration option to specify a custom base class for SQL connections.
36
+
37
+ ### Changed
38
+ - **Modernized CLI Interface:** Redesigned the tenant selection menu and prompts for a cleaner, more intuitive user experience.
39
+ - **Enhanced Error Feedback:** Improved messaging for invalid selections and missing configurations.
40
+ - **Optimized Performance:** Refactored internal discovery and configuration logic for better reliability in large applications.
41
+
42
+ ### Fixed
43
+ - Fixed a bug where tenant context was lost after running `reload!` in the Rails console.
44
+ - Fixed an issue where database connections could remain tied to a previous tenant after the context was cleared.
45
+ - Resolved all stability and code quality warnings.
46
+ - Fixed timestamp formatting in console output.
47
+
48
+ ---
49
+
9
50
  ## [0.1.5] - 2025-10-12
10
51
  ### Added
11
52
  - Minor Bug Fixes
@@ -58,10 +99,8 @@ This project adheres to [Semantic Versioning](https://semver.org/).
58
99
  - Tenant-specific database configuration.
59
100
  - Colorized console output for improved UX.
60
101
 
61
- ---
62
-
63
- ## [Unreleased]
64
-
102
+ [1.1.0]: https://github.com/Soumyadeep-ai/console_kit/releases/tag/v1.1.0
103
+ [1.0.0]: https://github.com/Soumyadeep-ai/console_kit/releases/tag/v1.0.0
65
104
  [0.1.5]: https://github.com/Soumyadeep-ai/console_kit/releases/tag/v0.1.5
66
105
  [0.1.4]: https://github.com/Soumyadeep-ai/console_kit/releases/tag/v0.1.4
67
106
  [0.1.3]: https://github.com/Soumyadeep-ai/console_kit/releases/tag/v0.1.3
data/README.md CHANGED
@@ -8,7 +8,7 @@
8
8
 
9
9
  A simple and flexible multi-tenant console setup toolkit for Rails applications.
10
10
 
11
- ConsoleKit helps you manage tenant-specific database connections and context configuration via an easy CLI interface and Rails integration.
11
+ ConsoleKit helps you manage tenant-specific database connections (SQL, MongoDB, Redis, Elasticsearch) and context configuration via an easy CLI interface and Rails integration.
12
12
 
13
13
  ## Installation
14
14
 
@@ -50,10 +50,24 @@ Then, edit config/initializers/console_kit.rb to define your tenants and context
50
50
  ConsoleKit.configure do |config|
51
51
  config.tenants = {
52
52
  tenant_one: {
53
- constants: { shard: :tenant_one_db, mongo_db: :tenant_one_mongo, partner_code: 'partnerA' }
53
+ constants: {
54
+ shard: :tenant_one_db,
55
+ mongo_db: :tenant_one_mongo,
56
+ partner_code: 'partnerA',
57
+ redis_db: 1,
58
+ elasticsearch_prefix: 'tenant_one',
59
+ environment: 'production'
60
+ }
54
61
  },
55
62
  tenant_two: {
56
- constants: { shard: :tenant_two_db, mongo_db: :tenant_two_mongo, partner_code: 'partnerB' }
63
+ constants: {
64
+ shard: :tenant_two_db,
65
+ mongo_db: :tenant_two_mongo,
66
+ partner_code: 'partnerB',
67
+ redis_db: 2,
68
+ elasticsearch_prefix: 'tenant_two',
69
+ environment: 'staging'
70
+ }
57
71
  }
58
72
  }
59
73
 
@@ -64,33 +78,72 @@ ConsoleKit.configure do |config|
64
78
  end
65
79
  ```
66
80
 
81
+ ## Supported Connections
82
+
83
+ ConsoleKit automatically detects and manages connections for:
84
+
85
+ | Connection | Gem Required | Config Key | Behavior |
86
+ |-----------------|-----------------|--------------------------|------------------------------------------------|
87
+ | SQL (ActiveRecord) | `activerecord` | `shard` | Calls `establish_connection` on your base class |
88
+ | MongoDB | `mongoid` | `mongo_db` | Calls `Mongoid.override_database` |
89
+ | Redis | `redis` | `redis_db` | Calls `Redis.current.select(db)` |
90
+ | Elasticsearch | `elasticsearch` | `elasticsearch_prefix` | Sets `Elasticsearch::Model.index_name_prefix=` |
91
+
92
+ Handlers are only activated when their corresponding gem is loaded.
93
+
67
94
  ## Console Usage
68
95
 
69
- When launching the Rails console, ConsoleKit will prompt you to select a tenant (if tenants are configured).
70
- You can also manually interact with it:
96
+ When launching the Rails console, ConsoleKit will prompt you to select a tenant (if multiple tenants are configured). On selection, a tenant banner is displayed showing the tenant name, environment safety warnings, and active connections.
71
97
 
72
- ### Get Current Tenant
73
- ```ruby
74
- ConsoleKit.current_tenant
75
- # => :tenant_one
76
- ```
98
+ ### Selection Options:
99
+ - **Number or Name:** Select a tenant by its index or name (case-insensitive).
100
+ - **0 (Skip):** Load the console without any tenant configuration.
101
+ - **exit / quit:** Immediately terminate the console session.
102
+
103
+ ### Console Helpers
104
+
105
+ The following helper methods are available in your Rails console:
77
106
 
78
- ### Reset Current Tenant
79
107
  ```ruby
80
- ConsoleKit.reset_current_tenant
81
- # => nil
108
+ # Switch to a different tenant
109
+ switch_tenant
110
+
111
+ # Print details about the current tenant
112
+ tenant_info
113
+
114
+ # List all available tenants
115
+ tenants
82
116
  ```
83
117
 
84
- ### Manually Disable Pretty Output
85
- ```ruby
86
- ConsoleKit.enable_pretty_output
118
+ ### Custom Prompt
119
+
120
+ ConsoleKit automatically sets your IRB/Pry prompt to show the active tenant:
121
+
122
+ ```
123
+ [tenant_one] main:001>
87
124
  ```
88
125
 
89
- ### Manually Disable Pretty Output
126
+ ### Other Methods
127
+
90
128
  ```ruby
129
+ # Get current tenant
130
+ ConsoleKit.current_tenant
131
+ # => :tenant_one
132
+
133
+ # Reset and re-select tenant
134
+ ConsoleKit.reset_current_tenant
135
+
136
+ # Toggle pretty output
137
+ ConsoleKit.enable_pretty_output
91
138
  ConsoleKit.disable_pretty_output
92
139
  ```
93
140
 
141
+ ### Environment Warnings
142
+
143
+ When a tenant has an `environment` key in its constants:
144
+ - **production**: A red warning is displayed at setup time.
145
+ - **staging**: A yellow warning is displayed at setup time.
146
+
94
147
  ## Development
95
148
 
96
149
  After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
data/SECURITY.md CHANGED
@@ -8,7 +8,9 @@ Once a new version is released, the previous version is branched and locked, and
8
8
 
9
9
  | Version | Supported |
10
10
  | ------- | ------------------ |
11
- | 0.1.5 | :white_check_mark: |
11
+ | 1.1.0 | :white_check_mark: |
12
+ | 1.0.0 | :x: |
13
+ | 0.1.5 | :x: |
12
14
  | 0.1.4 | :x: |
13
15
  | 0.1.3 | :x: |
14
16
  | 0.1.2 | :x: |
@@ -3,16 +3,43 @@
3
3
  module ConsoleKit
4
4
  # Stores ConsoleKit configurations such as tenant map and context behavior
5
5
  class Configuration
6
- attr_reader :pretty_output, :tenants, :context_class
6
+ attr_accessor :pretty_output, :tenants, :sql_base_class
7
+ attr_writer :context_class
7
8
 
8
- def initialize(tenants: nil, context_class: nil)
9
+ def initialize
9
10
  @pretty_output = true
10
- @tenants = tenants
11
- @context_class = context_class
11
+ @tenants = nil
12
+ @context_class = nil
13
+ @sql_base_class = 'ApplicationRecord'
12
14
  end
13
15
 
14
- %i[pretty_output tenants context_class].each do |attr|
15
- define_method("#{attr}=") { |value| instance_variable_set("@#{attr}", value) }
16
+ def context_class
17
+ case @context_class
18
+ when String, Symbol then resolve_context_class
19
+ else @context_class
20
+ end
21
+ end
22
+
23
+ def validate
24
+ validate!
25
+ true
26
+ rescue Error
27
+ false
28
+ end
29
+
30
+ def validate!
31
+ raise Error, 'ConsoleKit: `tenants` is not configured.' if @tenants.blank?
32
+ raise Error, 'ConsoleKit: `tenants` must be a Hash.' unless @tenants.is_a?(Hash)
33
+ raise Error, 'ConsoleKit: `context_class` is not configured.' if @context_class.blank?
34
+ end
35
+
36
+ private
37
+
38
+ def resolve_context_class
39
+ @context_class.to_s.constantize
40
+ rescue NameError
41
+ raise Error, "ConsoleKit: context_class '#{@context_class}' could not be found. " \
42
+ 'Ensure the class is defined before configuration is accessed.'
16
43
  end
17
44
  end
18
45
  end
@@ -1,18 +1,26 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'active_support/core_ext/class/subclasses'
4
+
3
5
  module ConsoleKit
4
6
  module Connections
5
7
  # Parent class for connection handlers
6
8
  class BaseConnectionHandler
9
+ class << self
10
+ def registry = descendants
11
+ end
12
+
7
13
  attr_reader :context
8
14
 
9
15
  def initialize(context) = @context = context
16
+ def connect = raise NotImplementedError, "#{self.class} must implement #connect"
17
+ def available? = raise NotImplementedError, "#{self.class} must implement #available?"
10
18
 
11
- def connect
12
- raise NotImplementedError, "#{self.class} must implement #connect"
13
- end
19
+ private
14
20
 
15
- def available? = false
21
+ def context_attribute(name)
22
+ @context.respond_to?(name, true) ? @context.send(name) : nil
23
+ end
16
24
  end
17
25
  end
18
26
  end
@@ -2,6 +2,8 @@
2
2
 
3
3
  require_relative 'sql_connection_handler'
4
4
  require_relative 'mongo_connection_handler'
5
+ require_relative 'redis_connection_handler'
6
+ require_relative 'elasticsearch_connection_handler'
5
7
 
6
8
  module ConsoleKit
7
9
  module Connections
@@ -12,12 +14,14 @@ module ConsoleKit
12
14
  handler_classes.filter_map do |klass|
13
15
  handler = klass.new(context)
14
16
  handler if handler.available?
17
+ rescue NotImplementedError
18
+ nil
15
19
  end
16
20
  end
17
21
 
18
22
  private
19
23
 
20
- def handler_classes = BaseConnectionHandler.descendants
24
+ def handler_classes = BaseConnectionHandler.registry
21
25
  end
22
26
  end
23
27
  end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base_connection_handler'
4
+
5
+ module ConsoleKit
6
+ module Connections
7
+ # Handles Elasticsearch connections
8
+ class ElasticsearchConnectionHandler < BaseConnectionHandler
9
+ def connect
10
+ prefix = context_attribute(:tenant_elasticsearch_prefix).presence
11
+ Output.print_info(switch_message(prefix))
12
+ Thread.current[:console_kit_elasticsearch_prefix] = prefix
13
+ apply_model_index_prefix(prefix)
14
+ end
15
+
16
+ def available? = defined?(Elasticsearch)
17
+
18
+ private
19
+
20
+ def apply_model_index_prefix(prefix)
21
+ return unless defined?(Elasticsearch::Model) && Elasticsearch::Model.respond_to?(:index_name_prefix=)
22
+
23
+ Elasticsearch::Model.index_name_prefix = prefix
24
+ end
25
+
26
+ def switch_message(prefix)
27
+ prefix ? "Setting Elasticsearch index prefix: #{prefix}" : 'Resetting Elasticsearch index prefix to default'
28
+ end
29
+ end
30
+ end
31
+ end
@@ -1,26 +1,26 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'forwardable'
4
3
  require_relative 'base_connection_handler'
5
4
 
6
5
  module ConsoleKit
7
6
  module Connections
8
7
  # Handles MongoDB connections
9
8
  class MongoConnectionHandler < BaseConnectionHandler
10
- extend Forwardable
11
-
12
- def_delegator :@context, :tenant_mongo_db
13
-
14
9
  def connect
15
- return if tenant_mongo_db.blank?
16
-
17
- Output.print_info("Switching to MongoDB client: #{tenant_mongo_db}")
18
- Mongoid.override_client(tenant_mongo_db)
10
+ db = context_attribute(:tenant_mongo_db).presence
11
+ Output.print_info(switch_message(db))
12
+ Mongoid.override_database(db)
19
13
  rescue NoMethodError
20
- Output.print_warning('Mongoid.override_client is not defined.')
14
+ Output.print_warning('Mongoid.override_database is not available in this version of Mongoid.')
21
15
  end
22
16
 
23
17
  def available? = defined?(Mongoid)
18
+
19
+ private
20
+
21
+ def switch_message(db)
22
+ db ? "Switching to MongoDB client: #{db}" : 'Resetting MongoDB client to default'
23
+ end
24
24
  end
25
25
  end
26
26
  end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base_connection_handler'
4
+
5
+ module ConsoleKit
6
+ module Connections
7
+ # Handles Redis connections
8
+ class RedisConnectionHandler < BaseConnectionHandler
9
+ DEFAULT_REDIS_DB = 0
10
+
11
+ def connect
12
+ db = context_attribute(:tenant_redis_db)
13
+ Output.print_info(switch_message(db))
14
+ select_redis_db(db.nil? ? DEFAULT_REDIS_DB : db)
15
+ end
16
+
17
+ def available? = defined?(Redis)
18
+
19
+ private
20
+
21
+ def select_redis_db(db)
22
+ if Redis.respond_to?(:current) && Redis.current
23
+ Redis.current.select(db)
24
+ elsif defined?(RedisClient) && db != DEFAULT_REDIS_DB
25
+ Output.print_warning("Redis DB #{db} configured but auto-select not supported with RedisClient. " \
26
+ 'Ensure your Redis configuration sets the correct DB.')
27
+ end
28
+ rescue NoMethodError
29
+ Output.print_warning('Redis.current is not available (deprecated in Redis v5+).')
30
+ end
31
+
32
+ def switch_message(db)
33
+ db ? "Switching to Redis DB: #{db}" : 'Resetting Redis connection to default'
34
+ end
35
+ end
36
+ end
37
+ end
@@ -1,24 +1,33 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'forwardable'
4
3
  require_relative 'base_connection_handler'
5
4
 
6
5
  module ConsoleKit
7
6
  module Connections
8
7
  # Handles SQL connections
9
8
  class SqlConnectionHandler < BaseConnectionHandler
10
- extend Forwardable
9
+ def connect
10
+ shard = context_attribute(:tenant_shard).presence&.to_sym
11
+ Output.print_info("#{connection_message(shard)} via #{base_class}")
12
+ shard ? base_class.establish_connection(shard) : base_class.establish_connection
13
+ end
11
14
 
12
- def_delegator :@context, :tenant_shard
15
+ def available? = sql_base_class_name.to_s.safe_constantize.present?
13
16
 
14
- def connect
15
- return if tenant_shard.blank?
17
+ private
18
+
19
+ def base_class
20
+ klass = sql_base_class_name.to_s.safe_constantize
21
+ return klass if klass
22
+
23
+ raise Error, "ConsoleKit: sql_base_class '#{sql_base_class_name}' could not be found."
24
+ end
16
25
 
17
- Output.print_info("Establishing SQL connection to shard: #{tenant_shard}")
18
- ApplicationRecord.establish_connection(tenant_shard.to_sym)
26
+ def connection_message(shard)
27
+ shard ? "Establishing SQL connection to shard: #{shard}" : 'Resetting SQL connection to default'
19
28
  end
20
29
 
21
- def available? = defined?(ApplicationRecord)
30
+ def sql_base_class_name = ConsoleKit.configuration.sql_base_class
22
31
  end
23
32
  end
24
33
  end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ConsoleKit
4
+ # Helper methods available in the Rails console
5
+ module ConsoleHelpers
6
+ def switch_tenant = ConsoleKit.reset_current_tenant
7
+
8
+ def tenant_info
9
+ tenant = ConsoleKit::Setup.current_tenant
10
+ unless tenant
11
+ ConsoleKit::Output.print_warning('No tenant is currently configured.')
12
+ return
13
+ end
14
+
15
+ constants = ConsoleKit.configuration.tenants[tenant]&.[](:constants) || {}
16
+ print_tenant_details(tenant, constants)
17
+ end
18
+
19
+ def tenants
20
+ names = ConsoleKit.configuration.tenants&.keys || []
21
+ ConsoleKit::Output.print_list(names, header: 'Available Tenants')
22
+ names
23
+ end
24
+
25
+ private
26
+
27
+ def print_tenant_details(tenant, constants)
28
+ ConsoleKit::Output.print_header("Tenant: #{tenant}")
29
+ {
30
+ 'Partner' => :partner_code, 'Shard' => :shard, 'Mongo DB' => :mongo_db,
31
+ 'Redis DB' => :redis_db, 'ES Prefix' => :elasticsearch_prefix, 'Environment' => :environment
32
+ }.each do |label, key|
33
+ ConsoleKit::Output.print_info(" #{label.ljust(13)}#{constants[key]}") unless constants[key].nil?
34
+ end
35
+ nil
36
+ end
37
+ end
38
+ end
@@ -15,24 +15,55 @@ module ConsoleKit
15
15
  }.freeze
16
16
 
17
17
  class << self
18
+ def silent = Thread.current[:console_kit_silent]
19
+
20
+ def silent=(val)
21
+ Thread.current[:console_kit_silent] = val
22
+ end
23
+
24
+ def silence
25
+ old_silent = silent
26
+ self.silent = true
27
+ yield
28
+ ensure
29
+ self.silent = old_silent
30
+ end
31
+
18
32
  TYPES.each_key do |type|
19
- define_method("print_#{type}") do |text, timestamp: false|
20
- formatted = (type == :header ? "\n=== #{text} ===" : text)
21
- print_with(type, formatted, timestamp)
33
+ define_method("print_#{type}") do |text, timestamp: false, newline: (type != :prompt)|
34
+ return if silent
35
+
36
+ formatted = (type == :header ? "\n--- #{text} ---" : text)
37
+ print_with(type, formatted, timestamp, { newline: newline })
22
38
  end
23
39
  end
24
40
 
41
+ def print_list(items, header: nil)
42
+ return if silent
43
+
44
+ print_header(header) if header
45
+ items.each { |item| puts " #{item}" }
46
+ end
47
+
48
+ def print_raw(text)
49
+ return if silent
50
+
51
+ puts text
52
+ end
53
+
25
54
  # Backtrace prints always with timestamp, no param
26
55
  def print_backtrace(exception)
27
- exception&.backtrace&.each { |line| print_with(:trace, " #{line}", true) }
56
+ return if silent
57
+
58
+ exception&.backtrace&.each { |line| print_with(:trace, " #{line}", true, { newline: true }) }
28
59
  end
29
60
 
30
61
  private
31
62
 
32
- def print_with(type, text, timestamp)
63
+ def print_with(type, text, timestamp, opts = {})
33
64
  meta = TYPES.fetch(type)
34
65
  message = build_message(text, meta[:symbol], timestamp)
35
- output(message, meta[:color])
66
+ emit(message, meta[:color], opts.fetch(:newline, true))
36
67
  end
37
68
 
38
69
  def build_message(text, symbol, timestamp)
@@ -43,10 +74,10 @@ module ConsoleKit
43
74
  def timestamp_prefix(timestamp) = prefix_for(timestamp) { Time.current.strftime('[%Y-%m-%d %H:%M:%S] ') }
44
75
  def symbol_prefix(symbol) = prefix_for(symbol) { |sym| "#{sym} " }
45
76
 
46
- def output(message, color)
47
- return puts message unless ConsoleKit.configuration.pretty_output && color
48
-
49
- puts "\e[#{color}m#{message}\e[0m"
77
+ def emit(message, color, newline)
78
+ writer = newline ? :puts : :print
79
+ formatted = ConsoleKit.configuration.pretty_output && color ? "\e[#{color}m#{message}\e[0m" : message
80
+ send(writer, formatted)
50
81
  end
51
82
  end
52
83
  end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ConsoleKit
4
+ # Sets the console prompt to show the current tenant
5
+ module Prompt
6
+ class << self
7
+ def apply
8
+ apply_irb_prompt if defined?(IRB)
9
+ apply_pry_prompt if defined?(Pry)
10
+ end
11
+
12
+ private
13
+
14
+ def tenant_label
15
+ tenant = ConsoleKit::Setup.current_tenant
16
+ tenant ? "[#{tenant}]" : '[no-tenant]'
17
+ end
18
+
19
+ def apply_irb_prompt
20
+ conf = IRB.conf
21
+ conf[:PROMPT] ||= {}
22
+ conf[: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
+ Pry.config.prompt = Pry::Prompt.new(
33
+ 'console_kit',
34
+ 'ConsoleKit tenant prompt',
35
+ [
36
+ proc { |obj, nest_level, _pry_instance| "#{Prompt.send(:tenant_label)} (#{obj}):#{nest_level}> " },
37
+ proc { |obj, nest_level, _pry_instance| "#{Prompt.send(:tenant_label)} (#{obj}):#{nest_level}* " }
38
+ ]
39
+ )
40
+ end
41
+ end
42
+ end
43
+ end
@@ -3,6 +3,16 @@
3
3
  module ConsoleKit
4
4
  # Railtie for integrating ConsoleKit with Rails console.
5
5
  class Railtie < Rails::Railtie
6
- console { ConsoleKit::Setup.setup }
6
+ console do
7
+ ConsoleKit::Setup.setup
8
+ ConsoleKit::Prompt.apply
9
+ if defined?(Pry)
10
+ TOPLEVEL_BINDING.receiver.extend(ConsoleKit::ConsoleHelpers)
11
+ elsif defined?(IRB::ExtendCommandBundle)
12
+ IRB::ExtendCommandBundle.include(ConsoleKit::ConsoleHelpers)
13
+ end
14
+ end
15
+
16
+ config.to_prepare { ConsoleKit::Setup.reapply if defined?(Rails::Console) }
7
17
  end
8
18
  end
@@ -9,27 +9,44 @@ module ConsoleKit
9
9
  # Does the initial setup
10
10
  module Setup
11
11
  class << self
12
- attr_reader :current_tenant
12
+ ENVIRONMENT_WARNINGS = {
13
+ 'production' => -> { Output.print_error('WARNING: You are connected to a PRODUCTION environment!') },
14
+ 'staging' => -> { Output.print_warning('You are connected to a staging environment.') }
15
+ }.freeze
16
+
17
+ def current_tenant = Thread.current[:console_kit_current_tenant]
18
+
19
+ def current_tenant=(val)
20
+ Thread.current[:console_kit_current_tenant] = val
21
+ end
13
22
 
14
23
  def setup = run_setup
15
- def tenant_setup_successful? = !@current_tenant.to_s.empty?
24
+ def tenant_setup_successful? = !current_tenant.to_s.empty?
25
+
26
+ def reapply
27
+ return unless tenant_setup_successful?
28
+
29
+ Output.silence { TenantConfigurator.configure_tenant(current_tenant) }
30
+ end
16
31
 
17
32
  def reset_current_tenant
18
33
  return warn_no_tenants unless tenants?
19
34
 
20
- warn_reset if @current_tenant
21
- TenantConfigurator.clear if @current_tenant
35
+ key = select_tenant_key
36
+ return cancel_switch if key == :abort || key.blank?
37
+
38
+ clear_current_tenant
39
+ return skip_tenant_message if %i[exit none].include?(key)
22
40
 
23
- @current_tenant = nil
24
- setup
41
+ configure(key)
25
42
  end
26
43
 
27
44
  private
28
45
 
29
46
  def run_setup
30
47
  return if tenant_setup_successful?
31
- return Output.print_error('No tenants configured.') if no_tenants?
32
48
 
49
+ ConsoleKit.configuration.validate!
33
50
  select_and_configure
34
51
  rescue StandardError => e
35
52
  handle_error(e)
@@ -37,17 +54,57 @@ module ConsoleKit
37
54
 
38
55
  def select_and_configure
39
56
  key = select_tenant_key
40
- return Output.print_error('No tenant selected. Loading without tenant configuration.') unless key
57
+ return handle_selection_result(key) if %i[exit abort none].include?(key) || key.blank?
41
58
 
42
59
  configure(key)
43
60
  end
44
61
 
62
+ def handle_selection_result(key)
63
+ exit_on_key if %i[exit abort].include?(key)
64
+
65
+ case key
66
+ when :none
67
+ Output.print_info('No tenant selected. Loading without tenant configuration.')
68
+ when nil, ''
69
+ Output.print_error('Tenant selection failed. Loading without tenant configuration.')
70
+ end
71
+ end
72
+
73
+ def exit_on_key
74
+ Output.print_info('Exiting console...')
75
+ Kernel.exit
76
+ end
77
+
45
78
  def configure(key)
46
79
  TenantConfigurator.configure_tenant(key)
47
80
  return unless TenantConfigurator.configuration_success
48
81
 
49
- @current_tenant = key
82
+ self.current_tenant = key
83
+ Prompt.apply
84
+ print_tenant_banner(key)
85
+ end
86
+
87
+ def print_tenant_banner(key)
88
+ constants = ConsoleKit.configuration.tenants[key]&.[](:constants) || {}
89
+ env = constants[:environment]&.to_s&.downcase
50
90
  Output.print_success("Tenant initialized: #{key}")
91
+ print_environment_warning(env) if env
92
+ print_active_connections
93
+ end
94
+
95
+ def print_environment_warning(env) = ENVIRONMENT_WARNINGS[env]&.call
96
+
97
+ def print_active_connections
98
+ names = active_connection_names
99
+ Output.print_info("Active connections: #{names.join(', ')}") unless names.empty?
100
+ end
101
+
102
+ def active_connection_names
103
+ ctx = context_class
104
+ return [] unless ctx
105
+
106
+ handlers = ConsoleKit::Connections::ConnectionManager.available_handlers(ctx)
107
+ handlers.map { |h| h.class.name.demodulize.delete_suffix('ConnectionHandler') }
51
108
  end
52
109
 
53
110
  def tenants = ConsoleKit.configuration.tenants
@@ -59,7 +116,17 @@ module ConsoleKit
59
116
  def single_tenant? = tenants.size == 1
60
117
  def non_interactive? = !$stdin.tty?
61
118
  def warn_no_tenants = Output.print_warning('Cannot reset tenant: No tenants configured.')
62
- def warn_reset = Output.print_warning("Resetting tenant: #{@current_tenant}")
119
+ def warn_reset = Output.print_warning("Resetting tenant: #{current_tenant}")
120
+ def cancel_switch = Output.print_warning('Tenant switch cancelled.')
121
+ def skip_tenant_message = Output.print_info('No tenant selected. Loading without tenant configuration.')
122
+
123
+ def clear_current_tenant
124
+ if current_tenant
125
+ warn_reset
126
+ TenantConfigurator.clear
127
+ end
128
+ self.current_tenant = nil
129
+ end
63
130
 
64
131
  def handle_error(error)
65
132
  Output.print_error("Error setting up tenant: #{error.message}")
@@ -7,7 +7,18 @@ module ConsoleKit
7
7
  # For tenant configuration
8
8
  module TenantConfigurator
9
9
  class << self
10
- attr_reader :configuration_success
10
+ HANDLER_ATTRIBUTES = {
11
+ Connections::SqlConnectionHandler => :tenant_shard,
12
+ Connections::MongoConnectionHandler => :tenant_mongo_db,
13
+ Connections::RedisConnectionHandler => :tenant_redis_db,
14
+ Connections::ElasticsearchConnectionHandler => :tenant_elasticsearch_prefix
15
+ }.freeze
16
+
17
+ def configuration_success = Thread.current[:console_kit_configuration_success]
18
+
19
+ def configuration_success=(val)
20
+ Thread.current[:console_kit_configuration_success] = val
21
+ end
11
22
 
12
23
  def configure_tenant(key)
13
24
  constants = ConsoleKit.configuration.tenants[key]&.[](:constants)
@@ -19,22 +30,34 @@ module ConsoleKit
19
30
  end
20
31
 
21
32
  def clear
22
- @configuration_success = false
23
- %i[tenant_shard tenant_mongo_db partner_identifier].each do |attr|
24
- ConsoleKit.configuration.context_class.public_send("#{attr}=", nil)
25
- end
33
+ ctx = ConsoleKit.configuration.context_class
34
+ return unless ctx
35
+
36
+ reset_tenant(ctx)
26
37
  Output.print_info('Tenant context has been cleared.')
27
38
  end
28
39
 
29
40
  private
30
41
 
42
+ def reset_tenant(ctx)
43
+ self.configuration_success = false
44
+ reset_context_attributes(ctx)
45
+ setup_connections(ctx)
46
+ end
47
+
48
+ def reset_context_attributes(ctx)
49
+ available_context_attributes(ctx).each do |attr|
50
+ ctx.public_send("#{attr}=", nil)
51
+ end
52
+ end
53
+
31
54
  def validate_constants!(constants)
32
55
  missing = %i[shard partner_code] - constants.keys
33
- raise "Tenant constants missing keys: #{missing.join(', ')}" unless missing.empty?
56
+ raise Error, "Tenant constants missing keys: #{missing.join(', ')}" unless missing.empty?
34
57
  end
35
58
 
36
59
  def missing_config_error(key)
37
- @configuration_success = false
60
+ self.configuration_success = false
38
61
  Output.print_error("No configuration found for tenant: #{key}")
39
62
  end
40
63
 
@@ -44,26 +67,51 @@ module ConsoleKit
44
67
  configure_success(key)
45
68
  end
46
69
 
70
+ def handler_available?(handler_class)
71
+ handler_class.new(nil).available?
72
+ rescue NotImplementedError, StandardError
73
+ false
74
+ end
75
+
76
+ def available_context_attributes(ctx)
77
+ attributes = %i[partner_identifier]
78
+ HANDLER_ATTRIBUTES.each do |handler, attr|
79
+ attributes << attr if handler_available?(handler) && ctx.respond_to?("#{attr}=")
80
+ end
81
+ attributes.select { |attr| ctx.respond_to?("#{attr}=") }
82
+ end
83
+
47
84
  def apply_context(constant)
48
85
  ctx = ConsoleKit.configuration.context_class
49
- ctx.tenant_shard = constant[:shard]
50
- ctx.tenant_mongo_db = constant[:mongo_db]
51
- ctx.partner_identifier = constant[:partner_code]
52
-
86
+ assign_context_attributes(ctx, constant)
53
87
  setup_connections(ctx)
54
88
  end
55
89
 
90
+ def assign_context_attributes(ctx, constant)
91
+ attribute_to_constant = {
92
+ partner_identifier: :partner_code,
93
+ tenant_shard: :shard,
94
+ tenant_mongo_db: :mongo_db,
95
+ tenant_redis_db: :redis_db,
96
+ tenant_elasticsearch_prefix: :elasticsearch_prefix
97
+ }
98
+
99
+ available_context_attributes(ctx).each do |attr|
100
+ ctx.public_send("#{attr}=", constant[attribute_to_constant[attr]])
101
+ end
102
+ end
103
+
56
104
  def setup_connections(context)
57
105
  ConsoleKit::Connections::ConnectionManager.available_handlers(context).each(&:connect)
58
106
  end
59
107
 
60
108
  def configure_success(key)
61
109
  Output.print_success("Tenant set to: #{key}")
62
- @configuration_success = true
110
+ self.configuration_success = true
63
111
  end
64
112
 
65
113
  def handle_error(error, key)
66
- @configuration_success = false
114
+ self.configuration_success = false
67
115
  Output.print_error("Failed to configure tenant '#{key}': #{error.message}")
68
116
  Output.print_backtrace(error)
69
117
  end
@@ -10,35 +10,56 @@ module ConsoleKit
10
10
 
11
11
  class << self
12
12
  def select
13
- attempt_selection(RETRY_LIMIT)
13
+ RETRY_LIMIT.times do
14
+ result = attempt_selection
15
+ return result unless result == :retry
16
+ end
17
+ nil
14
18
  end
15
19
 
16
20
  private
17
21
 
18
- def attempt_selection(retries_left)
19
- return nil if retries_left.zero?
20
-
22
+ def attempt_selection
21
23
  print_tenant_selection_menu
22
24
  selection = parse_user_selection
23
- selection ? resolve_selection(selection) : attempt_selection(retries_left - 1)
25
+ return :abort if selection == :abort
26
+ return :retry unless selection
27
+
28
+ selection.is_a?(Integer) ? resolve_selection(selection) : selection
24
29
  end
25
30
 
26
31
  def print_tenant_selection_menu
27
32
  Output.print_header('Multiple tenants detected. Please choose one:')
28
- Output.print_info(' 0. Load without tenant (no tenant configuration)')
33
+ Output.print_list(menu_items)
34
+ end
29
35
 
36
+ def menu_items
37
+ items = ['0. Skip (load without tenant configuration)']
30
38
  ConsoleKit.tenants.keys.each_with_index do |key, index|
31
- Output.print_info(" #{index + 1}. #{key} (partner: #{tenant_partner(key)})")
39
+ items << "#{index + 1}. #{key} (partner: #{tenant_partner(key)})"
32
40
  end
41
+ items
33
42
  end
34
43
 
35
44
  def tenant_partner(key) = ConsoleKit.tenants.dig(key, :constants, :partner_code) || 'N/A'
36
45
 
37
46
  def parse_user_selection
38
47
  input = read_input_with_default
39
- return handle_invalid_input('Invalid input. Please enter a number.') unless valid_integer?(input)
48
+ return :abort if input == :abort
49
+ return :exit if %w[exit quit].include?(input.downcase)
50
+ return find_tenant_by_name(input) unless valid_integer?(input)
51
+
52
+ validate_index_range(input.to_i)
53
+ end
40
54
 
41
- index = input.to_i
55
+ def find_tenant_by_name(input)
56
+ match = ConsoleKit.tenants.keys.find { |k| k.to_s.casecmp(input).zero? }
57
+ return match if match
58
+
59
+ handle_invalid_input("Invalid selection: '#{input}'. Please enter a number or tenant name.")
60
+ end
61
+
62
+ def validate_index_range(index)
42
63
  unless valid_selection_index?(index)
43
64
  return handle_invalid_input("Selection must be between 0 and #{max_index}.")
44
65
  end
@@ -47,11 +68,16 @@ module ConsoleKit
47
68
  end
48
69
 
49
70
  def read_input_with_default
50
- prompt_message = "\nEnter the number of the tenant you want " \
51
- "(or press Enter for default '#{DEFAULT_SELECTION}'): "
52
- Output.print_prompt(prompt_message)
53
- input = $stdin.gets&.chomp&.strip
54
- input.to_s.empty? ? DEFAULT_SELECTION : input
71
+ Output.print_prompt("Selection (number or name) [#{DEFAULT_SELECTION}]: ")
72
+ raw_input = $stdin.gets
73
+ raw_input ? normalize_input(raw_input) : :abort
74
+ rescue Interrupt
75
+ :abort
76
+ end
77
+
78
+ def normalize_input(raw_input)
79
+ input = raw_input.chomp.strip
80
+ input.empty? ? DEFAULT_SELECTION : input
55
81
  end
56
82
 
57
83
  def handle_invalid_input(message) = Output.print_warning(message).then { nil }
@@ -60,7 +86,7 @@ module ConsoleKit
60
86
  def valid_selection_index?(index) = index.between?(0, max_index)
61
87
 
62
88
  def resolve_selection(index)
63
- return nil if index.zero?
89
+ return :none if index.zero?
64
90
 
65
91
  ConsoleKit.tenants.keys[index - 1]
66
92
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ConsoleKit
4
- VERSION = '0.1.5'
4
+ VERSION = '1.1.0'
5
5
  end
data/lib/console_kit.rb CHANGED
@@ -1,29 +1,51 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'active_support/core_ext/object/blank'
4
+ require 'active_support/core_ext/object/inclusion'
5
+ require 'active_support/core_ext/string/inflections'
6
+
3
7
  require_relative 'console_kit/version'
4
8
  require_relative 'console_kit/configuration'
5
9
  require_relative 'console_kit/setup'
10
+ require_relative 'console_kit/console_helpers'
11
+ require_relative 'console_kit/prompt'
6
12
  require_relative 'console_kit/railtie' if defined?(Rails::Railtie)
7
13
 
8
- # Main module for console kit
14
+ # Main module for ConsoleKit
9
15
  module ConsoleKit
10
16
  # Base error class for ConsoleKit-related exceptions.
11
17
  class Error < StandardError; end
12
18
 
13
19
  class << self
14
20
  def configure = yield(configuration)
21
+ def configuration = @configuration ||= Configuration.new
22
+
23
+ def reset_configuration!
24
+ @configuration = nil
25
+ Setup.current_tenant = nil
26
+ TenantConfigurator.configuration_success = false if defined?(TenantConfigurator)
27
+ end
28
+
29
+ def pretty_output = configuration.pretty_output
30
+
31
+ def pretty_output=(val)
32
+ configuration.pretty_output = val
33
+ end
15
34
 
16
- def configuration = Thread.current[:console_kit_configuration] ||= Configuration.new
17
- def reset_configuration! = Thread.current[:console_kit_configuration] = nil
35
+ def tenants = configuration.tenants
18
36
 
19
- %i[pretty_output tenants context_class].each do |name|
20
- define_method(name) { configuration.public_send(name) }
21
- define_method("#{name}=") { |val| configuration.public_send("#{name}=", val) }
37
+ def tenants=(val)
38
+ configuration.tenants = val
39
+ end
40
+
41
+ def context_class = configuration.context_class
42
+
43
+ def context_class=(val)
44
+ configuration.context_class = val
22
45
  end
23
46
 
24
47
  def current_tenant = Setup.current_tenant
25
48
  def reset_current_tenant = Setup.reset_current_tenant
26
-
27
49
  def enable_pretty_output = configuration.pretty_output = true
28
50
  def disable_pretty_output = configuration.pretty_output = false
29
51
  end
@@ -11,20 +11,27 @@ Rails.application.config.after_initialize do
11
11
  # constants: {
12
12
  # shard: :shard_1,
13
13
  # mongo_db: 'mongo_db_1',
14
- # partner_code: 'partner_a'
14
+ # partner_code: 'partner_a',
15
+ # redis_db: 1,
16
+ # elasticsearch_prefix: 'tenant_a',
17
+ # environment: 'production'
15
18
  # }
16
19
  # },
17
20
  # tenant_b: {
18
21
  # constants: {
19
22
  # shard: :shard_2,
20
23
  # mongo_db: 'mongo_db_2',
21
- # partner_code: 'partner_b'
24
+ # partner_code: 'partner_b',
25
+ # redis_db: 2,
26
+ # elasticsearch_prefix: 'tenant_b',
27
+ # environment: 'staging'
22
28
  # }
23
29
  # }
24
30
  # }
25
31
  config.tenants = nil
26
32
 
27
- # TODO: Set your context class (e.g., CurrentContext)
33
+ # TODO: Set your context class (e.g., 'CurrentContext')
34
+ # Recommendation: Use a String to ensure the class is correctly re-resolved after `reload!`
28
35
  config.context_class = nil
29
36
 
30
37
  # Toggle pretty output on/off (default: true)
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: console_kit
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.5
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Soumyadeep Pal
@@ -9,20 +9,6 @@ bindir: exe
9
9
  cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
12
- - !ruby/object:Gem::Dependency
13
- name: mongoid
14
- requirement: !ruby/object:Gem::Requirement
15
- requirements:
16
- - - ">="
17
- - !ruby/object:Gem::Version
18
- version: '0'
19
- type: :runtime
20
- prerelease: false
21
- version_requirements: !ruby/object:Gem::Requirement
22
- requirements:
23
- - - ">="
24
- - !ruby/object:Gem::Version
25
- version: '0'
26
12
  - !ruby/object:Gem::Dependency
27
13
  name: rails
28
14
  requirement: !ruby/object:Gem::Requirement
@@ -44,22 +30,22 @@ executables: []
44
30
  extensions: []
45
31
  extra_rdoc_files: []
46
32
  files:
47
- - ".reek.yml"
48
- - ".rspec"
49
- - ".rubocop.yml"
50
33
  - CHANGELOG.md
51
34
  - CODE_OF_CONDUCT.md
52
35
  - LICENSE.txt
53
36
  - README.md
54
- - Rakefile
55
37
  - SECURITY.md
56
38
  - lib/console_kit.rb
57
39
  - lib/console_kit/configuration.rb
58
40
  - lib/console_kit/connections/base_connection_handler.rb
59
41
  - lib/console_kit/connections/connection_manager.rb
42
+ - lib/console_kit/connections/elasticsearch_connection_handler.rb
60
43
  - lib/console_kit/connections/mongo_connection_handler.rb
44
+ - lib/console_kit/connections/redis_connection_handler.rb
61
45
  - lib/console_kit/connections/sql_connection_handler.rb
46
+ - lib/console_kit/console_helpers.rb
62
47
  - lib/console_kit/output.rb
48
+ - lib/console_kit/prompt.rb
63
49
  - lib/console_kit/railtie.rb
64
50
  - lib/console_kit/setup.rb
65
51
  - lib/console_kit/tenant_configurator.rb
data/.reek.yml DELETED
@@ -1,4 +0,0 @@
1
- detectors:
2
- UncommunicativeVariableName:
3
- exclude:
4
- - e
data/.rspec DELETED
@@ -1,3 +0,0 @@
1
- --format documentation
2
- --color
3
- --require spec_helper
data/.rubocop.yml DELETED
@@ -1,11 +0,0 @@
1
- AllCops:
2
- TargetRubyVersion: 3.1
3
- NewCops: enable
4
-
5
- plugins:
6
- - rubocop-rspec
7
- - rubocop-rake
8
-
9
- Metrics/BlockLength:
10
- Exclude:
11
- - 'spec/**/*_spec.rb'
data/Rakefile DELETED
@@ -1,12 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'bundler/gem_tasks'
4
- require 'rspec/core/rake_task'
5
-
6
- RSpec::Core::RakeTask.new(:spec)
7
-
8
- require 'rubocop/rake_task'
9
-
10
- RuboCop::RakeTask.new
11
-
12
- task default: %i[spec rubocop]