runger_config 3.0.1 → 5.0.0

Sign up to get free protection for your applications and to get access to all the features.
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