anyway_config 2.0.0.pre2 → 2.0.0.rc1

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 (45) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +350 -0
  3. data/LICENSE.txt +1 -1
  4. data/README.md +444 -119
  5. data/lib/.rbnext/2.7/anyway/auto_cast.rb +33 -0
  6. data/lib/.rbnext/2.7/anyway/config.rb +404 -0
  7. data/lib/.rbnext/2.7/anyway/option_parser_builder.rb +31 -0
  8. data/lib/.rbnext/2.7/anyway/tracing.rb +187 -0
  9. data/lib/anyway/auto_cast.rb +33 -0
  10. data/lib/anyway/config.rb +235 -58
  11. data/lib/anyway/dynamic_config.rb +1 -1
  12. data/lib/anyway/env.rb +29 -19
  13. data/lib/anyway/ext/hash.rb +14 -6
  14. data/lib/anyway/loaders.rb +79 -0
  15. data/lib/anyway/loaders/base.rb +23 -0
  16. data/lib/anyway/loaders/env.rb +16 -0
  17. data/lib/anyway/loaders/yaml.rb +46 -0
  18. data/lib/anyway/option_parser_builder.rb +6 -3
  19. data/lib/anyway/optparse_config.rb +10 -6
  20. data/lib/anyway/rails.rb +16 -0
  21. data/lib/anyway/rails/config.rb +2 -69
  22. data/lib/anyway/rails/loaders.rb +5 -0
  23. data/lib/anyway/rails/loaders/credentials.rb +64 -0
  24. data/lib/anyway/rails/loaders/secrets.rb +39 -0
  25. data/lib/anyway/rails/loaders/yaml.rb +19 -0
  26. data/lib/anyway/rails/settings.rb +58 -0
  27. data/lib/anyway/railtie.rb +14 -2
  28. data/lib/anyway/settings.rb +29 -0
  29. data/lib/anyway/tracing.rb +187 -0
  30. data/lib/anyway/version.rb +1 -1
  31. data/lib/anyway_config.rb +27 -21
  32. data/lib/generators/anyway/app_config/USAGE +9 -0
  33. data/lib/generators/anyway/app_config/app_config_generator.rb +17 -0
  34. data/lib/generators/anyway/config/USAGE +13 -0
  35. data/lib/generators/anyway/config/config_generator.rb +46 -0
  36. data/lib/generators/anyway/config/templates/config.rb.tt +9 -0
  37. data/lib/generators/anyway/config/templates/config.yml.tt +13 -0
  38. data/lib/generators/anyway/install/USAGE +4 -0
  39. data/lib/generators/anyway/install/install_generator.rb +43 -0
  40. data/lib/generators/anyway/install/templates/application_config.rb.tt +17 -0
  41. metadata +75 -10
  42. data/lib/anyway/ext/string_serialize.rb +0 -38
  43. data/lib/anyway/loaders/env_loader.rb +0 -0
  44. data/lib/anyway/loaders/secrets_loader.rb +0 -0
  45. data/lib/anyway/loaders/yaml_loader.rb +0 -0
@@ -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,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.public_send(name)
26
+ end.then do |creds|
27
+ config.deep_merge!(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| config.deep_merge!(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.public_send(name)
52
+ end
53
+
54
+ def credentials_path
55
+ if ::Rails.application.config.respond_to?(:credentials)
56
+ ::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,39 @@
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
+ config.deep_merge!(secrets) if secrets
19
+ end
20
+
21
+ config
22
+ end
23
+
24
+ private
25
+
26
+ def secrets
27
+ @secrets ||= begin
28
+ ::Rails.application.secrets.tap do |_|
29
+ # Reset secrets state if the app hasn't been initialized
30
+ # See https://github.com/palkan/anyway_config/issues/14
31
+ next if ::Rails.application.initialized?
32
+ ::Rails.application.remove_instance_variable(:@secrets)
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Anyway
4
+ module Rails
5
+ module Loaders
6
+ class YAML < Anyway::Loaders::YAML
7
+ def parse_yml(*)
8
+ super[::Rails.env] || {}
9
+ end
10
+
11
+ private
12
+
13
+ def relative_config_path(path)
14
+ Pathname.new(path).relative_path_from(::Rails.root)
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,58 @@
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
14
+
15
+ if defined?(::Zeitwerk)
16
+ attr_reader :autoloader
17
+
18
+ def autoload_static_config_path=(val)
19
+ raise "Cannot setup autoloader after application has been initialized" if ::Rails.application.initialized?
20
+
21
+ return unless ::Rails.root.join(val).exist?
22
+
23
+ autoloader&.unload
24
+
25
+ @autoload_static_config_path = val
26
+
27
+ # See https://github.com/rails/rails/blob/8ab4fd12f18203b83d0f252db96d10731485ff6a/railties/lib/rails/autoloaders.rb#L10
28
+ @autoloader = Zeitwerk::Loader.new.tap do |loader|
29
+ loader.tag = "anyway.config"
30
+ loader.inflector = ActiveSupport::Dependencies::ZeitwerkIntegration::Inflector
31
+ loader.push_dir(::Rails.root.join(val))
32
+ loader.setup
33
+ end
34
+ end
35
+
36
+ def cleanup_autoload_paths
37
+ return unless autoload_static_config_path
38
+ ActiveSupport::Dependencies.autoload_paths.delete(::Rails.root.join(autoload_static_config_path).to_s)
39
+ end
40
+ else
41
+ def autoload_static_config_path=(val)
42
+ if autoload_static_config_path
43
+ ActiveSupport::Dependencies.autoload_paths.delete(::Rails.root.join(autoload_static_config_path).to_s)
44
+ end
45
+
46
+ @autoload_static_config_path = val
47
+ ActiveSupport::Dependencies.autoload_paths << ::Rails.root.join(val)
48
+ end
49
+
50
+ def cleanup_autoload_paths
51
+ :no_op
52
+ end
53
+ end
54
+ end
55
+
56
+ self.default_config_path = ->(name) { ::Rails.root.join("config", "#{name}.yml") }
57
+ end
58
+ end
@@ -1,11 +1,23 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Anyway # :nodoc:
4
+ DEFAULT_CONFIGS_PATH = "config/configs"
5
+
4
6
  class Railtie < ::Rails::Railtie # :nodoc:
5
7
  # Add settings to Rails config
6
8
  config.anyway_config = Anyway::Settings
7
9
 
8
- # Allow autoloading of app/configs in configuration files
9
- ActiveSupport::Dependencies.autoload_paths << "app/configs"
10
+ ActiveSupport.on_load(:before_configuration) do
11
+ config.anyway_config.autoload_static_config_path = DEFAULT_CONFIGS_PATH
12
+ end
13
+
14
+ # Remove `autoload_static_config_path` from Rails `autoload_paths`
15
+ # since we use our own autoloading mechanism
16
+ initializer "anyway_config.cleanup_autoload" do
17
+ Anyway::Settings.cleanup_autoload_paths
18
+ end
19
+
20
+ # Make sure loaders are not changed in runtime
21
+ config.after_initialize { Anyway.loaders.freeze }
10
22
  end
11
23
  end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Anyway
4
+ # Use Settings name to not confuse with Config.
5
+ #
6
+ # Settings contain the library-wide configuration.
7
+ class Settings
8
+ class << self
9
+ # Define whether to load data from
10
+ # *.yml.local (or credentials/local.yml.enc)
11
+ attr_accessor :use_local_files
12
+
13
+ # Return a path to YML config file given the config name
14
+ attr_accessor :default_config_path
15
+
16
+ # Enable source tracing
17
+ attr_accessor :tracing_enabled
18
+ end
19
+
20
+ # By default, use local files only in development (that's the purpose if the local files)
21
+ self.use_local_files = (ENV["RACK_ENV"] == "development" || ENV["RAILS_ENV"] == "development")
22
+
23
+ # By default, consider configs are stored in the ./config folder
24
+ self.default_config_path = ->(name) { "./config/#{name}.yml" }
25
+
26
+ # Tracing is enabled by default
27
+ self.tracing_enabled = true
28
+ end
29
+ end
@@ -0,0 +1,187 @@
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 Hash do
10
+ def inspect
11
+ "{#{map { |k, v| "#{k}: #{v.inspect}" }.join(", ")}}"
12
+ end
13
+ end
14
+
15
+ refine Thread::Backtrace::Location do
16
+ def path_lineno
17
+ "#{path}:#{lineno}"
18
+ end
19
+ end
20
+ end)
21
+
22
+ class Trace
23
+ UNDEF = Object.new
24
+
25
+ attr_reader :type, :value, :source
26
+
27
+ def initialize(type = :trace, value = UNDEF, **source)
28
+ @type = type
29
+ @source = source
30
+ @value = value == UNDEF ? Hash.new { |h, k| h[k] = Trace.new(:trace) } : value
31
+ end
32
+
33
+ def dig(...)
34
+ value.dig(...)
35
+ end
36
+
37
+ def record_value(val, *path, key, **opts)
38
+ trace =
39
+ if val.is_a?(Hash)
40
+ Trace.new.tap { _1.merge_values(val, **opts) }
41
+ else
42
+ Trace.new(:value, val, **opts)
43
+ end
44
+ target_trace = path.empty? ? self : value.dig(*path)
45
+ target_trace.value[key.to_s] = trace
46
+
47
+ val
48
+ end
49
+
50
+ def merge_values(hash, **opts)
51
+ return hash unless hash
52
+
53
+ hash.each do |key, val|
54
+ if val.is_a?(Hash)
55
+ value[key.to_s].merge_values(val, **opts)
56
+ else
57
+ value[key.to_s] = Trace.new(:value, val, **opts)
58
+ end
59
+ end
60
+
61
+ hash
62
+ end
63
+
64
+ def merge!(another_trace)
65
+ raise ArgumentError, "You can only merge into a :trace type, and this is :#{type}" unless trace?
66
+ raise ArgumentError, "You can only merge a :trace type, but trying :#{type}" unless another_trace.trace?
67
+
68
+ another_trace.value.each do |key, sub_trace|
69
+ if sub_trace.trace?
70
+ value[key].merge! sub_trace
71
+ else
72
+ value[key] = sub_trace
73
+ end
74
+ end
75
+ end
76
+
77
+ def keep_if(...)
78
+ raise ArgumentError, "You can only filter :trace type, and this is :#{type}" unless trace?
79
+ value.keep_if(...)
80
+ end
81
+
82
+ def clear
83
+ value.clear
84
+ end
85
+
86
+ def trace?
87
+ type == :trace
88
+ end
89
+
90
+ def to_h
91
+ if trace?
92
+ value.transform_values(&:to_h).tap { _1.default_proc = nil }
93
+ else
94
+ {value: value, source: source}
95
+ end
96
+ end
97
+
98
+ def pretty_print(q)
99
+ if trace?
100
+ q.nest(2) do
101
+ q.breakable ""
102
+ q.seplist(value, nil, :each) do |k, v|
103
+ q.group do
104
+ q.text k
105
+ q.text " =>"
106
+ q.breakable " " unless v.trace?
107
+ q.pp v
108
+ end
109
+ end
110
+ end
111
+ else
112
+ q.pp value
113
+ q.group(0, " (", ")") do
114
+ q.seplist(source, lambda { q.breakable " " }, :each) do |k, v|
115
+ q.group do
116
+ q.text k.to_s
117
+ q.text "="
118
+ q.text v.to_s
119
+ end
120
+ end
121
+ end
122
+ end
123
+ end
124
+ end
125
+
126
+ class << self
127
+ def capture
128
+ unless Settings.tracing_enabled
129
+ yield
130
+ return
131
+ end
132
+
133
+ trace = Trace.new
134
+ trace_stack.push trace
135
+ yield
136
+ trace_stack.last
137
+ ensure
138
+ trace_stack.pop
139
+ end
140
+
141
+ def trace_stack
142
+ (Thread.current[:__anyway__trace_stack__] ||= [])
143
+ end
144
+
145
+ def current_trace
146
+ trace_stack.last
147
+ end
148
+
149
+ alias tracing? current_trace
150
+
151
+ def source_stack
152
+ (Thread.current[:__anyway__trace_source_stack__] ||= [])
153
+ end
154
+
155
+ def current_trace_source
156
+ source_stack.last || accessor_source(caller_locations(2, 1).first)
157
+ end
158
+
159
+ def with_trace_source(src)
160
+ source_stack << src
161
+ yield
162
+ ensure
163
+ source_stack.pop
164
+ end
165
+
166
+ private
167
+
168
+ def accessor_source(location)
169
+ {type: :accessor, called_from: location.path_lineno}
170
+ end
171
+ end
172
+
173
+ module_function
174
+
175
+ def trace!(type, *path, **opts)
176
+ return yield unless Tracing.tracing?
177
+ source = {type: type}.merge(opts)
178
+ val = yield
179
+ if val.is_a?(Hash)
180
+ Tracing.current_trace.merge_values(val, **source)
181
+ else
182
+ Tracing.current_trace.record_value(yield, *path, **source)
183
+ end
184
+ val
185
+ end
186
+ end
187
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Anyway # :nodoc:
4
- VERSION = "2.0.0.pre2"
4
+ VERSION = "2.0.0.rc1"
5
5
  end
data/lib/anyway_config.rb CHANGED
@@ -1,32 +1,38 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Anyway # :nodoc:
4
- require "anyway/version"
5
-
6
- require "anyway/config"
7
- require "anyway/rails/config" if defined?(::Rails::VERSION)
8
- require "anyway/env"
9
-
10
- # Use Settings name to not confuse with Config.
11
- #
12
- # Settings contain the library-wide configuration.
13
- class Settings
14
- class << self
15
- # Define whether to load data from
16
- # *.yml.local (or credentials/local.yml.enc)
17
- attr_accessor :use_local_files
18
- end
3
+ require "ruby-next"
19
4
 
20
- # By default, use local files only in development (that's the purpose if the local files)
21
- self.use_local_files = (ENV["RACK_ENV"] == "development" || ENV["RAILS_ENV"] == "development")
22
- end
5
+ require "ruby-next/language/setup"
6
+ RubyNext::Language.setup_gem_load_path
7
+
8
+ require "anyway/version"
9
+
10
+ require "anyway/ext/deep_dup"
11
+ require "anyway/ext/deep_freeze"
12
+ require "anyway/ext/hash"
23
13
 
14
+ require "anyway/settings"
15
+ require "anyway/tracing"
16
+ require "anyway/config"
17
+ require "anyway/auto_cast"
18
+ require "anyway/env"
19
+ require "anyway/loaders"
20
+
21
+ module Anyway # :nodoc:
24
22
  class << self
25
23
  def env
26
24
  @env ||= ::Anyway::Env.new
27
25
  end
26
+
27
+ def loaders
28
+ @loaders ||= ::Anyway::Loaders::Registry.new
29
+ end
28
30
  end
29
31
 
30
- require "anyway/railtie" if defined?(::Rails::VERSION)
31
- require "anyway/testing" if ENV["RACK_ENV"] == "test" || ENV["RAILS_ENV"] == "test"
32
+ # Configure default loaders
33
+ loaders.append :yml, Loaders::YAML
34
+ loaders.append :env, Loaders::Env
32
35
  end
36
+
37
+ require "anyway/rails" if defined?(::Rails::VERSION)
38
+ require "anyway/testing" if ENV["RACK_ENV"] == "test" || ENV["RAILS_ENV"] == "test"