mc-settings 0.1.2 → 0.2.0

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