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.
Files changed (45) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +1 -0
  3. data/.travis.yml +14 -6
  4. data/Gemfile +1 -1
  5. data/MIT-LICENSE +6 -5
  6. data/README.md +171 -158
  7. data/Rakefile +4 -9
  8. data/ci/Gemfile-rails-4-2 +7 -0
  9. data/ci/Gemfile-rails-5-0 +6 -0
  10. data/ci/Gemfile-rails-5-1 +6 -0
  11. data/ci/Gemfile-rails-5-2 +6 -0
  12. data/ci/Gemfile-rails-6-0 +6 -0
  13. data/lib/generators/rails_settings/migration/migration_generator.rb +23 -0
  14. data/lib/generators/rails_settings/migration/templates/migration.rb +21 -0
  15. data/lib/ledermann-rails-settings.rb +1 -0
  16. data/lib/rails-settings.rb +21 -5
  17. data/lib/rails-settings/base.rb +48 -0
  18. data/lib/rails-settings/configuration.rb +39 -0
  19. data/lib/rails-settings/scopes.rb +34 -0
  20. data/lib/rails-settings/setting_object.rb +84 -0
  21. data/lib/rails-settings/version.rb +2 -2
  22. data/rails-settings.gemspec +22 -21
  23. data/spec/configuration_spec.rb +120 -0
  24. data/spec/database.yml +3 -0
  25. data/spec/queries_spec.rb +101 -0
  26. data/spec/scopes_spec.rb +31 -0
  27. data/spec/serialize_spec.rb +40 -0
  28. data/spec/setting_object_spec.rb +153 -0
  29. data/spec/settings_spec.rb +248 -0
  30. data/spec/spec_helper.rb +111 -0
  31. data/spec/support/matchers/perform_queries.rb +22 -0
  32. data/spec/support/query_counter.rb +17 -0
  33. metadata +130 -118
  34. data/Changelog.md +0 -17
  35. data/ci/Gemfile.rails-2.3.x +0 -5
  36. data/ci/Gemfile.rails-3.0.x +0 -5
  37. data/ci/Gemfile.rails-3.1.x +0 -5
  38. data/ci/Gemfile.rails-3.2.x +0 -5
  39. data/init.rb +0 -1
  40. data/lib/rails-settings/active_record.rb +0 -38
  41. data/lib/rails-settings/null_store.rb +0 -48
  42. data/lib/rails-settings/scoped_settings.rb +0 -14
  43. data/lib/rails-settings/settings.rb +0 -142
  44. data/test/settings_test.rb +0 -252
  45. data/test/test_helper.rb +0 -34
@@ -0,0 +1,6 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gem 'activerecord', '~> 6.0.0'
4
+ gem "sqlite3", "~> 1.4"
5
+
6
+ gemspec :path => "../"
@@ -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'
@@ -1,5 +1,21 @@
1
- require 'rails-settings/version'
2
- require 'rails-settings/null_store'
3
- require 'rails-settings/active_record'
4
- require 'rails-settings/settings'
5
- require 'rails-settings/scoped_settings'
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 = '1.2.0'
3
- end
2
+ VERSION = '2.5.0'
3
+ end
@@ -1,28 +1,29 @@
1
1
  # -*- encoding: utf-8 -*-
2
- $:.push File.expand_path('../lib', __FILE__)
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 |s|
6
- s.name = 'ledermann-rails-settings'
7
- s.version = RailsSettings::VERSION
8
- s.authors = ['Georg Ledermann']
9
- s.email = ['mail@georg-ledermann.de']
10
- s.homepage = 'https://github.com/ledermann/rails-settings'
11
- s.summary = %q{Settings management for ActiveRecord objects}
12
- s.description = %q{Ruby Gem that makes managing a table of key/value pairs easy. Think of it like a Hash stored in you database, that uses simple ActiveRecord like methods for manipulation.}
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
- s.rubyforge_project = 'ledermann-rails-settings'
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
- s.files = `git ls-files`.split("\n")
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
- # specify any dependencies here; for example:
22
- s.add_development_dependency "rake"
23
- s.add_development_dependency "sqlite3"
24
- # s.add_runtime_dependency "rest-client"
25
- s.add_dependency 'activerecord', '>= 2.3'
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