better_settings 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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 838170a01ea177f40b02a01a7e7308c6b1cf6528
4
+ data.tar.gz: 94ddb23c277b6ae2ba9c9eb12cfab77635163d87
5
+ SHA512:
6
+ metadata.gz: 0241b28bfe8bdc7d143c21dbf7526b88142bae2e958c4a9c85a8369ff14c390383a3db14abff98122fc7e2593384d2b0ca3b52fc6a3471700476f9eba173c161
7
+ data.tar.gz: 254487a597d7e78ca6569d2df82634c48551dfa2715a49533d29bfcd11ade1472df41eebf5cc5d7661fa3a1ba5dfd6610759e21663dc3b8da2ffa03e3aa68f41
@@ -0,0 +1,137 @@
1
+ BetterSettings [![Gem Version](https://img.shields.io/gem/v/better_settings.svg?colorB=e9573f)](https://rubygems.org/gems/better_settings) [![Build Status](https://travis-ci.org/ElMassimo/better_settings.svg)](https://travis-ci.org/ElMassimo/better_settings) [![Coverage Status](https://coveralls.io/repos/github/ElMassimo/better_settings/badge.svg?branch=master)](https://coveralls.io/github/ElMassimo/better_settings?branch=master) [![Inline docs](http://inch-ci.org/github/ElMassimo/better_settings.svg)](http://inch-ci.org/github/ElMassimo/better_settings) [![License](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/ElMassimo/better_settings/blob/master/LICENSE.txt)
2
+ =======================================
3
+
4
+ A robust settings library that can read YML files and provide an immutable object allowing to access settings through method calls. Can be used in __any Ruby app__, __not just Rails__.
5
+
6
+ ### Installation
7
+
8
+ Add this line to your application's Gemfile:
9
+
10
+ ```ruby
11
+ gem 'better_settings'
12
+ ```
13
+
14
+ And then execute:
15
+
16
+ $ bundle
17
+
18
+ Or install it yourself as:
19
+
20
+ $ gem install better_settings
21
+
22
+ ### Usage
23
+
24
+ #### 1. Define a class
25
+
26
+ Instead of defining a Settings constant for you, that task is left to you. Simply create a class in your application
27
+ that looks like:
28
+
29
+ ```ruby
30
+ # app/models/settings.rb
31
+ class Settings < BetterSettings
32
+ source Rails.root.join('config', 'application.yml'), namespace: Rails.env
33
+ end
34
+ ```
35
+
36
+ #### 2. Create your settings
37
+
38
+ Notice above we specified an absolute path to our settings file called `application.yml`. This is just a typical YAML file.
39
+ Also notice above that we specified a namespace for our environment. A namespace is just an optional string that corresponds to a key in the YAML file.
40
+
41
+ Using a namespace allows us to change our configuration depending on our environment:
42
+
43
+ ```yaml
44
+ # config/application.yml
45
+ defaults: &defaults
46
+ port: 80
47
+ mailer:
48
+ root: www.example.com
49
+ dynamic: <%= "Did you know you can use ERB inside the YML file? Env is #{ Rails.env }." %>
50
+
51
+ development:
52
+ <<: *defaults
53
+ port: 3000
54
+
55
+ test:
56
+ <<: *defaults
57
+
58
+ production:
59
+ <<: *defaults
60
+ ```
61
+
62
+ #### 3. Access your settings
63
+
64
+ >> Rails.env
65
+ => "development"
66
+
67
+ >> Settings.mailer
68
+ => "#<Settings ... >"
69
+
70
+ >> Settings.mailer.root
71
+ => "www.example.com
72
+
73
+ >> Settings.port
74
+ => 3000
75
+
76
+ >> Settings.dynamic
77
+ => "Did you know you can use ERB inside the YML file? Env is development."
78
+
79
+ You can use these settings anywhere, for example in a model:
80
+
81
+ class Post < ActiveRecord::Base
82
+ self.per_page = Settings.pagination.posts_per_page
83
+ end
84
+
85
+ ### Advanced Setup ⚙
86
+ Name it `Settings`, name it `Config`, name it whatever you want. Add as many or as few as you like, read from as many files as necessary (nested keys will be merged).
87
+
88
+ We usually read a few optional files for the `development` and `test` environment, which allows each developer to override some settings in their own local environment (we git ignore `development.yml` and `test.yml`).
89
+
90
+ ```ruby
91
+ # app/models/settings.rb
92
+ class Settings < BetterSettings
93
+ source Rails.root.join('config', 'application.yml'), namespace: Rails.env
94
+ source Rails.root.join('config', 'development.yml'), namespace: Rails.env, optional: true if Rails.env.development?
95
+ source Rails.root.join('config', 'test.yml'), namespace: Rails.env, optional: true if Rails.env.test?
96
+ end
97
+ ```
98
+ Our `application.yml` looks like this:
99
+ ```yaml
100
+ # application.yml
101
+ defaults: &defaults
102
+ auto_logout: false
103
+ secret_key_base: 'fake_secret_key_base'
104
+
105
+ server_defaults: &server_defaults
106
+ <<: *defaults
107
+ auto_logout: true
108
+ secret_key: <%= ENV['SECRET_KEY'] %>
109
+
110
+ development:
111
+ <<: *defaults
112
+ host: 'localhost'
113
+
114
+ test:
115
+ <<: *defaults
116
+ host: '127.0.0.1'
117
+
118
+ staging:
119
+ <<: *server_defaults
120
+ host: 'staging.example.com'
121
+
122
+ production:
123
+ <<: *server_defaults
124
+ host: 'example.com'
125
+ ```
126
+ A developer might want to override some settings by defining a `development.yml` such as:
127
+ ```yaml
128
+ development:
129
+ auto_logout: true
130
+ ````
131
+ The main advantage is that those changes won't be tracked by source control :smiley:
132
+
133
+ ## Opinionated Design
134
+ After using [settingslogic](https://github.com/settingslogic/settingslogic) for a long time, we learned some lessons, which are distilled in the following decisions:
135
+ - __Immutability:__ Once created settings can't be modified.
136
+ - __No Optional Setings:__ Any optional setting can be modeled in a safer way, this library doesn't allow them.
137
+ - __Not Tied to a Source File:__ Useful to create multiple environment-specific files.
@@ -0,0 +1,110 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'yaml'
4
+ require 'erb'
5
+ require 'open-uri'
6
+ require 'forwardable'
7
+
8
+ # Public: Rewrite of BetterSettings to enforce fail-fast and immutability, and
9
+ # avoid extending a core class like Hash which can be problematic.
10
+ class BetterSettings
11
+ extend Forwardable
12
+
13
+ VALID_SETTING_NAME = /^\w+$/
14
+ RESERVED_METHODS = %w[
15
+ settings
16
+ root_settings
17
+ ]
18
+
19
+ attr_reader :settings
20
+ def_delegators :settings, :to_h, :to_hash
21
+
22
+ # Public: Initializes a new settings object from a Hash or compatible object.
23
+ def initialize(hash, parent:)
24
+ @settings = hash.to_h.freeze
25
+ @parent = parent
26
+
27
+ # Create a getter method for each setting.
28
+ @settings.each { |key, value| create_accessor(key, value) }
29
+ end
30
+
31
+ # Internal: Returns a new Better Settings instance that combines the settings.
32
+ def merge(other_settings)
33
+ self.class.new(deep_merge(@settings, other_settings.to_h), parent: @parent)
34
+ end
35
+
36
+ # Internal: Display explicit errors for typos and missing settings.
37
+ # rubocop:disable Style/MethodMissing
38
+ def method_missing(name, *)
39
+ raise MissingSetting, "Missing setting '#{ name }' in #{ @parent }"
40
+ end
41
+
42
+ private
43
+
44
+ # Internal: Wrap nested hashes as settings to allow accessing keys as methods.
45
+ def auto_wrap(key, value)
46
+ case value
47
+ when Hash then self.class.new(value, parent: "'#{ key }' section in #{ @parent }")
48
+ when Array then value.map { |item| auto_wrap(key, item) }.freeze
49
+ else value.freeze
50
+ end
51
+ end
52
+
53
+ # Internal: Defines a getter for the specified setting.
54
+ def create_accessor(key, value)
55
+ raise InvalidSettingKey if !key.is_a?(String) || key !~ VALID_SETTING_NAME || RESERVED_METHODS.include?(key)
56
+ instance_variable_set("@#{ key }", auto_wrap(key, value))
57
+ singleton_class.send(:attr_reader, key)
58
+ end
59
+
60
+ # Internal: Recursively merges two hashes (in case ActiveSupport is not available).
61
+ def deep_merge(this_hash, other_hash)
62
+ this_hash.merge(other_hash) do |key, this_val, other_val|
63
+ if this_val.is_a?(Hash) && other_val.is_a?(Hash)
64
+ deep_merge(this_val, other_val)
65
+ else
66
+ other_val
67
+ end
68
+ end
69
+ end
70
+
71
+ class MissingSetting < StandardError; end
72
+ class InvalidSettingKey < StandardError; end
73
+
74
+ class << self
75
+ extend Forwardable
76
+ def_delegators :root_settings, :to_h, :to_hash, :method_missing
77
+
78
+ # Public: Loads a file as settings (merges it with any previously loaded settings).
79
+ def source(file_name, namespace: false, optional: false)
80
+ return if !File.exist?(file_name) && optional
81
+
82
+ # Load the specified yaml file and instantiate a Settings object.
83
+ settings = new(yaml_to_hash(file_name), parent: file_name)
84
+
85
+ # Take one of the settings keys if one is specified.
86
+ settings = settings.public_send(namespace) if namespace
87
+
88
+ # Merge settings if a source had previously been specified.
89
+ @root_settings = @root_settings ? @root_settings.merge(settings) : settings
90
+
91
+ # Allow to call any settings methods directly on the class.
92
+ singleton_class.extend(Forwardable)
93
+ singleton_class.def_delegators :root_settings, *@root_settings.settings.keys
94
+ end
95
+
96
+ private
97
+
98
+ # Internal: Methods called at the class level are delegated to this instance.
99
+ def root_settings
100
+ raise ArgumentError, '`source` must be specified for the settings' unless defined?(@root_settings)
101
+ @root_settings
102
+ end
103
+
104
+ # Internal: Parses a yml file that can optionally use ERB templating.
105
+ def yaml_to_hash(file_name)
106
+ return {} if (content = open(file_name).read).empty?
107
+ YAML.load(ERB.new(content).result).to_hash
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ class BetterSettings
4
+ VERSION = '1.0.0'
5
+ end
@@ -0,0 +1,126 @@
1
+ require 'spec_helper'
2
+ require 'support/settings'
3
+
4
+ describe BetterSettings do
5
+ def new_settings(value)
6
+ Settings.new(value, parent: 'new_settings')
7
+ end
8
+
9
+ it 'should access settings' do
10
+ expect(Settings.setting2).to eq 5
11
+ end
12
+
13
+ it 'should access nested settings' do
14
+ expect(Settings.setting1.setting1_child).to eq 'saweet'
15
+ end
16
+
17
+ it 'should access settings in nested arrays' do
18
+ expect(Settings.array.first.name).to eq 'first'
19
+ end
20
+
21
+ it 'should access deep nested settings' do
22
+ expect(Settings.setting1.deep.another).to eq 'my value'
23
+ end
24
+
25
+ it 'should access extra deep nested settings' do
26
+ expect(Settings.setting1.deep.child.value).to eq 2
27
+ end
28
+
29
+ it 'should enable erb' do
30
+ expect(Settings.setting3).to eq 25
31
+ end
32
+
33
+ it 'should namespace settings' do
34
+ expect(DevSettings.language.haskell.paradigm).to eq 'functional'
35
+ expect(DevSettings.language.smalltalk.paradigm).to eq 'object-oriented'
36
+ expect(DevSettings.environment).to eq 'development'
37
+ end
38
+
39
+ it 'should distinguish nested keys' do
40
+ expect(Settings.language.haskell.paradigm).to eq 'functional'
41
+ expect(Settings.language.smalltalk.paradigm).to eq 'object oriented'
42
+ end
43
+
44
+ it 'should not override global methods' do
45
+ expect(Settings.global).to eq 'GLOBAL'
46
+ expect(Settings.custom).to eq 'CUSTOM'
47
+ end
48
+
49
+ it 'should raise a helpful error message' do
50
+ expect {
51
+ Settings.missing
52
+ }.to raise_error(BetterSettings::MissingSetting, /Missing setting 'missing' in/)
53
+ expect {
54
+ Settings.language.missing
55
+ }.to raise_error(BetterSettings::MissingSetting, /Missing setting 'missing' in 'language' section/)
56
+ end
57
+
58
+ it 'should raise an error on a nil source argument' do
59
+ expect { NoSource.foo.bar }.to raise_error(ArgumentError, '`source` must be specified for the settings')
60
+ end
61
+
62
+ it 'should support instance usage as well' do
63
+ expect(new_settings(Settings.setting1).setting1_child).to eq 'saweet'
64
+ end
65
+
66
+ it 'should handle invalid name settings' do
67
+ expect {
68
+ new_settings('some-dash-setting#' => 'dashtastic')
69
+ }.to raise_error(BetterSettings::InvalidSettingKey)
70
+ end
71
+
72
+ it 'should handle settings with nil value' do
73
+ expect(Settings.nil).to eq nil
74
+ end
75
+
76
+ it 'should handle settings with false value' do
77
+ expect(Settings.false).to eq false
78
+ end
79
+
80
+ # If .name is called on BetterSettings itself, handle appropriately
81
+ # by delegating to Hash
82
+ it 'should have the parent class always respond with Module.name' do
83
+ expect(BetterSettings.name).to eq 'BetterSettings'
84
+ end
85
+
86
+ # If .name is not a property, delegate to superclass
87
+ it 'should respond with Module.name' do
88
+ expect(DevSettings.name).to eq 'DevSettings'
89
+ end
90
+
91
+ # If .name is a property, respond with that instead of delegating to superclass
92
+ it 'should allow a name setting to be overriden' do
93
+ expect(Settings.name).to eq 'test'
94
+ end
95
+
96
+ describe 'to_h' do
97
+ it 'should handle empty file' do
98
+ expect(NoSettings.to_h).to be_empty
99
+ end
100
+
101
+ it 'should be similar to the internal representation' do
102
+ expect(settings = Settings.send(:root_settings)).to be_is_a(Settings)
103
+ expect(hash = settings.send(:settings)).to be_is_a(Hash)
104
+ expect(Settings.to_h).to eq hash
105
+ end
106
+
107
+ it 'should not mutate the original when getting a copy' do
108
+ result = Settings.language.to_h.merge('haskell' => 'awesome')
109
+ expect(result.class).to eq Hash
110
+ expect(result).to eq(
111
+ 'haskell' => 'awesome',
112
+ 'smalltalk' => { 'paradigm' => 'object oriented' },
113
+ )
114
+ expect(Settings.language.haskell.paradigm).to eq('functional')
115
+ expect(Settings.language).not_to eq Settings.language.merge('paradigm' => 'functional')
116
+ end
117
+ end
118
+
119
+ describe '#to_hash' do
120
+ it 'should return a new instance of a Hash object' do
121
+ expect(Settings.to_hash).to be_kind_of(Hash)
122
+ expect(Settings.to_hash.class.name).to eq 'Hash'
123
+ expect(Settings.to_hash.object_id).not_to eq Settings.object_id
124
+ end
125
+ end
126
+ end
@@ -0,0 +1,8 @@
1
+ require 'simplecov'
2
+ require 'coveralls'
3
+ SimpleCov.start { add_filter '/spec/' }
4
+ Coveralls.wear!
5
+
6
+ require 'better_settings'
7
+ require 'rspec/given'
8
+ require 'pry-byebug'
@@ -0,0 +1,25 @@
1
+ class Settings < BetterSettings
2
+ source "#{ File.dirname(__FILE__) }/settings.yml"
3
+ source "#{File.dirname(__FILE__)}/settings_empty.yml"
4
+
5
+ def self.custom
6
+ 'CUSTOM'
7
+ end
8
+
9
+ def self.global
10
+ 'GLOBAL'
11
+ end
12
+ end
13
+
14
+ class DevSettings < BetterSettings
15
+ source "#{ File.dirname(__FILE__) }/settings.yml", namespace: :development
16
+ source "#{ File.dirname(__FILE__) }/dev.yml", namespace: 'development'
17
+ end
18
+
19
+ class NoSettings < BetterSettings
20
+ source "#{File.dirname(__FILE__)}/settings_empty.yml", optional: true
21
+ source "#{File.dirname(__FILE__)}/settings_none.yml", optional: true
22
+ end
23
+
24
+ class NoSource < BetterSettings
25
+ end
metadata ADDED
@@ -0,0 +1,111 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: better_settings
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Máximo Mussini
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2017-12-29 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: coveralls
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :development
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: pry-byebug
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: rake
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
+ - !ruby/object:Gem::Dependency
56
+ name: rspec-given
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '3.0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '3.0'
69
+ description: Settings solution for Rails applications that can read YAML files (ERB-enabled)
70
+ and allows to access using method calls.
71
+ email:
72
+ - maximomussini@gmail.com
73
+ executables: []
74
+ extensions: []
75
+ extra_rdoc_files:
76
+ - README.md
77
+ files:
78
+ - README.md
79
+ - lib/better_settings.rb
80
+ - lib/better_settings/version.rb
81
+ - spec/better_settings/better_settings_spec.rb
82
+ - spec/spec_helper.rb
83
+ - spec/support/settings.rb
84
+ homepage: https://github.com/ElMassimo/better_settings
85
+ licenses:
86
+ - MIT
87
+ metadata: {}
88
+ post_install_message:
89
+ rdoc_options: []
90
+ require_paths:
91
+ - lib
92
+ required_ruby_version: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '2.2'
97
+ required_rubygems_version: !ruby/object:Gem::Requirement
98
+ requirements:
99
+ - - ">="
100
+ - !ruby/object:Gem::Version
101
+ version: '0'
102
+ requirements: []
103
+ rubyforge_project:
104
+ rubygems_version: 2.6.11
105
+ signing_key:
106
+ specification_version: 4
107
+ summary: 'Settings for Rails applications: simple, immutable, better.'
108
+ test_files:
109
+ - spec/better_settings/better_settings_spec.rb
110
+ - spec/spec_helper.rb
111
+ - spec/support/settings.rb