ettin 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.
- checksums.yaml +7 -0
- data/.gitignore +14 -0
- data/.rspec +3 -0
- data/.rubocop.yml +143 -0
- data/.rubocop_todo.yml +0 -0
- data/.travis.yml +28 -0
- data/Gemfile +17 -0
- data/LICENSE.md +27 -0
- data/README.md +211 -0
- data/Rakefile +8 -0
- data/bin/ettin +47 -0
- data/ettin.gemspec +28 -0
- data/lib/ettin.rb +18 -0
- data/lib/ettin/config_files.rb +26 -0
- data/lib/ettin/deep_transform.rb +67 -0
- data/lib/ettin/hash_factory.rb +23 -0
- data/lib/ettin/key.rb +42 -0
- data/lib/ettin/options.rb +96 -0
- data/lib/ettin/source.rb +29 -0
- data/lib/ettin/sources/hash_source.rb +30 -0
- data/lib/ettin/sources/options_source.rb +31 -0
- data/lib/ettin/sources/yaml_source.rb +40 -0
- data/lib/ettin/version.rb +5 -0
- metadata +110 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 08cf35d041b4f0aa9bec85084709abc89591eff6
|
4
|
+
data.tar.gz: 3dabdab0049ec9b7f05c600c98edb62d46edc258
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: e13bc2b94e3961366bb05cbb6b1e4cfe0ef94c1ac0c2059a92d8c2426e3d438d19fb75ad57c88d4bf25c9883d6f10e54d3eef71c82f4c8f350e55ef786ccef55
|
7
|
+
data.tar.gz: 5302d5cfd460c68789b268ce57f210399fc5d7d6f953b1eb69dc47e1a35bb3352d5a336c658180624a539323c641d0b791d2acaeaeb2d6d51790dadf396d261b
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.rubocop.yml
ADDED
@@ -0,0 +1,143 @@
|
|
1
|
+
inherit_from: .rubocop_todo.yml
|
2
|
+
|
3
|
+
AllCops:
|
4
|
+
DisplayCopNames: true
|
5
|
+
TargetRubyVersion: 2.3
|
6
|
+
Exclude:
|
7
|
+
- '*.gemspec'
|
8
|
+
|
9
|
+
Naming/FileName:
|
10
|
+
ExpectMatchingDefinition: true
|
11
|
+
Exclude:
|
12
|
+
- 'spec/**/*'
|
13
|
+
- 'lib/*/version.rb'
|
14
|
+
|
15
|
+
Style/Documentation:
|
16
|
+
Exclude:
|
17
|
+
- 'spec/**/*'
|
18
|
+
|
19
|
+
# We disable this cop because we want to use Pathname#/
|
20
|
+
# and this cop is not configurable at all.
|
21
|
+
Layout/SpaceAroundOperators:
|
22
|
+
Enabled: false
|
23
|
+
|
24
|
+
Security/YAMLLoad:
|
25
|
+
Exclude:
|
26
|
+
- 'spec/**/*'
|
27
|
+
|
28
|
+
Style/Alias:
|
29
|
+
EnforcedStyle: prefer_alias_method
|
30
|
+
|
31
|
+
Metrics/LineLength:
|
32
|
+
Max: 100
|
33
|
+
AllowHeredoc: true
|
34
|
+
AllowURI: true
|
35
|
+
URISchemes:
|
36
|
+
- http
|
37
|
+
- https
|
38
|
+
|
39
|
+
Metrics/BlockLength:
|
40
|
+
Exclude:
|
41
|
+
- 'spec/**/*_spec.rb'
|
42
|
+
|
43
|
+
Layout/ElseAlignment:
|
44
|
+
Enabled: false
|
45
|
+
|
46
|
+
Layout/FirstParameterIndentation:
|
47
|
+
EnforcedStyle: consistent
|
48
|
+
|
49
|
+
Layout/AlignParameters:
|
50
|
+
EnforcedStyle: with_fixed_indentation
|
51
|
+
|
52
|
+
Layout/CaseIndentation:
|
53
|
+
EnforcedStyle: end
|
54
|
+
|
55
|
+
Layout/ClosingParenthesisIndentation:
|
56
|
+
Enabled: false
|
57
|
+
|
58
|
+
Style/ClassAndModuleChildren:
|
59
|
+
EnforcedStyle: nested
|
60
|
+
|
61
|
+
Style/CommentAnnotation:
|
62
|
+
Enabled: false
|
63
|
+
|
64
|
+
# Does not work for multi-line copyright notices.
|
65
|
+
Style/Copyright:
|
66
|
+
Enabled: false
|
67
|
+
|
68
|
+
Layout/EmptyLineBetweenDefs:
|
69
|
+
AllowAdjacentOneLineDefs: true
|
70
|
+
|
71
|
+
# These two cops do not differentiate between the scope the file is describing
|
72
|
+
# and any namespaces it is nested under. If this is not acceptable,
|
73
|
+
# no_empty_lines produces the least offensive results.
|
74
|
+
Layout/EmptyLinesAroundClassBody:
|
75
|
+
Enabled: false
|
76
|
+
Layout/EmptyLinesAroundModuleBody:
|
77
|
+
Enabled: false
|
78
|
+
|
79
|
+
# Produces poor results.
|
80
|
+
Style/GuardClause:
|
81
|
+
Enabled: false
|
82
|
+
|
83
|
+
Style/IfUnlessModifier:
|
84
|
+
Enabled: false
|
85
|
+
|
86
|
+
Layout/IndentArray:
|
87
|
+
EnforcedStyle: consistent
|
88
|
+
|
89
|
+
Layout/IndentHash:
|
90
|
+
EnforcedStyle: consistent
|
91
|
+
|
92
|
+
Layout/AlignHash:
|
93
|
+
EnforcedColonStyle: table
|
94
|
+
EnforcedHashRocketStyle: table
|
95
|
+
EnforcedLastArgumentHashStyle: always_ignore
|
96
|
+
|
97
|
+
Layout/MultilineMethodCallIndentation:
|
98
|
+
EnforcedStyle: indented
|
99
|
+
|
100
|
+
Layout/MultilineOperationIndentation:
|
101
|
+
EnforcedStyle: indented
|
102
|
+
|
103
|
+
# Produces poor results.
|
104
|
+
Style/Next:
|
105
|
+
Enabled: false
|
106
|
+
|
107
|
+
Style/RedundantReturn:
|
108
|
+
AllowMultipleReturnValues: true
|
109
|
+
|
110
|
+
Style/RegexpLiteral:
|
111
|
+
AllowInnerSlashes: true
|
112
|
+
|
113
|
+
Style/Semicolon:
|
114
|
+
AllowAsExpressionSeparator: true
|
115
|
+
|
116
|
+
Style/StringLiterals:
|
117
|
+
EnforcedStyle: double_quotes
|
118
|
+
|
119
|
+
Style/StringLiteralsInInterpolation:
|
120
|
+
EnforcedStyle: double_quotes
|
121
|
+
|
122
|
+
Layout/SpaceInsideBlockBraces:
|
123
|
+
SpaceBeforeBlockParameters: false
|
124
|
+
|
125
|
+
Style/SymbolArray:
|
126
|
+
EnforcedStyle: brackets
|
127
|
+
|
128
|
+
Lint/BlockAlignment:
|
129
|
+
EnforcedStyleAlignWith: start_of_block
|
130
|
+
#EnforcedStyleAlignWith: start_of_line
|
131
|
+
|
132
|
+
Lint/EndAlignment:
|
133
|
+
EnforcedStyleAlignWith: start_of_line
|
134
|
+
|
135
|
+
Lint/DefEndAlignment:
|
136
|
+
EnforcedStyleAlignWith: def
|
137
|
+
|
138
|
+
Performance/RedundantMerge:
|
139
|
+
Enabled: false
|
140
|
+
|
141
|
+
Style/WordArray:
|
142
|
+
EnforcedStyle: brackets
|
143
|
+
|
data/.rubocop_todo.yml
ADDED
File without changes
|
data/.travis.yml
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
sudo: false
|
2
|
+
language: ruby
|
3
|
+
rvm:
|
4
|
+
- 2.4
|
5
|
+
- 2.3
|
6
|
+
|
7
|
+
branches:
|
8
|
+
only:
|
9
|
+
- master
|
10
|
+
- develop
|
11
|
+
|
12
|
+
env:
|
13
|
+
global:
|
14
|
+
- CC_TEST_REPORTER_ID=dfb947d165289cdfab764c2f143dc0b5097878f4100b5580cca3b06932e04840
|
15
|
+
|
16
|
+
before_install: gem install bundler
|
17
|
+
|
18
|
+
before_script:
|
19
|
+
- curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter
|
20
|
+
- chmod +x ./cc-test-reporter
|
21
|
+
- ./cc-test-reporter before-build
|
22
|
+
|
23
|
+
script:
|
24
|
+
- bundle exec rspec --order=random
|
25
|
+
|
26
|
+
after_script:
|
27
|
+
- ./cc-test-reporter after-build --exit-code $TRAVIS_TEST_RESULT || true
|
28
|
+
|
data/Gemfile
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
source "https://rubygems.org"
|
4
|
+
|
5
|
+
git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
|
6
|
+
|
7
|
+
# Specify your gem's dependencies in ettin.gemspec
|
8
|
+
gemspec
|
9
|
+
|
10
|
+
group :development do
|
11
|
+
gem "rake"
|
12
|
+
gem "rubocop"
|
13
|
+
end
|
14
|
+
|
15
|
+
group :test do
|
16
|
+
gem "simplecov", require: false
|
17
|
+
end
|
data/LICENSE.md
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
Copyright (c) 2018, The Regents of the University of Michigan.
|
2
|
+
All rights reserved.
|
3
|
+
|
4
|
+
Redistribution and use in source and binary forms, with or without
|
5
|
+
modification, are permitted provided that the following conditions are
|
6
|
+
met:
|
7
|
+
|
8
|
+
* Redistributions of source code must retain the above copyright
|
9
|
+
notice, this list of conditions and the following disclaimer.
|
10
|
+
* Redistributions in binary form must reproduce the above copyright
|
11
|
+
notice, this list of conditions and the following disclaimer in the
|
12
|
+
documentation and/or other materials provided with the distribution.
|
13
|
+
* Neither the name of the The University of Michigan nor the
|
14
|
+
names of its contributors may be used to endorse or promote products
|
15
|
+
derived from this software without specific prior written permission.
|
16
|
+
|
17
|
+
THIS SOFTWARE IS PROVIDED BY THE REGENTS OF THE UNIVERSITY OF MICHIGAN AND
|
18
|
+
CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT
|
19
|
+
NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
20
|
+
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OF THE
|
21
|
+
UNIVERSITY OF MICHIGAN BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
22
|
+
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
|
23
|
+
TO,PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
24
|
+
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
25
|
+
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
26
|
+
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
|
27
|
+
EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
data/README.md
ADDED
@@ -0,0 +1,211 @@
|
|
1
|
+
# Ettin
|
2
|
+
|
3
|
+
[](https://travis-ci.org/mlibrary/ettin)
|
4
|
+
[](https://codeclimate.com/github/mlibrary/ettin/maintainability)
|
5
|
+
[](https://codeclimate.com/github/mlibrary/ettin/test_coverage)
|
6
|
+
[](https://gemnasium.com/github.com/mlibrary/ettin)
|
7
|
+
|
8
|
+
## Summary
|
9
|
+
|
10
|
+
Ettin manages loading and accessing settings from your configuration files,
|
11
|
+
in an environment-aware fashion. It has only a single dependency on `deep_merge`,
|
12
|
+
and provides only the functionality you actually need. It does not monkey-patch
|
13
|
+
ruby nor does it pollute the global namespace.
|
14
|
+
|
15
|
+
## Why should I use this over other options?
|
16
|
+
|
17
|
+
* Ettin has far fewer dependencies than the top configuration gems.
|
18
|
+
* Ettin does not pollute the global namespace.
|
19
|
+
* Ettin provides only the features you need; or, put another way, Ettin does
|
20
|
+
not offer you paths that should not be followed.
|
21
|
+
* Ettin is just plain ruby. No magic. No DSL.
|
22
|
+
* Ettin works _everywhere_.
|
23
|
+
|
24
|
+
## Compatibility
|
25
|
+
|
26
|
+
Ettin is compatible with every ruby version, library, and framework. This is
|
27
|
+
possible because it does not rely on any specific runtime enviroment rather
|
28
|
+
than the availability of the ruby core and standard library.
|
29
|
+
|
30
|
+
## Installation
|
31
|
+
|
32
|
+
1. Add it to your bundle and install like any other gem.
|
33
|
+
2. Ettin provides an executable that will create the recommended configuration
|
34
|
+
files for you. These files will be empty. You can also create them yourself,
|
35
|
+
or simply specify your own files when you load Ettin.
|
36
|
+
|
37
|
+
`bundle exec ettin -v -p some/path`
|
38
|
+
|
39
|
+
## Loading Settings
|
40
|
+
|
41
|
+
Ettin is just plain ruby. There's nothing special about the objects it
|
42
|
+
creates or how it creates them. As such, it's up to the application to
|
43
|
+
decide how it provides access to the configuration object. That may seem
|
44
|
+
scary or confusing, but it's not--it's just plain ruby. A few examples
|
45
|
+
are below:
|
46
|
+
|
47
|
+
Assign to a global constant using the default files:
|
48
|
+
|
49
|
+
```ruby
|
50
|
+
Settings = Ettin.for(Ettin.settings_files("config", "development"))
|
51
|
+
```
|
52
|
+
|
53
|
+
Assign with custom files:
|
54
|
+
|
55
|
+
```ruby
|
56
|
+
Settings = Ettin.for("config/path/1.yml", "config/path/2.yml")
|
57
|
+
```
|
58
|
+
|
59
|
+
Declare and assign to a top-level module:
|
60
|
+
|
61
|
+
```ruby
|
62
|
+
module MyApp
|
63
|
+
class << self
|
64
|
+
def config
|
65
|
+
@config ||= Ettin.for(Ettin.settings_files("config"), ENV["MYAPP_ENV"])
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
```
|
70
|
+
|
71
|
+
In a Rails initializer:
|
72
|
+
|
73
|
+
```ruby
|
74
|
+
Rails.application.configure do |config|
|
75
|
+
config.settings = Ettin.for(...)
|
76
|
+
end
|
77
|
+
```
|
78
|
+
|
79
|
+
|
80
|
+
## Default / Recommended Configuration Files
|
81
|
+
|
82
|
+
The provided ettin executable will create the following files,
|
83
|
+
including a file for each environment of production, development,
|
84
|
+
and test.
|
85
|
+
|
86
|
+
The name of the environment is not special, so you can easily create more.
|
87
|
+
|
88
|
+
config/settings.yml
|
89
|
+
config/settings/#{environment}.yml
|
90
|
+
config/environments/#{environment}.yml
|
91
|
+
|
92
|
+
config/settings.local.yml
|
93
|
+
config/settings/#{environment}.local.yml
|
94
|
+
config/environments/#{environment}.local.yml
|
95
|
+
|
96
|
+
Environment-specific settings take precedence over common, and the .local
|
97
|
+
files take precedence over those. The local files are intendended to be gitignored.
|
98
|
+
|
99
|
+
## Using the Settings
|
100
|
+
|
101
|
+
### Access
|
102
|
+
|
103
|
+
Entries are available via dot-notation:
|
104
|
+
|
105
|
+
```ruby
|
106
|
+
config.some_setting #=> 5
|
107
|
+
config.some.nested.setting #=> "my nested string"
|
108
|
+
```
|
109
|
+
|
110
|
+
...or `[]` notation:
|
111
|
+
|
112
|
+
```ruby
|
113
|
+
config[:some_setting] #=> 5
|
114
|
+
config["some_setting"] #=> 5
|
115
|
+
config[:some][:nested][:setting] #=> "my nested string"
|
116
|
+
```
|
117
|
+
|
118
|
+
When a setting is not present, the returned value will be `nil`. We find
|
119
|
+
that this is what most people expect. If you'd like an exception to be
|
120
|
+
thrown, you can use dot-notation with a bang added:
|
121
|
+
|
122
|
+
|
123
|
+
```ruby
|
124
|
+
config.some_missing_setting! #=> raises a KeyError
|
125
|
+
```
|
126
|
+
|
127
|
+
### Assignment
|
128
|
+
|
129
|
+
You can also change settings at runtime via a merge:
|
130
|
+
|
131
|
+
```ruby
|
132
|
+
config.some_setting #=> 5
|
133
|
+
config.merge!({some_setting: 22})
|
134
|
+
config.some_setting #=> 22
|
135
|
+
```
|
136
|
+
|
137
|
+
...or direction assignment:
|
138
|
+
|
139
|
+
|
140
|
+
```ruby
|
141
|
+
config.some_setting #=> 5
|
142
|
+
config.some_setting = 22
|
143
|
+
config.some_setting #=> 22
|
144
|
+
```
|
145
|
+
|
146
|
+
Both of these methods work for any level of nesting.
|
147
|
+
|
148
|
+
|
149
|
+
### ERB
|
150
|
+
|
151
|
+
In Ettin, YAML files support ERB by default.
|
152
|
+
|
153
|
+
|
154
|
+
```ruby
|
155
|
+
# in settings.yml
|
156
|
+
|
157
|
+
redis:
|
158
|
+
hostname: <%= ENV["REDIS_HOST"] %>
|
159
|
+
```
|
160
|
+
|
161
|
+
### Environment-specific Configuration Files
|
162
|
+
|
163
|
+
Environment-specific configuration files are supported. These files
|
164
|
+
take precedence over the common configuration, as you'd expect. The
|
165
|
+
|
166
|
+
## FAQs
|
167
|
+
|
168
|
+
### How can I reload the entire setting object?
|
169
|
+
|
170
|
+
Ettin's settings object is just a plain ruby object, so you should simply
|
171
|
+
assign your settings reference to something else.
|
172
|
+
|
173
|
+
### Why these specific files?
|
174
|
+
|
175
|
+
Ettin is designed to be an easy transition from users of
|
176
|
+
[config](https://github.com/railsconfig/config). We also think that these
|
177
|
+
locations are quite sensible.
|
178
|
+
|
179
|
+
### How do I validate my settings?
|
180
|
+
|
181
|
+
Validation is a concern that is driven by the application itself. Placing the
|
182
|
+
responsibility for that validation in Ettin would violate the single-responsibility
|
183
|
+
principle. You should validate the settings where they're used, such as in an
|
184
|
+
initialization step.
|
185
|
+
|
186
|
+
### How do I load environment variables into my settings?
|
187
|
+
|
188
|
+
Just use ERB. See the
|
189
|
+
[ERB docs](http://ruby-doc.org/stdlib-2.4.2/libdoc/erb/rdoc/ERB.html)
|
190
|
+
for more information.
|
191
|
+
|
192
|
+
### How can I pull in settings from another source?
|
193
|
+
|
194
|
+
Ettin supports hashes and paths to yaml files out of the box. You can extend
|
195
|
+
this support by creating a subclass of `Ettin::Source`. Your subclass will
|
196
|
+
need to define `::handles?(target)`, make a call of `register(self)`, and
|
197
|
+
define a `#load` method that returns a hash.
|
198
|
+
|
199
|
+
|
200
|
+
## Authors
|
201
|
+
|
202
|
+
* This project was inspired by [railsconfig](https://github.com/railsconfig/config).
|
203
|
+
* The author and maintainer is [Bryan Hockey](https://github.com/malakai97)
|
204
|
+
|
205
|
+
## License
|
206
|
+
|
207
|
+
Copyright (c) 2018 The Regents of the University of Michigan.
|
208
|
+
All Rights Reserved.
|
209
|
+
Licensed according to the terms of the Revised BSD License.
|
210
|
+
See LICENSE.md for details.
|
211
|
+
|
data/Rakefile
ADDED
data/bin/ettin
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require "bundler/setup"
|
5
|
+
require "ettin/config_files"
|
6
|
+
require "fileutils"
|
7
|
+
require "optparse"
|
8
|
+
|
9
|
+
options = {}
|
10
|
+
parser = OptionParser.new do |opts|
|
11
|
+
opts.banner = "Creates the standard, empty config files starting from the given path.\n" \
|
12
|
+
"Usage: ettin STARTPATH"
|
13
|
+
|
14
|
+
opts.on("-p", "--path STARTPATH", "The root under which the files will be created.") do |p|
|
15
|
+
options[:start_path] = p
|
16
|
+
end
|
17
|
+
|
18
|
+
opts.on("-v", "--[no-]verbose", "Print paths created") do |v|
|
19
|
+
options[:verbose] = v
|
20
|
+
end
|
21
|
+
|
22
|
+
opts.on("-h", "--help", "Prints this help") do
|
23
|
+
puts opts
|
24
|
+
exit
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
parser.parse!
|
29
|
+
|
30
|
+
unless options[:start_path]
|
31
|
+
puts "The --path option is required"
|
32
|
+
puts parser
|
33
|
+
exit 1
|
34
|
+
end
|
35
|
+
|
36
|
+
["development", "production", "test"]
|
37
|
+
.map {|env| Ettin::ConfigFiles.for(root: options[:start_path], env: env) }
|
38
|
+
.flatten
|
39
|
+
.sort.reverse
|
40
|
+
.uniq
|
41
|
+
.each do |path|
|
42
|
+
path.parent.mkpath
|
43
|
+
FileUtils.touch path.to_s
|
44
|
+
puts path if options[:verbose]
|
45
|
+
end
|
46
|
+
|
47
|
+
puts "" if options[:verbose]
|
data/ettin.gemspec
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
|
2
|
+
lib = File.expand_path("../lib", __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require "ettin/version"
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "ettin"
|
8
|
+
spec.version = Ettin::VERSION
|
9
|
+
spec.authors = ["Bryan Hockey"]
|
10
|
+
spec.email = ["bhock@umich.edu"]
|
11
|
+
|
12
|
+
spec.summary = %q{The best way to add settings in any ruby project.}
|
13
|
+
spec.description = %q{Ettin handles loading environment-specific settings in an easy, simple,
|
14
|
+
and maintainable manner with minimal dependencies or magic.}
|
15
|
+
spec.license = "Revised BSD"
|
16
|
+
|
17
|
+
spec.files = `git ls-files -z`.split("\x0").reject do |f|
|
18
|
+
f.match(%r{^(test|spec|features)/})
|
19
|
+
end
|
20
|
+
spec.bindir = "exe"
|
21
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
22
|
+
spec.require_paths = ["lib"]
|
23
|
+
|
24
|
+
spec.add_runtime_dependency "deep_merge"
|
25
|
+
|
26
|
+
spec.add_development_dependency "bundler"
|
27
|
+
spec.add_development_dependency "rspec"
|
28
|
+
end
|
data/lib/ettin.rb
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "ettin/version"
|
4
|
+
require "ettin/options"
|
5
|
+
require "ettin/hash_factory"
|
6
|
+
require "ettin/config_files"
|
7
|
+
|
8
|
+
# Ettin is the best way to add settings to
|
9
|
+
# any ruby project.
|
10
|
+
module Ettin
|
11
|
+
def self.settings_files(root, env)
|
12
|
+
ConfigFiles.for(root: root, env: env)
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.for(*targets)
|
16
|
+
Options.new(HashFactory.new.build(targets))
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "pathname"
|
4
|
+
|
5
|
+
module Ettin
|
6
|
+
|
7
|
+
# The configuration files for a given root and environment
|
8
|
+
class ConfigFiles
|
9
|
+
|
10
|
+
# @param root [String|Pathname]
|
11
|
+
# @param env [String]
|
12
|
+
# @return [Array<Pathname>]
|
13
|
+
def self.for(root:, env:)
|
14
|
+
root = Pathname.new(root)
|
15
|
+
[
|
16
|
+
root/"settings.yml",
|
17
|
+
root/"settings"/"#{env}.yml",
|
18
|
+
root/"environments"/"#{env}.yml",
|
19
|
+
root/"settings.local.yml",
|
20
|
+
root/"settings"/"#{env}.local.yml",
|
21
|
+
root/"environments"/"#{env}.local.yml"
|
22
|
+
]
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# This copyright notice applies to this file only.
|
4
|
+
# The original source was taken from:
|
5
|
+
# https://github.com/basecamp/deep_hash_transform
|
6
|
+
#
|
7
|
+
# Copyright (c) 2005-2014 David Heinemeier Hansson
|
8
|
+
#
|
9
|
+
# Permission is hereby granted, free of charge, to any person obtaining
|
10
|
+
# a copy of this software and associated documentation files (the
|
11
|
+
# "Software"), to deal in the Software without restriction, including
|
12
|
+
# without limitation the rights to use, copy, modify, merge, publish,
|
13
|
+
# distribute, sublicense, and/or sell copies of the Software, and to
|
14
|
+
# permit persons to whom the Software is furnished to do so, subject to
|
15
|
+
# the following conditions:
|
16
|
+
#
|
17
|
+
# The above copyright notice and this permission notice shall be
|
18
|
+
# included in all copies or substantial portions of the Software.
|
19
|
+
#
|
20
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
21
|
+
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
22
|
+
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
23
|
+
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
24
|
+
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
25
|
+
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
26
|
+
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
27
|
+
|
28
|
+
module Ettin
|
29
|
+
|
30
|
+
# Contains the logic for deep transformation of hash keys
|
31
|
+
module DeepTransform
|
32
|
+
# Returns a new hash with all keys converted by the block operation.
|
33
|
+
# This includes the keys from the root hash and from all
|
34
|
+
# nested hashes.
|
35
|
+
#
|
36
|
+
# hash = { person: { name: 'Rob', age: '28' } }
|
37
|
+
#
|
38
|
+
# hash.deep_transform_keys{ |key| key.to_s.upcase }
|
39
|
+
# # => {"PERSON"=>{"NAME"=>"Rob", "AGE"=>"28"}}
|
40
|
+
unless method_defined?(:deep_transform_keys)
|
41
|
+
def deep_transform_keys(&block)
|
42
|
+
result = {}
|
43
|
+
each do |key, value|
|
44
|
+
result[yield(key)] = value.is_a?(Hash) ? value.deep_transform_keys(&block) : value
|
45
|
+
end
|
46
|
+
result
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# Destructively convert all keys by using the block operation.
|
51
|
+
# This includes the keys from the root hash and from all
|
52
|
+
# nested hashes.
|
53
|
+
unless method_defined?(:deep_transform_keys!)
|
54
|
+
def deep_transform_keys!(&block)
|
55
|
+
keys.each do |key|
|
56
|
+
value = delete(key)
|
57
|
+
self[yield(key)] = value.is_a?(Hash) ? value.deep_transform_keys!(&block) : value
|
58
|
+
end
|
59
|
+
self
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
unless {}.respond_to?(:deep_transform_keys)
|
66
|
+
Hash.include Ettin::DeepTransform
|
67
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "deep_merge"
|
4
|
+
require "ettin/deep_transform"
|
5
|
+
require "ettin/source"
|
6
|
+
require "ettin/key"
|
7
|
+
|
8
|
+
module Ettin
|
9
|
+
|
10
|
+
# Loads and deeply merges targets into a hash structure.
|
11
|
+
class HashFactory
|
12
|
+
def build(*targets)
|
13
|
+
hash = Hash.new(nil)
|
14
|
+
targets
|
15
|
+
.flatten
|
16
|
+
.map {|target| Source.for(target) }
|
17
|
+
.map(&:load)
|
18
|
+
.map {|h| h.deep_transform_keys {|key| Key.new(key) } }
|
19
|
+
.each {|h| hash.deep_merge!(h, overwrite_arrays: true) }
|
20
|
+
hash
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
data/lib/ettin/key.rb
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Ettin
|
4
|
+
|
5
|
+
# Internal hash key that handles strings and symbols as
|
6
|
+
# the same type.
|
7
|
+
class Key
|
8
|
+
def initialize(key)
|
9
|
+
@key = key
|
10
|
+
end
|
11
|
+
|
12
|
+
def inspect
|
13
|
+
to_sym.inspect
|
14
|
+
end
|
15
|
+
|
16
|
+
def class
|
17
|
+
Symbol
|
18
|
+
end
|
19
|
+
|
20
|
+
def to_s
|
21
|
+
key.to_s
|
22
|
+
end
|
23
|
+
|
24
|
+
def to_sym
|
25
|
+
to_s.to_sym
|
26
|
+
end
|
27
|
+
|
28
|
+
def eql?(other)
|
29
|
+
to_sym == other.to_s.to_sym
|
30
|
+
end
|
31
|
+
alias_method :==, :eql?
|
32
|
+
|
33
|
+
def hash
|
34
|
+
key.to_s.to_sym.hash
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
attr_reader :key
|
40
|
+
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,96 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "ettin/key"
|
4
|
+
|
5
|
+
module Ettin
|
6
|
+
|
7
|
+
# An object that holds configuration settings / options
|
8
|
+
class Options
|
9
|
+
include Enumerable
|
10
|
+
extend Forwardable
|
11
|
+
|
12
|
+
def_delegators :@hash, :keys, :empty?
|
13
|
+
|
14
|
+
def initialize(hash)
|
15
|
+
@hash = hash
|
16
|
+
@hash.deep_transform_keys! {|key| key.to_s.to_sym }
|
17
|
+
@hash.default = nil
|
18
|
+
end
|
19
|
+
|
20
|
+
def method_missing(method, *args, &block)
|
21
|
+
super(method, *args, &block) unless respond_to?(method)
|
22
|
+
if bang?(method) && !key?(debang(method))
|
23
|
+
raise KeyError, "key #{debang(method)} not found"
|
24
|
+
else
|
25
|
+
self[debang(method)]
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
# We respond to:
|
30
|
+
# * all methods our parents respond to
|
31
|
+
# * all methods that are mostly alpha-numeric: /^[a-zA-Z_0-9]*$/
|
32
|
+
# * all methods that are mostly alpha-numeric + !: /^[a-zA-Z_0-9]*\!$/
|
33
|
+
def respond_to_missing?(method, include_all = false)
|
34
|
+
super(method, include_all) || /^[a-zA-Z_0-9]*\!?$/.match(method.to_s)
|
35
|
+
end
|
36
|
+
|
37
|
+
def key?(key)
|
38
|
+
hash.key?(Key.new(key))
|
39
|
+
end
|
40
|
+
alias_method :has_key?, :key?
|
41
|
+
|
42
|
+
def merge!(other)
|
43
|
+
hash.deep_merge!(other.to_h, overwrite_arrays: true)
|
44
|
+
end
|
45
|
+
|
46
|
+
def [](key)
|
47
|
+
convert(hash[Key.new(key)])
|
48
|
+
end
|
49
|
+
|
50
|
+
def []=(key, value)
|
51
|
+
hash[Key.new(key)] = value
|
52
|
+
end
|
53
|
+
|
54
|
+
def to_h
|
55
|
+
hash
|
56
|
+
end
|
57
|
+
alias_method :to_hash, :to_h
|
58
|
+
|
59
|
+
def eql?(other)
|
60
|
+
to_h == other.to_h
|
61
|
+
end
|
62
|
+
alias_method :==, :eql?
|
63
|
+
|
64
|
+
def each
|
65
|
+
hash.each {|k, v| yield k, convert(v) }
|
66
|
+
end
|
67
|
+
|
68
|
+
private
|
69
|
+
|
70
|
+
attr_reader :hash
|
71
|
+
|
72
|
+
def bang?(method)
|
73
|
+
method.to_s[-1] == "!"
|
74
|
+
end
|
75
|
+
|
76
|
+
def debang(method)
|
77
|
+
if bang?(method)
|
78
|
+
method.to_s.chop.to_sym
|
79
|
+
else
|
80
|
+
method
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def convert(value)
|
85
|
+
case value
|
86
|
+
when Hash
|
87
|
+
Options.new(value)
|
88
|
+
when Array
|
89
|
+
value.map {|i| convert(i) }
|
90
|
+
else
|
91
|
+
value
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
end
|
data/lib/ettin/source.rb
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Ettin
|
4
|
+
class Source
|
5
|
+
def self.for(target)
|
6
|
+
registry.find {|candidate| candidate.handles?(target) }
|
7
|
+
.new(target)
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.registry
|
11
|
+
@@registry ||= []
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.register(candidate)
|
15
|
+
registry.unshift(candidate)
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.register_default(candidate)
|
19
|
+
registry << candidate
|
20
|
+
end
|
21
|
+
|
22
|
+
def load
|
23
|
+
raise NotImplementedError
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
Dir["#{File.dirname(__FILE__)}/sources/**/*.rb"].each {|f| require f }
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "ettin/source"
|
4
|
+
|
5
|
+
module Ettin
|
6
|
+
module Sources
|
7
|
+
|
8
|
+
# Config data from a ruby hash
|
9
|
+
class HashSource < Source
|
10
|
+
register(self)
|
11
|
+
|
12
|
+
def self.handles?(target)
|
13
|
+
target.is_a? Hash
|
14
|
+
end
|
15
|
+
|
16
|
+
def initialize(hash)
|
17
|
+
@hash = hash.is_a?(Hash) ? hash : {}
|
18
|
+
end
|
19
|
+
|
20
|
+
def load
|
21
|
+
hash
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
attr_reader :hash
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "ettin/options"
|
4
|
+
require "ettin/source"
|
5
|
+
|
6
|
+
module Ettin
|
7
|
+
module Sources
|
8
|
+
|
9
|
+
# Config data from an Ettin::Options
|
10
|
+
class OptionsSource < Source
|
11
|
+
register(self)
|
12
|
+
|
13
|
+
def self.handles?(target)
|
14
|
+
target.is_a? Options
|
15
|
+
end
|
16
|
+
|
17
|
+
def initialize(options)
|
18
|
+
@hash = options.to_h
|
19
|
+
end
|
20
|
+
|
21
|
+
def load
|
22
|
+
hash
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
attr_reader :hash
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "ettin/source"
|
4
|
+
require "erb"
|
5
|
+
require "yaml"
|
6
|
+
|
7
|
+
module Ettin
|
8
|
+
module Sources
|
9
|
+
|
10
|
+
# Config data from a yaml file
|
11
|
+
class YamlSource < Source
|
12
|
+
register_default(self)
|
13
|
+
|
14
|
+
def self.handles?(_target)
|
15
|
+
true
|
16
|
+
end
|
17
|
+
|
18
|
+
def initialize(path)
|
19
|
+
@path = path
|
20
|
+
end
|
21
|
+
|
22
|
+
def load
|
23
|
+
return {} unless File.exist?(path)
|
24
|
+
begin
|
25
|
+
YAML.safe_load(ERB.new(File.read(path)).result) || {}
|
26
|
+
rescue Psych::SyntaxError => e
|
27
|
+
raise "YAML syntax error occurred while parsing #{@path}. " \
|
28
|
+
"Please note that YAML must be consistently indented using " \
|
29
|
+
"spaces. Tabs are not allowed. " \
|
30
|
+
"Error: #{e.message}"
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
attr_reader :path
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
40
|
+
end
|
metadata
ADDED
@@ -0,0 +1,110 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: ettin
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Bryan Hockey
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2018-02-05 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: deep_merge
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: bundler
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rspec
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
description: |-
|
56
|
+
Ettin handles loading environment-specific settings in an easy, simple,
|
57
|
+
and maintainable manner with minimal dependencies or magic.
|
58
|
+
email:
|
59
|
+
- bhock@umich.edu
|
60
|
+
executables: []
|
61
|
+
extensions: []
|
62
|
+
extra_rdoc_files: []
|
63
|
+
files:
|
64
|
+
- ".gitignore"
|
65
|
+
- ".rspec"
|
66
|
+
- ".rubocop.yml"
|
67
|
+
- ".rubocop_todo.yml"
|
68
|
+
- ".travis.yml"
|
69
|
+
- Gemfile
|
70
|
+
- LICENSE.md
|
71
|
+
- README.md
|
72
|
+
- Rakefile
|
73
|
+
- bin/ettin
|
74
|
+
- ettin.gemspec
|
75
|
+
- lib/ettin.rb
|
76
|
+
- lib/ettin/config_files.rb
|
77
|
+
- lib/ettin/deep_transform.rb
|
78
|
+
- lib/ettin/hash_factory.rb
|
79
|
+
- lib/ettin/key.rb
|
80
|
+
- lib/ettin/options.rb
|
81
|
+
- lib/ettin/source.rb
|
82
|
+
- lib/ettin/sources/hash_source.rb
|
83
|
+
- lib/ettin/sources/options_source.rb
|
84
|
+
- lib/ettin/sources/yaml_source.rb
|
85
|
+
- lib/ettin/version.rb
|
86
|
+
homepage:
|
87
|
+
licenses:
|
88
|
+
- Revised BSD
|
89
|
+
metadata: {}
|
90
|
+
post_install_message:
|
91
|
+
rdoc_options: []
|
92
|
+
require_paths:
|
93
|
+
- lib
|
94
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
95
|
+
requirements:
|
96
|
+
- - ">="
|
97
|
+
- !ruby/object:Gem::Version
|
98
|
+
version: '0'
|
99
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - ">="
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0'
|
104
|
+
requirements: []
|
105
|
+
rubyforge_project:
|
106
|
+
rubygems_version: 2.4.5.3
|
107
|
+
signing_key:
|
108
|
+
specification_version: 4
|
109
|
+
summary: The best way to add settings in any ruby project.
|
110
|
+
test_files: []
|