├── .coveralls.yml ├── lib ├── flag_shih_tzu │ ├── version.rb │ └── validators.rb └── flag_shih_tzu.rb ├── .gitignore ├── test ├── database.yml ├── test_helper.rb ├── schema.rb └── flag_shih_tzu_test.rb ├── Gemfile ├── .github ├── FUNDING.yml └── workflows │ ├── ci.yml │ └── discord-notifier.yml ├── gemfiles ├── ar-8.0.x ├── ar-7.0.x ├── ar-7.1.x └── ar-7.2.x ├── Rakefile ├── LICENSE ├── flag_shih_tzu.gemspec ├── bin └── test.bash ├── REEK ├── CHANGELOG.md └── README.md /.coveralls.yml: -------------------------------------------------------------------------------- 1 | service_name: travis-ci 2 | -------------------------------------------------------------------------------- /lib/flag_shih_tzu/version.rb: -------------------------------------------------------------------------------- 1 | module FlagShihTzu 2 | VERSION = "0.3.23" 3 | end 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | test/debug.log 2 | test/flag_shih_tzu_plugin.sqlite3.db 3 | coverage 4 | .idea 5 | *.gem 6 | .bundle 7 | Gemfile.lock 8 | gemfiles/*.lock 9 | pkg/* 10 | rdoc/* 11 | .ruby-version 12 | .ruby-gemset 13 | -------------------------------------------------------------------------------- /test/database.yml: -------------------------------------------------------------------------------- 1 | sqlite: 2 | adapter: sqlite3 3 | database: ":memory:" 4 | timeout: 500 5 | mysql: 6 | adapter: mysql2 7 | database: flag_shih_tzu_test 8 | username: foss 9 | encoding: utf8 10 | postgresql: 11 | adapter: postgresql 12 | database: flag_shih_tzu_test 13 | username: postgres 14 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "http://rubygems.org" 2 | 3 | # SO the gem can run the simple test suite against the raw bundled gems without the complex BUNDLE_GEMFILE setup 4 | gem "sqlite3", :platforms => [:ruby] 5 | gem "reek", ">= 2.2.1", :platforms => [:ruby] # Last version to support Ruby 1.9 6 | gem "roodi", ">= 5", :platforms => [:ruby] 7 | 8 | # Specify your gem's dependencies in flag_shih_tzu.gemspec 9 | gemspec 10 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | buy_me_a_coffee: pboling 4 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 5 | github: [pboling] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 6 | issuehunt: pboling # Replace with a single IssueHunt username 7 | ko_fi: pboling # Replace with a single Ko-fi username 8 | liberapay: pboling # Replace with a single Liberapay username 9 | open_collective: # Replace with a single Open Collective username 10 | patreon: galtzo # Replace with a single Patreon username 11 | polar: pboling 12 | thanks_dev: u/gh/pboling 13 | tidelift: rubygems/flag_shih_tzu # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 14 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Run test 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: [master] 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | timeout-minutes: 5 12 | strategy: 13 | fail-fast: false 14 | matrix: 15 | ruby: ['3.1', '3.2', '3.3', '3.4'] 16 | activerecord: ['7.0', '7.1', '7.2', '8.0'] 17 | continue-on-error: [true] 18 | name: Test on Ruby ${{ matrix.ruby }} x Rails ${{ matrix.activerecord }} 19 | env: 20 | BUNDLE_GEMFILE: gemfiles/ar-${{ matrix.activerecord }}.x 21 | steps: 22 | - uses: actions/checkout@v3 23 | 24 | - uses: ruby/setup-ruby@v1 25 | with: 26 | ruby-version: ${{matrix.ruby}} 27 | bundler-cache: true 28 | 29 | - name: Run tests 30 | run: bundle exec rake 31 | -------------------------------------------------------------------------------- /.github/workflows/discord-notifier.yml: -------------------------------------------------------------------------------- 1 | name: Discord Notify 2 | 3 | on: 4 | check_run: 5 | types: [completed] 6 | discussion: 7 | types: [ created ] 8 | discussion_comment: 9 | types: [ created ] 10 | fork: 11 | gollum: 12 | issues: 13 | types: [ opened ] 14 | issue_comment: 15 | types: [ created ] 16 | pull_request: 17 | types: [ opened, reopened, closed ] 18 | release: 19 | types: [ published ] 20 | watch: 21 | types: [ started ] 22 | 23 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 24 | jobs: 25 | # This workflow contains a single job called "build" 26 | notify: 27 | # The type of runner that the job will run on 28 | runs-on: ubuntu-latest 29 | 30 | # Steps represent a sequence of tasks that will be executed as part of the job 31 | steps: 32 | - name: Actions Status Discord 33 | uses: sarisia/actions-status-discord@v1 34 | if: always() 35 | with: 36 | webhook: ${{ secrets.DISCORD_WEBHOOK }} 37 | status: ${{ job.status }} 38 | username: GitHub Actions 39 | -------------------------------------------------------------------------------- /gemfiles/ar-8.0.x: -------------------------------------------------------------------------------- 1 | source "http://rubygems.org" 2 | 3 | gemspec :path => ".." 4 | 5 | gem "activerecord", "~> 8.0.0" 6 | gem "sqlite3", "~> 1.3", platforms: [:ruby] 7 | gem "mysql2", platforms: [:ruby] 8 | 9 | platform :jruby do 10 | gem "jdbc-sqlite3", github: "jruby/activerecord-jdbc-adapter", branch: "master" 11 | gem "jdbc-mysql", github: "jruby/activerecord-jdbc-adapter", branch: "master" 12 | gem "jdbc-postgres", github: "jruby/activerecord-jdbc-adapter", branch: "master" 13 | gem "activerecord-jdbc-adapter", github: "jruby/activerecord-jdbc-adapter", branch: "master" 14 | gem "activerecord-jdbcsqlite3-adapter", github: "jruby/activerecord-jdbc-adapter", branch: "master" 15 | gem "activerecord-jdbcmysql-adapter", github: "jruby/activerecord-jdbc-adapter", branch: "master" 16 | gem "activerecord-jdbcpostgresql-adapter", github: "jruby/activerecord-jdbc-adapter", branch: "master" 17 | end 18 | 19 | gem "reek", "~> 3", platforms: [:mri] 20 | gem "roodi", "~> 5", platforms: [:mri] 21 | gem "coveralls_reborn", platforms: [:mri] 22 | -------------------------------------------------------------------------------- /gemfiles/ar-7.0.x: -------------------------------------------------------------------------------- 1 | source "http://rubygems.org" 2 | 3 | gemspec :path => ".." 4 | 5 | gem "activerecord", "~> 7.0.0" 6 | gem "sqlite3", "~> 1.3", platforms: [:ruby] 7 | gem "mysql2", platforms: [:ruby] 8 | 9 | platform :jruby do 10 | gem "jdbc-sqlite3", github: "jruby/activerecord-jdbc-adapter", branch: "70-stable" 11 | gem "jdbc-mysql", github: "jruby/activerecord-jdbc-adapter", branch: "70-stable" 12 | gem "jdbc-postgres", github: "jruby/activerecord-jdbc-adapter", branch: "70-stable" 13 | gem "activerecord-jdbc-adapter", github: "jruby/activerecord-jdbc-adapter", branch: "70-stable" 14 | gem "activerecord-jdbcsqlite3-adapter", github: "jruby/activerecord-jdbc-adapter", branch: "70-stable" 15 | gem "activerecord-jdbcmysql-adapter", github: "jruby/activerecord-jdbc-adapter", branch: "70-stable" 16 | gem "activerecord-jdbcpostgresql-adapter", github: "jruby/activerecord-jdbc-adapter", branch: "70-stable" 17 | end 18 | 19 | gem "reek", "~> 3", platforms: [:mri] 20 | gem "roodi", "~> 5", platforms: [:mri] 21 | gem "coveralls_reborn", platforms: [:mri] 22 | -------------------------------------------------------------------------------- /gemfiles/ar-7.1.x: -------------------------------------------------------------------------------- 1 | source "http://rubygems.org" 2 | 3 | gemspec :path => ".." 4 | 5 | gem "activerecord", "~> 7.1.0" 6 | gem "sqlite3", "~> 1.3", platforms: [:ruby] 7 | gem "mysql2", platforms: [:ruby] 8 | 9 | platform :jruby do 10 | gem "jdbc-sqlite3", github: "jruby/activerecord-jdbc-adapter", branch: "70-stable" 11 | gem "jdbc-mysql", github: "jruby/activerecord-jdbc-adapter", branch: "70-stable" 12 | gem "jdbc-postgres", github: "jruby/activerecord-jdbc-adapter", branch: "70-stable" 13 | gem "activerecord-jdbc-adapter", github: "jruby/activerecord-jdbc-adapter", branch: "70-stable" 14 | gem "activerecord-jdbcsqlite3-adapter", github: "jruby/activerecord-jdbc-adapter", branch: "70-stable" 15 | gem "activerecord-jdbcmysql-adapter", github: "jruby/activerecord-jdbc-adapter", branch: "70-stable" 16 | gem "activerecord-jdbcpostgresql-adapter", github: "jruby/activerecord-jdbc-adapter", branch: "70-stable" 17 | end 18 | 19 | gem "reek", "~> 3", platforms: [:mri] 20 | gem "roodi", "~> 5", platforms: [:mri] 21 | gem "coveralls_reborn", platforms: [:mri] 22 | -------------------------------------------------------------------------------- /gemfiles/ar-7.2.x: -------------------------------------------------------------------------------- 1 | source "http://rubygems.org" 2 | 3 | gemspec :path => ".." 4 | 5 | gem "activerecord", "~> 7.2.0" 6 | gem "sqlite3", "~> 1.3", platforms: [:ruby] 7 | gem "mysql2", platforms: [:ruby] 8 | 9 | platform :jruby do 10 | gem "jdbc-sqlite3", github: "jruby/activerecord-jdbc-adapter", branch: "70-stable" 11 | gem "jdbc-mysql", github: "jruby/activerecord-jdbc-adapter", branch: "70-stable" 12 | gem "jdbc-postgres", github: "jruby/activerecord-jdbc-adapter", branch: "70-stable" 13 | gem "activerecord-jdbc-adapter", github: "jruby/activerecord-jdbc-adapter", branch: "70-stable" 14 | gem "activerecord-jdbcsqlite3-adapter", github: "jruby/activerecord-jdbc-adapter", branch: "70-stable" 15 | gem "activerecord-jdbcmysql-adapter", github: "jruby/activerecord-jdbc-adapter", branch: "70-stable" 16 | gem "activerecord-jdbcpostgresql-adapter", github: "jruby/activerecord-jdbc-adapter", branch: "70-stable" 17 | end 18 | 19 | gem "reek", "~> 3", platforms: [:mri] 20 | gem "roodi", "~> 5", platforms: [:mri] 21 | gem "coveralls_reborn", platforms: [:mri] 22 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rake' 2 | require 'rake/testtask' 3 | require 'rdoc/task' 4 | 5 | require 'bundler' 6 | Bundler::GemHelper.install_tasks 7 | 8 | desc 'Default: run unit tests.' 9 | task default: :test 10 | 11 | desc 'Test the flag_shih_tzu plugin.' 12 | Rake::TestTask.new(:test) do |t| 13 | t.libs << 'lib' 14 | t.pattern = 'test/**/*_test.rb' 15 | t.verbose = true 16 | end 17 | 18 | desc 'Generate documentation for the flag_shih_tzu plugin.' 19 | RDoc::Task.new(:rdoc) do |rdoc| 20 | rdoc.rdoc_dir = 'rdoc' 21 | rdoc.title = 'FlagShihTzu' 22 | rdoc.options << '--line-numbers' 23 | rdoc.rdoc_files.include('README.rdoc') 24 | rdoc.rdoc_files.include('lib/**/*.rb') 25 | end 26 | 27 | if defined?(Reek) # No Reek on JRuby 28 | require 'reek/rake/task' 29 | Reek::Rake::Task.new do |t| 30 | t.fail_on_error = true 31 | t.verbose = false 32 | t.source_files = 'lib/**/*.rb' 33 | end 34 | end 35 | 36 | if defined?(Roodi) # No Roodi on JRuby 37 | require 'roodi_task' 38 | RoodiTask.new do |t| 39 | t.verbose = false 40 | end 41 | end 42 | 43 | namespace :test do 44 | desc 'Test against all supported ActiveRecord versions' 45 | task :all do 46 | sh "bin/test.bash" 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2011 XING AG, http://www.xing.com/ 4 | Copyright (c) 2012 - 2018 Peter Boling, http://www.railsbling.com/ 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | require "logger" 2 | require "active_record" 3 | require "test/unit" 4 | require "yaml" 5 | 6 | ActiveRecord::Base.logger = Logger.new(File.dirname(__FILE__) + "/debug.log") 7 | ActiveRecord::Migration.verbose = false 8 | 9 | configs = YAML.load_file(File.dirname(__FILE__) + "/database.yml") 10 | if RUBY_PLATFORM == "java" 11 | configs["sqlite"]["adapter"] = "jdbcsqlite3" 12 | configs["mysql"]["adapter"] = "jdbcmysql" 13 | configs["postgresql"]["adapter"] = "jdbcpostgresql" 14 | end 15 | ActiveRecord::Base.configurations = configs 16 | 17 | # Run specific adapter tests like: 18 | # 19 | # DB=sqlite rake test:all 20 | # DB=mysql rake test:all 21 | # DB=postgresql rake test:all 22 | # 23 | db_name = (ENV["DB"] || "sqlite").to_sym 24 | ActiveRecord::Base.establish_connection(db_name) 25 | 26 | load(File.dirname(__FILE__) + "/schema.rb") 27 | 28 | class Test::Unit::TestCase 29 | 30 | def assert_array_similarity(expected, actual, message=nil) 31 | full_message = build_message(message, " expected but was\n.\n", expected, actual) 32 | assert_block(full_message) { (expected.size == actual.size) && (expected - actual == []) } 33 | end 34 | 35 | end 36 | 37 | # For code coverage, must be required before all application / gem / library code. 38 | begin 39 | unless ENV["NOCOVER"] 40 | require "coveralls" 41 | Coveralls.wear! 42 | end 43 | rescue LoadError 44 | # Some builds do not support coveralls 45 | end 46 | 47 | require "flag_shih_tzu" 48 | -------------------------------------------------------------------------------- /test/schema.rb: -------------------------------------------------------------------------------- 1 | ActiveRecord::Schema.define(:version => 0) do 2 | create_table :spaceships, :force => true do |t| 3 | t.integer :flags, :null => false, :default => 0 4 | t.string :incorrect_flags_column, :null => false, :default => '' 5 | end 6 | 7 | create_table :spaceships_with_custom_flags_column, :force => true do |t| 8 | t.integer :bits, :null => false, :default => 0 9 | end 10 | 11 | create_table :spaceships_with_2_custom_flags_column, :force => true do |t| 12 | t.integer :bits, :null => false, :default => 0 13 | t.integer :commanders, :null => false, :default => 0 14 | end 15 | 16 | create_table :spaceships_with_3_custom_flags_column, :force => true do |t| 17 | t.integer :engines, :null => false, :default => 0 18 | t.integer :weapons, :null => false, :default => 0 19 | t.integer :hal3000, :null => false, :default => 0 20 | end 21 | 22 | create_table :spaceships_with_3_custom_flags_column, :force => true do |t| 23 | t.integer :engines, :null => false, :default => 0 24 | t.integer :weapons, :null => false, :default => 0 25 | t.integer :hal3000, :null => false, :default => 0 26 | end 27 | 28 | create_table :spaceships_with_symbol_and_string_flag_columns, :force => true do |t| 29 | t.integer :peace, :null => false, :default => 0 30 | t.integer :love, :null => false, :default => 0 31 | t.integer :happiness, :null => false, :default => 0 32 | end 33 | 34 | create_table :spaceships_without_flags_column, :force => true do |t| 35 | end 36 | 37 | create_table :spaceships_with_non_integer_column, :force => true do |t| 38 | t.string :flags, :null => false, :default => 'A string' 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /flag_shih_tzu.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | require File.expand_path('../lib/flag_shih_tzu/version', __FILE__) 3 | 4 | Gem::Specification.new do |gem| 5 | gem.name = "flag_shih_tzu" 6 | gem.version = FlagShihTzu::VERSION 7 | gem.licenses = ['MIT'] 8 | gem.email = 'peter.boling@gmail.com' 9 | gem.platform = Gem::Platform::RUBY 10 | gem.authors = ["Peter Boling", "Patryk Peszko", "Sebastian Roebke", "David Anderson", "Tim Payton"] 11 | gem.homepage = "https://github.com/pboling/flag_shih_tzu" 12 | gem.summary = %q{🏁 Bit fields for ActiveRecord} 13 | gem.description = <<-EODOC 14 | 🏁 Bit fields for ActiveRecord: 15 | This gem lets you use a single integer column in an ActiveRecord model 16 | to store a collection of boolean attributes (flags). Each flag can be used 17 | almost in the same way you would use any boolean attribute on an 18 | ActiveRecord object. 19 | EODOC 20 | 21 | gem.files = `git ls-files`.split("\n") 22 | gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") 23 | gem.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) } 24 | gem.require_paths = ["lib"] 25 | 26 | gem.required_ruby_version = '>= 1.9.3' 27 | 28 | gem.add_development_dependency('activerecord', '>= 2.3.0') 29 | 30 | gem.add_development_dependency('bundler') 31 | gem.add_development_dependency('rake', '>= 0.9') 32 | gem.add_development_dependency('rdoc', '~> 6.5') # v6 requires Ruby 2.2+ 33 | gem.add_development_dependency('test-unit', '>= 3') 34 | gem.add_development_dependency('wwtd', '>= 1') 35 | gem.add_development_dependency("appraisal") 36 | # latest gem-release does not support back to the versions of Ruby still supported here 37 | # gem.add_development_dependency('gem-release', '~> 2') 38 | end 39 | -------------------------------------------------------------------------------- /lib/flag_shih_tzu/validators.rb: -------------------------------------------------------------------------------- 1 | # Active Record is not defined as a runtime dependency in the gemspec. 2 | unless defined?(::ActiveRecord) 3 | begin 4 | # If by some miracle it hasn't been loaded yet, try to load it. 5 | require "active_record" 6 | rescue LoadError 7 | # If it fails to load, then assume the user is try to use flag_shih_tzu with some other database adapter 8 | warn "FlagShihTzu probably won't work unless you have some version of Active Record loaded. Versions >= 2.3 are supported." 9 | end 10 | end 11 | 12 | if defined?(::ActiveRecord) && ::ActiveRecord::VERSION::MAJOR >= 3 13 | 14 | module ActiveModel 15 | # Open ActiveModel::Validations to define some additional ones 16 | module Validations 17 | 18 | # A simple EachValidator that will check for the presence of the flags specified 19 | class PresenceOfFlagsValidator < EachValidator 20 | def validate_each(record, attribute, value) 21 | value = record.send(:read_attribute_for_validation, attribute) 22 | check_flag(record, attribute) 23 | 24 | if value.blank? || value.zero? 25 | record.errors.add(attribute, :blank, **options) 26 | end 27 | end 28 | 29 | private 30 | 31 | def check_flag(record, attribute) 32 | unless record.class.flag_columns.include? attribute.to_s 33 | raise ArgumentError.new("#{attribute} is not one of the flags columns (#{record.class.flag_columns.join(', ')})") 34 | end 35 | end 36 | end 37 | 38 | # Use these validators in your model 39 | module HelperMethods 40 | # Validates that the specified attributes are flags and are not blank. 41 | # Happens by default on save. Example: 42 | # 43 | # class Spaceship < ActiveRecord::Base 44 | # include FlagShihTzu 45 | # 46 | # has_flags({ 1 => :warpdrive, 2 => :hyperspace }, :column => 'engines') 47 | # validates_presence_of_flags :engines 48 | # end 49 | # 50 | # The engines attribute must be a flag in the object and it cannot be blank. 51 | # 52 | # Configuration options: 53 | # * :message - A custom error message (default is: "can't be blank"). 54 | # * :on - Specifies when this validation is active. Runs in all 55 | # validation contexts by default (+nil+), other options are :create 56 | # and :update. 57 | # * :if - Specifies a method, proc or string to call to determine if 58 | # the validation should occur (e.g. :if => :allow_validation, or 59 | # :if => Proc.new { |user| user.signup_step > 2 }). The method, proc 60 | # or string should return or evaluate to a true or false value. 61 | # * :unless - Specifies a method, proc or string to call to determine 62 | # if the validation should not occur (e.g. :unless => :skip_validation, 63 | # or :unless => Proc.new { |spaceship| spaceship.warp_step <= 2 }). The method, 64 | # proc or string should return or evaluate to a true or false value. 65 | # * :strict - Specifies whether validation should be strict. 66 | # See ActiveModel::Validation#validates! for more information. 67 | def validates_presence_of_flags(*attr_names) 68 | validates_with PresenceOfFlagsValidator, _merge_attributes(attr_names) 69 | end 70 | end 71 | 72 | end 73 | end 74 | end 75 | -------------------------------------------------------------------------------- /bin/test.bash: -------------------------------------------------------------------------------- 1 | #!/bin/bash --login 2 | 3 | gem_installed() { 4 | num=$(gem list $1 | grep -e "^$1 " | wc -l) 5 | if [ $num -eq "1" ]; then 6 | echo "already installed $1" 7 | else 8 | echo "installing $1" 9 | gem install $1 10 | fi 11 | return 0 12 | } 13 | 14 | run_all_tests_for() { 15 | gemfile_location="gemfiles/Gemfile.activerecord-$2" 16 | rm -rf $gemfile_location.lock 17 | echo "rvm use $1@flag_shih_tzu-$2" 18 | rvm use $1@flag_shih_tzu-$2 --create 19 | gem_installed "bundler" 20 | echo "BUNDLE_GEMFILE=$gemfile_location bundle update --quiet" 21 | BUNDLE_GEMFILE=$gemfile_location bundle update --quiet 22 | echo "NOCOVER=true BUNDLE_GEMFILE=$gemfile_location bundle exec rake test" 23 | NOCOVER=true BUNDLE_GEMFILE=$gemfile_location bundle exec rake test 24 | Count=$(( $Count + 1 )) 25 | } 26 | 27 | # First run the tests for all versions supported on Ruby 1.9.3 28 | COMPATIBLE_VERSIONS=(2.3.x 3.0.x 3.1.x 3.2.x) 29 | Count=0 30 | while [ "x${COMPATIBLE_VERSIONS[Count]}" != "x" ] 31 | do 32 | rvm_ruby_version=1.9.3-p551 33 | rails_version=${COMPATIBLE_VERSIONS[Count]} 34 | run_all_tests_for $rvm_ruby_version $rails_version 35 | done 36 | 37 | # Then run the tests for all versions supported on Ruby 2.0.0 38 | COMPATIBLE_VERSIONS=(3.0.x 3.1.x 3.2.x 4.0.x 4.1.x) 39 | Count=0 40 | while [ "x${COMPATIBLE_VERSIONS[Count]}" != "x" ] 41 | do 42 | rvm_ruby_version=2.0.0-p648 43 | rails_version=${COMPATIBLE_VERSIONS[Count]} 44 | run_all_tests_for $rvm_ruby_version $rails_version 45 | done 46 | 47 | # Then run the tests for all versions supported on Ruby 2.1.5 48 | COMPATIBLE_VERSIONS=(3.2.x 4.0.x 4.1.x 4.2.x) 49 | Count=0 50 | while [ "x${COMPATIBLE_VERSIONS[Count]}" != "x" ] 51 | do 52 | rvm_ruby_version=2.1.10 53 | rails_version=${COMPATIBLE_VERSIONS[Count]} 54 | run_all_tests_for $rvm_ruby_version $rails_version 55 | done 56 | 57 | # Then run the tests for all versions supported on Ruby 2.2.3 58 | COMPATIBLE_VERSIONS=(3.2.x 4.0.x 4.1.x 4.2.x) 59 | Count=0 60 | while [ "x${COMPATIBLE_VERSIONS[Count]}" != "x" ] 61 | do 62 | rvm_ruby_version=2.2.7 63 | rails_version=${COMPATIBLE_VERSIONS[Count]} 64 | run_all_tests_for $rvm_ruby_version $rails_version 65 | done 66 | 67 | # Then run the tests for all versions supported on Ruby 2.5.1 68 | COMPATIBLE_VERSIONS=(5.0.x 5.1.x 5.2.x) 69 | Count=0 70 | while [ "x${COMPATIBLE_VERSIONS[Count]}" != "x" ] 71 | do 72 | rvm_ruby_version=2.5.1 73 | rails_version=${COMPATIBLE_VERSIONS[Count]} 74 | run_all_tests_for $rvm_ruby_version $rails_version 75 | done 76 | 77 | # Then run the tests for all versions supported on jruby-1.7.26 78 | # (which appears to pass for 3.1 - 4.2 inclusive) 79 | # TODO: Investigate 2 failures on Rails 2.3 and 3.0 80 | # assert_equal true, my_spaceship.update_flag!(:jeanlucpicard, false, true) 81 | # assert_equal true, my_spaceship.update_flag!(:jeanlucpicard, false) 82 | COMPATIBLE_VERSIONS=(3.1.x 3.2.x 4.0.x 4.1.x 4.2.x) 83 | Count=0 84 | while [ "x${COMPATIBLE_VERSIONS[Count]}" != "x" ] 85 | do 86 | rvm_ruby_version=jruby-1.7.26 87 | rails_version=${COMPATIBLE_VERSIONS[Count]} 88 | run_all_tests_for $rvm_ruby_version $rails_version 89 | done 90 | 91 | Then run the tests for all versions supported on jruby-9.1.8.0 (which is 9.1.5.0 in travis.yml) 92 | (which should be the same as the Ruby 2.2.3 compatibility set) 93 | COMPATIBLE_VERSIONS=(3.2.x 4.0.x 4.1.x 4.2.x 5.0.x 5.1.x) 94 | Count=0 95 | while [ "x${COMPATIBLE_VERSIONS[Count]}" != "x" ] 96 | do 97 | rvm_ruby_version=jruby-9.1.8.0 98 | rails_version=${COMPATIBLE_VERSIONS[Count]} 99 | run_all_tests_for $rvm_ruby_version $rails_version 100 | done 101 | -------------------------------------------------------------------------------- /REEK: -------------------------------------------------------------------------------- 1 | lib/flag_shih_tzu.rb -- 60 warnings: 2 | [4]:FlagShihTzu has no descriptive comment (IrresponsibleModule) 3 | [423, 431, 438, 445, 573]:FlagShihTzu takes parameters [colmn, flag] to 5 methods (DataClump) 4 | [532]:FlagShihTzu#chained_flags_with_signature has approx 6 statements (TooManyStatements) 5 | [566]:FlagShihTzu#collect_flags doesn't depend on instance state (maybe move it to another class?) (UtilityFunction) 6 | [433, 435]:FlagShihTzu#disable_flag calls self.class 2 times (DuplicateMethodCall) 7 | [432]:FlagShihTzu#disable_flag performs a nil-check (NilCheck) 8 | [425, 427]:FlagShihTzu#enable_flag calls self.class 2 times (DuplicateMethodCall) 9 | [424]:FlagShihTzu#enable_flag performs a nil-check (NilCheck) 10 | [446]:FlagShihTzu#flag_disabled? performs a nil-check (NilCheck) 11 | [439]:FlagShihTzu#flag_enabled? performs a nil-check (NilCheck) 12 | [502, 511, 512, 514, 515]:FlagShihTzu#update_flag! calls self.class 5 times (DuplicateMethodCall) 13 | [512, 515]:FlagShihTzu#update_flag! calls self.class.primary_key 2 times (DuplicateMethodCall) 14 | [500]:FlagShihTzu#update_flag! has approx 6 statements (TooManyStatements) 15 | [500]:FlagShihTzu#update_flag! has boolean parameter 'update_instance' (BooleanParameter) 16 | [503]:FlagShihTzu#update_flag! is controlled by argument update_instance (ControlParameter) 17 | [23]:FlagShihTzu::ClassMethods has no descriptive comment (IrresponsibleModule) 18 | [244, 257, 367, 386, 391]:FlagShihTzu::ClassMethods takes parameters [colmn, flag] to 5 methods (DataClump) 19 | [299, 301]:FlagShihTzu::ClassMethods#chained_flags_values calls flag.to_s 2 times (DuplicateMethodCall) 20 | [295]:FlagShihTzu::ClassMethods#chained_flags_values has approx 10 statements (TooManyStatements) 21 | [274, 276]:FlagShihTzu::ClassMethods#chained_flags_with calls chained_flags_condition(column, *args) 2 times (DuplicateMethodCall) 22 | [249, 249]:FlagShihTzu::ClassMethods#check_flag calls flag_mapping[colmn] 2 times (DuplicateMethodCall) 23 | [249]:FlagShihTzu::ClassMethods#check_flag performs a nil-check (NilCheck) 24 | [330]:FlagShihTzu::ClassMethods#check_flag_column has approx 11 statements (TooManyStatements) 25 | [346]:FlagShihTzu::ClassMethods#check_flag_column performs a nil-check (NilCheck) 26 | [263]:FlagShihTzu::ClassMethods#determine_flag_colmn_for performs a nil-check (NilCheck) 27 | [74, 80]:FlagShihTzu::ClassMethods#has_flags calls 1 << (flag_key - 1) 2 times (DuplicateMethodCall) 28 | [35, 42]:FlagShihTzu::ClassMethods#has_flags calls caller.first 2 times (DuplicateMethodCall) 29 | [139, 212, 220]:FlagShihTzu::ClassMethods#has_flags calls colmn.singularize 3 times (DuplicateMethodCall) 30 | [74, 80]:FlagShihTzu::ClassMethods#has_flags calls flag_key - 1 2 times (DuplicateMethodCall) 31 | [58, 74, 80]:FlagShihTzu::ClassMethods#has_flags calls flag_mapping[colmn] 3 times (DuplicateMethodCall) 32 | [157, 228]:FlagShihTzu::ClassMethods#has_flags calls flag_options[colmn] 2 times (DuplicateMethodCall) 33 | [34, 36, 38]:FlagShihTzu::ClassMethods#has_flags calls opts[:column] 3 times (DuplicateMethodCall) 34 | [61, 62]:FlagShihTzu::ClassMethods#has_flags calls self.flag_columns 2 times (DuplicateMethodCall) 35 | [52, 55]:FlagShihTzu::ClassMethods#has_flags calls self.flag_mapping 2 times (DuplicateMethodCall) 36 | [24]:FlagShihTzu::ClassMethods#has_flags has approx 26 statements (TooManyStatements) 37 | [55]:FlagShihTzu::ClassMethods#has_flags performs a nil-check (NilCheck) 38 | [411]:FlagShihTzu::ClassMethods#named_scope_method doesn't depend on instance state (maybe move it to another class?) (UtilityFunction) 39 | [315, 317]:FlagShihTzu::ClassMethods#parse_flag_options calls args.shift 2 times (DuplicateMethodCall) 40 | [314]:FlagShihTzu::ClassMethods#parse_flag_options doesn't depend on instance state (maybe move it to another class?) (UtilityFunction) 41 | [314]:FlagShihTzu::ClassMethods#parse_flag_options has approx 7 statements (TooManyStatements) 42 | [257]:FlagShihTzu::ClassMethods#set_flag_sql has 4 parameters (LongParameterList) 43 | [258]:FlagShihTzu::ClassMethods#set_flag_sql performs a nil-check (NilCheck) 44 | [373, 373]:FlagShihTzu::ClassMethods#sql_condition_for_flag calls flag_mapping[colmn] 2 times (DuplicateMethodCall) 45 | [373, 373]:FlagShihTzu::ClassMethods#sql_condition_for_flag calls flag_mapping[colmn][flag] 2 times (DuplicateMethodCall) 46 | [370, 374]:FlagShihTzu::ClassMethods#sql_condition_for_flag calls flag_options[colmn] 2 times (DuplicateMethodCall) 47 | [370, 374]:FlagShihTzu::ClassMethods#sql_condition_for_flag calls flag_options[colmn][:flag_query_mode] 2 times (DuplicateMethodCall) 48 | [367]:FlagShihTzu::ClassMethods#sql_condition_for_flag has 4 parameters (LongParameterList) 49 | [367]:FlagShihTzu::ClassMethods#sql_condition_for_flag has approx 7 statements (TooManyStatements) 50 | [367]:FlagShihTzu::ClassMethods#sql_condition_for_flag has boolean parameter 'enabled' (BooleanParameter) 51 | [373, 378]:FlagShihTzu::ClassMethods#sql_condition_for_flag is controlled by argument enabled (ControlParameter) 52 | [391]:FlagShihTzu::ClassMethods#sql_set_for_flag has 4 parameters (LongParameterList) 53 | [391]:FlagShihTzu::ClassMethods#sql_set_for_flag has boolean parameter 'enabled' (BooleanParameter) 54 | [391]:FlagShihTzu::ClassMethods#sql_set_for_flag has unused parameter 'custom_table_name' (UnusedParameters) 55 | [393]:FlagShihTzu::ClassMethods#sql_set_for_flag is controlled by argument enabled (ControlParameter) 56 | [404]:FlagShihTzu::ClassMethods#valid_flag_column_name? doesn't depend on instance state (maybe move it to another class?) (UtilityFunction) 57 | [396]:FlagShihTzu::ClassMethods#valid_flag_key? doesn't depend on instance state (maybe move it to another class?) (UtilityFunction) 58 | [400]:FlagShihTzu::ClassMethods#valid_flag_name? doesn't depend on instance state (maybe move it to another class?) (UtilityFunction) 59 | [21]:FlagShihTzu::DuplicateFlagColumnException has no descriptive comment (IrresponsibleModule) 60 | [20]:FlagShihTzu::NoSuchFlagException has no descriptive comment (IrresponsibleModule) 61 | [19]:FlagShihTzu::NoSuchFlagQueryModeException has no descriptive comment (IrresponsibleModule) 62 | lib/flag_shih_tzu/validators.rb -- 3 warnings: 63 | [29, 30]:ActiveModel::Validations::PresenceOfFlagsValidator#check_flag calls record.class 2 times (DuplicateMethodCall) 64 | [29, 30]:ActiveModel::Validations::PresenceOfFlagsValidator#check_flag calls record.class.flag_columns 2 times (DuplicateMethodCall) 65 | [29, 30]:ActiveModel::Validations::PresenceOfFlagsValidator#check_flag refers to record more than self (maybe move it to another class?) (FeatureEnvy) 66 | 63 total warnings 67 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # HEAD - UNRELEASED 2 | 3 | * Work merged into master branch goes here until it is released. 4 | 5 | # Version 0.3.23 - NOV.30.2018 6 | 7 | * Avoid establishing a database connection unless necessary by Jonathan del Strother 8 | 9 | # Version 0.3.22 - SEP.18.2018 10 | 11 | * When #selected_flags= passed with nil it clears flag bits, by xpol 12 | - This makes flag_shih_tzu behave like Rails: converts empty array to nil. 13 | 14 | # Version 0.3.21 - SEP.09.2018 15 | 16 | * Make required minimum Ruby version explicit: 1.9.3 and up by Peter Boling 17 | * Support Rails 5.2 by Peter Boling 18 | * Add Ruby 2.5 to build, and update/fix build by Peter Boling 19 | 20 | # Version 0.3.20 - SEP.08.2018 21 | 22 | * Fix generated instance methods. by xpol 23 | * Support Rails 5.1 saved_change by shiro16 24 | 25 | # Version 0.3.19 - MAY.15.2017 26 | 27 | * Fixed a bug in Rails 5 support. 28 | * Added Rails 5.1 to travis. 29 | 30 | # Version 0.3.18 - APR.30.2017 31 | * Switched from Fixnum to Integer for Ruby 2.4 happiness 32 | * Fixed build for all supported Ruby and Rails versions in supported matrix 33 | 34 | # Version 0.3.17 - APR.29.2017 35 | * Improved compatibility with Rails 5.0 36 | * Fixed warnings about Fixnums 37 | * Fixed compatibility with SQLlite 38 | * Added Ruby 2.3, and 2.4 to the Travis Matrix 39 | * Removed JRuby 1.7 from the Travis Matrix 40 | 41 | # Version 0.3.16 - JAN.16.2017 42 | 43 | * Fix complex custom sql queries with multiple references to the column by vegetaras 44 | * Improved documentation and compatibility matrix by Peter Boling 45 | 46 | # Version 0.3.15 - OCT.11.2015 47 | 48 | * Fixed testing for all supported environments by Peter Boling 49 | * Testing on Travis: added Ruby jruby-9.0.1.0 by Peter Boling 50 | * Documented compatibility matrix in table in README by Peter Boling 51 | 52 | # Version 0.3.14 - OCT.08.2015 53 | 54 | * Allow use without ActiveRecord (experimental) by jfcaiceo 55 | * Many net-zero code cleanups to follow Ruby Style Guide 56 | * Improved local testing script rake test:all 57 | * Testing on Travis: added Ruby 1.9.3, 2.1.5, 2.2.3, jruby-1.7.0 58 | * Testing on Travis: removed Ruby 2.1.2 59 | 60 | # Version 0.3.13 - MAR.13.2015 61 | 62 | * methods for use with form builders like simple_form by Peter Boling 63 | 64 | - chained_flags_with_signature 65 | - chained_#{colmn}_with_signature 66 | - as_flag_collection 67 | - as_#{colmn.singularize}_collection 68 | - selected_flags= 69 | - selected_#{colmn}= (already existed) 70 | * Testing on Travis: added Ruby 2.2.1 71 | * Testing on Travis: removed Ruby 1.9.2 72 | 73 | # Version 0.3.12 - OCT.01.2014 74 | 75 | * Improve testing instructions in readme by Peter Boling 76 | * fix check_flag_column to return false after warn by Peter Boling 77 | * bash script for running complete test suite on Ruby 1.9.3 and Ruby 2.1.2 by Peter Boling 78 | * Improve documentation in readme by trliner 79 | * use aliases to make attribute reader methods more DRY by trliner 80 | * add negative attribute reader and writer methods for improved interoperability with simple_form and formtastic by trliner 81 | * Adds specs for ActiveRecord version 4.1 by Peter Boling 82 | * Use Kernel#warn instead of puts by Peter Boling 83 | 84 | # Version 0.3.11 - JUL.09.2014 85 | 86 | * Rename some ambigously-named methods mixed into AR::Base by jdelStrother 87 | * Add dynamic ".*_values_for" helpers by atipugin 88 | 89 | # Version 0.3.10 - NOV.26.2013 90 | 91 | * Can run tests without coverage by specifying NOCOVER=true by Peter Boling 92 | * Improved test coverage by Peter Boling 93 | * Improved documentation by Peter Boling 94 | * Readme converted to Markdown by Peter Boling 95 | 96 | # Version 0.3.9 - NOV.25.2013 97 | 98 | * Removed runtime dependency on active record and active support by Peter Boling 99 | * Fixed Coveralls Configuration by Peter Boling 100 | * Improved Readme by Peter Boling 101 | 102 | # Version 0.3.8 - NOV.24.2013 103 | 104 | * Improved Readme / Documentation by Peter Boling 105 | * Added Badges by Peter Boling 106 | * Configured Coveralls by Peter Boling 107 | * Added Code Climate, Coveralls, Gemnasium, and Version Badges by Peter Boling 108 | 109 | # Version 0.3.7 - OCT.25.2013 110 | 111 | * Change `sql_in_for_flag` to consider values from the range [0, 2 * max - 1] by Blake Thomson 112 | 113 | # Version 0.3.6 - AUG.29.2013 114 | 115 | * Allow use with any gem manager by Peter Boling 116 | * No need to alter Ruby's load path by Peter Boling 117 | 118 | # Version 0.3.5 - AUG.06.2013 119 | 120 | * Fix Travis Build & Add Rails 4 by Peter M. Goldstein 121 | * Implemented update_flag! by Peter Boling (see https://github.com/pboling/flag_shih_tzu/issues/27) 122 | - sets a flag on a record without triggering callbacks or validations 123 | - optionally syncs the instance with new flag value, by default it does not. 124 | * Update gemspec by Peter Boling 125 | 126 | # Version 0.3.4 - JUN.20.2013 127 | 128 | * Allow non sequential flag numbers by Thomas Jachmann 129 | * Report correct source location for class_evaled methods. by Sebastian Korfmann 130 | * Implemented chained_flags_with, which allows optimizing the bit search by Tatsuhiko Miyagawa 131 | * [bugfix] flag_options[colmn][:column] is symbol, it causes error undefined method `length' for nil:NilClass by Artem Pisarev 132 | * Validator raises an error if the validated column is not a flags column. by David DIDIER 133 | * Allow multiple include by Peter Goldstein 134 | * fix a deprecation warning in rails4 by Mose 135 | * Add flag_keys convenience method. by Keith Pitty 136 | * [bugfix] Column names provided as symbols fully work now, as they are converted to strings by Peter Boling 137 | * [bugfix issues/28] Since 0.3.0 flags no longer work in a class using an alternative database connection by Peter Boling 138 | * [bugfix issues/7] Breaks db:create rake task by Peter Boling 139 | * convenience methods now have default parameter so `all_flags` works with arity 0. by Peter Boling 140 | * Many more tests, including arity tests by Peter Boling 141 | 142 | # Version 0.3.3 - JUN.20.2013 143 | 144 | - Does not exist. 145 | 146 | # Version 0.3.2 - NOV.06.2012 147 | 148 | * Adds skip column check option :check_for_column - from arturaz 149 | * Adds a 'smart' set_flag_sql method which will auto determine the correct column for the given flag - from arturaz 150 | * Changes the behavior of sql_set_for_flag to not use table names in the generated SQL 151 | - because it didn't actually work before 152 | - Now there is a test ensuring that the generated SQL can be executed by a real DB 153 | - This improved sql_set_for_flag underlies the public set_flag_sql method 154 | 155 | # Version 0.3.1 - NOV.06.2012 156 | 157 | * Adds new methods (for a flag column named 'bar', with many individual flags within) - from ddidier 158 | - all_bar, selected_bar, select_all_bar, unselect_all_bar, selected_bar=(selected_flags), has_bar? 159 | 160 | # Version 0.3.0 - NOV.05.2012 - first version maintained by Peter Boling 161 | 162 | * ClassWithHasFlags.set_#{flag_name}_sql # Returns the sql string for setting a flag for use in customized SQL 163 | * ClassWithHasFlags.unset_#{flag_name}_sql # Returns the sql string for unsetting a flag for use in customized SQL 164 | * ClassWithHasFlags.flag_columns # Returns the column_names used by FlagShihTzu as bit fields 165 | * has_flags :strict => true # DuplicateFlagColumnException raised when a single DB column is declared as a flag column twice 166 | * Less verbosity for expected conditions when the DB connection for the class is unavailable. 167 | * Tests for additional features, but does not change any behavior of 0.2.3 / 0.2.4 by default. 168 | * Easily migrate from 0.2.3 and 0.2.4. Goal is no code changes required. Minor version bump to encourage caution. 169 | 170 | # Version 0.2.4 - NOV.05.2012 - released last few changes from XING master 171 | 172 | * Fix deprecation warning for set_table_name 173 | * Optional bang methods 174 | * Complete Ruby 1.9(\.[^1]) and Rails 3.2.X compatibility 175 | 176 | # Version 0.2.3 - last version maintained by XING AG 177 | -------------------------------------------------------------------------------- /lib/flag_shih_tzu.rb: -------------------------------------------------------------------------------- 1 | # Would like to support other database adapters so no more hard dependency on Active Record. 2 | require "flag_shih_tzu/validators" 3 | 4 | module FlagShihTzu 5 | # taken from ActiveRecord::ConnectionAdapters::Column 6 | TRUE_VALUES = [true, 1, "1", "t", "T", "true", "TRUE"] 7 | 8 | DEFAULT_COLUMN_NAME = "flags" 9 | 10 | def self.included(base) 11 | base.extend(ClassMethods) 12 | base.class_attribute :flag_options unless defined?(base.flag_options) 13 | base.class_attribute :flag_mapping unless defined?(base.flag_mapping) 14 | base.class_attribute :flag_columns unless defined?(base.flag_columns) 15 | end 16 | 17 | # TODO: Inherit from StandardException 18 | class IncorrectFlagColumnException < Exception; end 19 | class NoSuchFlagQueryModeException < Exception; end 20 | class NoSuchFlagException < Exception; end 21 | class DuplicateFlagColumnException < Exception; end 22 | 23 | module ClassMethods 24 | def has_flags(*args) 25 | flag_hash, opts = parse_flag_options(*args) 26 | opts = 27 | { 28 | named_scopes: true, 29 | column: DEFAULT_COLUMN_NAME, 30 | flag_query_mode: :in_list, # or :bit_operator 31 | strict: false, 32 | check_for_column: true 33 | }.update(opts) 34 | if !valid_flag_column_name?(opts[:column]) 35 | warn %[FlagShihTzu says: Please use a String to designate column names! I see you here: #{caller.first}] 36 | opts[:column] = opts[:column].to_s 37 | end 38 | colmn = opts[:column] 39 | if opts[:check_for_column] && (active_record_class? && !check_flag_column(colmn)) 40 | warn( 41 | %[FlagShihTzu says: Flag column #{colmn} appears to be missing! 42 | To turn off this warning set check_for_column: false in has_flags definition here: #{caller.first}] 43 | ) 44 | return 45 | end 46 | 47 | # options are stored in a class level hash and apply per-column 48 | self.flag_options ||= {} 49 | flag_options[colmn] = opts 50 | 51 | # the mappings are stored in this class level hash and apply per-column 52 | self.flag_mapping ||= {} 53 | # If we already have an instance of the same column in the flag_mapping, 54 | # then there is a double definition on a column 55 | if opts[:strict] && !self.flag_mapping[colmn].nil? 56 | raise DuplicateFlagColumnException 57 | end 58 | flag_mapping[colmn] ||= {} 59 | 60 | # keep track of which flag columns are defined on this class 61 | self.flag_columns ||= [] 62 | self.flag_columns << colmn 63 | 64 | flag_hash.each do |flag_key, flag_name| 65 | unless valid_flag_key?(flag_key) 66 | raise ArgumentError, 67 | %[has_flags: flag keys should be positive integers, and #{flag_key} is not] 68 | end 69 | unless valid_flag_name?(flag_name) 70 | raise ArgumentError, 71 | %[has_flags: flag names should be symbols, and #{flag_name} is not] 72 | end 73 | # next if method already defined by flag_shih_tzu 74 | next if flag_mapping[colmn][flag_name] & (1 << (flag_key - 1)) 75 | if method_defined?(flag_name) 76 | raise ArgumentError, 77 | %[has_flags: flag name #{flag_name} already defined, please choose different name] 78 | end 79 | 80 | flag_mapping[colmn][flag_name] = 1 << (flag_key - 1) 81 | 82 | class_eval <<-EVAL, __FILE__, __LINE__ + 1 83 | def #{flag_name} 84 | flag_enabled?(:#{flag_name}, "#{colmn}") 85 | end 86 | alias :#{flag_name}? :#{flag_name} 87 | 88 | def #{flag_name}=(value) 89 | FlagShihTzu::TRUE_VALUES.include?(value) ? 90 | enable_flag(:#{flag_name}, "#{colmn}") : 91 | disable_flag(:#{flag_name}, "#{colmn}") 92 | end 93 | 94 | def not_#{flag_name} 95 | !#{flag_name} 96 | end 97 | alias :not_#{flag_name}? :not_#{flag_name} 98 | 99 | def not_#{flag_name}=(value) 100 | FlagShihTzu::TRUE_VALUES.include?(value) ? 101 | disable_flag(:#{flag_name}, "#{colmn}") : 102 | enable_flag(:#{flag_name}, "#{colmn}") 103 | end 104 | 105 | def #{flag_name}_changed? 106 | if colmn_changes = changes["#{colmn}"] 107 | flag_bit = self.class.flag_mapping["#{colmn}"][:#{flag_name}] 108 | (colmn_changes[0] & flag_bit) != (colmn_changes[1] & flag_bit) 109 | else 110 | false 111 | end 112 | end 113 | 114 | EVAL 115 | 116 | if active_record_class? 117 | class_eval <<-EVAL, __FILE__, __LINE__ + 1 118 | def self.#{flag_name}_condition(options = {}) 119 | sql_condition_for_flag( 120 | :#{flag_name}, 121 | "#{colmn}", 122 | true, 123 | options[:table_alias] || table_name 124 | ) 125 | end 126 | 127 | def self.not_#{flag_name}_condition(options = {}) 128 | sql_condition_for_flag( 129 | :#{flag_name}, 130 | "#{colmn}", 131 | false, 132 | options[:table_alias] || table_name 133 | ) 134 | end 135 | 136 | def self.set_#{flag_name}_sql 137 | sql_set_for_flag(:#{flag_name}, "#{colmn}", true) 138 | end 139 | 140 | def self.unset_#{flag_name}_sql 141 | sql_set_for_flag(:#{flag_name}, "#{colmn}", false) 142 | end 143 | EVAL 144 | 145 | # Define the named scopes if the user wants them and AR supports it 146 | if flag_options[colmn][:named_scopes] 147 | if ActiveRecord::VERSION::MAJOR == 2 && respond_to?(:named_scope) 148 | class_eval <<-EVAL, __FILE__, __LINE__ + 1 149 | named_scope :#{flag_name}, lambda { 150 | { conditions: #{flag_name}_condition } 151 | } 152 | named_scope :not_#{flag_name}, lambda { 153 | { conditions: not_#{flag_name}_condition } 154 | } 155 | EVAL 156 | elsif respond_to?(:scope) 157 | # Prevent deprecation notices on Rails 3 158 | # when using +named_scope+ instead of +scope+. 159 | # Prevent deprecation notices on Rails 4 160 | # when using +conditions+ instead of +where+. 161 | class_eval <<-EVAL, __FILE__, __LINE__ + 1 162 | scope :#{flag_name}, lambda { 163 | where(#{flag_name}_condition) 164 | } 165 | scope :not_#{flag_name}, lambda { 166 | where(not_#{flag_name}_condition) 167 | } 168 | EVAL 169 | end 170 | end 171 | 172 | if method_defined?(:saved_changes) 173 | class_eval <<-EVAL, __FILE__, __LINE__ + 1 174 | def saved_change_to_#{flag_name}? 175 | if colmn_changes = saved_changes["#{colmn}"] 176 | flag_bit = self.class.flag_mapping["#{colmn}"][:#{flag_name}] 177 | (colmn_changes[0] & flag_bit) != (colmn_changes[1] & flag_bit) 178 | else 179 | false 180 | end 181 | end 182 | EVAL 183 | end 184 | end 185 | 186 | # Define bang methods when requested 187 | if flag_options[colmn][:bang_methods] 188 | class_eval <<-EVAL, __FILE__, __LINE__ + 1 189 | def #{flag_name}! 190 | enable_flag(:#{flag_name}, "#{colmn}") 191 | end 192 | 193 | def not_#{flag_name}! 194 | disable_flag(:#{flag_name}, "#{colmn}") 195 | end 196 | EVAL 197 | end 198 | 199 | end 200 | 201 | if colmn != DEFAULT_COLUMN_NAME 202 | class_eval <<-EVAL, __FILE__, __LINE__ + 1 203 | 204 | def all_#{colmn} 205 | all_flags("#{colmn}") 206 | end 207 | 208 | def selected_#{colmn} 209 | selected_flags("#{colmn}") 210 | end 211 | 212 | def select_all_#{colmn} 213 | select_all_flags("#{colmn}") 214 | end 215 | 216 | def unselect_all_#{colmn} 217 | unselect_all_flags("#{colmn}") 218 | end 219 | 220 | # useful for a form builder 221 | def selected_#{colmn}=(chosen_flags) 222 | unselect_all_flags("#{colmn}") 223 | return if chosen_flags.nil? 224 | chosen_flags.each do |selected_flag| 225 | enable_flag(selected_flag.to_sym, "#{colmn}") if selected_flag.present? 226 | end 227 | end 228 | 229 | def has_#{colmn.singularize}? 230 | not selected_#{colmn}.empty? 231 | end 232 | 233 | def chained_#{colmn}_with_signature(*args) 234 | chained_flags_with_signature("#{colmn}", *args) 235 | end 236 | 237 | def as_#{colmn.singularize}_collection(*args) 238 | as_flag_collection("#{colmn}", *args) 239 | end 240 | 241 | EVAL 242 | end 243 | 244 | if active_record_class? 245 | class_eval <<-EVAL, __FILE__, __LINE__ + 1 246 | def self.#{colmn.singularize}_values_for(*flag_names) 247 | values = [] 248 | flag_names.each do |flag_name| 249 | if respond_to?(flag_name) 250 | values_for_flag = send(:sql_in_for_flag, flag_name, "#{colmn}") 251 | values = if values.present? 252 | values & values_for_flag 253 | else 254 | values_for_flag 255 | end 256 | end 257 | end 258 | 259 | values.sort 260 | end 261 | EVAL 262 | end 263 | end 264 | 265 | def check_flag(flag, colmn) 266 | unless colmn.is_a?(String) 267 | raise ArgumentError, 268 | %[Column name "#{colmn}" for flag "#{flag}" is not a string] 269 | end 270 | if flag_mapping[colmn].nil? || !flag_mapping[colmn].include?(flag) 271 | raise ArgumentError, 272 | %[Invalid flag "#{flag}"] 273 | end 274 | end 275 | 276 | # Returns SQL statement to enable/disable flag. 277 | # Automatically determines the correct column. 278 | def set_flag_sql(flag, value, colmn = nil, custom_table_name = table_name) 279 | colmn = determine_flag_colmn_for(flag) if colmn.nil? 280 | sql_set_for_flag(flag, colmn, value, custom_table_name) 281 | end 282 | 283 | def determine_flag_colmn_for(flag) 284 | return DEFAULT_COLUMN_NAME if flag_mapping.nil? 285 | flag_mapping.each_pair do |colmn, mapping| 286 | return colmn if mapping.include?(flag) 287 | end 288 | raise NoSuchFlagException.new( 289 | %[determine_flag_colmn_for: Couldn't determine column for your flags!] 290 | ) 291 | end 292 | 293 | def chained_flags_with(column = DEFAULT_COLUMN_NAME, *args) 294 | if (ActiveRecord::VERSION::MAJOR >= 3) 295 | where(chained_flags_condition(column, *args)) 296 | else 297 | all(conditions: chained_flags_condition(column, *args)) 298 | end 299 | end 300 | 301 | def chained_flags_condition(colmn = DEFAULT_COLUMN_NAME, *args) 302 | %[(#{flag_full_column_name(table_name, colmn)} in (#{chained_flags_values(colmn, *args).join(",")}))] 303 | end 304 | 305 | def flag_keys(colmn = DEFAULT_COLUMN_NAME) 306 | flag_mapping[colmn].keys 307 | end 308 | 309 | private 310 | 311 | def flag_full_column_name(table, column) 312 | "#{connection.quote_table_name(table)}.#{connection.quote_column_name(column)}" 313 | end 314 | 315 | def flag_full_column_name_for_assignment(table, column) 316 | if (ActiveRecord::VERSION::MAJOR <= 3) 317 | # If you're trying to do multi-table updates with Rails < 4, sorry - you're out of luck. 318 | connection.quote_column_name(column) 319 | else 320 | connection.quote_table_name_for_assignment(table, column) 321 | end 322 | end 323 | 324 | def flag_value_range_for_column(colmn) 325 | max = flag_mapping[colmn].values.max 326 | Range.new(0, (2 * max) - 1) 327 | end 328 | 329 | def chained_flags_values(colmn, *args) 330 | val = flag_value_range_for_column(colmn).to_a 331 | args.each do |flag| 332 | neg = false 333 | if flag.to_s.match(/^not_/) 334 | neg = true 335 | flag = flag.to_s.sub(/^not_/, "").to_sym 336 | end 337 | check_flag(flag, colmn) 338 | flag_values = sql_in_for_flag(flag, colmn) 339 | if neg 340 | val = val - flag_values 341 | else 342 | val = val & flag_values 343 | end 344 | end 345 | val 346 | end 347 | 348 | def parse_flag_options(*args) 349 | options = args.shift 350 | add_options = if args.size >= 1 351 | args.shift 352 | else 353 | options. 354 | keys. 355 | select { |key| !key.is_a?(Integer) }. 356 | inject({}) do |hash, key| 357 | hash[key] = options.delete(key) 358 | hash 359 | end 360 | end 361 | [options, add_options] 362 | end 363 | 364 | def check_flag_column(colmn, custom_table_name = table_name) 365 | # If you aren't using ActiveRecord (eg. you are outside rails) 366 | # then do not fail here 367 | # If you are using ActiveRecord then you only want to check for the 368 | # table if the table exists so it won't fail pre-migration 369 | has_ar = (!!defined?(ActiveRecord) && respond_to?(:descends_from_active_record?)) 370 | # Supposedly Rails 2.3 takes care of this, but this precaution 371 | # is needed for backwards compatibility 372 | has_table = if has_ar 373 | if ::ActiveRecord::VERSION::MAJOR >= 5 374 | connection.data_sources.include?(custom_table_name) 375 | else 376 | connection.tables.include?(custom_table_name) 377 | end 378 | else 379 | true 380 | end 381 | if has_table 382 | found_column = columns.detect { |column| column.name == colmn } 383 | # If you have not yet run the migration that adds the 'flags' column 384 | # then we don't want to fail, 385 | # because we need to be able to run the migration 386 | # If the column is there but is of the wrong type, 387 | # then we must fail, because flag_shih_tzu will not work 388 | if found_column.nil? 389 | warn( 390 | %[Error: Column "#{colmn}" doesn't exist on table "#{custom_table_name}". Did you forget to run migrations?] 391 | ) 392 | return false 393 | elsif found_column.type != :integer 394 | raise IncorrectFlagColumnException.new( 395 | %[Table "#{custom_table_name}" must have an integer column named "#{colmn}" in order to use FlagShihTzu.] 396 | ) 397 | end 398 | else 399 | # ActiveRecord gem may not have loaded yet? 400 | warn( 401 | %[FlagShihTzu#has_flags: Table "#{custom_table_name}" doesn't exist. Have all migrations been run?] 402 | ) if has_ar 403 | return false 404 | end 405 | 406 | true 407 | 408 | # Quietly ignore NoDatabaseErrors - presumably we're being run during, eg, `rails db:create`. 409 | # NoDatabaseError was only introduced in Rails 4.1, which is why this error-handling is a bit convoluted. 410 | rescue StandardError => e 411 | if defined?(ActiveRecord::NoDatabaseError) && e.is_a?(ActiveRecord::NoDatabaseError) 412 | true 413 | else 414 | raise 415 | end 416 | end 417 | 418 | def sql_condition_for_flag(flag, colmn, enabled = true, custom_table_name = table_name) 419 | check_flag(flag, colmn) 420 | 421 | if flag_options[colmn][:flag_query_mode] == :bit_operator 422 | # use & bit operator directly in the SQL query. 423 | # This has the drawback of not using an index on the flags colum. 424 | %[(#{flag_full_column_name(custom_table_name, colmn)} & #{flag_mapping[colmn][flag]} = #{enabled ? flag_mapping[colmn][flag] : 0})] 425 | elsif flag_options[colmn][:flag_query_mode] == :in_list 426 | # use IN() operator in the SQL query. 427 | # This has the drawback of becoming a big query 428 | # when you have lots of flags. 429 | neg = enabled ? "" : "not " 430 | %[(#{flag_full_column_name(custom_table_name, colmn)} #{neg}in (#{sql_in_for_flag(flag, colmn).join(",")}))] 431 | else 432 | raise NoSuchFlagQueryModeException 433 | end 434 | end 435 | 436 | # returns an array of integers suitable for a SQL IN statement. 437 | def sql_in_for_flag(flag, colmn) 438 | val = flag_mapping[colmn][flag] 439 | flag_value_range_for_column(colmn).select { |bits| bits & val == val } 440 | end 441 | 442 | def sql_set_for_flag(flag, colmn, enabled = true, custom_table_name = table_name) 443 | check_flag(flag, colmn) 444 | lhs_name = flag_full_column_name_for_assignment(custom_table_name, colmn) 445 | rhs_name = flag_full_column_name(custom_table_name, colmn) 446 | "#{lhs_name} = #{rhs_name} #{enabled ? "| " : "& ~" }#{flag_mapping[colmn][flag]}" 447 | end 448 | 449 | def valid_flag_key?(flag_key) 450 | flag_key > 0 && flag_key == flag_key.to_i 451 | end 452 | 453 | def valid_flag_name?(flag_name) 454 | flag_name.is_a?(Symbol) 455 | end 456 | 457 | def valid_flag_column_name?(colmn) 458 | colmn.is_a?(String) 459 | end 460 | 461 | # Returns the correct method to create a named scope. 462 | # Use to prevent deprecation notices on Rails 3 463 | # when using +named_scope+ instead of +scope+. 464 | def named_scope_method 465 | # Can't use respond_to because both AR 2 and 3 466 | # respond to both +scope+ and +named_scope+. 467 | ActiveRecord::VERSION::MAJOR == 2 ? :named_scope : :scope 468 | end 469 | 470 | def active_record_class? 471 | ancestors.include?(ActiveRecord::Base) 472 | end 473 | end 474 | 475 | # Performs the bitwise operation so the flag will return +true+. 476 | def enable_flag(flag, colmn = nil) 477 | colmn = determine_flag_colmn_for(flag) if colmn.nil? 478 | self.class.check_flag(flag, colmn) 479 | 480 | set_flags(flags(colmn) | self.class.flag_mapping[colmn][flag], colmn) 481 | end 482 | 483 | # Performs the bitwise operation so the flag will return +false+. 484 | def disable_flag(flag, colmn = nil) 485 | colmn = determine_flag_colmn_for(flag) if colmn.nil? 486 | self.class.check_flag(flag, colmn) 487 | 488 | set_flags(flags(colmn) & ~self.class.flag_mapping[colmn][flag], colmn) 489 | end 490 | 491 | def flag_enabled?(flag, colmn = nil) 492 | colmn = determine_flag_colmn_for(flag) if colmn.nil? 493 | self.class.check_flag(flag, colmn) 494 | 495 | get_bit_for(flag, colmn) == 0 ? false : true 496 | end 497 | 498 | def flag_disabled?(flag, colmn = nil) 499 | colmn = determine_flag_colmn_for(flag) if colmn.nil? 500 | self.class.check_flag(flag, colmn) 501 | 502 | !flag_enabled?(flag, colmn) 503 | end 504 | 505 | def flags(colmn = DEFAULT_COLUMN_NAME) 506 | self[colmn] || 0 507 | end 508 | 509 | def set_flags(value, colmn = DEFAULT_COLUMN_NAME) 510 | self[colmn] = value 511 | end 512 | 513 | def all_flags(colmn = DEFAULT_COLUMN_NAME) 514 | flag_mapping[colmn].keys 515 | end 516 | 517 | def selected_flags(colmn = DEFAULT_COLUMN_NAME) 518 | all_flags(colmn). 519 | map { |flag_name| self.send(flag_name) ? flag_name : nil }. 520 | compact 521 | end 522 | 523 | # Useful for a form builder 524 | # use selected_#{column}= for custom column names. 525 | def selected_flags=(chosen_flags) 526 | unselect_all_flags 527 | return if chosen_flags.nil? 528 | chosen_flags.each do |selected_flag| 529 | if selected_flag.present? 530 | enable_flag(selected_flag.to_sym, DEFAULT_COLUMN_NAME) 531 | end 532 | end 533 | end 534 | 535 | def select_all_flags(colmn = DEFAULT_COLUMN_NAME) 536 | all_flags(colmn).each do |flag| 537 | enable_flag(flag, colmn) 538 | end 539 | end 540 | 541 | def unselect_all_flags(colmn = DEFAULT_COLUMN_NAME) 542 | all_flags(colmn).each do |flag| 543 | disable_flag(flag, colmn) 544 | end 545 | end 546 | 547 | def has_flag?(colmn = DEFAULT_COLUMN_NAME) 548 | not selected_flags(colmn).empty? 549 | end 550 | 551 | # returns true if successful 552 | # third parameter allows you to specify that `self` should 553 | # also have its in-memory flag attribute updated. 554 | def update_flag!(flag, value, update_instance = false) 555 | truthy = FlagShihTzu::TRUE_VALUES.include?(value) 556 | sql = self.class.set_flag_sql(flag.to_sym, truthy) 557 | if update_instance 558 | if truthy 559 | enable_flag(flag) 560 | else 561 | disable_flag(flag) 562 | end 563 | end 564 | if (ActiveRecord::VERSION::MAJOR <= 3) 565 | self.class. 566 | update_all(sql, self.class.primary_key => id) == 1 567 | else 568 | self.class. 569 | where("#{self.class.primary_key} = ?", id). 570 | update_all(sql) == 1 571 | end 572 | end 573 | 574 | # Use with chained_flags_with to find records with specific flags 575 | # set to the same values as on this record. 576 | # For a record that has sent_warm_up_email = true and the other flags false: 577 | # 578 | # user.chained_flags_with_signature 579 | # => [:sent_warm_up_email, 580 | # :not_follow_up_called, 581 | # :not_sent_final_email, 582 | # :not_scheduled_appointment] 583 | # User.chained_flags_with("flags", *user.chained_flags_with_signature) 584 | # => the set of Users that have the same flags set as user. 585 | # 586 | def chained_flags_with_signature(colmn = DEFAULT_COLUMN_NAME, *args) 587 | flags_to_collect = args.empty? ? all_flags(colmn) : args 588 | truthy_and_chosen = 589 | selected_flags(colmn). 590 | select { |flag| flags_to_collect.include?(flag) } 591 | truthy_and_chosen.concat( 592 | collect_flags(*flags_to_collect) do |memo, flag| 593 | memo << "not_#{flag}".to_sym unless truthy_and_chosen.include?(flag) 594 | end 595 | ) 596 | end 597 | 598 | # Use with a checkbox form builder, like rails' or simple_form's 599 | # :selected_flags, used in the example below, is a method defined 600 | # by flag_shih_tzu for bulk setting flags like this: 601 | # 602 | # form_for @user do |f| 603 | # f.collection_check_boxes(:selected_flags, 604 | # f.object.as_flag_collection("flags", 605 | # :sent_warm_up_email, 606 | # :not_follow_up_called), 607 | # :first, 608 | # :last) 609 | # end 610 | # 611 | def as_flag_collection(colmn = DEFAULT_COLUMN_NAME, *args) 612 | flags_to_collect = args.empty? ? all_flags(colmn) : args 613 | collect_flags(*flags_to_collect) do |memo, flag| 614 | memo << [flag, flag_enabled?(flag, colmn)] 615 | end 616 | end 617 | 618 | private 619 | 620 | def collect_flags(*args) 621 | args.inject([]) do |memo, flag| 622 | yield memo, flag 623 | memo 624 | end 625 | end 626 | 627 | def get_bit_for(flag, colmn) 628 | flags(colmn) & self.class.flag_mapping[colmn][flag] 629 | end 630 | 631 | def determine_flag_colmn_for(flag) 632 | self.class.determine_flag_colmn_for(flag) 633 | end 634 | end 635 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🏁 FlagShihTzu 2 | 3 | [![Liberapay Goal Progress][⛳liberapay-img]][⛳liberapay] [![Sponsor Me on Github][🖇sponsor-img]][🖇sponsor] [![Buy me a coffee][🖇buyme-small-img]][🖇buyme] [![Donate on Polar][🖇polar-img]][🖇polar] [![Donate to my FLOSS or refugee efforts at ko-fi.com][🖇kofi-img]][🖇kofi] [![Donate to my FLOSS or refugee efforts using Patreon][🖇patreon-img]][🖇patreon] 4 | 5 | --- 6 | 7 | Bit fields for ActiveRecord 8 | 9 | | Project | FlagShihTzu | 10 | |------------------------ | ----------------- | 11 | | gem name | flag_shih_tzu | 12 | | license | [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](https://opensource.org/licenses/MIT) | 13 | | expert support | [![Get help on Codementor](https://cdn.codementor.io/badges/get_help_github.svg)](https://www.codementor.io/peterboling?utm_source=github&utm_medium=button&utm_term=peterboling&utm_campaign=github) | 14 | | download rank | [![Total Downloads](https://img.shields.io/gem/rt/flag_shih_tzu.svg)](https://rubygems.org/gems/flag_shih_tzu) [![Daily Downloads](https://img.shields.io/gem/rd/flag_shih_tzu.svg)](https://rubygems.org/gems/flag_shih_tzu) | 15 | | version | [![Gem Version](https://badge.fury.io/rb/flag_shih_tzu.png)](http://badge.fury.io/rb/flag_shih_tzu) | 16 | | dependencies | [![Depfu](https://badges.depfu.com/badges/f011a69cf2426f91483aaade580823ac/count.svg)](https://depfu.com/github/pboling/flag_shih_tzu?project_id=2685) | 17 | | code quality | [![Code Climate](https://codeclimate.com/github/pboling/flag_shih_tzu.png)](https://codeclimate.com/github/pboling/flag_shih_tzu) [![Reviewed by Hound](https://img.shields.io/badge/Reviewed_by-Hound-8E64B0.svg)](https://houndci.com) | 18 | | inline documenation | [![Inline docs](http://inch-ci.org/github/pboling/flag_shih_tzu.png)](http://inch-ci.org/github/pboling/flag_shih_tzu) | 19 | | continuous integration | [![Build Status](https://secure.travis-ci.org/pboling/flag_shih_tzu.png?branch=master)](https://travis-ci.org/pboling/flag_shih_tzu) | 20 | | test coverage | [![Coverage Status](https://coveralls.io/repos/pboling/flag_shih_tzu/badge.png)](https://coveralls.io/r/pboling/flag_shih_tzu) | 21 | | homepage | [https://github.com/pboling/flag_shih_tzu][homepage] | 22 | | documentation | [http://rdoc.info/github/pboling/flag_shih_tzu/frames][documentation] | 23 | | live chat | [![Join the chat at https://gitter.im/pboling/flag_shih_tzu](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/pboling/flag_shih_tzu?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) | 24 | | Spread ~♡ⓛⓞⓥⓔ♡~ | [🌏](https://about.me/peter.boling), [👼](https://angel.co/peter-boling), [:shipit:](http://coderwall.com/pboling), [![Tweet Peter](https://img.shields.io/twitter/follow/galtzo.svg?style=social&label=Follow)](http://twitter.com/galtzo) | 25 | 26 | Table of Contents 27 | ================= 28 | 29 | * [FlagShihTzu](#flagshihtzu) 30 | * [Summary](#summary) 31 | * [Prerequisites](#prerequisites) 32 | * [Compatibility Matrix](#compatibility-matrix) 33 | * [Installation](#installation) 34 | * [Rails 2.x](#rails-2x) 35 | * [Rails 3](#rails-3) 36 | * [Usage](#usage) 37 | * [Defaults (Important)](#defaults-important) 38 | * [Database Migration](#database-migration) 39 | * [Adding to the Model](#adding-to-the-model) 40 | * [Bit Fields: How it stores the values](#bit-fields-how-it-stores-the-values) 41 | * [Using a custom column name](#using-a-custom-column-name) 42 | * [Generated boolean patterned instance methods](#generated-boolean-patterned-instance-methods) 43 | * [Callbacks and Validations](#callbacks-and-validations) 44 | * [Generated class methods](#generated-class-methods) 45 | * [Generated named scopes](#generated-named-scopes) 46 | * [Examples for using the generated methods](#examples-for-using-the-generated-methods) 47 | * [Support for manually building conditions](#support-for-manually-building-conditions) 48 | * [Choosing a query mode](#choosing-a-query-mode) 49 | * [Updating flag column by raw sql](#updating-flag-column-by-raw-sql) 50 | * [Skipping flag column check](#skipping-flag-column-check) 51 | * [Running the gem tests](#running-the-gem-tests) 52 | * [Authors](#authors) 53 | * [How you can help!](#how-you-can-help) 54 | * [Contributing](#contributing) 55 | * [Versioning](#versioning) 56 | * [2012 Change of Ownership and 0.3.X Release Notes](#2012-change-of-ownership-and-03x-release-notes) 57 | * [Alternatives](#alternatives) 58 | * [License](#license) 59 | 60 | ## Summary 61 | 62 | An extension for [ActiveRecord](https://rubygems.org/gems/activerecord) 63 | to store a collection of boolean attributes in a single integer column 64 | as a [bit field][bit_field]. 65 | 66 | This gem lets you use a single integer column in an ActiveRecord model 67 | to store a collection of boolean attributes (flags). Each flag can be used 68 | almost in the same way you would use any boolean attribute on an 69 | ActiveRecord object. 70 | 71 | The benefits: 72 | 73 | * No migrations needed for new boolean attributes. This helps a lot 74 | if you have very large db-tables, on which you want to avoid `ALTER TABLE` 75 | whenever possible. 76 | * Only the one integer column needs to be indexed. 77 | * [Bitwise Operations][bitwise_operation] are fast! 78 | 79 | Using FlagShihTzu, you can add new boolean attributes whenever you want, 80 | without needing any migration. Just add a new flag to the `has_flags` call. 81 | 82 | What is a ["Shih Tzu"](http://en.wikipedia.org/wiki/Shih_Tzu)? 83 | 84 | 85 | ## Prerequisites 86 | 87 | The gem is actively being tested against: 88 | 89 | * MySQL, PostgreSQL and SQLite3 databases (Both Ruby and JRuby adapters) 90 | * ActiveRecord versions 2.3.x, 3.0.x, 3.1.x, 3.2.x, 4.0.x, 4.1.x, 4.2.x, 5.0.x, 5.1.x, 5.2.x 91 | * Ruby 1.9.3, 2.0.0, 2.1.10, 2.2.10, 2.3.7, 2.4.4, 2.5.1, jruby-1.7.x, jruby-9.1.x 92 | * Travis tests the supported builds. See [.travis.yml](https://github.com/pboling/flag_shih_tzu/blob/master/.travis.yml) for the matrix. 93 | * All of the supported builds can also be run locally using the `wwtd` gem. 94 | * Forthcoming flag_shih_tzu v1.0 will only support Ruby 2.2+, JRuby-9.1+ and Rails 4.2+ 95 | 96 | ### Compatibility Matrix 97 | 98 | | Ruby / Active Record | 2.3.x | 3.0.x | 3.1.x | 3.2.x | 4.0.x | 4.1.x | 4.2.x | 5.0.x | 5.1.x | 5.2.x | 99 | |:---------------------:|:-----:|:-----:|:-----:|:-----:|:-----:|:-----:|:-----:|:-----:|:-----:|:-----:| 100 | | 1.9.3 | ✓ | ✓ | ✓ | ✓ | | | | | | | 101 | | 2.0.0 | | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | | | | 102 | | 2.1.x | | | | ✓ | ✓ | ✓ | ✓ | | | | 103 | | 2.2.0-2.2.1 | | | | ✓ | ✓ | ✓ | ✓ | | | | 104 | | 2.2.2+ | | | | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | 105 | | 2.3.x | | | | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | 106 | | 2.4.x | | | | | | | ✓ | ✓ | ✓ | ✓ | 107 | | 2.5.x | | | | | | | ✓ | ✓ | ✓ | ✓ | 108 | | jruby-1.7.x | ? | ? | ✓ | ✓ | ✓ | ✓ | ✓ | | | | 109 | | jruby-9.1 | | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | 110 | | jruby-9.2 | | | | | | | ✓ | ✓ | ✓ | ✓ | 111 | 112 | * Notes 113 | - JRuby 1.7.x aims for MRI 1.9.3 compatibility 114 | - `?` indicates incompatible due to 2 failures in the test suite. 115 | - JRuby 9.1 aims for MRI 2.2 compatibility 116 | - JRuby 9.2 aims for MRI 2.5 compatibility 117 | - Forthcoming flag_shih_tzu v1.0 will only support Ruby 2.2+, JRuby-9.1+ and Rails 4.2+ 118 | 119 | **Legacy** 120 | 121 | * Ruby 1.8.7 compatibility is in the [0.2.X branch](https://github.com/pboling/flag_shih_tzu/tree/0.2.X) and no further releases are expected. If you need a patch submit a pull request. 122 | 123 | * Ruby 1.9.3, 2.0.0, 2.1.x, and 2.2.x compatibility is still current on master, and the 0.3.x series releases, but those EOL'd Rubies, and any others that become EOL'd in the meantime, will not be supported in the next major release, version 1.0. 124 | 125 | ## Installation 126 | 127 | ### Rails 2.x 128 | 129 | In environment.rb: 130 | 131 | ```ruby 132 | config.gem 'flag_shih_tzu' 133 | ``` 134 | 135 | Then: 136 | 137 | $ rake gems:install # use sudo if necessary 138 | 139 | ### Rails 3 140 | 141 | In Gemfile: 142 | 143 | ```ruby 144 | gem 'flag_shih_tzu' 145 | ``` 146 | 147 | Then: 148 | 149 | $ bundle install 150 | 151 | 152 | ## Usage 153 | 154 | FlagShihTzu assumes that your ActiveRecord model already has an [integer field][bit_field] 155 | to store the flags, which should be defined to not allow `NULL` values and 156 | should have a default value of `0`. 157 | 158 | ### Defaults (Important) 159 | 160 | * Due to the default of `0`, *all flags* are initially set to "false"). 161 | * For a default of true it will probably be easier in the long run to negate the flag's meaning / name. 162 | ** Such as `switched_on` => `switched_off` 163 | * If you **really** want a different, non-zero, default value for a flag column, proceed *adroitly* with a different sql default for the flag column. 164 | 165 | ### Database Migration 166 | 167 | I like to document the intent of the `flags` column in the migration when I can... 168 | 169 | ```ruby 170 | change_table :spaceships do |t| 171 | t.integer :flags, :null => false, :default => 0 # flag_shih_tzu-managed bit field 172 | # Effective booleans which will be stored on the flags column: 173 | # t.boolean :warpdrive 174 | # t.boolean :shields 175 | # t.boolean :electrolytes 176 | end 177 | ``` 178 | 179 | ### Adding to the Model 180 | 181 | ```ruby 182 | class Spaceship < ActiveRecord::Base 183 | include FlagShihTzu 184 | 185 | has_flags 1 => :warpdrive, 186 | 2 => :shields, 187 | 3 => :electrolytes 188 | end 189 | ``` 190 | 191 | `has_flags` takes a hash. The keys must be positive integers and represent 192 | the position of the bit being used to enable or disable the flag. 193 | **The keys must not be changed once in use, or you will get incorrect results.** 194 | That is why the plugin forces you to set them explicitly. 195 | The values are symbols for the flags being created. 196 | 197 | 198 | ### Bit Fields: How it stores the values 199 | 200 | As said, FlagShihTzu uses a single integer column to store the values for all 201 | the defined flags as a [bit field][bitfield]. 202 | 203 | The bit position of a flag corresponds to the given key. 204 | 205 | This way, we can use [bitwise operators][bit_operation] on the stored integer value to set, unset 206 | and check individual flags. 207 | 208 | `---+---+---+ +---+---+---` 209 | | | | | | | | | 210 | Bit position | 3 | 2 | 1 | | 3 | 2 | 1 | 211 | (flag key) | | | | | | | | 212 | `---+---+---+ +---+---+---` 213 | | | | | | | | | 214 | Bit value | 4 | 2 | 1 | | 4 | 2 | 1 | 215 | | | | | | | | | 216 | `---+---+---+ +---+---+---` 217 | | e | s | w | | e | s | w | 218 | | l | h | a | | l | h | a | 219 | | e | i | r | | e | i | r | 220 | | c | e | p | | c | e | p | 221 | | t | l | d | | t | l | d | 222 | | r | d | r | | r | d | r | 223 | | o | s | i | | o | s | i | 224 | | l | | v | | l | | v | 225 | | y | | e | | y | | e | 226 | | t | | | | t | | | 227 | | e | | | | e | | | 228 | | s | | | | s | | | 229 | `---+---+---+ +---+---+---` 230 | | 1 | 1 | 0 | = 4 ` 2 = 6 | 1 | 0 | 1 | = 4 ` 1 = 5 231 | `---+---+---+ +---+---+---` 232 | 233 | Read more about [bit fields][bit_field] here: http://en.wikipedia.org/wiki/Bit_field 234 | 235 | 236 | ### Using a custom column name 237 | 238 | The default column name to store the flags is `flags`, but you can provide a 239 | custom column name using the `:column` option. This allows you to use 240 | different columns for separate flags: 241 | 242 | ```ruby 243 | has_flags 1 => :warpdrive, 244 | 2 => :shields, 245 | 3 => :electrolytes, 246 | :column => 'features' 247 | 248 | has_flags 1 => :spock, 249 | 2 => :scott, 250 | 3 => :kirk, 251 | :column => 'crew' 252 | ``` 253 | 254 | ### Generated boolean patterned instance methods 255 | 256 | Calling `has_flags`, as shown above on the 'features' column, creates the following instance methods 257 | on Spaceship: 258 | 259 | Spaceship#all_features # [:warpdrive, :shields, :electrolytes] 260 | Spaceship#selected_features 261 | Spaceship#select_all_features 262 | Spaceship#unselect_all_features 263 | Spaceship#selected_features= 264 | 265 | Spaceship#warpdrive 266 | Spaceship#warpdrive? 267 | Spaceship#warpdrive= 268 | Spaceship#not_warpdrive 269 | Spaceship#not_warpdrive? 270 | Spaceship#not_warpdrive= 271 | Spaceship#warpdrive_changed? 272 | Spaceship#has_warpdrive? 273 | 274 | Spaceship#shields 275 | Spaceship#shields? 276 | Spaceship#shields= 277 | Spaceship#not_shields 278 | Spaceship#not_shields? 279 | Spaceship#not_shields= 280 | Spaceship#shields_changed? 281 | Spaceship#has_shield? 282 | 283 | Spaceship#electrolytes 284 | Spaceship#electrolytes? 285 | Spaceship#electrolytes= 286 | Spaceship#not_electrolytes 287 | Spaceship#not_electrolytes? 288 | Spaceship#not_electrolytes= 289 | Spaceship#electrolytes_changed? 290 | Spaceship#has_electrolyte? 291 | 292 | 293 | ### Callbacks and Validations 294 | 295 | Optionally, you can set the `:bang_methods` option to true to also define the bang methods: 296 | 297 | Spaceship#electrolytes! # will save the bitwise equivalent of electrolytes = true on the record 298 | Spaceship#not_electrolytes! # will save the bitwise equivalent of electrolytes = false on the record 299 | 300 | which respectively enables or disables the electrolytes flag. 301 | 302 | The `:bang_methods` does not save the records to the database, meaning it *cannot* engage validations and callbacks. 303 | 304 | Alternatively, if you do want to *save a flag* to the database, while still avoiding validations and callbacks, use `update_flag!` which: 305 | 306 | * sets a flag on a database record without triggering callbacks or validations 307 | * optionally syncs the ruby instance with new flag value, by default it does not. 308 | 309 | 310 | Example: 311 | 312 | ```ruby 313 | update_flag!(flag_name, flag_value, update_instance = false) 314 | ``` 315 | 316 | 317 | ### Generated class methods 318 | 319 | Calling `has_flags` as shown above creates the following class methods 320 | on Spaceship: 321 | 322 | ```ruby 323 | Spaceship.flag_columns # [:features, :crew] 324 | ``` 325 | 326 | 327 | ### Generated named scopes 328 | 329 | The following named scopes become available: 330 | 331 | ```ruby 332 | Spaceship.warpdrive # :conditions => "(spaceships.flags in (1,3,5,7))" 333 | Spaceship.not_warpdrive # :conditions => "(spaceships.flags not in (1,3,5,7))" 334 | Spaceship.shields # :conditions => "(spaceships.flags in (2,3,6,7))" 335 | Spaceship.not_shields # :conditions => "(spaceships.flags not in (2,3,6,7))" 336 | Spaceship.electrolytes # :conditions => "(spaceships.flags in (4,5,6,7))" 337 | Spaceship.not_electrolytes # :conditions => "(spaceships.flags not in (4,5,6,7))" 338 | ``` 339 | 340 | If you do not want the named scopes to be defined, set the 341 | `:named_scopes` option to false when calling `has_flags`: 342 | 343 | ```ruby 344 | has_flags 1 => :warpdrive, 2 => :shields, 3 => :electrolytes, :named_scopes => false 345 | ``` 346 | 347 | In a Rails 3+ application, FlagShihTzu will use `scope` internally to generate 348 | the scopes. The option on `has_flags` is still named `:named_scopes` however. 349 | 350 | 351 | ### Examples for using the generated methods 352 | 353 | ```ruby 354 | enterprise = Spaceship.new 355 | enterprise.warpdrive = true 356 | enterprise.shields = true 357 | enterprise.electrolytes = false 358 | enterprise.save 359 | 360 | if enterprise.shields? 361 | # ... 362 | end 363 | 364 | Spaceship.warpdrive.find(:all) 365 | Spaceship.not_electrolytes.count 366 | ``` 367 | 368 | 369 | ### Support for manually building conditions 370 | 371 | The following class methods may support you when manually building 372 | ActiveRecord conditions: 373 | 374 | ```ruby 375 | Spaceship.warpdrive_condition # "(spaceships.flags in (1,3,5,7))" 376 | Spaceship.not_warpdrive_condition # "(spaceships.flags not in (1,3,5,7))" 377 | Spaceship.shields_condition # "(spaceships.flags in (2,3,6,7))" 378 | Spaceship.not_shields_condition # "(spaceships.flags not in (2,3,6,7))" 379 | Spaceship.electrolytes_condition # "(spaceships.flags in (4,5,6,7))" 380 | Spaceship.not_electrolytes_condition # "(spaceships.flags not in (4,5,6,7))" 381 | ``` 382 | 383 | These methods also accept a `:table_alias` option that can be used when 384 | generating SQL that references the same table more than once: 385 | ```ruby 386 | Spaceship.shields_condition(:table_alias => 'evil_spaceships') # "(evil_spaceships.flags in (2,3,6,7))" 387 | ``` 388 | 389 | 390 | ### Choosing a query mode 391 | 392 | While the default way of building the SQL conditions uses an `IN()` list 393 | (as shown above), this approach will not work well for a high number of flags, 394 | as the value list for `IN()` grows. 395 | 396 | For MySQL, depending on your MySQL settings, this can even hit the 397 | `max_allowed_packet` limit with the generated query, or the similar query length maximum for PostgreSQL. 398 | 399 | In this case, consider changing the flag query mode to `:bit_operator` 400 | instead of `:in_list`, like so: 401 | 402 | ```ruby 403 | has_flags 1 => :warpdrive, 404 | 2 => :shields, 405 | :flag_query_mode => :bit_operator 406 | ``` 407 | 408 | This will modify the generated condition and named_scope methods to use bit 409 | operators in the SQL instead of an `IN()` list: 410 | 411 | ```ruby 412 | Spaceship.warpdrive_condition # "(spaceships.flags & 1 = 1)", 413 | Spaceship.not_warpdrive_condition # "(spaceships.flags & 1 = 0)", 414 | Spaceship.shields_condition # "(spaceships.flags & 2 = 2)", 415 | Spaceship.not_shields_condition # "(spaceships.flags & 2 = 0)", 416 | 417 | Spaceship.warpdrive # :conditions => "(spaceships.flags & 1 = 1)" 418 | Spaceship.not_warpdrive # :conditions => "(spaceships.flags & 1 = 0)" 419 | Spaceship.shields # :conditions => "(spaceships.flags & 2 = 2)" 420 | Spaceship.not_shields # :conditions => "(spaceships.flags & 2 = 0)" 421 | ``` 422 | 423 | The drawback is that due to the [bitwise operation][bitwise_operation] being done on the SQL side, 424 | this query can not use an index on the flags column. 425 | 426 | ### Updating flag column by raw sql 427 | 428 | If you need to do mass updates without initializing object for each row, you can 429 | use `#set_flag_sql` method on your class. Example: 430 | 431 | ```ruby 432 | Spaceship.set_flag_sql(:warpdrive, true) # "flags = flags | 1" 433 | Spaceship.set_flag_sql(:shields, false) # "flags = flags & ~2" 434 | ``` 435 | 436 | And then use it in: 437 | 438 | ```ruby 439 | Spaceship.update_all Spaceship.set_flag_sql(:shields, false) 440 | ``` 441 | 442 | Beware that using multiple flag manipulation sql statements in the same query 443 | probably will not have the desired effect (at least on sqlite3, not tested 444 | on other databases), so you *should not* do this: 445 | 446 | ```ruby 447 | Spaceship.update_all "#{Spaceship.set_flag_sql(:shields, false)},#{ 448 | Spaceship.set_flag_sql(:warpdrive, true)}" 449 | ``` 450 | 451 | General rule of thumb: issue only one flag update per update statement. 452 | 453 | ### Skipping flag column check 454 | 455 | By default when you call has_flags in your code it will automatically check 456 | your database to see if you have correct column defined. 457 | 458 | Sometimes this may not be a wanted behaviour (e.g. when loading model without 459 | database connection established) so you can set `:check_for_column` option to 460 | false to avoid it. 461 | 462 | ```ruby 463 | has_flags 1 => :warpdrive, 464 | 2 => :shields, 465 | :check_for_column => false 466 | ``` 467 | 468 | 469 | ## Running the gem tests 470 | 471 | WARNING: You may want to read [bin/test.bash](https://github.com/pboling/flag_shih_tzu/blob/master/bin/test.bash) first. 472 | Running the test script will switch rubies, create gemsets, install gems, and get a lil' crazy with the hips. 473 | 474 | Just: 475 | 476 | $ rake test:all 477 | 478 | This will internally use rvm and bundler to load specific Rubies and ActiveRecord versions 479 | before executing the tests (see `gemfiles/`), e.g.: 480 | 481 | $ NOCOVER=true BUNDLE_GEMFILE='gemfiles/Gemfile.activerecord-4.1.x' bundle exec rake test 482 | 483 | All tests will use an in-memory sqlite database by default. 484 | If you want to use a different database, see `test/database.yml`, 485 | install the required adapter gem and use the DB environment variable to 486 | specify which config from `test/database.yml` to use, e.g.: 487 | 488 | $ NOCOVER=true DB=mysql bundle exec rake 489 | 490 | You will also need to create, and configure access to, the test databases for any adapters you want to test, e.g. mysql: 491 | 492 | mysql> CREATE USER 'foss'@'localhost'; 493 | Query OK, 0 rows affected (0.00 sec) 494 | 495 | mysql> GRANT ALL PRIVILEGES ON *.* TO 'foss'@'localhost'; 496 | Query OK, 0 rows affected (0.00 sec) 497 | 498 | mysql> CREATE DATABASE flag_shih_tzu_test; 499 | Query OK, 1 row affected (0.00 sec) 500 | 501 | ## Authors 502 | 503 | [Peter Boling](http://github.com/pboling), 504 | [Patryk Peszko](http://github.com/ppeszko), 505 | [Sebastian Roebke](http://github.com/boosty), 506 | [David Anderson](http://github.com/alpinegizmo), 507 | [Tim Payton](http://github.com/dizzy42) 508 | and a helpful group of 509 | [contributors](https://github.com/pboling/flag_shih_tzu/contributors). 510 | Thanks! 511 | 512 | Find out more about Peter Boling's work 513 | [RailsBling.com](http://railsbling.com/). 514 | 515 | Find out more about XING 516 | [Devblog](http://devblog.xing.com/). 517 | 518 | 519 | ## How you can help! 520 | 521 | Take a look at the `reek` list, which is the file called `REEK`, and stat fixing things. Once you complete a change, run the tests. See "Running the gem tests". 522 | 523 | If the tests pass refresh the `reek` list: 524 | 525 | bundle exec rake reek > REEK 526 | 527 | Follow the instructions for "Contributing" below. 528 | 529 | 530 | ## Contributing 531 | 532 | 1. Fork it 533 | 2. Create your feature branch (`git checkout -b my-new-feature`) 534 | 3. Commit your changes (`git commit -am 'Added some feature'`) 535 | 4. Push to the branch (`git push origin my-new-feature`) 536 | 5. Make sure to add tests for it. This is important so I don't break it in a future version unintentionally. 537 | 6. Create new Pull Request 538 | 539 | 540 | ## Versioning 541 | 542 | This library aims to adhere to [Semantic Versioning 2.0.0](http://semver.org/). 543 | Violations of this scheme should be reported as bugs. Specifically, 544 | if a minor or patch version is released that breaks backward 545 | compatibility, a new version should be immediately released that 546 | restores compatibility. Breaking changes to the public API will 547 | only be introduced with new major versions. 548 | 549 | As a result of this policy, you can (and should) specify a 550 | dependency on this gem using the [Pessimistic Version Constraint](http://docs.rubygems.org/read/chapter/16#page74) with two digits of precision. 551 | 552 | For example: 553 | 554 | ```ruby 555 | spec.add_dependency 'flag_shih_tzu', '~> 0.0' 556 | ``` 557 | 558 | ## 2012 Change of Ownership and 0.3.X Release Notes 559 | 560 | FlagShihTzu was originally a [XING AG](http://www.xing.com/) project. [Peter Boling](http://peterboling.com) was a long time contributor and watcher of the project. 561 | In September 2012 XING transferred ownership of the project to Peter Boling. Peter Boling had been maintaining a 562 | fork with extended capabilities. These additional features become a part of the 0.3 line. The 0.2 line of the gem will 563 | remain true to XING's original. The 0.3 line aims to maintain complete parity and compatibility with XING's original as 564 | well. I will continue to monitor other forks for original ideas and improvements. Pull requests are welcome, but please 565 | rebase your work onto the current master to make integration easier. 566 | 567 | More information on the changes for 0.3.X: [pboling/flag_shih_tzu/wiki/Changes-for-0.3.x](https://github.com/pboling/flag_shih_tzu/wiki/Changes-for-0.3.x) 568 | 569 | ## Alternatives 570 | 571 | I discovered in October 2015 that Michael Grosser had created a competing tool, `bitfields`, way back in 2010, exactly a year after this tool was created. It was a very surreal moment, as I had thought this was the only game in town and it was when I began using and hacking on it. Once I got over that moment I became excited, because competition makes things better, right? So, now I am looking forward to a shootout some lazy Saturday. Until then there's this: http://www.railsbling.com/posts/why-use-flag_shih_tzu/ 572 | 573 | There is little that `bitfields` does better. The code is [less efficient](https://github.com/grosser/bitfields/blob/master/lib/bitfields.rb#L186 "recalculating and throwing away much of the result in many places"), albeit more readable, not as well tested, has almost zero inline documentation, and simply can't do many of the things I've built into `flag_shih_tzu`. If you are still on legacy Ruby or legacy Rails, or using jRuby, then use `flag_shih_tzu`. If you need multiple flag columns on a single model, use `flag_shih_tzu`. 574 | 575 | Will there ever be a merb/rails-like love fest between the projects? It would be interesting. I like his name better. I like my features better. I like some of his code better, and some of my code better. I've been wanting to do a full re-write of `flag_shih_tzu` ever since I inherited the project from [XING](https://github.com/xing), but I haven't had time. So I don't know. 576 | 577 | ## License 578 | 579 | MIT License 580 | 581 | Copyright (c) 2011 [XING AG](http://www.xing.com/) 582 | 583 | Copyright (c) 2012 - 2018 [Peter Boling][peterboling] of [RailsBling.com][railsbling] 584 | 585 | Permission is hereby granted, free of charge, to any person obtaining 586 | a copy of this software and associated documentation files (the 587 | "Software"), to deal in the Software without restriction, including 588 | without limitation the rights to use, copy, modify, merge, publish, 589 | distribute, sublicense, and/or sell copies of the Software, and to 590 | permit persons to whom the Software is furnished to do so, subject to 591 | the following conditions: 592 | 593 | The above copyright notice and this permission notice shall be 594 | included in all copies or substantial portions of the Software. 595 | 596 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 597 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 598 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 599 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 600 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 601 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 602 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 603 | 604 | [semver]: http://semver.org/ 605 | [pvc]: http://docs.rubygems.org/read/chapter/16#page74 606 | [railsbling]: http://www.railsbling.com 607 | [peterboling]: http://www.peterboling.com 608 | [documentation]: http://rdoc.info/github/pboling/flag_shih_tzu/frames 609 | [homepage]: https://github.com/pboling/flag_shih_tzu 610 | [bit_field]: http://en.wikipedia.org/wiki/Bit_field 611 | [bitwise_operation]: http://en.wikipedia.org/wiki/Bitwise_operation 612 | 613 | 614 | 615 | ## 🤑 One more thing 616 | 617 | Having arrived at the bottom of the page, please endure a final supplication. 618 | The primary maintainer of this gem, Peter Boling, wants 619 | Ruby to be a great place for people to solve problems, big and small. 620 | Please consider supporting his efforts via the giant yellow link below, 621 | or one of smaller ones, depending on button size preference. 622 | 623 | [![Buy me a latte][🖇buyme-img]][🖇buyme] 624 | 625 | [![Liberapay Goal Progress][⛳liberapay-img]][⛳liberapay] [![Sponsor Me on Github][🖇sponsor-img]][🖇sponsor] [![Donate on Polar][🖇polar-img]][🖇polar] [![Donate to my FLOSS or refugee efforts at ko-fi.com][🖇kofi-img]][🖇kofi] [![Donate to my FLOSS or refugee efforts using Patreon][🖇patreon-img]][🖇patreon] 626 | 627 | P.S. Use the gem => Discord for help 628 | 629 | [![Live Chat on Discord][✉️discord-invite-img]][✉️discord-invite] 630 | 631 | [⛳liberapay-img]: https://img.shields.io/liberapay/goal/pboling.svg?logo=liberapay 632 | [⛳liberapay]: https://liberapay.com/pboling/donate 633 | [🖇sponsor-img]: https://img.shields.io/badge/Sponsor_Me!-pboling.svg?style=social&logo=github 634 | [🖇sponsor]: https://github.com/sponsors/pboling 635 | [🖇polar-img]: https://img.shields.io/badge/polar-donate-yellow.svg 636 | [🖇polar]: https://polar.sh/pboling 637 | [🖇kofi-img]: https://img.shields.io/badge/a_more_different_coffee-✓-yellow.svg 638 | [🖇kofi]: https://ko-fi.com/O5O86SNP4 639 | [🖇patreon-img]: https://img.shields.io/badge/patreon-donate-yellow.svg 640 | [🖇patreon]: https://patreon.com/galtzo 641 | [🖇buyme-small-img]: https://img.shields.io/badge/buy_me_a_coffee-✓-yellow.svg?style=flat 642 | [🖇buyme-img]: https://img.buymeacoffee.com/button-api/?text=Buy%20me%20a%20latte&emoji=&slug=pboling&button_colour=FFDD00&font_colour=000000&font_family=Cookie&outline_colour=000000&coffee_colour=ffffff 643 | [🖇buyme]: https://www.buymeacoffee.com/pboling 644 | [✉️discord-invite]: https://discord.gg/3qme4XHNKN 645 | [✉️discord-invite-img]: https://img.shields.io/discord/1373797679469170758?style=for-the-badge 646 | -------------------------------------------------------------------------------- /test/flag_shih_tzu_test.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.dirname(__FILE__) + "/test_helper.rb") 2 | 3 | class Spaceship < ActiveRecord::Base 4 | self.table_name = "spaceships" 5 | include FlagShihTzu 6 | 7 | has_flags 1 => :warpdrive, 8 | 2 => :shields, 9 | 3 => :electrolytes 10 | end 11 | 12 | class SpaceshipWithoutNamedScopes < ActiveRecord::Base 13 | self.table_name = "spaceships" 14 | include FlagShihTzu 15 | 16 | has_flags(1 => :warpdrive, 17 | named_scopes: false) 18 | end 19 | 20 | class SpaceshipWithoutNamedScopesOldStyle < ActiveRecord::Base 21 | self.table_name = "spaceships" 22 | include FlagShihTzu 23 | 24 | has_flags({ 1 => :warpdrive }, 25 | named_scopes: false) 26 | end 27 | 28 | class SpaceshipWithCustomFlagsColumn < ActiveRecord::Base 29 | self.table_name = "spaceships_with_custom_flags_column" 30 | include FlagShihTzu 31 | 32 | has_flags(1 => :warpdrive, 33 | 2 => :hyperspace, 34 | column: "bits") 35 | end 36 | 37 | class SpaceshipWithColumnNameAsSymbol < ActiveRecord::Base 38 | self.table_name = "spaceships_with_custom_flags_column" 39 | include FlagShihTzu 40 | 41 | has_flags(1 => :warpdrive, 42 | 2 => :hyperspace, 43 | column: :bits) 44 | end 45 | 46 | class SpaceshipWith2CustomFlagsColumn < ActiveRecord::Base 47 | self.table_name = "spaceships_with_2_custom_flags_column" 48 | include FlagShihTzu 49 | 50 | has_flags( 51 | { 1 => :warpdrive, 2 => :hyperspace }, 52 | column: "bits") 53 | has_flags( 54 | { 1 => :jeanlucpicard, 2 => :dajanatroj }, 55 | column: "commanders") 56 | end 57 | 58 | class SpaceshipWith3CustomFlagsColumn < ActiveRecord::Base 59 | self.table_name = "spaceships_with_3_custom_flags_column" 60 | include FlagShihTzu 61 | 62 | has_flags({ 1 => :warpdrive, 63 | 2 => :hyperspace }, 64 | column: "engines") 65 | has_flags({ 1 => :photon, 66 | 2 => :laser, 67 | 3 => :ion_cannon, 68 | 4 => :particle_beam }, 69 | column: "weapons") 70 | has_flags({ 1 => :power, 71 | 2 => :anti_ax_routine }, 72 | column: "hal3000") 73 | end 74 | 75 | class SpaceshipWithInListQueryMode < ActiveRecord::Base 76 | self.table_name = "spaceships" 77 | include FlagShihTzu 78 | 79 | has_flags(1 => :warpdrive, 2 => :shields, flag_query_mode: :in_list) 80 | end 81 | 82 | class SpaceshipWithBitOperatorQueryMode < ActiveRecord::Base 83 | self.table_name = "spaceships" 84 | include FlagShihTzu 85 | 86 | has_flags(1 => :warpdrive, 2 => :shields, flag_query_mode: :bit_operator) 87 | end 88 | 89 | class SpaceshipWithBangMethods < ActiveRecord::Base 90 | self.table_name = "spaceships" 91 | include FlagShihTzu 92 | 93 | has_flags(1 => :warpdrive, 2 => :shields, bang_methods: true) 94 | end 95 | 96 | class SpaceshipWithMissingFlags < ActiveRecord::Base 97 | self.table_name = "spaceships" 98 | include FlagShihTzu 99 | 100 | has_flags 1 => :warpdrive, 101 | 3 => :electrolytes 102 | end 103 | 104 | class SpaceCarrier < Spaceship 105 | end 106 | 107 | if (ActiveRecord::VERSION::MAJOR >= 3) 108 | class SpaceshipWithSymbolAndStringFlagColumns < ActiveRecord::Base 109 | self.table_name = "spaceships_with_symbol_and_string_flag_columns" 110 | include FlagShihTzu 111 | 112 | has_flags({ 1 => :warpdrive, 113 | 2 => :hyperspace }, 114 | column: :peace, 115 | check_for_column: true) 116 | has_flags({ 1 => :photon, 117 | 2 => :laser, 118 | 3 => :ion_cannon, 119 | 4 => :particle_beam }, 120 | column: :love, 121 | check_for_column: true) 122 | has_flags({ 1 => :power, 123 | 2 => :anti_ax_routine }, 124 | column: "happiness", 125 | check_for_column: true) 126 | validates_presence_of_flags :peace, :love 127 | end 128 | 129 | class SpaceshipWithValidationsAndCustomFlagsColumn < ActiveRecord::Base 130 | self.table_name = "spaceships_with_custom_flags_column" 131 | include FlagShihTzu 132 | 133 | has_flags(1 => :warpdrive, 2 => :hyperspace, :column => "bits") 134 | validates_presence_of_flags :bits 135 | end 136 | 137 | class SpaceshipWithValidationsAnd3CustomFlagsColumn < ActiveRecord::Base 138 | self.table_name = "spaceships_with_3_custom_flags_column" 139 | include FlagShihTzu 140 | 141 | has_flags( 142 | { 1 => :warpdrive, 2 => :hyperspace }, 143 | column: "engines") 144 | has_flags( 145 | { 1 => :photon, 2 => :laser, 3 => :ion_cannon, 4 => :particle_beam }, 146 | column: "weapons") 147 | has_flags( 148 | { 1 => :power, 2 => :anti_ax_routine }, 149 | column: "hal3000") 150 | 151 | validates_presence_of_flags :engines, :weapons 152 | end 153 | 154 | class SpaceshipWithValidationsOnNonFlagsColumn < ActiveRecord::Base 155 | self.table_name = "spaceships_with_custom_flags_column" 156 | include FlagShihTzu 157 | 158 | has_flags(1 => :warpdrive, 2 => :hyperspace, column: "bits") 159 | validates_presence_of_flags :id 160 | end 161 | end 162 | 163 | # table planets is missing intentionally to see if 164 | # flag_shih_tzu handles missing tables gracefully 165 | class Planet < ActiveRecord::Base 166 | end 167 | 168 | class FlagShihTzuClassMethodsTest < Test::Unit::TestCase 169 | 170 | def setup 171 | Spaceship.destroy_all 172 | end 173 | 174 | def test_has_flags_should_raise_an_exception_when_flag_key_is_negative 175 | assert_raises ArgumentError do 176 | eval(<<-EOF 177 | class SpaceshipWithInvalidFlagKey < ActiveRecord::Base 178 | self.table_name = "spaceships" 179 | include FlagShihTzu 180 | 181 | has_flags({ -1 => :error }) 182 | end 183 | EOF 184 | ) 185 | end 186 | end 187 | 188 | def test_has_flags_should_raise_an_exception_when_flag_name_already_used 189 | assert_raises ArgumentError do 190 | eval(<<-EOF 191 | class SpaceshipWithAlreadyUsedFlag < ActiveRecord::Base 192 | self.table_name = "spaceships_with_2_custom_flags_column" 193 | include FlagShihTzu 194 | 195 | has_flags({ 1 => :jeanluckpicard }, column: "bits") 196 | has_flags({ 1 => :jeanluckpicard }, column: "commanders") 197 | end 198 | EOF 199 | ) 200 | end 201 | end 202 | 203 | def test_has_flags_should_raise_an_exception_when_desired_flag_name_method_already_defined 204 | assert_raises ArgumentError do 205 | eval(<<-EOF 206 | class SpaceshipWithAlreadyUsedMethod < ActiveRecord::Base 207 | self.table_name = "spaceships_with_2_custom_flags_column" 208 | include FlagShihTzu 209 | 210 | def jeanluckpicard; end 211 | 212 | has_flags({ 1 => :jeanluckpicard }, column: "bits") 213 | end 214 | EOF 215 | ) 216 | end 217 | end 218 | 219 | def test_has_flags_should_raise_an_exception_when_flag_name_method_defined_by_flagshitzu_if_strict 220 | assert_raises FlagShihTzu::DuplicateFlagColumnException do 221 | eval(<<-EOF 222 | class SpaceshipWithAlreadyUsedMethodByFlagshitzuStrict < ActiveRecord::Base 223 | self.table_name = "spaceships_with_2_custom_flags_column" 224 | include FlagShihTzu 225 | 226 | has_flags( 227 | { 1 => :jeanluckpicard }, 228 | column: "bits", 229 | strict: true 230 | ) 231 | has_flags( 232 | { 1 => :jeanluckpicard }, 233 | column: "bits", 234 | strict: true 235 | ) 236 | end 237 | EOF 238 | ) 239 | end 240 | end 241 | 242 | def test_has_flags_should_not_raise_an_exception_when_flag_name_method_defined_by_flagshitzu 243 | assert_nothing_raised ArgumentError do 244 | eval(<<-EOF 245 | class SpaceshipWithAlreadyUsedMethodByFlagshitzu < ActiveRecord::Base 246 | self.table_name = "spaceships_with_2_custom_flags_column" 247 | include FlagShihTzu 248 | 249 | has_flags({ 1 => :jeanluckpicard }, column: "bits") 250 | has_flags({ 1 => :jeanluckpicard }, column: "bits") 251 | end 252 | EOF 253 | ) 254 | end 255 | end 256 | 257 | def test_has_flags_should_raise_an_exception_when_flag_name_is_not_a_symbol 258 | assert_raises ArgumentError do 259 | eval(<<-EOF 260 | class SpaceshipWithInvalidFlagName < ActiveRecord::Base 261 | self.table_name = "spaceships" 262 | include FlagShihTzu 263 | 264 | has_flags({ 1 => "error" }) 265 | end 266 | EOF 267 | ) 268 | end 269 | end 270 | 271 | def test_should_define_a_sql_condition_method_for_flag_enabled 272 | assert_equal '("spaceships"."flags" in (1,3,5,7))', 273 | Spaceship.warpdrive_condition 274 | assert_equal '("spaceships"."flags" in (2,3,6,7))', 275 | Spaceship.shields_condition 276 | assert_equal '("spaceships"."flags" in (4,5,6,7))', 277 | Spaceship.electrolytes_condition 278 | end 279 | 280 | def test_should_define_a_sql_condition_method_for_flag_enabled_with_missing_flags 281 | assert_equal '("spaceships"."flags" in (1,3,5,7))', 282 | SpaceshipWithMissingFlags.warpdrive_condition 283 | assert_equal '("spaceships"."flags" in (4,5,6,7))', 284 | SpaceshipWithMissingFlags.electrolytes_condition 285 | end 286 | 287 | def test_should_accept_a_table_alias_option_for_sql_condition_method 288 | assert_equal '("old_spaceships"."flags" in (1,3,5,7))', 289 | Spaceship.warpdrive_condition(table_alias: "old_spaceships") 290 | end 291 | 292 | def test_should_define_a_sql_condition_method_for_flag_enabled_with_2_colmns 293 | assert_equal '("spaceships_with_2_custom_flags_column"."bits" in (1,3))', 294 | SpaceshipWith2CustomFlagsColumn.warpdrive_condition 295 | assert_equal '("spaceships_with_2_custom_flags_column"."bits" in (2,3))', 296 | SpaceshipWith2CustomFlagsColumn.hyperspace_condition 297 | assert_equal '("spaceships_with_2_custom_flags_column"."commanders" in (1,3))', 298 | SpaceshipWith2CustomFlagsColumn.jeanlucpicard_condition 299 | assert_equal '("spaceships_with_2_custom_flags_column"."commanders" in (2,3))', 300 | SpaceshipWith2CustomFlagsColumn.dajanatroj_condition 301 | end 302 | 303 | def test_should_define_a_sql_condition_method_for_flag_not_enabled 304 | assert_equal '("spaceships"."flags" not in (1,3,5,7))', 305 | Spaceship.not_warpdrive_condition 306 | assert_equal '("spaceships"."flags" not in (2,3,6,7))', 307 | Spaceship.not_shields_condition 308 | assert_equal '("spaceships"."flags" not in (4,5,6,7))', 309 | Spaceship.not_electrolytes_condition 310 | end 311 | 312 | def test_should_define_a_sql_condition_method_for_flag_not_enabled_with_missing_flags 313 | assert_equal '("spaceships"."flags" not in (1,3,5,7))', 314 | SpaceshipWithMissingFlags.not_warpdrive_condition 315 | assert_equal '("spaceships"."flags" not in (4,5,6,7))', 316 | SpaceshipWithMissingFlags.not_electrolytes_condition 317 | end 318 | 319 | def test_should_accept_a_table_alias_option_for_not_sql_condition_method 320 | assert_equal '("old_spaceships"."flags" not in (1,3,5,7))', 321 | Spaceship.not_warpdrive_condition(table_alias: "old_spaceships") 322 | end 323 | 324 | def test_sql_condition_for_flag_with_custom_table_name_and_default_query_mode 325 | assert_equal '("custom_spaceships"."flags" in (1,3,5,7))', 326 | Spaceship.send(:sql_condition_for_flag, 327 | :warpdrive, 328 | "flags", 329 | true, 330 | "custom_spaceships") 331 | end 332 | def test_sql_condition_for_flag_with_in_list_query_mode 333 | assert_equal '("spaceships"."flags" in (1,3))', 334 | SpaceshipWithInListQueryMode.send(:sql_condition_for_flag, 335 | :warpdrive, 336 | "flags", 337 | true, 338 | "spaceships") 339 | end 340 | def test_sql_condition_for_flag_with_bit_operator_query_mode 341 | assert_equal '("spaceships"."flags" & 1 = 1)', 342 | SpaceshipWithBitOperatorQueryMode.send(:sql_condition_for_flag, 343 | :warpdrive, 344 | "flags", 345 | true, 346 | "spaceships") 347 | end 348 | def test_sql_in_for_flag 349 | assert_equal [1, 3, 5, 7], 350 | Spaceship.send(:sql_in_for_flag, :warpdrive, "flags") 351 | end 352 | def test_sql_set_for_flag 353 | assert_equal '"flags" = "spaceships"."flags" | 1', 354 | Spaceship.send(:sql_set_for_flag, :warpdrive, "flags") 355 | end 356 | 357 | def test_should_define_a_sql_condition_method_for_flag_enabled_with_2_colmns_not_enabled 358 | assert_equal '("spaceships_with_2_custom_flags_column"."bits" not in (1,3))', 359 | SpaceshipWith2CustomFlagsColumn.not_warpdrive_condition 360 | assert_equal '("spaceships_with_2_custom_flags_column"."bits" not in (2,3))', 361 | SpaceshipWith2CustomFlagsColumn.not_hyperspace_condition 362 | assert_equal '("spaceships_with_2_custom_flags_column"."commanders" not in (1,3))', 363 | SpaceshipWith2CustomFlagsColumn.not_jeanlucpicard_condition 364 | assert_equal '("spaceships_with_2_custom_flags_column"."commanders" not in (2,3))', 365 | SpaceshipWith2CustomFlagsColumn.not_dajanatroj_condition 366 | end 367 | 368 | def test_should_define_a_sql_condition_method_for_flag_enabled_using_bit_operators 369 | assert_equal '("spaceships"."flags" & 1 = 1)', 370 | SpaceshipWithBitOperatorQueryMode.warpdrive_condition 371 | assert_equal '("spaceships"."flags" & 2 = 2)', 372 | SpaceshipWithBitOperatorQueryMode.shields_condition 373 | end 374 | 375 | def test_should_define_a_sql_condition_method_for_flag_not_enabled_using_bit_operators 376 | assert_equal '("spaceships"."flags" & 1 = 0)', 377 | SpaceshipWithBitOperatorQueryMode.not_warpdrive_condition 378 | assert_equal '("spaceships"."flags" & 2 = 0)', 379 | SpaceshipWithBitOperatorQueryMode.not_shields_condition 380 | end 381 | 382 | def test_should_define_a_named_scope_for_flag_enabled 383 | assert_where_value '("spaceships"."flags" in (1,3,5,7))', 384 | Spaceship.warpdrive 385 | assert_where_value '("spaceships"."flags" in (2,3,6,7))', 386 | Spaceship.shields 387 | assert_where_value '("spaceships"."flags" in (4,5,6,7))', 388 | Spaceship.electrolytes 389 | end 390 | 391 | def test_should_define_a_named_scope_for_flag_not_enabled 392 | assert_where_value '("spaceships"."flags" not in (1,3,5,7))', 393 | Spaceship.not_warpdrive 394 | assert_where_value '("spaceships"."flags" not in (2,3,6,7))', 395 | Spaceship.not_shields 396 | assert_where_value '("spaceships"."flags" not in (4,5,6,7))', 397 | Spaceship.not_electrolytes 398 | end 399 | 400 | def test_should_define_a_dynamic_column_value_helpers_for_flags 401 | assert_equal Spaceship.flag_values_for(:warpdrive), [1, 3, 5, 7] 402 | assert_equal Spaceship.flag_values_for(:warpdrive, :shields), [3, 7] 403 | end 404 | 405 | def test_should_define_a_named_scope_for_flag_enabled_with_2_columns 406 | assert_where_value '("spaceships_with_2_custom_flags_column"."bits" in (1,3))', 407 | SpaceshipWith2CustomFlagsColumn.warpdrive 408 | assert_where_value '("spaceships_with_2_custom_flags_column"."bits" in (2,3))', 409 | SpaceshipWith2CustomFlagsColumn.hyperspace 410 | assert_where_value '("spaceships_with_2_custom_flags_column"."commanders" in (1,3))', 411 | SpaceshipWith2CustomFlagsColumn.jeanlucpicard 412 | assert_where_value '("spaceships_with_2_custom_flags_column"."commanders" in (2,3))', 413 | SpaceshipWith2CustomFlagsColumn.dajanatroj 414 | end 415 | 416 | def test_should_define_a_named_scope_for_flag_not_enabled_with_2_columns 417 | assert_where_value '("spaceships_with_2_custom_flags_column"."bits" not in (1,3))', 418 | SpaceshipWith2CustomFlagsColumn.not_warpdrive 419 | assert_where_value '("spaceships_with_2_custom_flags_column"."bits" not in (2,3))', 420 | SpaceshipWith2CustomFlagsColumn.not_hyperspace 421 | assert_where_value '("spaceships_with_2_custom_flags_column"."commanders" not in (1,3))', 422 | SpaceshipWith2CustomFlagsColumn.not_jeanlucpicard 423 | assert_where_value '("spaceships_with_2_custom_flags_column"."commanders" not in (2,3))', 424 | SpaceshipWith2CustomFlagsColumn.not_dajanatroj 425 | end 426 | 427 | def test_should_define_a_named_scope_for_flag_enabled_using_bit_operators 428 | assert_where_value '("spaceships"."flags" & 1 = 1)', 429 | SpaceshipWithBitOperatorQueryMode.warpdrive 430 | assert_where_value '("spaceships"."flags" & 2 = 2)', 431 | SpaceshipWithBitOperatorQueryMode.shields 432 | end 433 | 434 | def test_should_define_a_named_scope_for_flag_not_enabled_using_bit_operators 435 | assert_where_value '("spaceships"."flags" & 1 = 0)', 436 | SpaceshipWithBitOperatorQueryMode.not_warpdrive 437 | assert_where_value '("spaceships"."flags" & 2 = 0)', 438 | SpaceshipWithBitOperatorQueryMode.not_shields 439 | end 440 | 441 | def test_should_work_with_raw_sql 442 | spaceship = Spaceship.new 443 | spaceship.enable_flag(:shields) 444 | spaceship.enable_flag(:electrolytes) 445 | spaceship.save! 446 | 447 | assert_equal false, spaceship.warpdrive 448 | assert_equal true, spaceship.shields 449 | assert_equal true, spaceship.electrolytes 450 | 451 | if (ActiveRecord::VERSION::MAJOR <= 3) 452 | Spaceship.update_all( 453 | Spaceship.set_flag_sql(:warpdrive, true), 454 | ["id=?", spaceship.id] 455 | ) 456 | else 457 | Spaceship.where("id=?", spaceship.id).update_all( 458 | Spaceship.set_flag_sql(:warpdrive, true) 459 | ) 460 | end 461 | 462 | spaceship.reload 463 | 464 | assert_equal true, spaceship.warpdrive 465 | assert_equal true, spaceship.shields 466 | assert_equal true, spaceship.electrolytes 467 | 468 | spaceship = Spaceship.new 469 | spaceship.enable_flag(:warpdrive) 470 | spaceship.enable_flag(:shields) 471 | spaceship.enable_flag(:electrolytes) 472 | spaceship.save! 473 | 474 | assert_equal true, spaceship.warpdrive 475 | assert_equal true, spaceship.shields 476 | assert_equal true, spaceship.electrolytes 477 | 478 | if (ActiveRecord::VERSION::MAJOR <= 3) 479 | Spaceship.update_all( 480 | Spaceship.set_flag_sql(:shields, false), 481 | ["id=?", spaceship.id] 482 | ) 483 | else 484 | Spaceship.where("id=?", spaceship.id).update_all( 485 | Spaceship.set_flag_sql(:shields, false) 486 | ) 487 | end 488 | 489 | spaceship.reload 490 | 491 | assert_equal true, spaceship.warpdrive 492 | assert_equal false, spaceship.shields 493 | assert_equal true, spaceship.electrolytes 494 | end 495 | 496 | def test_should_return_the_correct_number_of_items_from_a_named_scope 497 | spaceship = Spaceship.new 498 | spaceship.enable_flag(:warpdrive) 499 | spaceship.enable_flag(:shields) 500 | spaceship.save! 501 | spaceship.reload 502 | spaceship_2 = Spaceship.new 503 | spaceship_2.enable_flag(:warpdrive) 504 | spaceship_2.save! 505 | spaceship_2.reload 506 | spaceship_3 = Spaceship.new 507 | spaceship_3.enable_flag(:shields) 508 | spaceship_3.save! 509 | spaceship_3.reload 510 | assert_equal 1, Spaceship.not_warpdrive.count 511 | assert_equal 2, Spaceship.warpdrive.count 512 | assert_equal 1, Spaceship.not_shields.count 513 | assert_equal 2, Spaceship.shields.count 514 | assert_equal 1, Spaceship.warpdrive.shields.count 515 | assert_equal 0, Spaceship.not_warpdrive.not_shields.count 516 | end 517 | 518 | def test_should_return_the_correct_condition_with_chained_flags 519 | assert_equal '("spaceships"."flags" in (3,7))', 520 | Spaceship.chained_flags_condition("flags", 521 | :warpdrive, 522 | :shields) 523 | assert_equal '("spaceships"."flags" in (7))', 524 | Spaceship.chained_flags_condition("flags", 525 | :warpdrive, 526 | :shields, 527 | :electrolytes) 528 | assert_equal '("spaceships"."flags" in (2,6))', 529 | Spaceship.chained_flags_condition("flags", 530 | :not_warpdrive, 531 | :shields) 532 | end 533 | 534 | def test_should_return_the_correct_number_of_items_with_chained_flags_with 535 | spaceship = Spaceship.new 536 | spaceship.enable_flag(:warpdrive) 537 | spaceship.enable_flag(:shields) 538 | spaceship.save! 539 | spaceship.reload 540 | spaceship_2 = Spaceship.new 541 | spaceship_2.enable_flag(:warpdrive) 542 | spaceship_2.save! 543 | spaceship_2.reload 544 | spaceship_3 = Spaceship.new 545 | spaceship_3.enable_flag(:shields) 546 | spaceship_3.save! 547 | spaceship_3.reload 548 | spaceship_4 = Spaceship.new 549 | spaceship_4.save! 550 | spaceship_4.reload 551 | assert_equal 2, Spaceship.chained_flags_with("flags", 552 | :warpdrive).count 553 | assert_equal 1, Spaceship.chained_flags_with("flags", 554 | :warpdrive, 555 | :shields).count 556 | assert_equal 1, Spaceship.chained_flags_with("flags", 557 | :warpdrive, 558 | :not_shields).count 559 | assert_equal 0, Spaceship.chained_flags_with("flags", 560 | :not_warpdrive, 561 | :shields, 562 | :electrolytes).count 563 | assert_equal 1, Spaceship.chained_flags_with("flags", 564 | :not_warpdrive, 565 | :shields, 566 | :not_electrolytes).count 567 | assert_equal 1, Spaceship.chained_flags_with("flags", 568 | :not_warpdrive, 569 | :not_shields, 570 | :not_electrolytes).count 571 | end 572 | 573 | def test_should_not_define_named_scopes_if_not_wanted 574 | assert !SpaceshipWithoutNamedScopes.respond_to?(:warpdrive) 575 | assert !SpaceshipWithoutNamedScopesOldStyle.respond_to?(:warpdrive) 576 | end 577 | 578 | def test_should_work_with_a_custom_flags_column 579 | spaceship = SpaceshipWithCustomFlagsColumn.new 580 | spaceship.enable_flag(:warpdrive) 581 | spaceship.enable_flag(:hyperspace) 582 | spaceship.save! 583 | spaceship.reload 584 | assert_equal 3, spaceship.flags("bits") 585 | assert_equal '("spaceships_with_custom_flags_column"."bits" in (1,3))', 586 | SpaceshipWithCustomFlagsColumn.warpdrive_condition 587 | assert_equal '("spaceships_with_custom_flags_column"."bits" not in (1,3))', 588 | SpaceshipWithCustomFlagsColumn.not_warpdrive_condition 589 | assert_equal '("spaceships_with_custom_flags_column"."bits" in (2,3))', 590 | SpaceshipWithCustomFlagsColumn.hyperspace_condition 591 | assert_equal '("spaceships_with_custom_flags_column"."bits" not in (2,3))', 592 | SpaceshipWithCustomFlagsColumn.not_hyperspace_condition 593 | assert_where_value '("spaceships_with_custom_flags_column"."bits" in (1,3))', 594 | SpaceshipWithCustomFlagsColumn.warpdrive 595 | assert_where_value '("spaceships_with_custom_flags_column"."bits" not in (1,3))', 596 | SpaceshipWithCustomFlagsColumn.not_warpdrive 597 | assert_where_value '("spaceships_with_custom_flags_column"."bits" in (2,3))', 598 | SpaceshipWithCustomFlagsColumn.hyperspace 599 | assert_where_value '("spaceships_with_custom_flags_column"."bits" not in (2,3))', 600 | SpaceshipWithCustomFlagsColumn.not_hyperspace 601 | end 602 | 603 | def test_should_work_with_a_custom_flags_column_name_as_symbol 604 | spaceship = SpaceshipWithColumnNameAsSymbol.new 605 | spaceship.enable_flag(:warpdrive) 606 | spaceship.save! 607 | spaceship.reload 608 | assert_equal 1, spaceship.flags("bits") 609 | end 610 | 611 | def test_should_not_error_out_when_table_is_not_present 612 | assert_nothing_raised(ActiveRecord::StatementInvalid) do 613 | Planet.class_eval do 614 | include FlagShihTzu 615 | has_flags(1 => :habitable) 616 | end 617 | end 618 | end 619 | 620 | def test_should_not_error_out_when_column_is_not_present 621 | assert_nothing_raised(ActiveRecord::StatementInvalid) do 622 | Planet.class_eval do 623 | # Now it has a table that exists, but the column does not. 624 | self.table_name = "spaceships" 625 | include FlagShihTzu 626 | 627 | has_flags({ 1 => :warpdrive, 2 => :hyperspace }, 628 | column: :i_do_not_exist, 629 | check_for_column: true) 630 | end 631 | end 632 | end 633 | 634 | private 635 | 636 | def assert_where_value(expected, scope) 637 | actual = scope.where_clause.ast.expr 638 | 639 | assert_equal expected, actual 640 | end 641 | 642 | end 643 | 644 | class FlagShihTzuInstanceMethodsTest < Test::Unit::TestCase 645 | 646 | def setup 647 | @spaceship = Spaceship.new 648 | @big_spaceship = SpaceshipWith2CustomFlagsColumn.new 649 | @small_spaceship = SpaceshipWithCustomFlagsColumn.new 650 | end 651 | 652 | def test_should_enable_flag 653 | @spaceship.enable_flag(:warpdrive) 654 | assert @spaceship.flag_enabled?(:warpdrive) 655 | end 656 | 657 | def test_should_enable_flag_with_2_columns 658 | @big_spaceship.enable_flag(:warpdrive) 659 | assert @big_spaceship.flag_enabled?(:warpdrive) 660 | @big_spaceship.enable_flag(:jeanlucpicard) 661 | assert @big_spaceship.flag_enabled?(:jeanlucpicard) 662 | end 663 | 664 | def test_should_disable_flag 665 | @spaceship.enable_flag(:warpdrive) 666 | assert @spaceship.flag_enabled?(:warpdrive) 667 | 668 | @spaceship.disable_flag(:warpdrive) 669 | assert @spaceship.flag_disabled?(:warpdrive) 670 | end 671 | 672 | def test_should_disable_flag_with_2_columns 673 | @big_spaceship.enable_flag(:warpdrive) 674 | assert @big_spaceship.flag_enabled?(:warpdrive) 675 | @big_spaceship.enable_flag(:jeanlucpicard) 676 | assert @big_spaceship.flag_enabled?(:jeanlucpicard) 677 | 678 | @big_spaceship.disable_flag(:warpdrive) 679 | assert @big_spaceship.flag_disabled?(:warpdrive) 680 | @big_spaceship.disable_flag(:jeanlucpicard) 681 | assert @big_spaceship.flag_disabled?(:jeanlucpicard) 682 | end 683 | 684 | def test_should_store_the_flags_correctly 685 | @spaceship.enable_flag(:warpdrive) 686 | @spaceship.disable_flag(:shields) 687 | @spaceship.enable_flag(:electrolytes) 688 | 689 | @spaceship.save! 690 | @spaceship.reload 691 | 692 | assert_equal 5, @spaceship.flags 693 | assert @spaceship.flag_enabled?(:warpdrive) 694 | assert !@spaceship.flag_enabled?(:shields) 695 | assert @spaceship.flag_enabled?(:electrolytes) 696 | end 697 | 698 | def test_should_store_the_flags_correctly_wiht_2_colmns 699 | @big_spaceship.enable_flag(:warpdrive) 700 | @big_spaceship.disable_flag(:hyperspace) 701 | @big_spaceship.enable_flag(:dajanatroj) 702 | 703 | @big_spaceship.save! 704 | @big_spaceship.reload 705 | 706 | assert_equal 1, @big_spaceship.flags("bits") 707 | assert_equal 2, @big_spaceship.flags("commanders") 708 | 709 | assert @big_spaceship.flag_enabled?(:warpdrive) 710 | assert !@big_spaceship.flag_enabled?(:hyperspace) 711 | assert @big_spaceship.flag_enabled?(:dajanatroj) 712 | end 713 | 714 | def test_enable_flag_should_leave_the_flag_enabled_when_called_twice 715 | 2.times do 716 | @spaceship.enable_flag(:warpdrive) 717 | assert @spaceship.flag_enabled?(:warpdrive) 718 | end 719 | end 720 | 721 | def test_disable_flag_should_leave_the_flag_disabled_when_called_twice 722 | 2.times do 723 | @spaceship.disable_flag(:warpdrive) 724 | assert !@spaceship.flag_enabled?(:warpdrive) 725 | end 726 | end 727 | 728 | def test_should_define_an_attribute_reader_method 729 | assert_equal false, @spaceship.warpdrive 730 | end 731 | 732 | def test_should_define_a_negative_attribute_reader_method 733 | assert_equal true, @spaceship.not_warpdrive 734 | end 735 | 736 | # -------------------------------------------------- 737 | 738 | def test_should_define_an_all_flags_reader_method_with_arity_1 739 | assert_array_similarity [:electrolytes, :warpdrive, :shields], 740 | @spaceship.all_flags("flags") 741 | end 742 | 743 | def test_should_define_an_all_flags_reader_method_with_arity_0 744 | assert_array_similarity [:electrolytes, :warpdrive, :shields], 745 | @spaceship.all_flags 746 | end 747 | 748 | def test_should_define_a_selected_flags_reader_method_with_arity_1 749 | assert_array_similarity [], @spaceship.selected_flags("flags") 750 | 751 | @spaceship.warpdrive = true 752 | assert_array_similarity [:warpdrive], 753 | @spaceship.selected_flags("flags") 754 | 755 | @spaceship.electrolytes = true 756 | assert_array_similarity [:electrolytes, :warpdrive], 757 | @spaceship.selected_flags("flags") 758 | 759 | @spaceship.warpdrive = false 760 | @spaceship.electrolytes = false 761 | assert_array_similarity [], @spaceship.selected_flags("flags") 762 | end 763 | 764 | def test_should_define_a_selected_flags_reader_method_with_arity_0 765 | assert_array_similarity [], @spaceship.selected_flags 766 | 767 | @spaceship.warpdrive = true 768 | assert_array_similarity [:warpdrive], @spaceship.selected_flags 769 | 770 | @spaceship.electrolytes = true 771 | assert_array_similarity [:electrolytes, :warpdrive], 772 | @spaceship.selected_flags 773 | 774 | @spaceship.warpdrive = false 775 | @spaceship.electrolytes = false 776 | assert_array_similarity [], @spaceship.selected_flags 777 | end 778 | 779 | def test_should_define_a_select_all_flags_method_with_arity_1 780 | @spaceship.select_all_flags("flags") 781 | assert @spaceship.warpdrive 782 | assert @spaceship.shields 783 | assert @spaceship.electrolytes 784 | end 785 | 786 | def test_should_define_a_select_all_flags_method_with_arity_0 787 | @spaceship.select_all_flags 788 | assert @spaceship.warpdrive 789 | assert @spaceship.shields 790 | assert @spaceship.electrolytes 791 | end 792 | 793 | def test_should_define_an_unselect_all_flags_method_with_arity_1 794 | @spaceship.warpdrive = true 795 | @spaceship.shields = true 796 | @spaceship.electrolytes = true 797 | 798 | @spaceship.unselect_all_flags("flags") 799 | 800 | assert !@spaceship.warpdrive 801 | assert !@spaceship.shields 802 | assert !@spaceship.electrolytes 803 | end 804 | 805 | def test_should_define_an_unselect_all_flags_method_with_arity_0 806 | @spaceship.warpdrive = true 807 | @spaceship.shields = true 808 | @spaceship.electrolytes = true 809 | 810 | @spaceship.unselect_all_flags 811 | 812 | assert !@spaceship.warpdrive 813 | assert !@spaceship.shields 814 | assert !@spaceship.electrolytes 815 | end 816 | 817 | def test_should_define_an_has_flag_method_with_arity_1 818 | assert !@spaceship.has_flag?("flags") 819 | 820 | @spaceship.warpdrive = true 821 | assert @spaceship.has_flag?("flags") 822 | 823 | @spaceship.shields = true 824 | assert @spaceship.has_flag?("flags") 825 | 826 | @spaceship.electrolytes = true 827 | assert @spaceship.has_flag?("flags") 828 | 829 | @spaceship.unselect_all_flags("flags") 830 | assert !@spaceship.has_flag?("flags") 831 | end 832 | 833 | def test_should_define_an_has_flag_method_with_arity_0 834 | assert !@spaceship.has_flag? 835 | 836 | @spaceship.warpdrive = true 837 | assert @spaceship.has_flag? 838 | 839 | @spaceship.shields = true 840 | assert @spaceship.has_flag? 841 | 842 | @spaceship.electrolytes = true 843 | assert @spaceship.has_flag? 844 | 845 | @spaceship.unselect_all_flags 846 | assert !@spaceship.has_flag? 847 | end 848 | 849 | def test_should_define_a_selected_flags_writer_method 850 | @spaceship.selected_flags = [:warpdrive] 851 | assert @spaceship.warpdrive 852 | assert !@spaceship.shields 853 | assert !@spaceship.electrolytes 854 | 855 | @spaceship.selected_flags = [:warpdrive, :shields, :electrolytes] 856 | assert @spaceship.warpdrive 857 | assert @spaceship.shields 858 | assert @spaceship.electrolytes 859 | 860 | @spaceship.selected_flags = [] 861 | assert !@spaceship.warpdrive 862 | assert !@spaceship.shields 863 | assert !@spaceship.electrolytes 864 | 865 | @spaceship.selected_flags = [:warpdrive, :shields, :electrolytes] 866 | @spaceship.selected_flags = nil 867 | assert !@spaceship.warpdrive 868 | assert !@spaceship.shields 869 | assert !@spaceship.electrolytes 870 | end 871 | 872 | # -------------------------------------------------- 873 | 874 | def test_should_define_a_customized_all_flags_reader_method 875 | assert_array_similarity [:hyperspace, :warpdrive], 876 | @small_spaceship.all_bits 877 | end 878 | 879 | def test_should_define_a_customized_selected_flags_reader_method 880 | assert_array_similarity [], @small_spaceship.selected_bits 881 | 882 | @small_spaceship.warpdrive = true 883 | assert_array_similarity [:warpdrive], 884 | @small_spaceship.selected_bits 885 | 886 | @small_spaceship.hyperspace = true 887 | assert_array_similarity [:hyperspace, :warpdrive], 888 | @small_spaceship.selected_bits 889 | 890 | @small_spaceship.warpdrive = false 891 | @small_spaceship.hyperspace = false 892 | assert_array_similarity [], @small_spaceship.selected_bits 893 | end 894 | 895 | def test_should_define_a_customized_select_all_flags_method 896 | @small_spaceship.select_all_bits 897 | assert @small_spaceship.warpdrive 898 | assert @small_spaceship.hyperspace 899 | end 900 | 901 | def test_should_define_a_customized_unselect_all_flags_method 902 | @small_spaceship.warpdrive = true 903 | @small_spaceship.hyperspace = true 904 | 905 | @small_spaceship.unselect_all_bits 906 | 907 | assert !@small_spaceship.warpdrive 908 | assert !@small_spaceship.hyperspace 909 | end 910 | 911 | def test_should_define_a_customized_selected_flags_writer_method 912 | @small_spaceship.selected_bits = [:warpdrive] 913 | assert @small_spaceship.warpdrive 914 | assert !@small_spaceship.hyperspace 915 | 916 | @small_spaceship.selected_bits = [:hyperspace] 917 | assert !@small_spaceship.warpdrive 918 | assert @small_spaceship.hyperspace 919 | 920 | @small_spaceship.selected_bits = [:hyperspace, :warpdrive] 921 | assert @small_spaceship.warpdrive 922 | assert @small_spaceship.hyperspace 923 | 924 | @small_spaceship.selected_bits = [] 925 | assert !@small_spaceship.warpdrive 926 | assert !@small_spaceship.hyperspace 927 | 928 | @small_spaceship.selected_bits = nil 929 | assert !@small_spaceship.warpdrive 930 | assert !@small_spaceship.hyperspace 931 | end 932 | 933 | def test_should_define_a_customized_has_flag_method 934 | assert !@small_spaceship.has_bit? 935 | 936 | @small_spaceship.warpdrive = true 937 | assert @small_spaceship.has_bit? 938 | 939 | @small_spaceship.hyperspace = true 940 | assert @small_spaceship.has_bit? 941 | 942 | @small_spaceship.unselect_all_bits 943 | assert !@small_spaceship.has_bit? 944 | end 945 | 946 | # -------------------------------------------------- 947 | 948 | def test_should_define_a_customized_all_flags_reader_method_with_2_columns 949 | assert_array_similarity [:hyperspace, :warpdrive], 950 | @big_spaceship.all_bits 951 | assert_array_similarity [:dajanatroj, :jeanlucpicard], 952 | @big_spaceship.all_commanders 953 | end 954 | 955 | def test_should_define_a_customized_selected_flags_reader_method_with_2_columns 956 | assert_array_similarity [], 957 | @big_spaceship.selected_bits 958 | assert_array_similarity [], 959 | @big_spaceship.selected_commanders 960 | 961 | @big_spaceship.warpdrive = true 962 | @big_spaceship.jeanlucpicard = true 963 | assert_array_similarity [:warpdrive], 964 | @big_spaceship.selected_bits 965 | assert_array_similarity [:jeanlucpicard], 966 | @big_spaceship.selected_commanders 967 | 968 | @big_spaceship.hyperspace = true 969 | @big_spaceship.hyperspace = true 970 | @big_spaceship.jeanlucpicard = true 971 | @big_spaceship.dajanatroj = true 972 | assert_array_similarity [:hyperspace, :warpdrive], 973 | @big_spaceship.selected_bits 974 | assert_array_similarity [:dajanatroj, :jeanlucpicard], 975 | @big_spaceship.selected_commanders 976 | 977 | @big_spaceship.warpdrive = false 978 | @big_spaceship.hyperspace = false 979 | @big_spaceship.jeanlucpicard = false 980 | @big_spaceship.dajanatroj = false 981 | assert_array_similarity [], @big_spaceship.selected_bits 982 | assert_array_similarity [], @big_spaceship.selected_commanders 983 | end 984 | 985 | def test_should_define_a_customized_select_all_flags_method_with_2_columns 986 | @big_spaceship.select_all_bits 987 | @big_spaceship.select_all_commanders 988 | assert @big_spaceship.warpdrive 989 | assert @big_spaceship.hyperspace 990 | assert @big_spaceship.jeanlucpicard 991 | assert @big_spaceship.dajanatroj 992 | end 993 | 994 | def test_should_define_a_customized_unselect_all_flags_method_with_2_columns 995 | @big_spaceship.warpdrive = true 996 | @big_spaceship.hyperspace = true 997 | @big_spaceship.jeanlucpicard = true 998 | @big_spaceship.dajanatroj = true 999 | 1000 | @big_spaceship.unselect_all_bits 1001 | @big_spaceship.unselect_all_commanders 1002 | 1003 | assert !@big_spaceship.warpdrive 1004 | assert !@big_spaceship.hyperspace 1005 | assert !@big_spaceship.jeanlucpicard 1006 | assert !@big_spaceship.dajanatroj 1007 | end 1008 | 1009 | def test_should_define_a_customized_selected_flags_writer_method_with_2_columns 1010 | @big_spaceship.selected_bits = [:warpdrive] 1011 | @big_spaceship.selected_commanders = [:jeanlucpicard] 1012 | assert @big_spaceship.warpdrive 1013 | assert !@big_spaceship.hyperspace 1014 | assert @big_spaceship.jeanlucpicard 1015 | assert !@big_spaceship.dajanatroj 1016 | 1017 | @big_spaceship.selected_bits = [:hyperspace] 1018 | @big_spaceship.selected_commanders = [:dajanatroj] 1019 | assert !@big_spaceship.warpdrive 1020 | assert @big_spaceship.hyperspace 1021 | assert !@big_spaceship.jeanlucpicard 1022 | assert @big_spaceship.dajanatroj 1023 | 1024 | @big_spaceship.selected_bits = [:hyperspace, :warpdrive] 1025 | @big_spaceship.selected_commanders = [:dajanatroj, :jeanlucpicard] 1026 | assert @big_spaceship.warpdrive 1027 | assert @big_spaceship.hyperspace 1028 | assert @big_spaceship.jeanlucpicard 1029 | assert @big_spaceship.dajanatroj 1030 | 1031 | @big_spaceship.selected_bits = [] 1032 | @big_spaceship.selected_commanders = [] 1033 | assert !@big_spaceship.warpdrive 1034 | assert !@big_spaceship.hyperspace 1035 | assert !@big_spaceship.jeanlucpicard 1036 | assert !@big_spaceship.dajanatroj 1037 | 1038 | @big_spaceship.selected_bits = nil 1039 | @big_spaceship.selected_commanders = nil 1040 | assert !@big_spaceship.warpdrive 1041 | assert !@big_spaceship.hyperspace 1042 | assert !@big_spaceship.jeanlucpicard 1043 | assert !@big_spaceship.dajanatroj 1044 | end 1045 | 1046 | def test_should_define_a_customized_has_flag_method_with_2_columns 1047 | assert !@big_spaceship.has_bit? 1048 | assert !@big_spaceship.has_commander? 1049 | 1050 | @big_spaceship.warpdrive = true 1051 | @big_spaceship.jeanlucpicard = true 1052 | assert @big_spaceship.has_bit? 1053 | assert @big_spaceship.has_commander? 1054 | 1055 | @big_spaceship.hyperspace = true 1056 | @big_spaceship.dajanatroj = true 1057 | assert @big_spaceship.has_bit? 1058 | 1059 | @big_spaceship.unselect_all_bits 1060 | @big_spaceship.unselect_all_commanders 1061 | assert !@big_spaceship.has_bit? 1062 | end 1063 | 1064 | # -------------------------------------------------- 1065 | 1066 | def test_should_define_an_attribute_reader_predicate_method 1067 | assert_equal false, @spaceship.warpdrive? 1068 | end 1069 | 1070 | def test_should_define_a_negative_attribute_reader_predicate_method 1071 | assert_equal true, @spaceship.not_warpdrive? 1072 | end 1073 | 1074 | def test_should_define_an_attribute_writer_method 1075 | @spaceship.warpdrive = true 1076 | assert @spaceship.warpdrive 1077 | end 1078 | 1079 | def test_should_define_a_negative_attribute_writer_method 1080 | @spaceship.not_warpdrive = false 1081 | assert @spaceship.warpdrive 1082 | end 1083 | 1084 | def test_should_define_dirty_suffix_changed? 1085 | assert !@spaceship.warpdrive_changed? 1086 | assert !@spaceship.shields_changed? 1087 | 1088 | @spaceship.enable_flag(:warpdrive) 1089 | assert @spaceship.warpdrive_changed? 1090 | assert !@spaceship.shields_changed? 1091 | 1092 | @spaceship.enable_flag(:shields) 1093 | assert @spaceship.warpdrive_changed? 1094 | assert @spaceship.shields_changed? 1095 | 1096 | @spaceship.disable_flag(:warpdrive) 1097 | assert !@spaceship.warpdrive_changed? 1098 | assert @spaceship.shields_changed? 1099 | 1100 | @spaceship.disable_flag(:shields) 1101 | assert !@spaceship.warpdrive_changed? 1102 | assert !@spaceship.shields_changed? 1103 | end 1104 | 1105 | def test_should_respect_true_values_like_active_record 1106 | [true, 1, "1", "t", "T", "true", "TRUE"].each do |true_value| 1107 | @spaceship.warpdrive = true_value 1108 | assert @spaceship.warpdrive 1109 | end 1110 | 1111 | [false, 0, "0", "f", "F", "false", "FALSE"].each do |false_value| 1112 | @spaceship.warpdrive = false_value 1113 | assert !@spaceship.warpdrive 1114 | end 1115 | end 1116 | 1117 | def test_should_ignore_has_flags_call_if_column_does_not_exist_yet_default_check_for_column 1118 | assert_nothing_raised do 1119 | eval(<<-EOF 1120 | class SpaceshipWithoutFlagsColumn1 < ActiveRecord::Base 1121 | self.table_name = "spaceships_without_flags_column" 1122 | include FlagShihTzu 1123 | 1124 | has_flags 1 => :warpdrive, 1125 | 2 => :shields, 1126 | 3 => :electrolytes 1127 | end 1128 | EOF 1129 | ) 1130 | end 1131 | 1132 | assert !SpaceshipWithoutFlagsColumn1.method_defined?(:warpdrive) 1133 | end 1134 | 1135 | def test_should_ignore_has_flags_call_if_column_not_integer_default_check_for_column 1136 | assert_raises FlagShihTzu::IncorrectFlagColumnException do 1137 | eval(<<-EOF 1138 | class SpaceshipWithNonIntegerColumn1 < ActiveRecord::Base 1139 | self.table_name ="spaceships_with_non_integer_column" 1140 | include FlagShihTzu 1141 | 1142 | has_flags 1 => :warpdrive, 1143 | 2 => :shields, 1144 | 3 => :electrolytes 1145 | end 1146 | EOF 1147 | ) 1148 | end 1149 | 1150 | assert !SpaceshipWithNonIntegerColumn1.method_defined?(:warpdrive) 1151 | end 1152 | 1153 | def test_should_ignore_has_flags_call_if_column_does_not_exist_yet_and_check_for_column_true 1154 | assert_nothing_raised do 1155 | eval(<<-EOF 1156 | class SpaceshipWithoutFlagsColumn2 < ActiveRecord::Base 1157 | self.table_name = "spaceships_without_flags_column" 1158 | include FlagShihTzu 1159 | 1160 | has_flags 1 => :warpdrive, 1161 | 2 => :shields, 1162 | 3 => :electrolytes, 1163 | check_for_column: true 1164 | end 1165 | EOF 1166 | ) 1167 | end 1168 | assert !SpaceshipWithoutFlagsColumn2.send(:check_flag_column, "flags") 1169 | assert !SpaceshipWithoutFlagsColumn2.method_defined?(:warpdrive) 1170 | end 1171 | 1172 | def test_should_ignore_has_flags_call_if_column_not_integer_and_check_for_column_true 1173 | assert_raises FlagShihTzu::IncorrectFlagColumnException do 1174 | eval(<<-EOF 1175 | class SpaceshipWithNonIntegerColumn2 < ActiveRecord::Base 1176 | self.table_name ="spaceships_with_non_integer_column" 1177 | include FlagShihTzu 1178 | 1179 | has_flags 1 => :warpdrive, 1180 | 2 => :shields, 1181 | 3 => :electrolytes, 1182 | check_for_column: true 1183 | end 1184 | EOF 1185 | ) 1186 | end 1187 | 1188 | assert !SpaceshipWithNonIntegerColumn2.method_defined?(:warpdrive) 1189 | end 1190 | 1191 | def test_should_ignore_has_flags_call_if_column_does_not_exist_yet_and_check_for_column_false 1192 | assert_nothing_raised do 1193 | eval(<<-EOF 1194 | class SpaceshipWithoutFlagsColumn3 < ActiveRecord::Base 1195 | self.table_name = "spaceships_without_flags_column" 1196 | include FlagShihTzu 1197 | 1198 | has_flags 1 => :warpdrive, 1199 | 2 => :shields, 1200 | 3 => :electrolytes, 1201 | check_for_column: false 1202 | end 1203 | EOF 1204 | ) 1205 | end 1206 | 1207 | assert SpaceshipWithoutFlagsColumn3.method_defined?(:warpdrive) 1208 | end 1209 | 1210 | def test_should_ignore_has_flags_call_if_column_not_integer_and_check_for_column_false 1211 | assert_nothing_raised do 1212 | eval(<<-EOF 1213 | class SpaceshipWithNonIntegerColumn3 < ActiveRecord::Base 1214 | self.table_name ="spaceships_with_non_integer_column" 1215 | include FlagShihTzu 1216 | 1217 | has_flags 1 => :warpdrive, 1218 | 2 => :shields, 1219 | 3 => :electrolytes, 1220 | check_for_column: false 1221 | end 1222 | EOF 1223 | ) 1224 | end 1225 | 1226 | assert SpaceshipWithNonIntegerColumn3.method_defined?(:warpdrive) 1227 | end 1228 | 1229 | if ActiveRecord::VERSION::STRING >= "4.1." 1230 | def test_should_ignore_database_missing_errors 1231 | assert_nothing_raised do 1232 | eval(<<-EOF 1233 | class SpaceshipWithoutDatabaseConnection < ActiveRecord::Base 1234 | def self.connection 1235 | raise ActiveRecord::NoDatabaseError.new("Unknown database") 1236 | end 1237 | self.table_name ="spaceships" 1238 | include FlagShihTzu 1239 | 1240 | has_flags 1 => :warpdrive, 1241 | 2 => :shields, 1242 | 3 => :electrolytes 1243 | end 1244 | EOF 1245 | ) 1246 | end 1247 | assert SpaceshipWithoutDatabaseConnection.method_defined?(:warpdrive) 1248 | end 1249 | end 1250 | 1251 | def test_shouldnt_establish_a_connection_if_check_for_column_is_false 1252 | assert_nothing_raised do 1253 | eval(<<-EOF 1254 | class SpaceshipWithoutColumnCheck < ActiveRecord::Base 1255 | cattr_accessor :connection_established 1256 | def self.connection 1257 | self.connection_established = true 1258 | super 1259 | end 1260 | self.table_name ="spaceships" 1261 | include FlagShihTzu 1262 | 1263 | has_flags({ 1264 | 1 => :warpdrive, 1265 | 2 => :shields, 1266 | 3 => :electrolytes 1267 | }, check_for_column: false) 1268 | end 1269 | EOF 1270 | ) 1271 | end 1272 | assert SpaceshipWithoutColumnCheck.method_defined?(:warpdrive) 1273 | assert !SpaceshipWithoutColumnCheck.connection_established 1274 | end 1275 | 1276 | def test_column_guessing_for_default_column_2 1277 | assert_equal "flags", 1278 | @spaceship.class.determine_flag_colmn_for(:warpdrive) 1279 | end 1280 | 1281 | def test_column_guessing_for_default_column_1 1282 | assert_raises FlagShihTzu::NoSuchFlagException do 1283 | @spaceship.class.determine_flag_colmn_for(:xxx) 1284 | end 1285 | end 1286 | 1287 | def test_column_guessing_for_2_columns 1288 | assert_equal "commanders", 1289 | @big_spaceship.class.determine_flag_colmn_for(:jeanlucpicard) 1290 | assert_equal "bits", 1291 | @big_spaceship.class.determine_flag_colmn_for(:warpdrive) 1292 | end 1293 | 1294 | def test_update_flag_without_updating_instance! 1295 | my_spaceship = SpaceshipWith2CustomFlagsColumn.new 1296 | my_spaceship.enable_flag(:jeanlucpicard) 1297 | my_spaceship.disable_flag(:warpdrive) 1298 | my_spaceship.save 1299 | 1300 | assert_equal true, my_spaceship.jeanlucpicard 1301 | assert_equal false, my_spaceship.warpdrive 1302 | 1303 | assert_equal true, my_spaceship.update_flag!(:jeanlucpicard, false) 1304 | assert_equal true, my_spaceship.update_flag!(:warpdrive, true) 1305 | 1306 | # Not updating the instance here, 1307 | # so it won't reflect the result of the SQL update until after reloaded 1308 | assert_equal true, my_spaceship.jeanlucpicard 1309 | assert_equal false, my_spaceship.warpdrive 1310 | 1311 | my_spaceship.reload 1312 | 1313 | assert_equal false, my_spaceship.jeanlucpicard 1314 | assert_equal true, my_spaceship.warpdrive 1315 | end 1316 | 1317 | def test_update_flag_with_updating_instance! 1318 | my_spaceship = SpaceshipWith2CustomFlagsColumn.new 1319 | my_spaceship.enable_flag(:jeanlucpicard) 1320 | my_spaceship.disable_flag(:warpdrive) 1321 | my_spaceship.save 1322 | 1323 | assert_equal true, my_spaceship.jeanlucpicard 1324 | assert_equal false, my_spaceship.warpdrive 1325 | 1326 | assert_equal true, my_spaceship.update_flag!(:jeanlucpicard, false, true) 1327 | assert_equal true, my_spaceship.update_flag!(:warpdrive, true, true) 1328 | 1329 | # Updating the instance here, 1330 | # so it will reflect the result of the SQL update before and after reload 1331 | assert_equal false, my_spaceship.jeanlucpicard 1332 | assert_equal true, my_spaceship.warpdrive 1333 | 1334 | my_spaceship.reload 1335 | 1336 | assert_equal false, my_spaceship.jeanlucpicard 1337 | assert_equal true, my_spaceship.warpdrive 1338 | end 1339 | 1340 | # -------------------------------------------------- 1341 | 1342 | if (ActiveRecord::VERSION::MAJOR >= 3) 1343 | 1344 | def test_validation_should_raise_if_not_a_flag_column 1345 | spaceship = SpaceshipWithValidationsOnNonFlagsColumn.new 1346 | assert_raises ArgumentError do 1347 | spaceship.valid? 1348 | end 1349 | end 1350 | 1351 | def test_validation_should_succeed_with_a_blank_optional_flag 1352 | spaceship = Spaceship.new 1353 | assert_equal true, spaceship.valid? 1354 | end 1355 | 1356 | def test_validation_should_fail_with_a_nil_required_flag 1357 | spaceship = SpaceshipWithValidationsAndCustomFlagsColumn.new 1358 | spaceship.bits = nil 1359 | assert_equal false, spaceship.valid? 1360 | error_message = 1361 | if spaceship.errors.respond_to?(:messages) 1362 | spaceship.errors.messages[:bits] 1363 | else 1364 | spaceship.errors.get(:bits) 1365 | end 1366 | assert_equal ["can't be blank"], error_message 1367 | end 1368 | 1369 | def test_validation_should_fail_with_a_blank_required_flag 1370 | spaceship = SpaceshipWithValidationsAndCustomFlagsColumn.new 1371 | assert_equal false, spaceship.valid? 1372 | error_message = 1373 | if spaceship.errors.respond_to?(:messages) 1374 | spaceship.errors.messages[:bits] 1375 | else 1376 | spaceship.errors.get(:bits) 1377 | end 1378 | assert_equal ["can't be blank"], error_message 1379 | end 1380 | 1381 | def test_validation_should_succeed_with_a_set_required_flag 1382 | spaceship = SpaceshipWithValidationsAndCustomFlagsColumn.new 1383 | spaceship.warpdrive = true 1384 | assert_equal true, spaceship.valid? 1385 | end 1386 | 1387 | def test_validation_should_fail_with_a_blank_required_flag_among_2 1388 | spaceship = SpaceshipWithValidationsAnd3CustomFlagsColumn.new 1389 | assert_equal false, spaceship.valid? 1390 | engines_error_message = 1391 | if spaceship.errors.respond_to?(:messages) 1392 | spaceship.errors.messages[:engines] 1393 | else 1394 | spaceship.errors.get(:engines) 1395 | end 1396 | assert_equal ["can't be blank"], engines_error_message 1397 | 1398 | weapons_error_message = 1399 | if spaceship.errors.respond_to?(:messages) 1400 | spaceship.errors.messages[:weapons] 1401 | else 1402 | spaceship.errors.get(:weapons) 1403 | end 1404 | assert_equal ["can't be blank"], weapons_error_message 1405 | 1406 | spaceship.warpdrive = true 1407 | assert_equal false, spaceship.valid? 1408 | 1409 | weapons_error_message = 1410 | if spaceship.errors.respond_to?(:messages) 1411 | spaceship.errors.messages[:weapons] 1412 | else 1413 | spaceship.errors.get(:weapons) 1414 | end 1415 | assert_equal ["can't be blank"], weapons_error_message 1416 | end 1417 | 1418 | def test_validation_should_succeed_with_a_set_required_flag_among_2 1419 | spaceship = SpaceshipWithValidationsAnd3CustomFlagsColumn.new 1420 | spaceship.warpdrive = true 1421 | spaceship.photon = true 1422 | assert_equal true, spaceship.valid? 1423 | end 1424 | end 1425 | 1426 | end 1427 | 1428 | class FlagShihTzuDerivedClassTest < Test::Unit::TestCase 1429 | 1430 | def setup 1431 | @spaceship = SpaceCarrier.new 1432 | end 1433 | 1434 | def test_should_enable_flag 1435 | @spaceship.enable_flag(:warpdrive) 1436 | assert @spaceship.flag_enabled?(:warpdrive) 1437 | end 1438 | 1439 | def test_should_disable_flag 1440 | @spaceship.enable_flag(:warpdrive) 1441 | assert @spaceship.flag_enabled?(:warpdrive) 1442 | 1443 | @spaceship.disable_flag(:warpdrive) 1444 | assert @spaceship.flag_disabled?(:warpdrive) 1445 | end 1446 | 1447 | def test_should_store_the_flags_correctly 1448 | @spaceship.enable_flag(:warpdrive) 1449 | @spaceship.disable_flag(:shields) 1450 | @spaceship.enable_flag(:electrolytes) 1451 | 1452 | @spaceship.save! 1453 | @spaceship.reload 1454 | 1455 | assert_equal 5, @spaceship.flags 1456 | assert @spaceship.flag_enabled?(:warpdrive) 1457 | assert !@spaceship.flag_enabled?(:shields) 1458 | assert @spaceship.flag_enabled?(:electrolytes) 1459 | end 1460 | 1461 | def test_enable_flag_should_leave_the_flag_enabled_when_called_twice 1462 | 2.times do 1463 | @spaceship.enable_flag(:warpdrive) 1464 | assert @spaceship.flag_enabled?(:warpdrive) 1465 | end 1466 | end 1467 | 1468 | def test_disable_flag_should_leave_the_flag_disabled_when_called_twice 1469 | 2.times do 1470 | @spaceship.disable_flag(:warpdrive) 1471 | assert !@spaceship.flag_enabled?(:warpdrive) 1472 | end 1473 | end 1474 | 1475 | def test_should_define_an_attribute_reader_method 1476 | assert_equal false, @spaceship.warpdrive? 1477 | end 1478 | 1479 | def test_should_define_an_attribute_writer_method 1480 | @spaceship.warpdrive = true 1481 | assert @spaceship.warpdrive 1482 | end 1483 | 1484 | def test_should_respect_true_values_like_active_record 1485 | [true, 1, "1", "t", "T", "true", "TRUE"].each do |true_value| 1486 | @spaceship.warpdrive = true_value 1487 | assert @spaceship.warpdrive 1488 | end 1489 | 1490 | [false, 0, "0", "f", "F", "false", "FALSE"].each do |false_value| 1491 | @spaceship.warpdrive = false_value 1492 | assert !@spaceship.warpdrive 1493 | end 1494 | end 1495 | 1496 | def test_should_define_bang_methods 1497 | spaceship = SpaceshipWithBangMethods.new 1498 | spaceship.warpdrive! 1499 | assert spaceship.warpdrive 1500 | spaceship.not_warpdrive! 1501 | assert !spaceship.warpdrive 1502 | end 1503 | 1504 | def test_should_return_a_sql_set_method_for_flag 1505 | assert_equal '"flags" = "spaceships"."flags" | 1', 1506 | Spaceship.send(:sql_set_for_flag, :warpdrive, "flags", true) 1507 | assert_equal '"flags" = "spaceships"."flags" & ~1', 1508 | Spaceship.send(:sql_set_for_flag, :warpdrive, "flags", false) 1509 | end 1510 | 1511 | end 1512 | 1513 | class FlagShihTzuClassMethodsTest < Test::Unit::TestCase 1514 | 1515 | def test_should_track_columns_used_by_FlagShihTzu 1516 | assert_equal Spaceship.flag_columns, ["flags"] 1517 | assert_equal SpaceshipWith2CustomFlagsColumn.flag_columns, 1518 | ["bits", "commanders"] 1519 | assert_equal SpaceshipWith3CustomFlagsColumn.flag_columns, 1520 | ["engines", "weapons", "hal3000"] 1521 | if (ActiveRecord::VERSION::MAJOR >= 3) 1522 | assert_equal SpaceshipWithValidationsAnd3CustomFlagsColumn.flag_columns, 1523 | ["engines", "weapons", "hal3000"] 1524 | assert_equal SpaceshipWithSymbolAndStringFlagColumns.flag_columns, 1525 | ["peace", "love", "happiness"] 1526 | end 1527 | end 1528 | 1529 | end 1530 | --------------------------------------------------------------------------------