ettin 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
[![Build Status](https://travis-ci.org/mlibrary/ettin.svg?branch=master)](https://travis-ci.org/mlibrary/ettin)
|
4
|
+
[![Maintainability](https://api.codeclimate.com/v1/badges/efd34b151bad7dacb994/maintainability)](https://codeclimate.com/github/mlibrary/ettin/maintainability)
|
5
|
+
[![Test Coverage](https://api.codeclimate.com/v1/badges/efd34b151bad7dacb994/test_coverage)](https://codeclimate.com/github/mlibrary/ettin/test_coverage)
|
6
|
+
[![Dependency Status](https://gemnasium.com/badges/github.com/mlibrary/ettin.svg)](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: []
|