cconfig 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +2 -0
- data/.rspec +1 -0
- data/.rubocop.yml +31 -0
- data/.ruby-version +1 -0
- data/.travis.yml +22 -0
- data/CHANGELOG.md +14 -0
- data/CONTRIBUTING.md +14 -0
- data/COPYING +674 -0
- data/COPYING.LESSER +165 -0
- data/Gemfile +3 -0
- data/Gemfile.lock +57 -0
- data/README.md +139 -0
- data/Rakefile +24 -0
- data/cconfig.gemspec +31 -0
- data/config/rubocop-suse.yml +74 -0
- data/lib/cconfig.rb +25 -0
- data/lib/cconfig/cconfig.rb +73 -0
- data/lib/cconfig/errors.rb +30 -0
- data/lib/cconfig/hash_utils.rb +120 -0
- data/lib/cconfig/railtie.rb +47 -0
- data/lib/cconfig/version.rb +23 -0
- data/lib/tasks/info.rake +32 -0
- data/spec/config_spec.rb +87 -0
- data/spec/fixtures/bad.yml +1 -0
- data/spec/fixtures/config.yml +16 -0
- data/spec/fixtures/local.yml +7 -0
- data/spec/hash_utils_spec.rb +68 -0
- data/spec/spec_helper.rb +32 -0
- metadata +162 -0
data/lib/cconfig.rb
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
|
3
|
+
# Copyright (C) 2017 Miquel Sabaté Solà <msabate@suse.com>
|
4
|
+
#
|
5
|
+
# This file is part of CConfig.
|
6
|
+
#
|
7
|
+
# CConfig is free software: you can redistribute it and/or modify
|
8
|
+
# it under the terms of the GNU Lesser General Public License as published by
|
9
|
+
# the Free Software Foundation, either version 3 of the License, or
|
10
|
+
# (at your option) any later version.
|
11
|
+
#
|
12
|
+
# CConfig is distributed in the hope that it will be useful,
|
13
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
14
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
15
|
+
# GNU General Public License for more details.
|
16
|
+
#
|
17
|
+
# You should have received a copy of the GNU Lesser General Public License
|
18
|
+
# along with CConfig. If not, see <http://www.gnu.org/licenses/>.
|
19
|
+
|
20
|
+
# CConfig contains all the classes and modules that implement this library.
|
21
|
+
module CConfig
|
22
|
+
end
|
23
|
+
|
24
|
+
require "cconfig/cconfig"
|
25
|
+
require "cconfig/railtie" if defined?(Rails)
|
@@ -0,0 +1,73 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
|
3
|
+
# Copyright (C) 2017 Miquel Sabaté Solà <msabate@suse.com>
|
4
|
+
#
|
5
|
+
# This file is part of CConfig.
|
6
|
+
#
|
7
|
+
# CConfig is free software: you can redistribute it and/or modify
|
8
|
+
# it under the terms of the GNU Lesser General Public License as published by
|
9
|
+
# the Free Software Foundation, either version 3 of the License, or
|
10
|
+
# (at your option) any later version.
|
11
|
+
#
|
12
|
+
# CConfig is distributed in the hope that it will be useful,
|
13
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
14
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
15
|
+
# GNU General Public License for more details.
|
16
|
+
#
|
17
|
+
# You should have received a copy of the GNU Lesser General Public License
|
18
|
+
# along with CConfig. If not, see <http://www.gnu.org/licenses/>.
|
19
|
+
|
20
|
+
require "cconfig/hash_utils"
|
21
|
+
require "cconfig/errors"
|
22
|
+
require "yaml"
|
23
|
+
|
24
|
+
module CConfig
|
25
|
+
# Config is the main class of this library. It allows you to fetch the current
|
26
|
+
# configuration (after merging the values from all sources) as a hash. This
|
27
|
+
# has will have the special method `::CConfig::HashUtils::Extensions#enabled?`.
|
28
|
+
class Config
|
29
|
+
include ::CConfig::HashUtils
|
30
|
+
|
31
|
+
# Instantiate an object with `default` as the path to the default
|
32
|
+
# configuration, `local` as the alternate file, and `prefix` as the prefix
|
33
|
+
# for environment variables.
|
34
|
+
#
|
35
|
+
# Note: the `local` value will be discarded in favor of the
|
36
|
+
# `#{prefix}_LOCAL_CONFIG_PATH` environment variable if it was set.
|
37
|
+
def initialize(default:, local:, prefix:)
|
38
|
+
@default = default
|
39
|
+
@local = ENV["#{prefix.upcase}_LOCAL_CONFIG_PATH"] || local
|
40
|
+
@prefix = prefix
|
41
|
+
end
|
42
|
+
|
43
|
+
# Returns a hash with the app configuration contained in it.
|
44
|
+
def fetch
|
45
|
+
cfg = {}
|
46
|
+
cfg = YAML.load_file(@default) if File.file?(@default)
|
47
|
+
local = fetch_local
|
48
|
+
|
49
|
+
hsh = strict_merge_with_env(default: cfg, local: local, prefix: @prefix)
|
50
|
+
hsh.extend(::CConfig::HashUtils::Extensions)
|
51
|
+
end
|
52
|
+
|
53
|
+
# Returns a string representation of the evaluated configuration.
|
54
|
+
def to_s
|
55
|
+
hide_password(fetch.dup).to_yaml
|
56
|
+
end
|
57
|
+
|
58
|
+
protected
|
59
|
+
|
60
|
+
# Returns a hash with the alternate values that have to override the default
|
61
|
+
# ones.
|
62
|
+
def fetch_local
|
63
|
+
if File.file?(@local)
|
64
|
+
# Check for bad user input in the local config.yml file.
|
65
|
+
local = YAML.load_file(@local)
|
66
|
+
raise FormatError unless local.is_a?(::Hash)
|
67
|
+
local
|
68
|
+
else
|
69
|
+
{}
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
|
3
|
+
# Copyright (C) 2017 Miquel Sabaté Solà <msabate@suse.com>
|
4
|
+
#
|
5
|
+
# This file is part of CConfig.
|
6
|
+
#
|
7
|
+
# CConfig is free software: you can redistribute it and/or modify
|
8
|
+
# it under the terms of the GNU Lesser General Public License as published by
|
9
|
+
# the Free Software Foundation, either version 3 of the License, or
|
10
|
+
# (at your option) any later version.
|
11
|
+
#
|
12
|
+
# CConfig is distributed in the hope that it will be useful,
|
13
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
14
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
15
|
+
# GNU General Public License for more details.
|
16
|
+
#
|
17
|
+
# You should have received a copy of the GNU Lesser General Public License
|
18
|
+
# along with CConfig. If not, see <http://www.gnu.org/licenses/>.
|
19
|
+
|
20
|
+
module CConfig
|
21
|
+
# FormatError is the exception to be raised when a configuration file cannot
|
22
|
+
# be parsed.
|
23
|
+
class FormatError < StandardError
|
24
|
+
DEFAULT_MSG = "Wrong format for the config-local file!".freeze
|
25
|
+
|
26
|
+
def initialize
|
27
|
+
super(DEFAULT_MSG)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,120 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
|
3
|
+
# Copyright (C) 2017 Miquel Sabaté Solà <msabate@suse.com>
|
4
|
+
#
|
5
|
+
# This file is part of CConfig.
|
6
|
+
#
|
7
|
+
# CConfig is free software: you can redistribute it and/or modify
|
8
|
+
# it under the terms of the GNU Lesser General Public License as published by
|
9
|
+
# the Free Software Foundation, either version 3 of the License, or
|
10
|
+
# (at your option) any later version.
|
11
|
+
#
|
12
|
+
# CConfig is distributed in the hope that it will be useful,
|
13
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
14
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
15
|
+
# GNU General Public License for more details.
|
16
|
+
#
|
17
|
+
# You should have received a copy of the GNU Lesser General Public License
|
18
|
+
# along with CConfig. If not, see <http://www.gnu.org/licenses/>.
|
19
|
+
|
20
|
+
module CConfig
|
21
|
+
# HashUtils provides a handful of methods that help with the manipulation of
|
22
|
+
# hashes in the gem.
|
23
|
+
module HashUtils
|
24
|
+
# Extensions contains the methods to be provided for each hash object
|
25
|
+
# produced by this gem.
|
26
|
+
module Extensions
|
27
|
+
# Returns true if the given feature is enabled, false otherwise. This also
|
28
|
+
# works in embedded configuration values. For example: enabled?("a.b")
|
29
|
+
# will return true for:
|
30
|
+
# a:
|
31
|
+
# b:
|
32
|
+
# enabled: true
|
33
|
+
def enabled?(feature)
|
34
|
+
objs = feature.split(".")
|
35
|
+
if objs.length == 2
|
36
|
+
return false if !self[objs[0]][objs[1]] || self[objs[0]][objs[1]].empty?
|
37
|
+
self[objs[0]][objs[1]]["enabled"].eql?(true)
|
38
|
+
else
|
39
|
+
return false if !self[feature] || self[feature].empty?
|
40
|
+
self[feature]["enabled"].eql?(true)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
protected
|
46
|
+
|
47
|
+
# Applies a deep merge while respecting the values from environment
|
48
|
+
# variables. A deep merge consists of a merge of all the nested elements of
|
49
|
+
# the two given hashes `config` and `local`. The `config` hash is supposed
|
50
|
+
# to contain all the accepted keys, and the `local` hash is a subset of it.
|
51
|
+
#
|
52
|
+
# Moreover, let's say that we have the following hash: { "ldap" => {
|
53
|
+
# "enabled" => true } }. An environment variable that can modify the value
|
54
|
+
# of the previous hash has to be named `#{prefix}_LDAP_ENABLED`. The `prefix`
|
55
|
+
# argument specifies how all the environment variables have to start.
|
56
|
+
#
|
57
|
+
# Returns the merged hash, where the precedence of the merge is as follows:
|
58
|
+
# 1. The value of the related environment variable if set.
|
59
|
+
# 2. The value from the `local` hash.
|
60
|
+
# 3. The value from the `config` hash.
|
61
|
+
def strict_merge_with_env(default:, local:, prefix:)
|
62
|
+
hsh = {}
|
63
|
+
|
64
|
+
default.each do |k, v|
|
65
|
+
# The corresponding environment variable. If it's not the final value,
|
66
|
+
# then this just contains the partial prefix of the env. variable.
|
67
|
+
env = "#{prefix}_#{k}"
|
68
|
+
|
69
|
+
# If the current value is a hash, then go deeper to perform a deep
|
70
|
+
# merge, otherwise we merge the final value by respecting the order as
|
71
|
+
# specified in the documentation.
|
72
|
+
if v.is_a?(Hash)
|
73
|
+
l = local[k] || {}
|
74
|
+
hsh[k] = strict_merge_with_env(default: default[k], local: l, prefix: env)
|
75
|
+
else
|
76
|
+
hsh[k] = first_non_nil(get_env(env), local[k], v)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
hsh
|
80
|
+
end
|
81
|
+
|
82
|
+
# Hide any sensitive value, replacing it with "*" characters.
|
83
|
+
def hide_password(hsh)
|
84
|
+
hsh.each do |k, v|
|
85
|
+
if v.is_a?(Hash)
|
86
|
+
hsh[k] = hide_password(v)
|
87
|
+
elsif k == "password"
|
88
|
+
hsh[k] = "****"
|
89
|
+
end
|
90
|
+
end
|
91
|
+
hsh
|
92
|
+
end
|
93
|
+
|
94
|
+
private
|
95
|
+
|
96
|
+
# Get the typed value of the specified environment variable. If it doesn't
|
97
|
+
# exist, it will return nil. Otherwise, it will try to cast the fetched
|
98
|
+
# value into the proper type and return it.
|
99
|
+
def get_env(key)
|
100
|
+
env = ENV[key.upcase]
|
101
|
+
return nil if env.nil?
|
102
|
+
|
103
|
+
# Try to convert it into a boolean value.
|
104
|
+
return true if env.casecmp("true").zero?
|
105
|
+
return false if env.casecmp("false").zero?
|
106
|
+
|
107
|
+
# Try to convert it into an integer. Otherwise just keep the string.
|
108
|
+
begin
|
109
|
+
Integer(env)
|
110
|
+
rescue ArgumentError
|
111
|
+
env
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
# Returns the first value that is not nil from the given argument list.
|
116
|
+
def first_non_nil(*values)
|
117
|
+
values.each { |v| return v unless v.nil? }
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
|
3
|
+
# Copyright (C) 2017 Miquel Sabaté Solà <msabate@suse.com>
|
4
|
+
#
|
5
|
+
# This file is part of CConfig.
|
6
|
+
#
|
7
|
+
# CConfig is free software: you can redistribute it and/or modify
|
8
|
+
# it under the terms of the GNU Lesser General Public License as published by
|
9
|
+
# the Free Software Foundation, either version 3 of the License, or
|
10
|
+
# (at your option) any later version.
|
11
|
+
#
|
12
|
+
# CConfig is distributed in the hope that it will be useful,
|
13
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
14
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
15
|
+
# GNU General Public License for more details.
|
16
|
+
#
|
17
|
+
# You should have received a copy of the GNU Lesser General Public License
|
18
|
+
# along with CConfig. If not, see <http://www.gnu.org/licenses/>.
|
19
|
+
|
20
|
+
require "rails"
|
21
|
+
|
22
|
+
module CConfig
|
23
|
+
# This class will set up this gem for Ruby on Rails:
|
24
|
+
# - On initialization this Railtie will set the `APP_CONFIG` global
|
25
|
+
# constant with the resulting merged values of the configuration.
|
26
|
+
# - The `cconfig:info` rake task will be loaded.
|
27
|
+
class Railtie < Rails::Railtie
|
28
|
+
railtie_name :cconfig
|
29
|
+
|
30
|
+
initializer "cconfig" do |app|
|
31
|
+
prefix = ENV["CCONFIG_PREFIX"] || app.class.parent_name.inspect
|
32
|
+
default = File.join(Rails.root, "config", "config.yml")
|
33
|
+
local = File.join(Rails.root, "config", "config-local.yml")
|
34
|
+
cfg = ::CConfig::Config.new(default: default, local: local, prefix: prefix)
|
35
|
+
|
36
|
+
# NOTE: this is a global constant from now on. The Rails application
|
37
|
+
# expects this exact constant to be set by this Railtie.
|
38
|
+
::APP_CONFIG = cfg.fetch
|
39
|
+
end
|
40
|
+
|
41
|
+
rake_tasks do
|
42
|
+
Dir[File.join(File.dirname(__FILE__), "../tasks/*.rake")].each do |task|
|
43
|
+
load task
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
|
3
|
+
# Copyright (C) 2017 Miquel Sabaté Solà <msabate@suse.com>
|
4
|
+
#
|
5
|
+
# This file is part of CConfig.
|
6
|
+
#
|
7
|
+
# CConfig is free software: you can redistribute it and/or modify
|
8
|
+
# it under the terms of the GNU Lesser General Public License as published by
|
9
|
+
# the Free Software Foundation, either version 3 of the License, or
|
10
|
+
# (at your option) any later version.
|
11
|
+
#
|
12
|
+
# CConfig is distributed in the hope that it will be useful,
|
13
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
14
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
15
|
+
# GNU General Public License for more details.
|
16
|
+
#
|
17
|
+
# You should have received a copy of the GNU Lesser General Public License
|
18
|
+
# along with CConfig. If not, see <http://www.gnu.org/licenses/>.
|
19
|
+
|
20
|
+
module CConfig
|
21
|
+
# The current version of CConfig.
|
22
|
+
VERSION = "1.0.0".freeze
|
23
|
+
end
|
data/lib/tasks/info.rake
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
|
3
|
+
# Copyright (C) 2017 Miquel Sabaté Solà <msabate@suse.com>
|
4
|
+
#
|
5
|
+
# This file is part of CConfig.
|
6
|
+
#
|
7
|
+
# CConfig is free software: you can redistribute it and/or modify
|
8
|
+
# it under the terms of the GNU Lesser General Public License as published by
|
9
|
+
# the Free Software Foundation, either version 3 of the License, or
|
10
|
+
# (at your option) any later version.
|
11
|
+
#
|
12
|
+
# CConfig is distributed in the hope that it will be useful,
|
13
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
14
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
15
|
+
# GNU General Public License for more details.
|
16
|
+
#
|
17
|
+
# You should have received a copy of the GNU Lesser General Public License
|
18
|
+
# along with CConfig. If not, see <http://www.gnu.org/licenses/>.
|
19
|
+
|
20
|
+
namespace :cconfig do
|
21
|
+
desc "Prints the evaluated configuration"
|
22
|
+
task :info, [:prefix] => :environment do |_, args|
|
23
|
+
prefix = args[:prefix]
|
24
|
+
default = File.join(Rails.root, "config", "config.yml")
|
25
|
+
local = File.join(Rails.root, "config", "config-local.yml")
|
26
|
+
|
27
|
+
# Note that local will change if "#{prefix.upcase}_LOCAL_CONFIG_PATH" was
|
28
|
+
# specified.
|
29
|
+
cfg = ::CConfig::Config.new(default: default, local: local, prefix: prefix)
|
30
|
+
puts "Evaluated configuration:\n#{cfg}"
|
31
|
+
end
|
32
|
+
end
|
data/spec/config_spec.rb
ADDED
@@ -0,0 +1,87 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
|
3
|
+
# Copyright (C) 2017 Miquel Sabaté Solà <msabate@suse.com>
|
4
|
+
#
|
5
|
+
# This file is part of CConfig.
|
6
|
+
#
|
7
|
+
# CConfig is free software: you can redistribute it and/or modify
|
8
|
+
# it under the terms of the GNU Lesser General Public License as published by
|
9
|
+
# the Free Software Foundation, either version 3 of the License, or
|
10
|
+
# (at your option) any later version.
|
11
|
+
#
|
12
|
+
# CConfig is distributed in the hope that it will be useful,
|
13
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
14
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
15
|
+
# GNU General Public License for more details.
|
16
|
+
#
|
17
|
+
# You should have received a copy of the GNU Lesser General Public License
|
18
|
+
# along with CConfig. If not, see <http://www.gnu.org/licenses/>.
|
19
|
+
|
20
|
+
require "cconfig/cconfig"
|
21
|
+
|
22
|
+
# Returns a Config configured with the two given config files.
|
23
|
+
def get_config(default, local)
|
24
|
+
base = File.join(File.dirname(__FILE__), "fixtures")
|
25
|
+
default = File.join(base, default)
|
26
|
+
local = File.join(base, local)
|
27
|
+
::CConfig::Config.new(default: default, local: local, prefix: "test")
|
28
|
+
end
|
29
|
+
|
30
|
+
describe CConfig::Config do
|
31
|
+
after do
|
32
|
+
["TEST_LOCAL_CONFIG_PATH"].each { |key| ENV[key] = nil }
|
33
|
+
end
|
34
|
+
|
35
|
+
it "returns an empty config if neither the global nor the local were found" do
|
36
|
+
cfg = get_config("", "").fetch
|
37
|
+
expect(cfg).to be_empty
|
38
|
+
end
|
39
|
+
|
40
|
+
it "only uses the global if the local config was not found" do
|
41
|
+
cfg = get_config("config.yml", "").fetch
|
42
|
+
expect(cfg["gravatar"]["enabled"]).to be_truthy
|
43
|
+
end
|
44
|
+
|
45
|
+
it "merges both config files and work as expected" do
|
46
|
+
cfg = get_config("config.yml", "local.yml").fetch
|
47
|
+
|
48
|
+
expect(cfg.enabled?("gravatar")).to be_truthy
|
49
|
+
expect(cfg.enabled?("ldap")).to be_truthy
|
50
|
+
expect(cfg["ldap"]["hostname"]).to eq "ldap.example.com"
|
51
|
+
expect(cfg["ldap"]["port"]).to eq 389
|
52
|
+
expect(cfg["ldap"]["base"]).to eq "ou=users,dc=example,dc=com"
|
53
|
+
expect(cfg["unknown"]).to be nil
|
54
|
+
end
|
55
|
+
|
56
|
+
it "raises an error when the local file is badly formatted" do
|
57
|
+
bad = get_config("config.yml", "bad.yml")
|
58
|
+
msg = "Wrong format for the config-local file!"
|
59
|
+
expect { bad.fetch }.to raise_error(::CConfig::FormatError, msg)
|
60
|
+
end
|
61
|
+
|
62
|
+
it "returns the proper config while hiding passwords" do
|
63
|
+
cfg = get_config("config.yml", "local.yml")
|
64
|
+
fetched = cfg.fetch
|
65
|
+
evaled = YAML.safe_load(cfg.to_s)
|
66
|
+
|
67
|
+
expect(fetched).not_to eq(evaled)
|
68
|
+
fetched["ldap"]["authentication"]["password"] = "****"
|
69
|
+
expect(fetched).to eq(evaled)
|
70
|
+
end
|
71
|
+
|
72
|
+
it "works for nested options" do
|
73
|
+
cfg = get_config("config.yml", "").fetch
|
74
|
+
expect(cfg.enabled?("email.smtp")).to be true
|
75
|
+
end
|
76
|
+
|
77
|
+
it "selects the proper local file depending of the environment variable" do
|
78
|
+
# Instead of bad.yml (which will raise an error on `fetch`), we will pick up
|
79
|
+
# the local.yml file.
|
80
|
+
base = File.join(File.dirname(__FILE__), "fixtures")
|
81
|
+
local = File.join(base, "bad.yml")
|
82
|
+
ENV["TEST_LOCAL_CONFIG_PATH"] = File.join(base, "local.yml")
|
83
|
+
|
84
|
+
cfg = ::CConfig::Config.new(default: "config.yml", local: local, prefix: "test")
|
85
|
+
expect { cfg.fetch }.not_to raise_error
|
86
|
+
end
|
87
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
- ""
|