mc-settings 0.1.2 → 0.2.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,29 @@
1
+ codecov:
2
+ require_ci_to_pass: no
3
+
4
+ notify:
5
+ wait_for_ci: yes
6
+
7
+ parsers:
8
+ v1:
9
+ include_full_missed_files: true # To use with Ruby so we see files that have NO tests written
10
+
11
+ coverage:
12
+ range: 50..75
13
+ round: down
14
+ precision: 1
15
+ status:
16
+ project:
17
+ default: off
18
+ settings:
19
+ target: 70%
20
+ threshold: 10%
21
+ informational: true
22
+ if_not_found: success
23
+ if_ci_failed: error
24
+ paths:
25
+ - lib/
26
+ flags:
27
+ settings:
28
+ paths:
29
+ - lib/
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Setting
4
+ VERSION = '0.2.0'
5
+ end
@@ -1,11 +1,14 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'singleton'
2
4
  require 'yaml'
5
+ require 'erb'
3
6
 
4
7
  class Hash
5
8
  def recursive_merge!(other)
6
9
  other.keys.each do |k|
7
10
  if self[k].is_a?(Array) && other[k].is_a?(Array)
8
- self[k] += other[k]
11
+ self[k] = other[k]
9
12
  elsif self[k].is_a?(Hash) && other[k].is_a?(Hash)
10
13
  self[k].recursive_merge!(other[k])
11
14
  else
@@ -18,91 +21,103 @@ end
18
21
 
19
22
  class Setting
20
23
  class NotFound < RuntimeError; end
24
+
21
25
  class FileError < RuntimeError; end
26
+
22
27
  class AlreadyLoaded < RuntimeError; end
23
28
 
24
29
  include Singleton
25
-
26
30
  attr_reader :available_settings
27
31
 
28
- # This method can be called only once.
29
- #
30
- # Parameter hash looks like this:
31
- #
32
- # { :files => [ "file1.yml", "file2.yml", ...],
33
- # :path => "/var/www/apps/my-app/current/config/settings",
34
- # :local => true }
35
- #
36
- # If :local => true is set, we will load all *.yml files under :path/local directory
37
- # after all files in :files have been loaded. "Local" settings thus take precedence
38
- # by design. See README for more details.
39
- #
40
- def self.load(args = {})
41
- raise AlreadyLoaded.new('Settings already loaded') if self.instance.loaded?
42
- self.instance.load(args)
43
- end
32
+ class << self
33
+ # This method can be called only once.
34
+ #
35
+ # Parameter hash looks like this:
36
+ #
37
+ # { :files => [ "file1.yml", "file2.yml", ...],
38
+ # :path => "/var/www/apps/my-app/current/config/settings",
39
+ # :local => true }
40
+ #
41
+ # If :local => true is set, we will load all *.yml files under :path/local directory
42
+ # after all files in :files have been loaded. "Local" settings thus take precedence
43
+ # by design. See README for more details.
44
+ #
45
+ def load(**args)
46
+ raise AlreadyLoaded, 'Settings already loaded' if instance.loaded?
47
+
48
+ instance.load(**args)
49
+ end
44
50
 
45
- def self.reload(args = {})
46
- self.instance.load(args)
47
- end
51
+ def reload(**args)
52
+ instance.load(**args)
53
+ end
48
54
 
49
- # In Method invocation syntax we collapse Hash values
50
- # and return a single value if 'default' is found among keys
51
- # or Hash has only one key/value pair.
52
- #
53
- # For example, if the YML data is:
54
- # tax:
55
- # default: 0.0
56
- # california: 7.5
57
- #
58
- # Then calling Setting.tax returns "0.0""
59
- #
60
- # This is the preferred method of using settings class.
61
- #
62
- def self.method_missing(method, *args, &block)
63
- self.instance.value_for(method, args) do |v, args|
64
- self.instance.collapse_hashes(v, args)
55
+ # In Method invocation syntax we collapse Hash values
56
+ # and return a single value if 'default' is found among keys
57
+ # or Hash has only one key/value pair.
58
+ #
59
+ # For example, if the YML data is:
60
+ # tax:
61
+ # default: 0.0
62
+ # california: 7.5
63
+ #
64
+ # Then calling Setting.tax returns "0.0""
65
+ #
66
+ # This is the preferred method of using settings class.
67
+ #
68
+ def method_missing(method, *args)
69
+ instance.value_for(method, args) do |v, args|
70
+ instance.collapse_hashes(v, args)
71
+ end
65
72
  end
66
- end
67
73
 
68
- # In [] invocation syntax, we return settings value 'as is' without
69
- # Hash conversions.
70
- #
71
- # For example, if the YML data is:
72
- # tax:
73
- # default: 0.0
74
- # california: 7.5
75
- #
76
- # Then calling Setting['tax'] returns
77
- # { 'default' => "0.0", 'california' => "7.5"}
78
-
79
- def self.[](value)
80
- self.instance.value_for(value)
81
- end
74
+ def respond_to_missing?
75
+ true
76
+ end
77
+
78
+ # In [] invocation syntax, we return settings value 'as is' without
79
+ # Hash conversions.
80
+ #
81
+ # For example, if the YML data is:
82
+ # tax:
83
+ # default: 0.0
84
+ # california: 7.5
85
+ #
86
+ # Then calling Setting['tax'] returns
87
+ # { 'default' => "0.0", 'california' => "7.5"}
88
+
89
+ def [](value)
90
+ instance.value_for(value)
91
+ end
82
92
 
83
- # <b>DEPRECATED:</b> Please use <tt>method accessors</tt> instead.
84
- def self.available_settings
85
- self.instance.available_settings
93
+ # <b>DEPRECATED:</b> Please use <tt>method accessors</tt> instead.
94
+ def available_settings
95
+ instance.available_settings
96
+ end
86
97
  end
87
98
 
88
- #=================================================================
89
99
  # Instance Methods
90
- #=================================================================
91
100
 
92
101
  def initialize
93
- @available_settings ||= {}
102
+ @available_settings = {}
94
103
  end
95
104
 
96
- def has_key?(key)
97
- @available_settings.has_key?(key) ||
98
- (key[-1,1] == '?' && @available_settings.has_key?(key.chop))
105
+ # @param [Object] key
106
+ def key?(key)
107
+ @available_settings.key?(key) ||
108
+ (key[-1, 1] == '?' && @available_settings.key?(key.chop))
99
109
  end
100
110
 
111
+ alias has_key? key?
112
+
101
113
  def value_for(key, args = [])
102
114
  name = key.to_s
103
- raise NotFound.new("#{name} was not found") unless has_key?(name)
115
+ unless key?(name)
116
+ raise NotFound, "#{name} was not found"
117
+ end
118
+
104
119
  bool = false
105
- if name[-1,1] == '?'
120
+ if name[-1, 1] == '?'
106
121
  name.chop!
107
122
  bool = true
108
123
  end
@@ -112,7 +127,7 @@ class Setting
112
127
  v = yield(v, args)
113
128
  end
114
129
 
115
- if v.is_a?(Fixnum) && bool
130
+ if v.is_a?(Integer) && bool
116
131
  v.to_i > 0
117
132
  else
118
133
  v
@@ -121,59 +136,68 @@ class Setting
121
136
 
122
137
  # This method performs collapsing of the Hash settings values if the Hash
123
138
  # contains 'default' value, or just 1 element.
124
-
139
+
125
140
  def collapse_hashes(v, args)
126
141
  out = if v.is_a?(Hash)
127
- if args.empty?
128
- if v.has_key?("default")
129
- v['default'].nil? ? "" : v['default']
130
- elsif v.keys.size == 1
131
- v.values.first
132
- else
133
- v
134
- end
135
- else
136
- v[args.shift.to_s]
137
- end
138
- else
139
- v
140
- end
141
-
142
- if out.is_a?(Hash)
142
+ if args.empty?
143
+ if v.key?("default")
144
+ v['default'].nil? ? "" : v['default']
145
+ elsif v.keys.size == 1
146
+ v.values.first
147
+ else
148
+ v
149
+ end
150
+ else
151
+ v[args.shift.to_s]
152
+ end
153
+ else
154
+ v
155
+ end
156
+ if out.is_a?(Hash) && !args.empty?
143
157
  collapse_hashes(out, args)
158
+ elsif out.is_a?(Hash) && out.key?('default')
159
+ out['default']
144
160
  else
145
161
  out
146
162
  end
147
163
  end
148
-
164
+
149
165
  def loaded?
150
166
  @loaded
151
167
  end
152
168
 
153
- def load(params)
169
+ def load(**params)
154
170
  # reset settings hash
155
171
  @available_settings = {}
156
- @loaded = false
172
+ @loaded = false
157
173
 
158
174
  files = []
159
175
  path = params[:path] || Dir.pwd
160
176
  params[:files].each do |file|
161
177
  files << File.join(path, file)
162
178
  end
163
-
164
179
  if params[:local]
165
- files << Dir.glob(File.join(path, 'local', '*.yml'))
180
+ files << Dir.glob(File.join(path, 'local', '*.yml')).sort
166
181
  end
167
182
 
168
183
  files.flatten.each do |file|
169
- begin
170
- @available_settings.recursive_merge!(YAML::load(File.open(file)) || {}) if File.exists?(file)
171
- rescue Exception => e
172
- raise FileError.new("Error parsing file #{file}, with: #{e.message}")
184
+ if File.exist?(file)
185
+ @available_settings.recursive_merge! load_file(file)
173
186
  end
187
+ rescue StandardError => e
188
+ raise FileError, "Error parsing file #{file}, with: #{e.message}"
174
189
  end
175
190
 
176
191
  @loaded = true
177
192
  @available_settings
178
193
  end
194
+
195
+ private
196
+
197
+ def load_file(file)
198
+ ::YAML.load(
199
+ ::ERB.new(::IO.read(file)).result,
200
+ fallback: {},
201
+ )
202
+ end
179
203
  end
@@ -1,72 +1,33 @@
1
- # Generated by jeweler
2
- # DO NOT EDIT THIS FILE DIRECTLY
3
- # Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
4
- # -*- encoding: utf-8 -*-
1
+ # frozen_string_literal: true
2
+
3
+ lib = File.expand_path('lib', __dir__)
4
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
+
6
+ require_relative 'lib/mc-settings/version'
5
7
 
6
8
  Gem::Specification.new do |s|
7
- s.name = %q{mc-settings}
8
- s.version = "0.1.2"
9
+ s.name = 'mc-settings'
10
+ s.version = Setting::VERSION
9
11
 
10
12
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
- s.authors = ["Edwin Cruz", "Colin Shield"]
12
- s.date = %q{2011-01-18}
13
- s.description = %q{implement custom keys indenendently of environment}
14
- s.email = %q{rubydev@modcloth.com}
15
- s.extra_rdoc_files = [
16
- "LICENSE.txt",
17
- "README.rdoc"
18
- ]
19
- s.files = [
20
- ".document",
21
- "Gemfile",
22
- "Gemfile.lock",
23
- "LICENSE.txt",
24
- "README.rdoc",
25
- "Rakefile",
26
- "VERSION",
27
- "lib/mc-settings.rb",
28
- "lib/setting.rb",
29
- "mc-settings.gemspec",
30
- "spec/fixtures/joes-colors.yml",
31
- "spec/fixtures/sample.yml",
32
- "spec/mc_settings_spec.rb",
33
- "spec/spec_helper.rb",
34
- "spec/support/settings_helper.rb"
35
- ]
36
- s.homepage = %q{http://github.com/modcloth/mc-settings}
37
- s.licenses = ["MIT"]
38
- s.require_paths = ["lib"]
39
- s.rubygems_version = %q{1.3.7}
40
- s.summary = %q{Manage settings per environment}
41
- s.test_files = [
42
- "spec/mc_settings_spec.rb",
43
- "spec/spec_helper.rb",
44
- "spec/support/settings_helper.rb"
45
- ]
13
+ s.authors = ["Edwin Cruz", "Colin Shield", "Konstantin Gredeskoul"]
14
+ s.date = '2020-09-01'
15
+ s.description = 'Manage application configuration and settings per deployment environment'
16
+ s.summary = 'Manage application configuration and settings per deployment environment'
17
+ s.email = %w[softr8@gmail.com kigster@gmail.com]
18
+ s.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
19
+ s.extra_rdoc_files = %w[LICENSE.txt README.adoc]
20
+ s.homepage = 'https://github.com/kigster/mc-settings'
21
+ s.licenses = ["MIT"]
22
+ s.require_paths = ["lib"]
23
+ s.test_files = %w[spec/mc_settings_spec.rb spec/spec_helper.rb spec/support/settings_helper.rb]
46
24
 
47
- if s.respond_to? :specification_version then
48
- current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
49
- s.specification_version = 3
50
-
51
- if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
52
- s.add_development_dependency(%q<rspec>, [">= 0"])
53
- s.add_development_dependency(%q<bundler>, ["~> 1.0.0"])
54
- s.add_development_dependency(%q<jeweler>, ["~> 1.5.1"])
55
- s.add_development_dependency(%q<rcov>, [">= 0"])
56
- s.add_development_dependency(%q<ruby-debug>, [">= 0"])
57
- else
58
- s.add_dependency(%q<rspec>, [">= 0"])
59
- s.add_dependency(%q<bundler>, ["~> 1.0.0"])
60
- s.add_dependency(%q<jeweler>, ["~> 1.5.1"])
61
- s.add_dependency(%q<rcov>, [">= 0"])
62
- s.add_dependency(%q<ruby-debug>, [">= 0"])
63
- end
64
- else
65
- s.add_dependency(%q<rspec>, [">= 0"])
66
- s.add_dependency(%q<bundler>, ["~> 1.0.0"])
67
- s.add_dependency(%q<jeweler>, ["~> 1.5.1"])
68
- s.add_dependency(%q<rcov>, [">= 0"])
69
- s.add_dependency(%q<ruby-debug>, [">= 0"])
70
- end
25
+ s.add_development_dependency('bundler')
26
+ s.add_development_dependency('rspec', '~> 3.0')
27
+ s.add_development_dependency('simplecov')
28
+ s.add_development_dependency('rake')
29
+ s.add_development_dependency('pry-byebug')
30
+ s.add_development_dependency('rspec-mocks')
31
+ s.add_development_dependency('asciidoctor')
32
+ s.add_development_dependency('rspec-expectations')
71
33
  end
72
-
@@ -1,64 +1,76 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
4
+ require 'mc-settings'
2
5
  describe Setting do
3
- subject { Setting }
6
+ subject { described_class }
4
7
 
5
8
  context "Test with stubs" do
6
9
  before :each do
7
10
  stub_setting_files
8
- Setting.reload(
9
- :path => "config/settings",
10
- :files => ["default.yml", "environments/test.yml"],
11
- :local => true)
11
+ subject.reload(
12
+ path: "config/settings",
13
+ files: %w[default.yml environments/test.yml],
14
+ local: true
15
+ )
12
16
  end
13
17
 
14
18
  it 'should return test specific values' do
15
- Setting.available_settings['one'].should == "test"
16
- Setting.one.should == "test"
17
- Setting['one'].should == "test"
19
+ expect(subject.available_settings['one']).to eql "test"
20
+ expect(subject.one).to eql "test"
21
+ expect(subject['one']).to eql "test"
18
22
  end
19
23
 
20
24
  it "should handle custom values overriding everything else" do
21
- Setting.seven.should == "seven from custom"
25
+ expect(subject.seven).to eql "seven from custom"
22
26
  end
23
27
 
28
+ let(:six) { { "default" => "default value", "extra" => "recursively overriden", "deep_level" => { "value" => "even deeper level" } } }
29
+
24
30
  it "handles multiple values" do
25
- Setting[:six].should == {"default"=>"default value", "extra"=>"recursively overriden", "deep_level"=>{"value"=>"even deeper level"}}
26
- Setting.available_settings['six']['default'].should == "default value"
27
- Setting.seven.should == "seven from custom"
31
+ expect(subject[:six]).to eql six
32
+ expect(subject.available_settings['six']['default']).to eql "default value"
33
+ expect(subject.seven).to eql "seven from custom"
28
34
  end
29
35
 
30
36
  it "handles default key" do
31
- Setting.default_setting.should == 1
32
- Setting['seven']['default'].should == "seven from custom"
37
+ expect(subject.default_setting).to eql 1
38
+ expect(subject['seven']['default']).to eql "seven from custom"
33
39
  end
34
40
 
35
41
  it "should handle empty strings" do
36
- Setting.empty.should == ""
42
+ expect(subject.empty).to eql ""
37
43
  end
38
44
 
39
45
  it "should responds to ? mark" do
40
- Setting.autologin?.should == true
46
+ expect(subject.autologin?).to eql true
41
47
  end
42
48
 
43
49
  it "should returns false correctly" do
44
- Setting.flag_false.should be(false)
50
+ expect(subject.flag_false).to be_falsey
45
51
  end
46
52
 
47
- it "should merge keys recursivelly" do
48
- Setting.six(:extra).should == "recursively overriden"
49
- Setting.six(:deep_level, :value).should == "even deeper level"
53
+ it "should merge keys recursively" do
54
+ expect(subject.six(:extra)).to eql "recursively overriden"
55
+ expect(subject.six(:deep_level, :value)).to eql "even deeper level"
50
56
  end
51
57
 
52
58
  it "should create keys if it does not exist" do
53
- Setting.test_specific.should == "exist"
59
+ expect(subject.test_specific).to eql "exist"
60
+ end
61
+
62
+ context "working with arrays" do
63
+ it "should replace the whole array instead of appending new values" do
64
+ expect(subject.nested_array).to eql %w[first four five]
65
+ end
54
66
  end
55
67
  end
56
68
 
57
69
  context "When running with threads" do
58
70
  it "should keep its values" do
59
- 3.times do |time|
71
+ 3.times do |_time|
60
72
  Thread.new {
61
- Setting.available_settings.shoud_not be_empty
73
+ expect(subject.available_settings).not_to be_empty
62
74
  }
63
75
  end
64
76
  end
@@ -66,58 +78,84 @@ describe Setting do
66
78
 
67
79
  context "Test from file" do
68
80
  before :each do
69
- Setting.reload(
70
- :path => File.join(File.dirname(__FILE__)) + '/fixtures',
71
- :files => ['sample.yml']
81
+ subject.reload(
82
+ path: File.join(File.dirname(__FILE__)) + '/fixtures',
83
+ files: ['sample.yml']
72
84
  )
73
85
  end
74
86
 
75
87
  it 'should support [] syntax' do
76
- Setting['tax']['default'].should == 0.0
77
- Setting['tax'].should == { 'default' => 0.0, 'california' => 7.5 }
88
+ expect(subject['tax']['default']).to eql 0.0
89
+ expect(subject['tax']).to eql( 'default' => 0.0, 'california' => 7.5 )
78
90
  end
79
91
 
80
92
  it 'should support method invocation syntax' do
81
- Setting.tax.should == 0.0
82
-
83
- Setting.tax(:default).should == Setting.tax
84
- Setting.tax('default').should == Setting.tax
85
- Setting.tax(:california).should == 7.5
86
-
87
- Setting.states.should == ['CA', 'WA', 'NY']
88
- Setting.states(:default).should == Setting.states
89
- Setting.states(:ship_to).should == ['CA', 'NY']
93
+ expect(subject.tax).to eql 0.0
94
+ expect(subject.states).to eql %w[CA WA NY]
95
+ expect(subject.tax(:default)).to eql subject.tax
96
+ expect(subject.tax('default')).to eql subject.tax
97
+ expect(subject.tax(:california)).to eql 7.5
98
+ expect(subject.states(:default)).to eql subject.states
99
+ expect(subject.states(:ship_to)).to eql %w[CA NY]
90
100
  end
91
101
 
92
102
  it 'should correctly process Boolean values' do
93
- Setting.boolean_true?.should be(true)
94
- Setting.boolean_true.should == 4
95
- Setting.boolean_false?.should be(false)
96
- Setting.boolean_false?(:default).should be(false)
97
- Setting.boolean_false?(:negated).should be(true)
103
+ expect(subject.boolean_true?).to be_truthy
104
+ expect(subject.boolean_true).to eql 4
105
+ expect(subject.boolean_false?).to be_falsey
106
+ expect(subject.boolean_false?(:default)).to be_falsey
107
+ expect(subject.boolean_false?(:negated)).to be_truthy
98
108
  end
99
109
  end
100
110
 
101
111
  context "Test recursive overrides and nested hashes" do
102
112
  before :each do
103
- Setting.reload(
104
- :path => File.join(File.dirname(__FILE__)) + '/fixtures',
105
- :files => ['sample.yml', 'joes-colors.yml']
113
+ subject.reload(
114
+ path: File.join(File.dirname(__FILE__)) + '/fixtures',
115
+ files: %w[sample.yml joes-colors.yml]
106
116
  )
107
117
  end
108
118
 
109
119
  it 'should override colors with Joes and support nested hashes' do
110
- Setting.color.should == :grey # default
111
- Setting.color(:pants).should == :purple # default
112
-
113
- Setting.color(:pants, :school).should == :blue # in sample
114
- Setting.color(:pants, :favorite).should == :orange # joes override
120
+ expect(subject.color).to eql :grey # default
121
+ expect(subject.color(:pants)).to eql :purple # default
122
+ expect(subject.color(:pants, :school)).to eql :blue # in sample
123
+ expect(subject.color(:pants, :favorite)).to eql :orange # joes override
124
+ expect(subject.color(:shorts,:school)).to eql :black # in sample
125
+ expect(subject.color(:shorts, :favorite)).to eql :white # joe's override
126
+ expect(subject.color(:shorts)).to eql :stripes # joe's override of default
127
+ end
128
+ end
115
129
 
116
- Setting.color(:shorts, :school).should == :black # in sample
117
- Setting.color(:shorts, :favorite).should == :white # joe's override
130
+ context "Complex nested configs" do
131
+ before :each do
132
+ subject.reload(
133
+ path: File.join(File.dirname(__FILE__)) + '/fixtures',
134
+ files: ['shipping.yml']
135
+ )
136
+ end
118
137
 
119
- Setting.color(:shorts).should == :stripes # joe's override of default
138
+ it "should build correct tree with arrays and default values " do
139
+ expect(subject.shipping_config).to eql "Defaulted"
140
+ expect(subject.shipping_config(:domestic, :non_shippable_regions).first).to eql "US-AS"
141
+ expect(subject.shipping_config(:international, :service)).to eql 'Foo'
142
+ expect(subject.shipping_config(:international, :countries).size).to be > 0
143
+ expect(subject.shipping_config(:international, :shipping_carrier)).to eql 'Bar'
144
+ expect(subject.shipping_config(:domestic)['non_shippable_regions'].size).to be > 0
120
145
  end
146
+ end
121
147
 
148
+ context "Ruby code inside yml file" do
149
+ before :each do
150
+ subject.reload(
151
+ path: File.join(File.dirname(__FILE__)) + '/fixtures',
152
+ files: ['shipping.yml']
153
+ )
154
+ end
155
+ it "should interpret ruby code and put correct values" do
156
+ expect(subject.shipping_config).to eql "Defaulted"
157
+ expect(subject.number).to eql 5
158
+ expect(subject.stringified).to eql "stringified"
159
+ end
122
160
  end
123
- end
161
+ end