louisville 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +19 -0
  3. data/.rspec +2 -0
  4. data/.ruby-gemset +1 -0
  5. data/.ruby-version +1 -0
  6. data/.ruby-vesion +0 -0
  7. data/.travis.yml +42 -0
  8. data/Gemfile +11 -0
  9. data/LICENSE.txt +22 -0
  10. data/README.md +124 -0
  11. data/Rakefile +9 -0
  12. data/gemfiles/ar30.gemfile +13 -0
  13. data/gemfiles/ar31.gemfile +12 -0
  14. data/gemfiles/ar32.gemfile +12 -0
  15. data/gemfiles/ar40.gemfile +12 -0
  16. data/gemfiles/ar41.gemfile +12 -0
  17. data/gemfiles/ar42.gemfile +12 -0
  18. data/lib/louisville/collision_resolvers/abstract.rb +101 -0
  19. data/lib/louisville/collision_resolvers/none.rb +11 -0
  20. data/lib/louisville/collision_resolvers/numeric_sequence.rb +78 -0
  21. data/lib/louisville/collision_resolvers/string_sequence.rb +48 -0
  22. data/lib/louisville/config.rb +60 -0
  23. data/lib/louisville/extensions/collision.rb +87 -0
  24. data/lib/louisville/extensions/finder.rb +109 -0
  25. data/lib/louisville/extensions/history.rb +59 -0
  26. data/lib/louisville/extensions/setter.rb +44 -0
  27. data/lib/louisville/slug.rb +8 -0
  28. data/lib/louisville/slugger.rb +103 -0
  29. data/lib/louisville/util.rb +39 -0
  30. data/lib/louisville/version.rb +13 -0
  31. data/lib/louisville.rb +28 -0
  32. data/louisville.gemspec +21 -0
  33. data/spec/collision_spec.rb +84 -0
  34. data/spec/column_spec.rb +40 -0
  35. data/spec/finder_spec.rb +70 -0
  36. data/spec/history_spec.rb +96 -0
  37. data/spec/numeric_sequence_spec.rb +36 -0
  38. data/spec/setter_spec.rb +42 -0
  39. data/spec/slugger_spec.rb +89 -0
  40. data/spec/spec_helper.rb +35 -0
  41. data/spec/string_sequence_spec.rb +41 -0
  42. data/spec/support/database.example.yml +6 -0
  43. data/spec/support/database.yml +6 -0
  44. data/spec/support/schema.rb +20 -0
  45. metadata +112 -0
@@ -0,0 +1,87 @@
1
+ #
2
+ # The collision extension handles collisions as part of the save process. It uses a CollisionResolver
3
+ # object to handle the heavy lifting.
4
+ #
5
+ # Provide `collision: true`, or `collision: :name_of_collision_resolver` to your slug() invocation.
6
+ # No options are used.
7
+ #
8
+
9
+ module Louisville
10
+ module Extensions
11
+ module Collision
12
+
13
+
14
+ def self.configure_default_options(options)
15
+ options[:collision] = :string_sequence if options[:collision] == true
16
+ end
17
+
18
+
19
+ def self.included(base)
20
+ base.class_eval do
21
+ alias_method_chain :louisville_slug, :resolver
22
+ alias_method_chain :louisville_slug=, :resolver
23
+ alias_method_chain :louisville_slug_changed?, :resolver
24
+ alias_method_chain :validate_louisville_slug, :resolver
25
+
26
+ before_validation :make_louisville_slug_unique, :if => :should_uniquify_louisville_slug?
27
+ end
28
+ end
29
+
30
+
31
+ def louisville_slug_with_resolver
32
+ louisville_collision_resolver.read_slug
33
+ end
34
+
35
+
36
+
37
+ protected
38
+
39
+
40
+
41
+ def louisville_slug_with_resolver=(val)
42
+ louisville_collision_resolver.assign_slug(val)
43
+ end
44
+
45
+
46
+ def louisville_slug_changed_with_resolver?
47
+ louisville_collision_resolver.slug_changed?
48
+ end
49
+
50
+
51
+ def louisville_collision_resolver
52
+ @louisville_collision_resolver ||= begin
53
+ class_name = louisville_config.options_for(:collision)[:resolver] || louisville_config[:collision]
54
+ klass = Louisville::CollisionResolvers.const_get(:"#{class_name.to_s.classify}")
55
+ klass.new(self, louisville_config.options_for(:collision))
56
+ end
57
+ end
58
+
59
+
60
+ def make_louisville_slug_unique
61
+ return if louisville_collision_resolver.unique?
62
+
63
+ self.louisville_slug = louisville_collision_resolver.next_valid_slug
64
+ end
65
+
66
+
67
+ def should_uniquify_louisville_slug?
68
+ return false if louisville_config.option?(:setter) && desired_louisville_slug
69
+
70
+ louisville_collision_resolver.provides_collision_solution? && louisville_slug_changed?
71
+ end
72
+
73
+
74
+ def validate_louisville_slug_with_resolver
75
+ return false unless validate_louisville_slug_without_resolver
76
+
77
+ unless louisville_collision_resolver.unique?
78
+ self.errors.add(louisville_config[:column], :taken)
79
+ return false
80
+ end
81
+
82
+ true
83
+ end
84
+
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,109 @@
1
+ #
2
+ # The finder extension allows your class to use find('slug') to query for your record.
3
+ # If the history extension is enabled it will also query the history table to see if
4
+ # there are any previous slugs that match.
5
+ #
6
+ # The finder option is enabled by default, to disable provide `finder: false` to your slug() invocation.
7
+ # No options are used.
8
+ #
9
+
10
+ module Louisville
11
+ module Extensions
12
+ module Finder
13
+
14
+
15
+ def self.included(base)
16
+ base.extend ClassMethods
17
+ base.class_eval do
18
+ class << self
19
+ alias_method_chain :relation, :louisville_finder
20
+
21
+ if ActiveRecord::VERSION::MAJOR >= 4 && ActiveRecord::VERSION::MINOR >= 2
22
+ alias_method_chain :find, :louisville_finder
23
+ end
24
+
25
+ end
26
+ end
27
+ end
28
+
29
+
30
+
31
+ module ClassMethods
32
+
33
+ def find_with_louisville_finder(*args)
34
+ return find_without_lousville_finder(*args) if args.length != 1
35
+
36
+ id = args[0]
37
+ id = id.id if ActiveRecord::Base === id
38
+ return find_without_louisville_finder(*args) if Louisville::Util.numeric?(id)
39
+
40
+ relation_with_louisville_finder.find_one(id)
41
+ end
42
+
43
+ private
44
+
45
+ def relation_with_louisville_finder
46
+ rel = relation_without_louisville_finder
47
+ rel.extend RelationMethods unless rel.respond_to?(:find_one_with_louisville)
48
+ rel
49
+ end
50
+ end
51
+
52
+
53
+
54
+ module RelationMethods
55
+
56
+
57
+ def find_one(id)
58
+ id = id.id if ActiveRecord::Base === id
59
+
60
+ return super(id) if Louisville::Util.numeric?(id)
61
+
62
+ seq_column = "#{louisville_config[:column]}_sequence"
63
+
64
+ if self.column_names.include?(seq_column)
65
+ base, seq = Louisville::Util.slug_parts(id)
66
+ record = self.where(louisville_config[:column] => base, seq_column => seq).first
67
+ else
68
+ record = self.where(louisville_config[:column] => id).first
69
+ end
70
+
71
+ return record if record
72
+
73
+ if louisville_config.option?(:history)
74
+
75
+ base, seq = Louisville::Util.slug_parts(id)
76
+
77
+ record = Louisville::Slug.where(:slug_base => base, :slug_sequence => seq, :sluggable_type => ::Louisville::Util.polymorphic_name(self)).select(:sluggable_id).first
78
+ super(record.try(:sluggable_id) || id)
79
+ else
80
+ return super(id)
81
+ end
82
+ end
83
+
84
+
85
+ def exists?(id = :none)
86
+ id = id.id if ActiveRecord::Base === id
87
+
88
+ return super(id) if Louisville::Util.numeric?(id)
89
+
90
+ if String === id
91
+ return true if super(louisville_config[:column] => id)
92
+ return super(id) unless louisville_config.option?(:history)
93
+
94
+ base, seq = Louisville::Util.slug_parts(id)
95
+
96
+ Louisville::Slug.where(:slug_base => base, :slug_sequence => seq, :sluggable_type => ::Louisville::Util.polymorphic_name(name)).exists?
97
+
98
+ elsif ActiveRecord::VERSION::MAJOR == 3
99
+ return super(id == :none ? false : id)
100
+ else
101
+ return super(id)
102
+ end
103
+ end
104
+
105
+ end
106
+
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,59 @@
1
+ #
2
+ # The history extension stores previous slug values in the `slugs` table.
3
+ # It provides instances with a `historical_slugs` association that return Louisville::Slug records.
4
+ # Whenever a slug is changed the previous value is added to the table, the current value is never
5
+ # present in the history table.
6
+ #
7
+ # Provide `history: true` to your slug() invocation.
8
+ # No options are used.
9
+ #
10
+
11
+ module Louisville
12
+ module Extensions
13
+ module History
14
+
15
+
16
+ def self.included(base)
17
+ base.class_eval do
18
+
19
+ # provide an association for easy lookup, joining, etc.
20
+ has_many :historical_slugs, :class_name => 'Louisville::Slug', :dependent => :destroy, :as => :sluggable
21
+
22
+ # If our slug has changed we should manage the history.
23
+ after_save :delete_matching_historical_slug, :if => :louisville_slug_changed?
24
+ after_save :generate_historical_slug, :if => :louisville_slug_changed?
25
+ end
26
+ end
27
+
28
+
29
+
30
+ protected
31
+
32
+
33
+
34
+ # First, we delete any previous slugs that this record owned that match the current slug.
35
+ # This allows a record to return to a previous slug without duplication in the history table.
36
+ def delete_matching_historical_slug
37
+ current_value = self.louisville_slug
38
+
39
+ return unless current_value
40
+
41
+ base, seq = Louisville::Util.slug_parts(current_value)
42
+
43
+ self.historical_slugs.where(:slug_base => base, :slug_sequence => seq).delete_all
44
+ end
45
+
46
+
47
+ # Then we generate a new historical slug for the previous value (if there is one).
48
+ def generate_historical_slug
49
+ previous_value = self.send("#{louisville_config[:column]}_was")
50
+
51
+ return unless previous_value
52
+
53
+ base, seq = Louisville::Util.slug_parts(previous_value)
54
+
55
+ self.historical_slugs.create(:slug_base => base, :slug_sequence => seq)
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,44 @@
1
+ #
2
+ # Enables the slug to be dictated by a setter on the instance. If a setter is provided the collision
3
+ # extension will not uniquify but rather add a validation error.
4
+ #
5
+ # Provide `setter: true` or `setter: :name_of_accessor` to your slug() invocation.
6
+ # No options are used.
7
+ #
8
+
9
+ module Louisville
10
+ module Extensions
11
+ module Setter
12
+
13
+
14
+ def self.configure_default_options(options)
15
+ options[:setter] = "desired_#{options[:column]}" if options[:setter] == true
16
+ end
17
+
18
+
19
+ def self.included(base)
20
+ base.class_eval do
21
+ attr_accessor :desired_louisville_slug
22
+ alias_method :"#{louisville_config[:setter]}=", :desired_louisville_slug=
23
+
24
+ if respond_to?(:accessible_attributes) && accessible_attributes.any?
25
+ attr_accessible :desired_louisville_slug, louisville_config[:setter].to_sym
26
+ end
27
+
28
+ alias_method_chain :extract_louisville_slug_value_from_field, :setter
29
+ end
30
+ end
31
+
32
+
33
+
34
+ protected
35
+
36
+
37
+
38
+ def extract_louisville_slug_value_from_field_with_setter
39
+ self.desired_louisville_slug || extract_louisville_slug_value_from_field_without_setter
40
+ end
41
+
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,8 @@
1
+ module Louisville
2
+ class Slug < ActiveRecord::Base
3
+ self.table_name = :slugs
4
+
5
+ validates :sluggable_type, :sluggable_id, :slug_base, :slug_sequence, :presence => true
6
+ validates :slug_base, :uniqueness => {:scope => [:sluggable_id, :sluggable_type, :slug_sequence]}
7
+ end
8
+ end
@@ -0,0 +1,103 @@
1
+ module Louisville
2
+ module Slugger
3
+
4
+ def self.included(base)
5
+ base.extend ClassMethods
6
+ base.class_eval do
7
+
8
+ before_validation :apply_louisville_slug
9
+
10
+ validate :validate_louisville_slug, :if => :needs_to_validate_louisville_slug?
11
+ end
12
+ end
13
+
14
+
15
+
16
+ module ClassMethods
17
+
18
+ def slug(field, options = {})
19
+ @louisville_slugger = ::Louisville::Config.new(field, options)
20
+ @louisville_slugger.hook!(self)
21
+ @louisville_slugger
22
+ end
23
+
24
+ def louisville_config
25
+ @louisville_slugger || (superclass.respond_to?(:louisville_config) ? superclass.louisville_config : nil)
26
+ end
27
+
28
+ end
29
+
30
+
31
+
32
+ def louisville_slug
33
+ self.send(louisville_config[:column])
34
+ end
35
+
36
+
37
+ def louisville_config
38
+ self.class.louisville_config
39
+ end
40
+
41
+
42
+
43
+ protected
44
+
45
+
46
+
47
+ def louisville_slug=(val)
48
+ self.send("#{louisville_config[:column]}=", val)
49
+ end
50
+
51
+
52
+ def louisville_slug_changed?
53
+ self.send("#{louisville_config[:column]}_changed?")
54
+ end
55
+
56
+
57
+ def apply_louisville_slug
58
+ value = extract_louisville_slug_value_from_field
59
+ value = sanitize_louisville_slug(value) if value
60
+
61
+ # the value may have changed but the parameterized value may be the same
62
+ # charlie vs Charlie.
63
+ if self.louisville_slug
64
+ base = Louisville::Util.slug_base(self.louisville_slug)
65
+
66
+ # if the base hasn't changed let's not set the value since doing so may incur extra cost.
67
+ # namely, the numeric_sequence resolver would have to determine and apply the sequence.
68
+ if base != value
69
+ self.louisville_slug = value
70
+ end
71
+
72
+ else
73
+ self.louisville_slug = value
74
+ end
75
+ end
76
+
77
+
78
+ def sanitize_louisville_slug(value)
79
+ value.parameterize
80
+ end
81
+
82
+
83
+ def extract_louisville_slug_value_from_field
84
+ self.send(louisville_config[:field])
85
+ end
86
+
87
+
88
+ def validate_louisville_slug
89
+
90
+ if louisville_slug.blank?
91
+ errors.add(louisville_config[:column], :blank)
92
+ return false
93
+ end
94
+
95
+ true
96
+ end
97
+
98
+ def needs_to_validate_louisville_slug?
99
+ new_record? || louisville_slug_changed?
100
+ end
101
+
102
+ end
103
+ end
@@ -0,0 +1,39 @@
1
+ module Louisville
2
+ class Util
3
+
4
+
5
+ SLUG_MATCHER = /^(.+)--([\d]+)$/
6
+
7
+
8
+ class << self
9
+
10
+ def numeric?(id)
11
+ Integer === id || !!(id.to_s =~ /^[\d]+$/)
12
+ end
13
+
14
+
15
+ def slug_base(compare)
16
+ compare =~ SLUG_MATCHER
17
+ $1 || compare
18
+ end
19
+
20
+
21
+ def slug_sequence(compare)
22
+ compare =~ SLUG_MATCHER
23
+ [$2.to_i, 1].max
24
+ end
25
+
26
+
27
+ def slug_parts(compare)
28
+ [slug_base(compare), slug_sequence(compare)]
29
+ end
30
+
31
+
32
+ def polymorphic_name(klass)
33
+ klass.base_class.sti_name
34
+ end
35
+
36
+ end
37
+
38
+ end
39
+ end
@@ -0,0 +1,13 @@
1
+ module Louisville
2
+ module VERSION
3
+
4
+ MAJOR = 0
5
+ MINOR = 0
6
+ PATCH = 3
7
+ PRE = nil
8
+
9
+ def self.to_s
10
+ [MAJOR, MINOR, PATCH, PRE].compact.join('.')
11
+ end
12
+ end
13
+ end
data/lib/louisville.rb ADDED
@@ -0,0 +1,28 @@
1
+ require "louisville/version"
2
+
3
+ module Louisville
4
+
5
+ autoload :Config, 'louisville/config'
6
+ autoload :Slug, 'louisville/slug'
7
+ autoload :Slugger, 'louisville/slugger'
8
+ autoload :Util, 'louisville/util'
9
+
10
+ module Extensions
11
+
12
+ autoload :Collision, 'louisville/extensions/collision'
13
+ autoload :Finder, 'louisville/extensions/finder'
14
+ autoload :History, 'louisville/extensions/history'
15
+ autoload :Setter, 'louisville/extensions/setter'
16
+
17
+ end
18
+
19
+ module CollisionResolvers
20
+
21
+ autoload :Abstract, 'louisville/collision_resolvers/abstract'
22
+ autoload :None, 'louisville/collision_resolvers/none'
23
+ autoload :NumericSequence, 'louisville/collision_resolvers/numeric_sequence'
24
+ autoload :StringSequence, 'louisville/collision_resolvers/string_sequence'
25
+
26
+ end
27
+
28
+ end
@@ -0,0 +1,21 @@
1
+ # -*- encoding: utf-8 -*-
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'louisville/version'
5
+
6
+ Gem::Specification.new do |gem|
7
+ gem.name = "louisville"
8
+ gem.version = Louisville::VERSION
9
+ gem.authors = ["Mike Nelson"]
10
+ gem.email = ["mike@mnelson.io"]
11
+ gem.description = %q{A simple and extensible slugging library for ActiveRecord.}
12
+ gem.summary = %q{Simple and Extensible Slugging}
13
+ gem.homepage = "http://github.com/mnelson/louisville"
14
+
15
+ gem.files = `git ls-files`.split($/)
16
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
17
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
18
+ gem.require_paths = ["lib"]
19
+
20
+ gem.add_dependency 'activerecord', '>= 3.0'
21
+ end
@@ -0,0 +1,84 @@
1
+ require 'spec_helper'
2
+
3
+ describe Louisville::Extensions::Collision do
4
+
5
+ class MinimalCollisionUser < ActiveRecord::Base
6
+ self.table_name = :users
7
+
8
+ include Louisville::Slugger
9
+
10
+ slug :name, :collision => :none
11
+ end
12
+
13
+ class MinimalCollisionSequenceUser < ActiveRecord::Base
14
+ self.table_name = :users
15
+
16
+ include Louisville::Slugger
17
+
18
+ slug :name, :collision => :string_sequence, :setter => true
19
+
20
+ end
21
+
22
+ let(:mcu) { MinimalCollisionUser.new }
23
+ let(:mcsu) { MinimalCollisionSequenceUser.new }
24
+ let(:resolver) {
25
+ mcsu.send(:louisville_collision_resolver)
26
+ }
27
+
28
+ it 'should ensure the slug is unique' do
29
+ mcu.name = 'pete'
30
+ expect(mcu.save).to eq(true)
31
+
32
+ u2 = MinimalCollisionUser.new
33
+ u2.name = 'pete'
34
+ expect(u2.save).to eq(false)
35
+
36
+ expect(u2.errors[:slug].to_s).to match(/has already been taken/)
37
+ end
38
+
39
+ it 'should choose a collision resolver based on the config' do
40
+ expect(resolver).to be_a(Louisville::CollisionResolvers::StringSequence)
41
+ end
42
+
43
+ it 'should override the slug reader to read from the resolver' do
44
+ expect(resolver).to receive(:read_slug).once
45
+ mcsu.louisville_slug
46
+ end
47
+
48
+ it 'should override the slug writer to apply via the resolver' do
49
+ expect(resolver).to receive(:assign_slug).with('test').once
50
+ mcsu.send(:louisville_slug=, 'test')
51
+ end
52
+
53
+ context "#should_uniquify_louisville_slug?" do
54
+
55
+ it 'should not uniquify if the resolver does not provide a solution' do
56
+ resolver = double(:provides_collision_solution? => false)
57
+ allow(mcu).to receive(:louisville_collision_resolver){ resolver }
58
+ allow(mcu).to receive(:louisville_slug_changed?){ true }
59
+ expect(mcu.send(:should_uniquify_louisville_slug?)).to eq(false)
60
+ end
61
+
62
+ it 'should uniquify if the resolver provides a solution' do
63
+ resolver = double(:provides_collision_solution? => true)
64
+ allow(mcu).to receive(:louisville_collision_resolver){ resolver }
65
+ allow(mcu).to receive(:louisville_slug_changed?){ true }
66
+ expect(mcu.send(:should_uniquify_louisville_slug?)).to eq(true)
67
+ end
68
+
69
+ it 'should not uniquify if the setter extension is used and present' do
70
+ allow(mcsu).to receive(:louisville_slug_changed?){ true }
71
+ expect(mcsu.send(:should_uniquify_louisville_slug?)).to eq(true)
72
+ mcsu.desired_slug = 'test'
73
+ expect(mcsu.send(:should_uniquify_louisville_slug?)).to eq(false)
74
+ end
75
+
76
+ end
77
+
78
+ context "collision resolver utilities" do
79
+ it "should choose the correct 'latest' slug" do
80
+ expect(resolver.send(:provide_latest_slug, 'dog-b--4', 'dog-b--10')).to eq('dog-b--10')
81
+ end
82
+ end
83
+
84
+ end
@@ -0,0 +1,40 @@
1
+ require 'spec_helper'
2
+
3
+ describe 'Louisville::Slugger column variations' do
4
+
5
+ class ColumnVariationUser < ActiveRecord::Base
6
+ self.table_name = :users
7
+
8
+ include Louisville::Slugger
9
+
10
+ slug :name, :column => :other_slug, :history => true
11
+ end
12
+
13
+ it 'should use the provided column as the storage location' do
14
+ u = ColumnVariationUser.new
15
+ u.name = 'bob'
16
+ expect(u.save).to eq(true)
17
+
18
+ expect(u.slug).to eq(nil)
19
+ expect(u.other_slug).to eq('bob')
20
+ expect(u.other_slug_sequence).to eq(1)
21
+ end
22
+
23
+
24
+ it 'should not impact the history columns' do
25
+ u = ColumnVariationUser.new
26
+ u.name = 'bill'
27
+ expect(u.save).to eq(true)
28
+
29
+ u.name = 'billy'
30
+ expect(u.save).to eq(true)
31
+
32
+ history = Louisville::Slug.last
33
+
34
+ expect(history.slug_base).to eq('bill')
35
+ expect(history.slug_sequence).to eq(1)
36
+
37
+ expect(ColumnVariationUser.find('billy')).to eq(u)
38
+ expect(ColumnVariationUser.find('bill')).to eq(u)
39
+ end
40
+ end