ettin 1.0.0

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