simple_slug 0.3.3 → 0.4.2

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: 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: