├── vendor └── .keep ├── .coveralls.yml ├── Rakefile ├── lib ├── kakurenbo-puti.rb ├── kakurenbo_puti │ ├── version.rb │ └── active_record_base.rb └── kakurenbo_puti.rb ├── Gemfile ├── .gitignore ├── .travis.yml ├── spec ├── spec_helpers │ ├── active_record_model_spec.rb │ └── active_record_model.rb ├── spec_helper.rb └── kakurenbo_puti │ ├── situations │ └── deep_nest_spec.rb │ └── active_record_base_spec.rb ├── LICENSE.txt ├── kakurenbo-puti.gemspec └── README.md /vendor/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.coveralls.yml: -------------------------------------------------------------------------------- 1 | service_name: travis-ci 2 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | 3 | -------------------------------------------------------------------------------- /lib/kakurenbo-puti.rb: -------------------------------------------------------------------------------- 1 | require 'kakurenbo_puti' 2 | -------------------------------------------------------------------------------- /lib/kakurenbo_puti/version.rb: -------------------------------------------------------------------------------- 1 | module KakurenboPuti 2 | VERSION = '0.5.0' 3 | end 4 | -------------------------------------------------------------------------------- /lib/kakurenbo_puti.rb: -------------------------------------------------------------------------------- 1 | require 'active_record' 2 | require 'kakurenbo_puti/version' 3 | require 'kakurenbo_puti/active_record_base' 4 | 5 | ActiveRecord::Base.send :extend, KakurenboPuti::ActiveRecordBase 6 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | if ENV['ACTIVE_RECORD_VERSION'] 4 | gem 'activerecord', ENV['ACTIVE_RECORD_VERSION'] 5 | end 6 | 7 | # Specify your gem's dependencies in kakurenbo-puti.gemspec 8 | gemspec 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Test report files. 2 | /spec/reports/ 3 | /coverage/ 4 | 5 | # Documentation files. 6 | /doc/ 7 | /_yardoc/ 8 | /.yardoc 9 | 10 | # Temporary files. 11 | /.bundle/ 12 | /Gemfile.lock 13 | /pkg/ 14 | /tmp/ 15 | *.bundle 16 | *.so 17 | *.o 18 | *.a 19 | mkmf.log 20 | 21 | # IDE files. 22 | .idea/ 23 | 24 | # bundler directory. 25 | vendor/bundle/ 26 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | script: bundle exec rspec 3 | before_install: 4 | - gem install bundler 5 | rvm: 6 | - 2.2.0 7 | - 2.3.0 8 | - 2.4.0 9 | env: 10 | - ACTIVE_RECORD_VERSION='~> 4.2.0' 11 | - ACTIVE_RECORD_VERSION='~> 5.0.0' 12 | - ACTIVE_RECORD_VERSION='~> 5.1.0' 13 | matrix: 14 | exclude: 15 | - rvm: 2.2.0 16 | env: ACTIVE_RECORD_VERSION='~> 5.0.0' 17 | - rvm: 2.2.0 18 | env: ACTIVE_RECORD_VERSION='~> 5.1.0' 19 | -------------------------------------------------------------------------------- /spec/spec_helpers/active_record_model_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe '#define_active_record_model' do 4 | define_active_record_model :Drink do |t| 5 | t.integer :price 6 | end 7 | 8 | it 'created Drink class.' do 9 | expect(Object.const_defined? :Drink).to be_truthy 10 | end 11 | 12 | describe 'created class' do 13 | it 'has methods of ActiveRecord.' do 14 | expect(Drink).to respond_to(:create, :find, :where, :update_all) 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'bundler/setup' 3 | require 'spec_helpers/active_record_model' 4 | require 'coveralls' 5 | require 'kakurenbo-puti' 6 | 7 | RSpec.configure do |config| 8 | config.color = true 9 | config.mock_framework = :rspec 10 | config.before :all do 11 | ActiveRecord::Base.logger = Logger.new(STDOUT).tap { |logger| logger.level = Logger::WARN } 12 | ActiveRecord::Base.establish_connection(:adapter => 'sqlite3', :database => ':memory:') 13 | end 14 | end 15 | 16 | Coveralls.wear! 17 | -------------------------------------------------------------------------------- /spec/spec_helpers/active_record_model.rb: -------------------------------------------------------------------------------- 1 | require 'active_support' 2 | 3 | # Use ActiveSupport::Inflector#classify 4 | Inflector = Class.new.extend(ActiveSupport::Inflector) 5 | 6 | # Define model of ActiveRecord. 7 | # 8 | # @param [Symbol] Model name. 9 | # @yield [table] Table column definition. 10 | def define_active_record_model(model_name, &block) 11 | raise 'column definition block is nothing!' unless block_given? 12 | 13 | tableize_name = Inflector.tableize(model_name) 14 | 15 | before :each do 16 | migration = ActiveRecord::Migration.new 17 | migration.verbose = false 18 | migration.create_table tableize_name, &block 19 | 20 | mock_class = Class.new(ActiveRecord::Base) do 21 | define_singleton_method(:name) { model_name.to_s } 22 | reset_table_name 23 | end 24 | 25 | Object.const_set model_name, mock_class 26 | end 27 | 28 | after :each do 29 | migration = ActiveRecord::Migration.new 30 | migration.verbose = false 31 | migration.drop_table tableize_name 32 | 33 | Object.class_eval { remove_const model_name } 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 alfa-jpn 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /kakurenbo-puti.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'kakurenbo_puti/version' 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = 'kakurenbo-puti' 8 | spec.version = KakurenboPuti::VERSION 9 | spec.authors = ['alfa-jpn'] 10 | spec.email = ['a.nkmr.ja@gmail.com'] 11 | spec.homepage = 'https://github.com/alfa-jpn/kakurenbo-puti' 12 | spec.license = 'MIT' 13 | spec.summary = <<-EOF 14 | Lightweight soft-delete gem. 15 | EOF 16 | spec.description = <<-EOF 17 | kakurenbo-puti provides an ActiveRecord-friendly soft-delete gem. 18 | This gem does not override methods of ActiveRecord. 19 | 20 | I think that kakurenbo-puti is cho-iketeru! (very cool!) 21 | EOF 22 | 23 | spec.files = `git ls-files -z`.split("\x0") 24 | spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } 25 | spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) 26 | spec.require_paths = ['lib'] 27 | 28 | spec.required_ruby_version = '>= 2.0' 29 | 30 | spec.add_dependency 'activerecord', '>= 4.1.0' 31 | 32 | spec.add_development_dependency 'bundler', '~> 1.7' 33 | spec.add_development_dependency 'rake', '~> 10.0' 34 | spec.add_development_dependency 'rspec', '~> 3.0.0' 35 | spec.add_development_dependency 'yard', '~> 0.9' 36 | spec.add_development_dependency 'sqlite3', '~> 1.3.10' 37 | spec.add_development_dependency 'coveralls', '~> 0.7.8' 38 | end 39 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Kakurenbo\-Puti 2 | kakurenbo-puti provides an ActiveRecord-friendly soft-delete gem. 3 | This gem does not override methods of ActiveRecord. 4 | 5 | I think that kakurenbo-puti is cho-iketeru! (very cool!) 6 | 7 | [![Build Status](https://travis-ci.org/alfa-jpn/kakurenbo-puti.svg?branch=master)](https://travis-ci.org/alfa-jpn/kakurenbo-puti) 8 | [![Coverage Status](https://coveralls.io/repos/alfa-jpn/kakurenbo-puti/badge.svg)](https://coveralls.io/r/alfa-jpn/kakurenbo-puti) 9 | [![Code Climate](https://codeclimate.com/github/alfa-jpn/kakurenbo-puti/badges/gpa.svg)](https://codeclimate.com/github/alfa-jpn/kakurenbo-puti) 10 | 11 | ## Installation 12 | 13 | Add this line to your application's Gemfile: 14 | 15 | ```ruby 16 | gem 'kakurenbo-puti' 17 | ``` 18 | 19 | And then execute: 20 | 21 | $ bundle 22 | 23 | Or install it yourself as: 24 | 25 | $ gem install kakurenbo-puti 26 | 27 | ## Usage 28 | At first, add `soft_destroyed_at` column to your model. 29 | 30 | ```shell 31 | $ rails g migration AddSoftDestroyedAtToYourModel soft_destroyed_at:datetime:index 32 | $ rake db:migrate 33 | ``` 34 | 35 | Next, call `soft_deletable` in model. 36 | 37 | ```ruby 38 | 39 | class Member < ActiveRecord::Base 40 | soft_deletable 41 | end 42 | 43 | ``` 44 | 45 | 46 | ### Scopes 47 | 48 | ```ruby 49 | # Get models without soft-deleted 50 | Model.without_soft_destroyed 51 | 52 | # Get models only soft-deleted 53 | Model.only_soft_destroyed 54 | ``` 55 | 56 | ### Soft-delete record 57 | 58 | ```ruby 59 | model.soft_destroy 60 | 61 | # or 62 | model.soft_destroy! 63 | 64 | # check soft_destroyed 65 | model.soft_destroyed? # => true 66 | ``` 67 | 68 | ### Restore record 69 | 70 | ```ruby 71 | model.restore 72 | 73 | # or 74 | model.restore! 75 | ``` 76 | 77 | ## Advanced 78 | 79 | ### Definition of the dependency 80 | Use dependent_associations option of `soft-deletable`. 81 | This option is useable only in `belongs_to`. 82 | 83 | ```ruby 84 | 85 | class Parent < ActiveRecord::Base 86 | soft_deletable 87 | has_many :children 88 | end 89 | 90 | class Child < ActiveRecord::Base 91 | soft_deletable dependent_associations: [:parent] 92 | belongs_to :parent 93 | end 94 | 95 | # create instance 96 | parent = Parent.create! 97 | child = Child.create! 98 | 99 | # add child 100 | parent.children << child 101 | 102 | child.soft_destroyed? # false 103 | 104 | # soft-destroy parent 105 | parent.soft_destroy 106 | 107 | child.soft_destroyed? # true 108 | 109 | ``` 110 | 111 | ### Change column of datetime of soft-delete. 112 | 113 | ```ruby 114 | 115 | class Member < ActiveRecord::Base 116 | soft_deletable :column => :deleted_at 117 | end 118 | 119 | ``` 120 | 121 | # License 122 | This gem is released under the MIT license. 123 | -------------------------------------------------------------------------------- /spec/kakurenbo_puti/situations/deep_nest_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe KakurenboPuti::ActiveRecordBase do 4 | define_active_record_model :Parent do |t| 5 | t.datetime :soft_destroyed_at 6 | end 7 | 8 | define_active_record_model :OneChild do |t| 9 | t.integer :parent_id 10 | t.datetime :soft_destroyed_at 11 | end 12 | 13 | define_active_record_model :TwoChild do |t| 14 | t.integer :one_child_id 15 | t.datetime :soft_destroyed_at 16 | end 17 | 18 | define_active_record_model :ThreeChild do |t| 19 | t.integer :two_child_id 20 | t.datetime :soft_destroyed_at 21 | end 22 | 23 | define_active_record_model :FourChild do |t| 24 | t.integer :three_child_id 25 | t.datetime :soft_destroyed_at 26 | end 27 | 28 | define_active_record_model :FiveChild do |t| 29 | t.integer :four_child_id 30 | t.datetime :soft_destroyed_at 31 | end 32 | 33 | 34 | let :parent_class do 35 | Parent.tap do |klass| 36 | klass.class_eval { soft_deletable } 37 | end 38 | end 39 | 40 | let :one_child_class do 41 | OneChild.tap do |klass| 42 | klass.class_eval do 43 | soft_deletable dependent_associations: [:parent] 44 | belongs_to :parent 45 | end 46 | end 47 | end 48 | 49 | let :two_child_class do 50 | TwoChild.tap do |klass| 51 | klass.class_eval do 52 | soft_deletable dependent_associations: [:one_child] 53 | belongs_to :one_child 54 | end 55 | end 56 | end 57 | 58 | let :three_child_class do 59 | ThreeChild.tap do |klass| 60 | klass.class_eval do 61 | soft_deletable dependent_associations: [:two_child] 62 | belongs_to :two_child 63 | end 64 | end 65 | end 66 | 67 | let :four_child_class do 68 | FourChild.tap do |klass| 69 | klass.class_eval do 70 | soft_deletable dependent_associations: [:three_child] 71 | belongs_to :three_child 72 | end 73 | end 74 | end 75 | 76 | let :five_child_class do 77 | FiveChild.tap do |klass| 78 | klass.class_eval do 79 | soft_deletable dependent_associations: [:four_child] 80 | belongs_to :four_child 81 | end 82 | end 83 | end 84 | 85 | let :parent do 86 | parent_class.create! 87 | end 88 | 89 | let :one_child do 90 | one_child_class.create!(parent: parent) 91 | end 92 | 93 | let :two_child do 94 | two_child_class.create!(one_child: one_child) 95 | end 96 | 97 | let :three_child do 98 | three_child_class.create!(two_child: two_child) 99 | end 100 | 101 | let :four_child do 102 | four_child_class.create(three_child: three_child) 103 | end 104 | 105 | let :five_child do 106 | five_child_class.create(four_child: four_child) 107 | end 108 | 109 | 110 | context 'When delete root' do 111 | subject do 112 | parent.soft_destroy 113 | end 114 | 115 | it 'SoftDestroy dependent tail.' do 116 | expect { 117 | subject 118 | }.to change { 119 | five_child.soft_destroyed? 120 | }.from(false).to(true) 121 | end 122 | end 123 | end 124 | -------------------------------------------------------------------------------- /lib/kakurenbo_puti/active_record_base.rb: -------------------------------------------------------------------------------- 1 | module KakurenboPuti 2 | # Extension module of ActiveRecord::Base 3 | module ActiveRecordBase 4 | # Enable soft-delete. 5 | # @raise [StandardException] if Not found soft-deleted date column. 6 | # 7 | # @param [Symbol] column Name of soft-deleted date column. 8 | # @param [Array] dependent_associations Names of dependency association. 9 | def soft_deletable(column: :soft_destroyed_at, dependent_associations: []) 10 | Initializers.create_callbacks self 11 | Initializers.create_column_name_accessors self, column 12 | Initializers.create_scopes self, dependent_associations 13 | 14 | include InstanceMethods 15 | extend ClassMethods 16 | end 17 | 18 | module Initializers 19 | # Create callbacks. 20 | # @param [Class] target_class Class of target. 21 | def self.create_callbacks(target_class) 22 | target_class.class_eval do 23 | define_model_callbacks :restore 24 | define_model_callbacks :soft_destroy 25 | end 26 | end 27 | 28 | # Create attribute_accessors of column_name. 29 | # @param [Class] target_class Class of target. 30 | # @param [Symbol] column Name of column. 31 | def self.create_column_name_accessors(target_class, column) 32 | target_class.class_eval do 33 | define_singleton_method(:soft_delete_column) { column } 34 | delegate :soft_delete_column, to: :class 35 | end 36 | end 37 | 38 | # Create scopes. 39 | # @param [Class] target_class Class of target. 40 | # @param [Array] dependent_associations Names of dependency association. 41 | def self.create_scopes(target_class, dependent_associations) 42 | target_class.class_eval do 43 | scope :only_soft_destroyed, -> { where.not(id: without_soft_destroyed.select(:id)) } 44 | 45 | scope :without_soft_destroyed, (lambda do 46 | dependent_associations.inject(where(soft_delete_column => nil)) do |relation, name| 47 | association = relation.klass.reflect_on_all_associations.find{|a| a.name == name } 48 | raise 'dependent association is usable only in `belongs_to`.' unless association.belongs_to? 49 | 50 | parent_relation = association.klass.all 51 | if association.klass.method_defined?(:soft_delete_column) 52 | parent_relation = parent_relation.without_soft_destroyed 53 | end 54 | relation.where(association.foreign_key => parent_relation.select(:id)) 55 | end 56 | end) 57 | end 58 | end 59 | end 60 | 61 | module InstanceMethods 62 | # Restore model. 63 | # @return [Boolean] Return true if it is successfully restored. 64 | def restore 65 | true.tap { restore! } 66 | rescue 67 | false 68 | end 69 | 70 | # Restore model. 71 | # @raise [ActiveRecordError] 72 | def restore! 73 | run_callbacks(:restore) { update_column soft_delete_column, nil; self } 74 | end 75 | 76 | # Soft-Delete model. 77 | # @return [Boolean] Return true if it is successfully soft-deleted. 78 | def soft_destroy 79 | true.tap { soft_destroy! } 80 | rescue 81 | false 82 | end 83 | 84 | # Soft-Delete model. 85 | # @raise [ActiveRecordError] 86 | def soft_destroy! 87 | run_callbacks(:soft_destroy) { touch soft_delete_column; self } 88 | end 89 | 90 | # Check if model is soft-deleted. 91 | # @return [Boolean] Return true if model is soft-deleted. 92 | def soft_destroyed? 93 | self.class.only_soft_destroyed.where(id: id).exists? 94 | end 95 | end 96 | 97 | module ClassMethods 98 | def soft_destroy_all(conditions = nil) 99 | if conditions 100 | where(conditions).soft_destroy_all 101 | else 102 | all.each { |object| object.soft_destroy } 103 | end 104 | end 105 | end 106 | end 107 | end 108 | -------------------------------------------------------------------------------- /spec/kakurenbo_puti/active_record_base_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe KakurenboPuti::ActiveRecordBase do 4 | define_active_record_model :NormalModel do; end 5 | 6 | define_active_record_model :SoftDeleteModel do |t| 7 | t.datetime :soft_destroyed_at 8 | t.datetime :deleted_at 9 | end 10 | 11 | define_active_record_model :SoftDeleteChild do |t| 12 | t.integer :soft_delete_model_id 13 | t.integer :normal_model_id 14 | t.datetime :soft_destroyed_at 15 | end 16 | 17 | let :model_class do 18 | options_cache = model_class_options 19 | SoftDeleteModel.tap do |klass| 20 | klass.class_eval do 21 | soft_deletable options_cache 22 | has_many :soft_delete_children 23 | 24 | before_soft_destroy :cb_mock 25 | after_soft_destroy :cb_mock 26 | 27 | before_restore :cb_mock 28 | after_restore :cb_mock 29 | 30 | define_method(:cb_mock) { true } 31 | end 32 | end 33 | end 34 | 35 | let :child_class do 36 | options_cache = child_class_options 37 | SoftDeleteChild.tap do |klass| 38 | klass.class_eval do 39 | soft_deletable options_cache 40 | belongs_to :soft_delete_model 41 | belongs_to :normal_model 42 | end 43 | end 44 | end 45 | 46 | let :model_class_options do 47 | {} 48 | end 49 | 50 | let :child_class_options do 51 | { dependent_associations: [:soft_delete_model, :normal_model] } 52 | end 53 | 54 | let! :normal_model_instance do 55 | NormalModel.create! 56 | end 57 | 58 | let! :model_instance do 59 | model_class.create! 60 | end 61 | 62 | let! :child_instance do 63 | child_class.create!(soft_delete_model: model_instance, normal_model: normal_model_instance) 64 | end 65 | 66 | describe '.soft_delete_column' do 67 | it 'Return column name of soft-delete' do 68 | expect(model_class.soft_delete_column).to eq(:soft_destroyed_at) 69 | end 70 | 71 | context 'When with column option' do 72 | let :model_class_options do 73 | { column: :deleted_at } 74 | end 75 | 76 | it 'Return column name of option' do 77 | expect(model_class.soft_delete_column).to eq(model_class_options[:column]) 78 | end 79 | end 80 | end 81 | 82 | describe '.only_soft_destroyed' do 83 | subject do 84 | child_class.only_soft_destroyed 85 | end 86 | 87 | context 'When soft-deleted' do 88 | it 'Return a relation without soft-deleted model.' do 89 | expect { 90 | child_instance.soft_destroy! 91 | }.to change { 92 | subject.count 93 | }.by(1) 94 | end 95 | end 96 | 97 | context 'When parent is soft-deleted' do 98 | it 'Return a relation without parent soft-deleted model.' do 99 | expect { 100 | child_instance.soft_delete_model.soft_destroy! 101 | }.to change { 102 | subject.count 103 | }.by(1) 104 | end 105 | 106 | context 'When dependent association is nothing' do 107 | let :child_class_options do 108 | { dependent_associations: [] } 109 | end 110 | 111 | it 'Return a relation with parent soft-deleted model.' do 112 | expect { 113 | child_instance.soft_delete_model.soft_destroy! 114 | }.not_to change { 115 | subject.count 116 | } 117 | end 118 | end 119 | end 120 | 121 | context 'When parent is hard-deleted' do 122 | it 'Return a relation without parent soft-deleted model.' do 123 | expect { 124 | child_instance.normal_model.destroy! 125 | }.to change { 126 | subject.count 127 | }.by(1) 128 | end 129 | 130 | context 'When dependent association is nothing' do 131 | let :child_class_options do 132 | { dependent_associations: [] } 133 | end 134 | 135 | it 'Return a relation with parent soft-deleted model.' do 136 | expect { 137 | child_instance.normal_model.destroy! 138 | }.not_to change { 139 | subject.count 140 | } 141 | end 142 | end 143 | end 144 | end 145 | 146 | describe '.without_soft_destroyed' do 147 | subject do 148 | child_class.without_soft_destroyed 149 | end 150 | 151 | context 'When dependent association use in `has_many`' do 152 | subject do 153 | model_class.without_soft_destroyed 154 | end 155 | 156 | let :model_class_options do 157 | { dependent_associations: [:soft_delete_children] } 158 | end 159 | 160 | it 'raise error' do 161 | expect { subject }.to raise_error 162 | end 163 | end 164 | 165 | context 'When soft-deleted' do 166 | it 'Return a relation without soft-deleted model.' do 167 | expect { 168 | child_instance.soft_destroy! 169 | }.to change { 170 | subject.count 171 | }.by(-1) 172 | end 173 | end 174 | 175 | context 'When parent is soft-deleted' do 176 | it 'Return a relation without parent soft-deleted model.' do 177 | expect { 178 | child_instance.soft_delete_model.soft_destroy! 179 | }.to change { 180 | subject.count 181 | }.by(-1) 182 | end 183 | 184 | context 'When dependent association is nothing' do 185 | let :child_class_options do 186 | { dependent_associations: [] } 187 | end 188 | 189 | it 'Return a relation with parent soft-deleted model.' do 190 | expect { 191 | child_instance.soft_delete_model.soft_destroy! 192 | }.not_to change { 193 | subject.count 194 | } 195 | end 196 | end 197 | end 198 | 199 | context 'When parent is hard-deleted' do 200 | it 'Return a relation without parent soft-deleted model.' do 201 | expect { 202 | child_instance.normal_model.destroy! 203 | }.to change { 204 | subject.count 205 | }.by(-1) 206 | end 207 | 208 | context 'When dependent association is nothing' do 209 | let :child_class_options do 210 | { dependent_associations: [] } 211 | end 212 | 213 | it 'Return a relation with parent soft-deleted model.' do 214 | expect { 215 | child_instance.normal_model.destroy! 216 | }.not_to change { 217 | subject.count 218 | } 219 | end 220 | end 221 | end 222 | end 223 | 224 | describe '#restore' do 225 | before :each do 226 | model_instance.soft_destroy 227 | end 228 | 229 | subject do 230 | model_instance.restore 231 | end 232 | 233 | it 'Restore soft-deleted model.' do 234 | expect { 235 | subject 236 | }.to change { 237 | model_class.without_soft_destroyed.count 238 | }.by(1) 239 | end 240 | 241 | it 'Return truethy value.' do 242 | expect(subject).to be_truthy 243 | end 244 | 245 | it 'Run callbacks.' do 246 | expect(model_instance).to receive(:cb_mock).twice 247 | subject 248 | end 249 | 250 | context 'When raise exception.' do 251 | before :each do 252 | allow_any_instance_of(model_class).to receive(:update_column) { raise } 253 | end 254 | 255 | it 'Return falsey value.' do 256 | expect(subject).to be_falsey 257 | end 258 | end 259 | end 260 | 261 | describe '#restore!' do 262 | subject do 263 | model_instance.restore! 264 | end 265 | 266 | context 'When raise exception.' do 267 | before :each do 268 | allow_any_instance_of(model_class).to receive(:update_column) { raise } 269 | end 270 | 271 | it 'Raise Error.' do 272 | expect{ subject }.to raise_error 273 | end 274 | end 275 | end 276 | 277 | describe '#soft_delete' do 278 | subject do 279 | model_instance.soft_destroy 280 | end 281 | 282 | it 'Soft-delete model.' do 283 | expect { 284 | subject 285 | }.to change { 286 | model_class.without_soft_destroyed.count 287 | }.by(-1) 288 | end 289 | 290 | it 'Return truethy value.' do 291 | expect(subject).to be_truthy 292 | end 293 | 294 | it 'Run callbacks.' do 295 | expect(model_instance).to receive(:cb_mock).twice 296 | subject 297 | end 298 | 299 | context 'When raise exception.' do 300 | before :each do 301 | allow_any_instance_of(model_class).to receive(:touch) { raise } 302 | end 303 | 304 | it 'Return falsey value.' do 305 | expect(subject).to be_falsey 306 | end 307 | end 308 | end 309 | 310 | describe '#soft_delete!' do 311 | subject do 312 | model_instance.soft_delete! 313 | end 314 | 315 | context 'When raise exception.' do 316 | before :each do 317 | allow_any_instance_of(model_class).to receive(:update_column) { raise } 318 | end 319 | 320 | it 'Raise Error.' do 321 | expect{ subject }.to raise_error 322 | end 323 | end 324 | end 325 | 326 | describe '#soft_destroyed?' do 327 | subject do 328 | model_instance.soft_destroyed? 329 | end 330 | 331 | it 'Return falsey' do 332 | expect(subject).to be_falsey 333 | end 334 | 335 | context 'When model is soft-deleted' do 336 | before :each do 337 | model_instance.soft_destroy! 338 | end 339 | 340 | it 'Return truthy' do 341 | expect(subject).to be_truthy 342 | end 343 | end 344 | end 345 | 346 | describe '.soft_destroy_all' do 347 | subject do 348 | model_class.soft_destroy_all 349 | end 350 | let!(:model_instance) { model_class.create! } 351 | 352 | it 'SoftDelete model' do 353 | expect { 354 | subject 355 | }.to change { 356 | model_class.without_soft_destroyed.count 357 | }.to(0) 358 | end 359 | 360 | context 'with conditions' do 361 | subject do 362 | model_class.soft_destroy_all(id: model_instance.id) 363 | end 364 | 365 | it 'SoftDelete model' do 366 | expect { 367 | subject 368 | }.to change { 369 | model_class.without_soft_destroyed.count 370 | }.to(0) 371 | end 372 | end 373 | end 374 | end 375 | --------------------------------------------------------------------------------