pseudocephalopod 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/.document ADDED
@@ -0,0 +1,5 @@
1
+ README.rdoc
2
+ lib/**/*.rb
3
+ bin/*
4
+ features/**/*.feature
5
+ LICENSE
data/.gitignore ADDED
@@ -0,0 +1,13 @@
1
+ .DS_Store
2
+ .bundle
3
+ *.tmproj
4
+ tmtags
5
+ *~
6
+ \#*
7
+ .\#*
8
+ *.swp
9
+ coverage
10
+ rdoc
11
+ pkg
12
+ *.gem
13
+ *.gemspec
data/Gemfile ADDED
@@ -0,0 +1,9 @@
1
+ source :gemcutter
2
+
3
+ # Setup gems
4
+ gem "activerecord", "= 3.0.0.beta2"
5
+ gem "reversible_data"
6
+ gem "uuid"
7
+ gem "shoulda"
8
+ gem "sqlite3-ruby", :require => "sqlite3"
9
+ gem "redgreen", :require => nil if RUBY_VERSION < "1.9"
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009 Darcy Laycock
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,117 @@
1
+ # Pseudocephalopod #
2
+
3
+ ## About ###
4
+
5
+ Pseudocephalopod is a simple slug library for ActiveRecord 3.0 plus.
6
+
7
+ It's main features are:
8
+
9
+ 1. A very simple and tested codebase
10
+ 2. Support for slug history (e.g. if a users slug changes, it will record the old slug)
11
+ 3. Simple defaulting for slugs to UUID's (to avoid showing ID's.)
12
+ 4. Built on ActiveRecord 3.0
13
+ 5. If stringex is installed, uses stringex's transliteration stuff
14
+
15
+ Also, it's name is inspired by the Jason Wander series of books which I just happened to be
16
+ reading when I had the need for this.
17
+
18
+ ### Why? ###
19
+
20
+ I love the idea of friendly\_id, and most of the implementation but it felt bloated
21
+ to me and my experiences on getting it to work correctly with Rails 3 left a base taste
22
+ in my mouth / was altogether hacky.
23
+
24
+ Pseudocephalopod is very much inspired by friendly id but with a much simpler codebase
25
+ and built to work on Rails 3 from the start.
26
+
27
+ ## Usage ##
28
+
29
+ Using Pseudocephalopod is simple. In Rails, simply drop this in your gemfile:
30
+
31
+ gem 'pseudocephalopod'
32
+
33
+ Optionally restricting the version.
34
+
35
+ Next, if you wish to use slug history run:
36
+
37
+ $ rails generate pseudocephalopod:slugs
38
+
39
+ Otherwise, when calling is\_sluggable make sure to include :history => false
40
+
41
+ Next, you need to add a cached slug column to your model and add an index. In your migration,
42
+ you'd usually want something like:
43
+
44
+ add_column :users, :cached_slug, :string
45
+ add_index :users, :cached_slug
46
+
47
+ Or, using our build in generator:
48
+
49
+ $ rails generate pseudocephalopod:slug_migration Model
50
+
51
+ Lastly, in your model, call is\_sluggable:
52
+
53
+ class User
54
+ is_sluggable :name
55
+ end
56
+
57
+ is\_sluggable accepts the source method name as a symbol, and an optional has of options including:
58
+
59
+ * _:sync_ - when source column changes, save the result. Defaults to true.
60
+ * _:convertor_ - a symbol (for a method) or block for how to generate the base slug. Defaults to :to\_url if available, parameterize otherwise.
61
+ * _:history_ - use slug history (e.g. if the name changes, it records the previous version in a slugs table). Defaults to true
62
+ * _:uuid_ - If the slug is blank, uses a generated uuid instead. Defaults to true
63
+ * _:slug\_column_ - the column in which to store the slug. Defaults to _:cached\_slug_
64
+ * _:to\_param_ - if true (by default), overrides to_param to use the slug
65
+ * _:use\_cache_ - uses Pseudocephalopod.cache if available to cache any lookups e.g. in memcache.
66
+
67
+ Once installed, it provides the following methods:
68
+
69
+ ### User.find\_using\_slug "some-slug" ###
70
+
71
+ Finds a user from a slug (which can be the record's id, it's cached slug or, if enabled, slug history)
72
+
73
+ ### User.other\_than(record) ###
74
+
75
+ Returns a relationship which returns records other than the given.
76
+
77
+ ### User.with\_cached\_slug(record) ###
78
+
79
+ Returns a relationship which returns records with the given cached slug.
80
+
81
+ ### User#generate\_slug ###
82
+
83
+ Forces the generation of a current slug
84
+
85
+ ### User#generate\_slug! ###
86
+
87
+ Forces the generation of a current slug and saves it
88
+
89
+ ### User#autogenerate\_slug ###
90
+
91
+ Generates a slug if not already present.
92
+
93
+ ### User#has\_better\_slug? ###
94
+
95
+ When found via Model.find\_using\_slug, it will return try
96
+ if there is a better slug available. Intended for use in redirects etc.
97
+
98
+ ## Working on Pseudocephalopod ##
99
+
100
+ To run tests, simply do the following:
101
+
102
+ bundle install
103
+ rake
104
+
105
+ And it's ready!
106
+
107
+ ## Note on Patches/Pull Requests ##
108
+
109
+ * Fork the project.
110
+ * Make your feature addition or bug fix.
111
+ * Add tests for it. This is important so I don't break it in a future version unintentionally.
112
+ * Commit, do not mess with rakefile, version, or history. (if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)
113
+ * Send me a pull request. Bonus points for topic branches.
114
+
115
+ ## Copyright ##
116
+
117
+ Copyright (c) 2010 Darcy Laycock. See LICENSE for details.
data/Rakefile ADDED
@@ -0,0 +1,82 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ require File.expand_path('../lib/pseudocephalopod/version', __FILE__)
5
+
6
+ begin
7
+ require 'jeweler'
8
+ Jeweler::Tasks.new do |gem|
9
+ gem.version = Pseudocephalopod::Version::STRING
10
+ gem.name = "pseudocephalopod"
11
+ gem.summary = %Q{Super simple slugs for ActiveRecord 3.0 and higher, with support for slug history}
12
+ gem.description = %Q{Super simple slugs for ActiveRecord 3.0 and higher, with support for slug history}
13
+ gem.email = "sutto@sutto.net"
14
+ gem.homepage = "http://github.com/Sutto/pseudocephalopod"
15
+ gem.authors = ["Darcy Laycock"]
16
+ gem.add_dependency "activerecord", ">= 3.0.0.beta2"
17
+ gem.add_development_dependency "shoulda", ">= 0"
18
+ gem.add_development_dependency "reversible_data"
19
+ end
20
+ Jeweler::GemcutterTasks.new
21
+ rescue LoadError
22
+ puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
23
+ end
24
+
25
+ require 'rake/testtask'
26
+ Rake::TestTask.new(:test) do |test|
27
+ test.libs << 'lib' << 'test'
28
+ test.pattern = 'test/**/*_test.rb'
29
+ test.verbose = true
30
+ end
31
+
32
+ begin
33
+ require 'rcov/rcovtask'
34
+ Rcov::RcovTask.new do |test|
35
+ test.libs << 'test'
36
+ test.pattern = 'test/**/test_*.rb'
37
+ test.verbose = true
38
+ end
39
+ rescue LoadError
40
+ task :rcov do
41
+ abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov"
42
+ end
43
+ end
44
+
45
+ task :test => :check_dependencies
46
+
47
+ begin
48
+ require 'reek/adapters/rake_task'
49
+ Reek::RakeTask.new do |t|
50
+ t.fail_on_error = true
51
+ t.verbose = false
52
+ t.source_files = 'lib/**/*.rb'
53
+ end
54
+ rescue LoadError
55
+ task :reek do
56
+ abort "Reek is not available. In order to run reek, you must: sudo gem install reek"
57
+ end
58
+ end
59
+
60
+ begin
61
+ require 'roodi'
62
+ require 'roodi_task'
63
+ RoodiTask.new do |t|
64
+ t.verbose = false
65
+ end
66
+ rescue LoadError
67
+ task :roodi do
68
+ abort "Roodi is not available. In order to run roodi, you must: sudo gem install roodi"
69
+ end
70
+ end
71
+
72
+ task :default => :test
73
+
74
+ require 'rake/rdoctask'
75
+ Rake::RDocTask.new do |rdoc|
76
+ version = File.exist?('VERSION') ? File.read('VERSION') : ""
77
+
78
+ rdoc.rdoc_dir = 'rdoc'
79
+ rdoc.title = "pseudocephalopod #{version}"
80
+ rdoc.rdoc_files.include('README*')
81
+ rdoc.rdoc_files.include('lib/**/*.rb')
82
+ end
@@ -0,0 +1,24 @@
1
+ module Pseudocephalopod
2
+ module Generators
3
+ class SlugMigrationGenerator < Rails::Generators::NamedBase
4
+ include Rails::Generators::Migration
5
+
6
+ def self.source_root
7
+ @_ps_source_root ||= File.expand_path("templates", File.dirname(__FILE__))
8
+ end
9
+
10
+ def self.next_migration_number(dirname) #:nodoc:
11
+ if ActiveRecord::Base.timestamped_migrations
12
+ Time.now.utc.strftime("%Y%m%d%H%M%S")
13
+ else
14
+ "%.3d" % (current_migration_number(dirname) + 1)
15
+ end
16
+ end
17
+
18
+ def create_migration_file
19
+ migration_template "migration.rb", "db/migrate/add_cached_slug_to_#{table_name}.rb"
20
+ end
21
+
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,12 @@
1
+ class <%= migration_class_name %> < ActiveRecord::Migration
2
+
3
+ def self.up
4
+ add_column <%= table_name.to_sym.inspect %>, :cached_slug, :string
5
+ add_index <%= table_name.to_sym.inspect %>, :cached_slug
6
+ end
7
+
8
+ def self.down
9
+ remove_column <%= table_name.to_sym.inspect %>, :cached_slug
10
+ end
11
+
12
+ end
@@ -0,0 +1,24 @@
1
+ module Pseudocephalopod
2
+ module Generators
3
+ class SlugsGenerator < Rails::Generators::Base
4
+ include Rails::Generators::Migration
5
+
6
+ def self.source_root
7
+ @_ps_source_root ||= File.expand_path("templates", File.dirname(__FILE__))
8
+ end
9
+
10
+ def self.next_migration_number(dirname) #:nodoc:
11
+ if ActiveRecord::Base.timestamped_migrations
12
+ Time.now.utc.strftime("%Y%m%d%H%M%S")
13
+ else
14
+ "%.3d" % (current_migration_number(dirname) + 1)
15
+ end
16
+ end
17
+
18
+ def create_migration_file
19
+ migration_template "migration.rb", "db/migrate/create_pseudocephalopod_slugs.rb"
20
+ end
21
+
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,20 @@
1
+ class CreatePseudocephalopodSlugs < ActiveRecord::Migration
2
+
3
+ def self.up
4
+ create_table :slugs do |t|
5
+ t.string :scope
6
+ t.string :slug
7
+ t.integer :record_id
8
+ t.datetime :created_at
9
+ end
10
+ add_index :slugs, [:scope, :slug]
11
+ add_index :slugs, [:scope, :record_id]
12
+ add_index :slugs, [:scope, :slug, :created_at]
13
+ add_index :slugs, [:scope, :record_id, :created_at]
14
+ end
15
+
16
+ def self.down
17
+ drop_table :slugs
18
+ end
19
+
20
+ end
@@ -0,0 +1,109 @@
1
+ module Pseudocephalopod
2
+ module ActiveRecordMethods
3
+
4
+ def is_sluggable(source = :name, options = {})
5
+ extend ClassMethods
6
+ include InstanceMethods
7
+ extend Pseudocephalopod::Scopes
8
+ extend Pseudocephalopod::Finders
9
+ options.symbolize_keys!
10
+ # Define the attributes
11
+ class_attribute :cached_slug_column, :slug_source, :slug_source_convertor,
12
+ :default_uuid_slug, :store_slug_history, :sync_slugs, :slug_scope
13
+ attr_accessor :found_via_slug
14
+ # Set attribute values
15
+ set_slug_convertor options[:convertor]
16
+ self.slug_source = source.to_sym
17
+ self.cached_slug_column = (options[:slug_column] || :cached_slug).to_sym
18
+ self.default_uuid_slug = !!options.fetch(:uuid, true)
19
+ self.store_slug_history = !!options.fetch(:history, true)
20
+ self.sync_slugs = !!options.fetch(:sync, true)
21
+ self.slug_scope = options[:slug_scope]
22
+ include Pseudocephalopod::Caching if !!options.fetch(:use_cache, true)
23
+ alias_method :to_param, :to_slug if !!options.fetch(:to_param, true)
24
+ include Pseudocephalopod::SlugHistory if self.store_slug_history
25
+ before_validation :autogenerate_slug
26
+ end
27
+
28
+ module InstanceMethods
29
+
30
+ def to_slug
31
+ cached_slug.present? ? cached_slug : id.to_s
32
+ end
33
+
34
+ def generate_slug
35
+ slug_value = send(self.slug_source)
36
+ slug_value = self.slug_source_convertor.call(slug_value) if slug_value.present?
37
+ if slug_value.present?
38
+ scope = self.class.other_than(self).slug_scope_relation(self)
39
+ slug_value = Pseudocephalopod.next_value(scope, slug_value)
40
+ write_attribute self.cached_slug_column, slug_value
41
+ elsif self.default_uuid_slug
42
+ write_attribute self.cached_slug_column, Pseudocephalopod.generate_uuid_slug
43
+ else
44
+ write_attribute self.cached_slug_column, nil
45
+ end
46
+ end
47
+
48
+ def generate_slug!
49
+ generate_slug
50
+ save :validate => false
51
+ end
52
+
53
+ def autogenerate_slug
54
+ generate_slug if should_generate_slug?
55
+ end
56
+
57
+ def should_generate_slug?
58
+ send(self.cached_slug_column).blank? || (self.sync_slugs && send(:"#{self.slug_source}_changed?"))
59
+ end
60
+
61
+ def has_better_slug?
62
+ found_via_slug.present? && found_via_slug != to_slug
63
+ end
64
+
65
+ def slug_scope_key(nested_scope = [])
66
+ self.class.slug_scope_key(nested_scope)
67
+ end
68
+
69
+ end
70
+
71
+ module ClassMethods
72
+
73
+ def update_all_slugs!
74
+ find_each { |r| r.generate_slug! }
75
+ end
76
+
77
+ def slug_scope_key(nested_scope = [])
78
+ ([table_name, slug_scope] + Array(nested_scope)).flatten.compact.join("|")
79
+ end
80
+
81
+ def slug_scope_relation(record)
82
+ has_slug_scope? ? where(slug_scope => record.send(slug_scope)) : unscoped
83
+ end
84
+
85
+ protected
86
+
87
+ def has_slug_scope?
88
+ self.slug_scope.present?
89
+ end
90
+
91
+ def set_slug_convertor(convertor)
92
+ if convertor.present?
93
+ if convertor.is_a?(Symbol)
94
+ self.slug_source_convertor = proc { |r| r.try(convertor) }
95
+ elsif convertor.respond_to?(:call)
96
+ self.slug_source_convertor = convertor
97
+ end
98
+ else
99
+ if "".respond_to?(:to_url)
100
+ self.slug_source_convertor = proc { |r| r.to_s.to_url }
101
+ else
102
+ self.slug_source_convertor = proc { |r| ActiveSupport::Multibyte::Chars.new(r.to_s).parameterize }
103
+ end
104
+ end
105
+ end
106
+
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,62 @@
1
+ require 'digest/sha2'
2
+
3
+ module Pseudocephalopod
4
+ module Caching
5
+
6
+ def self.included(parent)
7
+ parent.extend ClassMethods
8
+
9
+ parent.class_eval do
10
+ include InstanceMethods
11
+ extend ClassMethods
12
+ after_save :globally_cache_slug
13
+ end
14
+ end
15
+
16
+ module InstanceMethods
17
+ def globally_cache_slug
18
+ return unless send(:"#{self.cached_slug_column}_changed?")
19
+ value = self.to_slug
20
+ self.class.cache_slug_lookup!(value, self) if value.present?
21
+ unless store_slug_history
22
+ value = send(:"#{self.cached_slug_column}_was")
23
+ self.class.cache_slug_lookup!(value, nil)
24
+ end
25
+ end
26
+ end
27
+
28
+ module ClassMethods
29
+
30
+ def find_using_slug(slug, options = {})
31
+ # First, attempt to load an id and then record from the cache.
32
+ if (cached_id = lookup_cached_id_from_slug(slug)).present?
33
+ return find(cached_id, options).tap { |r| r.found_via_slug = slug }
34
+ end
35
+ # Otherwise, fallback to the normal approach.
36
+ super.tap do |record|
37
+ cache_slug_lookup!(slug, record) if record.present?
38
+ end
39
+ end
40
+
41
+ def slug_cache_key(slug)
42
+ [Pseudocephalopod.cache_key_prefix, slug_scope_key(Digest::SHA256.hexdigest(slug.to_s.strip))].compact.join("/")
43
+ end
44
+
45
+ def has_cache_for_slug?(slug)
46
+ lookup_cached_id_from_slug(slug).present?
47
+ end
48
+
49
+ def cache_slug_lookup!(slug, record)
50
+ Pseudocephalopod.cache && Pseudocephalopod.cache.write(slug_cache_key(slug), record && record.id)
51
+ end
52
+
53
+ protected
54
+
55
+ def lookup_cached_id_from_slug(slug)
56
+ Pseudocephalopod.cache && Pseudocephalopod.cache.read(slug_cache_key(slug))
57
+ end
58
+
59
+ end
60
+
61
+ end
62
+ end
@@ -0,0 +1,19 @@
1
+ module Pseudocephalopod
2
+ module Finders
3
+
4
+ def find_using_slug(slug, options = {})
5
+ slug = slug.to_s
6
+ value = nil
7
+ value ||= find_by_id(slug.to_i, options) if slug =~ /\A\d+\Z/
8
+ value ||= with_cached_slug(slug).first(options)
9
+ value ||= find_using_slug_history(slug, options) if store_slug_history
10
+ value.found_via_slug = slug if value.present?
11
+ value
12
+ end
13
+
14
+ def find_using_slug!(slug, options = {})
15
+ find_using_slug(slug, options) or raise ActiveRecord::RecordNotFound
16
+ end
17
+
18
+ end
19
+ end
@@ -0,0 +1,19 @@
1
+ module Pseudocephalopod
2
+ class MemoryCache
3
+
4
+ @@cache = {}
5
+
6
+ def self.write(key, value)
7
+ @@cache[key.to_s] = value
8
+ end
9
+
10
+ def self.read(key)
11
+ @@cache[key.to_s]
12
+ end
13
+
14
+ def self.reset!
15
+ @@cache = {}
16
+ end
17
+
18
+ end
19
+ end
@@ -0,0 +1,9 @@
1
+ module Pseudocephalopod
2
+ class Railtie < Rails::Railtie
3
+
4
+ initializer "pseudocephalopod.initialize_cache" do
5
+ Pseudocephalopod.cache = Rails.cache if Rails.cache.present?
6
+ end
7
+
8
+ end
9
+ end
@@ -0,0 +1,13 @@
1
+ module Pseudocephalopod
2
+ module Scopes
3
+
4
+ def with_cached_slug(slug)
5
+ where(self.cached_slug_column => slug.to_s)
6
+ end
7
+
8
+ def other_than(record)
9
+ record.new_record? ? unscoped : where("#{quoted_table_name}.id != ?", record.id)
10
+ end
11
+
12
+ end
13
+ end
@@ -0,0 +1,32 @@
1
+ module Pseudocephalopod
2
+ class Slug < ActiveRecord::Base
3
+ set_table_name "slugs"
4
+
5
+ validates_presence_of :record_id, :slug, :scope
6
+
7
+ scope :ordered, order('created_at DESC')
8
+ scope :only_slug, select(:slug)
9
+ scope :for_record, lambda { |r| where(:record_id => r.id, :scope => Pseudocephalopod.key_for_scope(r)) }
10
+ scope :for_slug, lambda { |scope, slug| where(:scope=> scope.to_s, :slug => slug.to_s)}
11
+
12
+ def self.id_for(scope, slug)
13
+ ordered.for_slug(scope, slug).first.try(:record_id)
14
+ end
15
+
16
+ def self.record_slug(record, slug)
17
+ scope = Pseudocephalopod.key_for_scope(record)
18
+ # Clear slug history in this scope before recording the new slug
19
+ for_slug(scope, slug).delete_all
20
+ create :scope => scope, :record_id => record.id, :slug => slug.to_s
21
+ end
22
+
23
+ def self.previous_for(record)
24
+ ordered.only_slug.for_record(record).all.map(&:slug)
25
+ end
26
+
27
+ def self.remove_history_for(record)
28
+ for_record(record).delete_all
29
+ end
30
+
31
+ end
32
+ end
@@ -0,0 +1,43 @@
1
+ module Pseudocephalopod
2
+ module SlugHistory
3
+
4
+ def self.included(parent)
5
+ parent.class_eval do
6
+ include InstanceMethods
7
+ extend ClassMethods
8
+ after_save :record_slug_changes
9
+ after_destroy :remove_slug_history
10
+ end
11
+ end
12
+
13
+ module InstanceMethods
14
+
15
+ def previous_slugs
16
+ Pseudocephalopod.previous_slugs_for(self)
17
+ end
18
+
19
+ protected
20
+
21
+ def record_slug_changes
22
+ return unless send(:"#{self.cached_slug_column}_changed?")
23
+ value = send(:"#{self.cached_slug_column}_was")
24
+ Pseudocephalopod.record_slug(self, value) if value.present?
25
+ end
26
+
27
+ def remove_slug_history
28
+ Pseudocephalopod.remove_slug_history_for(self)
29
+ end
30
+
31
+ end
32
+
33
+ module ClassMethods
34
+
35
+ def find_using_slug_history(slug, options = {})
36
+ id = Pseudocephalopod.last_known_slug_id(self, slug)
37
+ id.present? ? find_by_id(id, options) : nil
38
+ end
39
+
40
+ end
41
+
42
+ end
43
+ end
@@ -0,0 +1,8 @@
1
+ module Pseudocephalopod
2
+ module Version
3
+ MAJOR = 0
4
+ MINOR = 1
5
+ PATCH = 0
6
+ STRING = [MAJOR, MINOR, PATCH].join(".")
7
+ end
8
+ end
@@ -0,0 +1,69 @@
1
+ require 'uuid'
2
+
3
+ module Pseudocephalopod
4
+
5
+ class << self
6
+
7
+ attr_accessor :cache_key_prefix, :cache
8
+
9
+ def with_counter(prefix, counter = 0)
10
+ counter < 1 ? prefix : "#{prefix}--#{counter}"
11
+ end
12
+
13
+ def next_value(scope, prefix)
14
+ counter = 0
15
+ slug = self.with_counter(prefix, counter)
16
+ while scope.with_cached_slug(slug).exists?
17
+ counter += 1
18
+ slug = self.with_counter(prefix, counter)
19
+ end
20
+ slug
21
+ end
22
+
23
+ def generate_uuid_slug
24
+ UUID.new.generate
25
+ end
26
+
27
+ def last_known_slug_id(scope, slug)
28
+ Pseudocephalopod::Slug.id_for(Pseudocephalopod.key_for_scope(scope), slug)
29
+ end
30
+
31
+ def record_slug(record, slug)
32
+ Pseudocephalopod::Slug.record_slug(record, slug)
33
+ end
34
+
35
+ def previous_slugs_for(record)
36
+ Pseudocephalopod::Slug.previous_for(record)
37
+ end
38
+
39
+ def remove_slug_history_for(record)
40
+ Pseudocephalopod::Slug.remove_history_for(record)
41
+ end
42
+
43
+ def key_for_scope(scope)
44
+ if scope.respond_to?(:slug_scope_key)
45
+ scope.slug_scope_key
46
+ elsif scope.class.respond_to?(:slug_scope_key)
47
+ scope.class.slug_scope_key
48
+ else
49
+ scope.to_s
50
+ end
51
+ end
52
+
53
+ end
54
+
55
+ self.cache_key_prefix ||= "cached-slugs"
56
+
57
+ autoload :Caching, 'pseudocephalopod/caching'
58
+ autoload :Scopes, 'pseudocephalopod/scopes'
59
+ autoload :Finders, 'pseudocephalopod/finders'
60
+ autoload :SlugHistory, 'pseudocephalopod/slug_history'
61
+ autoload :Slug, 'pseudocephalopod/slug'
62
+ autoload :MemoryCache, 'pseudocephalopod/memory_cache'
63
+
64
+ require 'pseudocephalopod/active_record_methods'
65
+ ActiveRecord::Base.extend Pseudocephalopod::ActiveRecordMethods
66
+
67
+ require 'pseudocephalopod/railtie' if defined?(Rails::Railtie)
68
+
69
+ end
@@ -0,0 +1,60 @@
1
+ require 'helper'
2
+
3
+ class CachingTest < Test::Unit::TestCase
4
+ with_tables :slugs, :users, :unslugged_users do
5
+
6
+ setup { Pseudocephalopod::MemoryCache.reset! }
7
+
8
+ should 'store a cache automatically after finding it' do
9
+ assert !User.has_cache_for_slug?("bob")
10
+ u = User.create :name => "Bob"
11
+ assert User.has_cache_for_slug?("bob")
12
+ Pseudocephalopod::MemoryCache.reset!
13
+ assert !User.has_cache_for_slug?("bob")
14
+ assert_equal u, User.find_using_slug("bob")
15
+ assert User.has_cache_for_slug?("bob")
16
+ # Second find from cache
17
+ assert_equal u, User.find_using_slug("bob")
18
+ end
19
+
20
+ should 'remove cache on models without history' do
21
+ UnsluggedUser.is_sluggable :name, :history => false
22
+ u = UnsluggedUser.create :name => "Bob"
23
+ assert UnsluggedUser.has_cache_for_slug?("bob")
24
+ assert !UnsluggedUser.has_cache_for_slug?("red")
25
+ assert !UnsluggedUser.has_cache_for_slug?("sam")
26
+ u.update_attributes :name => "Red"
27
+ u.update_attributes :name => "Sam"
28
+ assert !UnsluggedUser.has_cache_for_slug?("bob")
29
+ assert !UnsluggedUser.has_cache_for_slug?("red")
30
+ assert UnsluggedUser.has_cache_for_slug?("sam")
31
+ end
32
+
33
+ should 'automatically keep cache entries for history by default' do
34
+ assert !User.has_cache_for_slug?("bob")
35
+ assert !User.has_cache_for_slug?("red")
36
+ assert !User.has_cache_for_slug?("sam")
37
+ user = User.create :name => "bob"
38
+ assert User.has_cache_for_slug?("bob")
39
+ assert !User.has_cache_for_slug?("red")
40
+ assert !User.has_cache_for_slug?("sam")
41
+ user.update_attributes :name => "Red"
42
+ assert User.has_cache_for_slug?("bob")
43
+ assert User.has_cache_for_slug?("red")
44
+ assert !User.has_cache_for_slug?("sam")
45
+ user.update_attributes :name => "Sam"
46
+ assert User.has_cache_for_slug?("bob")
47
+ assert User.has_cache_for_slug?("red")
48
+ assert User.has_cache_for_slug?("sam")
49
+ end
50
+
51
+ should 'allow you to disable caching' do
52
+ UnsluggedUser.is_sluggable :name, :use_cache => false
53
+ assert !UnsluggedUser.respond_to?(:has_cache_for_slug?)
54
+ end
55
+
56
+ # Ensure we destroy the contents of the cache after each test.
57
+ teardown { Pseudocephalopod::MemoryCache.reset! }
58
+
59
+ end
60
+ end
data/test/helper.rb ADDED
@@ -0,0 +1,22 @@
1
+ $KCODE = 'UTF8'
2
+
3
+ require 'rubygems'
4
+ require 'bundler'
5
+ Bundler.setup
6
+ Bundler.require
7
+
8
+ require 'test/unit'
9
+ require 'shoulda'
10
+ require 'redgreen' if RUBY_VERSION < '1.9'
11
+
12
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
13
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
14
+ require 'pseudocephalopod'
15
+ require 'model_definitions'
16
+
17
+ # Use a memory cache for testing.
18
+ Pseudocephalopod.cache = Pseudocephalopod::MemoryCache
19
+
20
+ class Test::Unit::TestCase
21
+ extend ReversibleData::ShouldaMacros
22
+ end
@@ -0,0 +1,89 @@
1
+ require 'helper'
2
+ require 'digest/md5'
3
+
4
+ class IsSluggableTest < Test::Unit::TestCase
5
+ with_tables :slugs, :users, :unslugged_users do
6
+
7
+ should 'correctly sluggify a value' do
8
+ user = User.create(:name => "Bob")
9
+ assert_equal "bob", user.to_param
10
+ assert_equal "bob", user.cached_slug
11
+ end
12
+
13
+ should 'generate a uuid in place of a slug' do
14
+ user = User.create(:name => '')
15
+ assert user.cached_slug.present?
16
+ end
17
+
18
+ should 'return need to generate a slug when the cahced slug is blank' do
19
+ user = User.new(:name => "Ninja Stuff")
20
+ assert user.cached_slug.blank?
21
+ assert user.should_generate_slug?
22
+ user.save
23
+ assert user.cached_slug.present?
24
+ assert !user.should_generate_slug?
25
+ user.name = 'Awesome'
26
+ assert user.should_generate_slug?
27
+ end
28
+
29
+ should 'let you disable syncing a slug' do
30
+ UnsluggedUser.is_sluggable :name, :sync => false
31
+ user = UnsluggedUser.create(:name => "Ninja User")
32
+ assert !user.should_generate_slug?
33
+ user.name = "Another User Name"
34
+ assert !user.should_generate_slug?
35
+ end
36
+
37
+ should 'by default record slug history' do
38
+ user = User.create :name => "Bob"
39
+ assert_equal [], user.previous_slugs
40
+ user.update_attributes! :name => "Sal"
41
+ user.update_attributes! :name => "Red"
42
+ user.update_attributes! :name => "Jim"
43
+ assert_equal ["red", "sal", "bob"], user.previous_slugs
44
+ assert_equal user, User.find_using_slug("red")
45
+ assert_equal user, User.find_using_slug("sal")
46
+ assert_equal user, User.find_using_slug("bob")
47
+ assert_equal user, User.find_using_slug("jim")
48
+ end
49
+
50
+ should 'let you disable recording of slug history' do
51
+ UnsluggedUser.is_sluggable :name, :history => false
52
+ user = UnsluggedUser.create(:name => "Bob")
53
+ assert !user.respond_to?(:previous_slugs)
54
+ user.update_attributes! :name => "Red"
55
+ assert_equal user, UnsluggedUser.find_using_slug("red")
56
+ assert_not_equal user, UnsluggedUser.find_using_slug("bob")
57
+ assert UnsluggedUser.find_using_slug("bob").blank?
58
+ end
59
+
60
+ should "let you find a record by it's id as needed" do
61
+ user = User.create :name => "Bob"
62
+ assert_equal user, User.find_using_slug(user.id)
63
+ assert_equal user, User.find_using_slug(user.id.to_i)
64
+ end
65
+
66
+ should 'default to generate a uuid' do
67
+ user = User.create :name => ""
68
+ assert_match /\A[a-zA-Z0-9]{32}\Z/, user.cached_slug.gsub("-", "")
69
+ user = User.create
70
+ assert_match /\A[a-zA-Z0-9]{32}\Z/, user.cached_slug.gsub("-", "")
71
+ end
72
+
73
+ should 'automatically append a sequence to the end of conflicting slugs' do
74
+ u1 = User.create :name => "ninjas Are awesome"
75
+ u2 = User.create :name => "Ninjas are awesome"
76
+ assert_equal "ninjas-are-awesome", u1.to_slug
77
+ assert_equal "ninjas-are-awesome--1", u2.to_slug
78
+ end
79
+
80
+ should 'let you find out if there is a better way of finding a slug' do
81
+ user = User.create :name => "Bob"
82
+ user.update_attributes! :name => "Ralph"
83
+ assert !User.find_using_slug("ralph").has_better_slug?
84
+ assert User.find_using_slug("bob").has_better_slug?
85
+ assert User.find_using_slug(user.id).has_better_slug?
86
+ end
87
+
88
+ end
89
+ end
@@ -0,0 +1,28 @@
1
+ require 'reversible_data'
2
+
3
+ ReversibleData.in_memory!
4
+
5
+ # Define models here.
6
+
7
+ ReversibleData.add :slugs do |t|
8
+ t.string :scope
9
+ t.string :slug
10
+ t.integer :record_id
11
+ t.datetime :created_at
12
+ end
13
+
14
+ user = ReversibleData.add :users do |u|
15
+ u.string :name
16
+ u.string :cached_slug
17
+ u.timestamps
18
+ end
19
+
20
+ user.define_model do
21
+ is_sluggable :name
22
+ end
23
+
24
+ ReversibleData.add :unslugged_users do |u|
25
+ u.string :name
26
+ u.string :cached_slug
27
+ u.timestamps
28
+ end
@@ -0,0 +1,13 @@
1
+ require 'helper'
2
+
3
+ class PseudocephalopodTest < Test::Unit::TestCase
4
+
5
+ should 'return the correct counter versions' do
6
+ assert_equal 'awesome', Pseudocephalopod.with_counter('awesome')
7
+ assert_equal 'awesome', Pseudocephalopod.with_counter('awesome', 0)
8
+ assert_equal 'awesome', Pseudocephalopod.with_counter('awesome', -1)
9
+ assert_equal 'awesome--2', Pseudocephalopod.with_counter('awesome', 2)
10
+ assert_equal 'awesome--100', Pseudocephalopod.with_counter('awesome', 100)
11
+ end
12
+
13
+ end
@@ -0,0 +1,30 @@
1
+ require 'helper'
2
+
3
+ class FakedModel
4
+ attr_reader :id
5
+ def self.slug_scope_key; "faked_models"; end
6
+ def initialize(id); @id = id; end
7
+ end
8
+
9
+ class SlugHistoryTest < Test::Unit::TestCase
10
+ with_tables :slugs do
11
+
12
+ setup do
13
+ @record_a = FakedModel.new(12)
14
+ @record_b = FakedModel.new(4)
15
+ Pseudocephalopod.record_slug(@record_a, "awesome")
16
+ Pseudocephalopod.record_slug(@record_b, "awesome-1")
17
+ Pseudocephalopod.record_slug(@record_a, "ninjas")
18
+ end
19
+
20
+ should 'let you lookup a given record id easily' do
21
+ assert Pseudocephalopod.last_known_slug_id(FakedModel, "felafel").blank?
22
+ assert Pseudocephalopod.last_known_slug_id(FakedModel, "ninjas-2").blank?
23
+ assert_equal 12, Pseudocephalopod.last_known_slug_id(FakedModel, "awesome")
24
+ assert_equal 4, Pseudocephalopod.last_known_slug_id(FakedModel, "awesome-1")
25
+ assert_equal 12, Pseudocephalopod.last_known_slug_id(FakedModel, "ninjas")
26
+ end
27
+
28
+ end
29
+
30
+ end
metadata ADDED
@@ -0,0 +1,131 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: pseudocephalopod
3
+ version: !ruby/object:Gem::Version
4
+ prerelease: false
5
+ segments:
6
+ - 0
7
+ - 1
8
+ - 0
9
+ version: 0.1.0
10
+ platform: ruby
11
+ authors:
12
+ - Darcy Laycock
13
+ autorequire:
14
+ bindir: bin
15
+ cert_chain: []
16
+
17
+ date: 2010-04-23 00:00:00 +08:00
18
+ default_executable:
19
+ dependencies:
20
+ - !ruby/object:Gem::Dependency
21
+ name: activerecord
22
+ prerelease: false
23
+ requirement: &id001 !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - ">="
26
+ - !ruby/object:Gem::Version
27
+ segments:
28
+ - 3
29
+ - 0
30
+ - 0
31
+ - beta2
32
+ version: 3.0.0.beta2
33
+ type: :runtime
34
+ version_requirements: *id001
35
+ - !ruby/object:Gem::Dependency
36
+ name: shoulda
37
+ prerelease: false
38
+ requirement: &id002 !ruby/object:Gem::Requirement
39
+ requirements:
40
+ - - ">="
41
+ - !ruby/object:Gem::Version
42
+ segments:
43
+ - 0
44
+ version: "0"
45
+ type: :development
46
+ version_requirements: *id002
47
+ - !ruby/object:Gem::Dependency
48
+ name: reversible_data
49
+ prerelease: false
50
+ requirement: &id003 !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ segments:
55
+ - 0
56
+ version: "0"
57
+ type: :development
58
+ version_requirements: *id003
59
+ description: Super simple slugs for ActiveRecord 3.0 and higher, with support for slug history
60
+ email: sutto@sutto.net
61
+ executables: []
62
+
63
+ extensions: []
64
+
65
+ extra_rdoc_files:
66
+ - LICENSE
67
+ - README.md
68
+ files:
69
+ - .document
70
+ - .gitignore
71
+ - Gemfile
72
+ - LICENSE
73
+ - README.md
74
+ - Rakefile
75
+ - lib/generators/pseudocephalopod/slug_migration/slug_migration_generator.rb
76
+ - lib/generators/pseudocephalopod/slug_migration/templates/migration.rb
77
+ - lib/generators/pseudocephalopod/slugs/slugs_generator.rb
78
+ - lib/generators/pseudocephalopod/slugs/templates/migration.rb
79
+ - lib/pseudocephalopod.rb
80
+ - lib/pseudocephalopod/active_record_methods.rb
81
+ - lib/pseudocephalopod/caching.rb
82
+ - lib/pseudocephalopod/finders.rb
83
+ - lib/pseudocephalopod/memory_cache.rb
84
+ - lib/pseudocephalopod/railtie.rb
85
+ - lib/pseudocephalopod/scopes.rb
86
+ - lib/pseudocephalopod/slug.rb
87
+ - lib/pseudocephalopod/slug_history.rb
88
+ - lib/pseudocephalopod/version.rb
89
+ - test/caching_test.rb
90
+ - test/helper.rb
91
+ - test/is_sluggable_test.rb
92
+ - test/model_definitions.rb
93
+ - test/pseudocephalopod_test.rb
94
+ - test/slug_history_test.rb
95
+ has_rdoc: true
96
+ homepage: http://github.com/Sutto/pseudocephalopod
97
+ licenses: []
98
+
99
+ post_install_message:
100
+ rdoc_options:
101
+ - --charset=UTF-8
102
+ require_paths:
103
+ - lib
104
+ required_ruby_version: !ruby/object:Gem::Requirement
105
+ requirements:
106
+ - - ">="
107
+ - !ruby/object:Gem::Version
108
+ segments:
109
+ - 0
110
+ version: "0"
111
+ required_rubygems_version: !ruby/object:Gem::Requirement
112
+ requirements:
113
+ - - ">="
114
+ - !ruby/object:Gem::Version
115
+ segments:
116
+ - 0
117
+ version: "0"
118
+ requirements: []
119
+
120
+ rubyforge_project:
121
+ rubygems_version: 1.3.6
122
+ signing_key:
123
+ specification_version: 3
124
+ summary: Super simple slugs for ActiveRecord 3.0 and higher, with support for slug history
125
+ test_files:
126
+ - test/caching_test.rb
127
+ - test/helper.rb
128
+ - test/is_sluggable_test.rb
129
+ - test/model_definitions.rb
130
+ - test/pseudocephalopod_test.rb
131
+ - test/slug_history_test.rb