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
@@ -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