ledermann-rails-settings 1.2.0 → 2.5.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 +7 -0
- data/.gitignore +1 -0
- data/.travis.yml +14 -6
- data/Gemfile +1 -1
- data/MIT-LICENSE +6 -5
- data/README.md +171 -158
- data/Rakefile +4 -9
- data/ci/Gemfile-rails-4-2 +7 -0
- data/ci/Gemfile-rails-5-0 +6 -0
- data/ci/Gemfile-rails-5-1 +6 -0
- data/ci/Gemfile-rails-5-2 +6 -0
- data/ci/Gemfile-rails-6-0 +6 -0
- data/lib/generators/rails_settings/migration/migration_generator.rb +23 -0
- data/lib/generators/rails_settings/migration/templates/migration.rb +21 -0
- data/lib/ledermann-rails-settings.rb +1 -0
- data/lib/rails-settings.rb +21 -5
- data/lib/rails-settings/base.rb +48 -0
- data/lib/rails-settings/configuration.rb +39 -0
- data/lib/rails-settings/scopes.rb +34 -0
- data/lib/rails-settings/setting_object.rb +84 -0
- data/lib/rails-settings/version.rb +2 -2
- data/rails-settings.gemspec +22 -21
- data/spec/configuration_spec.rb +120 -0
- data/spec/database.yml +3 -0
- data/spec/queries_spec.rb +101 -0
- data/spec/scopes_spec.rb +31 -0
- data/spec/serialize_spec.rb +40 -0
- data/spec/setting_object_spec.rb +153 -0
- data/spec/settings_spec.rb +248 -0
- data/spec/spec_helper.rb +111 -0
- data/spec/support/matchers/perform_queries.rb +22 -0
- data/spec/support/query_counter.rb +17 -0
- metadata +130 -118
- data/Changelog.md +0 -17
- data/ci/Gemfile.rails-2.3.x +0 -5
- data/ci/Gemfile.rails-3.0.x +0 -5
- data/ci/Gemfile.rails-3.1.x +0 -5
- data/ci/Gemfile.rails-3.2.x +0 -5
- data/init.rb +0 -1
- data/lib/rails-settings/active_record.rb +0 -38
- data/lib/rails-settings/null_store.rb +0 -48
- data/lib/rails-settings/scoped_settings.rb +0 -14
- data/lib/rails-settings/settings.rb +0 -142
- data/test/settings_test.rb +0 -252
- data/test/test_helper.rb +0 -34
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'rails/generators'
|
2
|
+
require 'rails/generators/migration'
|
3
|
+
|
4
|
+
module RailsSettings
|
5
|
+
class MigrationGenerator < Rails::Generators::Base
|
6
|
+
include Rails::Generators::Migration
|
7
|
+
|
8
|
+
desc "Generates migration for rails-settings"
|
9
|
+
source_root File.expand_path('../templates', __FILE__)
|
10
|
+
|
11
|
+
def create_migration_file
|
12
|
+
migration_template 'migration.rb', 'db/migrate/rails_settings_migration.rb'
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.next_migration_number(dirname)
|
16
|
+
if ActiveRecord::Base.timestamped_migrations
|
17
|
+
Time.now.utc.strftime("%Y%m%d%H%M%S")
|
18
|
+
else
|
19
|
+
"%.3d" % (current_migration_number(dirname) + 1)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
MIGRATION_BASE_CLASS = if ActiveRecord::VERSION::MAJOR >= 5
|
2
|
+
ActiveRecord::Migration[5.0]
|
3
|
+
else
|
4
|
+
ActiveRecord::Migration
|
5
|
+
end
|
6
|
+
|
7
|
+
class RailsSettingsMigration < MIGRATION_BASE_CLASS
|
8
|
+
def self.up
|
9
|
+
create_table :settings do |t|
|
10
|
+
t.string :var, :null => false
|
11
|
+
t.text :value
|
12
|
+
t.references :target, :null => false, :polymorphic => true
|
13
|
+
t.timestamps :null => true
|
14
|
+
end
|
15
|
+
add_index :settings, [ :target_type, :target_id, :var ], :unique => true
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.down
|
19
|
+
drop_table :settings
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
require 'rails-settings'
|
data/lib/rails-settings.rb
CHANGED
@@ -1,5 +1,21 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
1
|
+
module RailsSettings
|
2
|
+
# In Rails 4, attributes can be protected by using the gem `protected_attributes`
|
3
|
+
# In Rails 5, protecting attributes is obsolete (there are `StrongParameters` only)
|
4
|
+
def self.can_protect_attributes?
|
5
|
+
defined?(ProtectedAttributes)
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
require 'rails-settings/setting_object'
|
10
|
+
require 'rails-settings/configuration'
|
11
|
+
require 'rails-settings/base'
|
12
|
+
require 'rails-settings/scopes'
|
13
|
+
|
14
|
+
ActiveRecord::Base.class_eval do
|
15
|
+
def self.has_settings(*args, &block)
|
16
|
+
RailsSettings::Configuration.new(*args.unshift(self), &block)
|
17
|
+
|
18
|
+
include RailsSettings::Base
|
19
|
+
extend RailsSettings::Scopes
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
module RailsSettings
|
2
|
+
module Base
|
3
|
+
def self.included(base)
|
4
|
+
base.class_eval do
|
5
|
+
has_many :setting_objects,
|
6
|
+
:as => :target,
|
7
|
+
:autosave => true,
|
8
|
+
:dependent => :delete_all,
|
9
|
+
:class_name => self.setting_object_class_name
|
10
|
+
|
11
|
+
def settings(var)
|
12
|
+
raise ArgumentError unless var.is_a?(Symbol)
|
13
|
+
raise ArgumentError.new("Unknown key: #{var}") unless self.class.default_settings[var]
|
14
|
+
|
15
|
+
if RailsSettings.can_protect_attributes?
|
16
|
+
setting_objects.detect { |s| s.var == var.to_s } || setting_objects.build({ :var => var.to_s }, :without_protection => true)
|
17
|
+
else
|
18
|
+
setting_objects.detect { |s| s.var == var.to_s } || setting_objects.build(:var => var.to_s, :target => self)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def settings=(value)
|
23
|
+
if value.nil?
|
24
|
+
setting_objects.each(&:mark_for_destruction)
|
25
|
+
else
|
26
|
+
raise ArgumentError
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def settings?(var=nil)
|
31
|
+
if var.nil?
|
32
|
+
setting_objects.any? { |setting_object| !setting_object.marked_for_destruction? && setting_object.value.present? }
|
33
|
+
else
|
34
|
+
settings(var).value.present?
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def to_settings_hash
|
39
|
+
settings_hash = self.class.default_settings.dup
|
40
|
+
settings_hash.each do |var, vals|
|
41
|
+
settings_hash[var] = settings_hash[var].merge(settings(var.to_sym).value)
|
42
|
+
end
|
43
|
+
settings_hash
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module RailsSettings
|
2
|
+
class Configuration
|
3
|
+
def initialize(*args, &block)
|
4
|
+
options = args.extract_options!
|
5
|
+
klass = args.shift
|
6
|
+
keys = args
|
7
|
+
|
8
|
+
raise ArgumentError unless klass
|
9
|
+
|
10
|
+
@klass = klass
|
11
|
+
|
12
|
+
if options[:persistent]
|
13
|
+
@klass.class_attribute :default_settings unless @klass.methods.include?(:default_settings)
|
14
|
+
else
|
15
|
+
@klass.class_attribute :default_settings
|
16
|
+
end
|
17
|
+
|
18
|
+
@klass.class_attribute :setting_object_class_name
|
19
|
+
@klass.default_settings ||= {}
|
20
|
+
@klass.setting_object_class_name = options[:class_name] || 'RailsSettings::SettingObject'
|
21
|
+
|
22
|
+
if block_given?
|
23
|
+
yield(self)
|
24
|
+
else
|
25
|
+
keys.each do |k|
|
26
|
+
key(k)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
raise ArgumentError.new('has_settings: No keys defined') if @klass.default_settings.blank?
|
31
|
+
end
|
32
|
+
|
33
|
+
def key(name, options={})
|
34
|
+
raise ArgumentError.new("has_settings: Symbol expected, but got a #{name.class}") unless name.is_a?(Symbol)
|
35
|
+
raise ArgumentError.new("has_settings: Option :defaults expected, but got #{options.keys.join(', ')}") unless options.blank? || (options.keys == [:defaults])
|
36
|
+
@klass.default_settings[name] = (options[:defaults] || {}).stringify_keys.freeze
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module RailsSettings
|
2
|
+
module Scopes
|
3
|
+
def with_settings
|
4
|
+
result = joins("INNER JOIN settings ON #{settings_join_condition}")
|
5
|
+
|
6
|
+
if ActiveRecord::VERSION::MAJOR < 5
|
7
|
+
result.uniq
|
8
|
+
else
|
9
|
+
result.distinct
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def with_settings_for(var)
|
14
|
+
raise ArgumentError.new('Symbol expected!') unless var.is_a?(Symbol)
|
15
|
+
joins("INNER JOIN settings ON #{settings_join_condition} AND settings.var = '#{var}'")
|
16
|
+
end
|
17
|
+
|
18
|
+
def without_settings
|
19
|
+
joins("LEFT JOIN settings ON #{settings_join_condition}").
|
20
|
+
where('settings.id IS NULL')
|
21
|
+
end
|
22
|
+
|
23
|
+
def without_settings_for(var)
|
24
|
+
raise ArgumentError.new('Symbol expected!') unless var.is_a?(Symbol)
|
25
|
+
joins("LEFT JOIN settings ON #{settings_join_condition} AND settings.var = '#{var}'").
|
26
|
+
where('settings.id IS NULL')
|
27
|
+
end
|
28
|
+
|
29
|
+
def settings_join_condition
|
30
|
+
"settings.target_id = #{table_name}.#{primary_key} AND
|
31
|
+
settings.target_type = '#{base_class.name}'"
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
module RailsSettings
|
2
|
+
class SettingObject < ActiveRecord::Base
|
3
|
+
self.table_name = 'settings'
|
4
|
+
|
5
|
+
belongs_to :target, :polymorphic => true
|
6
|
+
|
7
|
+
validates_presence_of :var, :target_type
|
8
|
+
validate do
|
9
|
+
errors.add(:value, "Invalid setting value") unless value.is_a? Hash
|
10
|
+
|
11
|
+
unless _target_class.default_settings[var.to_sym]
|
12
|
+
errors.add(:var, "#{var} is not defined!")
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
serialize :value, Hash
|
17
|
+
|
18
|
+
if RailsSettings.can_protect_attributes?
|
19
|
+
# attr_protected can not be used here because it touches the database which is not connected yet.
|
20
|
+
# So allow no attributes and override <tt>#sanitize_for_mass_assignment</tt>
|
21
|
+
attr_accessible
|
22
|
+
end
|
23
|
+
|
24
|
+
REGEX_SETTER = /\A([a-z]\w+)=\Z/i
|
25
|
+
REGEX_GETTER = /\A([a-z]\w+)\Z/i
|
26
|
+
|
27
|
+
def respond_to?(method_name, include_priv=false)
|
28
|
+
super || method_name.to_s =~ REGEX_SETTER || _setting?(method_name)
|
29
|
+
end
|
30
|
+
|
31
|
+
def method_missing(method_name, *args, &block)
|
32
|
+
if block_given?
|
33
|
+
super
|
34
|
+
else
|
35
|
+
if attribute_names.include?(method_name.to_s.sub('=',''))
|
36
|
+
super
|
37
|
+
elsif method_name.to_s =~ REGEX_SETTER && args.size == 1
|
38
|
+
_set_value($1, args.first)
|
39
|
+
elsif method_name.to_s =~ REGEX_GETTER && args.size == 0
|
40
|
+
_get_value($1)
|
41
|
+
else
|
42
|
+
super
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
protected
|
48
|
+
if RailsSettings.can_protect_attributes?
|
49
|
+
# Simulate attr_protected by removing all regular attributes
|
50
|
+
def sanitize_for_mass_assignment(attributes, role = nil)
|
51
|
+
attributes.except('id', 'var', 'value', 'target_id', 'target_type', 'created_at', 'updated_at')
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
private
|
56
|
+
def _get_value(name)
|
57
|
+
if value[name].nil?
|
58
|
+
_target_class.default_settings[var.to_sym][name]
|
59
|
+
else
|
60
|
+
value[name]
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def _set_value(name, v)
|
65
|
+
if value[name] != v
|
66
|
+
value_will_change!
|
67
|
+
|
68
|
+
if v.nil?
|
69
|
+
value.delete(name)
|
70
|
+
else
|
71
|
+
value[name] = v
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def _target_class
|
77
|
+
target_type.constantize
|
78
|
+
end
|
79
|
+
|
80
|
+
def _setting?(method_name)
|
81
|
+
_target_class.default_settings[var.to_sym].keys.include?(method_name.to_s)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
@@ -1,3 +1,3 @@
|
|
1
1
|
module RailsSettings
|
2
|
-
VERSION = '
|
3
|
-
end
|
2
|
+
VERSION = '2.5.0'
|
3
|
+
end
|
data/rails-settings.gemspec
CHANGED
@@ -1,28 +1,29 @@
|
|
1
1
|
# -*- encoding: utf-8 -*-
|
2
|
-
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
3
4
|
require 'rails-settings/version'
|
4
5
|
|
5
|
-
Gem::Specification.new do |
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
6
|
+
Gem::Specification.new do |gem|
|
7
|
+
gem.name = 'ledermann-rails-settings'
|
8
|
+
gem.version = RailsSettings::VERSION
|
9
|
+
gem.licenses = ['MIT']
|
10
|
+
gem.authors = ['Georg Ledermann']
|
11
|
+
gem.email = ['mail@georg-ledermann.de']
|
12
|
+
gem.description = %q{Settings gem for Ruby on Rails}
|
13
|
+
gem.summary = %q{Ruby gem to handle settings for ActiveRecord instances by storing them as serialized Hash in a separate database table. Namespaces and defaults included.}
|
14
|
+
gem.homepage = 'https://github.com/ledermann/rails-settings'
|
15
|
+
gem.required_ruby_version = '>= 2.4'
|
13
16
|
|
14
|
-
|
17
|
+
gem.files = `git ls-files`.split($/)
|
18
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
19
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
20
|
+
gem.require_paths = ['lib']
|
15
21
|
|
16
|
-
|
17
|
-
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
18
|
-
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
19
|
-
s.require_paths = ['lib']
|
22
|
+
gem.add_dependency 'activerecord', '>= 4.2'
|
20
23
|
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
s.add_development_dependency 'rake'
|
27
|
-
s.add_development_dependency 'sqlite3'
|
24
|
+
gem.add_development_dependency 'rake'
|
25
|
+
gem.add_development_dependency 'sqlite3'
|
26
|
+
gem.add_development_dependency 'rspec'
|
27
|
+
gem.add_development_dependency 'coveralls'
|
28
|
+
gem.add_development_dependency 'simplecov', '>= 0.11.2'
|
28
29
|
end
|
@@ -0,0 +1,120 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module RailsSettings
|
4
|
+
class Dummy
|
5
|
+
end
|
6
|
+
|
7
|
+
describe Configuration, 'successful' do
|
8
|
+
it "should define single key" do
|
9
|
+
Configuration.new(Dummy, :dashboard)
|
10
|
+
|
11
|
+
expect(Dummy.default_settings).to eq({ :dashboard => {} })
|
12
|
+
expect(Dummy.setting_object_class_name).to eq('RailsSettings::SettingObject')
|
13
|
+
end
|
14
|
+
|
15
|
+
it "should define multiple keys" do
|
16
|
+
Configuration.new(Dummy, :dashboard, :calendar)
|
17
|
+
|
18
|
+
expect(Dummy.default_settings).to eq({ :dashboard => {}, :calendar => {} })
|
19
|
+
expect(Dummy.setting_object_class_name).to eq('RailsSettings::SettingObject')
|
20
|
+
end
|
21
|
+
|
22
|
+
it "should define single key with class_name" do
|
23
|
+
Configuration.new(Dummy, :dashboard, :class_name => 'MyClass')
|
24
|
+
expect(Dummy.default_settings).to eq({ :dashboard => {} })
|
25
|
+
expect(Dummy.setting_object_class_name).to eq('MyClass')
|
26
|
+
end
|
27
|
+
|
28
|
+
it "should define multiple keys with class_name" do
|
29
|
+
Configuration.new(Dummy, :dashboard, :calendar, :class_name => 'MyClass')
|
30
|
+
|
31
|
+
expect(Dummy.default_settings).to eq({ :dashboard => {}, :calendar => {} })
|
32
|
+
expect(Dummy.setting_object_class_name).to eq('MyClass')
|
33
|
+
end
|
34
|
+
|
35
|
+
it "should define using block" do
|
36
|
+
Configuration.new(Dummy) do |c|
|
37
|
+
c.key :dashboard
|
38
|
+
c.key :calendar
|
39
|
+
end
|
40
|
+
|
41
|
+
expect(Dummy.default_settings).to eq({ :dashboard => {}, :calendar => {} })
|
42
|
+
expect(Dummy.setting_object_class_name).to eq('RailsSettings::SettingObject')
|
43
|
+
end
|
44
|
+
|
45
|
+
it "should define using block with defaults" do
|
46
|
+
Configuration.new(Dummy) do |c|
|
47
|
+
c.key :dashboard, :defaults => { :theme => 'red' }
|
48
|
+
c.key :calendar, :defaults => { :scope => 'all' }
|
49
|
+
end
|
50
|
+
|
51
|
+
expect(Dummy.default_settings).to eq({ :dashboard => { 'theme' => 'red' }, :calendar => { 'scope' => 'all'} })
|
52
|
+
expect(Dummy.setting_object_class_name).to eq('RailsSettings::SettingObject')
|
53
|
+
end
|
54
|
+
|
55
|
+
it "should define using block and class_name" do
|
56
|
+
Configuration.new(Dummy, :class_name => 'MyClass') do |c|
|
57
|
+
c.key :dashboard
|
58
|
+
c.key :calendar
|
59
|
+
end
|
60
|
+
|
61
|
+
expect(Dummy.default_settings).to eq({ :dashboard => {}, :calendar => {} })
|
62
|
+
expect(Dummy.setting_object_class_name).to eq('MyClass')
|
63
|
+
end
|
64
|
+
|
65
|
+
context 'persistent' do
|
66
|
+
it "should keep settings between multiple configurations initialization" do
|
67
|
+
Configuration.new(Dummy, :persistent => true) do |c|
|
68
|
+
c.key :dashboard, :defaults => { :theme => 'red' }
|
69
|
+
end
|
70
|
+
|
71
|
+
Configuration.new(Dummy, :calendar, :persistent => true)
|
72
|
+
|
73
|
+
expect(Dummy.default_settings).to eq({ :dashboard => { 'theme' => 'red' }, :calendar => {} })
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
describe Configuration, 'failure' do
|
79
|
+
it "should fail without args" do
|
80
|
+
expect {
|
81
|
+
Configuration.new
|
82
|
+
}.to raise_error(ArgumentError)
|
83
|
+
end
|
84
|
+
|
85
|
+
it "should fail without keys" do
|
86
|
+
expect {
|
87
|
+
Configuration.new(Dummy)
|
88
|
+
}.to raise_error(ArgumentError)
|
89
|
+
end
|
90
|
+
|
91
|
+
it "should fail without keys in block" do
|
92
|
+
expect {
|
93
|
+
Configuration.new(Dummy) do |c|
|
94
|
+
end
|
95
|
+
}.to raise_error(ArgumentError)
|
96
|
+
end
|
97
|
+
|
98
|
+
it "should fail with keys not being symbols" do
|
99
|
+
expect {
|
100
|
+
Configuration.new(Dummy, 42, "string")
|
101
|
+
}.to raise_error(ArgumentError)
|
102
|
+
end
|
103
|
+
|
104
|
+
it "should fail with keys not being symbols" do
|
105
|
+
expect {
|
106
|
+
Configuration.new(Dummy) do |c|
|
107
|
+
c.key 42, "string"
|
108
|
+
end
|
109
|
+
}.to raise_error(ArgumentError)
|
110
|
+
end
|
111
|
+
|
112
|
+
it "should fail with unknown option" do
|
113
|
+
expect {
|
114
|
+
Configuration.new(Dummy) do |c|
|
115
|
+
c.key :dashboard, :foo => {}
|
116
|
+
end
|
117
|
+
}.to raise_error(ArgumentError)
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|