fittings 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.document +5 -0
- data/Gemfile +3 -0
- data/Gemfile.lock +34 -0
- data/LICENSE.txt +20 -0
- data/README.rdoc +175 -0
- data/Rakefile +14 -0
- data/VERSION +1 -0
- data/fittings.gemspec +50 -0
- data/lib/mc-settings.rb +1 -0
- data/lib/setting.rb +187 -0
- data/spec/fixtures/joes-colors.yml +9 -0
- data/spec/fixtures/sample.yml +35 -0
- data/spec/fixtures/shipping.yml +36 -0
- data/spec/mc_settings_spec.rb +160 -0
- data/spec/spec_helper.rb +12 -0
- data/spec/support/settings_helper.rb +59 -0
- metadata +93 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 3d284c9153aca2da7768369111eece8dba1689fa
|
4
|
+
data.tar.gz: 32ceeb38ef5c2ddc8c2878f4c49ea3383de0118d
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: a20d5db9cdc38c55707b843860eae271c47d211ff1220a2c043e8780965c3e3c3dc35de1c358614aaf587b3caddb541e2f8eba130994f0326c87bda4838911a9
|
7
|
+
data.tar.gz: e2a67f2e123d73f37ecacb303d9a451bca42fef4c7325cbb8c7233c0dbe35532e589943fdac110d88f394bd9c04e86a3c961891dd8782508756f4a941240a47b
|
data/.document
ADDED
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
fittings (0.2.0)
|
5
|
+
|
6
|
+
GEM
|
7
|
+
remote: http://rubygems.org/
|
8
|
+
specs:
|
9
|
+
diff-lcs (1.3)
|
10
|
+
rake (12.1.0)
|
11
|
+
rspec (3.6.0)
|
12
|
+
rspec-core (~> 3.6.0)
|
13
|
+
rspec-expectations (~> 3.6.0)
|
14
|
+
rspec-mocks (~> 3.6.0)
|
15
|
+
rspec-core (3.6.0)
|
16
|
+
rspec-support (~> 3.6.0)
|
17
|
+
rspec-expectations (3.6.0)
|
18
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
19
|
+
rspec-support (~> 3.6.0)
|
20
|
+
rspec-mocks (3.6.0)
|
21
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
22
|
+
rspec-support (~> 3.6.0)
|
23
|
+
rspec-support (3.6.0)
|
24
|
+
|
25
|
+
PLATFORMS
|
26
|
+
ruby
|
27
|
+
|
28
|
+
DEPENDENCIES
|
29
|
+
fittings!
|
30
|
+
rake
|
31
|
+
rspec
|
32
|
+
|
33
|
+
BUNDLED WITH
|
34
|
+
1.15.4
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2010 Edwin Cruz
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,175 @@
|
|
1
|
+
= Application Settings Manager
|
2
|
+
|
3
|
+
== Summary
|
4
|
+
|
5
|
+
This gem provides an easy and Capistrano-friendly way to manage application configuration across
|
6
|
+
multiple environments, such as development, QA, staging, production, etc.
|
7
|
+
|
8
|
+
Applications typically rely on configuration settings, such as host names, URLs, usernames and many
|
9
|
+
more. Some change between environments, some do not. This gem assumes that application configuration
|
10
|
+
is represented by a Hash of arbitrary depth, and provides convenient and compact syntax to access the
|
11
|
+
settings through a singleton instance inside Setting class.
|
12
|
+
|
13
|
+
Configuration is stored in one or more YAML files with the top-level data structure being a Hash,
|
14
|
+
with keys being the names of individual settings. For example, consider the following sample
|
15
|
+
application configuration file:
|
16
|
+
|
17
|
+
tax:
|
18
|
+
default: 0.0
|
19
|
+
california: 7.5
|
20
|
+
states:
|
21
|
+
default:
|
22
|
+
- 'CA'
|
23
|
+
- 'WA'
|
24
|
+
- 'NY'
|
25
|
+
ship_to:
|
26
|
+
- 'CA'
|
27
|
+
- 'NY'
|
28
|
+
math_pi: 3.14159526
|
29
|
+
|
30
|
+
Setting Gem provides Setting.load(..) method to load configuration from files in a way that allows
|
31
|
+
some configuration files to override previously loaded values, and then offers a simple method API
|
32
|
+
to access the values, for example Setting.tax(:california) or Setting.tax. Supporting default values
|
33
|
+
in 2nd, 3rd, .. - level hashes is one of the advantages of using this gem.
|
34
|
+
|
35
|
+
By loading configuration from YAML files, Setting gem is inherently compatible with Capistrano deployment
|
36
|
+
methodology, where a certain set of files may become "activated" by simply sym-linking them into
|
37
|
+
the appropriate settings folder.
|
38
|
+
|
39
|
+
Note: using example above, "1st level" hash is the one with keys "tax", "states" and "math_pi".
|
40
|
+
2nd-level hash is, for example, the tax definition one, with keys "default" and "california".
|
41
|
+
|
42
|
+
== Usage in Code
|
43
|
+
|
44
|
+
Once configuration is initialized using Setting#load or Setting#reload methods (see below), they can be used in
|
45
|
+
code in the following way:
|
46
|
+
|
47
|
+
* Setting.key_name is optimized to return default value if available instead of a Hash.
|
48
|
+
* Setting.key_name(:sub_key_name) returns a value from the 2nd level hash.
|
49
|
+
* Setting.key_name(:sub_key_name, :sub_sub_key_name) returns value from the 3rd level hash if available. The algorithm is recursive, so only the maximum method stack depth will limit the number of nested hash values you can access this way.
|
50
|
+
* Special syntax Setting[:key_name], Setting[:key_name][:sub_key_name], etc also supported. This method, however, does not support default values (see below).
|
51
|
+
|
52
|
+
Method notation is recommended over square bracket notation for accessing single values. However,
|
53
|
+
square bracket notation may be useful when you want to fetch the entire 2nd level hash that
|
54
|
+
includes the default value, instead of the default value itself.
|
55
|
+
|
56
|
+
For example, given the above YAML file, you can access the settings in your code as follows:
|
57
|
+
|
58
|
+
Setting.tax => 0.0
|
59
|
+
Setting.tax(:california) => 7.5
|
60
|
+
Setting.math_pi => 3.14159526
|
61
|
+
Setting[:math_pi] => 3.14159526
|
62
|
+
Setting.states => [ 'CA', 'WA', 'NY' ]
|
63
|
+
Setting.states['ship_to'] => [ 'CA', 'NY' ]
|
64
|
+
|
65
|
+
Method-calling notation allows passing an array of keys to fetch a value from a nested hash.
|
66
|
+
This method also supports returning a default value, stored against the "default" key.
|
67
|
+
|
68
|
+
Setting.tax => 0.0
|
69
|
+
|
70
|
+
Square bracket syntax returns the actual nested hash, without any regard for the default value:
|
71
|
+
|
72
|
+
Setting[:tax] => { 'default' => 0.0, 'california' => 7.5 }
|
73
|
+
|
74
|
+
== Loading Settings
|
75
|
+
|
76
|
+
The gem should be initialized in your environment.rb (if using Rails), or in any other
|
77
|
+
application initialization block. Setting.load() method is provided for loading settings, and it
|
78
|
+
can be called only once in application lifecycle, or it will throw an exception. If you need to reload
|
79
|
+
settings completely, you can use reload() method with similar arguments.
|
80
|
+
|
81
|
+
Consider an example:
|
82
|
+
|
83
|
+
Setting.load(:path => "#{Rails.root}/config/settings",
|
84
|
+
:files => ["default.yml", "environments/#{Rails.env}.yml"],
|
85
|
+
:local => true)
|
86
|
+
|
87
|
+
The argument is an options hash that configures which YAML files to load, in what order, and from where.
|
88
|
+
|
89
|
+
* path specifies the "root" folder where settings files will be loaded from
|
90
|
+
* files is an array that lists file names relative to the :path. In the example above, settings folder contains subfolder "environments" where Rails-specific environment files are located (such as "development.yml", "staging.yml", "production.yml", etc)
|
91
|
+
* local can be optionally specified as a true value, and if specified Setting gem will load all *.yml files that live under the :path/local folder.
|
92
|
+
|
93
|
+
Below is list of YAML files loaded in order specified in the above example, assuming that "development" is
|
94
|
+
the Rails environment, and "local" folder exists with 3 additional YAML files in it:
|
95
|
+
|
96
|
+
config/settings/default.yml
|
97
|
+
config/settings/environments/development.yml
|
98
|
+
config/settings/local/authorize-net.yml
|
99
|
+
config/settings/local/paypal.yml
|
100
|
+
config/settings/local/other.yml
|
101
|
+
|
102
|
+
Each YML file defines a ruby Hash. During file loading, the hashes are merged,
|
103
|
+
so that values loaded in early files may be overwritten by values in subsequent
|
104
|
+
files. This is deliberate and by design: it allows you to create small "override"
|
105
|
+
files for each environment, or even each machine you want to deploy to. Exactly
|
106
|
+
how you split your application settings in files is up to you.
|
107
|
+
|
108
|
+
== Nested Hashes and Default Values
|
109
|
+
|
110
|
+
MC Setting gem provides a convenient way to access nested values, including full
|
111
|
+
support for the default values within nested hashes (as of 0.1.1).
|
112
|
+
|
113
|
+
Consider the following nested hash example:
|
114
|
+
|
115
|
+
default.yml:
|
116
|
+
|
117
|
+
services:
|
118
|
+
inventory:
|
119
|
+
url: http://ims.mycompany.com:3443/inventory_manager
|
120
|
+
name: Inventory Management
|
121
|
+
shipping:
|
122
|
+
url: http://ship.mycompany.com:3443/shipper
|
123
|
+
name: Shipping
|
124
|
+
|
125
|
+
Setting.load(:files => ['default.yml'], :path => ...)
|
126
|
+
|
127
|
+
Setting.services(:inventory) => { :url => "http://localhost:3443/inventory_manager" :name => "Inventory Management"}
|
128
|
+
Setting.services(:inventory, :url) => "http://localhost:3443/inventory_manager"
|
129
|
+
|
130
|
+
staging.yml
|
131
|
+
|
132
|
+
We are changing URLs for services in staging.yml, so they work in the staging environment. Service URLs have been updated
|
133
|
+
to use localhost:
|
134
|
+
|
135
|
+
services:
|
136
|
+
inventory:
|
137
|
+
url: http://localhost:8009/inventory_manager
|
138
|
+
shipping:
|
139
|
+
url: http://localhost:8008/shipper
|
140
|
+
|
141
|
+
|
142
|
+
Setting.load(:files => ['default.yml', 'staging.yml'], :path => ...)
|
143
|
+
|
144
|
+
Setting.services(:inventory) => { :url => "http://localhost:8009/inventory_manager" :name => "Inventory Management"}
|
145
|
+
Setting.services(:inventory, :url) => "http://localhost:8008/inventory_manager"
|
146
|
+
|
147
|
+
== Capistrano Recommendations
|
148
|
+
|
149
|
+
Assume the directory structure of your Rails application is as follows:
|
150
|
+
|
151
|
+
config/settings/default.yml
|
152
|
+
config/settings/environments/development.yml
|
153
|
+
config/settings/environments/staging.yml
|
154
|
+
config/settings/environments/production.yml
|
155
|
+
config/settings/local
|
156
|
+
config/settings/systems/reporting.yml
|
157
|
+
config/settings/systems/admin.yml
|
158
|
+
|
159
|
+
Note that the "local" directory is empty, and that "systems" directory contains several YAML files that provide alternative
|
160
|
+
configuration for a reporting server, and an admin server (both of which run in "production" rails environment).
|
161
|
+
|
162
|
+
When deploying to the main production site, neither YAML files inside "systems" folder are activated or used.
|
163
|
+
|
164
|
+
But upon deployment to the admin server, Capistrano could symlink "admin.yml" from config/settings/local folder, so the Setting gem
|
165
|
+
would load these values. So for each Capistrano role, you can define which files need to be symlinked into local, thus creating
|
166
|
+
a very flexible configuration scheme that's easily managed by Capistrano.
|
167
|
+
|
168
|
+
== Copyright
|
169
|
+
|
170
|
+
Copyright 2010 (c) ModCloth Inc.
|
171
|
+
|
172
|
+
Authors: 2010 Edwin Cruz & Konstantin Gredeskoul
|
173
|
+
|
174
|
+
See LICENSE.txt for further details.
|
175
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
require 'bundler/gem_tasks'
|
2
|
+
require 'rubygems/package_task'
|
3
|
+
require 'rspec/core/rake_task'
|
4
|
+
|
5
|
+
$: << File.join(File.dirname(__FILE__),'lib')
|
6
|
+
|
7
|
+
include Rake::DSL
|
8
|
+
|
9
|
+
gemspec = eval(File.read('fittings.gemspec'))
|
10
|
+
Gem::PackageTask.new(gemspec) {}
|
11
|
+
RSpec::Core::RakeTask.new(:spec)
|
12
|
+
Bundler::GemHelper.install_tasks
|
13
|
+
|
14
|
+
task :default => :spec
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.1.6
|
data/fittings.gemspec
ADDED
@@ -0,0 +1,50 @@
|
|
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 -*-
|
5
|
+
|
6
|
+
Gem::Specification.new do |s|
|
7
|
+
s.name = "fittings"
|
8
|
+
s.version = "0.2.0"
|
9
|
+
|
10
|
+
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-09-06}
|
13
|
+
s.description = %q{implement custom keys independently of environment}
|
14
|
+
s.email = %q{eng@stitchfix.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
|
+
"fittings.gemspec",
|
30
|
+
"spec/fixtures/joes-colors.yml",
|
31
|
+
"spec/fixtures/sample.yml",
|
32
|
+
"spec/fixtures/shipping.yml",
|
33
|
+
"spec/mc_settings_spec.rb",
|
34
|
+
"spec/spec_helper.rb",
|
35
|
+
"spec/support/settings_helper.rb"
|
36
|
+
]
|
37
|
+
s.homepage = %q{http://github.com/stitchfix/fittings}
|
38
|
+
s.licenses = ["MIT"]
|
39
|
+
s.require_paths = ["lib"]
|
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
|
+
]
|
46
|
+
|
47
|
+
s.add_development_dependency "rspec"
|
48
|
+
s.add_development_dependency "rake"
|
49
|
+
end
|
50
|
+
|
data/lib/mc-settings.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__)) + '/setting'
|
data/lib/setting.rb
ADDED
@@ -0,0 +1,187 @@
|
|
1
|
+
require 'singleton'
|
2
|
+
require 'yaml'
|
3
|
+
require 'erb'
|
4
|
+
|
5
|
+
class Hash
|
6
|
+
def recursive_merge!(other)
|
7
|
+
other.keys.each do |k|
|
8
|
+
if self[k].is_a?(Array) && other[k].is_a?(Array)
|
9
|
+
self[k] = other[k]
|
10
|
+
elsif self[k].is_a?(Hash) && other[k].is_a?(Hash)
|
11
|
+
self[k].recursive_merge!(other[k])
|
12
|
+
else
|
13
|
+
self[k] = other[k]
|
14
|
+
end
|
15
|
+
end
|
16
|
+
self
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
class Setting
|
21
|
+
class NotFound < RuntimeError; end
|
22
|
+
class FileError < RuntimeError; end
|
23
|
+
class AlreadyLoaded < RuntimeError; end
|
24
|
+
|
25
|
+
include Singleton
|
26
|
+
NUM_KLASS = if RUBY_VERSION.split(/\./)[0].to_i == 2 && RUBY_VERSION.split(/\./)[1].to_i >= 4
|
27
|
+
Integer
|
28
|
+
else
|
29
|
+
Fixnum
|
30
|
+
end
|
31
|
+
|
32
|
+
attr_reader :available_settings
|
33
|
+
|
34
|
+
# This method can be called only once.
|
35
|
+
#
|
36
|
+
# Parameter hash looks like this:
|
37
|
+
#
|
38
|
+
# { :files => [ "file1.yml", "file2.yml", ...],
|
39
|
+
# :path => "/var/www/apps/my-app/current/config/settings",
|
40
|
+
# :local => true }
|
41
|
+
#
|
42
|
+
# If :local => true is set, we will load all *.yml files under :path/local directory
|
43
|
+
# after all files in :files have been loaded. "Local" settings thus take precedence
|
44
|
+
# by design. See README for more details.
|
45
|
+
#
|
46
|
+
def self.load(args = {})
|
47
|
+
raise AlreadyLoaded.new('Settings already loaded') if self.instance.loaded?
|
48
|
+
self.instance.load(args)
|
49
|
+
end
|
50
|
+
|
51
|
+
def self.reload(args = {})
|
52
|
+
self.instance.load(args)
|
53
|
+
end
|
54
|
+
|
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 self.method_missing(method, *args, &block)
|
69
|
+
self.instance.value_for(method, args) do |v, args|
|
70
|
+
self.instance.collapse_hashes(v, args)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
# In [] invocation syntax, we return settings value 'as is' without
|
75
|
+
# Hash conversions.
|
76
|
+
#
|
77
|
+
# For example, if the YML data is:
|
78
|
+
# tax:
|
79
|
+
# default: 0.0
|
80
|
+
# california: 7.5
|
81
|
+
#
|
82
|
+
# Then calling Setting['tax'] returns
|
83
|
+
# { 'default' => "0.0", 'california' => "7.5"}
|
84
|
+
|
85
|
+
def self.[](value)
|
86
|
+
self.instance.value_for(value)
|
87
|
+
end
|
88
|
+
|
89
|
+
# <b>DEPRECATED:</b> Please use <tt>method accessors</tt> instead.
|
90
|
+
def self.available_settings
|
91
|
+
self.instance.available_settings
|
92
|
+
end
|
93
|
+
|
94
|
+
#=================================================================
|
95
|
+
# Instance Methods
|
96
|
+
#=================================================================
|
97
|
+
|
98
|
+
def initialize
|
99
|
+
@available_settings ||= {}
|
100
|
+
end
|
101
|
+
|
102
|
+
def has_key?(key)
|
103
|
+
@available_settings.has_key?(key) ||
|
104
|
+
(key[-1,1] == '?' && @available_settings.has_key?(key.chop))
|
105
|
+
end
|
106
|
+
|
107
|
+
def value_for(key, args = [])
|
108
|
+
name = key.to_s
|
109
|
+
raise NotFound.new("#{name} was not found") unless has_key?(name)
|
110
|
+
bool = false
|
111
|
+
if name[-1,1] == '?'
|
112
|
+
name.chop!
|
113
|
+
bool = true
|
114
|
+
end
|
115
|
+
|
116
|
+
v = @available_settings[name]
|
117
|
+
if block_given?
|
118
|
+
v = yield(v, args)
|
119
|
+
end
|
120
|
+
|
121
|
+
|
122
|
+
if v.is_a?(NUM_KLASS) && bool
|
123
|
+
v.to_i > 0
|
124
|
+
else
|
125
|
+
v
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
# This method performs collapsing of the Hash settings values if the Hash
|
130
|
+
# contains 'default' value, or just 1 element.
|
131
|
+
|
132
|
+
def collapse_hashes(v, args)
|
133
|
+
out = if v.is_a?(Hash)
|
134
|
+
if args.empty?
|
135
|
+
if v.has_key?("default")
|
136
|
+
v['default'].nil? ? "" : v['default']
|
137
|
+
elsif v.keys.size == 1
|
138
|
+
v.values.first
|
139
|
+
else
|
140
|
+
v
|
141
|
+
end
|
142
|
+
else
|
143
|
+
v[args.shift.to_s]
|
144
|
+
end
|
145
|
+
else
|
146
|
+
v
|
147
|
+
end
|
148
|
+
if out.is_a?(Hash) && !args.empty?
|
149
|
+
collapse_hashes(out, args)
|
150
|
+
elsif out.is_a?(Hash) && out.has_key?('default')
|
151
|
+
out['default']
|
152
|
+
else
|
153
|
+
out
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
def loaded?
|
158
|
+
@loaded
|
159
|
+
end
|
160
|
+
|
161
|
+
def load(params)
|
162
|
+
# reset settings hash
|
163
|
+
@available_settings = {}
|
164
|
+
@loaded = false
|
165
|
+
|
166
|
+
files = []
|
167
|
+
path = params[:path] || Dir.pwd
|
168
|
+
params[:files].each do |file|
|
169
|
+
files << File.join(path, file)
|
170
|
+
end
|
171
|
+
|
172
|
+
if params[:local]
|
173
|
+
files << Dir.glob(File.join(path, 'local', '*.yml')).sort
|
174
|
+
end
|
175
|
+
|
176
|
+
files.flatten.each do |file|
|
177
|
+
begin
|
178
|
+
@available_settings.recursive_merge!(YAML::load(ERB.new(IO.read(file)).result) || {}) if File.exists?(file)
|
179
|
+
rescue Exception => e
|
180
|
+
raise FileError.new("Error parsing file #{file}, with: #{e.message}")
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
@loaded = true
|
185
|
+
@available_settings
|
186
|
+
end
|
187
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
tax:
|
2
|
+
default: 0.0
|
3
|
+
california: 7.5
|
4
|
+
states:
|
5
|
+
default:
|
6
|
+
- 'CA'
|
7
|
+
- 'WA'
|
8
|
+
- 'NY'
|
9
|
+
ship_to:
|
10
|
+
- 'CA'
|
11
|
+
- 'NY'
|
12
|
+
boolean_true:
|
13
|
+
default: 4
|
14
|
+
boolean_false:
|
15
|
+
default: false
|
16
|
+
negated: true
|
17
|
+
color:
|
18
|
+
default:
|
19
|
+
:grey
|
20
|
+
pants:
|
21
|
+
default:
|
22
|
+
:purple
|
23
|
+
favorite:
|
24
|
+
:red
|
25
|
+
school:
|
26
|
+
:blue
|
27
|
+
shorts:
|
28
|
+
default:
|
29
|
+
:plaid
|
30
|
+
favorite:
|
31
|
+
:yellow
|
32
|
+
school:
|
33
|
+
:black
|
34
|
+
|
35
|
+
|
@@ -0,0 +1,36 @@
|
|
1
|
+
shipping_config:
|
2
|
+
default: <%= 'Defaulted' %>
|
3
|
+
domestic:
|
4
|
+
service: MyService
|
5
|
+
countries:
|
6
|
+
- "US"
|
7
|
+
non_shippable_regions:
|
8
|
+
- "US-AS"
|
9
|
+
- "US-GU"
|
10
|
+
- "US-MP"
|
11
|
+
- "US-PR"
|
12
|
+
- "US-UM"
|
13
|
+
- "US-VI"
|
14
|
+
- "US-AF"
|
15
|
+
- "US-AA"
|
16
|
+
- "US-AC"
|
17
|
+
- "US-AE"
|
18
|
+
- "US-AM"
|
19
|
+
- "US-AP"
|
20
|
+
- "US-FM"
|
21
|
+
- "US-PW"
|
22
|
+
- "US-MH"
|
23
|
+
international:
|
24
|
+
service: Foo
|
25
|
+
countries:
|
26
|
+
- "GU"
|
27
|
+
- "VI"
|
28
|
+
- "AS"
|
29
|
+
- "MP"
|
30
|
+
- "UM"
|
31
|
+
- "FR"
|
32
|
+
- "GR"
|
33
|
+
- "RU"
|
34
|
+
shipping_carrier: Bar
|
35
|
+
number: <%= 5%>
|
36
|
+
stringified: <%= nil || "stringified"%>
|
@@ -0,0 +1,160 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
|
2
|
+
describe Setting do
|
3
|
+
subject { Setting }
|
4
|
+
|
5
|
+
context "Test with stubs" do
|
6
|
+
before :each do
|
7
|
+
stub_setting_files
|
8
|
+
subject.reload(
|
9
|
+
:path => "config/settings",
|
10
|
+
:files => ["default.yml", "environments/test.yml"],
|
11
|
+
:local => true)
|
12
|
+
end
|
13
|
+
|
14
|
+
it 'should return test specific values' do
|
15
|
+
expect(subject.available_settings['one']).to eq("test")
|
16
|
+
expect(subject.one).to eq("test")
|
17
|
+
expect(subject['one']).to eq("test")
|
18
|
+
end
|
19
|
+
|
20
|
+
it "should handle custom values overriding everything else" do
|
21
|
+
expect(subject.seven).to eq("seven from custom")
|
22
|
+
end
|
23
|
+
|
24
|
+
it "handles multiple values" do
|
25
|
+
expect(subject[:six]).to eq({"default"=>"default value", "extra"=>"recursively overriden", "deep_level"=>{"value"=>"even deeper level"}})
|
26
|
+
expect(subject.available_settings['six']['default']).to eq("default value")
|
27
|
+
expect(subject.seven).to eq("seven from custom")
|
28
|
+
end
|
29
|
+
|
30
|
+
it "handles default key" do
|
31
|
+
expect(subject.default_setting).to eq(1)
|
32
|
+
expect(subject['seven']['default']).to eq("seven from custom")
|
33
|
+
end
|
34
|
+
|
35
|
+
it "should handle empty strings" do
|
36
|
+
expect(subject.empty).to eq("")
|
37
|
+
end
|
38
|
+
|
39
|
+
it "should responds to ? mark" do
|
40
|
+
expect(subject.autologin?).to eq(true)
|
41
|
+
end
|
42
|
+
|
43
|
+
it "should returns false correctly" do
|
44
|
+
expect(subject.flag_false).to be(false)
|
45
|
+
end
|
46
|
+
|
47
|
+
it "should merge keys recursivelly" do
|
48
|
+
expect(subject.six(:extra)).to eq("recursively overriden")
|
49
|
+
expect(subject.six(:deep_level, :value)).to eq("even deeper level")
|
50
|
+
end
|
51
|
+
|
52
|
+
it "should create keys if it does not exist" do
|
53
|
+
expect(subject.test_specific).to eq("exist")
|
54
|
+
end
|
55
|
+
|
56
|
+
context "working with arrays" do
|
57
|
+
it "should replace the whole array instead of appending new values" do
|
58
|
+
expect(subject.nested_array).to eq(['first', 'four', 'five'])
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
context "When running with threads" do
|
64
|
+
it "should keep its values" do
|
65
|
+
3.times do |time|
|
66
|
+
Thread.new {
|
67
|
+
subject.available_settings.shoud_not be_empty
|
68
|
+
}
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
context "Test from file" do
|
74
|
+
before :each do
|
75
|
+
subject.reload(
|
76
|
+
:path => File.join(File.dirname(__FILE__)) + '/fixtures',
|
77
|
+
:files => ['sample.yml']
|
78
|
+
)
|
79
|
+
end
|
80
|
+
|
81
|
+
it 'should support [] syntax' do
|
82
|
+
expect(subject['tax']['default']).to eq(0.0)
|
83
|
+
expect(subject['tax']).to eq({ 'default' => 0.0, 'california' => 7.5 })
|
84
|
+
end
|
85
|
+
|
86
|
+
it 'should support method invocation syntax' do
|
87
|
+
expect(subject.tax).to eq(0.0)
|
88
|
+
|
89
|
+
expect(subject.tax(:default)).to eq(subject.tax)
|
90
|
+
expect(subject.tax('default')).to eq(subject.tax)
|
91
|
+
expect(subject.tax(:california)).to eq(7.5)
|
92
|
+
|
93
|
+
expect(subject.states).to eq(['CA', 'WA', 'NY'])
|
94
|
+
expect(subject.states(:default)).to eq(subject.states)
|
95
|
+
expect(subject.states(:ship_to)).to eq(['CA', 'NY'])
|
96
|
+
end
|
97
|
+
|
98
|
+
it 'should correctly process Boolean values' do
|
99
|
+
expect(subject.boolean_true?).to be(true)
|
100
|
+
expect(subject.boolean_true).to eq(4)
|
101
|
+
expect(subject.boolean_false?).to be(false)
|
102
|
+
expect(subject.boolean_false?(:default)).to be(false)
|
103
|
+
expect(subject.boolean_false?(:negated)).to be(true)
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
context "Test recursive overrides and nested hashes" do
|
108
|
+
before :each do
|
109
|
+
subject.reload(
|
110
|
+
:path => File.join(File.dirname(__FILE__)) + '/fixtures',
|
111
|
+
:files => ['sample.yml', 'joes-colors.yml']
|
112
|
+
)
|
113
|
+
end
|
114
|
+
|
115
|
+
it 'should override colors with Joes and support nested hashes' do
|
116
|
+
expect(subject.color).to eq(:grey) # default
|
117
|
+
expect(subject.color(:pants)).to eq(:purple) # default
|
118
|
+
|
119
|
+
expect(subject.color(:pants, :school)).to eq(:blue) # in sample
|
120
|
+
expect(subject.color(:pants, :favorite)).to eq(:orange) # joes override
|
121
|
+
|
122
|
+
expect(subject.color(:shorts, :school)).to eq(:black) # in sample
|
123
|
+
expect(subject.color(:shorts, :favorite)).to eq(:white) # joe's override
|
124
|
+
|
125
|
+
expect(subject.color(:shorts)).to eq(:stripes) # joe's override of default
|
126
|
+
end
|
127
|
+
|
128
|
+
end
|
129
|
+
context "Complex nested configs" do
|
130
|
+
before :each do
|
131
|
+
subject.reload(
|
132
|
+
:path => File.join(File.dirname(__FILE__)) + '/fixtures',
|
133
|
+
:files => ['shipping.yml']
|
134
|
+
)
|
135
|
+
end
|
136
|
+
it "should build correct tree with arrays and default values " do
|
137
|
+
expect(subject.shipping_config).to eq("Defaulted")
|
138
|
+
expect(subject.shipping_config(:domestic, :non_shippable_regions).first).to eq("US-AS")
|
139
|
+
expect(subject.shipping_config(:international, :service)).to eq('Foo')
|
140
|
+
expect(subject.shipping_config(:international, :countries).size).to be > 0
|
141
|
+
expect(subject.shipping_config(:international, :shipping_carrier)).to eq('Bar')
|
142
|
+
#backward compatibility:
|
143
|
+
expect(subject.shipping_config(:domestic)['non_shippable_regions'].size).to be > 0
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
context "Ruby code inside yml file" do
|
148
|
+
before :each do
|
149
|
+
subject.reload(
|
150
|
+
:path => File.join(File.dirname(__FILE__)) + '/fixtures',
|
151
|
+
:files => ['shipping.yml']
|
152
|
+
)
|
153
|
+
end
|
154
|
+
it "should interpret ruby code and put correct values" do
|
155
|
+
expect(subject.shipping_config).to eq("Defaulted")
|
156
|
+
subject.number == 5
|
157
|
+
subject.stringified == "stringified"
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
2
|
+
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
3
|
+
require 'rspec'
|
4
|
+
require 'mc-settings'
|
5
|
+
|
6
|
+
# Requires supporting files with custom matchers and macros, etc,
|
7
|
+
# in ./support/ and its subdirectories.
|
8
|
+
Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each {|f| require f}
|
9
|
+
|
10
|
+
RSpec.configure do |config|
|
11
|
+
|
12
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
def stub_setting_files
|
2
|
+
defaults = <<-CONTENT
|
3
|
+
one: default
|
4
|
+
two:
|
5
|
+
three: 3
|
6
|
+
four: "4"
|
7
|
+
five: "default string"
|
8
|
+
default_setting: 1
|
9
|
+
six:
|
10
|
+
default: "default value"
|
11
|
+
extra: "extra"
|
12
|
+
deep_level:
|
13
|
+
value: "even deeper level"
|
14
|
+
seven:
|
15
|
+
default: "seven"
|
16
|
+
empty:
|
17
|
+
default:
|
18
|
+
autologin:
|
19
|
+
format: int
|
20
|
+
default: 7
|
21
|
+
flag_false:
|
22
|
+
default: false
|
23
|
+
nested_array:
|
24
|
+
- first
|
25
|
+
- second
|
26
|
+
- third
|
27
|
+
CONTENT
|
28
|
+
test = <<-CONTENT
|
29
|
+
one: test
|
30
|
+
two:
|
31
|
+
three: 5
|
32
|
+
four: "6"
|
33
|
+
five: "test string"
|
34
|
+
six:
|
35
|
+
extra: "recursively overriden"
|
36
|
+
test_specific: "exist"
|
37
|
+
nested_array:
|
38
|
+
- first
|
39
|
+
- four
|
40
|
+
- five
|
41
|
+
CONTENT
|
42
|
+
|
43
|
+
empty = <<-CONTENT
|
44
|
+
CONTENT
|
45
|
+
|
46
|
+
custom = <<-CONTENT
|
47
|
+
seven:
|
48
|
+
default: "seven from custom"
|
49
|
+
CONTENT
|
50
|
+
|
51
|
+
allow(File).to receive(:exists?).and_return(true)
|
52
|
+
allow(File).to receive(:exists?).with("config/settings/environments/development.yml").and_return(false)
|
53
|
+
allow(IO).to receive(:read).with("config/settings/default.yml").and_return(defaults)
|
54
|
+
allow(IO).to receive(:read).with("config/settings/environments/test.yml").and_return(test)
|
55
|
+
allow(IO).to receive(:read).with("config/settings/local/custom.yml").and_return(custom)
|
56
|
+
allow(IO).to receive(:read).with("config/settings/local/empty.yml").and_return(empty)
|
57
|
+
|
58
|
+
allow(Dir).to receive(:glob).and_return(["config/settings/local/empty.yml", "config/settings/local/custom.yml"])
|
59
|
+
end
|
metadata
ADDED
@@ -0,0 +1,93 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: fittings
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.2.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Edwin Cruz
|
8
|
+
- Colin Shield
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2011-09-06 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: rspec
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
requirements:
|
18
|
+
- - ">="
|
19
|
+
- !ruby/object:Gem::Version
|
20
|
+
version: '0'
|
21
|
+
type: :development
|
22
|
+
prerelease: false
|
23
|
+
version_requirements: !ruby/object:Gem::Requirement
|
24
|
+
requirements:
|
25
|
+
- - ">="
|
26
|
+
- !ruby/object:Gem::Version
|
27
|
+
version: '0'
|
28
|
+
- !ruby/object:Gem::Dependency
|
29
|
+
name: rake
|
30
|
+
requirement: !ruby/object:Gem::Requirement
|
31
|
+
requirements:
|
32
|
+
- - ">="
|
33
|
+
- !ruby/object:Gem::Version
|
34
|
+
version: '0'
|
35
|
+
type: :development
|
36
|
+
prerelease: false
|
37
|
+
version_requirements: !ruby/object:Gem::Requirement
|
38
|
+
requirements:
|
39
|
+
- - ">="
|
40
|
+
- !ruby/object:Gem::Version
|
41
|
+
version: '0'
|
42
|
+
description: implement custom keys independently of environment
|
43
|
+
email: eng@stitchfix.com
|
44
|
+
executables: []
|
45
|
+
extensions: []
|
46
|
+
extra_rdoc_files:
|
47
|
+
- LICENSE.txt
|
48
|
+
- README.rdoc
|
49
|
+
files:
|
50
|
+
- ".document"
|
51
|
+
- Gemfile
|
52
|
+
- Gemfile.lock
|
53
|
+
- LICENSE.txt
|
54
|
+
- README.rdoc
|
55
|
+
- Rakefile
|
56
|
+
- VERSION
|
57
|
+
- fittings.gemspec
|
58
|
+
- lib/mc-settings.rb
|
59
|
+
- lib/setting.rb
|
60
|
+
- spec/fixtures/joes-colors.yml
|
61
|
+
- spec/fixtures/sample.yml
|
62
|
+
- spec/fixtures/shipping.yml
|
63
|
+
- spec/mc_settings_spec.rb
|
64
|
+
- spec/spec_helper.rb
|
65
|
+
- spec/support/settings_helper.rb
|
66
|
+
homepage: http://github.com/stitchfix/fittings
|
67
|
+
licenses:
|
68
|
+
- MIT
|
69
|
+
metadata: {}
|
70
|
+
post_install_message:
|
71
|
+
rdoc_options: []
|
72
|
+
require_paths:
|
73
|
+
- lib
|
74
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
75
|
+
requirements:
|
76
|
+
- - ">="
|
77
|
+
- !ruby/object:Gem::Version
|
78
|
+
version: '0'
|
79
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
80
|
+
requirements:
|
81
|
+
- - ">="
|
82
|
+
- !ruby/object:Gem::Version
|
83
|
+
version: '0'
|
84
|
+
requirements: []
|
85
|
+
rubyforge_project:
|
86
|
+
rubygems_version: 2.6.13
|
87
|
+
signing_key:
|
88
|
+
specification_version: 4
|
89
|
+
summary: Manage settings per environment
|
90
|
+
test_files:
|
91
|
+
- spec/mc_settings_spec.rb
|
92
|
+
- spec/spec_helper.rb
|
93
|
+
- spec/support/settings_helper.rb
|