blueprint_config 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (40) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +19 -0
  3. data/.idea/.gitignore +8 -0
  4. data/.idea/blue_config.iml +72 -0
  5. data/.idea/misc.xml +4 -0
  6. data/.idea/modules.xml +8 -0
  7. data/.idea/vcs.xml +6 -0
  8. data/.rspec +1 -0
  9. data/.ruby-version +1 -0
  10. data/Gemfile +6 -0
  11. data/LICENSE.txt +22 -0
  12. data/README.md +152 -0
  13. data/Rakefile +8 -0
  14. data/blueprint_config.gemspec +31 -0
  15. data/lib/blueprint_config/backend/active_record.rb +51 -0
  16. data/lib/blueprint_config/backend/base.rb +34 -0
  17. data/lib/blueprint_config/backend/credentials.rb +26 -0
  18. data/lib/blueprint_config/backend/env.rb +47 -0
  19. data/lib/blueprint_config/backend/yaml.rb +30 -0
  20. data/lib/blueprint_config/backend_collection.rb +67 -0
  21. data/lib/blueprint_config/configuration.rb +64 -0
  22. data/lib/blueprint_config/options_array.rb +81 -0
  23. data/lib/blueprint_config/options_hash.rb +112 -0
  24. data/lib/blueprint_config/setting.rb +28 -0
  25. data/lib/blueprint_config/version.rb +5 -0
  26. data/lib/blueprint_config/yaml.rb +46 -0
  27. data/lib/blueprint_config.rb +71 -0
  28. data/lib/generators/blueprint_config/install/USAGE +8 -0
  29. data/lib/generators/blueprint_config/install/install_generator.rb +23 -0
  30. data/lib/generators/blueprint_config/install/templates/migration.rb.erb +18 -0
  31. data/spec/backend_collection_spec.rb +103 -0
  32. data/spec/blueprint_config/backend/active_record_spec.rb +41 -0
  33. data/spec/blueprint_config/backend/env_spec.rb +53 -0
  34. data/spec/blueprint_config/backend/yaml_spec.rb +35 -0
  35. data/spec/blueprint_config/options_array_spec.rb +109 -0
  36. data/spec/blueprint_config/options_hash_spec.rb +211 -0
  37. data/spec/config/app.yml +24 -0
  38. data/spec/configuration_spec.rb +98 -0
  39. data/spec/spec_helper.rb +16 -0
  40. metadata +163 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: be61b7fe3cee8680edfeddde8bbd290aa4463fd68ea2b2582651f740afe4685b
4
+ data.tar.gz: 9e6f745b5acc18080066a2ffb482eee0d69f704010ef923b8cab0f960c10ba48
5
+ SHA512:
6
+ metadata.gz: e2849e6eb9237668d42fa9d7c10fe3075798e2cbc296a4a21d76429dcd921252d012a0c96672b69debc18f919d650dc8a2f1897f3401b664c6b6b688326c6719
7
+ data.tar.gz: c76bd8289d7075f53358f9d7035b9d8e3479c4dc081c40c2c826e1cb3f561803bbe2380f66b741b6dea13636b2337c7cef4587dd29d241cb59a04790f236c97e
data/.gitignore ADDED
@@ -0,0 +1,19 @@
1
+ .idea
2
+ *.gem
3
+ *.rbc
4
+ .bundle
5
+ .config
6
+ .yardoc
7
+ Gemfile.lock
8
+ InstalledFiles
9
+ _yardoc
10
+ coverage
11
+ doc/
12
+ lib/bundler/man
13
+ pkg
14
+ rdoc
15
+ spec/reports
16
+ test/tmp
17
+ test/version_tmp
18
+ tmp
19
+ bundler_stubs
data/.idea/.gitignore ADDED
@@ -0,0 +1,8 @@
1
+ # Default ignored files
2
+ /shelf/
3
+ /workspace.xml
4
+ # Editor-based HTTP Client requests
5
+ /httpRequests/
6
+ # Datasource local storage ignored files
7
+ /dataSources/
8
+ /dataSources.local.xml
@@ -0,0 +1,72 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <module type="RUBY_MODULE" version="4">
3
+ <component name="ModuleRunConfigurationManager">
4
+ <shared />
5
+ </component>
6
+ <component name="NewModuleRootManager">
7
+ <content url="file://$MODULE_DIR$">
8
+ <sourceFolder url="file://$MODULE_DIR$/features" isTestSource="true" />
9
+ <sourceFolder url="file://$MODULE_DIR$/spec" isTestSource="true" />
10
+ <sourceFolder url="file://$MODULE_DIR$/test" isTestSource="true" />
11
+ </content>
12
+ <orderEntry type="inheritedJdk" />
13
+ <orderEntry type="sourceFolder" forTests="false" />
14
+ <orderEntry type="library" scope="PROVIDED" name="activemodel (v7.1.2, rbenv: 3.2.2) [gem]" level="application" />
15
+ <orderEntry type="library" scope="PROVIDED" name="activerecord (v7.1.2, rbenv: 3.2.2) [gem]" level="application" />
16
+ <orderEntry type="library" scope="PROVIDED" name="activesupport (v7.1.2, rbenv: 3.2.2) [gem]" level="application" />
17
+ <orderEntry type="library" scope="PROVIDED" name="base64 (v0.2.0, rbenv: 3.2.2) [gem]" level="application" />
18
+ <orderEntry type="library" scope="PROVIDED" name="bigdecimal (v3.1.5, rbenv: 3.2.2) [gem]" level="application" />
19
+ <orderEntry type="library" scope="PROVIDED" name="bundler (v2.4.22, rbenv: 3.2.2) [gem]" level="application" />
20
+ <orderEntry type="library" scope="PROVIDED" name="concurrent-ruby (v1.2.2, rbenv: 3.2.2) [gem]" level="application" />
21
+ <orderEntry type="library" scope="PROVIDED" name="connection_pool (v2.4.1, rbenv: 3.2.2) [gem]" level="application" />
22
+ <orderEntry type="library" scope="PROVIDED" name="diff-lcs (v1.5.0, rbenv: 3.2.2) [gem]" level="application" />
23
+ <orderEntry type="library" scope="PROVIDED" name="drb (v2.2.0, rbenv: 3.2.2) [gem]" level="application" />
24
+ <orderEntry type="library" scope="PROVIDED" name="i18n (v1.14.1, rbenv: 3.2.2) [gem]" level="application" />
25
+ <orderEntry type="library" scope="PROVIDED" name="minitest (v5.20.0, rbenv: 3.2.2) [gem]" level="application" />
26
+ <orderEntry type="library" scope="PROVIDED" name="mutex_m (v0.2.0, rbenv: 3.2.2) [gem]" level="application" />
27
+ <orderEntry type="library" scope="PROVIDED" name="rake (v13.1.0, rbenv: 3.2.2) [gem]" level="application" />
28
+ <orderEntry type="library" scope="PROVIDED" name="redis (v5.0.8, rbenv: 3.2.2) [gem]" level="application" />
29
+ <orderEntry type="library" scope="PROVIDED" name="redis-client (v0.19.1, rbenv: 3.2.2) [gem]" level="application" />
30
+ <orderEntry type="library" scope="PROVIDED" name="rspec (v3.12.0, rbenv: 3.2.2) [gem]" level="application" />
31
+ <orderEntry type="library" scope="PROVIDED" name="rspec-core (v3.12.2, rbenv: 3.2.2) [gem]" level="application" />
32
+ <orderEntry type="library" scope="PROVIDED" name="rspec-expectations (v3.12.3, rbenv: 3.2.2) [gem]" level="application" />
33
+ <orderEntry type="library" scope="PROVIDED" name="rspec-mocks (v3.12.6, rbenv: 3.2.2) [gem]" level="application" />
34
+ <orderEntry type="library" scope="PROVIDED" name="rspec-support (v3.12.1, rbenv: 3.2.2) [gem]" level="application" />
35
+ <orderEntry type="library" scope="PROVIDED" name="sqlite3 (v1.7.0, rbenv: 3.2.2) [gem]" level="application" />
36
+ <orderEntry type="library" scope="PROVIDED" name="timeout (v0.4.1, rbenv: 3.2.2) [gem]" level="application" />
37
+ <orderEntry type="library" scope="PROVIDED" name="tzinfo (v2.0.6, rbenv: 3.2.2) [gem]" level="application" />
38
+ </component>
39
+ <component name="RakeTasksCache">
40
+ <option name="myRootTask">
41
+ <RakeTaskImpl id="rake">
42
+ <subtasks>
43
+ <RakeTaskImpl description="Build econfig-0.1.1.gem into the pkg directory" fullCommand="build" id="build" />
44
+ <RakeTaskImpl id="build">
45
+ <subtasks>
46
+ <RakeTaskImpl description="Generate SHA512 checksum if econfig-0.1.1.gem into the checksums directory" fullCommand="build:checksum" id="checksum" />
47
+ </subtasks>
48
+ </RakeTaskImpl>
49
+ <RakeTaskImpl description="Remove any temporary products" fullCommand="clean" id="clean" />
50
+ <RakeTaskImpl description="Remove any generated files" fullCommand="clobber" id="clobber" />
51
+ <RakeTaskImpl description="Build and install econfig-0.1.1.gem into system gems" fullCommand="install" id="install" />
52
+ <RakeTaskImpl id="install">
53
+ <subtasks>
54
+ <RakeTaskImpl description="Build and install econfig-0.1.1.gem into system gems without network access" fullCommand="install:local" id="local" />
55
+ </subtasks>
56
+ </RakeTaskImpl>
57
+ <RakeTaskImpl description="Create tag v0.1.1 and build and push econfig-0.1.1.gem to rubygems.org" fullCommand="release[remote]" id="release[remote]" />
58
+ <RakeTaskImpl description="Run RSpec code examples" fullCommand="spec" id="spec" />
59
+ <RakeTaskImpl description="" fullCommand="default" id="default" />
60
+ <RakeTaskImpl description="" fullCommand="release" id="release" />
61
+ <RakeTaskImpl id="release">
62
+ <subtasks>
63
+ <RakeTaskImpl description="" fullCommand="release:guard_clean" id="guard_clean" />
64
+ <RakeTaskImpl description="" fullCommand="release:rubygem_push" id="rubygem_push" />
65
+ <RakeTaskImpl description="" fullCommand="release:source_control_push" id="source_control_push" />
66
+ </subtasks>
67
+ </RakeTaskImpl>
68
+ </subtasks>
69
+ </RakeTaskImpl>
70
+ </option>
71
+ </component>
72
+ </module>
data/.idea/misc.xml ADDED
@@ -0,0 +1,4 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <project version="4">
3
+ <component name="ProjectRootManager" version="2" project-jdk-name="rbenv: 3.2.2" project-jdk-type="RUBY_SDK" />
4
+ </project>
data/.idea/modules.xml ADDED
@@ -0,0 +1,8 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <project version="4">
3
+ <component name="ProjectModuleManager">
4
+ <modules>
5
+ <module fileurl="file://$PROJECT_DIR$/.idea/blue_config.iml" filepath="$PROJECT_DIR$/.idea/blue_config.iml" />
6
+ </modules>
7
+ </component>
8
+ </project>
data/.idea/vcs.xml ADDED
@@ -0,0 +1,6 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <project version="4">
3
+ <component name="VcsDirectoryMappings">
4
+ <mapping directory="" vcs="Git" />
5
+ </component>
6
+ </project>
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ -r spec_helper
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 3.2.2
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ # Specify your gem's dependencies in econfig.gemspec
6
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2024 Vladimir Elchinov, Rails Blueprint
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,152 @@
1
+ # Blueprint Config
2
+
3
+ Blueprint Config is a gem which allows you to easily configure your Ruby applications
4
+ in a multitude of ways.
5
+
6
+ It was highly inspired by other solutions, Econfig in first place.
7
+
8
+ ## Installation
9
+
10
+ Add this to your Gemfile:
11
+
12
+ ``` ruby
13
+ gem "blueprint_config"
14
+ ```
15
+
16
+ ## Using with Ruby on Rails app
17
+
18
+ In Rails, you'll want to add this in `config/application.rb`:
19
+
20
+ ``` ruby
21
+ module MyApp
22
+ class Application < Rails::Application
23
+ BlueprintConfig.configure_rails(config)
24
+ end
25
+ end
26
+ ```
27
+
28
+ This will create module "AppConfig" for accessing configuration options,
29
+ and load configuration in following order:
30
+ - config/app.yml
31
+ - credentials
32
+ - ENV variables
33
+ - settings in database
34
+ - config/app.local.yml
35
+
36
+ Settings form database will be available only after rails app initialization.
37
+ Everything else can be used in initializers.
38
+
39
+ ## Accessing configuration variables.
40
+
41
+ You can access configuration variables in any of followining ways:
42
+
43
+ ### Member access syntax:
44
+
45
+ ```ruby
46
+ irb(main):001> AppConfig.action_mailer.smtp_settings.address
47
+ => "127.0.0.1"
48
+ ```
49
+
50
+ If at some level variable is not defined and you try to access nested variable,
51
+ you'll gen an exception
52
+
53
+ ```ruby
54
+ irb(main):001> AppConfig.some.var
55
+ (irb):1:in `<main>': undefined method `var' for nil:NilClass (NoMethodError)
56
+
57
+ AppConfig.some.var
58
+ ^^^^
59
+ ```
60
+
61
+ If nil is suitable as a default value tyou can use safe navigation operator
62
+
63
+ ```ruby
64
+ irb(main):001> AppConfig.some&.var
65
+ => nil
66
+
67
+ ```
68
+
69
+
70
+
71
+ Optionaly you use bang methods to allways raise exception when variable is not defined
72
+
73
+ ```ruby
74
+ irb(main):001> AppConfig.some
75
+ => nil
76
+ irb(main):002> AppConfig.some!
77
+ (irb):2:in `<main>': Configuration key 'some' is not set (KeyError)
78
+ irb(main):003> AppConfig.some!.var!
79
+ (irb):3:in `<main>': Configuration key 'some' is not set (KeyError)
80
+ ```
81
+
82
+ Or use question mark to check if variable is defined
83
+
84
+ ```ruby
85
+ irb(main):001> AppConfig.some?
86
+ => false
87
+ irb(main):002> AppConfig.host?
88
+ => true
89
+ ```
90
+
91
+ Note: Because question mark methods return Boolean you cannot chain em.
92
+
93
+ ### Hash access syntax
94
+ You can use both symbols or strings as keys
95
+
96
+ ```ruby
97
+ irb(main):001> AppConfig[:action_mailer][:delivery_method]
98
+ => :letter_opener
99
+ irb(main):002> AppConfig['action_mailer']['delivery_method']
100
+ => :letter_opener
101
+
102
+ ```
103
+ Again, if some level is missing you'll get an exception
104
+ ```ruby
105
+ irb(main):001> AppConfig[:some][:var]
106
+ (irb):1:in `<main>': undefined method `[]' for nil:NilClass (NoMethodError)
107
+
108
+ AppConfig[:some][:var]
109
+ ^^^^^^
110
+ ```
111
+
112
+ ### Fetch
113
+
114
+ You can use hash-style fetch method, but it works only for one level
115
+ (but you can chain it). Default values as second parameter or block are
116
+ supported. Without default value missing key will raise exception.
117
+
118
+ ```ruby
119
+ irb(main):001> AppConfig.fetch(:host)
120
+ => "localhost"
121
+ irb(main):002> AppConfig.fetch(:var)
122
+ (irb):2:in `<main>': Configuration key 'var' is not set (KeyError)
123
+ irb(main):003> AppConfig.fetch(:var, 123)
124
+ => 123
125
+ irb(main):004> AppConfig.fetch(:var){123}
126
+ => 123
127
+ ```
128
+
129
+ ### Dig
130
+
131
+ Dig permits to sefely get value in nested structures (including arrays)
132
+
133
+ ```ruby
134
+ irb(main):001> AppConfig.dig(:action_mailer, :smtp_settings, :address)
135
+ => "127.0.0.1"
136
+ irb(main):002> AppConfig.dig(:action_mailer, :smtp_settings, :unknown)
137
+ => nil
138
+ ```
139
+ Bang version will raise exception when key at any level is missing
140
+ ```ruby
141
+ irb(main):001> AppConfig.dig!(:action_mailer, :smtp_settings, :unknown)
142
+ (irb):1:in `<main>': Configuration key 'action_mailer.smtp_settings.unknown' is not set (KeyError)
143
+ irb(main):002> AppConfig.dig!(:action, :smtp_settings, :address)
144
+ (irb):2:in `<main>': Configuration key 'action' is not set (KeyError)
145
+ ```
146
+
147
+ Whenever possible exception message specifies which key is missing.
148
+
149
+
150
+ ## License
151
+
152
+ MIT, see separate LICENSE.txt file
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
4
+
5
+ require 'rspec/core/rake_task'
6
+ RSpec::Core::RakeTask.new
7
+
8
+ task default: :spec
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'English'
4
+
5
+ lib = File.expand_path('lib', __dir__)
6
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
7
+ require 'blueprint_config/version'
8
+
9
+ Gem::Specification.new do |gem|
10
+ gem.name = 'blueprint_config'
11
+ gem.version = BlueprintConfig::VERSION
12
+ gem.authors = ['Vladimir Elchinov', 'Rails Blueprint']
13
+ gem.email = ['elik@elik.ru', 'info@railsblueprint.com']
14
+ gem.description = 'Flexible configuration for Ruby/Rails applications with a variety of backends'
15
+ gem.summary = 'Congifure Ruby apps'
16
+ gem.homepage = 'https://github.com/railsblueprint/blueprint_config'
17
+ gem.license = 'MIT'
18
+
19
+ gem.files = `git ls-files`.split($INPUT_RECORD_SEPARATOR)
20
+ gem.executables = gem.files.grep(%r{^bin/}).map { |f| File.basename(f) }
21
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
22
+ gem.require_paths = ['lib']
23
+
24
+ gem.required_ruby_version = '~> 3.0'
25
+
26
+ gem.add_development_dependency 'activerecord'
27
+ gem.add_development_dependency 'rake'
28
+ gem.add_development_dependency 'redis'
29
+ gem.add_development_dependency 'rspec'
30
+ gem.add_development_dependency 'sqlite3'
31
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_record'
4
+ require 'blueprint_config/backend/base'
5
+ require 'blueprint_config/setting'
6
+
7
+ module BlueprintConfig
8
+ module Backend
9
+ class ActiveRecord < Base
10
+ MISSING_TABLE_WARNING = <<-WARNING.gsub(/^ */, '')
11
+ =======================================================================
12
+ Settings table not found. Please add the configuration table by running:
13
+
14
+ rails generate blue_config:migration
15
+ rake db:migrate
16
+ =======================================================================
17
+ WARNING
18
+
19
+ def initialize(options = {})
20
+ @options = options
21
+ @updated_at = nil
22
+ @last_checked_at = nil
23
+ @mutex = Mutex.new
24
+ end
25
+
26
+ def load_keys
27
+ update_timestamp
28
+
29
+ data = Setting.all.map { |s| { s.key => s.parsed_value } }.reduce(:merge) || {}
30
+ return data.transform_keys(&:to_sym) unless @options[:nest]
31
+
32
+ nest_hash(data, @options[:nest_separator] || '.')
33
+ end
34
+
35
+ def update_timestamp
36
+ @mutex.synchronize do
37
+ @updated_at = Setting.maximum(:updated_at)
38
+ end
39
+ end
40
+
41
+ def fresh?
42
+ return true if @last_checked_at.present? && @last_checked_at > 1.second.ago
43
+
44
+ @mutex.synchronize do
45
+ @last_checked_at = Time.now
46
+ end
47
+ @updated_at.present? && @updated_at >= Setting.maximum(:updated_at)
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support/core_ext/hash/deep_merge'
4
+
5
+ module BlueprintConfig
6
+ module Backend
7
+ class Base
8
+ def nest_hash(hash, delimiter = '_')
9
+ hash.each_with_object({}) do |(key, value), results|
10
+ steps = key.split(delimiter).reverse
11
+ nested = steps.reduce(value) { |value, key| { key.to_sym => value } }
12
+
13
+ results.deep_merge!(nested) do |_key, a, b|
14
+ if a.is_a?(Hash) && b.is_a?(String)
15
+ a.deep_merge(nil => b)
16
+ elsif b.is_a?(Hash) && a.is_a?(String)
17
+ b.deep_merge(nil => a)
18
+ else
19
+ b
20
+ end
21
+ end
22
+ end
23
+ end
24
+
25
+ def source
26
+ self.class.name
27
+ end
28
+
29
+ def fresh?
30
+ true
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support/encrypted_configuration'
4
+
5
+ module BlueprintConfig
6
+ module Backend
7
+ class Credentials < Base
8
+ def load_keys
9
+ credentials.to_h
10
+ end
11
+
12
+ def credentials
13
+ if defined?(Rails)
14
+ Rails.application.credentials
15
+ else
16
+ ActiveSupport::EncryptedConfiguration.new(
17
+ config_path: 'config/credentials.yml.enc',
18
+ key_path: 'config/master.key',
19
+ env_key: 'RAILS_MASTER_KEY',
20
+ raise_if_missing_key: false
21
+ )
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'blueprint_config/backend/base'
4
+
5
+ module BlueprintConfig
6
+ module Backend
7
+ class ENV < Base
8
+ attr_accessor :options
9
+
10
+ def initialize(options = {})
11
+ @options = options
12
+ end
13
+
14
+ def load_keys
15
+ {
16
+ # env: ::ENV.to_h.transform_keys(&:to_sym),
17
+ ** transformed_env
18
+ }
19
+ end
20
+
21
+ def env_downcased
22
+ @env_downcased ||= ::ENV.to_h.transform_keys(&:downcase)
23
+ end
24
+
25
+ def env_downcased_keys
26
+ @env_downcased_keys ||= env_downcased.keys
27
+ end
28
+
29
+ def filtered_env
30
+ return env_downcased if @options[:allow_all]
31
+
32
+ allowed_keys = @options[:whitelist_keys]&.map(&:to_s) || []
33
+ @options[:whitelist_prefixes]&.each do |prefix|
34
+ allowed_keys += env_downcased_keys.select { |key| key.to_s.start_with?(prefix.to_s) }
35
+ end
36
+
37
+ env_downcased.slice(* allowed_keys)
38
+ end
39
+
40
+ def transformed_env
41
+ return filtered_env.transform_keys(&:to_sym) unless @options[:nest]
42
+
43
+ nest_hash(filtered_env, @options[:nest_separator] || '_')
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'erb'
4
+ require 'yaml'
5
+ require 'active_support/hash_with_indifferent_access'
6
+
7
+ module BlueprintConfig
8
+ module Backend
9
+ class YAML < Base
10
+ def initialize(path = 'config/app.yml')
11
+ @path = path
12
+ end
13
+
14
+ def load_keys
15
+ if File.exist?(path)
16
+ parsed = ::YAML.load(File.read(path), aliases: true).deep_symbolize_keys
17
+ parsed.fetch(:default, {}).deep_merge(parsed.fetch(BlueprintConfig.env&.to_sym, {}))
18
+ else
19
+ {}
20
+ end
21
+ end
22
+
23
+ private
24
+
25
+ def path
26
+ File.expand_path(@path, BlueprintConfig.root)
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BlueprintConfig
4
+ class BackendCollection
5
+ include Enumerable
6
+
7
+ def initialize
8
+ @backends = []
9
+ end
10
+
11
+ def [](name)
12
+ pair = @backends.assoc(name)
13
+ pair&.last
14
+ end
15
+
16
+ def each
17
+ if block_given?
18
+ @backends.each { |(_name, backend)| yield backend }
19
+ else
20
+ enum_for { @backends.length }
21
+ end
22
+ end
23
+
24
+ def push(name, backend)
25
+ exists?(name)
26
+ @backends.push([name, backend])
27
+ end
28
+ alias use push
29
+
30
+ def unshift(name, backend)
31
+ exists?(name)
32
+ @backends.unshift([name, backend])
33
+ end
34
+
35
+ def insert_before(other, name, backend)
36
+ exists?(name)
37
+ @backends.insert(index_of!(other), [name, backend])
38
+ end
39
+
40
+ def insert_after(other, name, backend)
41
+ exists?(name)
42
+ @backends.insert(index_of!(other) + 1, [name, backend])
43
+ end
44
+
45
+ def delete(name)
46
+ @backends.delete_at(index_of!(name))
47
+ end
48
+
49
+ def fresh?
50
+ @backends.all? { |(_name, backend)| backend.fresh? }
51
+ end
52
+
53
+ private
54
+
55
+ def exists?(name)
56
+ raise KeyError, "#{name} is already set" if index_of(name)
57
+ end
58
+
59
+ def index_of!(name)
60
+ index_of(name) or raise KeyError, "#{name} is not set"
61
+ end
62
+
63
+ def index_of(name)
64
+ @backends.index(@backends.assoc(name))
65
+ end
66
+ end
67
+ end