blueprint_config 1.0.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 (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
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'singleton'
4
+ require 'blueprint_config/options_hash'
5
+ require 'blueprint_config/options_array'
6
+
7
+ module BlueprintConfig
8
+ class Configuration
9
+ include Singleton
10
+
11
+ attr_accessor :config, :backends
12
+
13
+ %i[dig dig! fetch \[\] method_missing].each do |method|
14
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
15
+ def #{method}(...)
16
+ reload! unless backends&.fresh?
17
+ config.#{method}(...)
18
+ rescue KeyError => e
19
+ raise KeyError, e.message, caller[1..], cause: nil
20
+ end
21
+ RUBY
22
+ end
23
+
24
+ def init(&block)
25
+ backends = BackendCollection.new
26
+ block.call(backends)
27
+ @backends = backends
28
+ reload!
29
+ end
30
+
31
+ def refine(&block)
32
+ backends = @backends
33
+ block.call(backends)
34
+ @backends = backends
35
+ reload!
36
+ end
37
+
38
+ def reload!
39
+ new_config = @backends.each_with_object(OptionsHash.new) do |backend, config|
40
+ config.deep_merge! OptionsHash.new(backend.load_keys, source: backend.source)
41
+ end
42
+
43
+ @config = new_config
44
+ @config = process_erb(new_config)
45
+ end
46
+
47
+ def process_erb(object)
48
+ case object
49
+ when String
50
+ if object.start_with?('<%=') && object.end_with?('%>')
51
+ ERB.new(object).result(binding)
52
+ else
53
+ object
54
+ end
55
+ when OptionsArray
56
+ object.each_with_index { |o, index| object[index] = process_erb(o) }
57
+ when OptionsHash
58
+ object.each { |k, v| object[k] = process_erb(v) }
59
+ else
60
+ object
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BlueprintConfig
4
+ class OptionsArray < Array
5
+ attr_accessor :__sources, :__indeces, :__path
6
+
7
+ def initialize(options = [], path: nil, source: nil)
8
+ super() # important - brackets needed to create an empty array
9
+ @__path = path
10
+ @__sources = []
11
+ @__indeces = []
12
+ options.each do |elem|
13
+ __push(elem, source:)
14
+ end
15
+ end
16
+
17
+ def __assign(other)
18
+ if other.first.to_s == '__append'
19
+ concat other[1..]
20
+ @__sources.concat other.__sources[1..]
21
+ @__indeces.concat other.__indeces[1..]
22
+ else
23
+ clear
24
+ concat(other)
25
+ __sources.clear
26
+ __sources.concat(other.__sources)
27
+ __indeces.clear
28
+ __indeces.concat(other.__indeces)
29
+ end
30
+ self
31
+ end
32
+
33
+ def dig(key, *identifiers)
34
+ super(key, *identifiers)
35
+ end
36
+
37
+ def merge!(other, &block)
38
+ @__sources.reverse_merge!(other.__sources) if other.is_a?(OptionsHash)
39
+ super
40
+ end
41
+
42
+ def __push(elem, source: nil)
43
+ elem = OptionsHash.new(elem, path: [@__path, size].compact.join('.'), source:) if elem.is_a?(Hash)
44
+ elem = self.class.new(elem, path: [@__path, size].compact.join('.'), source:) if elem.is_a?(Array)
45
+ @__sources.push source
46
+ @__indeces.push size
47
+ push elem
48
+ end
49
+
50
+ def source(*args)
51
+ index = args.shift
52
+ if index >= size
53
+ raise IndexError, "Configuration key '#{[@__path, index].compact.join('.')}' is not set", caller[1..],
54
+ cause: nil
55
+ end
56
+
57
+ if args.empty?
58
+ "#{@__sources[index]} #{[@__path, @__indeces[index]].compact.join('.')}"
59
+ else
60
+ self[index].source(*args)
61
+ end
62
+ end
63
+
64
+ def dig!(*args, &block)
65
+ leading = args.shift
66
+ if args.empty?
67
+ fetch(leading, &block)
68
+ else
69
+ fetch(leading, &block).dig!(*args, &block)
70
+ end
71
+ rescue IndexError => e
72
+ raise e, e.message, caller[1..], cause: nil
73
+ end
74
+
75
+ def fetch(index, *args, &block)
76
+ super(index, *args, &block)
77
+ rescue IndexError
78
+ raise IndexError, "Configuration key '#{[@__path, index].compact.join('.')}' is not set", caller[1..], cause: nil
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,112 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BlueprintConfig
4
+ class OptionsHash < Hash
5
+ # include ActiveSupport::DeepMergeable
6
+
7
+ attr_accessor :__sources, :__path
8
+
9
+ def initialize(options = {}, path: nil, source: nil)
10
+ super() # important - brackets needed to create an empty hash
11
+ @__path = path
12
+ @__sources = {}
13
+ options.each do |key, value|
14
+ __set(key, value, source:)
15
+ end
16
+ end
17
+
18
+ def [](key)
19
+ super(key.to_sym)
20
+ end
21
+
22
+ def dig(key, *identifiers)
23
+ super(key.to_sym, *identifiers)
24
+ end
25
+
26
+ def merge!(other, &block)
27
+ @__sources.reverse_merge!(other.__sources) if other.is_a?(OptionsHash)
28
+ super
29
+ end
30
+
31
+ def __set(key, value, source: nil)
32
+ value = self.class.new(value, path: [@__path, key].compact.join('.'), source:) if value.is_a?(Hash)
33
+ value = OptionsArray.new(value, path: [@__path, key].compact.join('.'), source:) if value.is_a?(Array)
34
+ @__sources[key] = source
35
+ self[key.to_sym] = value
36
+ end
37
+
38
+ def source(*args)
39
+ key = args.shift
40
+ if args.empty?
41
+ "#{@__sources[key]} #{[@__path, key].compact.join('.')}"
42
+ else
43
+ unless key?(key)
44
+ raise KeyError, "Configuration key '#{[@__path, key].compact.join('.')}' is not set", caller[1..], cause: nil
45
+ end
46
+
47
+ self[key].source(*args)
48
+ end
49
+ end
50
+
51
+ def dig!(*args, &block)
52
+ leading = args.shift
53
+ if args.empty?
54
+ fetch(leading, &block)
55
+ else
56
+ fetch(leading, &block).dig!(*args, &block)
57
+ end
58
+ rescue KeyError => e
59
+ raise e, e.message, caller[1..], cause: nil
60
+ end
61
+
62
+ def method_missing(name, *args)
63
+ name_string = +name.to_s
64
+ if name_string.chomp!('=')
65
+ self[name_string] = args.first
66
+ else
67
+ questions = name_string.chomp!('?')
68
+ if questions
69
+ self[name_string].present?
70
+ else
71
+ bangs = name_string.chomp!('!')
72
+
73
+ if bangs
74
+ self[name_string].presence ||
75
+ raise(KeyError, "Configuration key '#{[@__path, name_string].compact.join('.')}' is not set", caller[1..])
76
+ else
77
+ self[name_string]
78
+ end
79
+ end
80
+ end
81
+ end
82
+
83
+ def fetch(key, *args, &block)
84
+ super(key.to_sym, *args, &block)
85
+ rescue KeyError
86
+ raise KeyError, "Configuration key '#{[@__path, key].compact.join('.')}' is not set", caller[1..], cause: nil
87
+ end
88
+
89
+ # deep_merge methods copied from ActiveSupport to avoid extra dependency
90
+ def deep_merge(other, &block)
91
+ dup.deep_merge!(other, &block)
92
+ end
93
+
94
+ def deep_merge!(other, &block)
95
+ merge!(other) do |key, this_val, other_val|
96
+ if this_val.is_a?(BlueprintConfig::OptionsHash) && this_val.deep_merge?(other_val)
97
+ this_val.deep_merge(other_val, &block)
98
+ elsif this_val.is_a?(BlueprintConfig::OptionsArray) && other_val.is_a?(BlueprintConfig::OptionsArray)
99
+ this_val.__assign(other_val, &block)
100
+ elsif block_given?
101
+ block.call(key, this_val, other_val)
102
+ else
103
+ other_val
104
+ end
105
+ end
106
+ end
107
+
108
+ def deep_merge?(other)
109
+ other.is_a?(self.class)
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_record'
4
+
5
+ module BlueprintConfig
6
+ class Setting < ::ActiveRecord::Base
7
+ self.inheritance_column = nil
8
+
9
+ enum type: { section: 0, string: 1, integer: 2, boolean: 3, json: 4, selection: 5, set: 6 }
10
+
11
+ def parsed_json_value
12
+ parsed = begin
13
+ JSON.parse(value)
14
+ rescue StandardError
15
+ nil
16
+ end
17
+ parsed.is_a?(Hash) ? parsed.with_indifferent_access : parsed
18
+ end
19
+
20
+ def parsed_value
21
+ return value.to_i if integer?
22
+ return value.to_b if boolean?
23
+ return parsed_json_value if json? || set?
24
+
25
+ value
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BlueprintConfig
4
+ VERSION = '1.0.0'
5
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'yaml'
4
+ require 'erb'
5
+
6
+ module Econfig
7
+ class YAML
8
+ def initialize(path)
9
+ @path = path
10
+ @mutex = Mutex.new
11
+ @options = nil
12
+ end
13
+
14
+ def keys
15
+ Set.new(options.keys)
16
+ end
17
+
18
+ def get(key)
19
+ options[key]
20
+ end
21
+
22
+ def has_key?(key)
23
+ options.key?(key)
24
+ end
25
+
26
+ private
27
+
28
+ def path
29
+ raise Econfig::UninitializedError, 'Econfig.root is not set' unless Econfig.root
30
+
31
+ File.expand_path(@path, Econfig.root)
32
+ end
33
+
34
+ def options
35
+ return @options if @options
36
+
37
+ @mutex.synchronize do
38
+ @options ||= if File.exist?(path)
39
+ ::YAML.load(::ERB.new(File.read(path)).result)[Econfig.env] || {}
40
+ else
41
+ {}
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'blueprint_config/version'
4
+ require 'blueprint_config/backend/env'
5
+ require 'blueprint_config/backend/yaml'
6
+ require 'blueprint_config/configuration'
7
+ require 'blueprint_config/backend_collection'
8
+
9
+ module BlueprintConfig
10
+ class << self
11
+ attr_accessor :root, :env, :before_initialize, :after_initialize
12
+ attr_writer :shortcut_name, :env_options
13
+
14
+ def shortcut_name
15
+ @shortcut_name || 'AppConfig'
16
+ end
17
+
18
+ def env_options
19
+ @env_options || {}
20
+ end
21
+
22
+ def define_shortcut
23
+ Object.const_set shortcut_name, instance
24
+ end
25
+
26
+ def instance
27
+ BlueprintConfig::Configuration.instance
28
+ end
29
+
30
+ def init
31
+ before_initialize&.call
32
+ end
33
+
34
+ def configure_rails(config)
35
+ config.before_configuration do |_app|
36
+ BlueprintConfig.root ||= Rails.root
37
+ BlueprintConfig.env ||= Rails.env
38
+ BlueprintConfig.define_shortcut
39
+ BlueprintConfig.before_initialize.call
40
+ end
41
+
42
+ config.after_initialize do |_app|
43
+ BlueprintConfig.after_initialize.call
44
+ end
45
+ end
46
+ end
47
+ end
48
+
49
+ BlueprintConfig.env_options ||= {}
50
+
51
+ BlueprintConfig.before_initialize ||= proc do
52
+ require 'blueprint_config/backend/credentials'
53
+ require 'blueprint_config/backend/active_record'
54
+
55
+ BlueprintConfig.instance.init do |backends|
56
+ backends.use :app, BlueprintConfig::Backend::YAML.new('config/app.yml')
57
+ backends.use :credentials, BlueprintConfig::Backend::Credentials.new
58
+ backends.use :env, BlueprintConfig::Backend::ENV.new(BlueprintConfig.env_options)
59
+ backends.use :app_local, BlueprintConfig::Backend::YAML.new('config/app.local.yml')
60
+ end
61
+ end
62
+
63
+ BlueprintConfig.after_initialize ||= proc do
64
+ BlueprintConfig.instance.refine do |backends|
65
+ if backends[:env]
66
+ backends.insert_after :env, :db, BlueprintConfig::Backend::ActiveRecord.new
67
+ else
68
+ backends.push :db, BlueprintConfig::Backend::ActiveRecord.new
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,8 @@
1
+ Description:
2
+ Generate a migration for config settings in ActiveRecord
3
+
4
+ Example:
5
+ rails generate blueprint:install
6
+
7
+ This will create:
8
+ db/migrate/20240107123730_create_blueprint_settings.rb
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails/generators'
4
+ require 'rails/generators/active_record'
5
+
6
+ module BlueprintConfig
7
+ class InstallGenerator < Rails::Generators::Base
8
+ include ActiveRecord::Generators::Migration
9
+
10
+ TEMPLATES = File.join(File.dirname(__FILE__), 'templates')
11
+ source_paths << TEMPLATES
12
+
13
+ def create_migration_file
14
+ migration_template 'migration.rb.erb', 'db/migrate/create_blueprint_settings.rb'
15
+ end
16
+
17
+ private
18
+
19
+ def migration_version
20
+ "[#{ActiveRecord::VERSION::STRING.to_f}]"
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,18 @@
1
+ class CreateBlueConfig < ActiveRecord::Migration<%= migration_version %>
2
+ def change
3
+ reversible do |dir|
4
+ dir.up do
5
+ # Ensure this incremental update migration is idempotent
6
+ # with monolithic install migration.
7
+ return if connection.table_exists?(:settings)
8
+ end
9
+ end
10
+
11
+ create_table :settings, id: :uuid do |t|
12
+ t.string :key, null: false, index: { unique: true }
13
+ t.integer :type, null: false, default: 0
14
+ t.string :value
15
+ t.timestamps
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,103 @@
1
+ # frozen_string_literal: true
2
+
3
+ describe BlueprintConfig::BackendCollection do
4
+ let(:collection) { described_class.new }
5
+ let(:memory) { double('memory') }
6
+ let(:yaml) { double('yaml') }
7
+
8
+ describe '#[]' do
9
+ it 'retrieves a backend' do
10
+ collection.push :memory, memory
11
+ collection[:memory].should equal(memory)
12
+ end
13
+ end
14
+
15
+ describe '#each' do
16
+ it 'can be called without a block to receive an enumerator' do
17
+ collection.push :memory, memory
18
+ collection.each.take(1).should eq([memory])
19
+ end
20
+ end
21
+
22
+ describe '#push' do
23
+ it 'adds a new backend at the bottom' do
24
+ collection.push :memory, memory
25
+ collection.push :yaml, yaml
26
+ collection.to_a.should eq([memory, yaml])
27
+ end
28
+
29
+ it 'is aliased as `use`' do
30
+ collection.use :memory, memory
31
+ collection.use :yaml, yaml
32
+ collection.to_a.should eq([memory, yaml])
33
+ end
34
+
35
+ it 'raises an error if backend already exist' do
36
+ collection.push :memory, memory
37
+ expect { collection.push :memory, yaml }.to raise_error(KeyError, 'memory is already set')
38
+ end
39
+ end
40
+
41
+ describe '#unshift' do
42
+ it 'adds a new backend at the top' do
43
+ collection.unshift :memory, memory
44
+ collection.unshift :yaml, yaml
45
+ collection.to_a.should eq([yaml, memory])
46
+ end
47
+
48
+ it 'raises an error if backend already exist' do
49
+ collection.unshift :memory, memory
50
+ expect { collection.unshift :memory, yaml }.to raise_error(KeyError, 'memory is already set')
51
+ end
52
+ end
53
+
54
+ describe '#insert_before' do
55
+ it 'adds a new before the given backend' do
56
+ collection.push :quox, :quox
57
+ collection.push :baz, :baz
58
+ collection.push :foo, :foo
59
+ collection.insert_before :baz, :memory, memory
60
+ collection.to_a.should eq([:quox, memory, :baz, :foo])
61
+ end
62
+
63
+ it 'raises an error if the backend does not exist' do
64
+ expect { collection.insert_before :baz, :memory, memory }.to raise_error(KeyError, /baz is not set/)
65
+ end
66
+
67
+ it 'raises an error if the backend already exists' do
68
+ collection.push :foo, :foo
69
+ expect { collection.insert_before :foo, :foo, memory }.to raise_error(KeyError, /foo is already set/)
70
+ end
71
+ end
72
+
73
+ describe '#insert_after' do
74
+ it 'adds a new after the given backend' do
75
+ collection.push :quox, :quox
76
+ collection.push :baz, :baz
77
+ collection.push :foo, :foo
78
+ collection.insert_after :baz, :memory, memory
79
+ collection.to_a.should eq([:quox, :baz, memory, :foo])
80
+ end
81
+
82
+ it 'raises an error if the backend does not exist' do
83
+ expect { collection.insert_after :baz, :memory, memory }.to raise_error(KeyError, /baz is not set/)
84
+ end
85
+
86
+ it 'raises an error if the backend already exists' do
87
+ collection.push :foo, :foo
88
+ expect { collection.insert_after :foo, :foo, memory }.to raise_error(KeyError, /foo is already set/)
89
+ end
90
+ end
91
+
92
+ describe '#delete' do
93
+ it 'removes the given backend' do
94
+ collection.push :memory, memory
95
+ collection.delete :memory
96
+ collection[:memory].should be_nil
97
+ end
98
+
99
+ it 'raises an error if the backend does not exist' do
100
+ expect { collection.delete :redis }.to raise_error(KeyError, /redis is not set/)
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_record'
4
+ require 'blueprint_config/backend/active_record'
5
+
6
+ ActiveRecord::Base.establish_connection adapter: 'sqlite3', database: ':memory:'
7
+
8
+ ActiveRecord::Base.connection.create_table :settings do |t|
9
+ t.string :key, null: false, index: { unique: true }
10
+ t.integer :type, null: false, default: 0
11
+ t.string :value
12
+ t.timestamps
13
+ end
14
+
15
+ describe BlueprintConfig::Backend::ActiveRecord do
16
+ let(:options) { {} }
17
+ let(:subject) { described_class.new(options).load_keys }
18
+ around do |example|
19
+ ActiveRecord::Base.transaction do
20
+ BlueprintConfig::Setting.create(key: 'foo', type: :string, value: 'bar')
21
+ BlueprintConfig::Setting.create(key: 'x', type: :integer, value: '1')
22
+ BlueprintConfig::Setting.create(key: 'a.b', type: :string, value: '1')
23
+
24
+ example.run
25
+ raise ActiveRecord::Rollback
26
+ end
27
+ end
28
+
29
+ context 'with default options' do
30
+ it 'loads all keys' do
31
+ expect(subject).to eq({ foo: 'bar', "a.b": '1', x: 1 })
32
+ end
33
+ end
34
+
35
+ context 'when nesting enabled' do
36
+ let(:options) { { nest: true } }
37
+ it 'loads all keys' do
38
+ expect(subject).to eq({ foo: 'bar', a: { b: '1' }, x: 1 })
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe BlueprintConfig::Backend::ENV do
4
+ let(:options) { {} }
5
+ let(:subject) { BlueprintConfig::Backend::ENV.new(options).load_keys }
6
+
7
+ before do
8
+ ENV['FOO_BAR'] = 'monkey'
9
+ ENV['TEST'] = 'test'
10
+ end
11
+
12
+ context 'when everything is allowed', :aggregate_failures do
13
+ let(:options) { { allow_all: true } }
14
+ it 'copies all env variables' do
15
+ expect(subject[:foo_bar]).to eq('monkey')
16
+ end
17
+ end
18
+
19
+ context 'when everything is allowed and keys are nested', :aggregate_failures do
20
+ let(:options) { { allow_all: true, nest: true } }
21
+ it 'copies all env variables nesting em' do
22
+ expect(subject[:foo][:bar]).to eq('monkey')
23
+ end
24
+ end
25
+
26
+ context 'when keys are whitelisted', :aggregate_failures do
27
+ let(:options) { { whitelist_keys: [:test] } }
28
+ it 'copies whitelisted keys' do
29
+ expect(subject.keys).to eq([:test])
30
+ end
31
+ end
32
+
33
+ context 'when keys are prefix-whitelisted', :aggregate_failures do
34
+ let(:options) { { whitelist_prefixes: %i[foo f] } }
35
+ it 'copies whitelisted keys' do
36
+ expect(subject.keys).to eq([:foo_bar])
37
+ end
38
+ end
39
+
40
+ context 'when keys are prefix-whitelisted and', :aggregate_failures do
41
+ let(:options) { { whitelist_prefixes: %i[foo f], nest: true } }
42
+ it 'copies whitelisted keys' do
43
+ expect(subject.keys).to eq([:foo])
44
+ end
45
+ end
46
+
47
+ context 'when combines prefix-whitelist and whitelist', :aggregate_failures do
48
+ let(:options) { { whitelist_prefixes: [:foo], whitelist_keys: [:test] } }
49
+ it 'copies whitelisted keys' do
50
+ expect(subject.keys).to eq(%i[test foo_bar])
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ describe BlueprintConfig::Backend::YAML do
4
+ let(:subject) { described_class.new('config/app.yml').load_keys }
5
+
6
+ context 'when file exists' do
7
+ context 'when env is not set' do
8
+ it 'returns default section' do
9
+ allow(BlueprintConfig).to receive(:env).and_return(nil)
10
+ expect(subject).to eq(
11
+ {
12
+ array: %w[a b x],
13
+ array2: [{ a: { d: 1, e: 2 }, b: 2 }, { b: 1, c: 3 }, { x: 4, y: 5 }],
14
+ nested: { a: 1, b: 2 }
15
+ }
16
+ )
17
+ end
18
+ end
19
+
20
+ context 'when env is set' do
21
+ it 'returns default section merged with env section' do
22
+ allow(BlueprintConfig).to receive(:env).and_return('test')
23
+ expect(subject).to eq(
24
+ {
25
+ array: %w[a b x],
26
+ array2: [{ a: { d: 1, e: 2 }, b: 2 }, { b: 1, c: 3 }, { x: 4, y: 5 }],
27
+ nested: { a: 3, b: 2, c: 4 },
28
+ quox: 'baz',
29
+ envir: "<%= ENV['APP_EXAMPLE'] %>"
30
+ }
31
+ )
32
+ end
33
+ end
34
+ end
35
+ end