├── .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 | [![Build Status](https://github.com/ledermann/rails-settings/actions/workflows/main.yml/badge.svg)](https://github.com/ledermann/rails-settings/actions) 4 | [![Code Climate](https://codeclimate.com/github/ledermann/rails-settings.svg)](https://codeclimate.com/github/ledermann/rails-settings) 5 | [![Coverage Status](https://coveralls.io/repos/ledermann/rails-settings/badge.svg?branch=master)](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 | --------------------------------------------------------------------------------