runger_config 3.0.1 → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +44 -35
  3. data/README.md +83 -83
  4. data/lib/generators/runger/app_config/app_config_generator.rb +13 -0
  5. data/lib/generators/runger/config/config_generator.rb +49 -0
  6. data/lib/generators/runger/install/install_generator.rb +45 -0
  7. data/lib/generators/{anyway → runger}/install/templates/application_config.rb.tt +1 -1
  8. data/lib/{anyway → runger}/auto_cast.rb +4 -4
  9. data/lib/{anyway → runger}/config.rb +124 -104
  10. data/lib/runger/dynamic_config.rb +29 -0
  11. data/lib/runger/ejson_parser.rb +40 -0
  12. data/lib/runger/env.rb +70 -0
  13. data/lib/runger/ext/deep_dup.rb +45 -0
  14. data/lib/runger/ext/deep_freeze.rb +40 -0
  15. data/lib/runger/ext/flatten_names.rb +33 -0
  16. data/lib/runger/ext/hash.rb +37 -0
  17. data/lib/runger/ext/string_constantize.rb +21 -0
  18. data/lib/runger/loaders/base.rb +17 -0
  19. data/lib/runger/loaders/doppler.rb +57 -0
  20. data/lib/runger/loaders/ejson.rb +91 -0
  21. data/lib/runger/loaders/env.rb +12 -0
  22. data/lib/runger/loaders/yaml.rb +86 -0
  23. data/lib/runger/loaders.rb +75 -0
  24. data/lib/runger/option_parser_builder.rb +27 -0
  25. data/lib/{anyway → runger}/optparse_config.rb +15 -14
  26. data/lib/runger/rails/autoload.rb +38 -0
  27. data/lib/runger/rails/config.rb +19 -0
  28. data/lib/runger/rails/loaders/credentials.rb +58 -0
  29. data/lib/runger/rails/loaders/secrets.rb +31 -0
  30. data/lib/runger/rails/loaders/yaml.rb +4 -0
  31. data/lib/runger/rails/loaders.rb +5 -0
  32. data/lib/runger/rails/settings.rb +83 -0
  33. data/lib/runger/rails.rb +22 -0
  34. data/lib/{anyway → runger}/railtie.rb +9 -8
  35. data/lib/{anyway → runger}/rbs.rb +30 -30
  36. data/lib/runger/settings.rb +109 -0
  37. data/lib/runger/testing/helpers.rb +34 -0
  38. data/lib/{anyway → runger}/testing.rb +3 -3
  39. data/lib/runger/tracing.rb +195 -0
  40. data/lib/{anyway → runger}/type_casting.rb +19 -14
  41. data/lib/{anyway → runger}/utils/deep_merge.rb +2 -2
  42. data/lib/runger/utils/which.rb +16 -0
  43. data/lib/runger/version.rb +5 -0
  44. data/lib/{anyway.rb → runger.rb} +1 -1
  45. data/lib/runger_config.rb +56 -0
  46. data/sig/{anyway_config.rbs → runger_config.rbs} +1 -1
  47. metadata +68 -68
  48. data/lib/anyway/dynamic_config.rb +0 -31
  49. data/lib/anyway/ejson_parser.rb +0 -40
  50. data/lib/anyway/env.rb +0 -72
  51. data/lib/anyway/ext/deep_dup.rb +0 -48
  52. data/lib/anyway/ext/deep_freeze.rb +0 -44
  53. data/lib/anyway/ext/flatten_names.rb +0 -37
  54. data/lib/anyway/ext/hash.rb +0 -40
  55. data/lib/anyway/ext/string_constantize.rb +0 -24
  56. data/lib/anyway/loaders/base.rb +0 -21
  57. data/lib/anyway/loaders/doppler.rb +0 -61
  58. data/lib/anyway/loaders/ejson.rb +0 -89
  59. data/lib/anyway/loaders/env.rb +0 -16
  60. data/lib/anyway/loaders/yaml.rb +0 -83
  61. data/lib/anyway/loaders.rb +0 -77
  62. data/lib/anyway/option_parser_builder.rb +0 -29
  63. data/lib/anyway/rails/autoload.rb +0 -40
  64. data/lib/anyway/rails/config.rb +0 -23
  65. data/lib/anyway/rails/loaders/credentials.rb +0 -62
  66. data/lib/anyway/rails/loaders/secrets.rb +0 -35
  67. data/lib/anyway/rails/loaders/yaml.rb +0 -9
  68. data/lib/anyway/rails/loaders.rb +0 -5
  69. data/lib/anyway/rails/settings.rb +0 -83
  70. data/lib/anyway/rails.rb +0 -24
  71. data/lib/anyway/settings.rb +0 -111
  72. data/lib/anyway/testing/helpers.rb +0 -36
  73. data/lib/anyway/tracing.rb +0 -188
  74. data/lib/anyway/utils/which.rb +0 -18
  75. data/lib/anyway/version.rb +0 -5
  76. data/lib/anyway_config.rb +0 -49
  77. data/lib/generators/anyway/app_config/app_config_generator.rb +0 -17
  78. data/lib/generators/anyway/config/config_generator.rb +0 -46
  79. data/lib/generators/anyway/install/install_generator.rb +0 -47
  80. /data/lib/generators/{anyway → runger}/app_config/USAGE +0 -0
  81. /data/lib/generators/{anyway → runger}/config/USAGE +0 -0
  82. /data/lib/generators/{anyway → runger}/config/templates/config.rb.tt +0 -0
  83. /data/lib/generators/{anyway → runger}/config/templates/config.yml.tt +0 -0
  84. /data/lib/generators/{anyway → runger}/install/USAGE +0 -0
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Runger::Rails::Loaders::Secrets < Runger::Loaders::Base
4
+ def call(name:, **_options)
5
+ return {} unless ::Rails.application.respond_to?(:secrets)
6
+
7
+ # Create a new hash cause secrets are mutable!
8
+ config = {}
9
+
10
+ trace!(:secrets) do
11
+ secrets.public_send(name)
12
+ end.then do |secrets|
13
+ ::Runger::Utils.deep_merge!(config, secrets) if secrets
14
+ end
15
+
16
+ config
17
+ end
18
+
19
+ private
20
+
21
+ def secrets
22
+ @secrets ||=
23
+ ::Rails.application.secrets.tap do |_|
24
+ # Reset secrets state if the app hasn't been initialized
25
+ # See https://github.com/palkan/runger_config/issues/14
26
+ next if ::Rails.application.initialized?
27
+
28
+ ::Rails.application.remove_instance_variable(:@secrets)
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Runger::Rails::Loaders::YAML < Runger::Loaders::YAML
4
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'runger/rails/loaders/credentials'
4
+ require 'runger/rails/loaders/secrets'
5
+ require 'runger/rails/loaders/yaml'
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Try to require zeitwerk
4
+ begin
5
+ require 'active_support/dependencies/zeitwerk_integration'
6
+ require 'zeitwerk'
7
+ rescue LoadError
8
+ end
9
+
10
+ class Runger::Settings
11
+ class << self
12
+ attr_reader :autoload_static_config_path, :autoloader
13
+ attr_writer :autoload_via_zeitwerk
14
+
15
+ def autoload_static_config_path=(val)
16
+ if autoload_via_zeitwerk
17
+ raise('Cannot setup autoloader after application has been initialized') if ::Rails.application.initialized?
18
+
19
+ return unless ::Rails.root.join(val).exist?
20
+
21
+ return if val == autoload_static_config_path
22
+
23
+ autoloader&.unload
24
+
25
+ @autoload_static_config_path = val
26
+
27
+ # See Rails 6 https://github.com/rails/rails/blob/8ab4fd12f18203b83d0f252db96d10731485ff6a/railties/lib/rails/autoloaders.rb#L10
28
+ # and Rails 7 https://github.com/rails/rails/blob/5462fbd5de1900c1b1ce1c9dc11c1a2d8cdcd809/railties/lib/rails/autoloaders.rb#L15
29
+ @autoloader =
30
+ Zeitwerk::Loader.new.tap do |loader|
31
+ loader.tag = 'runger.config'
32
+
33
+ if defined?(ActiveSupport::Dependencies::ZeitwerkIntegration::Inflector)
34
+ loader.inflector = ActiveSupport::Dependencies::ZeitwerkIntegration::Inflector
35
+ elsif defined?(::Rails::Autoloaders::Inflector)
36
+ loader.inflector = ::Rails::Autoloaders::Inflector
37
+ end
38
+ loader.push_dir(::Rails.root.join(val))
39
+ loader.setup
40
+ end
41
+ else
42
+ if autoload_static_config_path
43
+ old_path = ::Rails.root.join(autoload_static_config_path).to_s
44
+ ActiveSupport::Dependencies.autoload_paths.delete(old_path)
45
+ ::Rails.application.config.eager_load_paths.delete(old_path)
46
+ end
47
+
48
+ @autoload_static_config_path = val
49
+ new_path = ::Rails.root.join(val).to_s
50
+ ActiveSupport::Dependencies.autoload_paths << new_path
51
+ ::Rails.application.config.eager_load_paths << new_path
52
+ end
53
+ end
54
+
55
+ def cleanup_autoload_paths
56
+ return unless autoload_via_zeitwerk
57
+
58
+ return unless autoload_static_config_path
59
+
60
+ ActiveSupport::Dependencies.autoload_paths.delete(::Rails.root.join(autoload_static_config_path).to_s)
61
+ end
62
+
63
+ def autoload_via_zeitwerk
64
+ return @autoload_via_zeitwerk if instance_variable_defined?(:@autoload_via_zeitwerk)
65
+
66
+ @autoload_via_zeitwerk = defined?(::Zeitwerk)
67
+ end
68
+
69
+ def current_environment
70
+ @current_environment || ::Rails.env.to_s
71
+ end
72
+
73
+ def app_root
74
+ ::Rails.root
75
+ end
76
+ end
77
+
78
+ self.default_config_path = ->(name) { ::Rails.root.join('config', "#{name}.yml") }
79
+ self.known_environments = %w[test development production]
80
+ self.use_local_files ||= ::Rails.env.development?
81
+ # Don't try read defaults when no key defined
82
+ self.default_environmental_key = nil
83
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Runger::Rails
4
+ end
5
+
6
+ require 'runger/rails/config'
7
+ require 'runger/rails/loaders'
8
+ require 'runger/rails/settings'
9
+
10
+ # Configure Rails loaders
11
+ Runger.loaders.override(:yml, Runger::Rails::Loaders::YAML)
12
+
13
+ if Rails::VERSION::MAJOR >= 7 && Rails::VERSION::MINOR >= 1
14
+ Runger.loaders.insert_after(:yml, :credentials, Runger::Rails::Loaders::Credentials)
15
+ else
16
+ Runger.loaders.insert_after(:yml, :secrets, Runger::Rails::Loaders::Secrets)
17
+ Runger.loaders.insert_after(:secrets, :credentials, Runger::Rails::Loaders::Credentials)
18
+ end
19
+
20
+ # Load Railties after configuring loaders.
21
+ # The application could be already initialized, and that would make `Runger.loaders` frozen
22
+ require 'runger/railtie'
@@ -1,28 +1,29 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Anyway # :nodoc:
4
- DEFAULT_CONFIGS_PATH = "config/configs"
3
+ module Runger # :nodoc:
4
+ DEFAULT_CONFIGS_PATH = 'config/configs'
5
5
 
6
6
  class Railtie < ::Rails::Railtie # :nodoc:
7
7
  # Add settings to Rails config
8
- config.anyway_config = Anyway::Settings
8
+ config.runger_config = Runger::Settings
9
9
 
10
10
  config.before_configuration do
11
11
  next if ::Rails.application.initialized?
12
- config.anyway_config.autoload_static_config_path = DEFAULT_CONFIGS_PATH
12
+
13
+ config.runger_config.autoload_static_config_path = DEFAULT_CONFIGS_PATH
13
14
  end
14
15
 
15
16
  config.before_eager_load do
16
- Anyway::Settings.autoloader&.eager_load
17
+ Runger::Settings.autoloader&.eager_load
17
18
  end
18
19
 
19
20
  # Remove `autoload_static_config_path` from Rails `autoload_paths`
20
21
  # since we use our own autoloading mechanism
21
- initializer "anyway_config.cleanup_autoload" do
22
- Anyway::Settings.cleanup_autoload_paths
22
+ initializer 'runger_config.cleanup_autoload' do
23
+ Runger::Settings.cleanup_autoload_paths
23
24
  end
24
25
 
25
26
  # Make sure loaders are not changed in runtime
26
- config.after_initialize { Anyway.loaders.freeze }
27
+ config.after_initialize { Runger.loaders.freeze }
27
28
  end
28
29
  end
@@ -1,35 +1,35 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Anyway
3
+ module Runger
4
4
  module RBSGenerator
5
5
  TYPE_TO_CLASS = {
6
- string: "String",
7
- integer: "Integer",
8
- float: "Float",
9
- date: "Date",
10
- datetime: "DateTime",
11
- uri: "URI",
12
- boolean: "bool"
6
+ string: 'String',
7
+ integer: 'Integer',
8
+ float: 'Float',
9
+ date: 'Date',
10
+ datetime: 'DateTime',
11
+ uri: 'URI',
12
+ boolean: 'bool',
13
13
  }.freeze
14
14
 
15
15
  # Generate RBS signature from a config class
16
16
  def to_rbs
17
- *namespace, class_name = name.split("::")
17
+ *namespace, class_name = name.split('::')
18
18
 
19
19
  buf = []
20
20
  indent = 0
21
- interface_name = "_Config"
21
+ interface_name = '_Config'
22
22
 
23
23
  if namespace.empty?
24
24
  interface_name = "_#{class_name}"
25
25
  else
26
- buf << "module #{namespace.join("::")}"
26
+ buf << "module #{namespace.join('::')}"
27
27
  indent += 1
28
28
  end
29
29
 
30
30
  # Using interface emulates a module we include to provide getters and setters
31
31
  # (thus making `super` possible)
32
- buf << "#{" " * indent}interface #{interface_name}"
32
+ buf << "#{' ' * indent}interface #{interface_name}"
33
33
  indent += 1
34
34
 
35
35
  # Generating setters and getters for config attributes
@@ -39,17 +39,17 @@ module Anyway
39
39
  type =
40
40
  case type
41
41
  in NilClass
42
- "untyped"
42
+ 'untyped'
43
43
  in Symbol
44
- TYPE_TO_CLASS.fetch(type) { defaults[param] ? "Symbol" : "untyped" }
44
+ TYPE_TO_CLASS.fetch(type) { defaults[param] ? 'Symbol' : 'untyped' }
45
45
  in Array
46
- "Array[untyped]"
46
+ 'Array[untyped]'
47
47
  in array: _, type:, **nil
48
- "Array[#{TYPE_TO_CLASS.fetch(type, "untyped")}]"
48
+ "Array[#{TYPE_TO_CLASS.fetch(type, 'untyped')}]"
49
49
  in Hash
50
- "Hash[string,untyped]"
50
+ 'Hash[string,untyped]'
51
51
  in TrueClass | FalseClass
52
- "bool"
52
+ 'bool'
53
53
  else
54
54
  type.class.to_s
55
55
  end
@@ -57,36 +57,36 @@ module Anyway
57
57
  getter_type = type
58
58
  getter_type = "#{type}?" unless required_attributes.include?(param)
59
59
 
60
- buf << "#{" " * indent}def #{param}: () -> #{getter_type}"
61
- buf << "#{" " * indent}def #{param}=: (#{type}) -> void"
60
+ buf << "#{' ' * indent}def #{param}: () -> #{getter_type}"
61
+ buf << "#{' ' * indent}def #{param}=: (#{type}) -> void"
62
62
 
63
- if type == "bool" || type == "bool?"
64
- buf << "#{" " * indent}def #{param}?: () -> #{getter_type}"
63
+ if ['bool', 'bool?'].include?(type)
64
+ buf << "#{' ' * indent}def #{param}?: () -> #{getter_type}"
65
65
  end
66
66
  end
67
67
 
68
68
  indent -= 1
69
- buf << "#{" " * indent}end"
69
+ buf << "#{' ' * indent}end"
70
70
 
71
- buf << ""
71
+ buf << ''
72
72
 
73
- buf << "#{" " * indent}class #{class_name} < #{superclass.name}"
73
+ buf << "#{' ' * indent}class #{class_name} < #{superclass.name}"
74
74
  indent += 1
75
75
 
76
- buf << "#{" " * indent}include #{interface_name}"
76
+ buf << "#{' ' * indent}include #{interface_name}"
77
77
 
78
78
  indent -= 1
79
- buf << "#{" " * indent}end"
79
+ buf << "#{' ' * indent}end"
80
80
 
81
81
  unless namespace.empty?
82
- buf << "end"
82
+ buf << 'end'
83
83
  end
84
84
 
85
- buf << ""
85
+ buf << ''
86
86
 
87
87
  buf.join("\n")
88
88
  end
89
89
  end
90
90
 
91
- Config.extend RBSGenerator
91
+ Config.extend(RBSGenerator)
92
92
  end
@@ -0,0 +1,109 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'pathname'
4
+
5
+ # Use Settings name to not confuse with Config.
6
+ #
7
+ # Settings contain the library-wide configuration.
8
+ class Runger::Settings
9
+ # Future encapsulates settings that will be introduced in the upcoming version
10
+ # with the default values, which could break compatibility
11
+ class Future
12
+ class << self
13
+ def setting(name, default_value)
14
+ settings[name] = default_value
15
+
16
+ define_method(name) do
17
+ store[name]
18
+ end
19
+
20
+ define_method(:"#{name}=") do |val|
21
+ store[name] = val
22
+ end
23
+ end
24
+
25
+ def settings
26
+ @settings ||= {}
27
+ end
28
+ end
29
+
30
+ def initialize
31
+ @store = {}
32
+ end
33
+
34
+ def use(*names)
35
+ store.clear
36
+ names.each { store[_1] = self.class.settings[_1] }
37
+ end
38
+
39
+ setting :unwrap_known_environments, true
40
+
41
+ private
42
+
43
+ attr_reader :store
44
+ end
45
+
46
+ class << self
47
+ # Define whether to load data from
48
+ # *.yml.local (or credentials/local.yml.enc)
49
+ attr_accessor :use_local_files,
50
+ :current_environment,
51
+ :default_environmental_key,
52
+ :known_environments
53
+
54
+ # A proc returning a path to YML config file given the config name
55
+ attr_reader :default_config_path
56
+
57
+ def default_config_path=(val)
58
+ if val.is_a?(Proc)
59
+ @default_config_path = val
60
+ return
61
+ end
62
+
63
+ val = val.to_s
64
+
65
+ @default_config_path = ->(name) { File.join(val, "#{name}.yml") }
66
+ end
67
+
68
+ # Enable source tracing
69
+ attr_accessor :tracing_enabled
70
+
71
+ def future
72
+ @future ||= Future.new
73
+ end
74
+
75
+ def app_root
76
+ Pathname.new(Dir.pwd)
77
+ end
78
+
79
+ def default_environmental_key?
80
+ !default_environmental_key.nil?
81
+ end
82
+
83
+ def matching_env?(env)
84
+ return true if env.nil? || env.to_s == current_environment
85
+
86
+ if env.is_a?(::Hash)
87
+ envs = env[:except]
88
+ excluded_envs = [envs].flat_map(&:to_s)
89
+ excluded_envs.none?(current_environment)
90
+ elsif env.is_a?(::Array)
91
+ env.flat_map(&:to_s).include?(current_environment)
92
+ else
93
+ false
94
+ end
95
+ end
96
+ end
97
+
98
+ # By default, use RUNGER_ENV
99
+ self.current_environment = ENV.fetch('RUNGER_ENV', nil)
100
+
101
+ # By default, use local files only in development (that's the purpose if the local files)
102
+ self.use_local_files = (ENV['RUNGER_ENV'] == 'development' || ENV['RACK_ENV'] == 'development' || ENV['RAILS_ENV'] == 'development' || (defined?(Rails) && Rails.env.development?))
103
+
104
+ # By default, consider configs are stored in the ./config folder
105
+ self.default_config_path = ->(name) { "./config/#{name}.yml" }
106
+
107
+ # Tracing is enabled by default
108
+ self.tracing_enabled = true
109
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Runger::Testing::Helpers
4
+ # Sets the ENV variables to the provided
5
+ # values and restore outside the block
6
+ #
7
+ # Also resets Runger.env before and after calling the block
8
+ # to make sure that the values are not cached.
9
+ #
10
+ # NOTE: to remove the env value, pass `nil` as the value
11
+ def with_env(data)
12
+ was_values = []
13
+
14
+ data.each do |key, val|
15
+ was_values << [key, ENV.fetch(key, nil)]
16
+ next ENV.delete(key) if val.nil?
17
+
18
+ ENV[key] = val
19
+ end
20
+
21
+ # clear cached env values
22
+ Runger.env.clear
23
+ yield
24
+ ensure
25
+ was_values.each do |(key, val)|
26
+ next ENV.delete(key) if val.nil?
27
+
28
+ ENV[key] = val
29
+ end
30
+
31
+ # clear cache again
32
+ Runger.env.clear
33
+ end
34
+ end
@@ -1,13 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "anyway/testing/helpers"
3
+ require 'runger/testing/helpers'
4
4
 
5
5
  if defined?(RSpec::Core) && RSpec.respond_to?(:configure)
6
6
  RSpec.configure do |config|
7
7
  config.include(
8
- Anyway::Testing::Helpers,
8
+ Runger::Testing::Helpers,
9
9
  type: :config,
10
- file_path: %r{spec/configs}
10
+ file_path: %r{spec/configs},
11
11
  )
12
12
  end
13
13
  end
@@ -0,0 +1,195 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Provides method to trace values association
4
+ module Runger::Tracing
5
+ using Runger::Ext::DeepDup
6
+
7
+ using(Module.new do
8
+ refine Thread::Backtrace::Location do
9
+ def path_lineno = "#{path}:#{lineno}"
10
+ end
11
+ end)
12
+
13
+ class Trace
14
+ UNDEF = Object.new
15
+
16
+ attr_reader :type, :value, :source
17
+
18
+ def initialize(type = :trace, value = UNDEF, **source)
19
+ @type = type
20
+ @source = source
21
+ @value = (value == UNDEF) ? Hash.new { |h, k| h[k] = Trace.new(:trace) } : value
22
+ end
23
+
24
+ def dig(...)
25
+ value.dig(...)
26
+ end
27
+
28
+ def record_value(val, *path, **options)
29
+ key = path.pop
30
+ trace =
31
+ if val.is_a?(Hash)
32
+ Trace.new.tap { _1.merge_values(val, **options) }
33
+ else
34
+ Trace.new(:value, val, **options)
35
+ end
36
+
37
+ target_trace = path.empty? ? self : value.dig(*path)
38
+ target_trace.record_key(key.to_s, trace)
39
+
40
+ val
41
+ end
42
+
43
+ def merge_values(hash, **options)
44
+ return hash unless hash
45
+
46
+ hash.each do |key, val|
47
+ if val.is_a?(Hash)
48
+ value[key.to_s].merge_values(val, **options)
49
+ else
50
+ value[key.to_s] = Trace.new(:value, val, **options)
51
+ end
52
+ end
53
+
54
+ hash
55
+ end
56
+
57
+ def record_key(key, key_trace)
58
+ @value = Hash.new { |h, k| h[k] = Trace.new(:trace) } unless value.is_a?(::Hash)
59
+
60
+ value[key] = key_trace
61
+ end
62
+
63
+ def merge!(another_trace)
64
+ unless trace?
65
+ raise(ArgumentError,
66
+ "You can only merge into a :trace type, and this is :#{type}")
67
+ end
68
+ unless another_trace.trace?
69
+ raise(ArgumentError,
70
+ "You can only merge a :trace type, but trying :#{type}")
71
+ end
72
+
73
+ another_trace.value.each do |key, sub_trace|
74
+ if sub_trace.trace?
75
+ value[key].merge!(sub_trace)
76
+ else
77
+ value[key] = sub_trace
78
+ end
79
+ end
80
+ end
81
+
82
+ def keep_if(...)
83
+ raise(ArgumentError, "You can only filter :trace type, and this is :#{type}") unless trace?
84
+
85
+ value.keep_if(...)
86
+ end
87
+
88
+ def clear = value.clear
89
+
90
+ def trace? = type == :trace
91
+
92
+ def to_h
93
+ if trace?
94
+ value.transform_values(&:to_h).tap { _1.default_proc = nil }
95
+ else
96
+ { value:, source: }
97
+ end
98
+ end
99
+
100
+ def dup = self.class.new(type, value.dup, **source)
101
+
102
+ def pretty_print(q)
103
+ if trace?
104
+ q.nest(2) do
105
+ q.breakable('')
106
+ q.seplist(value, nil, :each) do |k, v|
107
+ q.group do
108
+ q.text k
109
+ q.text ' =>'
110
+ if v.trace?
111
+ q.text ' { '
112
+ q.pp(v)
113
+ q.breakable(' ')
114
+ q.text '}'
115
+ else
116
+ q.breakable(' ')
117
+ q.pp(v)
118
+ end
119
+ end
120
+ end
121
+ end
122
+ else
123
+ q.pp(value)
124
+ q.group(0, ' (', ')') do
125
+ q.seplist(source, lambda { q.breakable(' ') }, :each) do |k, v|
126
+ q.group do
127
+ q.text k.to_s
128
+ q.text '='
129
+ q.text v.to_s
130
+ end
131
+ end
132
+ end
133
+ end
134
+ end
135
+ end
136
+
137
+ class << self
138
+ def capture
139
+ unless ::Runger::Settings.tracing_enabled
140
+ yield
141
+ return
142
+ end
143
+
144
+ trace = Trace.new
145
+ trace_stack.push(trace)
146
+ yield
147
+ trace_stack.last
148
+ ensure
149
+ trace_stack.pop
150
+ end
151
+
152
+ def trace_stack
153
+ (Thread.current[:__runger__trace_stack__] ||= [])
154
+ end
155
+
156
+ def current_trace = trace_stack.last
157
+
158
+ alias tracing? current_trace
159
+
160
+ def source_stack
161
+ (Thread.current[:__runger__trace_source_stack__] ||= [])
162
+ end
163
+
164
+ def current_trace_source
165
+ source_stack.last || accessor_source(caller_locations(2, 1).first)
166
+ end
167
+
168
+ def with_trace_source(src)
169
+ source_stack << src
170
+ yield
171
+ ensure
172
+ source_stack.pop
173
+ end
174
+
175
+ private
176
+
177
+ def accessor_source(location)
178
+ { type: :accessor, called_from: location.path_lineno }
179
+ end
180
+ end
181
+
182
+ module_function
183
+
184
+ def trace!(type, *path, **options)
185
+ return yield unless ::Runger::Tracing.tracing?
186
+
187
+ val = yield
188
+ if val.is_a?(Hash)
189
+ ::Runger::Tracing.current_trace.merge_values(val, type:, **options)
190
+ elsif !path.empty?
191
+ ::Runger::Tracing.current_trace.record_value(val, *path, type:, **options)
192
+ end
193
+ val
194
+ end
195
+ end