vector_mcp 0.3.1 → 0.3.2

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.
@@ -19,6 +19,7 @@ require_relative "security/session_context"
19
19
  require_relative "security/strategies/api_key"
20
20
  require_relative "security/strategies/jwt_token"
21
21
  require_relative "security/strategies/custom"
22
+ require_relative "middleware"
22
23
 
23
24
  module VectorMCP
24
25
  # The `Server` class is the central component for an MCP server implementation.
@@ -70,7 +71,7 @@ module VectorMCP
70
71
  PROTOCOL_VERSION = "2024-11-05"
71
72
 
72
73
  attr_reader :logger, :name, :version, :protocol_version, :tools, :resources, :prompts, :roots, :in_flight_requests,
73
- :auth_manager, :authorization, :security_middleware
74
+ :auth_manager, :authorization, :security_middleware, :middleware_manager
74
75
  attr_accessor :transport
75
76
 
76
77
  # Initializes a new VectorMCP server.
@@ -98,8 +99,8 @@ module VectorMCP
98
99
  @name = name_pos || name || "UnnamedServer"
99
100
  @version = version
100
101
  @protocol_version = options[:protocol_version] || PROTOCOL_VERSION
101
- @logger = VectorMCP.logger
102
- @logger.level = options[:log_level] if options[:log_level]
102
+ @logger = VectorMCP.logger_for("server")
103
+ # NOTE: log level should be configured via VectorMCP.configure_logging instead
103
104
 
104
105
  @transport = nil
105
106
  @tools = {}
@@ -121,6 +122,9 @@ module VectorMCP
121
122
  @authorization = Security::Authorization.new
122
123
  @security_middleware = Security::Middleware.new(@auth_manager, @authorization)
123
124
 
125
+ # Initialize middleware manager
126
+ @middleware_manager = Middleware::Manager.new
127
+
124
128
  setup_default_handlers
125
129
 
126
130
  @logger.info("Server instance '#{@name}' v#{@version} (MCP Protocol: #{@protocol_version}, Gem: v#{VectorMCP::VERSION}) initialized.")
@@ -133,7 +137,7 @@ module VectorMCP
133
137
  # @param transport [:stdio, :sse, VectorMCP::Transport::Base] The transport to use.
134
138
  # Can be a symbol (`:stdio`, `:sse`) or an initialized transport instance.
135
139
  # If a symbol is provided, the method will instantiate the corresponding transport class.
136
- # If `:sse` is chosen, ensure `async` and `falcon` gems are available.
140
+ # If `:sse` is chosen, it uses Puma as the HTTP server.
137
141
  # @param options [Hash] Transport-specific options (e.g., `:host`, `:port` for SSE).
138
142
  # These are passed to the transport's constructor if a symbol is provided for `transport`.
139
143
  # @return [void]
@@ -148,7 +152,7 @@ module VectorMCP
148
152
  require_relative "transport/sse"
149
153
  VectorMCP::Transport::SSE.new(self, **options)
150
154
  rescue LoadError => e
151
- logger.fatal("SSE transport requires additional dependencies. Install the 'async' and 'falcon' gems.")
155
+ logger.fatal("SSE transport requires additional dependencies.")
152
156
  raise NotImplementedError, "SSE transport dependencies not available: #{e.message}"
153
157
  end
154
158
  when VectorMCP::Transport::Base # Allow passing an initialized transport instance
@@ -202,9 +206,9 @@ module VectorMCP
202
206
  # Enable authorization with optional policy configuration block
203
207
  # @param block [Proc] optional block for configuring authorization policies
204
208
  # @return [void]
205
- def enable_authorization!(&)
209
+ def enable_authorization!(&block)
206
210
  @authorization.enable!
207
- instance_eval(&) if block_given?
211
+ instance_eval(&block) if block_given?
208
212
  @logger.info("Authorization enabled")
209
213
  end
210
214
 
@@ -218,29 +222,29 @@ module VectorMCP
218
222
  # Add authorization policy for tools
219
223
  # @param block [Proc] policy block that receives (user, action, tool)
220
224
  # @return [void]
221
- def authorize_tools(&)
222
- @authorization.add_policy(:tool, &)
225
+ def authorize_tools(&block)
226
+ @authorization.add_policy(:tool, &block)
223
227
  end
224
228
 
225
229
  # Add authorization policy for resources
226
230
  # @param block [Proc] policy block that receives (user, action, resource)
227
231
  # @return [void]
228
- def authorize_resources(&)
229
- @authorization.add_policy(:resource, &)
232
+ def authorize_resources(&block)
233
+ @authorization.add_policy(:resource, &block)
230
234
  end
231
235
 
232
236
  # Add authorization policy for prompts
233
237
  # @param block [Proc] policy block that receives (user, action, prompt)
234
238
  # @return [void]
235
- def authorize_prompts(&)
236
- @authorization.add_policy(:prompt, &)
239
+ def authorize_prompts(&block)
240
+ @authorization.add_policy(:prompt, &block)
237
241
  end
238
242
 
239
243
  # Add authorization policy for roots
240
244
  # @param block [Proc] policy block that receives (user, action, root)
241
245
  # @return [void]
242
- def authorize_roots(&)
243
- @authorization.add_policy(:root, &)
246
+ def authorize_roots(&block)
247
+ @authorization.add_policy(:root, &block)
244
248
  end
245
249
 
246
250
  # Check if security features are enabled
@@ -255,6 +259,46 @@ module VectorMCP
255
259
  @security_middleware.security_status
256
260
  end
257
261
 
262
+ # --- Middleware Management ---
263
+
264
+ # Register middleware for specific hook types
265
+ # @param middleware_class [Class] Middleware class inheriting from VectorMCP::Middleware::Base
266
+ # @param hooks [Symbol, Array<Symbol>] Hook types to register for (e.g., :before_tool_call, [:before_tool_call, :after_tool_call])
267
+ # @param priority [Integer] Execution priority (lower numbers execute first, default: 100)
268
+ # @param conditions [Hash] Conditions for when middleware should run
269
+ # @option conditions [Array<String>] :only_operations Only run for these operations
270
+ # @option conditions [Array<String>] :except_operations Don't run for these operations
271
+ # @option conditions [Array<String>] :only_users Only run for these user IDs
272
+ # @option conditions [Array<String>] :except_users Don't run for these user IDs
273
+ # @option conditions [Boolean] :critical If true, errors in this middleware stop execution
274
+ # @example
275
+ # server.use_middleware(MyMiddleware, :before_tool_call)
276
+ # server.use_middleware(AuthMiddleware, [:before_request, :after_response], priority: 10)
277
+ # server.use_middleware(LoggingMiddleware, :after_tool_call, conditions: { only_operations: ['important_tool'] })
278
+ def use_middleware(middleware_class, hooks, priority: Middleware::Hook::DEFAULT_PRIORITY, conditions: {})
279
+ @middleware_manager.register(middleware_class, hooks, priority: priority, conditions: conditions)
280
+ @logger.info("Registered middleware: #{middleware_class.name}")
281
+ end
282
+
283
+ # Remove all middleware hooks for a specific class
284
+ # @param middleware_class [Class] Middleware class to remove
285
+ def remove_middleware(middleware_class)
286
+ @middleware_manager.unregister(middleware_class)
287
+ @logger.info("Removed middleware: #{middleware_class.name}")
288
+ end
289
+
290
+ # Get middleware statistics
291
+ # @return [Hash] Statistics about registered middleware
292
+ def middleware_stats
293
+ @middleware_manager.stats
294
+ end
295
+
296
+ # Clear all middleware (useful for testing)
297
+ def clear_middleware!
298
+ @middleware_manager.clear!
299
+ @logger.info("Cleared all middleware")
300
+ end
301
+
258
302
  private
259
303
 
260
304
  # Add API key authentication strategy
@@ -276,8 +320,8 @@ module VectorMCP
276
320
  # Add custom authentication strategy
277
321
  # @param handler [Proc] custom authentication handler block
278
322
  # @return [void]
279
- def add_custom_auth(&)
280
- strategy = Security::Strategies::Custom.new(&)
323
+ def add_custom_auth(&block)
324
+ strategy = Security::Strategies::Custom.new(&block)
281
325
  @auth_manager.add_strategy(:custom, strategy)
282
326
  end
283
327
 
@@ -95,10 +95,43 @@ module VectorMCP
95
95
  def sample(request_params, timeout: nil)
96
96
  validate_sampling_preconditions
97
97
 
98
- sampling_req_obj = VectorMCP::Sampling::Request.new(request_params)
99
- @logger.info("[Session #{@id}] Sending sampling/createMessage request to client.")
100
-
101
- send_sampling_request(sampling_req_obj, timeout)
98
+ # Create middleware context for sampling
99
+ context = VectorMCP::Middleware::Context.new(
100
+ operation_type: :sampling,
101
+ operation_name: "createMessage",
102
+ params: request_params,
103
+ session: self,
104
+ server: @server,
105
+ metadata: { start_time: Time.now, timeout: timeout }
106
+ )
107
+
108
+ # Execute before_sampling_request hooks
109
+ context = @server.middleware_manager.execute_hooks(:before_sampling_request, context)
110
+ raise context.error if context.error?
111
+
112
+ begin
113
+ sampling_req_obj = VectorMCP::Sampling::Request.new(request_params)
114
+ @logger.info("[Session #{@id}] Sending sampling/createMessage request to client.")
115
+
116
+ result = send_sampling_request(sampling_req_obj, timeout)
117
+
118
+ # Set result in context
119
+ context.result = result
120
+
121
+ # Execute after_sampling_response hooks
122
+ context = @server.middleware_manager.execute_hooks(:after_sampling_response, context)
123
+
124
+ context.result
125
+ rescue StandardError => e
126
+ # Set error in context and execute error hooks
127
+ context.error = e
128
+ context = @server.middleware_manager.execute_hooks(:on_sampling_error, context)
129
+
130
+ # Re-raise unless middleware handled the error
131
+ raise e unless context.result
132
+
133
+ context.result
134
+ end
102
135
  end
103
136
 
104
137
  private
@@ -2,5 +2,5 @@
2
2
 
3
3
  module VectorMCP
4
4
  # The current version of the VectorMCP gem.
5
- VERSION = "0.3.1"
5
+ VERSION = "0.3.2"
6
6
  end
data/lib/vector_mcp.rb CHANGED
@@ -11,15 +11,16 @@ require_relative "vector_mcp/util"
11
11
  require_relative "vector_mcp/image_util"
12
12
  require_relative "vector_mcp/handlers/core"
13
13
  require_relative "vector_mcp/transport/stdio"
14
- # require_relative "vector_mcp/transport/sse" # Load on demand to avoid async dependencies
15
- require_relative "vector_mcp/logging"
14
+ # require_relative "vector_mcp/transport/sse" # Load on demand
15
+ require_relative "vector_mcp/logger"
16
+ require_relative "vector_mcp/middleware"
16
17
  require_relative "vector_mcp/server"
17
18
 
18
19
  # The VectorMCP module provides a full-featured, opinionated Ruby implementation
19
20
  # of the **Model Context Protocol (MCP)**. It gives developers everything needed
20
21
  # to spin up an MCP-compatible server—including:
21
22
  #
22
- # * **Transport adapters** (synchronous `stdio` or asynchronous HTTP + SSE)
23
+ # * **Transport adapters** (synchronous `stdio` or HTTP + SSE)
23
24
  # * **High-level abstractions** for *tools*, *resources*, and *prompts*
24
25
  # * **JSON-RPC 2.0** message handling with sensible defaults and detailed
25
26
  # error reporting helpers
@@ -45,44 +46,18 @@ require_relative "vector_mcp/server"
45
46
  # order to serve multiple concurrent clients over HTTP.
46
47
  #
47
48
  module VectorMCP
48
- # @return [Logger] the shared logger instance for the library.
49
- @logger = Logger.new($stderr, level: Logger::INFO, progname: "VectorMCP")
50
-
51
- # @return [VectorMCP::Logging::Core] the new structured logging system
52
- @logging_core = nil
53
-
54
49
  class << self
55
- # @!attribute [r] logger
56
- # @return [Logger] the shared logger instance for the library (legacy compatibility).
57
- def logger
58
- if @logging_core
59
- @logging_core.legacy_logger
60
- else
61
- @logger
62
- end
63
- end
64
-
65
- # Initialize the new structured logging system
66
- # @param config [Hash, VectorMCP::Logging::Configuration] logging configuration
67
- # @return [VectorMCP::Logging::Core] the logging core instance
68
- def setup_logging(config = {})
69
- configuration = config.is_a?(Logging::Configuration) ? config : Logging::Configuration.new(config)
70
- @logging_core = Logging::Core.new(configuration)
71
- end
72
-
73
50
  # Get a component-specific logger
74
51
  # @param component [String, Symbol] the component name
75
- # @return [VectorMCP::Logging::Component] component logger
52
+ # @return [VectorMCP::Logger] component logger
76
53
  def logger_for(component)
77
- setup_logging unless @logging_core
78
- @logging_core.logger_for(component)
54
+ Logger.for(component)
79
55
  end
80
56
 
81
- # Configure the logging system
82
- # @yield [VectorMCP::Logging::Configuration] configuration block
83
- def configure_logging(&)
84
- setup_logging unless @logging_core
85
- @logging_core.configure(&)
57
+ # Get the default logger
58
+ # @return [VectorMCP::Logger] default logger
59
+ def logger
60
+ @logger ||= Logger.for("vectormcp")
86
61
  end
87
62
 
88
63
  # Creates a new {VectorMCP::Server} instance. This is a **thin wrapper** around
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: vector_mcp
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.1
4
+ version: 0.3.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sergio Bayona
@@ -66,47 +66,47 @@ dependencies:
66
66
  - !ruby/object:Gem::Version
67
67
  version: '3.0'
68
68
  - !ruby/object:Gem::Dependency
69
- name: puma
69
+ name: jwt
70
70
  requirement: !ruby/object:Gem::Requirement
71
71
  requirements:
72
72
  - - "~>"
73
73
  - !ruby/object:Gem::Version
74
- version: '6.4'
74
+ version: '2.7'
75
75
  type: :runtime
76
76
  prerelease: false
77
77
  version_requirements: !ruby/object:Gem::Requirement
78
78
  requirements:
79
79
  - - "~>"
80
80
  - !ruby/object:Gem::Version
81
- version: '6.4'
81
+ version: '2.7'
82
82
  - !ruby/object:Gem::Dependency
83
- name: rack
83
+ name: puma
84
84
  requirement: !ruby/object:Gem::Requirement
85
85
  requirements:
86
86
  - - "~>"
87
87
  - !ruby/object:Gem::Version
88
- version: '3.0'
88
+ version: '6.4'
89
89
  type: :runtime
90
90
  prerelease: false
91
91
  version_requirements: !ruby/object:Gem::Requirement
92
92
  requirements:
93
93
  - - "~>"
94
94
  - !ruby/object:Gem::Version
95
- version: '3.0'
95
+ version: '6.4'
96
96
  - !ruby/object:Gem::Dependency
97
- name: jwt
97
+ name: rack
98
98
  requirement: !ruby/object:Gem::Requirement
99
99
  requirements:
100
100
  - - "~>"
101
101
  - !ruby/object:Gem::Version
102
- version: '2.7'
102
+ version: '3.0'
103
103
  type: :runtime
104
104
  prerelease: false
105
105
  version_requirements: !ruby/object:Gem::Requirement
106
106
  requirements:
107
107
  - - "~>"
108
108
  - !ruby/object:Gem::Version
109
- version: '2.7'
109
+ version: '3.0'
110
110
  description: A Ruby gem implementing the Model Context Protocol (MCP) server-side
111
111
  specification. Provides a framework for creating MCP servers that expose tools,
112
112
  resources, prompts, and roots to LLM clients with comprehensive security features,
@@ -129,19 +129,12 @@ files:
129
129
  - lib/vector_mcp/errors.rb
130
130
  - lib/vector_mcp/handlers/core.rb
131
131
  - lib/vector_mcp/image_util.rb
132
- - lib/vector_mcp/logging.rb
133
- - lib/vector_mcp/logging/component.rb
134
- - lib/vector_mcp/logging/configuration.rb
135
- - lib/vector_mcp/logging/constants.rb
136
- - lib/vector_mcp/logging/core.rb
137
- - lib/vector_mcp/logging/filters/component.rb
138
- - lib/vector_mcp/logging/filters/level.rb
139
- - lib/vector_mcp/logging/formatters/base.rb
140
- - lib/vector_mcp/logging/formatters/json.rb
141
- - lib/vector_mcp/logging/formatters/text.rb
142
- - lib/vector_mcp/logging/outputs/base.rb
143
- - lib/vector_mcp/logging/outputs/console.rb
144
- - lib/vector_mcp/logging/outputs/file.rb
132
+ - lib/vector_mcp/logger.rb
133
+ - lib/vector_mcp/middleware.rb
134
+ - lib/vector_mcp/middleware/base.rb
135
+ - lib/vector_mcp/middleware/context.rb
136
+ - lib/vector_mcp/middleware/hook.rb
137
+ - lib/vector_mcp/middleware/manager.rb
145
138
  - lib/vector_mcp/sampling/request.rb
146
139
  - lib/vector_mcp/sampling/result.rb
147
140
  - lib/vector_mcp/security.rb
@@ -180,7 +173,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
180
173
  requirements:
181
174
  - - ">="
182
175
  - !ruby/object:Gem::Version
183
- version: 3.1.0
176
+ version: 3.0.6
184
177
  required_rubygems_version: !ruby/object:Gem::Requirement
185
178
  requirements:
186
179
  - - ">="
@@ -1,131 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module VectorMCP
4
- module Logging
5
- class Component
6
- attr_reader :name, :core, :config
7
-
8
- def initialize(name, core, config = {})
9
- @name = name.to_s
10
- @core = core
11
- @config = config
12
- @context = {}
13
- end
14
-
15
- def with_context(context)
16
- old_context = @context
17
- @context = @context.merge(context)
18
- yield
19
- ensure
20
- @context = old_context
21
- end
22
-
23
- def add_context(context)
24
- @context = @context.merge(context)
25
- end
26
-
27
- def clear_context
28
- @context = {}
29
- end
30
-
31
- def trace(message = nil, context: {}, &block)
32
- log_with_block(Logging::LEVELS[:TRACE], message, context, &block)
33
- end
34
-
35
- def debug(message = nil, context: {}, &block)
36
- log_with_block(Logging::LEVELS[:DEBUG], message, context, &block)
37
- end
38
-
39
- def info(message = nil, context: {}, &block)
40
- log_with_block(Logging::LEVELS[:INFO], message, context, &block)
41
- end
42
-
43
- def warn(message = nil, context: {}, &block)
44
- log_with_block(Logging::LEVELS[:WARN], message, context, &block)
45
- end
46
-
47
- def error(message = nil, context: {}, &block)
48
- log_with_block(Logging::LEVELS[:ERROR], message, context, &block)
49
- end
50
-
51
- def fatal(message = nil, context: {}, &block)
52
- log_with_block(Logging::LEVELS[:FATAL], message, context, &block)
53
- end
54
-
55
- def security(message = nil, context: {}, &block)
56
- log_with_block(Logging::LEVELS[:SECURITY], message, context, &block)
57
- end
58
-
59
- def level
60
- @core.configuration.level_for(@name)
61
- end
62
-
63
- def level_enabled?(level)
64
- level >= self.level
65
- end
66
-
67
- def trace?
68
- level_enabled?(Logging::LEVELS[:TRACE])
69
- end
70
-
71
- def debug?
72
- level_enabled?(Logging::LEVELS[:DEBUG])
73
- end
74
-
75
- def info?
76
- level_enabled?(Logging::LEVELS[:INFO])
77
- end
78
-
79
- def warn?
80
- level_enabled?(Logging::LEVELS[:WARN])
81
- end
82
-
83
- def error?
84
- level_enabled?(Logging::LEVELS[:ERROR])
85
- end
86
-
87
- def fatal?
88
- level_enabled?(Logging::LEVELS[:FATAL])
89
- end
90
-
91
- def security?
92
- level_enabled?(Logging::LEVELS[:SECURITY])
93
- end
94
-
95
- def measure(message, context: {}, level: :info, &block)
96
- start_time = Time.now
97
- result = nil
98
- error = nil
99
-
100
- begin
101
- result = block.call
102
- rescue StandardError => e
103
- error = e
104
- raise
105
- ensure
106
- duration = Time.now - start_time
107
- measure_context = context.merge(
108
- duration_ms: (duration * 1000).round(2),
109
- success: error.nil?
110
- )
111
- measure_context[:error] = error.class.name if error
112
-
113
- send(level, "#{message} completed", context: measure_context)
114
- end
115
-
116
- result
117
- end
118
-
119
- private
120
-
121
- def log_with_block(level, message, context, &block)
122
- return unless level_enabled?(level)
123
-
124
- message = block.call if block_given?
125
-
126
- full_context = @context.merge(context)
127
- @core.log(level, @name, message, full_context)
128
- end
129
- end
130
- end
131
- end
@@ -1,156 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "yaml"
4
-
5
- module VectorMCP
6
- module Logging
7
- class Configuration
8
- DEFAULT_CONFIG = {
9
- level: "INFO",
10
- format: "text",
11
- output: "console",
12
- components: {},
13
- console: {
14
- colorize: true,
15
- include_timestamp: true,
16
- include_thread: false
17
- },
18
- file: {
19
- path: nil,
20
- rotation: "daily",
21
- max_size: "100MB",
22
- max_files: 7
23
- }
24
- }.freeze
25
-
26
- attr_reader :config
27
-
28
- def initialize(config = {})
29
- @config = deep_merge(DEFAULT_CONFIG, normalize_config(config))
30
- validate_config!
31
- end
32
-
33
- def self.from_file(path)
34
- config = YAML.load_file(path)
35
- new(config["logging"] || config)
36
- rescue StandardError => e
37
- raise ConfigurationError, "Failed to load configuration from #{path}: #{e.message}"
38
- end
39
-
40
- def self.from_env
41
- config = {}
42
-
43
- config[:level] = ENV["VECTORMCP_LOG_LEVEL"] if ENV["VECTORMCP_LOG_LEVEL"]
44
- config[:format] = ENV["VECTORMCP_LOG_FORMAT"] if ENV["VECTORMCP_LOG_FORMAT"]
45
- config[:output] = ENV["VECTORMCP_LOG_OUTPUT"] if ENV["VECTORMCP_LOG_OUTPUT"]
46
-
47
- config[:file] = { path: ENV["VECTORMCP_LOG_FILE_PATH"] } if ENV["VECTORMCP_LOG_FILE_PATH"]
48
-
49
- new(config)
50
- end
51
-
52
- def level_for(component)
53
- component_level = @config[:components][component.to_s]
54
- level_value = component_level || @config[:level]
55
- Logging.level_value(level_value)
56
- end
57
-
58
- def set_component_level(component, level)
59
- @config[:components][component.to_s] = if level.is_a?(Integer)
60
- Logging.level_name(level)
61
- else
62
- level.to_s.upcase
63
- end
64
- end
65
-
66
- def component_config(component_name)
67
- @config[:components][component_name.to_s] || {}
68
- end
69
-
70
- def console_config
71
- @config[:console]
72
- end
73
-
74
- def file_config
75
- @config[:file]
76
- end
77
-
78
- def format
79
- @config[:format]
80
- end
81
-
82
- def output
83
- @config[:output]
84
- end
85
-
86
- def configure(&)
87
- instance_eval(&) if block_given?
88
- validate_config!
89
- end
90
-
91
- def level(new_level)
92
- @config[:level] = new_level.to_s.upcase
93
- end
94
-
95
- def component(name, level:)
96
- @config[:components][name.to_s] = level.to_s.upcase
97
- end
98
-
99
- def console(options = {})
100
- @config[:console].merge!(options)
101
- end
102
-
103
- def file(options = {})
104
- @config[:file].merge!(options)
105
- end
106
-
107
- def to_h
108
- @config.dup
109
- end
110
-
111
- private
112
-
113
- def normalize_config(config)
114
- case config
115
- when Hash
116
- config.transform_keys(&:to_sym)
117
- when String
118
- { level: config }
119
- else
120
- {}
121
- end
122
- end
123
-
124
- def deep_merge(hash1, hash2)
125
- result = hash1.dup
126
- hash2.each do |key, value|
127
- result[key] = if result[key].is_a?(Hash) && value.is_a?(Hash)
128
- deep_merge(result[key], value)
129
- else
130
- value
131
- end
132
- end
133
- result
134
- end
135
-
136
- def validate_config!
137
- validate_level!(@config[:level])
138
- @config[:components].each_value do |level|
139
- validate_level!(level)
140
- end
141
-
142
- raise ConfigurationError, "Invalid format: #{@config[:format]}" unless %w[text json].include?(@config[:format])
143
-
144
- return if %w[console file both].include?(@config[:output])
145
-
146
- raise ConfigurationError, "Invalid output: #{@config[:output]}"
147
- end
148
-
149
- def validate_level!(level)
150
- return if Logging::LEVELS.key?(level.to_s.upcase.to_sym)
151
-
152
- raise ConfigurationError, "Invalid log level: #{level}"
153
- end
154
- end
155
- end
156
- end