friendly_id4 4.0.0.beta1
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.
- data/.gitignore +11 -0
- data/Guide.md +368 -0
- data/README.md +92 -0
- data/Rakefile +111 -0
- data/WhatsNew.md +142 -0
- data/bench.rb +71 -0
- data/friendly_id.gemspec +31 -0
- data/lib/friendly_id.rb +14 -0
- data/lib/friendly_id/base.rb +29 -0
- data/lib/friendly_id/configuration.rb +31 -0
- data/lib/friendly_id/finder_methods.rb +14 -0
- data/lib/friendly_id/history.rb +27 -0
- data/lib/friendly_id/migration.rb +17 -0
- data/lib/friendly_id/model.rb +22 -0
- data/lib/friendly_id/object_utils.rb +27 -0
- data/lib/friendly_id/scoped.rb +54 -0
- data/lib/friendly_id/slug.rb +3 -0
- data/lib/friendly_id/slug_sequencer.rb +80 -0
- data/lib/friendly_id/slugged.rb +51 -0
- data/lib/friendly_id/version.rb +9 -0
- data/lib/generators/friendly_id_generator.rb +21 -0
- data/test/config/mysql.yml +5 -0
- data/test/config/postgres.yml +6 -0
- data/test/config/sqlite3.yml +3 -0
- data/test/core_test.rb +43 -0
- data/test/helper.rb +86 -0
- data/test/history_test.rb +46 -0
- data/test/object_utils_test.rb +26 -0
- data/test/schema.rb +53 -0
- data/test/scoped_test.rb +49 -0
- data/test/shared.rb +76 -0
- data/test/slugged_test.rb +111 -0
- metadata +163 -0
data/Rakefile
ADDED
@@ -0,0 +1,111 @@
|
|
1
|
+
require "rubygems"
|
2
|
+
require "rake/testtask"
|
3
|
+
|
4
|
+
def rubies(&block)
|
5
|
+
["ruby-1.9.2-p180", "ree-1.8.7-2011.03", "jruby-1.6.2", "rbx-2.0.0pre"].each do |ruby|
|
6
|
+
ENV["RB"] = ruby
|
7
|
+
yield
|
8
|
+
ENV["RB"] = nil
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
def versions(&block)
|
13
|
+
["3.1.0.rc4", "3.0.9"].each do |version|
|
14
|
+
ENV["AR"] = version
|
15
|
+
yield
|
16
|
+
ENV["AR"] = nil
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def adapters(&block)
|
21
|
+
["mysql", "postgres", "sqlite3"].each do |adapter|
|
22
|
+
ENV["DB"] = adapter
|
23
|
+
yield
|
24
|
+
ENV["DB"] = nil
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
task :default => :test
|
29
|
+
|
30
|
+
Rake::TestTask.new do |t|
|
31
|
+
t.test_files = FileList['test/*_test.rb']
|
32
|
+
t.verbose = true
|
33
|
+
end
|
34
|
+
|
35
|
+
task :clean do
|
36
|
+
%x{rm -rf *.gem doc pkg}
|
37
|
+
%x{rm -f `find . -name '*.rbc'`}
|
38
|
+
end
|
39
|
+
|
40
|
+
task :gem do
|
41
|
+
%x{gem build friendly_id.gemspec}
|
42
|
+
end
|
43
|
+
|
44
|
+
task :yard do
|
45
|
+
%x{bundle exec yard doc --files=*.md}
|
46
|
+
end
|
47
|
+
|
48
|
+
desc "Bundle for all supported Ruby/AR versions"
|
49
|
+
task :bundle do
|
50
|
+
rubies do
|
51
|
+
versions do
|
52
|
+
command = "#{ENV["RB"]} -S bundle"
|
53
|
+
puts "#{command} (with #{ENV['AR']})"
|
54
|
+
`#{command}`
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
namespace :test do
|
60
|
+
|
61
|
+
desc "Test with all configured adapters"
|
62
|
+
task :adapters do
|
63
|
+
adapters {|a| puts %x{rake test}}
|
64
|
+
end
|
65
|
+
|
66
|
+
desc "Test with all configured Rubies"
|
67
|
+
task :rubies do
|
68
|
+
rubies {|r| puts %x{rake-#{ENV["RB"]} test}}
|
69
|
+
end
|
70
|
+
|
71
|
+
desc "Test with all configured versions"
|
72
|
+
task :versions do
|
73
|
+
versions {|v| puts %x{rake test}}
|
74
|
+
end
|
75
|
+
|
76
|
+
desc "Test all rubies, versions and adapters"
|
77
|
+
task :prerelease do
|
78
|
+
rubies do
|
79
|
+
versions do
|
80
|
+
adapters do
|
81
|
+
puts %x{rake test}
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
desc "Run each test class in a separate process"
|
88
|
+
task :isolated do
|
89
|
+
dir = File.expand_path("../test", __FILE__)
|
90
|
+
Dir["#{dir}/*_test.rb"].each do |test|
|
91
|
+
puts "Running #{test}:"
|
92
|
+
puts %x{ruby #{test}}
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
namespace :db do
|
98
|
+
desc "Set up the database schema"
|
99
|
+
task :up do
|
100
|
+
require File.expand_path("../test/helper", __FILE__)
|
101
|
+
FriendlyId::Test::Schema.up
|
102
|
+
end
|
103
|
+
|
104
|
+
desc "Destroy the database schema"
|
105
|
+
task :down do
|
106
|
+
require File.expand_path("../test/helper", __FILE__)
|
107
|
+
FriendlyId::Test::Schema.down
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
task :doc => :yard
|
data/WhatsNew.md
ADDED
@@ -0,0 +1,142 @@
|
|
1
|
+
# What's new in FriendlyId 4.0?
|
2
|
+
|
3
|
+
This is a rewrite/rethink of FriendlyId. It will probably be released some time
|
4
|
+
in August or September 2011, once I've had the chance to actually use it in a
|
5
|
+
real website for a while.
|
6
|
+
|
7
|
+
It's probably not wise to use this on a real site right now unless you're
|
8
|
+
comfortable with the source code and willing to fix bugs that will likely occur.
|
9
|
+
|
10
|
+
That said, I will soon be deploying this on a high-traffic, production site, so
|
11
|
+
I have a personal stake in making this work well. Your feedback is most welcome.
|
12
|
+
|
13
|
+
If you want to try it out, grab the source, or [install the
|
14
|
+
gem](https://rubygems.org/gems/friendly_id4).
|
15
|
+
|
16
|
+
## Back to basics
|
17
|
+
|
18
|
+
This isn't the "big rewrite," it's the "small rewrite."
|
19
|
+
|
20
|
+
Adding new features with each release is not sustainable. This release *removes*
|
21
|
+
features, but makes it possible to add them back as addons. We can also remove
|
22
|
+
some complexity by relying on the better default functionality provided by newer
|
23
|
+
versions of Active Support and Active Record. Let's see how small we can make
|
24
|
+
this!
|
25
|
+
|
26
|
+
Here's what's changed:
|
27
|
+
|
28
|
+
## Active Record 3+ only
|
29
|
+
|
30
|
+
For 2.3 support, you can use FriendlyId 3, which will continue to be maintained
|
31
|
+
until people don't want it any more.
|
32
|
+
|
33
|
+
## In-table slugs
|
34
|
+
|
35
|
+
FriendlyId no longer creates a separate slugs table - it just stores the
|
36
|
+
generated slug value in the model table, which is simpler, faster and what most
|
37
|
+
people seem to want. Keeping slug history in a separate table is an optional
|
38
|
+
add-on for FriendlyId 4.
|
39
|
+
|
40
|
+
## No more multiple finds
|
41
|
+
|
42
|
+
Person.find "joe-schmoe" # Supported
|
43
|
+
Person.find ["joe-schmoe", "john-doe"] # No longer supported
|
44
|
+
|
45
|
+
If you want find by more than one friendly id, build your own query:
|
46
|
+
|
47
|
+
Person.where(:slug => ["joe-schmoe", "john-doe"])
|
48
|
+
|
49
|
+
This lets us do *far* less monkeypatching in Active Record. How much less?
|
50
|
+
FriendlyId overrides the base find with a mere 2 lines of code, and otherwise
|
51
|
+
changes nothing else. This means more stability and less breakage between Rails
|
52
|
+
updates.
|
53
|
+
|
54
|
+
## No more finder status
|
55
|
+
|
56
|
+
FriendlyId 3 offered finder statuses to help you determine when an outdated
|
57
|
+
or non-friendly id was used to find the record, so that you could decide whether
|
58
|
+
to permanently redirect to the canonical URL. However, there's a simpler way to
|
59
|
+
do that, so this feature has been removed:
|
60
|
+
|
61
|
+
if request.path != person_path(@person)
|
62
|
+
return redirect_to @person, :status => :moved_permanently
|
63
|
+
end
|
64
|
+
|
65
|
+
## No more slug history - unless you want it
|
66
|
+
|
67
|
+
Since slugs are now stored in-table, when you update them, finds for the
|
68
|
+
previous slug will no longer work. This can be a problem for permalinks, since
|
69
|
+
renaming a friendly_id will lead to 404's.
|
70
|
+
|
71
|
+
This was transparently handled by FriendlyId 3, but there were three problems:
|
72
|
+
|
73
|
+
* Not everybody wants or needs this
|
74
|
+
* Performance was negatively affected
|
75
|
+
* Determining whether a current or old id was used was expensive, clunky, and
|
76
|
+
inconsistent when finding inside relations.
|
77
|
+
|
78
|
+
Here's how to do this in FriendlyId 4:
|
79
|
+
|
80
|
+
class PostsController < ApplicationController
|
81
|
+
|
82
|
+
before_filter :find_post
|
83
|
+
|
84
|
+
...
|
85
|
+
|
86
|
+
def find_post
|
87
|
+
return unless params[:id]
|
88
|
+
@post = begin
|
89
|
+
Post.find params[:id]
|
90
|
+
rescue ActiveRecord::RecordNotFound
|
91
|
+
Post.find_by_friendly_id params[:id]
|
92
|
+
end
|
93
|
+
# If an old id or a numeric id was used to find the record, then
|
94
|
+
# the request path will not match the post_path, and we should do
|
95
|
+
# a 301 redirect that uses the current friendly_id
|
96
|
+
if request.path != post_path(@post)
|
97
|
+
return redirect_to @post, :status => :moved_permanently
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
Under FriendlyId 4 this is a little more verbose, but offers much finer-grained
|
102
|
+
control over the finding process, performs better, and has a much simpler
|
103
|
+
implementation.
|
104
|
+
|
105
|
+
## "Reserved words" are now just a normal validation
|
106
|
+
|
107
|
+
Rather than use a custom reserved words validator, use the validations provided
|
108
|
+
by Active Record. FriendlyId still reserves "new" and "edit" by default to avoid
|
109
|
+
routing problems.
|
110
|
+
|
111
|
+
validates_exclusion_of :name, :in => ["bad", "word"]
|
112
|
+
|
113
|
+
You can configure the default words reserved by FriendlyId in
|
114
|
+
`FriendlyId::Configuration::DEFAULTS[:reserved_words]`.
|
115
|
+
|
116
|
+
## "Allow nil" is now just another validation
|
117
|
+
|
118
|
+
Previous versions of FriendlyId offered a special option to allow nil slug
|
119
|
+
values, but this is now the default. If you don't want this, then simply add a
|
120
|
+
validation to the slug column, and/or declare the column `NOT NULL` in your
|
121
|
+
database.
|
122
|
+
|
123
|
+
## Bye-bye Babosa
|
124
|
+
|
125
|
+
[Babosa](http://github.com/norman/babosa) is FriendlyId 3's slugging library.
|
126
|
+
|
127
|
+
FriendlyId 4 doesn't use it by default because the most important pieces of it
|
128
|
+
were already accepted into Active Support 3.
|
129
|
+
|
130
|
+
However, Babosa is still useful, for example, for idiomatically transliterating
|
131
|
+
Cyrillic ([or other
|
132
|
+
language](https://github.com/norman/babosa/tree/master/lib/babosa/transliterator))
|
133
|
+
strings to ASCII. It's very easy to include - just override
|
134
|
+
`#normalize_friendly_id` in your model:
|
135
|
+
|
136
|
+
class MyModel < ActiveRecord::Base
|
137
|
+
...
|
138
|
+
|
139
|
+
def normalize_friendly_id(text)
|
140
|
+
text.to_slug.normalize! :transliterate => :russian
|
141
|
+
end
|
142
|
+
end
|
data/bench.rb
ADDED
@@ -0,0 +1,71 @@
|
|
1
|
+
require File.expand_path("../test/helper", __FILE__)
|
2
|
+
require "ffaker"
|
3
|
+
require "friendly_id/migration"
|
4
|
+
|
5
|
+
N = 1000
|
6
|
+
|
7
|
+
migration do |m|
|
8
|
+
m.add_column :users, :slug, :string
|
9
|
+
m.add_index :users, :slug, :unique => true
|
10
|
+
end
|
11
|
+
|
12
|
+
migration do |m|
|
13
|
+
m.create_table :posts do |t|
|
14
|
+
t.string :name
|
15
|
+
t.string :slug
|
16
|
+
end
|
17
|
+
m.add_index :posts, :slug, :unique => true
|
18
|
+
end
|
19
|
+
CreateFriendlyIdSlugs.up
|
20
|
+
|
21
|
+
|
22
|
+
class Array
|
23
|
+
def rand
|
24
|
+
self[Kernel.rand(length)]
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
class User
|
29
|
+
include FriendlyId::Slugged
|
30
|
+
has_friendly_id :name
|
31
|
+
end
|
32
|
+
|
33
|
+
class Post < ActiveRecord::Base
|
34
|
+
include FriendlyId::History
|
35
|
+
has_friendly_id :name
|
36
|
+
end
|
37
|
+
|
38
|
+
USERS = []
|
39
|
+
BOOKS = []
|
40
|
+
POSTS = []
|
41
|
+
|
42
|
+
100.times do
|
43
|
+
name = Faker::Name.name
|
44
|
+
USERS << (User.create! :name => name).friendly_id
|
45
|
+
POSTS << (Post.create! :name => name).friendly_id
|
46
|
+
BOOKS << (Book.create! :name => name).id
|
47
|
+
end
|
48
|
+
|
49
|
+
Benchmark.bmbm do |x|
|
50
|
+
x.report 'find (without FriendlyId)' do
|
51
|
+
N.times {Book.find BOOKS.rand}
|
52
|
+
end
|
53
|
+
x.report 'find (in-table slug)' do
|
54
|
+
N.times {User.find USERS.rand}
|
55
|
+
end
|
56
|
+
x.report 'find (external slug)' do
|
57
|
+
N.times {Post.find_by_friendly_id POSTS.rand}
|
58
|
+
end
|
59
|
+
|
60
|
+
x.report 'insert (without FriendlyId)' do
|
61
|
+
N.times {transaction {Book.create :name => Faker::Name.name}}
|
62
|
+
end
|
63
|
+
|
64
|
+
x.report 'insert (in-table-slug)' do
|
65
|
+
N.times {transaction {User.create :name => Faker::Name.name}}
|
66
|
+
end
|
67
|
+
|
68
|
+
x.report 'insert (external slug)' do
|
69
|
+
N.times {transaction {Post.create :name => Faker::Name.name}}
|
70
|
+
end
|
71
|
+
end
|
data/friendly_id.gemspec
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
|
4
|
+
require "friendly_id/version"
|
5
|
+
|
6
|
+
Gem::Specification.new do |s|
|
7
|
+
s.name = "friendly_id4"
|
8
|
+
s.version = FriendlyId::Version::STRING
|
9
|
+
s.authors = ["Norman Clarke"]
|
10
|
+
s.email = ["norman@njclarke.com"]
|
11
|
+
s.homepage = "http://norman.github.com/friendly_id"
|
12
|
+
s.summary = "A comprehensive slugging and pretty-URL plugin."
|
13
|
+
s.rubyforge_project = "friendly_id"
|
14
|
+
s.files = `git ls-files`.split("\n")
|
15
|
+
s.test_files = `git ls-files -- {test}/*`.split("\n")
|
16
|
+
s.require_paths = ["lib"]
|
17
|
+
|
18
|
+
s.add_development_dependency "activerecord", "~> 3.0"
|
19
|
+
s.add_development_dependency "sqlite3", "~> 1.3"
|
20
|
+
s.add_development_dependency "cutest", "~> 1.1.2"
|
21
|
+
s.add_development_dependency "ffaker"
|
22
|
+
s.add_development_dependency "maruku"
|
23
|
+
s.add_development_dependency "yard"
|
24
|
+
s.add_development_dependency "mocha"
|
25
|
+
|
26
|
+
s.description = <<-EOM
|
27
|
+
FriendlyId is the "Swiss Army bulldozer" of slugging and permalink plugins
|
28
|
+
for Ruby on Rails. It allows you to create pretty URL's and work with
|
29
|
+
human-friendly strings as if they were numeric ids for ActiveRecord models.
|
30
|
+
EOM
|
31
|
+
end
|
data/lib/friendly_id.rb
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
require "friendly_id/base"
|
2
|
+
require "friendly_id/model"
|
3
|
+
require "friendly_id/object_utils"
|
4
|
+
require "friendly_id/configuration"
|
5
|
+
require "friendly_id/finder_methods"
|
6
|
+
|
7
|
+
# FriendlyId is a comprehensive Ruby library for ActiveRecord permalinks and
|
8
|
+
# slugs.
|
9
|
+
# @author Norman Clarke
|
10
|
+
module FriendlyId
|
11
|
+
autoload :Slugged, "friendly_id/slugged"
|
12
|
+
autoload :Scoped, "friendly_id/scoped"
|
13
|
+
autoload :History, "friendly_id/history"
|
14
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module FriendlyId
|
2
|
+
# Class methods that will be added to ActiveRecord::Base.
|
3
|
+
module Base
|
4
|
+
extend self
|
5
|
+
|
6
|
+
def has_friendly_id(*args)
|
7
|
+
options = args.extract_options!
|
8
|
+
base = args.shift
|
9
|
+
friendly_id_config.set options.merge(:base => base)
|
10
|
+
include Model
|
11
|
+
# @NOTE: AR-specific code here
|
12
|
+
validates_exclusion_of base, :in => Configuration::DEFAULTS[:reserved_words]
|
13
|
+
before_save do |record|
|
14
|
+
record.instance_eval {@current_friendly_id = friendly_id}
|
15
|
+
end
|
16
|
+
self
|
17
|
+
end
|
18
|
+
|
19
|
+
def friendly_id_config
|
20
|
+
@friendly_id_config ||= Configuration.new(self)
|
21
|
+
end
|
22
|
+
|
23
|
+
def uses_friendly_id?
|
24
|
+
defined? @friendly_id_config
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
ActiveRecord::Base.extend FriendlyId::Base
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module FriendlyId
|
2
|
+
# The configuration paramters passed to +has_friendly_id+ will be stored
|
3
|
+
# in this object.
|
4
|
+
class Configuration
|
5
|
+
attr_accessor :base
|
6
|
+
attr_reader :klass
|
7
|
+
|
8
|
+
DEFAULTS = {
|
9
|
+
:config_error_message => 'FriendlyId has no such config option "%s"',
|
10
|
+
:reserved_words => ["new", "edit"]
|
11
|
+
}
|
12
|
+
|
13
|
+
def initialize(klass, values = nil)
|
14
|
+
@klass = klass
|
15
|
+
set values
|
16
|
+
end
|
17
|
+
|
18
|
+
def method_missing(symbol, *args, &block)
|
19
|
+
option = symbol.to_s.gsub(/=\z/, '')
|
20
|
+
raise ArgumentError, DEFAULTS[:config_error_message] % option
|
21
|
+
end
|
22
|
+
|
23
|
+
def set(values)
|
24
|
+
values and values.each {|name, value| self.send "#{name}=", value}
|
25
|
+
end
|
26
|
+
|
27
|
+
def query_field
|
28
|
+
base
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module FriendlyId
|
2
|
+
# These methods will override the finder methods in ActiveRecord::Relation.
|
3
|
+
module FinderMethods
|
4
|
+
|
5
|
+
protected
|
6
|
+
|
7
|
+
def find_one(id)
|
8
|
+
return super if !@klass.uses_friendly_id? or id.unfriendly_id?
|
9
|
+
where(@klass.friendly_id_config.query_field => id).first or super
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
ActiveRecord::Relation.send :include, FriendlyId::FinderMethods
|