├── .github
└── workflows
│ └── main.yml
├── .gitignore
├── Gemfile
├── MIT-LICENSE
├── README.md
├── Rakefile
├── gemfiles
├── rails_6_1.gemfile
├── rails_7_0.gemfile
└── rails_7_1.gemfile
├── lib
├── generators
│ └── rails_settings
│ │ └── migration
│ │ ├── migration_generator.rb
│ │ └── templates
│ │ └── migration.rb
├── ledermann-rails-settings.rb
├── rails-settings.rb
└── rails-settings
│ ├── base.rb
│ ├── configuration.rb
│ ├── scopes.rb
│ ├── setting_object.rb
│ └── version.rb
├── rails-settings.gemspec
└── spec
├── configuration_spec.rb
├── database.yml
├── queries_spec.rb
├── scopes_spec.rb
├── serialize_spec.rb
├── setting_object_spec.rb
├── settings_spec.rb
├── spec_helper.rb
└── support
├── matchers
└── perform_queries.rb
└── query_counter.rb
/.github/workflows/main.yml:
--------------------------------------------------------------------------------
1 | name: Test
2 |
3 | on:
4 | - push
5 | - pull_request
6 |
7 | jobs:
8 | build:
9 | runs-on: ubuntu-latest
10 |
11 | strategy:
12 | fail-fast: false
13 | matrix:
14 | ruby_version:
15 | - '2.7'
16 | - '3.0'
17 | - '3.1'
18 | - '3.2'
19 | - '3.3'
20 | - '3.4'
21 | gemfile:
22 | - gemfiles/rails_6_1.gemfile
23 | - gemfiles/rails_7_0.gemfile
24 | - gemfiles/rails_7_1.gemfile
25 | exclude:
26 | - ruby_version: '3.4'
27 | gemfile: gemfiles/rails_6_1.gemfile
28 | - ruby_version: '3.4'
29 | gemfile: gemfiles/rails_7_0.gemfile
30 |
31 | name: Ruby ${{ matrix.ruby_version }} / Gemfile ${{ matrix.gemfile }}
32 |
33 | env:
34 | BUNDLE_GEMFILE: ${{ matrix.gemfile }}
35 | COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }}
36 |
37 | steps:
38 | - uses: actions/checkout@v4
39 |
40 | - name: Setup Ruby
41 | uses: ruby/setup-ruby@v1
42 | with:
43 | ruby-version: ${{ matrix.ruby_version }}
44 | bundler-cache: true
45 |
46 | - name: RSpec
47 | run: bundle exec rake
48 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.gem
2 | .bundle
3 | Gemfile.lock
4 | pkg/*
5 | coverage/*
6 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source 'https://rubygems.org'
2 |
3 | # Specify your gem's dependencies in rails-settings.gemspec
4 | gemspec
5 |
--------------------------------------------------------------------------------
/MIT-LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2012-2024 Georg Ledermann
2 |
3 | MIT License
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining
6 | a copy of this software and associated documentation files (the
7 | "Software"), to deal in the Software without restriction, including
8 | without limitation the rights to use, copy, modify, merge, publish,
9 | distribute, sublicense, and/or sell copies of the Software, and to
10 | permit persons to whom the Software is furnished to do so, subject to
11 | the following conditions:
12 |
13 | The above copyright notice and this permission notice shall be
14 | included in all copies or substantial portions of the Software.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Settings for Rails
2 |
3 | [](https://github.com/ledermann/rails-settings/actions)
4 | [](https://codeclimate.com/github/ledermann/rails-settings)
5 | [](https://coveralls.io/r/ledermann/rails-settings?branch=master)
6 |
7 | Ruby gem to handle settings for ActiveRecord instances by storing them as serialized Hash in a separate database table. Namespaces and defaults included.
8 |
9 | ## Requirements
10 |
11 | - Ruby 2.7 or newer
12 | - Rails 6.1 or newer (including Rails 7.0)
13 |
14 | ## Installation
15 |
16 | Include the gem in your Gemfile and run `bundle` to install it:
17 |
18 | ```ruby
19 | gem 'ledermann-rails-settings'
20 | ```
21 |
22 | Generate and run the migration:
23 |
24 | ```shell
25 | rails g rails_settings:migration
26 | rake db:migrate
27 | ```
28 |
29 | ## Usage
30 |
31 | ### Define settings
32 |
33 | ```ruby
34 | class User < ActiveRecord::Base
35 | has_settings do |s|
36 | s.key :dashboard, :defaults => { :theme => 'blue', :view => 'monthly', :filter => false }
37 | s.key :calendar, :defaults => { :scope => 'company'}
38 | end
39 | end
40 | ```
41 |
42 | If no defaults are needed, a simplified syntax can be used:
43 |
44 | ```ruby
45 | class User < ActiveRecord::Base
46 | has_settings :dashboard, :calendar
47 | end
48 | ```
49 |
50 | Every setting is handled by the class `RailsSettings::SettingObject`. You can use your own class, e.g. for validations:
51 |
52 | ```ruby
53 | class Project < ActiveRecord::Base
54 | has_settings :info, :class_name => 'ProjectSettingObject'
55 | end
56 |
57 | class ProjectSettingObject < RailsSettings::SettingObject
58 | validate do
59 | unless self.owner_name.present? && self.owner_name.is_a?(String)
60 | errors.add(:base, "Owner name is missing")
61 | end
62 | end
63 | end
64 | ```
65 |
66 | In case you need to define settings separatedly for the same models, you can use the persistent option
67 |
68 | ```ruby
69 | module UserDashboardConcern
70 | extend ActiveSupport::Concern
71 |
72 | included do
73 | has_settings persistent: true do |s|
74 | s.key :dashboard
75 | end
76 | end
77 | end
78 |
79 | class User < ActiveRecord::Base
80 | has_settings persistent: true do |s|
81 | s.key :calendar
82 | end
83 | end
84 | ```
85 |
86 | ### Set settings
87 |
88 | ```ruby
89 | user = User.find(1)
90 | user.settings(:dashboard).theme = 'black'
91 | user.settings(:calendar).scope = 'all'
92 | user.settings(:calendar).display = 'daily'
93 | user.save! # saves new or changed settings, too
94 | ```
95 |
96 | or
97 |
98 | ```ruby
99 | user = User.find(1)
100 | user.settings(:dashboard).update! :theme => 'black'
101 | user.settings(:calendar).update! :scope => 'all', :display => 'daily'
102 | ```
103 |
104 | ### Get settings
105 |
106 | ```ruby
107 | user = User.find(1)
108 | user.settings(:dashboard).theme
109 | # => 'black
110 |
111 | user.settings(:dashboard).view
112 | # => 'monthly' (it's the default)
113 |
114 | user.settings(:calendar).scope
115 | # => 'all'
116 | ```
117 |
118 | ### Delete settings
119 |
120 | ```ruby
121 | user = User.find(1)
122 | user.settings(:dashboard).update! :theme => nil
123 |
124 | user.settings(:dashboard).view = nil
125 | user.settings(:dashboard).save!
126 | ```
127 |
128 | ### Using scopes
129 |
130 | ```ruby
131 | User.with_settings
132 | # => all users having any setting
133 |
134 | User.without_settings
135 | # => all users without having any setting
136 |
137 | User.with_settings_for(:calendar)
138 | # => all users having a setting for 'calendar'
139 |
140 | User.without_settings_for(:calendar)
141 | # => all users without having settings for 'calendar'
142 | ```
143 |
144 | ### Eager Loading
145 |
146 | ```ruby
147 | User.includes(:setting_objects)
148 | # => Eager load setting_objects when querying many users
149 | ```
150 |
151 | ## Compatibility
152 |
153 | Version 2 is a complete rewrite and has a new DSL, so it's **not** compatible with Version 1. In addition, Rails 2.3 is not supported anymore. But the database schema is unchanged, so you can continue to use the data created by 1.x, no conversion is needed.
154 |
155 | If you don't want to upgrade, you find the old version in the [1.x](https://github.com/ledermann/rails-settings/commits/1.x) branch. But don't expect any updates there.
156 |
157 | ## Changelog
158 |
159 | See https://github.com/ledermann/rails-settings/releases
160 |
161 | ## License
162 |
163 | MIT License
164 |
165 | Copyright (c) 2012-2024 [Georg Ledermann](https://ledermann.dev)
166 |
167 | This gem is a complete rewrite of [rails-settings](https://github.com/Squeegy/rails-settings) by [Alex Wayne](https://github.com/Squeegy)
168 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | require 'bundler/gem_tasks'
2 | require 'rspec/core/rake_task'
3 |
4 | RSpec::Core::RakeTask.new(:spec)
5 |
6 | task default: :spec
7 |
--------------------------------------------------------------------------------
/gemfiles/rails_6_1.gemfile:
--------------------------------------------------------------------------------
1 | source 'https://rubygems.org'
2 |
3 | gem 'activerecord', '~> 6.1.2'
4 | gem "sqlite3", "~> 1.4"
5 |
6 | gemspec :path => "../"
7 |
--------------------------------------------------------------------------------
/gemfiles/rails_7_0.gemfile:
--------------------------------------------------------------------------------
1 | source 'https://rubygems.org'
2 |
3 | gem 'activerecord', '~> 7.0.0'
4 | gem "sqlite3", "~> 1.4"
5 |
6 | gemspec :path => "../"
7 |
--------------------------------------------------------------------------------
/gemfiles/rails_7_1.gemfile:
--------------------------------------------------------------------------------
1 | source 'https://rubygems.org'
2 |
3 | gem 'activerecord', '~> 7.1.0'
4 | gem "sqlite3", "~> 1.4"
5 |
6 | gemspec :path => "../"
7 |
--------------------------------------------------------------------------------
/lib/generators/rails_settings/migration/migration_generator.rb:
--------------------------------------------------------------------------------
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',
13 | 'db/migrate/rails_settings_migration.rb'
14 | end
15 |
16 | def self.next_migration_number(dirname)
17 | if timestamped_migrations?
18 | Time.now.utc.strftime('%Y%m%d%H%M%S')
19 | else
20 | '%.3d' % (current_migration_number(dirname) + 1)
21 | end
22 | end
23 |
24 | def self.timestamped_migrations?
25 | (ActiveRecord::Base.respond_to?(:timestamped_migrations) && ActiveRecord::Base.timestamped_migrations) ||
26 | (ActiveRecord.respond_to?(:timestamped_migrations) && ActiveRecord.timestamped_migrations)
27 | end
28 | end
29 | end
30 |
--------------------------------------------------------------------------------
/lib/generators/rails_settings/migration/templates/migration.rb:
--------------------------------------------------------------------------------
1 | class RailsSettingsMigration < ActiveRecord::Migration[5.0]
2 | def self.up
3 | create_table :settings do |t|
4 | t.string :var, null: false
5 | t.text :value
6 | t.references :target, null: false, polymorphic: true
7 | t.timestamps null: true
8 | end
9 | add_index :settings, %i[target_type target_id var], unique: true
10 | end
11 |
12 | def self.down
13 | drop_table :settings
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/lib/ledermann-rails-settings.rb:
--------------------------------------------------------------------------------
1 | require 'rails-settings'
2 |
--------------------------------------------------------------------------------
/lib/rails-settings.rb:
--------------------------------------------------------------------------------
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
22 |
--------------------------------------------------------------------------------
/lib/rails-settings/base.rb:
--------------------------------------------------------------------------------
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 | unless self.class.default_settings[var]
14 | raise ArgumentError.new("Unknown key: #{var}")
15 | end
16 |
17 | if RailsSettings.can_protect_attributes?
18 | setting_objects.detect { |s| s.var == var.to_s } ||
19 | setting_objects.build({ var: var.to_s }, without_protection: true)
20 | else
21 | setting_objects.detect { |s| s.var == var.to_s } ||
22 | setting_objects.build(var: var.to_s, target: self)
23 | end
24 | end
25 |
26 | def settings=(value)
27 | if value.nil?
28 | setting_objects.each(&:mark_for_destruction)
29 | else
30 | raise ArgumentError
31 | end
32 | end
33 |
34 | def settings?(var = nil)
35 | if var.nil?
36 | setting_objects.any? do |setting_object|
37 | !setting_object.marked_for_destruction? &&
38 | setting_object.value.present?
39 | end
40 | else
41 | settings(var).value.present?
42 | end
43 | end
44 |
45 | def to_settings_hash
46 | settings_hash = self.class.default_settings.dup
47 | settings_hash.each do |var, vals|
48 | settings_hash[var] = settings_hash[var].merge(
49 | settings(var.to_sym).value,
50 | )
51 | end
52 | settings_hash
53 | end
54 | end
55 | end
56 | end
57 | end
58 |
--------------------------------------------------------------------------------
/lib/rails-settings/configuration.rb:
--------------------------------------------------------------------------------
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 | unless @klass.methods.include?(:default_settings)
14 | @klass.class_attribute :default_settings
15 | end
16 | else
17 | @klass.class_attribute :default_settings
18 | end
19 |
20 | @klass.class_attribute :setting_object_class_name
21 | @klass.default_settings ||= {}
22 | @klass.setting_object_class_name =
23 | options[:class_name] || 'RailsSettings::SettingObject'
24 |
25 | if block_given?
26 | yield(self)
27 | else
28 | keys.each { |k| key(k) }
29 | end
30 |
31 | if @klass.default_settings.blank?
32 | raise ArgumentError.new('has_settings: No keys defined')
33 | end
34 | end
35 |
36 | def key(name, options = {})
37 | unless name.is_a?(Symbol)
38 | raise ArgumentError.new(
39 | "has_settings: Symbol expected, but got a #{name.class}",
40 | )
41 | end
42 | unless options.blank? || (options.keys == [:defaults])
43 | raise ArgumentError.new(
44 | "has_settings: Option :defaults expected, but got #{options.keys.join(', ')}",
45 | )
46 | end
47 | @klass.default_settings[name] = (
48 | options[:defaults] || {}
49 | ).stringify_keys.freeze
50 | end
51 | end
52 | end
53 |
--------------------------------------------------------------------------------
/lib/rails-settings/scopes.rb:
--------------------------------------------------------------------------------
1 | module RailsSettings
2 | module Scopes
3 | def with_settings
4 | result = joins("INNER JOIN settings ON #{settings_join_condition}")
5 | result.distinct
6 | end
7 |
8 | def with_settings_for(var)
9 | raise ArgumentError.new('Symbol expected!') unless var.is_a?(Symbol)
10 | joins(
11 | "INNER JOIN settings ON #{settings_join_condition} AND settings.var = '#{var}'",
12 | )
13 | end
14 |
15 | def without_settings
16 | joins("LEFT JOIN settings ON #{settings_join_condition}").where(
17 | 'settings.id IS NULL',
18 | )
19 | end
20 |
21 | def without_settings_for(var)
22 | raise ArgumentError.new('Symbol expected!') unless var.is_a?(Symbol)
23 | joins(
24 | "LEFT JOIN settings ON #{settings_join_condition} AND settings.var = '#{var}'",
25 | ).where('settings.id IS NULL')
26 | end
27 |
28 | def settings_join_condition
29 | "settings.target_id = #{table_name}.#{primary_key} AND
30 | settings.target_type = '#{base_class.name}'"
31 | end
32 | end
33 | end
34 |
--------------------------------------------------------------------------------
/lib/rails-settings/setting_object.rb:
--------------------------------------------------------------------------------
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 | if ActiveRecord.version >= Gem::Version.new('7.1.0.beta1')
17 | serialize :value, type: Hash
18 | else
19 | serialize :value, Hash
20 | end
21 |
22 | if RailsSettings.can_protect_attributes?
23 | # attr_protected can not be used here because it touches the database which is not connected yet.
24 | # So allow no attributes and override #sanitize_for_mass_assignment
25 | attr_accessible
26 | end
27 |
28 | REGEX_SETTER = /\A([a-z]\w*)=\Z/i
29 | REGEX_GETTER = /\A([a-z]\w*)\Z/i
30 |
31 | def respond_to?(method_name, include_priv = false)
32 | super || method_name.to_s =~ REGEX_SETTER || _setting?(method_name)
33 | end
34 |
35 | def method_missing(method_name, *args, &block)
36 | if block_given?
37 | super
38 | else
39 | if attribute_names.include?(method_name.to_s.sub('=', ''))
40 | super
41 | elsif method_name.to_s =~ REGEX_SETTER && args.size == 1
42 | _set_value($1, args.first)
43 | elsif method_name.to_s =~ REGEX_GETTER && args.size == 0
44 | _get_value($1)
45 | else
46 | super
47 | end
48 | end
49 | end
50 |
51 | protected
52 |
53 | if RailsSettings.can_protect_attributes?
54 | # Simulate attr_protected by removing all regular attributes
55 | def sanitize_for_mass_assignment(attributes, role = nil)
56 | attributes.except(
57 | 'id',
58 | 'var',
59 | 'value',
60 | 'target_id',
61 | 'target_type',
62 | 'created_at',
63 | 'updated_at',
64 | )
65 | end
66 | end
67 |
68 | private
69 |
70 | def _get_value(name)
71 | if value[name].nil?
72 | default_value = _get_default_value(name)
73 | _deep_dup(default_value)
74 | else
75 | value[name]
76 | end
77 | end
78 |
79 | def _get_default_value(name)
80 | default_value = _target_class.default_settings[var.to_sym][name]
81 |
82 | if default_value.respond_to?(:call)
83 | default_value.call(target)
84 | else
85 | default_value
86 | end
87 | end
88 |
89 | def _deep_dup(nested_hashes_and_or_arrays)
90 | Marshal.load(Marshal.dump(nested_hashes_and_or_arrays))
91 | end
92 |
93 | def _set_value(name, v)
94 | if value[name] != v
95 | value_will_change!
96 |
97 | if v.nil?
98 | value.delete(name)
99 | else
100 | value[name] = v
101 | end
102 | end
103 | end
104 |
105 | def _target_class
106 | target_type.constantize
107 | end
108 |
109 | def _setting?(method_name)
110 | _target_class.default_settings[var.to_sym].keys.include?(method_name.to_s)
111 | end
112 | end
113 | end
114 |
--------------------------------------------------------------------------------
/lib/rails-settings/version.rb:
--------------------------------------------------------------------------------
1 | module RailsSettings
2 | VERSION = '2.6.2'
3 | end
4 |
--------------------------------------------------------------------------------
/rails-settings.gemspec:
--------------------------------------------------------------------------------
1 | # -*- encoding: utf-8 -*-
2 | lib = File.expand_path('../lib', __FILE__)
3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4 | require 'rails-settings/version'
5 |
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 = ['georg@ledermann.dev']
12 | gem.description = 'Settings gem for Ruby on Rails'
13 | gem.summary =
14 | 'Ruby gem to handle settings for ActiveRecord instances by storing them as serialized Hash in a separate database table. Namespaces and defaults included.'
15 | gem.homepage = 'https://github.com/ledermann/rails-settings'
16 | gem.required_ruby_version = '>= 2.7'
17 |
18 | gem.files = `git ls-files`.split($/)
19 | gem.executables = gem.files.grep(%r{^bin/}).map { |f| File.basename(f) }
20 | gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
21 | gem.require_paths = ['lib']
22 |
23 | gem.add_dependency 'activerecord', '>= 6.1'
24 |
25 | gem.add_development_dependency 'rake'
26 | gem.add_development_dependency 'sqlite3'
27 | gem.add_development_dependency 'rspec'
28 | gem.add_development_dependency 'coveralls_reborn'
29 | gem.add_development_dependency 'simplecov', '>= 0.11.2'
30 | end
31 |
--------------------------------------------------------------------------------
/spec/configuration_spec.rb:
--------------------------------------------------------------------------------
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(
13 | 'RailsSettings::SettingObject',
14 | )
15 | end
16 |
17 | it 'should define multiple keys' do
18 | Configuration.new(Dummy, :dashboard, :calendar)
19 |
20 | expect(Dummy.default_settings).to eq({ dashboard: {}, calendar: {} })
21 | expect(Dummy.setting_object_class_name).to eq(
22 | 'RailsSettings::SettingObject',
23 | )
24 | end
25 |
26 | it 'should define single key with class_name' do
27 | Configuration.new(Dummy, :dashboard, class_name: 'MyClass')
28 | expect(Dummy.default_settings).to eq({ dashboard: {} })
29 | expect(Dummy.setting_object_class_name).to eq('MyClass')
30 | end
31 |
32 | it 'should define multiple keys with class_name' do
33 | Configuration.new(Dummy, :dashboard, :calendar, class_name: 'MyClass')
34 |
35 | expect(Dummy.default_settings).to eq({ dashboard: {}, calendar: {} })
36 | expect(Dummy.setting_object_class_name).to eq('MyClass')
37 | end
38 |
39 | it 'should define using block' do
40 | Configuration.new(Dummy) do |c|
41 | c.key :dashboard
42 | c.key :calendar
43 | end
44 |
45 | expect(Dummy.default_settings).to eq({ dashboard: {}, calendar: {} })
46 | expect(Dummy.setting_object_class_name).to eq(
47 | 'RailsSettings::SettingObject',
48 | )
49 | end
50 |
51 | it 'should define using block with defaults' do
52 | Configuration.new(Dummy) do |c|
53 | c.key :dashboard, defaults: { theme: 'red' }
54 | c.key :calendar, defaults: { scope: 'all' }
55 | end
56 |
57 | expect(Dummy.default_settings).to eq(
58 | { dashboard: { 'theme' => 'red' }, calendar: { 'scope' => 'all' } },
59 | )
60 | expect(Dummy.setting_object_class_name).to eq(
61 | 'RailsSettings::SettingObject',
62 | )
63 | end
64 |
65 | it 'should define using block and class_name' do
66 | Configuration.new(Dummy, class_name: 'MyClass') do |c|
67 | c.key :dashboard
68 | c.key :calendar
69 | end
70 |
71 | expect(Dummy.default_settings).to eq({ dashboard: {}, calendar: {} })
72 | expect(Dummy.setting_object_class_name).to eq('MyClass')
73 | end
74 |
75 | context 'persistent' do
76 | it 'should keep settings between multiple configurations initialization' do
77 | Configuration.new(Dummy, persistent: true) do |c|
78 | c.key :dashboard, defaults: { theme: 'red' }
79 | end
80 |
81 | Configuration.new(Dummy, :calendar, persistent: true)
82 |
83 | expect(Dummy.default_settings).to eq(
84 | { dashboard: { 'theme' => 'red' }, calendar: {} },
85 | )
86 | end
87 | end
88 | end
89 |
90 | describe Configuration, 'failure' do
91 | it 'should fail without args' do
92 | expect { Configuration.new }.to raise_error(ArgumentError)
93 | end
94 |
95 | it 'should fail without keys' do
96 | expect { Configuration.new(Dummy) }.to raise_error(ArgumentError)
97 | end
98 |
99 | it 'should fail without keys in block' do
100 | expect { Configuration.new(Dummy) { |c| } }.to raise_error(ArgumentError)
101 | end
102 |
103 | it 'should fail with keys not being symbols' do
104 | expect { Configuration.new(Dummy, 42, 'string') }.to raise_error(
105 | ArgumentError,
106 | )
107 | end
108 |
109 | it 'should fail with keys not being symbols' do
110 | expect {
111 | Configuration.new(Dummy) { |c| c.key 42, 'string' }
112 | }.to raise_error(ArgumentError)
113 | end
114 |
115 | it 'should fail with unknown option' do
116 | expect {
117 | Configuration.new(Dummy) { |c| c.key :dashboard, foo: {} }
118 | }.to raise_error(ArgumentError)
119 | end
120 | end
121 | end
122 |
--------------------------------------------------------------------------------
/spec/database.yml:
--------------------------------------------------------------------------------
1 | sqlite:
2 | adapter: sqlite3
3 | database: ':memory:'
4 |
--------------------------------------------------------------------------------
/spec/queries_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe 'Queries performed' do
4 | context 'New record' do
5 | let!(:user) { User.new name: 'Mr. Pink' }
6 |
7 | it 'should be saved by one SQL query' do
8 | expect { user.save! }.to perform_queries(1)
9 | end
10 |
11 | it 'should be saved with settings for one key by two SQL queries' do
12 | expect {
13 | user.settings(:dashboard).foo = 42
14 | user.settings(:dashboard).bar = 'string'
15 | user.save!
16 | }.to perform_queries(2)
17 | end
18 |
19 | it 'should be saved with settings for two keys by three SQL queries' do
20 | expect {
21 | user.settings(:dashboard).foo = 42
22 | user.settings(:dashboard).bar = 'string'
23 | user.settings(:calendar).bar = 'string'
24 | user.save!
25 | }.to perform_queries(3)
26 | end
27 | end
28 |
29 | context 'Existing record without settings' do
30 | let!(:user) { User.create! name: 'Mr. Pink' }
31 |
32 | it 'should be saved without SQL queries' do
33 | expect { user.save! }.to perform_queries(0)
34 | end
35 |
36 | it 'should be saved with settings for one key by two SQL queries' do
37 | expect {
38 | user.settings(:dashboard).foo = 42
39 | user.settings(:dashboard).bar = 'string'
40 | user.save!
41 | }.to perform_queries(2)
42 | end
43 |
44 | it 'should be saved with settings for two keys by three SQL queries' do
45 | expect {
46 | user.settings(:dashboard).foo = 42
47 | user.settings(:dashboard).bar = 'string'
48 | user.settings(:calendar).bar = 'string'
49 | user.save!
50 | }.to perform_queries(3)
51 | end
52 | end
53 |
54 | context 'Existing record with settings' do
55 | let!(:user) do
56 | User.create! name: 'Mr. Pink' do |user|
57 | user.settings(:dashboard).theme = 'pink'
58 | user.settings(:calendar).scope = 'all'
59 | end
60 | end
61 |
62 | it 'should be saved without SQL queries' do
63 | expect { user.save! }.to perform_queries(0)
64 | end
65 |
66 | it 'should be saved with settings for one key by one SQL queries' do
67 | expect {
68 | user.settings(:dashboard).foo = 42
69 | user.settings(:dashboard).bar = 'string'
70 | user.save!
71 | }.to perform_queries(1)
72 | end
73 |
74 | it 'should be saved with settings for two keys by two SQL queries' do
75 | expect {
76 | user.settings(:dashboard).foo = 42
77 | user.settings(:dashboard).bar = 'string'
78 | user.settings(:calendar).bar = 'string'
79 | user.save!
80 | }.to perform_queries(2)
81 | end
82 |
83 | it 'should be destroyed by two SQL queries' do
84 | expect { user.destroy }.to perform_queries(2)
85 | end
86 |
87 | it 'should update settings by one SQL query' do
88 | expect {
89 | user.settings(:dashboard).update! foo: 'bar'
90 | }.to perform_queries(1)
91 | end
92 | end
93 | end
94 |
--------------------------------------------------------------------------------
/spec/scopes_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe 'scopes' do
4 | let!(:user1) do
5 | User.create! name: 'Mr. White' do |user|
6 | user.settings(:dashboard).theme = 'white'
7 | end
8 | end
9 | let!(:user2) { User.create! name: 'Mr. Blue' }
10 |
11 | it 'should find objects with existing settings' do
12 | expect(User.with_settings).to eq([user1])
13 | end
14 |
15 | it 'should find objects with settings for key' do
16 | expect(User.with_settings_for(:dashboard)).to eq([user1])
17 | expect(User.with_settings_for(:foo)).to eq([])
18 | end
19 |
20 | it 'should records without settings' do
21 | expect(User.without_settings).to eq([user2])
22 | end
23 |
24 | it 'should records without settings for key' do
25 | expect(User.without_settings_for(:foo)).to eq([user1, user2])
26 | expect(User.without_settings_for(:dashboard)).to eq([user2])
27 | end
28 |
29 | it 'should require symbol as key' do
30 | [nil, 'string', 42].each do |invalid_key|
31 | expect { User.without_settings_for(invalid_key) }.to raise_error(
32 | ArgumentError,
33 | )
34 | expect { User.with_settings_for(invalid_key) }.to raise_error(
35 | ArgumentError,
36 | )
37 | end
38 | end
39 | end
40 |
--------------------------------------------------------------------------------
/spec/serialize_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe 'Serialization' do
4 | let!(:user) do
5 | User.create! name: 'Mr. White' do |user|
6 | user.settings(:dashboard).theme = 'white'
7 | user.settings(:calendar).scope = 'all'
8 | end
9 | end
10 |
11 | describe 'created settings' do
12 | it 'should be serialized' do
13 | user.reload
14 |
15 | dashboard_settings = user.setting_objects.where(var: 'dashboard').first
16 | calendar_settings = user.setting_objects.where(var: 'calendar').first
17 |
18 | expect(dashboard_settings.var).to eq('dashboard')
19 | expect(dashboard_settings.value).to eq({ 'theme' => 'white' })
20 |
21 | expect(calendar_settings.var).to eq('calendar')
22 | expect(calendar_settings.value).to eq({ 'scope' => 'all' })
23 | end
24 | end
25 |
26 | describe 'updated settings' do
27 | it 'should be serialized' do
28 | user.settings(:dashboard).update! smart: true
29 |
30 | dashboard_settings = user.setting_objects.where(var: 'dashboard').first
31 | calendar_settings = user.setting_objects.where(var: 'calendar').first
32 |
33 | expect(dashboard_settings.var).to eq('dashboard')
34 | expect(dashboard_settings.value).to eq(
35 | { 'theme' => 'white', 'smart' => true },
36 | )
37 |
38 | expect(calendar_settings.var).to eq('calendar')
39 | expect(calendar_settings.value).to eq({ 'scope' => 'all' })
40 | end
41 | end
42 | end
43 |
--------------------------------------------------------------------------------
/spec/setting_object_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe RailsSettings::SettingObject do
4 | let(:user) { User.create! name: 'Mr. Pink' }
5 |
6 | if RailsSettings.can_protect_attributes?
7 | let(:new_setting_object) do
8 | user.setting_objects.build({ var: 'dashboard' }, without_protection: true)
9 | end
10 | let(:saved_setting_object) do
11 | user.setting_objects.create!(
12 | { var: 'dashboard', value: { 'theme' => 'pink', 'filter' => false } },
13 | without_protection: true,
14 | )
15 | end
16 | else
17 | let(:new_setting_object) do
18 | user.setting_objects.build({ var: 'dashboard' })
19 | end
20 | let(:saved_setting_object) do
21 | user.setting_objects.create!(
22 | { var: 'dashboard', value: { 'theme' => 'pink', 'filter' => false } },
23 | )
24 | end
25 | end
26 |
27 | describe 'serialization' do
28 | it 'should have a hash default' do
29 | expect(RailsSettings::SettingObject.new.value).to eq({})
30 | end
31 | end
32 |
33 | describe 'Getter and Setter' do
34 | context 'on unsaved settings' do
35 | it 'should respond to setters' do
36 | expect(new_setting_object).to respond_to(:foo=)
37 | expect(new_setting_object).to respond_to(:bar=)
38 | expect(new_setting_object).to respond_to(:x=)
39 | end
40 |
41 | it 'should not respond to some getters' do
42 | expect { new_setting_object.foo! }.to raise_error(NoMethodError)
43 | expect { new_setting_object.foo? }.to raise_error(NoMethodError)
44 | end
45 |
46 | it 'should not respond if a block is given' do
47 | expect { new_setting_object.foo {} }.to raise_error(NoMethodError)
48 | end
49 |
50 | it 'should not respond if params are given' do
51 | expect { new_setting_object.foo(42) }.to raise_error(NoMethodError)
52 | expect { new_setting_object.foo(42, 43) }.to raise_error(NoMethodError)
53 | end
54 |
55 | it 'should return nil for unknown attribute' do
56 | expect(new_setting_object.foo).to eq(nil)
57 | expect(new_setting_object.bar).to eq(nil)
58 | expect(new_setting_object.c).to eq(nil)
59 | end
60 |
61 | it 'should return defaults' do
62 | expect(new_setting_object.theme).to eq('blue')
63 | expect(new_setting_object.view).to eq('monthly')
64 | expect(new_setting_object.filter).to eq(true)
65 | expect(new_setting_object.a).to eq('b')
66 | end
67 |
68 | it 'should return defaults when using `try`' do
69 | expect(new_setting_object.try(:theme)).to eq('blue')
70 | expect(new_setting_object.try(:view)).to eq('monthly')
71 | expect(new_setting_object.try(:filter)).to eq(true)
72 | end
73 |
74 | it 'should return value from target method if proc is a default value' do
75 | expect(new_setting_object.owner_name).to eq('Mr. Pink')
76 | end
77 |
78 | it 'should store different objects to value hash' do
79 | new_setting_object.integer = 42
80 | new_setting_object.float = 1.234
81 | new_setting_object.string = 'Hello, World!'
82 | new_setting_object.array = [1, 2, 3]
83 | new_setting_object.symbol = :foo
84 |
85 | expect(new_setting_object.value).to eq(
86 | 'integer' => 42,
87 | 'float' => 1.234,
88 | 'string' => 'Hello, World!',
89 | 'array' => [1, 2, 3],
90 | 'symbol' => :foo,
91 | )
92 | end
93 |
94 | it 'should set and return attributes' do
95 | new_setting_object.theme = 'pink'
96 | new_setting_object.foo = 42
97 | new_setting_object.bar = 'hello'
98 |
99 | expect(new_setting_object.theme).to eq('pink')
100 | expect(new_setting_object.foo).to eq(42)
101 | expect(new_setting_object.bar).to eq('hello')
102 | end
103 |
104 | it 'should set dirty trackers on change' do
105 | new_setting_object.theme = 'pink'
106 | expect(new_setting_object).to be_value_changed
107 | expect(new_setting_object).to be_changed
108 | end
109 | end
110 |
111 | context 'on saved settings' do
112 | it 'should not set dirty trackers on setting same value' do
113 | saved_setting_object.theme = 'pink'
114 | expect(saved_setting_object).not_to be_value_changed
115 | expect(saved_setting_object).not_to be_changed
116 | end
117 |
118 | it 'should delete key on assigning nil' do
119 | saved_setting_object.theme = nil
120 | expect(saved_setting_object.value).to eq({ 'filter' => false })
121 | end
122 | end
123 | end
124 |
125 | describe 'update' do
126 | it 'should save' do
127 | expect(new_setting_object.update(foo: 42, bar: 'string')).to be_truthy
128 | new_setting_object.reload
129 |
130 | expect(new_setting_object.foo).to eq(42)
131 | expect(new_setting_object.bar).to eq('string')
132 | expect(new_setting_object).not_to be_new_record
133 | expect(new_setting_object.id).not_to be_zero
134 | end
135 |
136 | it 'should not save blank hash' do
137 | expect(new_setting_object.update({})).to be_truthy
138 | end
139 |
140 | if RailsSettings.can_protect_attributes?
141 | it 'should not allow changing protected attributes' do
142 | new_setting_object.update!(var: 'calendar', foo: 42)
143 |
144 | expect(new_setting_object.var).to eq('dashboard')
145 | expect(new_setting_object.foo).to eq(42)
146 | end
147 | end
148 | end
149 |
150 | describe 'save' do
151 | it 'should save' do
152 | new_setting_object.foo = 42
153 | new_setting_object.bar = 'string'
154 | expect(new_setting_object.save).to be_truthy
155 | new_setting_object.reload
156 |
157 | expect(new_setting_object.foo).to eq(42)
158 | expect(new_setting_object.bar).to eq('string')
159 | expect(new_setting_object).not_to be_new_record
160 | expect(new_setting_object.id).not_to be_zero
161 | end
162 | end
163 |
164 | describe 'validation' do
165 | it 'should not validate for unknown var' do
166 | new_setting_object.var = 'unknown-var'
167 |
168 | expect(new_setting_object).not_to be_valid
169 | expect(new_setting_object.errors[:var]).to be_present
170 | end
171 | end
172 | end
173 |
--------------------------------------------------------------------------------
/spec/settings_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe 'Defaults' do
4 | it 'should be stored for simple class' do
5 | expect(Account.default_settings).to eq(portal: {})
6 | end
7 |
8 | # Modifying spec because it gives like this output on hash value
9 | # "owner_name"=>#
10 | it 'should be stored for parent class' do
11 | expect(User.default_settings.keys).to eq(%i[dashboard calendar])
12 | end
13 |
14 | it 'should be stored for child class' do
15 | expect(GuestUser.default_settings).to eq(
16 | dashboard: {
17 | 'theme' => 'red',
18 | 'view' => 'monthly',
19 | 'filter' => true,
20 | },
21 | )
22 | end
23 | end
24 |
25 | describe 'Getter/Setter' do
26 | let(:account) { Account.new subdomain: 'foo' }
27 |
28 | it 'should handle method syntax' do
29 | account.settings(:portal).enabled = true
30 | account.settings(:portal).template = 'black'
31 |
32 | expect(account.settings(:portal).enabled).to eq(true)
33 | expect(account.settings(:portal).template).to eq('black')
34 | end
35 |
36 | it 'should return nil for not existing key' do
37 | expect(account.settings(:portal).foo).to eq(nil)
38 | end
39 | end
40 |
41 | describe 'Objects' do
42 | context 'without defaults' do
43 | let(:account) { Account.new subdomain: 'foo' }
44 |
45 | it 'should have blank settings' do
46 | expect(account.settings(:portal).value).to eq({})
47 | end
48 |
49 | it 'should allow saving a blank value' do
50 | account.save!
51 | expect(account.settings(:portal).save).to be_truthy
52 | end
53 |
54 | it 'should allow removing all values' do
55 | account.settings(:portal).premium = true
56 | account.settings(:portal).fee = 42.5
57 | account.save!
58 |
59 | account.settings(:portal).premium = nil
60 | expect(account.save).to be_truthy
61 |
62 | account.settings(:portal).fee = nil
63 | expect(account.save).to be_truthy
64 | end
65 |
66 | it 'should not add settings on saving' do
67 | account.save!
68 | expect(RailsSettings::SettingObject.count).to eq(0)
69 | end
70 |
71 | it 'should save object with settings' do
72 | account.settings(:portal).premium = true
73 | account.settings(:portal).fee = 42.5
74 | account.save!
75 |
76 | account.reload
77 | expect(account.settings(:portal).premium).to eq(true)
78 | expect(account.settings(:portal).fee).to eq(42.5)
79 |
80 | expect(RailsSettings::SettingObject.count).to eq(1)
81 | expect(RailsSettings::SettingObject.first.value).to eq(
82 | { 'premium' => true, 'fee' => 42.5 },
83 | )
84 | end
85 |
86 | it 'should save settings separated' do
87 | account.save!
88 |
89 | settings = account.settings(:portal)
90 | settings.enabled = true
91 | settings.template = 'black'
92 | settings.save!
93 |
94 | account.reload
95 | expect(account.settings(:portal).enabled).to eq(true)
96 | expect(account.settings(:portal).template).to eq('black')
97 | end
98 | end
99 |
100 | context 'with defaults' do
101 | let(:user) { User.new name: 'Mr. Brown' }
102 |
103 | it 'should have default settings' do
104 | expect(user.settings(:dashboard).theme).to eq('blue')
105 | expect(user.settings(:dashboard).view).to eq('monthly')
106 | expect(user.settings(:dashboard).owner_name).to eq('Mr. Brown')
107 | expect(user.settings(:dashboard).filter).to eq(true)
108 | expect(user.settings(:calendar).scope).to eq('company')
109 | end
110 |
111 | it 'should have default settings after changing one' do
112 | user.settings(:dashboard).theme = 'gray'
113 |
114 | expect(user.settings(:dashboard).theme).to eq('gray')
115 | expect(user.settings(:dashboard).view).to eq('monthly')
116 | expect(user.settings(:dashboard).owner_name).to eq('Mr. Brown')
117 | expect(user.settings(:dashboard).filter).to eq(true)
118 | expect(user.settings(:calendar).scope).to eq('company')
119 | end
120 |
121 | it 'should overwrite settings' do
122 | user.settings(:dashboard).theme = 'brown'
123 | user.settings(:dashboard).filter = false
124 | user.settings(:dashboard).owner_name = 'Mr. Vishal'
125 | user.save!
126 |
127 | user.reload
128 | expect(user.settings(:dashboard).theme).to eq('brown')
129 | expect(user.settings(:dashboard).filter).to eq(false)
130 | expect(user.settings(:dashboard).owner_name).to eq('Mr. Vishal')
131 | expect(RailsSettings::SettingObject.count).to eq(1)
132 | expect(RailsSettings::SettingObject.first.value).to eq(
133 | { 'filter' => false, 'owner_name' => 'Mr. Vishal', 'theme' => 'brown' },
134 | )
135 | end
136 |
137 | it 'should merge settings with defaults' do
138 | user.settings(:dashboard).theme = 'brown'
139 | user.save!
140 |
141 | user.reload
142 | expect(user.settings(:dashboard).theme).to eq('brown')
143 | expect(user.settings(:dashboard).filter).to eq(true)
144 | expect(RailsSettings::SettingObject.count).to eq(1)
145 | expect(RailsSettings::SettingObject.first.value).to eq(
146 | { 'theme' => 'brown' },
147 | )
148 | end
149 |
150 | context 'when default value is an Array' do
151 | it 'should not mutate default_settings' do
152 | expected_return_value = User.default_settings[:calendar]['events'].dup
153 |
154 | user.settings(:calendar).events.push('new_value')
155 | expect(User.default_settings[:calendar]['events']).to eq(
156 | expected_return_value,
157 | )
158 | end
159 | end
160 |
161 | context 'when default value is a Hash' do
162 | it 'should not mutate default_settings' do
163 | expected_return_value = User.default_settings[:calendar]['profile'].dup
164 |
165 | user.settings(:calendar).profile.update('new_key' => 'new_value')
166 | expect(User.default_settings[:calendar]['profile']).to eq(
167 | expected_return_value,
168 | )
169 | end
170 | end
171 | end
172 | end
173 |
174 | describe 'Object without settings' do
175 | let!(:user) { User.create! name: 'Mr. White' }
176 |
177 | it 'should respond to #settings?' do
178 | expect(user.settings?).to eq(false)
179 | expect(user.settings?(:dashboard)).to eq(false)
180 | end
181 |
182 | it 'should have no setting objects' do
183 | expect(RailsSettings::SettingObject.count).to eq(0)
184 | end
185 |
186 | it 'should add settings' do
187 | user.settings(:dashboard).update! smart: true
188 |
189 | user.reload
190 | expect(user.settings(:dashboard).smart).to eq(true)
191 | end
192 |
193 | it 'should not save settings if assigned nil' do
194 | expect {
195 | user.settings = nil
196 | user.save!
197 | }.to_not change(RailsSettings::SettingObject, :count)
198 | end
199 | end
200 |
201 | describe 'Object with settings' do
202 | let!(:user) do
203 | User.create! name: 'Mr. White' do |user|
204 | user.settings(:dashboard).theme = 'white'
205 | user.settings(:calendar).scope = 'all'
206 | end
207 | end
208 |
209 | it 'should respond to #settings?' do
210 | expect(user.settings?).to eq(true)
211 |
212 | expect(user.settings?(:dashboard)).to eq(true)
213 | expect(user.settings?(:calendar)).to eq(true)
214 | end
215 |
216 | it 'should have two setting objects' do
217 | expect(RailsSettings::SettingObject.count).to eq(2)
218 | end
219 |
220 | it 'should update settings' do
221 | user.settings(:dashboard).update! smart: true
222 | user.reload
223 |
224 | expect(user.settings(:dashboard).smart).to eq(true)
225 | expect(user.settings(:dashboard).theme).to eq('white')
226 | expect(user.settings(:calendar).scope).to eq('all')
227 | end
228 |
229 | it 'should update settings by saving object' do
230 | user.settings(:dashboard).smart = true
231 | user.save!
232 |
233 | user.reload
234 | expect(user.settings(:dashboard).smart).to eq(true)
235 | end
236 |
237 | it 'should destroy settings with nil' do
238 | expect {
239 | user.settings = nil
240 | user.save!
241 | }.to change(RailsSettings::SettingObject, :count).by(-2)
242 |
243 | expect(user.settings?).to eq(false)
244 | end
245 |
246 | it 'should raise exception on assigning other than nil' do
247 | expect {
248 | user.settings = :foo
249 | user.save!
250 | }.to raise_error(ArgumentError)
251 | end
252 | end
253 |
254 | describe 'Customized SettingObject' do
255 | let(:project) { Project.create! name: 'Heist' }
256 |
257 | it 'should not accept invalid attributes' do
258 | project.settings(:info).owner_name = 42
259 | expect(project.settings(:info)).not_to be_valid
260 |
261 | project.settings(:info).owner_name = ''
262 | expect(project.settings(:info)).not_to be_valid
263 | end
264 |
265 | it 'should accept valid attributes' do
266 | project.settings(:info).owner_name = 'Mr. Brown'
267 | expect(project.settings(:info)).to be_valid
268 | end
269 | end
270 |
271 | describe 'to_settings_hash' do
272 | let(:user) do
273 | User.new name: 'Mrs. Fin' do |user|
274 | user.settings(:dashboard).theme = 'green'
275 | user.settings(:dashboard).owner_name = 'Mr. Vishal'
276 | user.settings(:dashboard).sound = 11
277 | user.settings(:calendar).scope = 'some'
278 | end
279 | end
280 |
281 | # Modifying spec because it gives like this output on hash value
282 | # "owner_name"=>#
283 | it 'should return defaults' do
284 | expect(User.new.to_settings_hash.keys).to eq(%i[dashboard calendar])
285 | end
286 |
287 | it 'should return merged settings' do
288 | expect(user.to_settings_hash).to eq(
289 | {
290 | dashboard: {
291 | 'a' => 'b',
292 | 'filter' => true,
293 | 'owner_name' => 'Mr. Vishal',
294 | 'sound' => 11,
295 | 'theme' => 'green',
296 | 'view' => 'monthly',
297 | },
298 | calendar: {
299 | 'scope' => 'some',
300 | 'events' => [],
301 | 'profile' => {
302 | },
303 | },
304 | },
305 | )
306 | end
307 | end
308 |
--------------------------------------------------------------------------------
/spec/spec_helper.rb:
--------------------------------------------------------------------------------
1 | require 'simplecov'
2 | require 'coveralls'
3 |
4 | SimpleCov.formatter =
5 | SimpleCov::Formatter::MultiFormatter.new(
6 | [SimpleCov::Formatter::HTMLFormatter, Coveralls::SimpleCov::Formatter],
7 | )
8 | SimpleCov.start { add_filter '/spec/' }
9 |
10 | # Requires supporting ruby files with custom matchers and macros, etc,
11 | # in spec/support/ and its subdirectories.
12 | Dir[
13 | File.expand_path(File.join(File.dirname(__FILE__), 'support', '**', '*.rb'))
14 | ].each { |f| require f }
15 |
16 | # This file was generated by the `rspec --init` command. Conventionally, all
17 | # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
18 | # Require this file using `require "spec_helper"` to ensure that it is only
19 | # loaded once.
20 | #
21 | # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
22 | RSpec.configure do |config|
23 | # Run specs in random order to surface order dependencies. If you find an
24 | # order dependency and want to debug it, you can fix the order by providing
25 | # the seed, which is printed after each run.
26 | # --seed 1234
27 | # config.order = 'random'
28 |
29 | config.before(:each) { clear_db }
30 |
31 | config.after :suite do
32 | RailsSettingsMigration.migrate(:down)
33 | end
34 | end
35 |
36 | require 'active_record'
37 | require 'protected_attributes' if ENV['PROTECTED_ATTRIBUTES'] == 'true'
38 | require 'rails-settings'
39 |
40 | if I18n.respond_to?(:enforce_available_locales=)
41 | I18n.enforce_available_locales = false
42 | end
43 |
44 | class User < ActiveRecord::Base
45 | has_settings do |s|
46 | s.key :dashboard,
47 | defaults: {
48 | theme: 'blue',
49 | view: 'monthly',
50 | a: 'b',
51 | filter: true,
52 | owner_name: ->(target) { target.name },
53 | }
54 | s.key :calendar, defaults: { scope: 'company', events: [], profile: {} }
55 | end
56 | end
57 |
58 | class GuestUser < User
59 | has_settings do |s|
60 | s.key :dashboard, defaults: { theme: 'red', view: 'monthly', filter: true }
61 | end
62 | end
63 |
64 | class Account < ActiveRecord::Base
65 | has_settings :portal
66 | end
67 |
68 | class Project < ActiveRecord::Base
69 | has_settings :info, class_name: 'ProjectSettingObject'
70 | end
71 |
72 | class ProjectSettingObject < RailsSettings::SettingObject
73 | validate do
74 | unless self.owner_name.present? && self.owner_name.is_a?(String)
75 | errors.add(:base, 'Owner name is missing')
76 | end
77 | end
78 | end
79 |
80 | def setup_db
81 | ActiveRecord::Base.configurations =
82 | YAML.load_file(File.dirname(__FILE__) + '/database.yml')
83 | ActiveRecord::Base.establish_connection(:sqlite)
84 | ActiveRecord::Migration.verbose = false
85 |
86 | print "Testing with ActiveRecord #{ActiveRecord::VERSION::STRING}"
87 | puts
88 |
89 | require File.expand_path(
90 | '../../lib/generators/rails_settings/migration/templates/migration.rb',
91 | __FILE__,
92 | )
93 | RailsSettingsMigration.migrate(:up)
94 |
95 | ActiveRecord::Schema.define(version: 1) do
96 | create_table :users do |t|
97 | t.string :type
98 | t.string :name
99 | end
100 |
101 | create_table :accounts do |t|
102 | t.string :subdomain
103 | end
104 |
105 | create_table :projects do |t|
106 | t.string :name
107 | end
108 | end
109 | end
110 |
111 | def clear_db
112 | User.delete_all
113 | Account.delete_all
114 | RailsSettings::SettingObject.delete_all
115 | end
116 |
117 | setup_db
118 |
--------------------------------------------------------------------------------
/spec/support/matchers/perform_queries.rb:
--------------------------------------------------------------------------------
1 | RSpec::Matchers.define :perform_queries do |expected|
2 | match { |block| query_count(&block) == expected }
3 |
4 | failure_message do |actual|
5 | "Expected to run #{expected} queries, got #{@counter.query_count}"
6 | end
7 |
8 | def query_count(&block)
9 | @counter = ActiveRecord::QueryCounter.new
10 | ActiveSupport::Notifications.subscribe(
11 | 'sql.active_record',
12 | @counter.to_proc,
13 | )
14 | yield
15 | ActiveSupport::Notifications.unsubscribe(@counter.to_proc)
16 |
17 | @counter.query_count
18 | end
19 |
20 | def supports_block_expectations?
21 | true
22 | end
23 | end
24 |
--------------------------------------------------------------------------------
/spec/support/query_counter.rb:
--------------------------------------------------------------------------------
1 | module ActiveRecord
2 | class QueryCounter
3 | attr_reader :query_count
4 |
5 | def initialize
6 | @query_count = 0
7 | end
8 |
9 | def to_proc
10 | lambda(&method(:callback))
11 | end
12 |
13 | def callback(name, start, finish, message_id, values)
14 | @query_count += 1 unless %w[CACHE SCHEMA].include?(values[:name]) ||
15 | values[:sql] =~ /^begin/i || values[:sql] =~ /^commit/i
16 | end
17 | end
18 | end
19 |
--------------------------------------------------------------------------------