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.
- checksums.yaml +7 -0
- data/.gitignore +19 -0
- data/.idea/.gitignore +8 -0
- data/.idea/blue_config.iml +72 -0
- data/.idea/misc.xml +4 -0
- data/.idea/modules.xml +8 -0
- data/.idea/vcs.xml +6 -0
- data/.rspec +1 -0
- data/.ruby-version +1 -0
- data/Gemfile +6 -0
- data/LICENSE.txt +22 -0
- data/README.md +152 -0
- data/Rakefile +8 -0
- data/blueprint_config.gemspec +31 -0
- data/lib/blueprint_config/backend/active_record.rb +51 -0
- data/lib/blueprint_config/backend/base.rb +34 -0
- data/lib/blueprint_config/backend/credentials.rb +26 -0
- data/lib/blueprint_config/backend/env.rb +47 -0
- data/lib/blueprint_config/backend/yaml.rb +30 -0
- data/lib/blueprint_config/backend_collection.rb +67 -0
- data/lib/blueprint_config/configuration.rb +64 -0
- data/lib/blueprint_config/options_array.rb +81 -0
- data/lib/blueprint_config/options_hash.rb +112 -0
- data/lib/blueprint_config/setting.rb +28 -0
- data/lib/blueprint_config/version.rb +5 -0
- data/lib/blueprint_config/yaml.rb +46 -0
- data/lib/blueprint_config.rb +71 -0
- data/lib/generators/blueprint_config/install/USAGE +8 -0
- data/lib/generators/blueprint_config/install/install_generator.rb +23 -0
- data/lib/generators/blueprint_config/install/templates/migration.rb.erb +18 -0
- data/spec/backend_collection_spec.rb +103 -0
- data/spec/blueprint_config/backend/active_record_spec.rb +41 -0
- data/spec/blueprint_config/backend/env_spec.rb +53 -0
- data/spec/blueprint_config/backend/yaml_spec.rb +35 -0
- data/spec/blueprint_config/options_array_spec.rb +109 -0
- data/spec/blueprint_config/options_hash_spec.rb +211 -0
- data/spec/config/app.yml +24 -0
- data/spec/configuration_spec.rb +98 -0
- data/spec/spec_helper.rb +16 -0
- 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,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,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
|