diff --git a/backend/app/models/post_implication.rb b/backend/app/models/post_implication.rb index 21c5a59..2ea8ec3 100644 --- a/backend/app/models/post_implication.rb +++ b/backend/app/models/post_implication.rb @@ -8,6 +8,7 @@ class PostImplication < ApplicationRecord validates :parent_post_id, presence: true validate :parent_post_mustnt_be_itself + validate :parent_post_mustnt_create_cycle private @@ -16,4 +17,24 @@ class PostImplication < ApplicationRecord errors.add :parent_post_id, '親投稿に同じ投稿を設定することはできません.' end end + + def parent_post_mustnt_create_cycle + return if post_id.blank? || parent_post_id.blank? + return if errors[:parent_post_id].present? + + ancestor_ids = Set.new + frontier_ids = [parent_post_id] + + until frontier_ids.empty? + if frontier_ids.include?(post_id) + errors.add :parent_post_id, :invalid + return + end + + ancestor_ids.merge(frontier_ids) + frontier_ids = PostImplication.where(post_id: frontier_ids) + .distinct.pluck(:parent_post_id) + .reject { ancestor_ids.include?(_1) } + end + end end diff --git a/backend/spec/models/post_implication_spec.rb b/backend/spec/models/post_implication_spec.rb index 8a11600..3cd34f7 100644 --- a/backend/spec/models/post_implication_spec.rb +++ b/backend/spec/models/post_implication_spec.rb @@ -48,4 +48,47 @@ RSpec.describe PostImplication, type: :model do expect(duplicate).not_to be_valid expect(duplicate.errors[:post_id]).to be_present end + + it 'rejects a parent that would create a two-post cycle' do + described_class.create!(post: post_record, parent_post:) + + implication = described_class.new( + post: parent_post, parent_post: post_record) + + expect(implication).not_to be_valid + expect(implication.errors.of_kind?(:parent_post_id, :invalid)).to be true + end + + it 'rejects a parent that would create a three-post cycle' do + ancestor_post = create(:post) + described_class.create!(post: post_record, parent_post:) + described_class.create!(post: parent_post, parent_post: ancestor_post) + + implication = described_class.new( + post: ancestor_post, parent_post: post_record) + + expect(implication).not_to be_valid + expect(implication.errors.of_kind?(:parent_post_id, :invalid)).to be true + end + + it 'allows a parent with an ancestor when no cycle would result' do + ancestor_post = create(:post) + described_class.create!(post: parent_post, parent_post: ancestor_post) + + implication = described_class.new(post: post_record, parent_post:) + + expect(implication).to be_valid + end + + it 'terminates when existing ancestors already contain a cycle' do + ancestor_post = create(:post) + described_class.create!(post: parent_post, parent_post: ancestor_post) + legacy_implication = described_class.new( + post: ancestor_post, parent_post:) + legacy_implication.save!(validate: false) + + implication = described_class.new(post: post_record, parent_post:) + + expect(implication).to be_valid + end end