├── .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 | [](https://opensource.org/licenses/MIT) |
13 | | expert support | [](https://www.codementor.io/peterboling?utm_source=github&utm_medium=button&utm_term=peterboling&utm_campaign=github) |
14 | | download rank | [](https://rubygems.org/gems/flag_shih_tzu) [](https://rubygems.org/gems/flag_shih_tzu) |
15 | | version | [](http://badge.fury.io/rb/flag_shih_tzu) |
16 | | dependencies | [](https://depfu.com/github/pboling/flag_shih_tzu?project_id=2685) |
17 | | code quality | [](https://codeclimate.com/github/pboling/flag_shih_tzu) [](https://houndci.com) |
18 | | inline documenation | [](http://inch-ci.org/github/pboling/flag_shih_tzu) |
19 | | continuous integration | [](https://travis-ci.org/pboling/flag_shih_tzu) |
20 | | test coverage | [](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 | [](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), [](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 |
--------------------------------------------------------------------------------