cconfig 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -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
@@ -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
+ - ""