better_settings 1.0.0

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