ledermann-rails-settings 1.2.0 → 2.5.0

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