runger_config 2.6.0

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