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 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
@@ -0,0 +1,14 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+
10
+ # Ignore because we're a gem
11
+ /Gemfile.lock
12
+
13
+ # rspec failure tracking
14
+ .rspec_status
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --require spec_helper
2
+ --format documentation
3
+ --color
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
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ task default: :spec
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
@@ -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
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ettin
4
+ VERSION = "1.0.0"
5
+ 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: []