cconfig 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 +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
|
+
- ""
|