runger_config 2.6.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 (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