simple_slug 0.3.3 → 0.4.2

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: c874adc1930625e01a5acd27c1fe86702b83f165
4
- data.tar.gz: 42c385427b5cd8f19414cc8f04425e0303d53cf6
2
+ SHA256:
3
+ metadata.gz: 1a485f4e0407ecb15b0b0a93a58f1cb61a49e7d41e595f4491b26dcc50f12dbd
4
+ data.tar.gz: f35ff289be593ab1d830d2f0e3d507a616d1d559786698260030cdda4e0c5f36
5
5
  SHA512:
6
- metadata.gz: d4509499086e9cd6a8f56d41861c1a83328be626ba89edea4e2a0599f729a81b6345d43064515f16f7d4aee395fceb519878b356470bf44f46051f15c60bd91a
7
- data.tar.gz: e43c614e9f37982a6a7b6a8eec8897161ca097093dade94674e7661cc5655b748b0333301137ecd89fb2860dcdc61a8393b7fa5e9729bf6f54c02bb7f1ae1c34
6
+ metadata.gz: 628e2c7fa28d567ed80e809013546c567924125b5c2e8e5e01bd80fabd2d5c27c90076b4673faafb029055d93a76b02e743d9ae650f51920b3b73b85f5caaef2
7
+ data.tar.gz: 63554fd5ca7d1fdb18054c775b5ee15379c258d1435c5cfeac07bb184995b5c313188d654ba4f4f0e72ecfc0cdc6ba4cc4c0eea832317eafcc989b6e9ab86664
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
@@ -20,3 +21,4 @@ spec/reports
20
21
  test/tmp
21
22
  test/version_tmp
22
23
  tmp
24
+ .byebug_history
@@ -1,4 +1,4 @@
1
1
  language: ruby
2
2
 
3
3
  rvm:
4
- - 2.3.0
4
+ - 2.6.3
data/Gemfile CHANGED
@@ -2,3 +2,5 @@ source 'https://rubygems.org'
2
2
 
3
3
  # Specify your gem's dependencies in simple_slug.gemspec
4
4
  gemspec
5
+
6
+ gem 'byebug'
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
- mattr_accessor :exclude_regexp
13
- @@exclude_regexp = /\A\d+\z/
14
+ mattr_accessor :slug_regexp
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,30 +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
+ slug_regexp: SimpleSlug.slug_regexp,
15
+ min_length: SimpleSlug.min_length,
14
16
  max_length: SimpleSlug.max_length,
15
17
  callback_type: SimpleSlug.callback_type,
16
- add_validation: SimpleSlug.add_validation
18
+ validation: SimpleSlug.validation
17
19
  )
20
+ self.simple_slug_adapter = SimpleSlug::Adapter.new(self)
18
21
 
19
22
  include InstanceMethods
20
23
  extend ClassMethods
21
24
 
22
25
  send(simple_slug_options[:callback_type], :simple_slug_generate, if: :should_generate_new_slug?) if simple_slug_options[:callback_type]
23
26
 
24
- if simple_slug_options[:add_validation]
25
- validates simple_slug_options[:slug_column],
27
+ if simple_slug_options[:validation]
28
+ validates *simple_slug_adapter.column_names,
26
29
  presence: true,
30
+ uniqueness: {case_sensitive: true},
27
31
  exclusion: {in: SimpleSlug.excludes},
28
- format: {without: SimpleSlug.exclude_regexp}
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? }
29
34
  end
30
35
 
31
36
  if simple_slug_options[:history]
32
- after_save :simple_slug_reset_unsaved_slug, :simple_slug_create_history_slug
37
+ after_save :simple_slug_reset, :simple_slug_save_history
33
38
  after_destroy :simple_slug_cleanup_history
34
39
  include InstanceHistoryMethods
35
40
  end
@@ -39,137 +44,46 @@ module SimpleSlug
39
44
  module ClassMethods
40
45
  def simple_slug_find(id_param)
41
46
  return unless id_param
42
- if id_param.is_a?(Integer) || id_param =~ /\A\d+\z/
47
+ if id_param.is_a?(Integer) || id_param =~ SimpleSlug::NUMBER_REGEXP
43
48
  find(id_param)
44
49
  else
45
- finder_method = simple_slug_options[:history] ? :find_by : :find_by!
46
- send(finder_method, simple_slug_options[: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)
47
51
  end
48
52
  end
49
53
 
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)
56
+ end
57
+
50
58
  alias_method :friendly_find, :simple_slug_find
51
59
  end
52
60
 
53
61
  module InstanceMethods
54
62
  def to_param
55
- simple_slug_stored_slug.presence || super
63
+ simple_slug_adapter.get_prev(self).presence || super
56
64
  end
57
65
 
58
66
  def should_generate_new_slug?
59
- return true if simple_slug_options[:history]
60
- return simple_slug_get.blank? unless simple_slug_options[:locales]
61
- simple_slug_options[:locales].any? { |locale| simple_slug_get(locale).blank? }
67
+ simple_slug_adapter.column_names.any?{|cn| send(cn).blank? }
62
68
  end
63
69
 
64
70
  def simple_slug_generate(force=false)
65
- (simple_slug_options[:locales] || [nil]).each do |locale|
66
- simple_slug_generate_for_locale(locale, force)
67
- end
68
- end
69
-
70
- def simple_slug_generate_for_locale(locale=nil, force=false)
71
- simple_slug_with_locale(locale) do
72
- simple_slug = simple_slug_normalize(simple_slug_base)
73
- simple_slug = simple_slug.first(simple_slug_options[:max_length]) if simple_slug_options[:max_length]
74
- return if !force && simple_slug == simple_slug_get(locale).to_s.sub(/--\d+\z/, '')
75
- resolved_simple_slug = simple_slug_resolve(simple_slug, locale)
76
- simple_slug_set(resolved_simple_slug, locale)
77
- end
78
- end
79
-
80
- def simple_slug_with_locale(locale)
81
- if defined? Globalize
82
- Globalize.with_locale(locale) do
83
- I18n.with_locale(locale) { yield }
84
- end
85
- else
86
- I18n.with_locale(locale) { yield }
87
- end
88
- end
89
-
90
- def simple_slug_generate_(force=false, locale=nil)
91
- simple_slug = simple_slug_normalize(simple_slug_base)
92
- simple_slug = simple_slug.first(simple_slug_options[:max_length]) if simple_slug_options[:max_length]
93
- return true if !force && simple_slug == simple_slug_get(locale).to_s.sub(/--\d+\z/, '')
94
- resolved_simple_slug = simple_slug_resolve(simple_slug, locale)
95
- simple_slug_set(resolved_simple_slug, locale)
96
- end
97
-
98
- def simple_slug_base
99
- simple_slug_options[:slug_method].map{|m| send(m).to_s }.reject(&:blank?).join(' ')
100
- end
101
-
102
- def simple_slug_normalize(base)
103
- base = SimpleSlug.normalize_cyrillic(base) unless SimpleSlug::CYRILLIC_LOCALES.include?(I18n.locale)
104
- parameterize_args = ActiveSupport::VERSION::MAJOR > 4 ? {separator: '-'} : '-'
105
- normalized = I18n.transliterate(base).parameterize(parameterize_args).downcase
106
- normalized.to_s =~ SimpleSlug::STARTS_WITH_NUMBER_REGEXP ? "_#{normalized}" : normalized
107
- end
108
-
109
- def simple_slug_resolve(slug_value, locale=nil)
110
- if simple_slug_exists?(slug_value, locale)
111
- loop do
112
- slug_value_with_suffix = simple_slug_next(slug_value)
113
- break slug_value_with_suffix unless simple_slug_exists?(slug_value_with_suffix, locale)
114
- end
115
- else
116
- slug_value
117
- end
118
- end
119
-
120
- def simple_slug_next(slug_value)
121
- "#{slug_value}--#{rand(99999)}"
122
- end
123
-
124
- def simple_slug_exists?(slug_value, locale=nil)
125
- simple_slug_base_exists?(slug_value, locale) || simple_slug_history_exists?(slug_value)
126
- end
127
-
128
- def simple_slug_base_exists?(slug_value, locale=nil)
129
- base_scope = self.class.unscoped.where(simple_slug_column(locale) => slug_value)
130
- base_scope = base_scope.where('id != ?', id) if persisted?
131
- base_scope.exists?
132
- end
133
-
134
- def simple_slug_history_exists?(slug_value)
135
- return false unless simple_slug_options[:history]
136
- base_scope = ::SimpleSlug::HistorySlug.where(sluggable_type: self.class.name, slug: slug_value)
137
- base_scope = base_scope.where('sluggable_id != ?', id) if persisted?
138
- base_scope.exists?
139
- end
140
-
141
- def simple_slug_set(value, locale=nil)
142
- send "#{simple_slug_column(locale)}=", value
143
- end
144
-
145
- def simple_slug_get(locale=nil)
146
- send simple_slug_column(locale)
147
- end
148
-
149
- def simple_slug_stored_slug(locale=nil)
150
- send("#{simple_slug_column(locale)}_was")
151
- end
152
-
153
- def simple_slug_column(locale=nil)
154
- [simple_slug_options[:slug_column], locale].compact.join('_')
71
+ simple_slug_adapter.generate(self, force: force)
155
72
  end
156
73
  end
157
74
 
158
75
  module InstanceHistoryMethods
159
- def simple_slug_reset_unsaved_slug
160
- return true if errors.blank?
161
- simple_slug_set simple_slug_stored_slug
76
+ def simple_slug_reset
77
+ errors.blank? || simple_slug_adapter.reset(self)
162
78
  end
163
79
 
164
80
  def simple_slug_cleanup_history
165
81
  ::SimpleSlug::HistorySlug.where(sluggable_type: self.class.name, sluggable_id: id).delete_all
166
82
  end
167
83
 
168
- def simple_slug_create_history_slug
169
- return true unless slug_changed?
170
- ::SimpleSlug::HistorySlug.where(sluggable_type: self.class.name, sluggable_id: id, slug: simple_slug_get).first_or_create
84
+ def simple_slug_save_history
85
+ simple_slug_adapter.save_history(self)
171
86
  end
172
87
  end
173
-
174
88
  end
175
89
  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.3'
2
+ VERSION = '0.4.2'
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.1'
20
+ spec.add_dependency 'activerecord', '>= 4.0.0', '< 6.2'
22
21
  spec.add_dependency 'i18n', '~> 0.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,21 +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 'exclude regexps' do
14
- expect( SimpleSlug.exclude_regexp).to eq /\A\d+\z/
15
- end
16
-
17
- it 'max length' do
18
- expect( SimpleSlug.max_length).to eq 240
13
+ it 'has max length' do
14
+ expect( SimpleSlug.max_length).to eq 191
19
15
  end
20
16
  end
21
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', sluggable_id: 1, 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,151 +1,193 @@
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
13
  end
14
14
 
15
- class SlugGenerationRspecModelLocalized < RspecActiveModelBase
16
- attr_accessor :slug_en, :name_en
17
- alias_method :slug_en_was, :slug_en
18
15
 
19
- simple_slug :name, locales: [nil, :en]
16
+ class SlugWithoutValidationRspecModel < RspecActiveRecordBase
17
+ simple_slug :name, validation: false
18
+ end
20
19
 
21
- def name
22
- I18n.locale == :en ? name_en : @name
20
+ class SlugWithoutCallbackRspecModel < RspecActiveRecordBase
21
+ simple_slug :name, callback_type: nil
22
+ end
23
+
24
+ class SlugLocalizedRspecModel < RspecActiveRecordBase
25
+ simple_slug :name_for_slug, history: true, locales: [nil, :en]
26
+
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 slug generation' do
45
- allow_any_instance_of(SlugGenerationRspecModel).to receive(:should_generate_new_slug?).and_return(false)
46
- expect(SlugGenerationRspecModel.create(name: 'Hello').slug).to be_blank
55
+ it 'reject punctuation' do
56
+ expect(SlugRspecModel.new(slug: 'test.test')).not_to be_valid
47
57
  end
48
- end
49
58
 
50
- describe 'resolve conflicts' do
51
- it 'duplicate slug' do
52
- record = SlugGenerationRspecModel.new(name: 'Hi')
53
- expect(record).to receive(:simple_slug_exists?).once.ordered.with('hi', nil).and_return(true)
54
- expect(record).to receive(:simple_slug_exists?).once.ordered.with(/hi--\d+/, nil).and_return(false)
55
- record.save
56
- expect(record.slug).to start_with('hi--')
59
+ it 'fallback to prefixed id on blank slug source' do
60
+ expect(SlugWithFallbackOnBlankRspecModel.create({}).slug).to start_with '__'
57
61
  end
58
62
 
59
- it 'numeric slug' do
60
- record = SlugGenerationRspecModel.new(name: '123')
61
- expect(record).to receive(:simple_slug_exists?).with('_123', nil).and_return(false)
62
- record.save
63
- expect(record.slug).to eq '_123'
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
64
68
  end
65
69
  end
66
70
 
67
- describe '#to_param' do
68
- before do
69
- allow_any_instance_of(SlugGenerationRspecModel).to receive(:simple_slug_exists?).and_return(false)
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--')
70
76
  end
71
77
 
72
- it 'slug if exists' do
73
- expect(SlugGenerationRspecModel.create(name: 'Hello').to_param).to eq 'hello'
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
74
85
  end
86
+ end
75
87
 
76
- it 'id without slug' do
77
- expect(SlugGenerationRspecModel.create(id: 1).to_param).to eq '1'
88
+ describe '#to_param' do
89
+ before do
90
+ allow_any_instance_of(SlugRspecModel).to receive(:simple_slug_exists?).and_return(false)
78
91
  end
79
- end
80
92
 
81
- describe '#friendly_find' do
82
- it '#find if integer like' do
83
- expect(SlugGenerationRspecModel).to receive(:find).with(1)
84
- SlugGenerationRspecModel.friendly_find(1)
93
+ it 'use slug if present' do
94
+ expect(SlugRspecModel.create(name: 'Hello').to_param).to eq 'hello'
85
95
  end
86
96
 
87
- it '#find if numeric string' do
88
- expect(SlugGenerationRspecModel).to receive(:find).with('1')
89
- SlugGenerationRspecModel.friendly_find('1')
97
+ it 'do not use unsaved slug' do
98
+ expect(SlugRspecModel.new(name: 'Hello').to_param).to be_falsey
90
99
  end
91
100
 
92
- it 'find by slug' do
93
- expect(SlugGenerationRspecModel).to receive(:find_by!).with('slug' => 'title').and_return(double)
94
- SlugGenerationRspecModel.friendly_find('title')
101
+ it 'use id if slug blank' do
102
+ expect(SlugRspecModel.create(id: 1).to_param).to eq '1'
95
103
  end
96
104
  end
97
105
 
98
- describe 'max length' do
99
- before do
100
- allow_any_instance_of(SlugGenerationRspecModel).to receive(:simple_slug_exists?).and_return(false)
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')
101
110
  end
102
111
 
103
- after do
104
- SlugGenerationRspecModel.simple_slug_options.delete(:max_length)
112
+ it 'by slug' do
113
+ expect(SlugRspecModel).to receive(:find_by!).with('slug' => 'title').and_return(double)
114
+ SlugRspecModel.friendly_find('title')
105
115
  end
116
+ end
106
117
 
118
+ describe 'max length' do
107
119
  it 'cuts slug to max length' do
108
- record = SlugGenerationRspecModel.new(name: 'Hello' * 100)
109
- record.simple_slug_generate
110
- expect(record.slug.length).to eq 240
111
- end
112
-
113
- it 'use max length from per model options' do
114
- SlugGenerationRspecModel.simple_slug_options[:max_length] = 100
115
- record = SlugGenerationRspecModel.new(name: 'Hello' * 100)
120
+ record = SlugRspecModel.new(name: 'Hello' * 100)
116
121
  record.simple_slug_generate
117
- expect(record.slug.length).to eq 100
122
+ expect(record.slug.length).to eq 191
118
123
  end
119
124
 
120
- it 'omit max length' do
121
- SimpleSlug.max_length = nil
122
- record = SlugGenerationRspecModel.new(name: 'Hello' * 100)
125
+ it 'return full slug without max_length option' do
126
+ record = SlugWithoutMaxLengthRspecModel.new(name: 'Hello' * 100)
123
127
  record.simple_slug_generate
124
128
  expect(record.slug.length).to eq 500
125
129
  end
126
130
  end
127
131
 
128
- describe 'add_validation' do
129
- it 'skip validation' do
130
- 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
131
135
  end
132
136
  end
133
137
 
134
- describe 'callback_type' do
135
- it 'skip callback' do
136
- 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?)
137
141
  end
138
142
  end
139
143
 
140
144
  describe 'localized' do
141
- before do
142
- allow_any_instance_of(SlugGenerationRspecModelLocalized).to receive(:simple_slug_exists?).and_return(false)
143
- end
144
-
145
145
  it 'generate slug for locales' do
146
- record = SlugGenerationRspecModelLocalized.create(name: 'Hello', name_en: 'Hello en')
146
+ record = SlugLocalizedRspecModel.create(name: 'Hello')
147
147
  expect(record.slug).to eq 'hello'
148
148
  expect(record.slug_en).to eq 'hello-en'
149
149
  end
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
+
165
+ describe '#to_param' do
166
+ it 'use unlocalized column for default locale' do
167
+ record = SlugLocalizedRspecModel.create(name: 'Hello')
168
+ expect(record.to_param).to eq 'hello'
169
+ end
170
+
171
+ it 'use localized column for non-default locales' do
172
+ record = SlugLocalizedRspecModel.create(name: 'Hello')
173
+ I18n.with_locale(:en) do
174
+ expect(record.to_param).to eq 'hello-en'
175
+ end
176
+ end
177
+ end
178
+
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
183
+ end
184
+
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
190
+ end
191
+ end
150
192
  end
151
193
  end
@@ -1,47 +1,44 @@
1
+ require 'sqlite3'
1
2
  require 'active_record'
2
3
  require 'i18n'
3
4
  require 'active_support/core_ext'
5
+ require 'byebug'
4
6
  require 'simple_slug'
5
7
 
6
- # just silence warning
7
8
  I18n.enforce_available_locales = false
8
9
  I18n.default_locale = :uk
9
10
 
10
- class RspecActiveModelBase
11
- include ActiveModel::Model
12
- include ActiveModel::AttributeMethods
13
- extend ActiveModel::Callbacks
14
-
15
- include SimpleSlug::ModelAddition
16
-
17
- define_model_callbacks :validation, :save, :destroy
18
-
19
- attr_accessor :id, :slug, :name, :created_at
20
- alias_method :slug_was, :slug
21
-
22
- def self.create(attributes, *)
23
- record = new(attributes)
24
- record.save
25
- record
26
- end
27
-
28
- def save
29
- run_callbacks(:validation) { run_callbacks(:save) { } }
30
- end
31
-
32
- def destroy
33
- run_callbacks(:destroy) { @destroyed = true }
34
- end
35
-
36
- def persisted?
37
- 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
38
38
  end
39
+ end
39
40
 
40
- def slug_changed?
41
- slug.present?
42
- end
43
41
 
44
- def destroyed?
45
- !!@destroyed
46
- end
47
- 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.3
4
+ version: 0.4.2
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: 2016-08-29 00:00:00.000000000 Z
11
+ date: 2021-01-10 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.1'
22
+ version: '6.2'
23
23
  type: :runtime
24
24
  prerelease: false
25
25
  version_requirements: !ruby/object:Gem::Requirement
@@ -29,7 +29,7 @@ dependencies:
29
29
  version: 4.0.0
30
30
  - - "<"
31
31
  - !ruby/object:Gem::Version
32
- version: '5.1'
32
+ version: '6.2'
33
33
  - !ruby/object:Gem::Dependency
34
34
  name: i18n
35
35
  requirement: !ruby/object:Gem::Requirement
@@ -48,16 +48,16 @@ dependencies:
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: