├── .github └── workflows │ ├── ci-on-merge.yml │ └── ci.yml ├── .gitignore ├── CHANGELOG.md ├── Gemfile ├── Gemfile.lock ├── LICENSE.txt ├── README.md ├── Rakefile ├── array_enum.gemspec ├── bin ├── console └── setup ├── lib ├── array_enum.rb └── array_enum │ ├── railtie.rb │ ├── subset_validator.rb │ └── version.rb └── test ├── array_enum_test.rb ├── subset_validator_test.rb └── test_helper.rb /.github/workflows/ci-on-merge.yml: -------------------------------------------------------------------------------- 1 | name: BUILD 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | test: 10 | runs-on: ubuntu-latest 11 | strategy: 12 | matrix: 13 | ruby-version: [3.0, 3.1, 3.3] 14 | 15 | services: 16 | postgres: 17 | image: postgres 18 | ports: 19 | - 5432:5432 20 | env: 21 | POSTGRES_PASSWORD: postgres 22 | options: >- 23 | --health-cmd pg_isready 24 | --health-interval 10s 25 | --health-timeout 5s 26 | --health-retries 5 27 | 28 | steps: 29 | - name: Install libraries 30 | run: | 31 | sudo apt-get install -y libpq-dev postgresql-client 32 | 33 | - name: Configure database 34 | env: 35 | PGPASSWORD: postgres 36 | PGUSER: postgres 37 | PGHOST: localhost 38 | run: | 39 | echo "Configuring PostgresSQL" 40 | psql -c 'create database "array_enum_test";' 41 | 42 | - name: Checkout code 43 | uses: actions/checkout@v4.1.1 44 | 45 | - name : Ruby Setup 46 | uses: ruby/setup-ruby@v1.173.0 47 | with: 48 | ruby-version: ${{ matrix.ruby-version }} 49 | bundler-cache: true 50 | 51 | - name: Install dependencies 52 | run: bundle install 53 | 54 | - name: Run test 55 | env: 56 | PGPASSWORD: postgres 57 | PGUSER: postgres 58 | PGHOST: localhost 59 | run: bundle exec rake -s test 60 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | - pull_request 5 | - push 6 | 7 | jobs: 8 | test: 9 | runs-on: ubuntu-latest 10 | strategy: 11 | matrix: 12 | ruby-version: [3.0, 3.1, 3.3] 13 | 14 | services: 15 | postgres: 16 | image: postgres 17 | ports: 18 | - 5432:5432 19 | env: 20 | POSTGRES_PASSWORD: postgres 21 | options: >- 22 | --health-cmd pg_isready 23 | --health-interval 10s 24 | --health-timeout 5s 25 | --health-retries 5 26 | 27 | steps: 28 | - name: Install libraries 29 | run: | 30 | sudo apt-get install -y libpq-dev postgresql-client 31 | 32 | - name: Configure database 33 | env: 34 | PGPASSWORD: postgres 35 | PGUSER: postgres 36 | PGHOST: localhost 37 | run: | 38 | echo "Configuring PostgresSQL" 39 | psql -c 'create database "array_enum_test";' 40 | 41 | - name: Checkout code 42 | uses: actions/checkout@v4.1.1 43 | 44 | - name : Ruby Setup 45 | uses: ruby/setup-ruby@v1.173.0 46 | with: 47 | ruby-version: ${{ matrix.ruby-version }} 48 | bundler-cache: true 49 | 50 | - name: Install dependencies 51 | run: bundle install 52 | 53 | - name: Run test 54 | env: 55 | PGPASSWORD: postgres 56 | PGUSER: postgres 57 | PGHOST: localhost 58 | run: bundle exec rake -s test 59 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /_yardoc/ 4 | /coverage/ 5 | /doc/ 6 | /pkg/ 7 | /spec/reports/ 8 | /tmp/ 9 | .tool-versions 10 | .byebug_history 11 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 1.6.0 2 | ---------- 3 | - Bump Rails related dependencies to 7.2 (#23) 4 | - Cast appropriate type when generating where query. (#22) 5 | 6 | 1.5.0 7 | ---------- 8 | - Fix the PG::AmbiguousColumn error when we do a join and filter on 2 tables that has the same array_enum column name. (#19) 9 | - Drop Ruby 2.7 support (#19) 10 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in array_enum.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | array_enum (1.6.0) 5 | activemodel 6 | 7 | GEM 8 | remote: https://rubygems.org/ 9 | specs: 10 | activemodel (7.2.0) 11 | activesupport (= 7.2.0) 12 | activerecord (7.2.0) 13 | activemodel (= 7.2.0) 14 | activesupport (= 7.2.0) 15 | timeout (>= 0.4.0) 16 | activesupport (7.2.0) 17 | base64 18 | bigdecimal 19 | concurrent-ruby (~> 1.0, >= 1.3.1) 20 | connection_pool (>= 2.2.5) 21 | drb 22 | i18n (>= 1.6, < 2) 23 | logger (>= 1.4.2) 24 | minitest (>= 5.1) 25 | securerandom (>= 0.3) 26 | tzinfo (~> 2.0, >= 2.0.5) 27 | base64 (0.2.0) 28 | bigdecimal (3.1.8) 29 | concurrent-ruby (1.3.4) 30 | connection_pool (2.4.1) 31 | drb (2.2.1) 32 | i18n (1.14.5) 33 | concurrent-ruby (~> 1.0) 34 | logger (1.6.0) 35 | minitest (5.25.1) 36 | pg (1.5.6) 37 | rake (13.2.0) 38 | securerandom (0.3.1) 39 | timeout (0.4.1) 40 | tzinfo (2.0.6) 41 | concurrent-ruby (~> 1.0) 42 | 43 | PLATFORMS 44 | ruby 45 | 46 | DEPENDENCIES 47 | activerecord 48 | array_enum! 49 | bundler (>= 1.17) 50 | minitest (>= 5.0) 51 | pg 52 | rake (>= 10.0) 53 | 54 | BUNDLED WITH 55 | 2.5.6 56 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019 Wojciech Wnętrzak 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Gem Version](https://badge.fury.io/rb/array_enum.svg)](https://badge.fury.io/rb/array_enum) 2 | [![BUILD](https://github.com/freeletics/array_enum/actions/workflows/ci-on-merge.yml/badge.svg)](https://github.com/freeletics/array_enum/actions/workflows/ci-on-merge.yml) 3 | 4 | 5 | # ArrayEnum 6 | 7 | Extension for `ActiveRecord` that adds support for `PostgreSQL` array columns, mapping string values to integers. 8 | 9 | ## Installation 10 | 11 | `gem install array_enum` or use `Gemfile` with bundler 12 | 13 | ## Usage 14 | 15 | ### ActiveRecord extension 16 | 17 | Database will store integers that after reading will map to string values. 18 | 19 | ```ruby 20 | ActiveRecord::Schema.define do 21 | create_table :users, force: true do |t| 22 | t.integer :favourite_colors, array: true, null: false, default: [] 23 | end 24 | end 25 | 26 | class User < ActiveRecord::Base 27 | extend ArrayEnum 28 | 29 | array_enum favourite_colors: {"red" => 1, "blue" => 2, "green" => 3} 30 | end 31 | 32 | user = User.create!(favourite_colors: ["red", "green"]) 33 | user.favourite_colors # => ["red", "green"] 34 | User.favourite_colors # => {"red" => 1, "blue" => 2, "green" => 3} 35 | ``` 36 | 37 | Several scopes are made available on your model to find records based on a value or an array of values: 38 | 39 | ```ruby 40 | user1 = User.create!(favourite_colors: ["red", "green"]) 41 | user2 = User.create!(favourite_colors: ["red"]) 42 | 43 | # Find a record that has _all_ the provided values in the array enum attribute 44 | User.with_favourite_colors("red") # => [user1, user2] 45 | User.with_favourite_colors(%w[red green]) # => [user1] 46 | User.with_favourite_colors(%w[red blue]) # => [] 47 | User.with_favourite_colors(%w[green blue]) # => [] 48 | 49 | # Find a record that has the provided values, and _only those values_, in the array enum attribute 50 | User.only_with_favourite_colors("red") # => [user2] 51 | User.only_with_favourite_colors(%w[red green]) # => [user1] 52 | User.only_with_favourite_colors(%w[red blue]) # => [] 53 | User.only_with_favourite_colors(%w[green blue]) # => [] 54 | 55 | # Find a record that has _at least one_ of the provided values in the array enum attribute 56 | User.with_any_of_favourite_colors("red") # => [user1, user2] 57 | User.with_any_of_favourite_colors(%w[red green]) # => [user1, user2] 58 | User.with_any_of_favourite_colors(%w[red blue]) # => [user1, user2] 59 | User.with_any_of_favourite_colors(%w[green blue]) # => [user1] 60 | ``` 61 | 62 | Attempting to find a record with a value that is not in the enum will fail: 63 | 64 | ```ruby 65 | User.with_favourite_colors("yellow") # => ArgumentError["yellow is not a valid value for favourite_colors"] 66 | ``` 67 | 68 | ### Subset Validator 69 | 70 | Additionally `subset` validator is provided that can help to ensure correct values are passed during validation. 71 | 72 | ```ruby 73 | class CreateUser 74 | include ActiveModel::Model 75 | 76 | attr_accessor :favourite_colors 77 | 78 | validates :favourite_colors, subset: ["green", "blue"] 79 | # or: 80 | # validates :favourite_colors, subset: { in: ->(record) { Color.pluck(:name) } } 81 | end 82 | 83 | CreateUser.new(favourite_colors: ["black"]).valid? # => false 84 | ``` 85 | 86 | ## Development 87 | 88 | After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment. 89 | 90 | To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org). 91 | 92 | ## Contributing 93 | 94 | Bug reports and pull requests are welcome on GitHub at https://github.com/freeletics/array_enum. 95 | 96 | ## License 97 | 98 | The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT). 99 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "rake/testtask" 3 | 4 | Rake::TestTask.new(:test) do |t| 5 | t.libs << "test" 6 | t.libs << "lib" 7 | t.test_files = FileList["test/**/*_test.rb"] 8 | end 9 | 10 | task :default => :test 11 | -------------------------------------------------------------------------------- /array_enum.gemspec: -------------------------------------------------------------------------------- 1 | lib = File.expand_path('lib', __dir__) 2 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 3 | require 'array_enum/version' 4 | 5 | Gem::Specification.new do |spec| 6 | spec.name = 'array_enum' 7 | spec.version = ArrayEnum::VERSION 8 | spec.authors = ['Wojciech Wnętrzak'] 9 | spec.email = ['w.wnetrzak@gmail.com', 'eng@freeletics.com'] 10 | 11 | spec.summary = 'String to integer mapping for PostgreSQL array columns.' 12 | spec.description = 'Extension for ActiveRecord that adds support for PostgreSQL array columns, mapping string values to integers.' 13 | spec.homepage = 'https://github.com/freeletics/array_enum' 14 | spec.license = 'MIT' 15 | 16 | # Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host' 17 | # to allow pushing to a single host or delete this section to allow pushing to any host. 18 | if spec.respond_to?(:metadata) 19 | spec.metadata['homepage_uri'] = spec.homepage 20 | spec.metadata['source_code_uri'] = 'https://github.com/freeletics/array_enum' 21 | else 22 | raise 'RubyGems 2.0 or newer is required to protect against ' \ 23 | 'public gem pushes.' 24 | end 25 | 26 | # Specify which files should be added to the gem when it is released. 27 | # The `git ls-files -z` loads the files in the RubyGem that have been added into git. 28 | spec.files = Dir.chdir(File.expand_path(__dir__)) do 29 | `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } 30 | end 31 | spec.bindir = 'exe' 32 | spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } 33 | spec.require_paths = ['lib'] 34 | 35 | spec.required_ruby_version = '>= 3.0' 36 | 37 | spec.add_dependency 'activemodel' 38 | 39 | spec.add_development_dependency 'activerecord' 40 | spec.add_development_dependency 'bundler', '>= 1.17' 41 | spec.add_development_dependency 'minitest', '>= 5.0' 42 | spec.add_development_dependency 'pg' 43 | spec.add_development_dependency 'rake', '>= 10.0' 44 | end 45 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "bundler/setup" 4 | require "array_enum" 5 | 6 | # You can add fixtures and/or initialization code here to make experimenting 7 | # with your gem easier. You can also use a different console, if you like. 8 | 9 | # (If you use this, don't forget to add pry to your Gemfile!) 10 | # require "pry" 11 | # Pry.start 12 | 13 | require "irb" 14 | IRB.start(__FILE__) 15 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle install 7 | 8 | # Do any other automated setup that you need to do here 9 | -------------------------------------------------------------------------------- /lib/array_enum.rb: -------------------------------------------------------------------------------- 1 | require 'array_enum/version' 2 | require 'array_enum/subset_validator' 3 | require 'array_enum/railtie' if defined?(Rails::Railtie) 4 | require 'active_support/hash_with_indifferent_access' 5 | require 'active_support/core_ext/string/inflections' 6 | 7 | module ArrayEnum 8 | MISSING_VALUE_MESSAGE = '%s is not a valid value for %s'.freeze 9 | private_constant :MISSING_VALUE_MESSAGE 10 | 11 | def array_enum(definitions) 12 | definitions.each do |attr_name, mapping| 13 | attr_symbol = attr_name.to_sym 14 | mapping_hash = ActiveSupport::HashWithIndifferentAccess.new(mapping) 15 | 16 | define_singleton_method(attr_name.to_s.pluralize) do 17 | mapping_hash 18 | end 19 | 20 | { 21 | "with_#{attr_name}" => '@>', 22 | "only_with_#{attr_name}" => '=', 23 | "with_any_of_#{attr_name}" => '&&' 24 | }.each do |method_name, comparison_operator| 25 | define_singleton_method(method_name.to_sym) do |values| 26 | db_values = Array(values).map do |value| 27 | mapping_hash[value] || raise(ArgumentError, format(MISSING_VALUE_MESSAGE, value: value, attr: attr_name)) 28 | end 29 | where("#{table_name}.#{attr_name} #{comparison_operator} ARRAY[:db_values]::integer[]", db_values: db_values) 30 | end 31 | end 32 | 33 | define_method(attr_symbol) do 34 | Array(self[attr_symbol]).map { |value| mapping_hash.key(value) } 35 | end 36 | 37 | define_method("#{attr_name}=".to_sym) do |values| 38 | self[attr_symbol] = Array(values).map do |value| 39 | mapping_hash[value] || raise(ArgumentError, format(MISSING_VALUE_MESSAGE, value: value, attr: attr_name)) 40 | end.uniq 41 | end 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /lib/array_enum/railtie.rb: -------------------------------------------------------------------------------- 1 | require "rails/railtie" 2 | 3 | class ArrayEnum::Railtie < Rails::Railtie 4 | initializer "array_enum.subset_validator" do 5 | # ActiveModel expects top level class name for validator to have syntax "validates subset: []" 6 | ::SubsetValidator = ArrayEnum::SubsetValidator unless defined?(::SubsetValidator) 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /lib/array_enum/subset_validator.rb: -------------------------------------------------------------------------------- 1 | require "active_model/validator" 2 | 3 | class ArrayEnum::SubsetValidator < ActiveModel::EachValidator 4 | def validate_each(record, attribute, value) 5 | wrapped_value = [value].flatten # Handles nil value 6 | subset = delimiter.respond_to?(:call) ? delimiter.call(record) : delimiter 7 | 8 | diff = wrapped_value.reject { |element| subset.include?(element) } 9 | 10 | unless diff.empty? 11 | record.errors.add(attribute, :inclusion, **options.except(:in, :within).merge!(value: diff)) 12 | end 13 | end 14 | 15 | private 16 | 17 | def delimiter 18 | @delimiter ||= options[:in] || options[:within] 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/array_enum/version.rb: -------------------------------------------------------------------------------- 1 | module ArrayEnum 2 | VERSION = '1.6.0' 3 | end 4 | -------------------------------------------------------------------------------- /test/array_enum_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class ArrayEnumTest < Minitest::Test 4 | def setup 5 | User.delete_all 6 | end 7 | 8 | def test_assigning_enum_values 9 | user = User.new(favourite_colors: %w[red blue]) 10 | assert_equal %w[red blue], user.favourite_colors 11 | end 12 | 13 | def test_assigning_enum_values_as_symbols 14 | user = User.new(favourite_colors: %i[red blue]) 15 | assert_equal %w[red blue], user.favourite_colors 16 | end 17 | 18 | def test_raising_error_on_unknown_value 19 | error = assert_raises(ArgumentError) do 20 | User.new(favourite_colors: ['black']) 21 | end 22 | assert_match(/black is not a valid value for favourite_colors/, error.message) 23 | end 24 | 25 | def test_storing_values_as_integers 26 | user = User.create!(favourite_colors: ['red']) 27 | user.reload 28 | assert_equal '{1}', user.read_attribute_before_type_cast('favourite_colors') 29 | end 30 | 31 | # with_attr scope 32 | def test_quering_db_with_single_matching_value 33 | user = User.create!(favourite_colors: ['red']) 34 | assert_equal [user], User.with_favourite_colors('red') 35 | end 36 | 37 | def test_quering_db_with_single_matching_symbol_value 38 | user = User.create!(favourite_colors: ['red']) 39 | assert_equal [user], User.with_favourite_colors(:red) 40 | end 41 | 42 | def test_quering_db_by_one_of_matching_value 43 | user = User.create!(favourite_colors: %w[red blue]) 44 | assert_equal [user], User.with_favourite_colors('red') 45 | end 46 | 47 | def test_quering_db_by_excluded_value_does_not_return_record 48 | User.create!(favourite_colors: %w[red blue]) 49 | assert_equal [], User.with_favourite_colors('green') 50 | end 51 | 52 | def test_quering_db_by_many_values_does_not_return_record_on_excluded_value 53 | User.create!(favourite_colors: %w[red blue]) 54 | assert_equal [], User.with_favourite_colors(%w[red green]) 55 | end 56 | 57 | def test_quering_db_only_with_single_matching_value 58 | user = User.create!(favourite_colors: ['red']) 59 | assert_equal [user], User.only_with_favourite_colors(['red']) 60 | end 61 | 62 | def test_quering_db_only_with_single_matching_value_from_many_values_does_not_return_record 63 | User.create!(favourite_colors: %w[red blue]) 64 | assert_equal [], User.only_with_favourite_colors(['red']) 65 | end 66 | 67 | def test_quering_db_by_non_existing_value_raises_error 68 | User.create!(favourite_colors: %w[red blue]) 69 | error = assert_raises(ArgumentError) do 70 | User.with_favourite_colors('black') 71 | end 72 | assert_match(/black is not a valid value for favourite_colors/, error.message) 73 | end 74 | 75 | # with_any_of_attr scope 76 | def test_with_any_of_attr_matching_value 77 | user = User.create!(favourite_colors: ['red']) 78 | assert_equal [user], User.with_any_of_favourite_colors('red') 79 | end 80 | 81 | def test_with_any_of_attr_matching_symbol_value 82 | user = User.create!(favourite_colors: ['red']) 83 | assert_equal [user], User.with_any_of_favourite_colors(:red) 84 | end 85 | 86 | def test_with_any_of_attr_one_of_matching_value 87 | user = User.create!(favourite_colors: %w[red blue]) 88 | assert_equal [user], User.with_any_of_favourite_colors('red') 89 | end 90 | 91 | def test_with_any_of_attr_excluded_value_does_not_return_record 92 | User.create!(favourite_colors: %w[red blue]) 93 | assert_equal [], User.with_any_of_favourite_colors('green') 94 | end 95 | 96 | def test_with_any_of_attr_many_value_when_single_match 97 | user = User.create!(favourite_colors: %w[red blue]) 98 | assert_equal [user], User.with_any_of_favourite_colors(%w[red green]) 99 | end 100 | 101 | def test_with_any_of_attr_by_non_existing_value_raises_error 102 | User.create!(favourite_colors: %w[red blue]) 103 | error = assert_raises(ArgumentError) do 104 | User.with_any_of_favourite_colors('black') 105 | end 106 | assert_match(/black is not a valid value for favourite_colors/, error.message) 107 | end 108 | 109 | def test_lists_values 110 | assert_equal User.favourite_colors, { 'red' => 1, 'blue' => 2, 'green' => 3 } 111 | end 112 | 113 | def test_values_can_be_accessed_indifferently 114 | assert_equal User.favourite_colors[:red], 1 115 | assert_equal User.favourite_colors[:blue], 2 116 | assert_equal User.favourite_colors[:green], 3 117 | assert_equal User.favourite_colors['red'], 1 118 | end 119 | 120 | def test_user_with_join_table_which_has_same_array_enum_col 121 | user = User.create!(favourite_colors: %w[red blue], 122 | profiles: [Profile.new(slug: 'profile_slug')]) 123 | 124 | assert_equal [user], User.joins(:profiles).with_any_of_favourite_colors(%w[red]) 125 | end 126 | end 127 | -------------------------------------------------------------------------------- /test/subset_validator_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class SubsetValidatorTest < Minitest::Test 4 | class CreateUser 5 | include ActiveModel::Model 6 | 7 | attr_accessor :favourite_colors 8 | 9 | validates :favourite_colors, subset: ["green", "blue"] 10 | end 11 | 12 | class CreateUserWithProc 13 | include ActiveModel::Model 14 | 15 | attr_accessor :favourite_colors 16 | 17 | validates :favourite_colors, subset: { in: ->(record) { ["green", "blue"] } } 18 | end 19 | 20 | def test_valid_with_matching_values 21 | create_user = CreateUser.new(favourite_colors: ["green"]) 22 | assert create_user.valid? 23 | end 24 | 25 | def test_valid_with_matching_proc_values 26 | create_user = CreateUserWithProc.new(favourite_colors: ["green"]) 27 | assert create_user.valid? 28 | end 29 | 30 | def test_invalid_without_value 31 | create_user = CreateUser.new 32 | assert create_user.invalid? 33 | end 34 | 35 | def test_invalid_without_proc_value 36 | create_user = CreateUserWithProc.new 37 | assert create_user.invalid? 38 | end 39 | 40 | def test_valid_without_array 41 | create_user = CreateUser.new(favourite_colors: "green") 42 | assert create_user.valid? 43 | end 44 | 45 | def test_valid_without_proc_array 46 | create_user = CreateUserWithProc.new(favourite_colors: "green") 47 | assert create_user.valid? 48 | end 49 | 50 | def test_invalid_when_no_matching_value 51 | create_user = CreateUser.new(favourite_colors: ["black"]) 52 | assert create_user.invalid? 53 | end 54 | 55 | def test_invalid_when_no_matching_proc_value 56 | create_user = CreateUserWithProc.new(favourite_colors: ["black"]) 57 | assert create_user.invalid? 58 | end 59 | 60 | def test_error_details_when_invalid 61 | create_user = CreateUser.new(favourite_colors: ["black"]) 62 | create_user.validate 63 | expected_error = {favourite_colors: [{error: :inclusion, value: ["black"]}]} 64 | assert_equal expected_error, create_user.errors.details 65 | end 66 | 67 | def test_error_details_when_proc_invalid 68 | create_user = CreateUserWithProc.new(favourite_colors: ["black"]) 69 | create_user.validate 70 | expected_error = {favourite_colors: [{error: :inclusion, value: ["black"]}]} 71 | assert_equal expected_error, create_user.errors.details 72 | end 73 | 74 | def test_error_message_when_invalid 75 | create_user = CreateUser.new(favourite_colors: ["black"]) 76 | create_user.validate 77 | expected_error = {favourite_colors: ["is not included in the list"]} 78 | assert_equal expected_error, create_user.errors.messages 79 | end 80 | 81 | def test_error_message_when_proc_invalid 82 | create_user = CreateUserWithProc.new(favourite_colors: ["black"]) 83 | create_user.validate 84 | expected_error = {favourite_colors: ["is not included in the list"]} 85 | assert_equal expected_error, create_user.errors.messages 86 | end 87 | end 88 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift File.expand_path('../lib', __dir__) 2 | require 'active_record' 3 | require 'array_enum' 4 | require 'minitest/autorun' 5 | 6 | ActiveRecord::Base.establish_connection( 7 | adapter: 'postgresql', 8 | host: 'localhost', 9 | port: '5432', 10 | username: 'postgres', 11 | database: 'array_enum_test' 12 | ) 13 | ActiveRecord::Base.connection.execute('SELECT 1') # catch db errors early 14 | 15 | ActiveRecord::Schema.define do 16 | self.verbose = false 17 | 18 | create_table :users, force: true do |t| 19 | t.integer :favourite_colors, array: true 20 | end 21 | end 22 | 23 | ActiveRecord::Schema.define do 24 | self.verbose = false 25 | 26 | create_table :profiles, force: true do |t| 27 | t.integer :favourite_colors, array: true 28 | t.string :slug 29 | t.integer :user_id 30 | end 31 | end 32 | 33 | class User < ActiveRecord::Base 34 | extend ArrayEnum 35 | has_many :profiles 36 | array_enum favourite_colors: { 'red' => 1, 'blue' => 2, 'green' => 3 } 37 | end 38 | 39 | class Profile < ActiveRecord::Base 40 | extend ArrayEnum 41 | 42 | array_enum favourite_colors: { 'red' => 1, 'blue' => 2, 'green' => 3 } 43 | end 44 | 45 | # Mimic constant assigment from railtie 46 | SubsetValidator = ArrayEnum::SubsetValidator 47 | --------------------------------------------------------------------------------