diff --git a/lib/friendly_id/slug_generator.rb b/lib/friendly_id/slug_generator.rb index 372db573..d9d24891 100644 --- a/lib/friendly_id/slug_generator.rb +++ b/lib/friendly_id/slug_generator.rb @@ -12,6 +12,10 @@ def available?(slug) return false if @config.reserved_words.include?(slug) end + if @config.treat_numeric_as_conflict && purely_numeric_slug?(slug) + return false + end + !@scope.exists_by_friendly_id?(slug) end @@ -19,5 +23,17 @@ def generate(candidates) candidates.each { |c| return c if available?(c) } nil end + + private + + def purely_numeric_slug?(slug) + return false unless slug + begin + Integer(slug, 10) + slug.to_s == Integer(slug, 10).to_s + rescue + false + end + end end end diff --git a/lib/friendly_id/slugged.rb b/lib/friendly_id/slugged.rb index 8e8fae23..83e37102 100644 --- a/lib/friendly_id/slugged.rb +++ b/lib/friendly_id/slugged.rb @@ -386,7 +386,7 @@ def unset_slug_if_invalid # {FriendlyId::Configuration FriendlyId::Configuration}. module Configuration attr_writer :slug_column, :slug_limit, :sequence_separator - attr_accessor :slug_generator_class + attr_accessor :slug_generator_class, :treat_numeric_as_conflict # Makes FriendlyId use the slug column for querying. # @return String The slug column. diff --git a/test/numeric_slug_test.rb b/test/numeric_slug_test.rb index fd92fb03..982dab12 100644 --- a/test/numeric_slug_test.rb +++ b/test/numeric_slug_test.rb @@ -1,5 +1,17 @@ require "helper" +class Article < ActiveRecord::Base + extend FriendlyId + friendly_id :name, use: :slugged +end + +class ArticleWithNumericPrevention < ActiveRecord::Base + self.table_name = "articles" + extend FriendlyId + friendly_id :name, use: :slugged + friendly_id_config.treat_numeric_as_conflict = true +end + class NumericSlugTest < TestCaseClass include FriendlyId::Test include FriendlyId::Test::Shared::Core @@ -28,4 +40,61 @@ def model_class assert model_class.friendly.exists?("123") end end + + test "should prevent purely numeric slugs when treat_numeric_as_conflict is enabled" do + transaction do + record = ArticleWithNumericPrevention.create! name: "123" + refute_equal "123", record.slug + assert_match(/\A123-[0-9a-f-]{36}\z/, record.slug) + end + end + + test "should allow non-numeric slugs when treat_numeric_as_conflict is enabled" do + transaction do + record = ArticleWithNumericPrevention.create! name: "abc123" + assert_equal "abc123", record.slug + end + end + + test "should allow alphanumeric slugs when treat_numeric_as_conflict is enabled" do + transaction do + record = ArticleWithNumericPrevention.create! name: "product-123" + assert_equal "product-123", record.slug + end + end + + test "should handle zero as numeric when treat_numeric_as_conflict is enabled" do + transaction do + record = ArticleWithNumericPrevention.create! name: "0" + refute_equal "0", record.slug + assert_match(/\A0-[0-9a-f-]{36}\z/, record.slug) + end + end + + test "should handle large numbers as numeric when treat_numeric_as_conflict is enabled" do + transaction do + record = ArticleWithNumericPrevention.create! name: "999999999" + refute_equal "999999999", record.slug + assert_match(/\A999999999-[0-9a-f-]{36}\z/, record.slug) + end + end + + test "should find records with UUID-suffixed numeric slugs when treat_numeric_as_conflict is enabled" do + transaction do + record = ArticleWithNumericPrevention.create! name: "123" + found = ArticleWithNumericPrevention.friendly.find(record.slug) + assert_equal record.id, found.id + end + end + + test "should resolve conflicts between multiple numeric slugs when treat_numeric_as_conflict is enabled" do + transaction do + record1 = ArticleWithNumericPrevention.create! name: "456" + record2 = ArticleWithNumericPrevention.create! name: "456" + + refute_equal record1.slug, record2.slug + assert_match(/\A456-[0-9a-f-]{36}\z/, record1.slug) + assert_match(/\A456-[0-9a-f-]{36}\z/, record2.slug) + end + end end