hooks-ruby 0.0.2 → 0.0.4

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.
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hooks
4
+ module Core
5
+ # Global failbot component for error reporting
6
+ #
7
+ # This is a stub implementation that does nothing by default.
8
+ # Users can replace this with their own implementation for services
9
+ # like Sentry, Rollbar, etc.
10
+ class Failbot
11
+ # Report an error or exception
12
+ #
13
+ # @param error_or_message [Exception, String] Exception object or error message
14
+ # @param context [Hash] Optional context information
15
+ # @return [void]
16
+ def report(error_or_message, context = {})
17
+ # Override in subclass for actual error reporting
18
+ end
19
+
20
+ # Report a critical error
21
+ #
22
+ # @param error_or_message [Exception, String] Exception object or error message
23
+ # @param context [Hash] Optional context information
24
+ # @return [void]
25
+ def critical(error_or_message, context = {})
26
+ # Override in subclass for actual error reporting
27
+ end
28
+
29
+ # Report a warning
30
+ #
31
+ # @param message [String] Warning message
32
+ # @param context [Hash] Optional context information
33
+ # @return [void]
34
+ def warning(message, context = {})
35
+ # Override in subclass for actual warning reporting
36
+ end
37
+
38
+ # Capture an exception during block execution
39
+ #
40
+ # @param context [Hash] Optional context information
41
+ # @return [Object] Return value of the block
42
+ def capture(context = {})
43
+ yield
44
+ rescue => e
45
+ report(e, context)
46
+ raise
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hooks
4
+ module Core
5
+ # Global registry for shared components accessible throughout the application
6
+ class GlobalComponents
7
+ @test_stats = nil
8
+ @test_failbot = nil
9
+
10
+ # Get the global stats instance
11
+ # @return [Hooks::Plugins::Instruments::StatsBase] Stats instance for metrics reporting
12
+ def self.stats
13
+ @test_stats || PluginLoader.get_instrument_plugin(:stats)
14
+ end
15
+
16
+ # Get the global failbot instance
17
+ # @return [Hooks::Plugins::Instruments::FailbotBase] Failbot instance for error reporting
18
+ def self.failbot
19
+ @test_failbot || PluginLoader.get_instrument_plugin(:failbot)
20
+ end
21
+
22
+ # Set a custom stats instance (for testing)
23
+ # @param stats_instance [Object] Custom stats instance
24
+ def self.stats=(stats_instance)
25
+ @test_stats = stats_instance
26
+ end
27
+
28
+ # Set a custom failbot instance (for testing)
29
+ # @param failbot_instance [Object] Custom failbot instance
30
+ def self.failbot=(failbot_instance)
31
+ @test_failbot = failbot_instance
32
+ end
33
+
34
+ # Reset components to default instances (for testing)
35
+ #
36
+ # @return [void]
37
+ def self.reset
38
+ @test_stats = nil
39
+ @test_failbot = nil
40
+ # Clear and reload default instruments
41
+ PluginLoader.clear_plugins
42
+ require_relative "../plugins/instruments/stats"
43
+ require_relative "../plugins/instruments/failbot"
44
+ PluginLoader.instance_variable_set(:@instrument_plugins, {
45
+ stats: Hooks::Plugins::Instruments::Stats.new,
46
+ failbot: Hooks::Plugins::Instruments::Failbot.new
47
+ })
48
+ end
49
+ end
50
+ end
51
+ end
@@ -1,8 +1,23 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Hooks
4
+ # Global logger accessor module
5
+ #
6
+ # Provides a singleton-like access pattern for the application logger.
7
+ # The logger instance is set during application initialization and can
8
+ # be accessed throughout the application lifecycle.
9
+ #
10
+ # @example Setting the logger instance
11
+ # Hooks::Log.instance = Logger.new(STDOUT)
12
+ #
13
+ # @example Accessing the logger
14
+ # Hooks::Log.instance.info("Application started")
4
15
  module Log
5
16
  class << self
17
+ # Get or set the global logger instance
18
+ # @return [Logger] The global logger instance
19
+ # @attr_reader instance [Logger] Current logger instance
20
+ # @attr_writer instance [Logger] Set the logger instance
6
21
  attr_accessor :instance
7
22
  end
8
23
  end
@@ -5,14 +5,16 @@ require_relative "../security"
5
5
 
6
6
  module Hooks
7
7
  module Core
8
- # Loads and caches all plugins (auth + handlers) at boot time
8
+ # Loads and caches all plugins (auth + handlers + lifecycle + instruments) at boot time
9
9
  class PluginLoader
10
10
  # Class-level registries for loaded plugins
11
11
  @auth_plugins = {}
12
12
  @handler_plugins = {}
13
+ @lifecycle_plugins = []
14
+ @instrument_plugins = { stats: nil, failbot: nil }
13
15
 
14
16
  class << self
15
- attr_reader :auth_plugins, :handler_plugins
17
+ attr_reader :auth_plugins, :handler_plugins, :lifecycle_plugins, :instrument_plugins
16
18
 
17
19
  # Load all plugins at boot time
18
20
  #
@@ -22,6 +24,8 @@ module Hooks
22
24
  # Clear existing registries
23
25
  @auth_plugins = {}
24
26
  @handler_plugins = {}
27
+ @lifecycle_plugins = []
28
+ @instrument_plugins = { stats: nil, failbot: nil }
25
29
 
26
30
  # Load built-in plugins first
27
31
  load_builtin_plugins
@@ -29,6 +33,11 @@ module Hooks
29
33
  # Load custom plugins if directories are configured
30
34
  load_custom_auth_plugins(config[:auth_plugin_dir]) if config[:auth_plugin_dir]
31
35
  load_custom_handler_plugins(config[:handler_plugin_dir]) if config[:handler_plugin_dir]
36
+ load_custom_lifecycle_plugins(config[:lifecycle_plugin_dir]) if config[:lifecycle_plugin_dir]
37
+ load_custom_instrument_plugins(config[:instruments_plugin_dir]) if config[:instruments_plugin_dir]
38
+
39
+ # Load default instruments if no custom ones were loaded
40
+ load_default_instruments
32
41
 
33
42
  # Log loaded plugins
34
43
  log_loaded_plugins
@@ -65,12 +74,29 @@ module Hooks
65
74
  plugin_class
66
75
  end
67
76
 
77
+ # Get instrument plugin instance by type
78
+ #
79
+ # @param instrument_type [Symbol] Type of instrument (:stats or :failbot)
80
+ # @return [Object] The instrument plugin instance
81
+ # @raise [StandardError] if instrument not found
82
+ def get_instrument_plugin(instrument_type)
83
+ instrument_instance = @instrument_plugins[instrument_type]
84
+
85
+ unless instrument_instance
86
+ raise StandardError, "Instrument plugin '#{instrument_type}' not found"
87
+ end
88
+
89
+ instrument_instance
90
+ end
91
+
68
92
  # Clear all loaded plugins (for testing purposes)
69
93
  #
70
94
  # @return [void]
71
95
  def clear_plugins
72
96
  @auth_plugins = {}
73
97
  @handler_plugins = {}
98
+ @lifecycle_plugins = []
99
+ @instrument_plugins = { stats: nil, failbot: nil }
74
100
  end
75
101
 
76
102
  private
@@ -97,7 +123,7 @@ module Hooks
97
123
  Dir.glob(File.join(auth_plugin_dir, "*.rb")).sort.each do |file_path|
98
124
  begin
99
125
  load_custom_auth_plugin(file_path, auth_plugin_dir)
100
- rescue => e
126
+ rescue StandardError, SyntaxError => e
101
127
  raise StandardError, "Failed to load auth plugin from #{file_path}: #{e.message}"
102
128
  end
103
129
  end
@@ -113,12 +139,44 @@ module Hooks
113
139
  Dir.glob(File.join(handler_plugin_dir, "*.rb")).sort.each do |file_path|
114
140
  begin
115
141
  load_custom_handler_plugin(file_path, handler_plugin_dir)
116
- rescue => e
142
+ rescue StandardError, SyntaxError => e
117
143
  raise StandardError, "Failed to load handler plugin from #{file_path}: #{e.message}"
118
144
  end
119
145
  end
120
146
  end
121
147
 
148
+ # Load custom lifecycle plugins from directory
149
+ #
150
+ # @param lifecycle_plugin_dir [String] Directory containing custom lifecycle plugins
151
+ # @return [void]
152
+ def load_custom_lifecycle_plugins(lifecycle_plugin_dir)
153
+ return unless lifecycle_plugin_dir && Dir.exist?(lifecycle_plugin_dir)
154
+
155
+ Dir.glob(File.join(lifecycle_plugin_dir, "*.rb")).sort.each do |file_path|
156
+ begin
157
+ load_custom_lifecycle_plugin(file_path, lifecycle_plugin_dir)
158
+ rescue StandardError, SyntaxError => e
159
+ raise StandardError, "Failed to load lifecycle plugin from #{file_path}: #{e.message}"
160
+ end
161
+ end
162
+ end
163
+
164
+ # Load custom instrument plugins from directory
165
+ #
166
+ # @param instruments_plugin_dir [String] Directory containing custom instrument plugins
167
+ # @return [void]
168
+ def load_custom_instrument_plugins(instruments_plugin_dir)
169
+ return unless instruments_plugin_dir && Dir.exist?(instruments_plugin_dir)
170
+
171
+ Dir.glob(File.join(instruments_plugin_dir, "*.rb")).sort.each do |file_path|
172
+ begin
173
+ load_custom_instrument_plugin(file_path, instruments_plugin_dir)
174
+ rescue StandardError, SyntaxError => e
175
+ raise StandardError, "Failed to load instrument plugin from #{file_path}: #{e.message}"
176
+ end
177
+ end
178
+ end
179
+
122
180
  # Load a single custom auth plugin file
123
181
  #
124
182
  # @param file_path [String] Path to the auth plugin file
@@ -189,6 +247,90 @@ module Hooks
189
247
  @handler_plugins[class_name] = handler_class
190
248
  end
191
249
 
250
+ # Load a single custom lifecycle plugin file
251
+ #
252
+ # @param file_path [String] Path to the lifecycle plugin file
253
+ # @param lifecycle_plugin_dir [String] Base directory for lifecycle plugins
254
+ # @return [void]
255
+ def load_custom_lifecycle_plugin(file_path, lifecycle_plugin_dir)
256
+ # Security: Ensure the file path doesn't escape the lifecycle plugin directory
257
+ normalized_lifecycle_dir = Pathname.new(File.expand_path(lifecycle_plugin_dir))
258
+ normalized_file_path = Pathname.new(File.expand_path(file_path))
259
+ unless normalized_file_path.descend.any? { |path| path == normalized_lifecycle_dir }
260
+ raise SecurityError, "Lifecycle plugin path outside of lifecycle plugin directory: #{file_path}"
261
+ end
262
+
263
+ # Extract class name from file (e.g., logging_lifecycle.rb -> LoggingLifecycle)
264
+ file_name = File.basename(file_path, ".rb")
265
+ class_name = file_name.split("_").map(&:capitalize).join("")
266
+
267
+ # Security: Validate class name
268
+ unless valid_lifecycle_class_name?(class_name)
269
+ raise StandardError, "Invalid lifecycle plugin class name: #{class_name}"
270
+ end
271
+
272
+ # Load the file
273
+ require file_path
274
+
275
+ # Get the class and validate it
276
+ lifecycle_class = Object.const_get(class_name)
277
+ unless lifecycle_class < Hooks::Plugins::Lifecycle
278
+ raise StandardError, "Lifecycle plugin class must inherit from Hooks::Plugins::Lifecycle: #{class_name}"
279
+ end
280
+
281
+ # Register the plugin instance
282
+ @lifecycle_plugins << lifecycle_class.new
283
+ end
284
+
285
+ # Load a single custom instrument plugin file
286
+ #
287
+ # @param file_path [String] Path to the instrument plugin file
288
+ # @param instruments_plugin_dir [String] Base directory for instrument plugins
289
+ # @return [void]
290
+ def load_custom_instrument_plugin(file_path, instruments_plugin_dir)
291
+ # Security: Ensure the file path doesn't escape the instruments plugin directory
292
+ normalized_instruments_dir = Pathname.new(File.expand_path(instruments_plugin_dir))
293
+ normalized_file_path = Pathname.new(File.expand_path(file_path))
294
+ unless normalized_file_path.descend.any? { |path| path == normalized_instruments_dir }
295
+ raise SecurityError, "Instrument plugin path outside of instruments plugin directory: #{file_path}"
296
+ end
297
+
298
+ # Extract class name from file (e.g., custom_stats.rb -> CustomStats)
299
+ file_name = File.basename(file_path, ".rb")
300
+ class_name = file_name.split("_").map(&:capitalize).join("")
301
+
302
+ # Security: Validate class name
303
+ unless valid_instrument_class_name?(class_name)
304
+ raise StandardError, "Invalid instrument plugin class name: #{class_name}"
305
+ end
306
+
307
+ # Load the file
308
+ require file_path
309
+
310
+ # Get the class and validate it
311
+ instrument_class = Object.const_get(class_name)
312
+
313
+ # Determine instrument type based on inheritance
314
+ if instrument_class < Hooks::Plugins::Instruments::StatsBase
315
+ @instrument_plugins[:stats] = instrument_class.new
316
+ elsif instrument_class < Hooks::Plugins::Instruments::FailbotBase
317
+ @instrument_plugins[:failbot] = instrument_class.new
318
+ else
319
+ raise StandardError, "Instrument plugin class must inherit from StatsBase or FailbotBase: #{class_name}"
320
+ end
321
+ end
322
+
323
+ # Load default instrument implementations if no custom ones were loaded
324
+ #
325
+ # @return [void]
326
+ def load_default_instruments
327
+ require_relative "../plugins/instruments/stats"
328
+ require_relative "../plugins/instruments/failbot"
329
+
330
+ @instrument_plugins[:stats] ||= Hooks::Plugins::Instruments::Stats.new
331
+ @instrument_plugins[:failbot] ||= Hooks::Plugins::Instruments::Failbot.new
332
+ end
333
+
192
334
  # Log summary of loaded plugins
193
335
  #
194
336
  # @return [void]
@@ -201,6 +343,8 @@ module Hooks
201
343
 
202
344
  log.info "Loaded #{@auth_plugins.size} auth plugins: #{@auth_plugins.keys.join(', ')}"
203
345
  log.info "Loaded #{@handler_plugins.size} handler plugins: #{@handler_plugins.keys.join(', ')}"
346
+ log.info "Loaded #{@lifecycle_plugins.size} lifecycle plugins"
347
+ log.info "Loaded instruments: #{@instrument_plugins.keys.select { |k| @instrument_plugins[k] }.join(', ')}"
204
348
  end
205
349
 
206
350
  # Validate that an auth plugin class name is safe to load
@@ -244,6 +388,48 @@ module Hooks
244
388
 
245
389
  true
246
390
  end
391
+
392
+ # Validate that a lifecycle plugin class name is safe to load
393
+ #
394
+ # @param class_name [String] The class name to validate
395
+ # @return [Boolean] true if the class name is safe, false otherwise
396
+ def valid_lifecycle_class_name?(class_name)
397
+ # Must be a string
398
+ return false unless class_name.is_a?(String)
399
+
400
+ # Must not be empty or only whitespace
401
+ return false if class_name.strip.empty?
402
+
403
+ # Must match a safe pattern: alphanumeric + underscore, starting with uppercase
404
+ # Examples: LoggingLifecycle, MetricsLifecycle, CustomLifecycle
405
+ return false unless class_name.match?(/\A[A-Z][a-zA-Z0-9_]*\z/)
406
+
407
+ # Must not be a system/built-in class name
408
+ return false if Hooks::Security::DANGEROUS_CLASSES.include?(class_name)
409
+
410
+ true
411
+ end
412
+
413
+ # Validate that an instrument plugin class name is safe to load
414
+ #
415
+ # @param class_name [String] The class name to validate
416
+ # @return [Boolean] true if the class name is safe, false otherwise
417
+ def valid_instrument_class_name?(class_name)
418
+ # Must be a string
419
+ return false unless class_name.is_a?(String)
420
+
421
+ # Must not be empty or only whitespace
422
+ return false if class_name.strip.empty?
423
+
424
+ # Must match a safe pattern: alphanumeric + underscore, starting with uppercase
425
+ # Examples: CustomStats, CustomFailbot, DatadogStats
426
+ return false unless class_name.match?(/\A[A-Z][a-zA-Z0-9_]*\z/)
427
+
428
+ # Must not be a system/built-in class name
429
+ return false if Hooks::Security::DANGEROUS_CLASSES.include?(class_name)
430
+
431
+ true
432
+ end
247
433
  end
248
434
  end
249
435
  end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hooks
4
+ module Core
5
+ # Global stats component for metrics reporting
6
+ #
7
+ # This is a stub implementation that does nothing by default.
8
+ # Users can replace this with their own implementation for services
9
+ # like DataDog, New Relic, etc.
10
+ class Stats
11
+ # Record a metric
12
+ #
13
+ # @param metric_name [String] Name of the metric
14
+ # @param value [Numeric] Value to record
15
+ # @param tags [Hash] Optional tags/labels for the metric
16
+ # @return [void]
17
+ def record(metric_name, value, tags = {})
18
+ # Override in subclass for actual metrics reporting
19
+ end
20
+
21
+ # Increment a counter
22
+ #
23
+ # @param metric_name [String] Name of the counter
24
+ # @param tags [Hash] Optional tags/labels for the metric
25
+ # @return [void]
26
+ def increment(metric_name, tags = {})
27
+ # Override in subclass for actual metrics reporting
28
+ end
29
+
30
+ # Record a timing metric
31
+ #
32
+ # @param metric_name [String] Name of the timing metric
33
+ # @param duration [Numeric] Duration in seconds
34
+ # @param tags [Hash] Optional tags/labels for the metric
35
+ # @return [void]
36
+ def timing(metric_name, duration, tags = {})
37
+ # Override in subclass for actual metrics reporting
38
+ end
39
+
40
+ # Measure execution time of a block
41
+ #
42
+ # @param metric_name [String] Name of the timing metric
43
+ # @param tags [Hash] Optional tags/labels for the metric
44
+ # @return [Object] Return value of the block
45
+ def measure(metric_name, tags = {})
46
+ start_time = Time.now
47
+ result = yield
48
+ duration = Time.now - start_time
49
+ timing(metric_name, duration, tags)
50
+ result
51
+ end
52
+ end
53
+ end
54
+ end
@@ -2,6 +2,8 @@
2
2
 
3
3
  require "rack/utils"
4
4
  require_relative "../../core/log"
5
+ require_relative "../../core/global_components"
6
+ require_relative "../../core/component_access"
5
7
 
6
8
  module Hooks
7
9
  module Plugins
@@ -10,6 +12,8 @@ module Hooks
10
12
  #
11
13
  # All custom Auth plugins must inherit from this class
12
14
  class Base
15
+ extend Hooks::Core::ComponentAccess
16
+
13
17
  # Validate request
14
18
  #
15
19
  # @param payload [String] Raw request body
@@ -21,18 +25,6 @@ module Hooks
21
25
  raise NotImplementedError, "Validator must implement .valid? class method"
22
26
  end
23
27
 
24
- # Short logger accessor for all subclasses
25
- # @return [Hooks::Log] Logger instance for request validation
26
- #
27
- # Provides a convenient way for validators to log messages without needing
28
- # to reference the full Hooks::Log namespace.
29
- #
30
- # @example Logging an error in an inherited class
31
- # log.error("oh no an error occured")
32
- def self.log
33
- Hooks::Log.instance
34
- end
35
-
36
28
  # Retrieve the secret from the environment variable based on the key set in the configuration
37
29
  #
38
30
  # Note: This method is intended to be used by subclasses
@@ -3,6 +3,7 @@
3
3
  require "openssl"
4
4
  require "time"
5
5
  require_relative "base"
6
+ require_relative "timestamp_validator"
6
7
 
7
8
  module Hooks
8
9
  module Plugins
@@ -39,6 +40,7 @@ module Hooks
39
40
  DEFAULT_CONFIG = {
40
41
  algorithm: "sha256",
41
42
  format: "algorithm=signature", # Format: algorithm=hash
43
+ header: "X-Signature", # Default header containing the signature
42
44
  timestamp_tolerance: 300, # 5 minutes tolerance for timestamp validation
43
45
  version_prefix: "v0" # Default version prefix for versioned signatures
44
46
  }.freeze
@@ -117,7 +119,10 @@ module Hooks
117
119
 
118
120
  # Validate timestamp if required (for services that include timestamp validation)
119
121
  if validator_config[:timestamp_header]
120
- return false unless valid_timestamp?(normalized_headers, validator_config)
122
+ unless valid_timestamp?(normalized_headers, validator_config)
123
+ log.warn("Auth::HMAC validation failed: Invalid timestamp")
124
+ return false
125
+ end
121
126
  end
122
127
 
123
128
  # Compute expected signature
@@ -153,7 +158,7 @@ module Hooks
153
158
  tolerance = validator_config[:timestamp_tolerance] || DEFAULT_CONFIG[:timestamp_tolerance]
154
159
 
155
160
  DEFAULT_CONFIG.merge({
156
- header: validator_config[:header] || "X-Signature",
161
+ header: validator_config[:header] || DEFAULT_CONFIG[:header],
157
162
  timestamp_header: validator_config[:timestamp_header],
158
163
  timestamp_tolerance: tolerance,
159
164
  algorithm: algorithm,
@@ -180,6 +185,7 @@ module Hooks
180
185
  #
181
186
  # Checks if the provided timestamp is within the configured tolerance
182
187
  # of the current time. This prevents replay attacks using old requests.
188
+ # Supports both ISO 8601 UTC timestamps and Unix timestamps.
183
189
  #
184
190
  # @param headers [Hash<String, String>] Normalized HTTP headers
185
191
  # @param config [Hash<Symbol, Object>] Validator configuration
@@ -189,25 +195,21 @@ module Hooks
189
195
  # @api private
190
196
  def self.valid_timestamp?(headers, config)
191
197
  timestamp_header = config[:timestamp_header]
198
+ tolerance = config[:timestamp_tolerance] || 300
192
199
  return false unless timestamp_header
193
200
 
194
- timestamp_header = timestamp_header.downcase
195
- timestamp_value = headers[timestamp_header]
196
-
201
+ timestamp_value = headers[timestamp_header.downcase]
197
202
  return false unless timestamp_value
198
203
 
199
- # Security: Strict timestamp validation - must be only digits with no leading zeros
200
- return false unless timestamp_value.match?(/\A[1-9]\d*\z/) || timestamp_value == "0"
201
-
202
- timestamp = timestamp_value.to_i
203
-
204
- # Ensure timestamp is a positive integer (reject zero and negative)
205
- return false unless timestamp > 0
206
-
207
- current_time = Time.now.to_i
208
- tolerance = config[:timestamp_tolerance]
204
+ timestamp_validator.valid?(timestamp_value, tolerance)
205
+ end
209
206
 
210
- (current_time - timestamp).abs <= tolerance
207
+ # Get timestamp validator instance
208
+ #
209
+ # @return [TimestampValidator] Singleton timestamp validator instance
210
+ # @api private
211
+ def self.timestamp_validator
212
+ @timestamp_validator ||= TimestampValidator.new
211
213
  end
212
214
 
213
215
  # Compute HMAC signature based on configuration requirements
@@ -257,7 +259,7 @@ module Hooks
257
259
  # - {body}: Replaced with the raw payload
258
260
  # @example Template usage
259
261
  # template: "{version}:{timestamp}:{body}"
260
- # result: "v0:1609459200:{"event":"push"}"
262
+ # result: "v0:1609459200:{\"event\":\"push\"}"
261
263
  # @api private
262
264
  def self.build_signing_payload(payload:, headers:, config:)
263
265
  template = config[:payload_template]
@@ -287,7 +289,7 @@ module Hooks
287
289
  # - :algorithm_prefixed: "sha256=abc123..." (GitHub style)
288
290
  # - :hash_only: "abc123..." (Shopify style)
289
291
  # - :version_prefixed: "v0=abc123..." (Slack style)
290
- # @note Defaults to algorithm_prefixed format for unknown format styles
292
+ # @note Defaults to algorithm-prefixed format for unknown format styles
291
293
  # @api private
292
294
  def self.format_signature(hash, config)
293
295
  format_style = FORMATS[config[:format]]