simple_slug 0.3.4 → 0.4.3

Sign up to get free protection for your applications and to get access to all the features.
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: