simple_slug 0.3.4 → 0.4.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 4ee4149b3d1f301a58ec9bc4ff2000727be10650
4
- data.tar.gz: 8e9554c782d1b5b33187e3848873f0d12b81ca5d
2
+ SHA256:
3
+ metadata.gz: 355d5d538c1f8ae0da92c5a55c12b61d4920131d68f73bf0cc353fc018b777e1
4
+ data.tar.gz: 4e9aa08c961ddc1de5811c000b863f191ae3780de257bde157ea3ae7f9ed1d47
5
5
  SHA512:
6
- metadata.gz: 9392260bc6505c78f5044916e93d79dcfb3d0dc1bb64570da75c859fb5932d6034d87438af8c3ade9e911ccb728879a8c7712b4fab28488d19b356d99e7b7948
7
- data.tar.gz: de23e545145e537dce36ca62973648724ef9dd5e748b0998de3cda46a5c10670004dc6f5e6f8a299ca77de997ec045486eef177c3eeb960b10748d1bf3f3bc67
6
+ metadata.gz: 8d44218bf0da7e4119b32e37790e8f866deccc600aae3c6260297bd88fa8557a2fd39f3216a0e7d3a4a29c5a62af551a0bf910480a84bdbcccf6a8868f45500a
7
+ data.tar.gz: 00f5b303bc4c2384bcb09356f42f93c58786ceefc3e1e15a1ee26eb99e116c58b36e88ef216844379fa53e5a1f4b982a32fc7b3ce56549f0e8ce0e3bd823d4be
data/.gitignore CHANGED
@@ -12,6 +12,7 @@ Gemfile.lock
12
12
  InstalledFiles
13
13
  _yardoc
14
14
  coverage
15
+ /bin
15
16
  doc/
16
17
  lib/bundler/man
17
18
  pkg
@@ -1,4 +1,4 @@
1
1
  language: ruby
2
2
 
3
3
  rvm:
4
- - 2.3.0
4
+ - 2.6.3
data/README.md CHANGED
@@ -1,7 +1,6 @@
1
1
  # SimpleSlug
2
2
 
3
3
  [![Build Status](https://travis-ci.org/leschenko/simple_slug.png?branch=master)](https://travis-ci.org/leschenko/simple_slug)
4
- [![Dependency Status](https://gemnasium.com/leschenko/simple_slug.png)](https://gemnasium.com/leschenko/simple_slug)
5
4
 
6
5
  Simple friendly url generator for ActiveRecord models with history.
7
6
 
@@ -37,6 +36,14 @@ class User < ActiveRecord::Base
37
36
  end
38
37
  ```
39
38
 
39
+ Add localization with `slug_YOUR_LOCALE` like columns:
40
+
41
+ ```ruby
42
+ class User < ActiveRecord::Base
43
+ simple_slug :full_name, locales: [nil, :it]
44
+ end
45
+ ```
46
+
40
47
  If you want to control when slug is generated, define `should_generate_new_slug?` method:
41
48
 
42
49
  ```ruby
@@ -1,13 +1,14 @@
1
1
  class CreateSimpleSlugHistorySlug < ActiveRecord::Migration
2
2
  def change
3
- create_table :simple_slug_history_slug do |t|
4
- t.string :slug, null: false
3
+ create_table :simple_slug_history_slugs do |t|
4
+ t.string :slug, null: false, limit: 191
5
+ t.string :locale, limit: 10
5
6
  t.integer :sluggable_id, null: false
6
7
  t.string :sluggable_type, limit: 50, null: false
7
- t.datetime :created_at
8
+ t.timestamps
8
9
  end
9
10
 
10
- add_index :simple_slug_history_slug, [:slug, :sluggable_type], unique: true
11
- add_index :simple_slug_history_slug, [:sluggable_type, :sluggable_id]
11
+ add_index :simple_slug_history_slugs, :slug
12
+ add_index :simple_slug_history_slugs, [:sluggable_type, :sluggable_id], name: 'simple_slug_history_slugs_on_sluggable_type_and_sluggable_id'
12
13
  end
13
14
  end
@@ -4,34 +4,36 @@ require 'simple_slug/model_addition'
4
4
  require 'simple_slug/railtie' if Object.const_defined?(:Rails)
5
5
 
6
6
  module SimpleSlug
7
+ autoload :Adapter, 'simple_slug/adapter'
8
+ autoload :ModelAddition, 'simple_slug/model_addition'
7
9
  autoload :HistorySlug, 'simple_slug/history_slug'
8
10
 
9
11
  mattr_accessor :excludes
10
- @@excludes = %w(new edit show index session login logout sign_in sign_out users admin stylesheets assets javascripts images)
12
+ @@excludes = %w(new edit show index session login logout sign_in sign_out users admin stylesheets javascripts images fonts assets)
11
13
 
12
14
  mattr_accessor :slug_regexp
13
- @@slug_regexp = /\A\w+[\w\d\-_]*\z/
15
+ @@slug_regexp = /\A(?:\w+[\w\d\-_]*|--\d+)\z/
14
16
 
15
17
  mattr_accessor :slug_column
16
18
  @@slug_column = 'slug'
17
19
 
20
+ mattr_accessor :min_length
21
+ @@min_length = 3
22
+
18
23
  mattr_accessor :max_length
19
- @@max_length = 240
24
+ @@max_length = 191
20
25
 
21
26
  mattr_accessor :callback_type
22
27
  @@callback_type = :before_validation
23
28
 
24
- mattr_accessor :add_validation
25
- @@add_validation = true
29
+ mattr_accessor :validation
30
+ @@validation = true
26
31
 
27
32
  STARTS_WITH_NUMBER_REGEXP =/\A\d+/
28
- CYRILLIC_LOCALES = [:uk, :ru, :be].freeze
33
+ NUMBER_REGEXP =/\A\d+\z/
34
+ RESOLVE_SUFFIX_REGEXP = /--\d+\z/
29
35
 
30
36
  def self.setup
31
37
  yield self
32
38
  end
33
-
34
- def self.normalize_cyrillic(base)
35
- base.tr('АаВЕеіКкМНОоРрСсТуХх', 'AaBEeiKkMHOoPpCcTyXx')
36
- end
37
39
  end
@@ -0,0 +1,123 @@
1
+ module SimpleSlug
2
+ class Adapter
3
+ attr_reader :model, :options, :locales
4
+ attr_accessor :current_locale
5
+
6
+ def initialize(model)
7
+ @model = model
8
+ @options = model.simple_slug_options
9
+ @locales = Array(@options[:locales] || [nil])
10
+ end
11
+
12
+ def finder_method
13
+ options[:history] ? :find_by : :find_by!
14
+ end
15
+
16
+ def valid_locale?(locale)
17
+ locales.include?(locale)
18
+ end
19
+
20
+ def current_locale
21
+ valid_locale?(I18n.locale) ? I18n.locale : nil
22
+ end
23
+
24
+ def column_names
25
+ locales.map{|l| column_name(l) }
26
+ end
27
+
28
+ def column_name(locale=I18n.locale)
29
+ [options[:slug_column], (locale if valid_locale?(locale))].compact.join('_')
30
+ end
31
+
32
+ def get(record)
33
+ record.send(column_name)
34
+ end
35
+
36
+ def get_prev(record)
37
+ record.send("#{column_name}_was")
38
+ end
39
+
40
+ def set(record, value)
41
+ record.send("#{column_name}=", value)
42
+ end
43
+
44
+ def each_locale
45
+ locales.each do |l|
46
+ with_locale(l || I18n.default_locale) { yield }
47
+ end
48
+ end
49
+
50
+ def reset(record)
51
+ each_locale{ set record, get_prev(record) }
52
+ end
53
+
54
+ def save_history(record)
55
+ each_locale do
56
+ slug_was = record.saved_change_to_attribute(column_name).try!(:first)
57
+ next if slug_was.blank?
58
+ ::SimpleSlug::HistorySlug.where(sluggable_type: record.class.name, slug: slug_was, locale: current_locale).first_or_initialize.update(sluggable_id: record.id)
59
+ end
60
+ end
61
+
62
+ def generate(record, force: false)
63
+ each_locale do
64
+ next unless force || record.should_generate_new_slug?
65
+ simple_slug = normalize(slug_base(record))
66
+ simple_slug = "__#{record.id || rand(9999)}" if simple_slug.blank? && options[:fallback_on_blank]
67
+ return if simple_slug == get(record).to_s.sub(SimpleSlug::RESOLVE_SUFFIX_REGEXP, '')
68
+ set(record, resolve(record, simple_slug))
69
+ end
70
+ end
71
+
72
+ def normalize(base)
73
+ parameterize_args = ActiveSupport::VERSION::MAJOR > 4 ? {separator: '-'} : '-'
74
+ normalized = I18n.transliterate(base).parameterize(**parameterize_args).downcase
75
+ normalized = "_#{normalized}" if normalized =~ SimpleSlug::STARTS_WITH_NUMBER_REGEXP
76
+ normalized = normalized[0..options[:max_length].pred] if options[:max_length]
77
+ normalized
78
+ end
79
+
80
+ def add_suffix(slug_value)
81
+ "#{slug_value}--#{rand(99999)}"
82
+ end
83
+
84
+ def slug_base(record)
85
+ options[:slug_method].map{|m| record.send(m).to_s }.reject(&:blank?).join(' ')
86
+ end
87
+
88
+ def resolve(record, slug_value)
89
+ return slug_value unless slug_exists?(record, slug_value)
90
+ loop do
91
+ slug_with_suffix = add_suffix(slug_value)
92
+ break slug_with_suffix unless slug_exists?(record, slug_with_suffix)
93
+ end
94
+ end
95
+
96
+ def slug_exists?(record, slug_value)
97
+ model_slug_exists?(record, slug_value) || history_slug_exists?(record, slug_value)
98
+ end
99
+
100
+ def model_slug_exists?(record, slug_value)
101
+ base_scope = record.class.unscoped.where(column_name => slug_value)
102
+ base_scope = base_scope.where('id != ?', record.id) if record.persisted?
103
+ base_scope.exists?
104
+ end
105
+
106
+ def history_slug_exists?(record, slug_value)
107
+ return false unless options[:history]
108
+ base_scope = SimpleSlug::HistorySlug.where(sluggable_type: record.class.name, slug: slug_value)
109
+ base_scope = base_scope.where('sluggable_id != ?', record.id) if record.persisted?
110
+ base_scope.exists?
111
+ end
112
+
113
+ def with_locale(locale)
114
+ if defined? Globalize
115
+ Globalize.with_locale(locale) do
116
+ I18n.with_locale(locale) { yield }
117
+ end
118
+ else
119
+ I18n.with_locale(locale) { yield }
120
+ end
121
+ end
122
+ end
123
+ end
@@ -1,5 +1,6 @@
1
1
  module SimpleSlug
2
2
  class HistorySlug < ActiveRecord::Base
3
+ self.table_name = 'simple_slug_history_slugs'
3
4
  belongs_to :sluggable, polymorphic: true
4
5
  end
5
6
  end
@@ -6,33 +6,35 @@ module SimpleSlug
6
6
 
7
7
  module SingletonMethods
8
8
  def simple_slug(*args)
9
- class_attribute :simple_slug_options, instance_writer: false
9
+ class_attribute :simple_slug_options, :simple_slug_adapter, instance_writer: false
10
10
  options = args.extract_options!
11
11
  self.simple_slug_options = options.reverse_merge(
12
12
  slug_column: SimpleSlug.slug_column,
13
13
  slug_method: args,
14
14
  slug_regexp: SimpleSlug.slug_regexp,
15
+ min_length: SimpleSlug.min_length,
15
16
  max_length: SimpleSlug.max_length,
16
17
  callback_type: SimpleSlug.callback_type,
17
- add_validation: SimpleSlug.add_validation
18
+ validation: SimpleSlug.validation
18
19
  )
20
+ self.simple_slug_adapter = SimpleSlug::Adapter.new(self)
19
21
 
20
22
  include InstanceMethods
21
23
  extend ClassMethods
22
24
 
23
25
  send(simple_slug_options[:callback_type], :simple_slug_generate, if: :should_generate_new_slug?) if simple_slug_options[:callback_type]
24
26
 
25
- if simple_slug_options[:add_validation]
26
- simple_slug_locales.each do |locale|
27
- validates simple_slug_column(locale),
28
- presence: true,
29
- exclusion: {in: SimpleSlug.excludes},
30
- format: {with: simple_slug_options[:slug_regexp]}
31
- end
27
+ if simple_slug_options[:validation]
28
+ validates *simple_slug_adapter.column_names,
29
+ presence: true,
30
+ uniqueness: {case_sensitive: true},
31
+ exclusion: {in: SimpleSlug.excludes},
32
+ format: {with: simple_slug_options[:slug_regexp]},
33
+ length: {minimum: simple_slug_options[:min_length], maximum: simple_slug_options[:max_length]}.reject{|_, v| v.blank? }
32
34
  end
33
35
 
34
36
  if simple_slug_options[:history]
35
- after_save :simple_slug_reset_unsaved_slug, :simple_slug_create_history_slug
37
+ after_save :simple_slug_reset, :simple_slug_save_history
36
38
  after_destroy :simple_slug_cleanup_history
37
39
  include InstanceHistoryMethods
38
40
  end
@@ -42,144 +44,45 @@ module SimpleSlug
42
44
  module ClassMethods
43
45
  def simple_slug_find(id_param)
44
46
  return unless id_param
45
- if id_param.is_a?(Integer) || id_param =~ /\A\d+\z/
47
+ if id_param.is_a?(Integer) || id_param =~ SimpleSlug::NUMBER_REGEXP
46
48
  find(id_param)
47
49
  else
48
- finder_method = simple_slug_options[:history] ? :find_by : :find_by!
49
- send(finder_method, simple_slug_column => id_param) or find(::SimpleSlug::HistorySlug.find_by!(slug: id_param).sluggable_id)
50
+ send(simple_slug_adapter.finder_method, simple_slug_adapter.column_name => id_param) or simple_slug_history_find(id_param)
50
51
  end
51
52
  end
52
53
 
53
- alias_method :friendly_find, :simple_slug_find
54
-
55
- def simple_slug_column(locale=I18n.locale)
56
- if simple_slug_localized?(locale)
57
- [simple_slug_options[:slug_column], locale].compact.join('_')
58
- else
59
- simple_slug_options[:slug_column]
60
- end
61
- end
62
-
63
- def simple_slug_columns
64
- simple_slug_locales.map{|locale| simple_slug_column(locale) }
65
- end
66
-
67
- def simple_slug_locales
68
- Array(simple_slug_options[:locales] || [nil])
54
+ def simple_slug_history_find(slug, locale=I18n.locale)
55
+ find(SimpleSlug::HistorySlug.find_by!(locale: (locale if simple_slug_adapter.valid_locale?(locale)), slug: slug).sluggable_id)
69
56
  end
70
57
 
71
- def simple_slug_localized?(locale=I18n.locale)
72
- return unless locale
73
- simple_slug_locales.include?(locale.to_sym)
74
- end
58
+ alias_method :friendly_find, :simple_slug_find
75
59
  end
76
60
 
77
61
  module InstanceMethods
78
62
  def to_param
79
- simple_slug_stored_slug.presence || super
63
+ simple_slug_adapter.get_prev(self).presence || super
80
64
  end
81
65
 
82
66
  def should_generate_new_slug?
83
- return true if simple_slug_options[:history]
84
- return simple_slug_get.blank? unless simple_slug_options[:locales]
85
- simple_slug_options[:locales].any? { |locale| simple_slug_get(locale).blank? }
67
+ simple_slug_adapter.column_names.any?{|cn| send(cn).blank? }
86
68
  end
87
69
 
88
70
  def simple_slug_generate(force=false)
89
- (simple_slug_options[:locales] || [nil]).each do |locale|
90
- simple_slug_generate_for_locale(locale, force)
91
- end
92
- end
93
-
94
- def simple_slug_generate_for_locale(locale=I18n.locale, force=false)
95
- simple_slug_with_locale(locale) do
96
- simple_slug = simple_slug_normalize(simple_slug_base)
97
- simple_slug = simple_slug.first(simple_slug_options[:max_length]) if simple_slug_options[:max_length]
98
- return if !force && simple_slug == simple_slug_get(locale).to_s.sub(/--\d+\z/, '')
99
- resolved_simple_slug = simple_slug_resolve(simple_slug, locale)
100
- simple_slug_set(resolved_simple_slug, locale)
101
- end
102
- end
103
-
104
- def simple_slug_with_locale(locale)
105
- if defined? Globalize
106
- Globalize.with_locale(locale) do
107
- I18n.with_locale(locale) { yield }
108
- end
109
- else
110
- I18n.with_locale(locale) { yield }
111
- end
112
- end
113
-
114
- def simple_slug_base
115
- simple_slug_options[:slug_method].map{|m| send(m).to_s }.reject(&:blank?).join(' ')
116
- end
117
-
118
- def simple_slug_normalize(base)
119
- base = SimpleSlug.normalize_cyrillic(base) unless SimpleSlug::CYRILLIC_LOCALES.include?(I18n.locale)
120
- parameterize_args = ActiveSupport::VERSION::MAJOR > 4 ? {separator: '-'} : '-'
121
- normalized = I18n.transliterate(base).parameterize(parameterize_args).downcase
122
- normalized.to_s =~ SimpleSlug::STARTS_WITH_NUMBER_REGEXP ? "_#{normalized}" : normalized
123
- end
124
-
125
- def simple_slug_resolve(slug_value, locale=I18n.locale)
126
- if simple_slug_exists?(slug_value, locale)
127
- loop do
128
- slug_value_with_suffix = simple_slug_next(slug_value)
129
- break slug_value_with_suffix unless simple_slug_exists?(slug_value_with_suffix, locale)
130
- end
131
- else
132
- slug_value
133
- end
134
- end
135
-
136
- def simple_slug_next(slug_value)
137
- "#{slug_value}--#{rand(99999)}"
138
- end
139
-
140
- def simple_slug_exists?(slug_value, locale=I18n.locale)
141
- simple_slug_base_exists?(slug_value, locale) || simple_slug_history_exists?(slug_value)
142
- end
143
-
144
- def simple_slug_base_exists?(slug_value, locale=I18n.locale)
145
- base_scope = self.class.unscoped.where(self.class.simple_slug_column(locale) => slug_value)
146
- base_scope = base_scope.where('id != ?', id) if persisted?
147
- base_scope.exists?
148
- end
149
-
150
- def simple_slug_history_exists?(slug_value)
151
- return false unless simple_slug_options[:history]
152
- base_scope = ::SimpleSlug::HistorySlug.where(sluggable_type: self.class.name, slug: slug_value)
153
- base_scope = base_scope.where('sluggable_id != ?', id) if persisted?
154
- base_scope.exists?
155
- end
156
-
157
- def simple_slug_set(value, locale=I18n.locale)
158
- send "#{self.class.simple_slug_column(locale)}=", value
159
- end
160
-
161
- def simple_slug_get(locale=I18n.locale)
162
- send self.class.simple_slug_column(locale)
163
- end
164
-
165
- def simple_slug_stored_slug(locale=I18n.locale)
166
- send("#{self.class.simple_slug_column(locale)}_was")
71
+ simple_slug_adapter.generate(self, force: force)
167
72
  end
168
73
  end
169
74
 
170
75
  module InstanceHistoryMethods
171
- def simple_slug_reset_unsaved_slug
172
- return true if errors.blank?
173
- simple_slug_set simple_slug_stored_slug
76
+ def simple_slug_reset
77
+ errors.blank? || simple_slug_adapter.reset(self)
174
78
  end
175
79
 
176
80
  def simple_slug_cleanup_history
177
81
  ::SimpleSlug::HistorySlug.where(sluggable_type: self.class.name, sluggable_id: id).delete_all
178
82
  end
179
83
 
180
- def simple_slug_create_history_slug
181
- return true unless slug_changed?
182
- ::SimpleSlug::HistorySlug.where(sluggable_type: self.class.name, slug: simple_slug_get).first_or_create{|hs| hs.sluggable_id = id }
84
+ def simple_slug_save_history
85
+ simple_slug_adapter.save_history(self)
183
86
  end
184
87
  end
185
88
  end
@@ -1,11 +1,9 @@
1
1
  module SimpleSlug
2
2
  class Railtie < Rails::Railtie
3
3
  initializer 'simple_slug.model_additions' do
4
-
5
4
  ActiveSupport.on_load :active_record do
6
5
  include SimpleSlug::ModelAddition
7
6
  end
8
-
9
7
  end
10
8
  end
11
9
  end
@@ -1,3 +1,3 @@
1
1
  module SimpleSlug
2
- VERSION = '0.3.4'
2
+ VERSION = '0.4.3'
3
3
  end
@@ -1,4 +1,3 @@
1
- # coding: utf-8
2
1
  lib = File.expand_path('../lib', __FILE__)
3
2
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
3
  require 'simple_slug/version'
@@ -9,7 +8,7 @@ Gem::Specification.new do |spec|
9
8
  spec.authors = ['Alex Leschenko']
10
9
  spec.email = ['leschenko.al@gmail.com']
11
10
  spec.summary = %q{Friendly url generator with history.}
12
- spec.description = %q{Simple friendly url generator for ActiveRecord with history."}
11
+ spec.description = %q{Simple friendly url generator for ActiveRecord with history.}
13
12
  spec.homepage = 'https://github.com/leschenko/simple_slug'
14
13
  spec.license = 'MIT'
15
14
 
@@ -18,10 +17,11 @@ Gem::Specification.new do |spec|
18
17
  spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
18
  spec.require_paths = ['lib']
20
19
 
21
- spec.add_dependency 'activerecord', '>= 4.0.0', '< 5.2'
22
- spec.add_dependency 'i18n', '~> 0.7'
20
+ spec.add_dependency 'activerecord', '>= 4.0.0', '< 6.2'
21
+ spec.add_dependency 'i18n', '~> 1.8.7'
23
22
 
24
- spec.add_development_dependency 'bundler', '~> 1.5'
23
+ spec.add_development_dependency 'bundler'
25
24
  spec.add_development_dependency 'rake'
26
25
  spec.add_development_dependency 'rspec'
26
+ spec.add_development_dependency 'sqlite3'
27
27
  end
@@ -1,17 +1,17 @@
1
1
  require 'spec_helper'
2
2
 
3
3
  describe SimpleSlug do
4
- context 'defaults' do
5
- it 'slug column' do
4
+ describe 'config' do
5
+ it 'has column name' do
6
6
  expect(SimpleSlug.slug_column).to eq 'slug'
7
7
  end
8
8
 
9
- it 'excludes' do
9
+ it 'has excludes' do
10
10
  expect(SimpleSlug.excludes).to include('new', 'edit')
11
11
  end
12
12
 
13
- it 'max length' do
14
- expect( SimpleSlug.max_length).to eq 240
13
+ it 'has max length' do
14
+ expect( SimpleSlug.max_length).to eq 191
15
15
  end
16
16
  end
17
17
  end
@@ -1,54 +1,79 @@
1
1
  require 'spec_helper'
2
2
 
3
- class SlugHistoryRspecModel < RspecActiveModelBase
3
+ class SlugHistoryRspecModel < RspecActiveRecordBase
4
4
  simple_slug :name, history: true
5
+
6
+ def should_generate_new_slug?
7
+ true
8
+ end
5
9
  end
6
10
 
7
- describe 'slug history' do
8
- describe 'history records handling' do
9
- before do
10
- expect_any_instance_of(SlugHistoryRspecModel).to receive(:simple_slug_exists?).and_return(false)
11
+ class SlugLocalizedHistoryRspecModel < RspecActiveRecordBase
12
+ simple_slug :name_for_slug, history: true, locales: [nil, :en]
13
+
14
+ def name_for_slug
15
+ [name, (I18n.locale unless I18n.locale == I18n.default_locale)].compact.join(' ')
16
+ end
17
+
18
+ def should_generate_new_slug?
19
+ true
20
+ end
21
+ end
22
+
23
+ describe 'history' do
24
+ before :each do
25
+ RspecActiveRecordBase.delete_all
26
+ SimpleSlug::HistorySlug.delete_all
27
+ end
28
+
29
+ describe 'persistence' do
30
+ it 'save previous on change' do
31
+ sluggable = SlugHistoryRspecModel.create(id: 1, name: 'Hello')
32
+ expect(SimpleSlug::HistorySlug.where(sluggable_type: 'SlugHistoryRspecModel', sluggable_id: 1).exists?).to be_falsey
33
+ sluggable.update(name: 'Bye')
34
+ hs = SimpleSlug::HistorySlug.where(sluggable_type: 'SlugHistoryRspecModel', sluggable_id: 1).to_a
35
+ expect(hs.size).to eq 1
36
+ expect(hs.first.locale).to be_falsey
37
+ expect(hs.first.slug).to eq 'hello'
11
38
  end
12
39
 
13
- it 'create' do
14
- relation = double
15
- expect(::SimpleSlug::HistorySlug).to receive(:where).once.ordered.with(sluggable_type: 'SlugHistoryRspecModel', slug: 'hello').and_return(relation)
16
- expect(relation).to receive(:first_or_create)
17
- SlugHistoryRspecModel.create(id: 1, name: 'Hello')
40
+ it 'remove on destroy' do
41
+ sluggable = SlugHistoryRspecModel.create(id: 1, name: 'Hello')
42
+ sluggable.update(name: 'Bye')
43
+ expect{ sluggable.destroy }.to change{ SimpleSlug::HistorySlug.where(sluggable_type: 'SlugHistoryRspecModel', sluggable_id: 1).count }.from(1).to(0)
18
44
  end
19
45
 
20
- it 'cleanup' do
21
- relation = double
22
- expect(relation).to receive(:first_or_create)
23
- allow(::SimpleSlug::HistorySlug).to receive(:where).and_return(relation)
24
- expect(relation).to receive(:delete_all)
25
- SlugHistoryRspecModel.create(name: 'Hello', id: 1).destroy
46
+ context 'localized' do
47
+ it 'save previous on change' do
48
+ SlugLocalizedHistoryRspecModel.create(id: 1, name: 'Hello').update(name: 'Bye')
49
+ hs = SimpleSlug::HistorySlug.where(sluggable_type: 'SlugLocalizedHistoryRspecModel', sluggable_id: 1).to_a
50
+ expect(hs.map(&:locale)).to match_array [nil, 'en']
51
+ expect(hs.map(&:slug)).to match_array %w(hello hello-en)
52
+ end
26
53
  end
27
54
  end
28
55
 
29
56
  describe 'conflicts' do
30
- it 'history slug exists' do
31
- record = SlugGenerationRspecModel.new(name: 'Hi')
32
- allow(record).to receive(:simple_slug_base_exists?).and_return(false)
33
- expect(record).to receive(:simple_slug_history_exists?).once.ordered.and_return(true)
34
- expect(record).to receive(:simple_slug_history_exists?).once.ordered.and_return(false)
35
- record.save
36
- expect(record.slug).to start_with('hi--')
57
+ it 'resolve with suffix' do
58
+ SlugHistoryRspecModel.create(name: 'Hello').update(name: 'Bye')
59
+ record = SlugHistoryRspecModel.create(name: 'Hello')
60
+ expect(record.slug).to start_with('hello--')
37
61
  end
38
- end
39
62
 
40
- describe '#friendly_find' do
41
- before do
42
- allow(SlugHistoryRspecModel).to receive(:find_by)
63
+ context 'localized' do
64
+ it 'resolve with suffix' do
65
+ SlugLocalizedHistoryRspecModel.create(name: 'Hello').update(name: 'Bye')
66
+ record = SlugLocalizedHistoryRspecModel.create(name: 'Hello')
67
+ expect(record.slug).to start_with('hello--')
68
+ expect(record.slug_en).to start_with('hello-en--')
69
+ end
43
70
  end
71
+ end
44
72
 
45
- it 'find from history' do
46
- record = double('history')
47
- allow(record).to receive(:sluggable_id).and_return(1)
48
- expect(::SimpleSlug::HistorySlug).to receive(:find_by!).with(slug: 'title').and_return(record)
49
- expect(SlugHistoryRspecModel).to receive(:find).with(1).and_return(record)
50
- SlugHistoryRspecModel.friendly_find('title')
73
+ describe 'find' do
74
+ it 'use history' do
75
+ SlugLocalizedHistoryRspecModel.create(id: 1, name: 'Hello').update(name: 'Bye')
76
+ expect(SlugLocalizedHistoryRspecModel.friendly_find('hello')).to be_truthy
51
77
  end
52
78
  end
53
-
54
79
  end
@@ -1,186 +1,192 @@
1
1
  require 'spec_helper'
2
2
 
3
- class SlugGenerationRspecModel < RspecActiveModelBase
3
+ class SlugRspecModel < RspecActiveRecordBase
4
4
  simple_slug :name
5
5
  end
6
6
 
7
- class SlugGenerationRspecModelWithoutValidation < RspecActiveModelBase
8
- simple_slug :name, add_validation: false
7
+ class SlugWithFallbackOnBlankRspecModel < RspecActiveRecordBase
8
+ simple_slug :name, fallback_on_blank: true
9
9
  end
10
10
 
11
- class SlugGenerationRspecModelWithoutCallback < RspecActiveModelBase
12
- simple_slug :name, callback_type: nil
11
+ class SlugWithoutMaxLengthRspecModel < RspecActiveRecordBase
12
+ simple_slug :name, max_length: nil
13
+ end
14
+
15
+
16
+ class SlugWithoutValidationRspecModel < RspecActiveRecordBase
17
+ simple_slug :name, validation: false
13
18
  end
14
19
 
15
- class SlugGenerationRspecModelLocalized < RspecActiveModelBase
16
- attr_accessor :slug_en, :name_en
17
- alias_method :slug_en_was, :slug_en
20
+ class SlugWithoutCallbackRspecModel < RspecActiveRecordBase
21
+ simple_slug :name, callback_type: nil
22
+ end
18
23
 
19
- simple_slug :name, locales: [nil, :en]
24
+ class SlugLocalizedRspecModel < RspecActiveRecordBase
25
+ simple_slug :name_for_slug, history: true, locales: [nil, :en]
20
26
 
21
- def name
22
- I18n.locale == :en ? name_en : @name
27
+ def name_for_slug
28
+ [name, (I18n.locale unless I18n.locale == I18n.default_locale)].compact.join(' ')
23
29
  end
24
30
  end
25
31
 
26
32
  describe SimpleSlug::ModelAddition do
27
- describe 'slug generation' do
28
- before do
29
- allow_any_instance_of(SlugGenerationRspecModel). to receive(:simple_slug_exists?).and_return(false)
33
+ before :each do
34
+ RspecActiveRecordBase.delete_all
35
+ SimpleSlug::HistorySlug.delete_all
36
+ end
37
+
38
+ describe 'slug' do
39
+ it 'generate on save' do
40
+ expect(SlugRspecModel.create(name: 'Hello').slug).to eq 'hello'
30
41
  end
31
42
 
32
- it 'after save' do
33
- expect(SlugGenerationRspecModel.create(name: 'Hello').slug).to eq 'hello'
43
+ it 'add prefix for numbers' do
44
+ expect(SlugRspecModel.create(name: '123').slug).to eq '_123'
34
45
  end
35
46
 
36
- it 'skip excludes' do
37
- expect(SlugGenerationRspecModel.new(name: 'new')).not_to be_valid
47
+ it 'reject excludes' do
48
+ expect(SlugRspecModel.new(name: 'new')).not_to be_valid
38
49
  end
39
50
 
40
- it 'skip integers' do
41
- expect(SlugGenerationRspecModel.new(name: '123')).not_to be_valid
51
+ it 'reject spaces' do
52
+ expect(SlugRspecModel.new(slug: 'test test')).not_to be_valid
42
53
  end
43
54
 
44
- it 'skip spaces' do
45
- expect(SlugGenerationRspecModel.new(slug: 'test test')).not_to be_valid
55
+ it 'reject punctuation' do
56
+ expect(SlugRspecModel.new(slug: 'test.test')).not_to be_valid
46
57
  end
47
58
 
48
- it 'skip punctuation' do
49
- expect(SlugGenerationRspecModel.new(slug: 'test.test')).not_to be_valid
59
+ it 'fallback to prefixed id on blank slug source' do
60
+ expect(SlugWithFallbackOnBlankRspecModel.create({}).slug).to start_with '__'
50
61
  end
51
62
 
52
- it 'skip slug generation' do
53
- allow_any_instance_of(SlugGenerationRspecModel).to receive(:should_generate_new_slug?).and_return(false)
54
- expect(SlugGenerationRspecModel.create(name: 'Hello').slug).to be_blank
63
+ describe '#should_generate_new_slug?' do
64
+ it 'can omit generation' do
65
+ allow_any_instance_of(SlugRspecModel).to receive(:should_generate_new_slug?).and_return(false)
66
+ expect(SlugRspecModel.create(name: 'Hello').slug).to be_blank
67
+ end
55
68
  end
56
69
  end
57
70
 
58
- describe 'resolve conflicts' do
59
- it 'duplicate slug' do
60
- record = SlugGenerationRspecModel.new(name: 'Hi')
61
- expect(record).to receive(:simple_slug_exists?).once.ordered.with('hi', nil).and_return(true)
62
- expect(record).to receive(:simple_slug_exists?).once.ordered.with(/hi--\d+/, nil).and_return(false)
63
- record.save
64
- expect(record.slug).to start_with('hi--')
71
+ describe 'conflicts' do
72
+ it 'resolve with suffix' do
73
+ SlugRspecModel.create(name: 'Hello')
74
+ record = SlugHistoryRspecModel.create(name: 'Hello')
75
+ expect(record.slug).to start_with('hello--')
65
76
  end
66
77
 
67
- it 'numeric slug' do
68
- record = SlugGenerationRspecModel.new(name: '123')
69
- expect(record).to receive(:simple_slug_exists?).with('_123', nil).and_return(false)
70
- record.save
71
- expect(record.slug).to eq '_123'
78
+ context 'localized' do
79
+ it 'resolve with suffix' do
80
+ SlugLocalizedRspecModel.create(name: 'Hello')
81
+ record = SlugLocalizedRspecModel.create(name: 'Hello')
82
+ expect(record.slug).to start_with('hello--')
83
+ expect(record.slug_en).to start_with('hello-en--')
84
+ end
72
85
  end
73
86
  end
74
87
 
75
88
  describe '#to_param' do
76
89
  before do
77
- allow_any_instance_of(SlugGenerationRspecModel).to receive(:simple_slug_exists?).and_return(false)
90
+ allow_any_instance_of(SlugRspecModel).to receive(:simple_slug_exists?).and_return(false)
78
91
  end
79
92
 
80
- it 'slug if exists' do
81
- expect(SlugGenerationRspecModel.create(name: 'Hello').to_param).to eq 'hello'
93
+ it 'use slug if present' do
94
+ expect(SlugRspecModel.create(name: 'Hello').to_param).to eq 'hello'
82
95
  end
83
96
 
84
- it 'id without slug' do
85
- expect(SlugGenerationRspecModel.create(id: 1).to_param).to eq '1'
97
+ it 'do not use unsaved slug' do
98
+ expect(SlugRspecModel.new(name: 'Hello').to_param).to be_falsey
86
99
  end
87
- end
88
100
 
89
- describe '#friendly_find' do
90
- it '#find if integer like' do
91
- expect(SlugGenerationRspecModel).to receive(:find).with(1)
92
- SlugGenerationRspecModel.friendly_find(1)
101
+ it 'use id if slug blank' do
102
+ expect(SlugRspecModel.create(id: 1).to_param).to eq '1'
93
103
  end
104
+ end
94
105
 
95
- it '#find if numeric string' do
96
- expect(SlugGenerationRspecModel).to receive(:find).with('1')
97
- SlugGenerationRspecModel.friendly_find('1')
106
+ describe 'find' do
107
+ it 'by id on integer like param' do
108
+ expect(SlugRspecModel).to receive(:find).with('1')
109
+ SlugRspecModel.friendly_find('1')
98
110
  end
99
111
 
100
- it 'find by slug' do
101
- expect(SlugGenerationRspecModel).to receive(:find_by!).with('slug' => 'title').and_return(double)
102
- SlugGenerationRspecModel.friendly_find('title')
112
+ it 'by slug' do
113
+ expect(SlugRspecModel).to receive(:find_by!).with('slug' => 'title').and_return(double)
114
+ SlugRspecModel.friendly_find('title')
103
115
  end
104
116
  end
105
117
 
106
118
  describe 'max length' do
107
- before do
108
- allow_any_instance_of(SlugGenerationRspecModel).to receive(:simple_slug_exists?).and_return(false)
109
- end
110
-
111
- after do
112
- SlugGenerationRspecModel.simple_slug_options.delete(:max_length)
113
- end
114
-
115
119
  it 'cuts slug to max length' do
116
- record = SlugGenerationRspecModel.new(name: 'Hello' * 100)
117
- record.simple_slug_generate
118
- expect(record.slug.length).to eq 240
119
- end
120
-
121
- it 'use max length from per model options' do
122
- SlugGenerationRspecModel.simple_slug_options[:max_length] = 100
123
- record = SlugGenerationRspecModel.new(name: 'Hello' * 100)
120
+ record = SlugRspecModel.new(name: 'Hello' * 100)
124
121
  record.simple_slug_generate
125
- expect(record.slug.length).to eq 100
122
+ expect(record.slug.length).to eq 191
126
123
  end
127
124
 
128
- it 'omit max length' do
129
- SimpleSlug.max_length = nil
130
- record = SlugGenerationRspecModel.new(name: 'Hello' * 100)
125
+ it 'return full slug without max_length option' do
126
+ record = SlugWithoutMaxLengthRspecModel.new(name: 'Hello' * 100)
131
127
  record.simple_slug_generate
132
128
  expect(record.slug.length).to eq 500
133
129
  end
134
130
  end
135
131
 
136
- describe 'add_validation' do
137
- it 'skip validation' do
138
- expect(SlugGenerationRspecModelWithoutValidation.validators_on(:slug)).to be_blank
132
+ describe 'validation' do
133
+ it 'optionally skip validations' do
134
+ expect(SlugWithoutValidationRspecModel.validators_on(:slug)).to be_blank
139
135
  end
140
136
  end
141
137
 
142
- describe 'callback_type' do
143
- it 'skip callback' do
144
- expect(SlugGenerationRspecModelWithoutCallback.new).not_to receive(:should_generate_new_slug?)
138
+ describe 'callbacks' do
139
+ it 'optionally skip callback' do
140
+ expect(SlugWithoutCallbackRspecModel.new).not_to receive(:should_generate_new_slug?)
145
141
  end
146
142
  end
147
143
 
148
144
  describe 'localized' do
149
- before do
150
- allow_any_instance_of(SlugGenerationRspecModelLocalized).to receive(:simple_slug_exists?).and_return(false)
151
- end
152
-
153
145
  it 'generate slug for locales' do
154
- record = SlugGenerationRspecModelLocalized.create(name: 'Hello', name_en: 'Hello en')
146
+ record = SlugLocalizedRspecModel.create(name: 'Hello')
155
147
  expect(record.slug).to eq 'hello'
156
148
  expect(record.slug_en).to eq 'hello-en'
157
149
  end
158
150
 
151
+ describe '#should_generate_new_slug?' do
152
+ it 'keep slug when present' do
153
+ record = SlugLocalizedRspecModel.create(name: 'Hello')
154
+ expect{ record.update(name: 'Bye') }.not_to change{ record.slug }
155
+ end
156
+
157
+ it 'generate slug when blank' do
158
+ record = SlugLocalizedRspecModel.create(name: 'Hello')
159
+ record.name = 'bye'
160
+ record.slug_en = nil
161
+ expect{ record.save }.to change{ record.slug_en }.to('bye-en')
162
+ end
163
+ end
164
+
159
165
  describe '#to_param' do
160
- it 'generate not localized for default locale' do
161
- record = SlugGenerationRspecModelLocalized.create(name: 'Hello', name_en: 'Hello en')
166
+ it 'use unlocalized column for default locale' do
167
+ record = SlugLocalizedRspecModel.create(name: 'Hello')
162
168
  expect(record.to_param).to eq 'hello'
163
169
  end
164
170
 
165
- it 'generate localized' do
166
- record = SlugGenerationRspecModelLocalized.create(name: 'Hello', name_en: 'Hello en')
171
+ it 'use localized column for non-default locales' do
172
+ record = SlugLocalizedRspecModel.create(name: 'Hello')
167
173
  I18n.with_locale(:en) do
168
174
  expect(record.to_param).to eq 'hello-en'
169
175
  end
170
176
  end
171
177
  end
172
178
 
173
- describe '#simple_slug_find' do
174
- it 'use default slug column with default locale' do
175
- record = SlugGenerationRspecModelLocalized.create(name: 'Hello', name_en: 'Hello en')
176
- expect(SlugGenerationRspecModelLocalized).to receive(:find_by!).with('slug' => 'hello').and_return(record)
177
- SlugGenerationRspecModelLocalized.simple_slug_find('hello')
179
+ describe 'find' do
180
+ it 'use default slug column for default locale' do
181
+ record = SlugLocalizedRspecModel.create(name: 'Hello')
182
+ expect(SlugLocalizedRspecModel.simple_slug_find('hello')).to eq record
178
183
  end
179
184
 
180
- it 'use localized slug column' do
181
- record = SlugGenerationRspecModelLocalized.create(name: 'Hello', name_en: 'Hello en')
182
- expect(SlugGenerationRspecModelLocalized).to receive(:find_by!).with('slug_en' => 'hello-en').and_return(record)
183
- I18n.with_locale(:en) { SlugGenerationRspecModelLocalized.simple_slug_find('hello-en') }
185
+ it 'use localized slug column for non-default locale' do
186
+ record = SlugLocalizedRspecModel.create(name: 'Hello')
187
+ I18n.with_locale(:en) do
188
+ expect(SlugLocalizedRspecModel.simple_slug_find('hello-en')).to eq record
189
+ end
184
190
  end
185
191
  end
186
192
  end
@@ -1,48 +1,44 @@
1
+ require 'sqlite3'
1
2
  require 'active_record'
2
3
  require 'i18n'
3
4
  require 'active_support/core_ext'
4
5
  require 'byebug'
5
6
  require 'simple_slug'
6
7
 
7
- # just silence warning
8
8
  I18n.enforce_available_locales = false
9
9
  I18n.default_locale = :uk
10
10
 
11
- class RspecActiveModelBase
12
- include ActiveModel::Model
13
- include ActiveModel::AttributeMethods
14
- extend ActiveModel::Callbacks
15
-
16
- include SimpleSlug::ModelAddition
17
-
18
- define_model_callbacks :validation, :save, :destroy
19
-
20
- attr_accessor :id, :slug, :name, :created_at
21
- alias_method :slug_was, :slug
22
-
23
- def self.create(attributes, *)
24
- record = new(attributes)
25
- record.save
26
- record
27
- end
28
-
29
- def save
30
- run_callbacks(:validation) { run_callbacks(:save) { } }
31
- end
32
-
33
- def destroy
34
- run_callbacks(:destroy) { @destroyed = true }
35
- end
36
-
37
- def persisted?
38
- true
11
+ ActiveRecord::Base.establish_connection(
12
+ adapter: 'sqlite3',
13
+ database: ':memory:'
14
+ )
15
+
16
+ # ActiveRecord::Base.logger = Logger.new(STDOUT)
17
+
18
+ RSpec.configure do |config|
19
+ config.before(:suite) do
20
+ ActiveRecord::Migration.verbose = false
21
+
22
+ ActiveRecord::Schema.define do
23
+ create_table :rspec_active_record_bases, force: true do |t|
24
+ t.string :name
25
+ t.string :slug, limit: 191
26
+ t.string :slug_en, limit: 191
27
+ t.timestamps
28
+ end
29
+
30
+ create_table :simple_slug_history_slugs, force: true do |t|
31
+ t.string :slug, null: false, limit: 191
32
+ t.string :locale, limit: 10
33
+ t.integer :sluggable_id, null: false
34
+ t.string :sluggable_type, limit: 50, null: false
35
+ t.timestamps
36
+ end
37
+ end
39
38
  end
39
+ end
40
40
 
41
- def slug_changed?
42
- slug.present?
43
- end
44
41
 
45
- def destroyed?
46
- !!@destroyed
47
- end
48
- end
42
+ class RspecActiveRecordBase < ActiveRecord::Base
43
+ include SimpleSlug::ModelAddition
44
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: simple_slug
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.4
4
+ version: 0.4.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alex Leschenko
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2017-05-27 00:00:00.000000000 Z
11
+ date: 2021-01-11 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -19,7 +19,7 @@ dependencies:
19
19
  version: 4.0.0
20
20
  - - "<"
21
21
  - !ruby/object:Gem::Version
22
- version: '5.2'
22
+ version: '6.2'
23
23
  type: :runtime
24
24
  prerelease: false
25
25
  version_requirements: !ruby/object:Gem::Requirement
@@ -29,35 +29,35 @@ dependencies:
29
29
  version: 4.0.0
30
30
  - - "<"
31
31
  - !ruby/object:Gem::Version
32
- version: '5.2'
32
+ version: '6.2'
33
33
  - !ruby/object:Gem::Dependency
34
34
  name: i18n
35
35
  requirement: !ruby/object:Gem::Requirement
36
36
  requirements:
37
37
  - - "~>"
38
38
  - !ruby/object:Gem::Version
39
- version: '0.7'
39
+ version: 1.8.7
40
40
  type: :runtime
41
41
  prerelease: false
42
42
  version_requirements: !ruby/object:Gem::Requirement
43
43
  requirements:
44
44
  - - "~>"
45
45
  - !ruby/object:Gem::Version
46
- version: '0.7'
46
+ version: 1.8.7
47
47
  - !ruby/object:Gem::Dependency
48
48
  name: bundler
49
49
  requirement: !ruby/object:Gem::Requirement
50
50
  requirements:
51
- - - "~>"
51
+ - - ">="
52
52
  - !ruby/object:Gem::Version
53
- version: '1.5'
53
+ version: '0'
54
54
  type: :development
55
55
  prerelease: false
56
56
  version_requirements: !ruby/object:Gem::Requirement
57
57
  requirements:
58
- - - "~>"
58
+ - - ">="
59
59
  - !ruby/object:Gem::Version
60
- version: '1.5'
60
+ version: '0'
61
61
  - !ruby/object:Gem::Dependency
62
62
  name: rake
63
63
  requirement: !ruby/object:Gem::Requirement
@@ -86,7 +86,21 @@ dependencies:
86
86
  - - ">="
87
87
  - !ruby/object:Gem::Version
88
88
  version: '0'
89
- description: Simple friendly url generator for ActiveRecord with history."
89
+ - !ruby/object:Gem::Dependency
90
+ name: sqlite3
91
+ requirement: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - ">="
94
+ - !ruby/object:Gem::Version
95
+ version: '0'
96
+ type: :development
97
+ prerelease: false
98
+ version_requirements: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - ">="
101
+ - !ruby/object:Gem::Version
102
+ version: '0'
103
+ description: Simple friendly url generator for ActiveRecord with history.
90
104
  email:
91
105
  - leschenko.al@gmail.com
92
106
  executables: []
@@ -101,6 +115,7 @@ files:
101
115
  - Rakefile
102
116
  - db/migrate/20140113000001_create_simple_slug_history_slug.rb
103
117
  - lib/simple_slug.rb
118
+ - lib/simple_slug/adapter.rb
104
119
  - lib/simple_slug/history_slug.rb
105
120
  - lib/simple_slug/model_addition.rb
106
121
  - lib/simple_slug/railtie.rb
@@ -114,7 +129,7 @@ homepage: https://github.com/leschenko/simple_slug
114
129
  licenses:
115
130
  - MIT
116
131
  metadata: {}
117
- post_install_message:
132
+ post_install_message:
118
133
  rdoc_options: []
119
134
  require_paths:
120
135
  - lib
@@ -129,9 +144,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
129
144
  - !ruby/object:Gem::Version
130
145
  version: '0'
131
146
  requirements: []
132
- rubyforge_project:
133
- rubygems_version: 2.5.1
134
- signing_key:
147
+ rubygems_version: 3.0.6
148
+ signing_key:
135
149
  specification_version: 4
136
150
  summary: Friendly url generator with history.
137
151
  test_files: