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.
@@ -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
+ - ""