├── .github └── workflows │ └── rspec.yml ├── .gitignore ├── .rspec ├── .travis.yml ├── Appraisals ├── Gemfile ├── Gemfile.lock ├── Guardfile ├── MIT-LICENSE ├── README.md ├── Rakefile ├── gemfiles ├── activerecord_4.2.gemfile ├── activerecord_4.2.gemfile.lock ├── activerecord_5.0.gemfile ├── activerecord_5.0.gemfile.lock ├── activerecord_5.1.gemfile ├── activerecord_5.1.gemfile.lock ├── activerecord_5.2.gemfile ├── activerecord_5.2.gemfile.lock ├── activerecord_6.0.gemfile ├── activerecord_6.0.gemfile.lock ├── activerecord_edge.gemfile └── activerecord_edge.gemfile.lock ├── has_array_of.gemspec ├── lib ├── has_array_of.rb └── has_array_of │ ├── associated_array.rb │ ├── associated_array │ └── relation.rb │ ├── associated_belongs.rb │ ├── builders.rb │ ├── railtie.rb │ └── version.rb └── spec ├── has_array_of ├── associated_array │ └── relation_spec.rb ├── associated_array_spec.rb ├── associated_belongs_spec.rb └── builders_spec.rb ├── spec_helper.rb └── support ├── contexts.rb ├── db.rb ├── matchers.rb └── with_model.rb /.github/workflows/rspec.yml: -------------------------------------------------------------------------------- 1 | name: RSpec 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - master 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | services: 13 | postgres: 14 | image: postgres:12.0 15 | env: 16 | POSTGRES_USER: runner 17 | POSTGRES_DB: has_array_of_test 18 | ports: 19 | # will assign a random free host port 20 | - 5432/tcp 21 | # needed because the postgres container does not provide a healthcheck 22 | options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 23 | strategy: 24 | matrix: 25 | ruby: 26 | - 2.4.x 27 | - 2.5.x 28 | - 2.6.x 29 | gemfile: 30 | - gemfiles/activerecord_4.2.gemfile 31 | - gemfiles/activerecord_5.0.gemfile 32 | - gemfiles/activerecord_5.1.gemfile 33 | - gemfiles/activerecord_5.2.gemfile 34 | - gemfiles/activerecord_6.0.gemfile 35 | - gemfiles/activerecord_edge.gemfile 36 | exclude: 37 | - ruby: 2.4.x 38 | gemfile: gemfiles/activerecord_6.0.gemfile 39 | - ruby: 2.4.x 40 | gemfile: gemfiles/activerecord_edge.gemfile 41 | env: 42 | BUNDLE_GEMFILE: ${{ format('{0}/{1}', github.workspace, matrix.gemfile) }} 43 | BUNDLE_PATH: ${{ format('{0}/vendor/bundle', github.workspace) }} 44 | steps: 45 | - name: Checkout 46 | uses: actions/checkout@v1 47 | - name: Setup ruby 48 | uses: actions/setup-ruby@v1 49 | with: 50 | ruby-version: ${{ matrix.ruby }} 51 | - name: Install PostgreSQL lib 52 | run: sudo apt-get install libpq-dev 53 | - name: Install Bundler 54 | run: command -v bundler || gem install bundler:1.17.3 55 | - name: Cache Bundler 56 | uses: actions/cache@v1 57 | id: cache-bundler 58 | with: 59 | path: vendor/bundle 60 | key: ${{ runner.os }}-gem-${{ matrix.ruby }}-${{ hashFiles(format('{0}/{1}.lock', github.workspace, matrix.gemfile)) }} 61 | restore-keys: | 62 | ${{ runner.os }}-gem-${{ matrix.ruby }}- 63 | - name: Install Bundler dependencies 64 | if: steps.cache-bundler.outputs.cache-hit != 'true' 65 | run: bundle install --path=$BUNDLE_PATH --deployment --jobs=4 66 | - name: Check Bunlder dependencies 67 | run: bundle check --path=$BUNDLE_PATH 68 | - name: RSpec 69 | run: bundle exec rspec 70 | env: 71 | POSTGRES_HOST: localhost 72 | POSTGRES_PORT: ${{ job.services.postgres.ports[5432] }} 73 | 74 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .bundle/ 2 | log/*.log 3 | pkg/ 4 | spec/dummy/db/*.sqlite3 5 | spec/dummy/db/*.sqlite3-journal 6 | spec/dummy/log/*.log 7 | spec/dummy/tmp/ 8 | spec/dummy/.sass-cache 9 | *.swp 10 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --require spec_helper 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | rvm: 3 | - 2.4 4 | - 2.5 5 | - 2.6 6 | 7 | gemfile: 8 | - gemfiles/activerecord_4.2.gemfile 9 | - gemfiles/activerecord_5.0.gemfile 10 | - gemfiles/activerecord_5.1.gemfile 11 | - gemfiles/activerecord_5.2.gemfile 12 | - gemfiles/activerecord_6.0.gemfile 13 | - gemfiles/activerecord_edge.gemfile 14 | 15 | matrix: 16 | exclude: 17 | - rvm: 2.4 18 | gemfile: gemfiles/activerecord_edge.gemfile 19 | - rvm: 2.4 20 | gemfile: gemfiles/activerecord_6.0.gemfile 21 | 22 | before_script: 23 | - "bundle exec rake db:setup PGUSER=postgres" 24 | 25 | services: 26 | - postgresql 27 | -------------------------------------------------------------------------------- /Appraisals: -------------------------------------------------------------------------------- 1 | appraise "activerecord-4.2" do 2 | gem 'activerecord', '~> 4.2.11.1' 3 | gem 'pg', '~> 0.15' 4 | end 5 | 6 | appraise "activerecord-5.0" do 7 | gem 'activerecord', '~> 5.0.7.2' 8 | gem 'pg', '>= 0.18', '< 2.0' 9 | end 10 | 11 | appraise "activerecord-5.1" do 12 | gem 'activerecord', '~> 5.1.7' 13 | gem 'pg', '>= 0.18', '< 2.0' 14 | end 15 | 16 | appraise "activerecord-5.2" do 17 | gem 'activerecord', '~> 5.2.3' 18 | gem 'pg', '>= 0.18', '< 2.0' 19 | end 20 | 21 | appraise "activerecord-6.0" do 22 | gem 'activerecord', '~> 6.0.1' 23 | gem 'pg', '>= 0.18', '< 2.0' 24 | end 25 | 26 | appraise "activerecord-edge" do 27 | gem "activerecord", git: "https://github.com/rails/rails" 28 | end 29 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | # Declare your gem's dependencies in has_array_of.gemspec. 4 | # Bundler will treat runtime dependencies like base dependencies, and 5 | # development dependencies will be added by default to the :development group. 6 | gemspec 7 | 8 | # Declare any dependencies that are still in development here instead of in 9 | # your gemspec. These might include edge Rails or gems from your path or 10 | # Git. Remember to move these dependencies to your gemspec before releasing 11 | # your gem to rubygems.org. 12 | 13 | # To use debugger 14 | # gem 'debugger' 15 | 16 | gem 'with_model', git: 'https://github.com/Casecommons/with_model' 17 | 18 | group :local_development do 19 | gem 'pry' 20 | gem 'pry-byebug' 21 | end 22 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GIT 2 | remote: https://github.com/Casecommons/with_model 3 | revision: 0309b7689223e7a4e8be236161b66f27227987db 4 | specs: 5 | with_model (2.1.2) 6 | activerecord (>= 4.2, < 6.1) 7 | 8 | PATH 9 | remote: . 10 | specs: 11 | has_array_of (0.0.1) 12 | activerecord (>= 4.2) 13 | pg 14 | railties (>= 4.2) 15 | 16 | GEM 17 | remote: https://rubygems.org/ 18 | specs: 19 | actionpack (6.0.1) 20 | actionview (= 6.0.1) 21 | activesupport (= 6.0.1) 22 | rack (~> 2.0) 23 | rack-test (>= 0.6.3) 24 | rails-dom-testing (~> 2.0) 25 | rails-html-sanitizer (~> 1.0, >= 1.2.0) 26 | actionview (6.0.1) 27 | activesupport (= 6.0.1) 28 | builder (~> 3.1) 29 | erubi (~> 1.4) 30 | rails-dom-testing (~> 2.0) 31 | rails-html-sanitizer (~> 1.1, >= 1.2.0) 32 | activemodel (6.0.1) 33 | activesupport (= 6.0.1) 34 | activerecord (6.0.1) 35 | activemodel (= 6.0.1) 36 | activesupport (= 6.0.1) 37 | activesupport (6.0.1) 38 | concurrent-ruby (~> 1.0, >= 1.0.2) 39 | i18n (>= 0.7, < 2) 40 | minitest (~> 5.1) 41 | tzinfo (~> 1.1) 42 | zeitwerk (~> 2.2) 43 | appraisal (2.2.0) 44 | bundler 45 | rake 46 | thor (>= 0.14.0) 47 | builder (3.2.3) 48 | byebug (11.0.1) 49 | coderay (1.1.2) 50 | concurrent-ruby (1.1.5) 51 | crass (1.0.5) 52 | database_cleaner (1.7.0) 53 | diff-lcs (1.3) 54 | erubi (1.9.0) 55 | i18n (1.7.0) 56 | concurrent-ruby (~> 1.0) 57 | loofah (2.3.1) 58 | crass (~> 1.0.2) 59 | nokogiri (>= 1.5.9) 60 | method_source (0.9.2) 61 | mini_portile2 (2.4.0) 62 | minitest (5.13.0) 63 | nokogiri (1.10.5) 64 | mini_portile2 (~> 2.4.0) 65 | pg (1.1.4) 66 | pry (0.12.2) 67 | coderay (~> 1.1.0) 68 | method_source (~> 0.9.0) 69 | pry-byebug (3.7.0) 70 | byebug (~> 11.0) 71 | pry (~> 0.10) 72 | rack (2.0.8) 73 | rack-test (1.1.0) 74 | rack (>= 1.0, < 3) 75 | rails-dom-testing (2.0.3) 76 | activesupport (>= 4.2.0) 77 | nokogiri (>= 1.6) 78 | rails-html-sanitizer (1.3.0) 79 | loofah (~> 2.3) 80 | railties (6.0.1) 81 | actionpack (= 6.0.1) 82 | activesupport (= 6.0.1) 83 | method_source 84 | rake (>= 0.8.7) 85 | thor (>= 0.20.3, < 2.0) 86 | rake (13.0.0) 87 | rspec (3.9.0) 88 | rspec-core (~> 3.9.0) 89 | rspec-expectations (~> 3.9.0) 90 | rspec-mocks (~> 3.9.0) 91 | rspec-core (3.9.0) 92 | rspec-support (~> 3.9.0) 93 | rspec-expectations (3.9.0) 94 | diff-lcs (>= 1.2.0, < 2.0) 95 | rspec-support (~> 3.9.0) 96 | rspec-mocks (3.9.0) 97 | diff-lcs (>= 1.2.0, < 2.0) 98 | rspec-support (~> 3.9.0) 99 | rspec-support (3.9.0) 100 | thor (0.20.3) 101 | thread_safe (0.3.6) 102 | tzinfo (1.2.5) 103 | thread_safe (~> 0.1) 104 | zeitwerk (2.2.1) 105 | 106 | PLATFORMS 107 | ruby 108 | 109 | DEPENDENCIES 110 | appraisal 111 | bundler (>= 1.17) 112 | database_cleaner (~> 1.7.0) 113 | has_array_of! 114 | pry 115 | pry-byebug 116 | rake (~> 13.0) 117 | rspec (~> 3.9.0) 118 | with_model! 119 | 120 | BUNDLED WITH 121 | 1.17.3 122 | -------------------------------------------------------------------------------- /Guardfile: -------------------------------------------------------------------------------- 1 | guard :rspec, cmd: 'bundle exec rspec' do 2 | watch(%r{^spec/.+_spec\.rb$}) 3 | watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" } 4 | watch('spec/spec_helper.rb') { "spec" } 5 | 6 | watch(%r{^spec/support/(.+)\.rb$}) { "spec" } 7 | watch('spec/rails_helper.rb') { "spec" } 8 | end 9 | -------------------------------------------------------------------------------- /MIT-LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2014 Vladimir Kochnev 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | HasArrayOf 2 | ========== 3 | 4 | ![](https://github.com/marshall-lee/has_array_of/workflows/RSpec/badge.svg) 5 | 6 | *WARNING*: This gem is a work in progress and hasn't released yet. 7 | 8 | This plugin implements alternative way to do `has_and_belongs_to_many` association in Rails using the power of PostgreSQL arrays. In many cases when you just need [acts_as_list](https://github.com/swanandp/acts_as_list) or [acts-as-taggable-on](https://github.com/mbleigh/acts-as-taggable-on) functionality the traditional approach using many-to-many with join tables is unnecessary. We can just store integer array of ids. 9 | 10 | # How does it work? 11 | 12 | Suppose we have a playlist that contains many videos. One video can be included in many playlists. It's a classic many-to-many situation but we implement it differently. 13 | 14 | ```ruby 15 | # db/migrate/20141027125227_create_playlist.rb 16 | class CreatePlaylist < ActiveRecord::Migration 17 | def change 18 | create_table :playlists do |t| 19 | t.integer :video_ids, array: true # adding array fields works only starting from Rails 4 20 | t.index :video_ids, using: :gin # we add GIN index to speed up specific queries on array 21 | end 22 | end 23 | end 24 | 25 | # app/models/playlist.rb 26 | class Playlist < ActiveRecord::Base 27 | has_array_of :videos # by convention, it assumes that Post has a video_ids array field 28 | end 29 | 30 | # app/models/video.rb 31 | class Video < ActiveRecord::Base 32 | belongs_to_array_in_many :playlists # optional 33 | end 34 | ``` 35 | 36 | Now we can work with `videos` like with regular array. It will correctly proxy all changes to `video_ids` field. 37 | 38 | ```ruby 39 | playlist = Playlist.find(1) 40 | playlist.videos = [video1,video2] # playlist.video_ids = [1, 2] 41 | playlist.videos[0] = video3 # playlist.video_ids[0] = 3 42 | playlist.videos.insert(1, video4) # playlist.video_ids = [3, 4, 2] 43 | playlist.videos.delete_at(1) # playlist.video_ids = [3, 2] 44 | playlist.videos.pop # playlist.video_ids = [3] 45 | # ... and so on 46 | 47 | video3.playlists 48 | # => [playlist] 49 | ``` 50 | 51 | `has_array_of` also adds some search scopes: 52 | 53 | ```ruby 54 | Playlist.with_videos_containing(video1, video2) 55 | Playlist.with_videos_contained_in(video1, video2, video3, video4, ...) 56 | Playlist.with_any_videos_from(video1, video2, video3, video4, ...) 57 | ``` 58 | 59 | Anything like associated lists or arrays can be implemented such way. Now, the more typical example: 60 | 61 | ```ruby 62 | class Tag; end 63 | 64 | class Post 65 | has_array_of :tags 66 | end 67 | ``` 68 | 69 | Tags, arrays, lists — they're all the same! 70 | 71 | # Contribute? 72 | 73 | 1. Fork it 74 | 2. `% bundle install` 75 | 3. `% createdb has_array_of_test` 76 | 4. `% bundle exec rspec` 77 | 5. ... 78 | 6. Make pull request! 79 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | begin 2 | require 'rubygems' 3 | require 'bundler/setup' 4 | rescue LoadError 5 | puts 'You must `gem install bundler` and `bundle install` to run rake tasks' 6 | end 7 | 8 | task default: :spec 9 | 10 | require 'rdoc/task' 11 | 12 | RDoc::Task.new(:rdoc) do |rdoc| 13 | rdoc.rdoc_dir = 'rdoc' 14 | rdoc.title = 'HasArrayOf' 15 | rdoc.options << '--line-numbers' 16 | rdoc.rdoc_files.include('README.rdoc') 17 | rdoc.rdoc_files.include('lib/**/*.rb') 18 | end 19 | 20 | require 'rspec/core/rake_task' 21 | RSpec::Core::RakeTask.new do |t| 22 | t.pattern = 'spec/**/*_spec.rb' 23 | end 24 | 25 | Bundler::GemHelper.install_tasks 26 | 27 | task 'db:setup' do 28 | if system("createdb has_array_of_test") 29 | puts 'Database has_array_of_test was successfully created.' 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /gemfiles/activerecord_4.2.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "with_model", git: "https://github.com/Casecommons/with_model" 6 | gem "activerecord", "~> 4.2.11.1" 7 | gem "pg", "~> 0.15" 8 | 9 | group :local_development do 10 | gem "pry" 11 | gem "pry-byebug" 12 | end 13 | 14 | gemspec path: "../" 15 | -------------------------------------------------------------------------------- /gemfiles/activerecord_4.2.gemfile.lock: -------------------------------------------------------------------------------- 1 | GIT 2 | remote: https://github.com/Casecommons/with_model 3 | revision: 0309b7689223e7a4e8be236161b66f27227987db 4 | specs: 5 | with_model (2.1.2) 6 | activerecord (>= 4.2, < 6.1) 7 | 8 | PATH 9 | remote: .. 10 | specs: 11 | has_array_of (0.0.1) 12 | activerecord (>= 4.2) 13 | pg 14 | railties (>= 4.2) 15 | 16 | GEM 17 | remote: https://rubygems.org/ 18 | specs: 19 | actionpack (4.2.11.1) 20 | actionview (= 4.2.11.1) 21 | activesupport (= 4.2.11.1) 22 | rack (~> 1.6) 23 | rack-test (~> 0.6.2) 24 | rails-dom-testing (~> 1.0, >= 1.0.5) 25 | rails-html-sanitizer (~> 1.0, >= 1.0.2) 26 | actionview (4.2.11.1) 27 | activesupport (= 4.2.11.1) 28 | builder (~> 3.1) 29 | erubis (~> 2.7.0) 30 | rails-dom-testing (~> 1.0, >= 1.0.5) 31 | rails-html-sanitizer (~> 1.0, >= 1.0.3) 32 | activemodel (4.2.11.1) 33 | activesupport (= 4.2.11.1) 34 | builder (~> 3.1) 35 | activerecord (4.2.11.1) 36 | activemodel (= 4.2.11.1) 37 | activesupport (= 4.2.11.1) 38 | arel (~> 6.0) 39 | activesupport (4.2.11.1) 40 | i18n (~> 0.7) 41 | minitest (~> 5.1) 42 | thread_safe (~> 0.3, >= 0.3.4) 43 | tzinfo (~> 1.1) 44 | appraisal (2.2.0) 45 | bundler 46 | rake 47 | thor (>= 0.14.0) 48 | arel (6.0.4) 49 | builder (3.2.3) 50 | byebug (11.0.1) 51 | coderay (1.1.2) 52 | concurrent-ruby (1.1.5) 53 | crass (1.0.5) 54 | database_cleaner (1.7.0) 55 | diff-lcs (1.3) 56 | erubis (2.7.0) 57 | i18n (0.9.5) 58 | concurrent-ruby (~> 1.0) 59 | loofah (2.3.1) 60 | crass (~> 1.0.2) 61 | nokogiri (>= 1.5.9) 62 | method_source (0.9.2) 63 | mini_portile2 (2.4.0) 64 | minitest (5.13.0) 65 | nokogiri (1.10.5) 66 | mini_portile2 (~> 2.4.0) 67 | pg (0.21.0) 68 | pry (0.12.2) 69 | coderay (~> 1.1.0) 70 | method_source (~> 0.9.0) 71 | pry-byebug (3.7.0) 72 | byebug (~> 11.0) 73 | pry (~> 0.10) 74 | rack (1.6.11) 75 | rack-test (0.6.3) 76 | rack (>= 1.0) 77 | rails-deprecated_sanitizer (1.0.3) 78 | activesupport (>= 4.2.0.alpha) 79 | rails-dom-testing (1.0.9) 80 | activesupport (>= 4.2.0, < 5.0) 81 | nokogiri (~> 1.6) 82 | rails-deprecated_sanitizer (>= 1.0.1) 83 | rails-html-sanitizer (1.3.0) 84 | loofah (~> 2.3) 85 | railties (4.2.11.1) 86 | actionpack (= 4.2.11.1) 87 | activesupport (= 4.2.11.1) 88 | rake (>= 0.8.7) 89 | thor (>= 0.18.1, < 2.0) 90 | rake (13.0.0) 91 | rspec (3.9.0) 92 | rspec-core (~> 3.9.0) 93 | rspec-expectations (~> 3.9.0) 94 | rspec-mocks (~> 3.9.0) 95 | rspec-core (3.9.0) 96 | rspec-support (~> 3.9.0) 97 | rspec-expectations (3.9.0) 98 | diff-lcs (>= 1.2.0, < 2.0) 99 | rspec-support (~> 3.9.0) 100 | rspec-mocks (3.9.0) 101 | diff-lcs (>= 1.2.0, < 2.0) 102 | rspec-support (~> 3.9.0) 103 | rspec-support (3.9.0) 104 | thor (0.20.3) 105 | thread_safe (0.3.6) 106 | tzinfo (1.2.5) 107 | thread_safe (~> 0.1) 108 | 109 | PLATFORMS 110 | ruby 111 | 112 | DEPENDENCIES 113 | activerecord (~> 4.2.11.1) 114 | appraisal 115 | bundler (>= 1.17) 116 | database_cleaner (~> 1.7.0) 117 | has_array_of! 118 | pg (~> 0.15) 119 | pry 120 | pry-byebug 121 | rake (~> 13.0) 122 | rspec (~> 3.9.0) 123 | with_model! 124 | 125 | BUNDLED WITH 126 | 1.17.3 127 | -------------------------------------------------------------------------------- /gemfiles/activerecord_5.0.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "with_model", git: "https://github.com/Casecommons/with_model" 6 | gem "activerecord", "~> 5.0.7.2" 7 | gem "pg", ">= 0.18", "< 2.0" 8 | 9 | group :local_development do 10 | gem "pry" 11 | gem "pry-byebug" 12 | end 13 | 14 | gemspec path: "../" 15 | -------------------------------------------------------------------------------- /gemfiles/activerecord_5.0.gemfile.lock: -------------------------------------------------------------------------------- 1 | GIT 2 | remote: https://github.com/Casecommons/with_model 3 | revision: 0309b7689223e7a4e8be236161b66f27227987db 4 | specs: 5 | with_model (2.1.2) 6 | activerecord (>= 4.2, < 6.1) 7 | 8 | PATH 9 | remote: .. 10 | specs: 11 | has_array_of (0.0.1) 12 | activerecord (>= 4.2) 13 | pg 14 | railties (>= 4.2) 15 | 16 | GEM 17 | remote: https://rubygems.org/ 18 | specs: 19 | actionpack (5.0.7.2) 20 | actionview (= 5.0.7.2) 21 | activesupport (= 5.0.7.2) 22 | rack (~> 2.0) 23 | rack-test (~> 0.6.3) 24 | rails-dom-testing (~> 2.0) 25 | rails-html-sanitizer (~> 1.0, >= 1.0.2) 26 | actionview (5.0.7.2) 27 | activesupport (= 5.0.7.2) 28 | builder (~> 3.1) 29 | erubis (~> 2.7.0) 30 | rails-dom-testing (~> 2.0) 31 | rails-html-sanitizer (~> 1.0, >= 1.0.3) 32 | activemodel (5.0.7.2) 33 | activesupport (= 5.0.7.2) 34 | activerecord (5.0.7.2) 35 | activemodel (= 5.0.7.2) 36 | activesupport (= 5.0.7.2) 37 | arel (~> 7.0) 38 | activesupport (5.0.7.2) 39 | concurrent-ruby (~> 1.0, >= 1.0.2) 40 | i18n (>= 0.7, < 2) 41 | minitest (~> 5.1) 42 | tzinfo (~> 1.1) 43 | appraisal (2.2.0) 44 | bundler 45 | rake 46 | thor (>= 0.14.0) 47 | arel (7.1.4) 48 | builder (3.2.3) 49 | byebug (11.0.1) 50 | coderay (1.1.2) 51 | concurrent-ruby (1.1.5) 52 | crass (1.0.5) 53 | database_cleaner (1.7.0) 54 | diff-lcs (1.3) 55 | erubis (2.7.0) 56 | i18n (1.7.0) 57 | concurrent-ruby (~> 1.0) 58 | loofah (2.3.1) 59 | crass (~> 1.0.2) 60 | nokogiri (>= 1.5.9) 61 | method_source (0.9.2) 62 | mini_portile2 (2.4.0) 63 | minitest (5.13.0) 64 | nokogiri (1.10.5) 65 | mini_portile2 (~> 2.4.0) 66 | pg (1.1.4) 67 | pry (0.12.2) 68 | coderay (~> 1.1.0) 69 | method_source (~> 0.9.0) 70 | pry-byebug (3.7.0) 71 | byebug (~> 11.0) 72 | pry (~> 0.10) 73 | rack (2.0.7) 74 | rack-test (0.6.3) 75 | rack (>= 1.0) 76 | rails-dom-testing (2.0.3) 77 | activesupport (>= 4.2.0) 78 | nokogiri (>= 1.6) 79 | rails-html-sanitizer (1.3.0) 80 | loofah (~> 2.3) 81 | railties (5.0.7.2) 82 | actionpack (= 5.0.7.2) 83 | activesupport (= 5.0.7.2) 84 | method_source 85 | rake (>= 0.8.7) 86 | thor (>= 0.18.1, < 2.0) 87 | rake (13.0.0) 88 | rspec (3.9.0) 89 | rspec-core (~> 3.9.0) 90 | rspec-expectations (~> 3.9.0) 91 | rspec-mocks (~> 3.9.0) 92 | rspec-core (3.9.0) 93 | rspec-support (~> 3.9.0) 94 | rspec-expectations (3.9.0) 95 | diff-lcs (>= 1.2.0, < 2.0) 96 | rspec-support (~> 3.9.0) 97 | rspec-mocks (3.9.0) 98 | diff-lcs (>= 1.2.0, < 2.0) 99 | rspec-support (~> 3.9.0) 100 | rspec-support (3.9.0) 101 | thor (0.20.3) 102 | thread_safe (0.3.6) 103 | tzinfo (1.2.5) 104 | thread_safe (~> 0.1) 105 | 106 | PLATFORMS 107 | ruby 108 | 109 | DEPENDENCIES 110 | activerecord (~> 5.0.7.2) 111 | appraisal 112 | bundler (>= 1.17) 113 | database_cleaner (~> 1.7.0) 114 | has_array_of! 115 | pg (>= 0.18, < 2.0) 116 | pry 117 | pry-byebug 118 | rake (~> 13.0) 119 | rspec (~> 3.9.0) 120 | with_model! 121 | 122 | BUNDLED WITH 123 | 1.17.3 124 | -------------------------------------------------------------------------------- /gemfiles/activerecord_5.1.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "with_model", git: "https://github.com/Casecommons/with_model" 6 | gem "activerecord", "~> 5.1.7" 7 | gem "pg", ">= 0.18", "< 2.0" 8 | 9 | group :local_development do 10 | gem "pry" 11 | gem "pry-byebug" 12 | end 13 | 14 | gemspec path: "../" 15 | -------------------------------------------------------------------------------- /gemfiles/activerecord_5.1.gemfile.lock: -------------------------------------------------------------------------------- 1 | GIT 2 | remote: https://github.com/Casecommons/with_model 3 | revision: 0309b7689223e7a4e8be236161b66f27227987db 4 | specs: 5 | with_model (2.1.2) 6 | activerecord (>= 4.2, < 6.1) 7 | 8 | PATH 9 | remote: .. 10 | specs: 11 | has_array_of (0.0.1) 12 | activerecord (>= 4.2) 13 | pg 14 | railties (>= 4.2) 15 | 16 | GEM 17 | remote: https://rubygems.org/ 18 | specs: 19 | actionpack (5.1.7) 20 | actionview (= 5.1.7) 21 | activesupport (= 5.1.7) 22 | rack (~> 2.0) 23 | rack-test (>= 0.6.3) 24 | rails-dom-testing (~> 2.0) 25 | rails-html-sanitizer (~> 1.0, >= 1.0.2) 26 | actionview (5.1.7) 27 | activesupport (= 5.1.7) 28 | builder (~> 3.1) 29 | erubi (~> 1.4) 30 | rails-dom-testing (~> 2.0) 31 | rails-html-sanitizer (~> 1.0, >= 1.0.3) 32 | activemodel (5.1.7) 33 | activesupport (= 5.1.7) 34 | activerecord (5.1.7) 35 | activemodel (= 5.1.7) 36 | activesupport (= 5.1.7) 37 | arel (~> 8.0) 38 | activesupport (5.1.7) 39 | concurrent-ruby (~> 1.0, >= 1.0.2) 40 | i18n (>= 0.7, < 2) 41 | minitest (~> 5.1) 42 | tzinfo (~> 1.1) 43 | appraisal (2.2.0) 44 | bundler 45 | rake 46 | thor (>= 0.14.0) 47 | arel (8.0.0) 48 | builder (3.2.3) 49 | byebug (11.0.1) 50 | coderay (1.1.2) 51 | concurrent-ruby (1.1.5) 52 | crass (1.0.5) 53 | database_cleaner (1.7.0) 54 | diff-lcs (1.3) 55 | erubi (1.9.0) 56 | i18n (1.7.0) 57 | concurrent-ruby (~> 1.0) 58 | loofah (2.3.1) 59 | crass (~> 1.0.2) 60 | nokogiri (>= 1.5.9) 61 | method_source (0.9.2) 62 | mini_portile2 (2.4.0) 63 | minitest (5.13.0) 64 | nokogiri (1.10.5) 65 | mini_portile2 (~> 2.4.0) 66 | pg (1.1.4) 67 | pry (0.12.2) 68 | coderay (~> 1.1.0) 69 | method_source (~> 0.9.0) 70 | pry-byebug (3.7.0) 71 | byebug (~> 11.0) 72 | pry (~> 0.10) 73 | rack (2.0.7) 74 | rack-test (1.1.0) 75 | rack (>= 1.0, < 3) 76 | rails-dom-testing (2.0.3) 77 | activesupport (>= 4.2.0) 78 | nokogiri (>= 1.6) 79 | rails-html-sanitizer (1.3.0) 80 | loofah (~> 2.3) 81 | railties (5.1.7) 82 | actionpack (= 5.1.7) 83 | activesupport (= 5.1.7) 84 | method_source 85 | rake (>= 0.8.7) 86 | thor (>= 0.18.1, < 2.0) 87 | rake (13.0.0) 88 | rspec (3.9.0) 89 | rspec-core (~> 3.9.0) 90 | rspec-expectations (~> 3.9.0) 91 | rspec-mocks (~> 3.9.0) 92 | rspec-core (3.9.0) 93 | rspec-support (~> 3.9.0) 94 | rspec-expectations (3.9.0) 95 | diff-lcs (>= 1.2.0, < 2.0) 96 | rspec-support (~> 3.9.0) 97 | rspec-mocks (3.9.0) 98 | diff-lcs (>= 1.2.0, < 2.0) 99 | rspec-support (~> 3.9.0) 100 | rspec-support (3.9.0) 101 | thor (0.20.3) 102 | thread_safe (0.3.6) 103 | tzinfo (1.2.5) 104 | thread_safe (~> 0.1) 105 | 106 | PLATFORMS 107 | ruby 108 | 109 | DEPENDENCIES 110 | activerecord (~> 5.1.7) 111 | appraisal 112 | bundler (>= 1.17) 113 | database_cleaner (~> 1.7.0) 114 | has_array_of! 115 | pg (>= 0.18, < 2.0) 116 | pry 117 | pry-byebug 118 | rake (~> 13.0) 119 | rspec (~> 3.9.0) 120 | with_model! 121 | 122 | BUNDLED WITH 123 | 1.17.3 124 | -------------------------------------------------------------------------------- /gemfiles/activerecord_5.2.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "with_model", git: "https://github.com/Casecommons/with_model" 6 | gem "activerecord", "~> 5.2.3" 7 | gem "pg", ">= 0.18", "< 2.0" 8 | 9 | group :local_development do 10 | gem "pry" 11 | gem "pry-byebug" 12 | end 13 | 14 | gemspec path: "../" 15 | -------------------------------------------------------------------------------- /gemfiles/activerecord_5.2.gemfile.lock: -------------------------------------------------------------------------------- 1 | GIT 2 | remote: https://github.com/Casecommons/with_model 3 | revision: 0309b7689223e7a4e8be236161b66f27227987db 4 | specs: 5 | with_model (2.1.2) 6 | activerecord (>= 4.2, < 6.1) 7 | 8 | PATH 9 | remote: .. 10 | specs: 11 | has_array_of (0.0.1) 12 | activerecord (>= 4.2) 13 | pg 14 | railties (>= 4.2) 15 | 16 | GEM 17 | remote: https://rubygems.org/ 18 | specs: 19 | actionpack (5.2.3) 20 | actionview (= 5.2.3) 21 | activesupport (= 5.2.3) 22 | rack (~> 2.0) 23 | rack-test (>= 0.6.3) 24 | rails-dom-testing (~> 2.0) 25 | rails-html-sanitizer (~> 1.0, >= 1.0.2) 26 | actionview (5.2.3) 27 | activesupport (= 5.2.3) 28 | builder (~> 3.1) 29 | erubi (~> 1.4) 30 | rails-dom-testing (~> 2.0) 31 | rails-html-sanitizer (~> 1.0, >= 1.0.3) 32 | activemodel (5.2.3) 33 | activesupport (= 5.2.3) 34 | activerecord (5.2.3) 35 | activemodel (= 5.2.3) 36 | activesupport (= 5.2.3) 37 | arel (>= 9.0) 38 | activesupport (5.2.3) 39 | concurrent-ruby (~> 1.0, >= 1.0.2) 40 | i18n (>= 0.7, < 2) 41 | minitest (~> 5.1) 42 | tzinfo (~> 1.1) 43 | appraisal (2.2.0) 44 | bundler 45 | rake 46 | thor (>= 0.14.0) 47 | arel (9.0.0) 48 | builder (3.2.3) 49 | byebug (11.0.1) 50 | coderay (1.1.2) 51 | concurrent-ruby (1.1.5) 52 | crass (1.0.5) 53 | database_cleaner (1.7.0) 54 | diff-lcs (1.3) 55 | erubi (1.9.0) 56 | i18n (1.7.0) 57 | concurrent-ruby (~> 1.0) 58 | loofah (2.3.1) 59 | crass (~> 1.0.2) 60 | nokogiri (>= 1.5.9) 61 | method_source (0.9.2) 62 | mini_portile2 (2.4.0) 63 | minitest (5.13.0) 64 | nokogiri (1.10.5) 65 | mini_portile2 (~> 2.4.0) 66 | pg (1.1.4) 67 | pry (0.12.2) 68 | coderay (~> 1.1.0) 69 | method_source (~> 0.9.0) 70 | pry-byebug (3.7.0) 71 | byebug (~> 11.0) 72 | pry (~> 0.10) 73 | rack (2.0.7) 74 | rack-test (1.1.0) 75 | rack (>= 1.0, < 3) 76 | rails-dom-testing (2.0.3) 77 | activesupport (>= 4.2.0) 78 | nokogiri (>= 1.6) 79 | rails-html-sanitizer (1.3.0) 80 | loofah (~> 2.3) 81 | railties (5.2.3) 82 | actionpack (= 5.2.3) 83 | activesupport (= 5.2.3) 84 | method_source 85 | rake (>= 0.8.7) 86 | thor (>= 0.19.0, < 2.0) 87 | rake (13.0.0) 88 | rspec (3.9.0) 89 | rspec-core (~> 3.9.0) 90 | rspec-expectations (~> 3.9.0) 91 | rspec-mocks (~> 3.9.0) 92 | rspec-core (3.9.0) 93 | rspec-support (~> 3.9.0) 94 | rspec-expectations (3.9.0) 95 | diff-lcs (>= 1.2.0, < 2.0) 96 | rspec-support (~> 3.9.0) 97 | rspec-mocks (3.9.0) 98 | diff-lcs (>= 1.2.0, < 2.0) 99 | rspec-support (~> 3.9.0) 100 | rspec-support (3.9.0) 101 | thor (0.20.3) 102 | thread_safe (0.3.6) 103 | tzinfo (1.2.5) 104 | thread_safe (~> 0.1) 105 | 106 | PLATFORMS 107 | ruby 108 | 109 | DEPENDENCIES 110 | activerecord (~> 5.2.3) 111 | appraisal 112 | bundler (>= 1.17) 113 | database_cleaner (~> 1.7.0) 114 | has_array_of! 115 | pg (>= 0.18, < 2.0) 116 | pry 117 | pry-byebug 118 | rake (~> 13.0) 119 | rspec (~> 3.9.0) 120 | with_model! 121 | 122 | BUNDLED WITH 123 | 1.17.3 124 | -------------------------------------------------------------------------------- /gemfiles/activerecord_6.0.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "with_model", git: "https://github.com/Casecommons/with_model" 6 | gem "activerecord", "~> 6.0.1" 7 | gem "pg", ">= 0.18", "< 2.0" 8 | 9 | group :local_development do 10 | gem "pry" 11 | gem "pry-byebug" 12 | end 13 | 14 | gemspec path: "../" 15 | -------------------------------------------------------------------------------- /gemfiles/activerecord_6.0.gemfile.lock: -------------------------------------------------------------------------------- 1 | GIT 2 | remote: https://github.com/Casecommons/with_model 3 | revision: 0309b7689223e7a4e8be236161b66f27227987db 4 | specs: 5 | with_model (2.1.2) 6 | activerecord (>= 4.2, < 6.1) 7 | 8 | PATH 9 | remote: .. 10 | specs: 11 | has_array_of (0.0.1) 12 | activerecord (>= 4.2) 13 | pg 14 | railties (>= 4.2) 15 | 16 | GEM 17 | remote: https://rubygems.org/ 18 | specs: 19 | actionpack (6.0.1) 20 | actionview (= 6.0.1) 21 | activesupport (= 6.0.1) 22 | rack (~> 2.0) 23 | rack-test (>= 0.6.3) 24 | rails-dom-testing (~> 2.0) 25 | rails-html-sanitizer (~> 1.0, >= 1.2.0) 26 | actionview (6.0.1) 27 | activesupport (= 6.0.1) 28 | builder (~> 3.1) 29 | erubi (~> 1.4) 30 | rails-dom-testing (~> 2.0) 31 | rails-html-sanitizer (~> 1.1, >= 1.2.0) 32 | activemodel (6.0.1) 33 | activesupport (= 6.0.1) 34 | activerecord (6.0.1) 35 | activemodel (= 6.0.1) 36 | activesupport (= 6.0.1) 37 | activesupport (6.0.1) 38 | concurrent-ruby (~> 1.0, >= 1.0.2) 39 | i18n (>= 0.7, < 2) 40 | minitest (~> 5.1) 41 | tzinfo (~> 1.1) 42 | zeitwerk (~> 2.2) 43 | appraisal (2.2.0) 44 | bundler 45 | rake 46 | thor (>= 0.14.0) 47 | builder (3.2.3) 48 | byebug (11.0.1) 49 | coderay (1.1.2) 50 | concurrent-ruby (1.1.5) 51 | crass (1.0.5) 52 | database_cleaner (1.7.0) 53 | diff-lcs (1.3) 54 | erubi (1.9.0) 55 | i18n (1.7.0) 56 | concurrent-ruby (~> 1.0) 57 | loofah (2.3.1) 58 | crass (~> 1.0.2) 59 | nokogiri (>= 1.5.9) 60 | method_source (0.9.2) 61 | mini_portile2 (2.4.0) 62 | minitest (5.13.0) 63 | nokogiri (1.10.5) 64 | mini_portile2 (~> 2.4.0) 65 | pg (1.1.4) 66 | pry (0.12.2) 67 | coderay (~> 1.1.0) 68 | method_source (~> 0.9.0) 69 | pry-byebug (3.7.0) 70 | byebug (~> 11.0) 71 | pry (~> 0.10) 72 | rack (2.0.7) 73 | rack-test (1.1.0) 74 | rack (>= 1.0, < 3) 75 | rails-dom-testing (2.0.3) 76 | activesupport (>= 4.2.0) 77 | nokogiri (>= 1.6) 78 | rails-html-sanitizer (1.3.0) 79 | loofah (~> 2.3) 80 | railties (6.0.1) 81 | actionpack (= 6.0.1) 82 | activesupport (= 6.0.1) 83 | method_source 84 | rake (>= 0.8.7) 85 | thor (>= 0.20.3, < 2.0) 86 | rake (13.0.0) 87 | rspec (3.9.0) 88 | rspec-core (~> 3.9.0) 89 | rspec-expectations (~> 3.9.0) 90 | rspec-mocks (~> 3.9.0) 91 | rspec-core (3.9.0) 92 | rspec-support (~> 3.9.0) 93 | rspec-expectations (3.9.0) 94 | diff-lcs (>= 1.2.0, < 2.0) 95 | rspec-support (~> 3.9.0) 96 | rspec-mocks (3.9.0) 97 | diff-lcs (>= 1.2.0, < 2.0) 98 | rspec-support (~> 3.9.0) 99 | rspec-support (3.9.0) 100 | thor (0.20.3) 101 | thread_safe (0.3.6) 102 | tzinfo (1.2.5) 103 | thread_safe (~> 0.1) 104 | zeitwerk (2.2.1) 105 | 106 | PLATFORMS 107 | ruby 108 | 109 | DEPENDENCIES 110 | activerecord (~> 6.0.1) 111 | appraisal 112 | bundler (>= 1.17) 113 | database_cleaner (~> 1.7.0) 114 | has_array_of! 115 | pg (>= 0.18, < 2.0) 116 | pry 117 | pry-byebug 118 | rake (~> 13.0) 119 | rspec (~> 3.9.0) 120 | with_model! 121 | 122 | BUNDLED WITH 123 | 1.17.3 124 | -------------------------------------------------------------------------------- /gemfiles/activerecord_edge.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "with_model", git: "https://github.com/Casecommons/with_model" 6 | gem "activerecord", git: "https://github.com/rails/rails" 7 | 8 | group :local_development do 9 | gem "pry" 10 | gem "pry-byebug" 11 | end 12 | 13 | gemspec path: "../" 14 | -------------------------------------------------------------------------------- /gemfiles/activerecord_edge.gemfile.lock: -------------------------------------------------------------------------------- 1 | GIT 2 | remote: https://github.com/Casecommons/with_model 3 | revision: 0309b7689223e7a4e8be236161b66f27227987db 4 | specs: 5 | with_model (2.1.2) 6 | activerecord (>= 4.2, < 6.1) 7 | 8 | GIT 9 | remote: https://github.com/rails/rails 10 | revision: 53627bd55e0e7c6af6884a968f1171a3ce34bbae 11 | specs: 12 | actionpack (6.1.0.alpha) 13 | actionview (= 6.1.0.alpha) 14 | activesupport (= 6.1.0.alpha) 15 | rack (~> 2.0) 16 | rack-test (>= 0.6.3) 17 | rails-dom-testing (~> 2.0) 18 | rails-html-sanitizer (~> 1.0, >= 1.2.0) 19 | actionview (6.1.0.alpha) 20 | activesupport (= 6.1.0.alpha) 21 | builder (~> 3.1) 22 | erubi (~> 1.4) 23 | rails-dom-testing (~> 2.0) 24 | rails-html-sanitizer (~> 1.1, >= 1.2.0) 25 | activemodel (6.1.0.alpha) 26 | activesupport (= 6.1.0.alpha) 27 | activerecord (6.1.0.alpha) 28 | activemodel (= 6.1.0.alpha) 29 | activesupport (= 6.1.0.alpha) 30 | activesupport (6.1.0.alpha) 31 | concurrent-ruby (~> 1.0, >= 1.0.2) 32 | i18n (>= 0.7, < 2) 33 | minitest (~> 5.1) 34 | tzinfo (~> 1.1) 35 | zeitwerk (~> 2.2) 36 | railties (6.1.0.alpha) 37 | actionpack (= 6.1.0.alpha) 38 | activesupport (= 6.1.0.alpha) 39 | method_source 40 | rake (>= 0.8.7) 41 | thor (>= 0.20.3, < 2.0) 42 | 43 | PATH 44 | remote: .. 45 | specs: 46 | has_array_of (0.0.1) 47 | activerecord (>= 4.2) 48 | pg 49 | railties (>= 4.2) 50 | 51 | GEM 52 | remote: https://rubygems.org/ 53 | specs: 54 | appraisal (2.2.0) 55 | bundler 56 | rake 57 | thor (>= 0.14.0) 58 | builder (3.2.3) 59 | byebug (11.0.1) 60 | coderay (1.1.2) 61 | concurrent-ruby (1.1.5) 62 | crass (1.0.5) 63 | database_cleaner (1.7.0) 64 | diff-lcs (1.3) 65 | erubi (1.9.0) 66 | i18n (1.7.0) 67 | concurrent-ruby (~> 1.0) 68 | loofah (2.3.1) 69 | crass (~> 1.0.2) 70 | nokogiri (>= 1.5.9) 71 | method_source (0.9.2) 72 | mini_portile2 (2.4.0) 73 | minitest (5.13.0) 74 | nokogiri (1.10.5) 75 | mini_portile2 (~> 2.4.0) 76 | pg (1.1.4) 77 | pry (0.12.2) 78 | coderay (~> 1.1.0) 79 | method_source (~> 0.9.0) 80 | pry-byebug (3.7.0) 81 | byebug (~> 11.0) 82 | pry (~> 0.10) 83 | rack (2.0.7) 84 | rack-test (1.1.0) 85 | rack (>= 1.0, < 3) 86 | rails-dom-testing (2.0.3) 87 | activesupport (>= 4.2.0) 88 | nokogiri (>= 1.6) 89 | rails-html-sanitizer (1.3.0) 90 | loofah (~> 2.3) 91 | rake (13.0.0) 92 | rspec (3.9.0) 93 | rspec-core (~> 3.9.0) 94 | rspec-expectations (~> 3.9.0) 95 | rspec-mocks (~> 3.9.0) 96 | rspec-core (3.9.0) 97 | rspec-support (~> 3.9.0) 98 | rspec-expectations (3.9.0) 99 | diff-lcs (>= 1.2.0, < 2.0) 100 | rspec-support (~> 3.9.0) 101 | rspec-mocks (3.9.0) 102 | diff-lcs (>= 1.2.0, < 2.0) 103 | rspec-support (~> 3.9.0) 104 | rspec-support (3.9.0) 105 | thor (0.20.3) 106 | thread_safe (0.3.6) 107 | tzinfo (1.2.5) 108 | thread_safe (~> 0.1) 109 | zeitwerk (2.2.1) 110 | 111 | PLATFORMS 112 | ruby 113 | 114 | DEPENDENCIES 115 | activerecord! 116 | appraisal 117 | bundler (>= 1.17) 118 | database_cleaner (~> 1.7.0) 119 | has_array_of! 120 | pry 121 | pry-byebug 122 | rake (~> 13.0) 123 | rspec (~> 3.9.0) 124 | with_model! 125 | 126 | BUNDLED WITH 127 | 1.17.3 128 | -------------------------------------------------------------------------------- /has_array_of.gemspec: -------------------------------------------------------------------------------- 1 | $:.push File.expand_path("../lib", __FILE__) 2 | 3 | # Maintain your gem's version: 4 | require "has_array_of/version" 5 | 6 | # Describe your gem and declare its dependencies: 7 | Gem::Specification.new do |s| 8 | s.name = "has_array_of" 9 | s.version = HasArrayOf::VERSION 10 | s.author = "Vladimir Kochnev" 11 | s.email = "hashtable@yandex.ru" 12 | s.homepage = "https://github.com/marshall-lee/has_array_of" 13 | s.summary = "Associations on top of PostgreSQL arrays" 14 | s.description = "Adds possibility of has_many and belongs_to_many associations using PostgreSQL arrays of ids." 15 | s.license = "MIT" 16 | 17 | s.files = Dir["{app,config,db,lib}/**/*", "MIT-LICENSE", "Rakefile", "README.md"] 18 | 19 | unless RUBY_PLATFORM =~ /java/ 20 | s.add_dependency 'pg' 21 | else 22 | s.add_dependency 'activerecord-jdbcpostgresql-adapter' 23 | end 24 | 25 | s.add_dependency 'activerecord', '>= 4.2' 26 | s.add_dependency 'railties', '>= 4.2' 27 | 28 | s.add_development_dependency 'bundler', '>= 1.17' 29 | s.add_development_dependency 'appraisal' 30 | s.add_development_dependency 'rake', '~> 13.0' 31 | s.add_development_dependency 'rspec', '~> 3.9.0' 32 | s.add_development_dependency 'database_cleaner', '~> 1.7.0' 33 | s.add_development_dependency 'with_model', '~> 2.1.2' 34 | end 35 | -------------------------------------------------------------------------------- /lib/has_array_of.rb: -------------------------------------------------------------------------------- 1 | require 'active_record' 2 | module HasArrayOf 3 | end 4 | require 'has_array_of/builders' 5 | require 'has_array_of/railtie' if defined?(Rails) 6 | -------------------------------------------------------------------------------- /lib/has_array_of/associated_array.rb: -------------------------------------------------------------------------------- 1 | module HasArrayOf::AssociatedArray 2 | def self.define_in(owner_model, options) 3 | name = options[:name] 4 | singular_name = options[:singular_name] 5 | ids_attribute = options[:ids_attribute] 6 | try_pkey_proc = options[:try_pkey_proc] = proc { |obj| obj.try(owner_model.primary_key) } 7 | owner_model.class_eval do 8 | define_method name do 9 | Relation.new(self, options[:model], ids_attribute) 10 | end 11 | 12 | define_method "#{name}=" do |objects| 13 | ids = if objects.respond_to? :pluck 14 | objects.pluck(owner_model.primary_key) 15 | else 16 | objects.map(&try_pkey_proc) 17 | end 18 | write_attribute(ids_attribute, ids) 19 | end 20 | 21 | to_ids = proc do |first, *rest| 22 | ary = if rest.empty? 23 | Array.wrap(first) 24 | else 25 | [first, *rest] 26 | end 27 | ary.map(&try_pkey_proc) 28 | end 29 | 30 | define_singleton_method "with_#{name}_containing" do |*args| 31 | ids = to_ids[args] 32 | if ids.empty? 33 | all 34 | else 35 | where "#{ids_attribute} @> ARRAY[?]", ids 36 | end 37 | end 38 | 39 | define_singleton_method "with_#{name}_contained_in" do |*args| 40 | ids = to_ids[args] 41 | if ids.empty? 42 | none 43 | else 44 | where "#{ids_attribute} <@ ARRAY[?]", to_ids[args] 45 | end 46 | end 47 | 48 | define_singleton_method "with_any_#{singular_name}_from" do |*args| 49 | ids = to_ids[args] 50 | if ids.empty? 51 | none 52 | else 53 | where "#{ids_attribute} && ARRAY[?]", ids 54 | end 55 | end 56 | end 57 | end 58 | end 59 | 60 | require 'has_array_of/associated_array/relation.rb' 61 | -------------------------------------------------------------------------------- /lib/has_array_of/associated_array/relation.rb: -------------------------------------------------------------------------------- 1 | module HasArrayOf 2 | class AssociatedArray::Relation 3 | def initialize(owner, model, ids_attr, scope: model.all) 4 | @owner = owner 5 | @model = model 6 | @foreign_id_attr = model.primary_key 7 | @ids_attr = ids_attr 8 | @scope = scope 9 | build_query! 10 | end 11 | 12 | def ids 13 | owner[ids_attr] 14 | end 15 | 16 | def ids=(new_ids) 17 | owner[ids_attr] = new_ids 18 | end 19 | 20 | def load 21 | relation.load 22 | self 23 | end 24 | 25 | def records 26 | @relation.load 27 | records = @relation.instance_variable_get(:@records) 28 | unless @records.equal? records 29 | @records = records.index_by(&foreign_id_for_proc).values_at(*ids) 30 | @records.compact! 31 | end 32 | @records 33 | end 34 | 35 | def where(*args) 36 | self.class.new(@owner, @model, @ids_attr, scope: @scope.where(*args)) 37 | end 38 | 39 | def where!(*args) 40 | @scope.where!(*args) 41 | build_query! 42 | self 43 | end 44 | 45 | def to_ary 46 | records.dup 47 | end 48 | alias to_a to_ary 49 | 50 | def each(&block) 51 | records.each(&block) 52 | end 53 | 54 | include Enumerable 55 | 56 | def pluck(*column_names) 57 | raise NotImplementedError 58 | end 59 | 60 | def ==(other) 61 | to_a == other 62 | end 63 | 64 | def mutate_ids 65 | relation.reset 66 | ret = yield 67 | self.ids = ids 68 | build_query! 69 | ret 70 | end 71 | 72 | def <<(object) 73 | mutate_ids do 74 | ids << foreign_id_for(object) 75 | self 76 | end 77 | end 78 | 79 | def []=(*index, val) 80 | mutate_ids do 81 | if val.is_a? Array 82 | ids[*index] = val.map(&foreign_id_for_proc) 83 | else 84 | ids[*index] = foreign_id_for(val) 85 | end 86 | val 87 | end 88 | end 89 | 90 | def collect! 91 | if block_given 92 | map!(Proc.new) 93 | else 94 | to_enum(:collect!) 95 | end 96 | end 97 | 98 | def compact! 99 | mutate_ids do 100 | ids.compact! 101 | self 102 | end 103 | end 104 | 105 | def concat(other) 106 | mutate_ids do 107 | ids.concat(other.map(&foreign_id_for_proc)) 108 | self 109 | end 110 | end 111 | 112 | def delete(object) 113 | # TODO: optimize 114 | mutate_ids do 115 | id = ids.delete(foreign_id_for(object)) 116 | if id 117 | @model.find(id) 118 | end 119 | end 120 | end 121 | 122 | def delete_at(index) 123 | # TODO: optimize 124 | mutate_ids do 125 | id = ids.delete_at(index) 126 | if id 127 | @model.find(id) 128 | end 129 | end 130 | end 131 | 132 | def delete_if 133 | if block_given? 134 | hash = ids_to_objects_hash 135 | mutate_ids do 136 | ids.delete_if { |id| yield hash[id] } 137 | self 138 | end 139 | else 140 | to_enum(:delete_if) 141 | end 142 | end 143 | 144 | def fill(*args) 145 | if block_given? 146 | mutate_ids do 147 | ids.fill(*args) do |index| 148 | foreign_id_for(yield index) 149 | end 150 | end 151 | else 152 | mutate_ids do 153 | obj = args.shift 154 | ids.fill(foreign_id_for(obj), *args) 155 | end 156 | end 157 | self 158 | end 159 | 160 | def insert(index, *objects) 161 | mutate_ids do 162 | ids.insert(index, *objects.map(&foreign_id_for_proc)) 163 | self 164 | end 165 | end 166 | 167 | def keep_if 168 | if block_given? 169 | hash = ids_to_objects_hash 170 | mutate_ids do 171 | ids.keep_if { |id| yield hash[id] } 172 | self 173 | end 174 | else 175 | to_enum(:keep_if) 176 | end 177 | end 178 | 179 | def map! 180 | if block_given? 181 | data = to_a 182 | mutate_ids do 183 | data.each_with_index do |object, index| 184 | ids[index] = foreign_id_for(yield object) 185 | end 186 | end 187 | else 188 | to_enum :map! 189 | end 190 | end 191 | 192 | def pop 193 | # TODO: optimize 194 | mutate_ids do 195 | @model.find(ids.pop) 196 | end 197 | end 198 | 199 | def push(*objects) 200 | mutate_ids do 201 | ids.push(*objects.map(&foreign_id_for_proc)) 202 | self 203 | end 204 | end 205 | 206 | def reject! 207 | if block_given? 208 | hash = ids_to_objects_hash 209 | mutate_ids do 210 | if ids.reject! { |id| yield hash[id] } 211 | self 212 | end 213 | end 214 | else 215 | to_enum(:reject!) 216 | end 217 | end 218 | 219 | def replace(other_ary) 220 | mutate_ids do 221 | ids.replace other_ary.map(&foreign_id_for_proc) 222 | self 223 | end 224 | end 225 | 226 | def reverse! 227 | mutate_ids do 228 | ids.reverse! 229 | self 230 | end 231 | end 232 | 233 | def rotate!(count=1) 234 | mutate_ids do 235 | ids.rotate! count 236 | self 237 | end 238 | end 239 | 240 | def select! 241 | if block_given? 242 | hash = ids_to_objects_hash 243 | mutate_ids do 244 | if ids.select! { |id| yield hash[id] } 245 | self 246 | end 247 | end 248 | else 249 | to_enum(:select!) 250 | end 251 | end 252 | 253 | def shift 254 | # TODO: optimize 255 | mutate_ids do 256 | @model.find(ids.shift) 257 | end 258 | end 259 | 260 | def shuffle!(args={}) 261 | mutate_ids do 262 | ids.shuffle!(args) 263 | self 264 | end 265 | end 266 | 267 | def uniq! 268 | if block_given? 269 | hash = ids_to_objects_hash 270 | mutate_ids do 271 | ids.uniq! do |id| 272 | yield hash[id] 273 | end 274 | end 275 | else 276 | mutate_ids do 277 | ids.uniq! 278 | end 279 | end 280 | self 281 | end 282 | 283 | def unshift(*args) 284 | mutate_ids do 285 | ids.unshift(*args.map(&foreign_id_for_proc)) 286 | end 287 | self 288 | end 289 | 290 | private 291 | 292 | def foreign_id_for(obj) 293 | obj[foreign_id_attr] if obj 294 | end 295 | 296 | def foreign_id_for_proc 297 | @foreign_id_for_proc ||= method(:foreign_id_for) 298 | end 299 | 300 | def ids_to_objects_hash 301 | index_by(&foreign_id_for_proc) 302 | end 303 | 304 | def build_query! 305 | @relation = @model.where(foreign_id_attr => ids.compact).merge(@scope) 306 | end 307 | 308 | attr_reader :owner, :ids_attr 309 | attr_reader :foreign_id_attr 310 | attr_reader :relation 311 | delegate :each, to: :records 312 | 313 | # relation_methods = ::ActiveRecord::Relation.public_instance_methods - instance_methods - private_instance_methods 314 | delegate :loaded?, 315 | :to_sql, 316 | :to => :relation 317 | delegate :size, :length, :to => :ids 318 | end 319 | end 320 | -------------------------------------------------------------------------------- /lib/has_array_of/associated_belongs.rb: -------------------------------------------------------------------------------- 1 | module HasArrayOf 2 | module AssociatedBelongs 3 | def self.define_in(model, options) 4 | name = options[:name] 5 | array_name = options[:array_name] 6 | class_name = options[:class_name] 7 | with_method_name = "with_#{array_name}_containing" 8 | model.class_eval do 9 | associated_model = nil 10 | associated_model_proc = -> { associated_model ||= class_name.constantize } 11 | 12 | define_method name do 13 | associated_model_proc.call.send(with_method_name, [self]) 14 | end 15 | end 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/has_array_of/builders.rb: -------------------------------------------------------------------------------- 1 | require 'has_array_of/associated_array' 2 | require 'has_array_of/associated_belongs' 3 | 4 | module HasArrayOf 5 | module Builders 6 | extend ActiveSupport::Concern 7 | 8 | module ClassMethods 9 | def has_array_of(name, options = {}) 10 | extension = if block_given? 11 | Module.new(&proc) 12 | end 13 | singular_name = name.to_s.singularize 14 | class_name = (options[:class_name] || singular_name.camelize).to_s 15 | ids_attribute = "#{singular_name}_ids".to_sym 16 | model = class_name.constantize 17 | 18 | AssociatedArray.define_in self, name: name, 19 | singular_name: singular_name, 20 | ids_attribute: ids_attribute, 21 | model: model, 22 | extension: extension 23 | end 24 | 25 | def belongs_to_array_in_many(name, options={}) 26 | name = name.to_s 27 | class_name = (options[:class_name] || name.singularize.camelize).to_s 28 | array_name = if options[:array_name] 29 | options[:array_name].to_s 30 | else 31 | self.name.underscore.pluralize 32 | end 33 | AssociatedBelongs.define_in self, name: name, 34 | class_name: class_name, 35 | array_name: array_name 36 | end 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /lib/has_array_of/railtie.rb: -------------------------------------------------------------------------------- 1 | module HasArrayOf 2 | class Railtie < ::Rails::Railtie 3 | ActiveSupport.on_load(:active_record) do 4 | ActiveRecord::Base.send :include, HasArrayOf::Builders 5 | end 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /lib/has_array_of/version.rb: -------------------------------------------------------------------------------- 1 | module HasArrayOf 2 | VERSION = "0.0.1" 3 | end 4 | -------------------------------------------------------------------------------- /spec/has_array_of/associated_array/relation_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe HasArrayOf::AssociatedArray::Relation do 4 | include_context "Video model" 5 | include_context "Playlist model" 6 | include_context "TV series" 7 | 8 | let!(:two_food_chains_and_pony_videos) { 9 | [food_chain, return_of_harmony, food_chain] 10 | } 11 | let!(:two_food_chains_and_pony_video_ids) { 12 | two_food_chains_and_pony_videos.map(&:id) 13 | } 14 | let!(:two_food_chains_and_pony) { 15 | Playlist.create(video_ids: two_food_chains_and_pony_video_ids) 16 | } 17 | 18 | describe "associated collection reader" do 19 | it "should respond to scope method" do 20 | expect(my_cool_list).to respond_to(:videos) 21 | end 22 | 23 | it "should fetch correct objects" do 24 | expect(Video.count).to eq(4) 25 | expect(adventure_time_season6.videos).to eq(adventure_time_videos) 26 | expect(mlp_season2.videos).to eq(mlp_videos) 27 | end 28 | 29 | it "should correctly deal with dups" do 30 | expect(two_food_chains_and_pony.videos).to eq(two_food_chains_and_pony_videos) 31 | expect(two_food_chains_and_pony.videos.map(&:id)).to eq(two_food_chains_and_pony_video_ids) 32 | end 33 | 34 | it "should correctly deal with nil" do 35 | playlist = Playlist.create video_ids: [nil, something_big.id, nil] 36 | expect(playlist.videos).to eq([something_big]) 37 | playlist = Playlist.create video_ids: [nil, nil, nil] 38 | expect(playlist.videos).to eq([]) 39 | end 40 | 41 | describe "when chaining with other queries" do 42 | let(:playlist) { two_food_chains_and_pony } 43 | 44 | it "should fetch correct objects" do 45 | expect(playlist.videos.where("title like '%Pony%'")).to eq([return_of_harmony]) 46 | expect(playlist.videos.where("title like '%Adventure%'")).to eq([food_chain, food_chain]) 47 | end 48 | 49 | describe "when having nils" do 50 | before do 51 | playlist.videos << nil 52 | playlist.save 53 | end 54 | 55 | it "should fetch correct objects" do 56 | videos = playlist.videos 57 | expect(videos.where("title like '%Pony%'")).to eq([return_of_harmony]) 58 | videos.where!("title like '%Adventure%'") 59 | expect(videos.to_a).to eq([food_chain, food_chain]) 60 | end 61 | end 62 | end 63 | end 64 | 65 | describe "associated collection assigner" do 66 | it "should respond to assignment method" do 67 | expect(my_cool_list).to respond_to(:videos=) 68 | end 69 | 70 | it "should reflect changes" do 71 | mlp_season2.videos = adventure_time_videos 72 | expect(mlp_season2.videos).to eq(adventure_time_videos) 73 | end 74 | 75 | it "should affect ids" do 76 | mlp_season2.videos = adventure_time_videos 77 | expected_ids = adventure_time_videos.map(&:id) 78 | expect(mlp_season2.video_ids).to eq(expected_ids) 79 | end 80 | end 81 | 82 | describe "method <<" do 83 | let(:expected_videos) { [*my_cool_videos, escape_from_the_citadel] } 84 | let(:expected_video_ids) { expected_videos.map(&:id) } 85 | 86 | it "should respond to append method" do 87 | expect(my_cool_list.videos).to respond_to(:<<) 88 | end 89 | 90 | it "should reflect changes" do 91 | videos = my_cool_list.videos 92 | videos << escape_from_the_citadel 93 | expect(videos).to eq(expected_videos) 94 | end 95 | 96 | it "should reflect changes when loaded" do 97 | videos = my_cool_list.videos.load 98 | videos << escape_from_the_citadel 99 | expect(videos).to eq(expected_videos) 100 | end 101 | 102 | it "should modify to_sql" do 103 | videos = my_cool_list.videos 104 | expect(videos.to_sql).to have_sql_IN_stmt(my_cool_videos.map(&:id)) 105 | videos << escape_from_the_citadel 106 | expect(videos.to_sql).to have_sql_IN_stmt(expected_videos.map(&:id)) 107 | end 108 | 109 | it "should affect ids" do 110 | my_cool_list.videos << escape_from_the_citadel 111 | expect(my_cool_list.video_ids).to eq(expected_video_ids) 112 | end 113 | 114 | it "should reset loaded state" do 115 | videos = my_cool_list.videos.load 116 | expect(videos).to be_loaded 117 | videos << escape_from_the_citadel 118 | expect(videos).not_to be_loaded 119 | end 120 | 121 | it "should correctly append nil values" do 122 | videos = my_cool_list.videos 123 | videos << nil 124 | expect(my_cool_list.video_ids).to eq([*my_cool_video_ids, nil]) 125 | end 126 | end 127 | 128 | describe "method []=" do 129 | it "should respond to []=" do 130 | expect(my_cool_list.videos).to respond_to(:[]=) 131 | end 132 | 133 | describe "when access by integer index" do 134 | let(:expected_videos) { [escape_from_the_citadel, something_big] } 135 | let(:expected_video_ids) { expected_videos.map(&:id) } 136 | 137 | it "should reflect changes" do 138 | videos = my_cool_list.videos 139 | videos[0] = escape_from_the_citadel 140 | expect(videos).to eq(expected_videos) 141 | end 142 | 143 | it "should reflect changes when loaded" do 144 | videos = my_cool_list.videos.load 145 | videos[0] = escape_from_the_citadel 146 | expect(videos).to eq(expected_videos) 147 | end 148 | 149 | it "should affect ids" do 150 | my_cool_list.videos[0] = escape_from_the_citadel 151 | expect(my_cool_list.video_ids).to eq(expected_video_ids) 152 | end 153 | 154 | it "should modify to_sql" do 155 | videos = my_cool_list.videos 156 | expect(videos.to_sql).to have_sql_IN_stmt(my_cool_videos.map(&:id)) 157 | videos[0] = escape_from_the_citadel 158 | expect(videos.to_sql).to have_sql_IN_stmt(expected_videos.map(&:id)) 159 | end 160 | 161 | it "should reset loaded state" do 162 | videos = my_cool_list.videos.load 163 | expect(videos).to be_loaded 164 | videos[0] = escape_from_the_citadel 165 | expect(videos).not_to be_loaded 166 | end 167 | 168 | it "should correctly deal with nil values" do 169 | videos = my_cool_list.videos 170 | videos[0] = nil 171 | expect(my_cool_list.video_ids[0]).to be_nil 172 | end 173 | end 174 | 175 | describe "when access with start and length" do 176 | let!(:harlem_shake) { 177 | Video.create(title: "The Harlem Shake") 178 | } 179 | let!(:gangnam_style) { 180 | Video.create(title: "Gangnam Style") 181 | } 182 | let!(:spider_dog) { 183 | Video.create(title: "Mutant Giant Spider Dog") 184 | } 185 | let!(:chandelier) { 186 | Video.create(title: "Sia - Chandelier (Official Video)") 187 | } 188 | let!(:another_memes) { 189 | [harlem_shake, gangnam_style, spider_dog, chandelier] 190 | } 191 | let!(:another_meme_ids) { 192 | another_memes.map(&:id) 193 | } 194 | let!(:another_playlist) { 195 | Playlist.create(video_ids: another_meme_ids) 196 | } 197 | let(:expected_videos) { 198 | [harlem_shake, return_of_harmony, something_big, chandelier] 199 | } 200 | let(:expected_video_ids) { 201 | expected_videos.map(&:id) 202 | } 203 | 204 | it "should reflect changes" do 205 | videos = another_playlist.videos 206 | videos[1,2] = [return_of_harmony, something_big] 207 | expect(videos).to eq(expected_videos) 208 | end 209 | 210 | it "should reflect changes when loaded" do 211 | videos = another_playlist.videos.load 212 | videos[1,2] = [return_of_harmony, something_big] 213 | expect(videos).to eq(expected_videos) 214 | end 215 | 216 | it "should affect ids" do 217 | another_playlist.videos[1,2] = [return_of_harmony, something_big] 218 | expect(another_playlist.video_ids).to eq(expected_video_ids) 219 | end 220 | 221 | it "should modify to_sql" do 222 | videos = another_playlist.videos.load 223 | expect(videos.to_sql).to include("(#{another_meme_ids.join(', ')})") 224 | videos[1,2] = [return_of_harmony, something_big] 225 | expect(videos.to_sql).to include("(#{expected_video_ids.join(', ')})") 226 | end 227 | 228 | it "should reset loaded state" do 229 | videos = another_playlist.videos.load 230 | expect(videos).to be_loaded 231 | videos[1,2] = [return_of_harmony, something_big] 232 | expect(videos).not_to be_loaded 233 | end 234 | 235 | it "should correctly deal with nil values" do 236 | videos = another_playlist.videos 237 | videos[1,2] = [nil, nil, nil] 238 | expect(another_playlist.video_ids).to eq([harlem_shake.id, nil, nil, nil, chandelier.id]) 239 | end 240 | end 241 | end 242 | 243 | xdescribe "compact! method" do 244 | before { 245 | my_cool_list.videos << nil 246 | my_cool_list.save 247 | } 248 | let(:expected_videos) { [return_of_harmony, something_big] } 249 | let(:expected_video_ids) { expected_videos.map(&:id) } 250 | 251 | it "should respond to compact! method" do 252 | expect(my_cool_list.videos).to respond_to(:compact!) 253 | end 254 | 255 | it "should contain nil" do 256 | expect(my_cool_list.videos).to include(nil) 257 | expect(my_cool_list.videos.length).to eq(3) 258 | end 259 | 260 | it "should reflect changes" do 261 | videos = my_cool_list.videos 262 | videos.compact! 263 | expect(videos).to eq(expected_videos) 264 | end 265 | 266 | it "should reflect changes when loaded" do 267 | videos = my_cool_list.videos.load 268 | videos.compact! 269 | expect(videos).to eq(expected_videos) 270 | end 271 | 272 | it "should modify to_sql" do 273 | videos = my_cool_list.videos 274 | expect(videos.to_sql).to have_sql_IN_stmt(my_cool_videos.map(&:id)) 275 | videos.compact! 276 | expect(videos.to_sql).to have_sql_IN_stmt(expected_videos.map(&:id)) 277 | end 278 | 279 | it "should affect ids" do 280 | my_cool_list.videos.compact! 281 | expect(my_cool_list.video_ids).to eq(expected_video_ids) 282 | end 283 | 284 | it "should return self" do 285 | videos = my_cool_list.videos 286 | expect(videos.compact!).to eq(videos) 287 | end 288 | 289 | it "should reset loaded state" do 290 | videos = my_cool_list.videos.load 291 | expect(videos).to be_loaded 292 | videos.compact! 293 | expect(videos).not_to be_loaded 294 | end 295 | end 296 | 297 | describe "concat method" do 298 | let(:other_videos) { [escape_from_the_citadel, food_chain] } 299 | let(:expected_videos) { [return_of_harmony, something_big] + other_videos } 300 | let(:expected_video_ids) { expected_videos.map(&:id) } 301 | 302 | it "should respond to compact! method" do 303 | expect(my_cool_list.videos).to respond_to(:compact!) 304 | end 305 | 306 | it "should reflect changes" do 307 | videos = my_cool_list.videos 308 | videos.concat other_videos 309 | expect(videos).to eq(expected_videos) 310 | end 311 | 312 | it "should reflect changes when loaded" do 313 | videos = my_cool_list.videos.load 314 | videos.concat other_videos 315 | expect(videos).to eq(expected_videos) 316 | end 317 | 318 | it "should modify to_sql" do 319 | videos = my_cool_list.videos 320 | expect(videos.to_sql).to have_sql_IN_stmt(my_cool_videos.map(&:id)) 321 | videos.concat other_videos 322 | expect(videos.to_sql).to have_sql_IN_stmt(expected_videos.map(&:id)) 323 | end 324 | 325 | it "should affect ids" do 326 | my_cool_list.videos.concat other_videos 327 | expect(my_cool_list.video_ids).to eq(expected_video_ids) 328 | end 329 | 330 | it "should return self" do 331 | videos = my_cool_list.videos 332 | expect(videos.concat other_videos).to eq(videos) 333 | end 334 | 335 | it "should reset loaded state" do 336 | videos = my_cool_list.videos.load 337 | expect(videos).to be_loaded 338 | videos.concat other_videos 339 | expect(videos).not_to be_loaded 340 | end 341 | end 342 | 343 | describe "delete method" do 344 | let(:expected_videos) { [return_of_harmony] } 345 | let(:expected_video_ids) { expected_videos.map(&:id) } 346 | 347 | it "should respond to delete method" do 348 | expect(my_cool_list.videos).to respond_to(:delete) 349 | end 350 | 351 | it "should reflect changes" do 352 | videos = my_cool_list.videos 353 | videos.delete something_big 354 | expect(videos).to eq(expected_videos) 355 | end 356 | 357 | it "should reflect changes when loaded" do 358 | videos = my_cool_list.videos.load 359 | videos.delete something_big 360 | expect(videos).to eq(expected_videos) 361 | end 362 | 363 | it "should modify to_sql" do 364 | videos = my_cool_list.videos 365 | expect(videos.to_sql).to have_sql_IN_stmt(my_cool_videos.map(&:id)) 366 | videos.delete something_big 367 | expect(videos.to_sql).to have_sql_IN_stmt(expected_videos.map(&:id)) 368 | end 369 | 370 | it "should affect ids" do 371 | my_cool_list.videos.delete something_big 372 | expect(my_cool_list.video_ids).to eq(expected_video_ids) 373 | end 374 | 375 | it "should return deleted element" do 376 | videos = my_cool_list.videos 377 | expect(videos.delete something_big).to eq(something_big) 378 | end 379 | 380 | it "should return nil when deleting non-existing element" do 381 | videos = my_cool_list.videos 382 | expect(videos.delete escape_from_the_citadel).to be_nil 383 | end 384 | 385 | it "should reset loaded state" do 386 | videos = my_cool_list.videos.load 387 | expect(videos).to be_loaded 388 | videos.delete something_big 389 | expect(videos).not_to be_loaded 390 | end 391 | end 392 | 393 | describe "delete_at method" do 394 | let(:expected_videos) { [something_big] } 395 | let(:expected_video_ids) { expected_videos.map(&:id) } 396 | 397 | it "should respond to delete_at method" do 398 | expect(my_cool_list.videos).to respond_to(:delete_at) 399 | end 400 | 401 | it "should reflect changes" do 402 | videos = my_cool_list.videos 403 | videos.delete_at 0 404 | expect(videos).to eq(expected_videos) 405 | end 406 | 407 | it "should reflect changes when loaded" do 408 | videos = my_cool_list.videos.load 409 | videos.delete_at 0 410 | expect(videos).to eq(expected_videos) 411 | end 412 | 413 | it "should modify to_sql" do 414 | videos = my_cool_list.videos 415 | expect(videos.to_sql).to have_sql_IN_stmt(my_cool_videos.map(&:id)) 416 | videos.delete_at 0 417 | expect(videos.to_sql).to have_sql_IN_stmt(expected_videos.map(&:id)) 418 | end 419 | 420 | it "should affect ids" do 421 | my_cool_list.videos.delete_at 0 422 | expect(my_cool_list.video_ids).to eq(expected_video_ids) 423 | end 424 | 425 | it "should return deleted element" do 426 | videos = my_cool_list.videos 427 | expect(videos.delete_at 0).to eq(return_of_harmony) 428 | end 429 | 430 | it "should return nil when deleting non-existing element" do 431 | videos = my_cool_list.videos 432 | expect(videos.delete_at 2).to be_nil 433 | end 434 | 435 | it "should reset loaded state" do 436 | videos = my_cool_list.videos.load 437 | expect(videos).to be_loaded 438 | videos.delete_at 0 439 | expect(videos).not_to be_loaded 440 | end 441 | end 442 | 443 | describe "delete_if method" do 444 | let(:expected_videos) { [return_of_harmony] } 445 | let(:expected_video_ids) { expected_videos.map(&:id) } 446 | let(:block) { proc { |video| video.title.include? "Something" } } 447 | 448 | it "should respond to delete_if method" do 449 | expect(my_cool_list.videos).to respond_to(:delete_if) 450 | end 451 | 452 | it "should reflect changes" do 453 | videos = my_cool_list.videos 454 | videos.delete_if(&block) 455 | expect(videos).to eq(expected_videos) 456 | end 457 | 458 | it "should reflect changes when loaded" do 459 | videos = my_cool_list.videos.load 460 | videos.delete_if(&block) 461 | expect(videos).to eq(expected_videos) 462 | end 463 | 464 | it "should modify to_sql" do 465 | videos = my_cool_list.videos 466 | expect(videos.to_sql).to have_sql_IN_stmt(my_cool_videos.map(&:id)) 467 | videos.delete_if(&block) 468 | expect(videos.to_sql).to have_sql_IN_stmt(expected_videos.map(&:id)) 469 | end 470 | 471 | it "should affect ids" do 472 | my_cool_list.videos.delete_if(&block) 473 | expect(my_cool_list.video_ids).to eq(expected_video_ids) 474 | end 475 | 476 | it "should return self" do 477 | videos = my_cool_list.videos 478 | expect(videos.delete_if(&block)).to eq(videos) 479 | end 480 | 481 | it "should reset loaded state" do 482 | videos = my_cool_list.videos.load 483 | expect(videos).to be_loaded 484 | videos.delete_if(&block) 485 | expect(videos).not_to be_loaded 486 | end 487 | 488 | it "should return enumerator when calling without block" do 489 | expect(my_cool_list.videos.delete_if).to be_a(Enumerator) 490 | expect(my_cool_list.videos.delete_if.inspect).to include("delete_if") 491 | end 492 | end 493 | 494 | describe "fill method" do 495 | let(:expected_video_ids) { expected_videos.map(&:id) } 496 | 497 | it "should respond to fill method" do 498 | expect(my_cool_list.videos).to respond_to(:fill) 499 | end 500 | 501 | describe "when calling without block" do 502 | let(:expected_videos) { [food_chain, food_chain] } 503 | 504 | it "should reflect changes" do 505 | videos = my_cool_list.videos 506 | videos.fill food_chain 507 | expect(videos).to eq(expected_videos) 508 | end 509 | 510 | it "should reflect changes when loaded" do 511 | videos = my_cool_list.videos.load 512 | videos.fill food_chain 513 | expect(videos).to eq(expected_videos) 514 | end 515 | 516 | it "should modify to_sql" do 517 | videos = my_cool_list.videos 518 | expect(videos.to_sql).to have_sql_IN_stmt(my_cool_videos.map(&:id)) 519 | videos.fill food_chain 520 | expect(videos.to_sql).to have_sql_IN_stmt(expected_videos.map(&:id)) 521 | end 522 | 523 | it "should affect ids" do 524 | my_cool_list.videos.fill food_chain 525 | expect(my_cool_list.video_ids).to eq(expected_video_ids) 526 | end 527 | 528 | it "should return self" do 529 | videos = my_cool_list.videos 530 | expect(videos.fill food_chain).to eq(videos) 531 | end 532 | 533 | it "should reset loaded state" do 534 | videos = my_cool_list.videos.load 535 | expect(videos).to be_loaded 536 | videos.fill food_chain 537 | expect(videos).not_to be_loaded 538 | end 539 | 540 | describe "and with index and length or with range" do 541 | before(:each) do 542 | my_cool_list.videos.concat [escape_from_the_citadel, food_chain] 543 | my_cool_list.save 544 | end 545 | 546 | it "should reflect changes" do 547 | videos = my_cool_list.videos 548 | videos.fill food_chain, 2, 2 549 | expect(videos).to eq([return_of_harmony, something_big, food_chain, food_chain]) 550 | videos.fill escape_from_the_citadel, 0..1 551 | expect(videos).to eq([escape_from_the_citadel, escape_from_the_citadel, food_chain, food_chain]) 552 | videos.fill something_big, 1 553 | expect(videos).to eq([escape_from_the_citadel, something_big, something_big, something_big]) 554 | expect(videos.length).to eq(4) 555 | end 556 | 557 | it "should affect ids" do 558 | videos = my_cool_list.videos 559 | videos.fill food_chain, 2, 2 560 | expect(videos.map(&:id)).to eq([return_of_harmony, something_big, food_chain, food_chain].map(&:id)) 561 | videos.fill escape_from_the_citadel, 0..1 562 | expect(videos.map(&:id)).to eq([escape_from_the_citadel, escape_from_the_citadel, food_chain, food_chain].map(&:id)) 563 | videos.fill something_big, 1 564 | expect(videos.map(&:id)).to eq([escape_from_the_citadel, something_big, something_big, something_big].map(&:id)) 565 | end 566 | end 567 | end 568 | 569 | describe "when calling with block" do 570 | let(:other_videos) { [escape_from_the_citadel, food_chain] } 571 | let(:expected_videos) { other_videos } 572 | 573 | it "should reflect changes" do 574 | videos = my_cool_list.videos 575 | videos.fill { |i| other_videos[i] } 576 | expect(videos).to eq(expected_videos) 577 | end 578 | 579 | it "should reflect changes when loaded" do 580 | videos = my_cool_list.videos.load 581 | videos.fill { |i| other_videos[i] } 582 | expect(videos).to eq(expected_videos) 583 | end 584 | 585 | it "should modify to_sql" do 586 | videos = my_cool_list.videos 587 | expect(videos.to_sql).to have_sql_IN_stmt(my_cool_videos.map(&:id)) 588 | videos.fill { |i| other_videos[i] } 589 | expect(videos.to_sql).to have_sql_IN_stmt(expected_videos.map(&:id)) 590 | end 591 | 592 | it "should affect ids" do 593 | my_cool_list.videos.fill { |i| other_videos[i] } 594 | expect(my_cool_list.video_ids).to eq(expected_video_ids) 595 | end 596 | 597 | it "should return self" do 598 | videos = my_cool_list.videos 599 | expect(videos.fill { |i| other_videos[i] }).to eq(videos) 600 | end 601 | 602 | it "should reset loaded state" do 603 | videos = my_cool_list.videos.load 604 | expect(videos).to be_loaded 605 | videos.fill { |i| other_videos[i] } 606 | expect(videos).not_to be_loaded 607 | end 608 | 609 | describe "and with index and length or with range" do 610 | it "should reflect changes" do 611 | videos = my_cool_list.videos 612 | videos.fill(2, 2) { |i| other_videos[i-2] } 613 | expect(videos).to eq([return_of_harmony, something_big, escape_from_the_citadel, food_chain]) 614 | videos.fill(0..1) { |i| other_videos[i] } 615 | expect(videos).to eq([escape_from_the_citadel, food_chain, escape_from_the_citadel, food_chain]) 616 | end 617 | 618 | it "should affect ids" do 619 | videos = my_cool_list.videos 620 | videos.fill(2, 2) { |i| other_videos[i-2] } 621 | expect(videos.map(&:id)).to eq([return_of_harmony, something_big, escape_from_the_citadel, food_chain].map(&:id)) 622 | videos.fill(0..1) { |i| other_videos[i] } 623 | expect(videos.map(&:id)).to eq([escape_from_the_citadel, food_chain, escape_from_the_citadel, food_chain].map(&:id)) 624 | end 625 | end 626 | end 627 | end 628 | 629 | describe "insert method" do 630 | let(:other_videos) { [escape_from_the_citadel, food_chain] } 631 | let(:expected_videos) { [return_of_harmony, escape_from_the_citadel, food_chain, something_big] } 632 | let(:expected_video_ids) { expected_videos.map(&:id) } 633 | 634 | it "should respond to insert method" do 635 | expect(my_cool_list.videos).to respond_to(:insert) 636 | end 637 | 638 | it "should reflect changes" do 639 | videos = my_cool_list.videos 640 | videos.insert 1, *other_videos 641 | expect(videos).to eq(expected_videos) 642 | end 643 | 644 | it "should reflect changes when loaded" do 645 | videos = my_cool_list.videos.load 646 | videos.insert 1, *other_videos 647 | expect(videos).to eq(expected_videos) 648 | end 649 | 650 | it "should modify to_sql" do 651 | videos = my_cool_list.videos 652 | expect(videos.to_sql).to have_sql_IN_stmt(my_cool_videos.map(&:id)) 653 | videos.insert 1, *other_videos 654 | expect(videos.to_sql).to have_sql_IN_stmt(expected_videos.map(&:id)) 655 | end 656 | 657 | it "should affect ids" do 658 | my_cool_list.videos.insert 1, *other_videos 659 | expect(my_cool_list.video_ids).to eq(expected_video_ids) 660 | end 661 | 662 | it "should return self" do 663 | videos = my_cool_list.videos 664 | expect(videos.insert 1, *other_videos).to eq(videos) 665 | end 666 | 667 | it "should reset loaded state" do 668 | videos = my_cool_list.videos.load 669 | expect(videos).to be_loaded 670 | videos.insert 1, *other_videos 671 | expect(videos).not_to be_loaded 672 | end 673 | end 674 | 675 | describe "keep_if method" do 676 | let(:expected_videos) { [something_big] } 677 | let(:expected_video_ids) { expected_videos.map(&:id) } 678 | let(:block) { proc { |video| video.title.include? "Something" } } 679 | 680 | it "should respond to keep_if method" do 681 | expect(my_cool_list.videos).to respond_to(:keep_if) 682 | end 683 | 684 | it "should reflect changes" do 685 | videos = my_cool_list.videos 686 | videos.keep_if(&block) 687 | expect(videos).to eq(expected_videos) 688 | end 689 | 690 | it "should reflect changes when loaded" do 691 | videos = my_cool_list.videos.load 692 | videos.keep_if(&block) 693 | expect(videos).to eq(expected_videos) 694 | end 695 | 696 | it "should modify to_sql" do 697 | videos = my_cool_list.videos 698 | expect(videos.to_sql).to have_sql_IN_stmt(my_cool_videos.map(&:id)) 699 | videos.keep_if(&block) 700 | expect(videos.to_sql).to have_sql_IN_stmt(expected_videos.map(&:id)) 701 | end 702 | 703 | it "should affect ids" do 704 | my_cool_list.videos.keep_if(&block) 705 | expect(my_cool_list.video_ids).to eq(expected_video_ids) 706 | end 707 | 708 | it "should return self" do 709 | videos = my_cool_list.videos 710 | expect(videos.keep_if(&block)).to eq(videos) 711 | end 712 | 713 | it "should reset loaded state" do 714 | videos = my_cool_list.videos.load 715 | expect(videos).to be_loaded 716 | videos.keep_if(&block) 717 | expect(videos).not_to be_loaded 718 | end 719 | 720 | it "should return enumerator when calling without block" do 721 | expect(my_cool_list.videos.keep_if).to be_a(Enumerator) 722 | expect(my_cool_list.videos.keep_if.inspect).to include("keep_if") 723 | end 724 | end 725 | 726 | describe "map! method" do 727 | let(:expected_videos) { my_cool_videos.drop 1 } 728 | let(:expected_video_ids) { expected_videos.map(&:id) } 729 | 730 | it "should respond to map! method" do 731 | expect(my_cool_list.videos).to respond_to(:map!) 732 | end 733 | 734 | # TODO: in progress 735 | 736 | it "should return enumerator when calling without block" do 737 | expect(my_cool_list.videos.map!).to be_a(Enumerator) 738 | expect(my_cool_list.videos.map!.inspect).to include("map!") 739 | end 740 | end 741 | 742 | describe "pop method" do 743 | let(:expected_videos) { my_cool_videos.take(my_cool_videos.length-1) } 744 | let(:expected_video_ids) { expected_videos.map(&:id) } 745 | 746 | it "should respond to pop method" do 747 | expect(my_cool_list.videos).to respond_to(:pop) 748 | end 749 | 750 | it "should reflect changes" do 751 | videos = my_cool_list.videos 752 | videos.pop 753 | expect(videos).to eq(expected_videos) 754 | end 755 | 756 | it "should reflect changes when loaded" do 757 | videos = my_cool_list.videos.load 758 | videos.pop 759 | expect(videos).to eq(expected_videos) 760 | end 761 | 762 | it "should modify to_sql" do 763 | videos = my_cool_list.videos 764 | expect(videos.to_sql).to have_sql_IN_stmt(my_cool_videos.map(&:id)) 765 | videos.pop 766 | expect(videos.to_sql).to have_sql_IN_stmt(expected_videos.map(&:id)) 767 | end 768 | 769 | it "should affect ids" do 770 | my_cool_list.videos.pop 771 | expect(my_cool_list.video_ids).to eq(expected_video_ids) 772 | end 773 | 774 | it "should return popped element" do 775 | videos = my_cool_list.videos 776 | expect(videos.pop).to eq(something_big) 777 | end 778 | 779 | it "should reset loaded state" do 780 | videos = my_cool_list.videos.load 781 | expect(videos).to be_loaded 782 | videos.pop 783 | expect(videos).not_to be_loaded 784 | end 785 | end 786 | 787 | describe "push method" do 788 | let(:other_videos) { [escape_from_the_citadel, food_chain] } 789 | let(:expected_videos) { my_cool_videos + other_videos } 790 | let(:expected_video_ids) { expected_videos.map(&:id) } 791 | 792 | it "should respond to push method" do 793 | expect(my_cool_list.videos).to respond_to(:push) 794 | end 795 | 796 | it "should reflect changes" do 797 | videos = my_cool_list.videos 798 | videos.push(*other_videos) 799 | expect(videos).to eq(expected_videos) 800 | end 801 | 802 | it "should reflect changes when loaded" do 803 | videos = my_cool_list.videos.load 804 | videos.push(*other_videos) 805 | expect(videos).to eq(expected_videos) 806 | end 807 | 808 | it "should modify to_sql" do 809 | videos = my_cool_list.videos 810 | expect(videos.to_sql).to have_sql_IN_stmt(my_cool_list.videos.map(&:id)) 811 | videos.push(*other_videos) 812 | expect(videos.to_sql).to have_sql_IN_stmt(expected_videos.map(&:id)) 813 | end 814 | 815 | it "should affect ids" do 816 | my_cool_list.videos.push(*other_videos) 817 | expect(my_cool_list.video_ids).to eq(expected_video_ids) 818 | end 819 | 820 | it "should return self" do 821 | videos = my_cool_list.videos 822 | expect(videos.push(*other_videos)).to eq(videos) 823 | end 824 | 825 | it "should reset loaded state" do 826 | videos = my_cool_list.videos.load 827 | expect(videos).to be_loaded 828 | videos.push(*other_videos) 829 | expect(videos).not_to be_loaded 830 | end 831 | end 832 | 833 | describe "reject! method" do 834 | let(:block) { proc { |video| video.title.include? "Something" } } 835 | 836 | it "should respond to reject! method" do 837 | expect(my_cool_list.videos).to respond_to(:reject!) 838 | end 839 | 840 | it "should return self if changes are made" do 841 | expect(my_cool_list.videos.reject!(&block)).to eq(my_cool_list.videos) 842 | end 843 | 844 | it "should return nil if changes are not made" do 845 | expect(my_cool_list.videos.reject!{ false }).to be_nil 846 | end 847 | 848 | it "should return enumerator when calling without block" do 849 | expect(my_cool_list.videos.reject!).to be_a(Enumerator) 850 | expect(my_cool_list.videos.reject!.inspect).to include("reject!") 851 | end 852 | end 853 | 854 | describe "replace method" do 855 | let(:other_videos) { [escape_from_the_citadel, food_chain] } 856 | let(:expected_videos) { other_videos } 857 | let(:expected_video_ids) { expected_videos.map(&:id) } 858 | 859 | it "should respond to replace method" do 860 | expect(my_cool_list.videos).to respond_to(:replace) 861 | end 862 | 863 | it "should reflect changes" do 864 | videos = my_cool_list.videos 865 | videos.replace other_videos 866 | expect(videos).to eq(expected_videos) 867 | end 868 | 869 | it "should reflect changes when loaded" do 870 | videos = my_cool_list.videos.load 871 | videos.replace other_videos 872 | expect(videos).to eq(expected_videos) 873 | end 874 | 875 | it "should modify to_sql" do 876 | videos = my_cool_list.videos 877 | expect(videos.to_sql).to have_sql_IN_stmt(my_cool_list.videos.map(&:id)) 878 | videos.replace other_videos 879 | expect(videos.to_sql).to have_sql_IN_stmt(expected_videos.map(&:id)) 880 | end 881 | 882 | it "should affect ids" do 883 | my_cool_list.videos.replace other_videos 884 | expect(my_cool_list.video_ids).to eq(expected_video_ids) 885 | end 886 | 887 | it "should return self" do 888 | videos = my_cool_list.videos 889 | expect(videos.replace other_videos).to eq(videos) 890 | end 891 | 892 | it "should reset loaded state" do 893 | videos = my_cool_list.videos.load 894 | expect(videos).to be_loaded 895 | videos.replace other_videos 896 | expect(videos).not_to be_loaded 897 | end 898 | end 899 | 900 | describe "reverse! method" do 901 | let(:expected_videos) { [something_big, return_of_harmony] } 902 | let(:expected_video_ids) { expected_videos.map(&:id) } 903 | 904 | it "should respond to reverse! method" do 905 | expect(my_cool_list.videos).to respond_to(:reverse!) 906 | end 907 | 908 | it "should reflect changes" do 909 | videos = my_cool_list.videos 910 | videos.reverse! 911 | expect(videos).to eq(expected_videos) 912 | end 913 | 914 | it "should reflect changes when loaded" do 915 | videos = my_cool_list.videos.load 916 | videos.reverse! 917 | expect(videos).to eq(expected_videos) 918 | end 919 | 920 | it "should modify to_sql" do 921 | videos = my_cool_list.videos 922 | expect(videos.to_sql).to have_sql_IN_stmt(my_cool_list.videos.map(&:id)) 923 | videos.reverse! 924 | expect(videos.to_sql).to have_sql_IN_stmt(expected_videos.map(&:id)) 925 | end 926 | 927 | it "should affect ids" do 928 | my_cool_list.videos.reverse! 929 | expect(my_cool_list.video_ids).to eq(expected_video_ids) 930 | end 931 | 932 | it "should return self" do 933 | videos = my_cool_list.videos 934 | expect(videos.reverse!).to eq(videos) 935 | end 936 | 937 | it "should reset loaded state" do 938 | videos = my_cool_list.videos.load 939 | expect(videos).to be_loaded 940 | videos.reverse! 941 | expect(videos).not_to be_loaded 942 | end 943 | end 944 | 945 | describe "rotate! method" do 946 | before do 947 | my_cool_list.videos << food_chain 948 | my_cool_list.save 949 | end 950 | let(:expected_video_ids) { expected_videos.map(&:id) } 951 | 952 | it "should respond to rotate! method" do 953 | expect(my_cool_list.videos).to respond_to(:rotate!) 954 | end 955 | 956 | describe "with count=1" do 957 | let(:expected_videos) { [something_big, food_chain, return_of_harmony] } 958 | 959 | it "should respond to rotate! method" do 960 | expect(my_cool_list.videos).to respond_to(:rotate!) 961 | end 962 | 963 | it "should reflect changes" do 964 | videos = my_cool_list.videos 965 | videos.rotate! 966 | expect(videos).to eq(expected_videos) 967 | end 968 | 969 | it "should reflect changes when loaded" do 970 | videos = my_cool_list.videos.load 971 | videos.rotate! 972 | expect(videos).to eq(expected_videos) 973 | end 974 | 975 | it "should modify to_sql" do 976 | videos = my_cool_list.videos 977 | expect(videos.to_sql).to have_sql_IN_stmt(my_cool_list.videos.map(&:id)) 978 | videos.rotate! 979 | expect(videos.to_sql).to have_sql_IN_stmt(expected_videos.map(&:id)) 980 | end 981 | 982 | it "should affect ids" do 983 | my_cool_list.videos.rotate! 984 | expect(my_cool_list.video_ids).to eq(expected_video_ids) 985 | end 986 | 987 | it "should return self" do 988 | videos = my_cool_list.videos 989 | expect(videos.rotate!).to eq(videos) 990 | end 991 | 992 | it "should reset loaded state" do 993 | videos = my_cool_list.videos.load 994 | expect(videos).to be_loaded 995 | videos.rotate! 996 | expect(videos).not_to be_loaded 997 | end 998 | end 999 | 1000 | describe "with count=2" do 1001 | let(:expected_videos) { [food_chain, return_of_harmony, something_big] } 1002 | 1003 | it "should reflect changes" do 1004 | videos = my_cool_list.videos 1005 | videos.rotate! 2 1006 | expect(videos).to eq(expected_videos) 1007 | end 1008 | 1009 | it "should reflect changes when loaded" do 1010 | videos = my_cool_list.videos.load 1011 | videos.rotate! 2 1012 | expect(videos).to eq(expected_videos) 1013 | end 1014 | 1015 | it "should modify to_sql" do 1016 | videos = my_cool_list.videos 1017 | expect(videos.to_sql).to have_sql_IN_stmt(my_cool_list.videos.map(&:id)) 1018 | videos.rotate! 2 1019 | expect(videos.to_sql).to have_sql_IN_stmt(expected_videos.map(&:id)) 1020 | end 1021 | 1022 | it "should affect ids" do 1023 | my_cool_list.videos.rotate! 2 1024 | expect(my_cool_list.video_ids).to eq(expected_video_ids) 1025 | end 1026 | 1027 | it "should return self" do 1028 | videos = my_cool_list.videos 1029 | expect(videos.rotate! 2).to eq(videos) 1030 | end 1031 | 1032 | it "should reset loaded state" do 1033 | videos = my_cool_list.videos.load 1034 | expect(videos).to be_loaded 1035 | videos.rotate! 2 1036 | expect(videos).not_to be_loaded 1037 | end 1038 | end 1039 | end 1040 | 1041 | describe "select! method" do 1042 | let(:block) { proc { |video| video.title.include? "Something" } } 1043 | 1044 | it "should respond to select! method" do 1045 | expect(my_cool_list.videos).to respond_to(:select!) 1046 | end 1047 | 1048 | it "should return self if changes are made" do 1049 | expect(my_cool_list.videos.select!(&block)).to eq(my_cool_list.videos) 1050 | end 1051 | 1052 | it "should return nil if changes are not made" do 1053 | expect(my_cool_list.videos.select!{ true }).to be_nil 1054 | end 1055 | 1056 | it "should return enumerator when calling without block" do 1057 | expect(my_cool_list.videos.select!).to be_a(Enumerator) 1058 | expect(my_cool_list.videos.select!.inspect).to include("select!") 1059 | end 1060 | end 1061 | 1062 | describe "shift method" do 1063 | let(:expected_videos) { my_cool_videos.drop 1 } 1064 | let(:expected_video_ids) { expected_videos.map(&:id) } 1065 | 1066 | it "should respond to shift method" do 1067 | expect(my_cool_list.videos).to respond_to(:shift) 1068 | end 1069 | 1070 | it "should reflect changes" do 1071 | videos = my_cool_list.videos 1072 | videos.shift 1073 | expect(videos).to eq(expected_videos) 1074 | end 1075 | 1076 | it "should reflect changes when loaded" do 1077 | videos = my_cool_list.videos.load 1078 | videos.shift 1079 | expect(videos).to eq(expected_videos) 1080 | end 1081 | 1082 | it "should modify to_sql" do 1083 | videos = my_cool_list.videos 1084 | expect(videos.to_sql).to have_sql_IN_stmt(my_cool_videos.map(&:id)) 1085 | videos.shift 1086 | expect(videos.to_sql).to have_sql_IN_stmt(expected_videos.map(&:id)) 1087 | end 1088 | 1089 | it "should affect ids" do 1090 | my_cool_list.videos.shift 1091 | expect(my_cool_list.video_ids).to eq(expected_video_ids) 1092 | end 1093 | 1094 | it "should return shifted element" do 1095 | videos = my_cool_list.videos 1096 | expect(videos.shift).to eq(return_of_harmony) 1097 | end 1098 | 1099 | it "should reset loaded state" do 1100 | videos = my_cool_list.videos.load 1101 | expect(videos).to be_loaded 1102 | videos.shift 1103 | expect(videos).not_to be_loaded 1104 | end 1105 | end 1106 | 1107 | describe "shuffle! method" do 1108 | before do 1109 | my_cool_list.videos << food_chain 1110 | my_cool_list.save 1111 | end 1112 | 1113 | it "should respond to shuffle! method" do 1114 | expect(my_cool_list.videos).to respond_to(:shuffle!) 1115 | end 1116 | 1117 | it "should reflect changes" do 1118 | is_changed = false 1119 | 10.times do 1120 | old_videos = my_cool_list.videos.to_a 1121 | my_cool_list.videos.shuffle! 1122 | expect(my_cool_list.videos).to contain_exactly(something_big, return_of_harmony, food_chain) 1123 | is_changed ||= old_videos != my_cool_list.videos.to_a 1124 | end 1125 | expect(is_changed).to eq(true) 1126 | end 1127 | 1128 | it "should affect ids" do 1129 | 10.times do 1130 | old_videos = my_cool_list.videos.to_a 1131 | my_cool_list.videos.shuffle! 1132 | expect(my_cool_list.videos.map(&:id)).to contain_exactly(*[something_big, return_of_harmony, food_chain].map(&:id)) 1133 | is_changed ||= old_videos == my_cool_list.videos 1134 | end 1135 | end 1136 | 1137 | it "should return self" do 1138 | expect(my_cool_list.videos.shuffle!).to eq(my_cool_list.videos) 1139 | end 1140 | end 1141 | 1142 | describe "uniq! method" do 1143 | let(:expected_videos) { [return_of_harmony, something_big, food_chain] } 1144 | let(:expected_video_ids) { expected_videos.map(&:id) } 1145 | 1146 | before do 1147 | my_cool_list.videos << something_big << food_chain 1148 | end 1149 | 1150 | it "should respond to uniq! method" do 1151 | expect(my_cool_list.videos).to respond_to(:uniq!) 1152 | end 1153 | 1154 | describe "when calling without block" do 1155 | it "should reflect changes" do 1156 | videos = my_cool_list.videos 1157 | videos.uniq! 1158 | expect(videos).to eq(expected_videos) 1159 | end 1160 | 1161 | it "should reflect changes when loaded" do 1162 | videos = my_cool_list.videos.load 1163 | videos.uniq! 1164 | expect(videos).to eq(expected_videos) 1165 | end 1166 | 1167 | it "should modify to_sql" do 1168 | videos = my_cool_list.videos 1169 | expect(videos.to_sql).to have_sql_IN_stmt(my_cool_list.videos.map(&:id)) 1170 | videos.uniq! 1171 | expect(videos.to_sql).to have_sql_IN_stmt(expected_videos.map(&:id)) 1172 | end 1173 | 1174 | it "should affect ids" do 1175 | my_cool_list.videos.uniq! 1176 | expect(my_cool_list.video_ids).to eq(expected_video_ids) 1177 | end 1178 | 1179 | it "should return self" do 1180 | videos = my_cool_list.videos 1181 | expect(videos.uniq!).to eq(videos) 1182 | end 1183 | 1184 | it "should reset loaded state" do 1185 | videos = my_cool_list.videos.load 1186 | expect(videos).to be_loaded 1187 | videos.uniq! 1188 | expect(videos).not_to be_loaded 1189 | end 1190 | end 1191 | 1192 | describe "when calling with block" do 1193 | let(:block) do 1194 | proc { |video| video.id % 2 } 1195 | end 1196 | 1197 | it "should reflect changes" do 1198 | my_cool_list.videos.uniq!(&block) 1199 | expect(my_cool_list.videos).to eq([return_of_harmony, something_big]) 1200 | end 1201 | 1202 | it "should affect ids" do 1203 | my_cool_list.videos.uniq!(&block) 1204 | expect(my_cool_list.videos.map(&:id)).to eq([return_of_harmony, something_big].map(&:id)) 1205 | end 1206 | end 1207 | end 1208 | 1209 | describe "unshift method" do 1210 | let(:other_videos) { [escape_from_the_citadel, food_chain] } 1211 | let(:expected_videos) { other_videos + my_cool_videos } 1212 | let(:expected_video_ids) { expected_videos.map(&:id) } 1213 | 1214 | it "should respond to unshift method" do 1215 | expect(my_cool_list.videos).to respond_to(:unshift) 1216 | end 1217 | 1218 | it "should reflect changes" do 1219 | videos = my_cool_list.videos 1220 | videos.unshift(*other_videos) 1221 | expect(videos).to eq(expected_videos) 1222 | end 1223 | 1224 | it "should reflect changes when loaded" do 1225 | videos = my_cool_list.videos.load 1226 | videos.unshift(*other_videos) 1227 | expect(videos).to eq(expected_videos) 1228 | end 1229 | 1230 | it "should modify to_sql" do 1231 | videos = my_cool_list.videos 1232 | expect(videos.to_sql).to have_sql_IN_stmt(my_cool_list.videos.map(&:id)) 1233 | videos.unshift(*other_videos) 1234 | expect(videos.to_sql).to have_sql_IN_stmt(expected_videos.map(&:id)) 1235 | end 1236 | 1237 | it "should affect ids" do 1238 | my_cool_list.videos.unshift(*other_videos) 1239 | expect(my_cool_list.video_ids).to eq(expected_video_ids) 1240 | end 1241 | 1242 | it "should return self" do 1243 | videos = my_cool_list.videos 1244 | expect(videos.unshift(*other_videos)).to eq(videos) 1245 | end 1246 | 1247 | it "should reset loaded state" do 1248 | videos = my_cool_list.videos.load 1249 | expect(videos).to be_loaded 1250 | videos.unshift(*other_videos) 1251 | expect(videos).not_to be_loaded 1252 | end 1253 | end 1254 | end 1255 | -------------------------------------------------------------------------------- /spec/has_array_of/associated_array_spec.rb: -------------------------------------------------------------------------------- 1 | 2 | RSpec.describe HasArrayOf::AssociatedArray do 3 | include_context "Video model" 4 | include_context "Playlist model" 5 | include_context "TV series" 6 | 7 | describe "associated collection assigner" do 8 | it "should respond to assignment method" do 9 | expect(my_cool_list).to respond_to(:videos=) 10 | end 11 | 12 | it "should reflect changes" do 13 | mlp_season2.videos = adventure_time_videos 14 | expect(mlp_season2.videos).to eq(adventure_time_videos) 15 | end 16 | 17 | it "should affect ids" do 18 | mlp_season2.videos = adventure_time_videos 19 | expected_ids = adventure_time_videos.map(&:id) 20 | expect(mlp_season2.video_ids).to eq(expected_ids) 21 | end 22 | end 23 | 24 | describe "`containing` scope" do 25 | it "should respond to scope method" do 26 | expect(Playlist).to respond_to(:with_videos_containing) 27 | end 28 | 29 | it "should fetch correct results" do 30 | expect(Playlist.with_videos_containing(something_big)) 31 | .to contain_exactly(adventure_time_season6, 32 | my_cool_list) 33 | 34 | expect(Playlist.with_videos_containing(return_of_harmony)) 35 | .to contain_exactly(mlp_season2, 36 | my_cool_list) 37 | 38 | expect(Playlist.with_videos_containing(something_big, escape_from_the_citadel)) 39 | .to contain_exactly(adventure_time_season6) 40 | 41 | expect(Playlist.with_videos_containing(food_chain)).to eq([]) 42 | end 43 | 44 | describe "when passing relation as an argument" do 45 | it "should fetch correct results" do 46 | relation = Video.where("title like ? or title like ?", '%Citadel%', '%Something%') 47 | expect(Playlist.with_videos_containing(relation)).to contain_exactly(adventure_time_season6) 48 | end 49 | end 50 | 51 | describe "when passing empty array" do 52 | it "should fetch all entries" do 53 | expect(Playlist.with_videos_containing([])).to contain_exactly(*Playlist.all) 54 | end 55 | end 56 | end 57 | 58 | describe "`contained in` scope" do 59 | it "should respond to scope method" do 60 | expect(Playlist).to respond_to(:with_videos_contained_in) 61 | end 62 | 63 | it "should fetch correct results" do 64 | expect(Playlist.with_videos_contained_in(something_big, return_of_harmony, escape_from_the_citadel)) 65 | .to contain_exactly(adventure_time_season6, 66 | mlp_season2, 67 | my_cool_list) 68 | 69 | expect(Playlist.with_videos_contained_in(food_chain, return_of_harmony)) 70 | .to contain_exactly(mlp_season2) 71 | 72 | expect(Playlist.with_videos_contained_in(return_of_harmony, something_big, food_chain)) 73 | .to contain_exactly(mlp_season2, 74 | my_cool_list) 75 | 76 | expect(Playlist.with_videos_contained_in(food_chain)).to eq([]) 77 | end 78 | 79 | describe "when passing empty array" do 80 | it "should fetch nothing" do 81 | expect(Playlist.with_videos_contained_in([])).to eq [] 82 | end 83 | end 84 | end 85 | 86 | describe "`with any from` scope" do 87 | it "should respond to scope method" do 88 | expect(Playlist).to respond_to(:with_any_video_from) 89 | end 90 | 91 | it "should fetch correct results" do 92 | expect(Playlist.with_any_video_from(return_of_harmony)).to contain_exactly(my_cool_list, 93 | mlp_season2) 94 | 95 | expect(Playlist.with_any_video_from(something_big)).to contain_exactly(my_cool_list, 96 | adventure_time_season6) 97 | 98 | expect(Playlist.with_any_video_from(food_chain)).to eq([]) 99 | end 100 | 101 | describe "when passing empty array" do 102 | it "should fetch nothing" do 103 | expect(Playlist.with_any_video_from([])).to eq [] 104 | end 105 | end 106 | end 107 | end 108 | -------------------------------------------------------------------------------- /spec/has_array_of/associated_belongs_spec.rb: -------------------------------------------------------------------------------- 1 | 2 | RSpec.describe HasArrayOf::AssociatedBelongs do 3 | include_context "Video model belonging to Playlist" 4 | include_context "Playlist model" 5 | include_context "TV series" 6 | 7 | it "should respond to association method" do 8 | expect(return_of_harmony).to respond_to(:playlists) 9 | end 10 | 11 | it "should fetch correct results" do 12 | expect(return_of_harmony.playlists).to contain_exactly(mlp_season2, my_cool_list) 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /spec/has_array_of/builders_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe HasArrayOf::Builders do 4 | describe ActiveRecord::Base do 5 | subject { described_class } 6 | it { should respond_to(:has_array_of) } 7 | it { should respond_to(:belongs_to_array_in_many) } 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'bundler/setup' 2 | require 'rails' 3 | require 'database_cleaner' 4 | require 'has_array_of' 5 | 6 | Dir[File.join(__dir__, 'support/**/*.rb')].each { |f| require f } 7 | 8 | RSpec.configure do |config| 9 | config.expect_with :rspec do |expectations| 10 | expectations.include_chain_clauses_in_custom_matcher_descriptions = true 11 | end 12 | 13 | config.mock_with :rspec do |mocks| 14 | mocks.verify_partial_doubles = true 15 | end 16 | 17 | config.filter_run :focus 18 | config.run_all_when_everything_filtered = true 19 | 20 | config.disable_monkey_patching! 21 | 22 | if config.files_to_run.one? 23 | config.default_formatter = 'doc' 24 | end 25 | 26 | #config.profile_examples = 10 27 | 28 | config.order = :random 29 | 30 | Kernel.srand config.seed 31 | 32 | config.before(:suite) do 33 | DatabaseCleaner.strategy = :transaction 34 | DatabaseCleaner.clean_with(:truncation) 35 | end 36 | 37 | config.around(:each) do |example| 38 | DatabaseCleaner.cleaning do 39 | example.run 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /spec/support/contexts.rb: -------------------------------------------------------------------------------- 1 | RSpec.shared_context "Video model" do 2 | with_model :Video do 3 | table do |t| 4 | t.text :title 5 | end 6 | end 7 | end 8 | 9 | RSpec.shared_context "Playlist model" do 10 | with_model :Playlist do 11 | table do |t| 12 | t.integer :video_ids, array: true, default: [] 13 | end 14 | 15 | model do 16 | has_array_of :videos 17 | end 18 | end 19 | end 20 | 21 | RSpec.shared_context "Video model belonging to Playlist" do 22 | with_model :Video do 23 | table do |t| 24 | t.text :title 25 | end 26 | 27 | model do 28 | belongs_to_array_in_many :playlists 29 | end 30 | end 31 | end 32 | 33 | RSpec.shared_context "TV series" do 34 | let!(:return_of_harmony) { 35 | Video.create(title: "My Little Pony s02e01 'The Return of Harmony'") # id=1 36 | } 37 | let!(:something_big) { 38 | Video.create(title: "Adventure Time s06e10 'Something Big'") # id=2 39 | } 40 | let!(:escape_from_the_citadel) { 41 | Video.create(title: "Adventure Time s06e02 'Escape from the Citadel'") 42 | } 43 | let!(:food_chain) { 44 | Video.create(title: "Adventure Time s06e07 'Food Chain'") 45 | } 46 | 47 | let!(:adventure_time_videos) { [something_big, escape_from_the_citadel] } 48 | let!(:adventure_time_season6) { 49 | Playlist.create(video_ids: adventure_time_videos.map(&:id)) 50 | } 51 | 52 | let!(:mlp_videos) { [return_of_harmony] } 53 | let!(:mlp_season2) { 54 | Playlist.create(video_ids: mlp_videos.map(&:id)) 55 | } 56 | 57 | let!(:my_cool_videos) { 58 | [return_of_harmony, something_big] 59 | } 60 | let!(:my_cool_video_ids) { my_cool_videos.map(&:id) } 61 | let!(:my_cool_list) { 62 | Playlist.create(video_ids: my_cool_video_ids.dup) 63 | } 64 | end 65 | -------------------------------------------------------------------------------- /spec/support/db.rb: -------------------------------------------------------------------------------- 1 | require 'active_record' 2 | 3 | ActiveRecord::Base.establish_connection(adapter: 'postgresql', 4 | database: 'has_array_of_test', 5 | host: ENV['POSTGRES_HOST'], 6 | port: ENV['POSTGRES_PORT'], 7 | min_messages: 'warning') 8 | -------------------------------------------------------------------------------- /spec/support/matchers.rb: -------------------------------------------------------------------------------- 1 | require 'rspec/expectations' 2 | 3 | RSpec::Matchers.define :have_sql_IN_stmt do |expected| 4 | if expected.empty? 5 | raise NotImplementedError 6 | elsif expected.one? 7 | match do |actual| 8 | actual.include? "= #{expected.first}" 9 | end 10 | elsif expected.many? 11 | match do |actual| 12 | actual.include? "IN (#{expected.join(', ')})" 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /spec/support/with_model.rb: -------------------------------------------------------------------------------- 1 | require "with_model" 2 | 3 | RSpec.configure do |config| 4 | config.extend WithModel 5 | end 6 | --------------------------------------------------------------------------------