friendly_id 3.3.3.0 → 4.0.0.beta7

Sign up to get free protection for your applications and to get access to all the features.
Files changed (79) hide show
  1. data/.gitignore +11 -0
  2. data/.travis.yml +24 -0
  3. data/.yardopts +4 -0
  4. data/Changelog.md +9 -10
  5. data/README.md +39 -48
  6. data/Rakefile +56 -58
  7. data/WhatsNew.md +95 -0
  8. data/bench.rb +63 -0
  9. data/friendly_id.gemspec +40 -0
  10. data/gemfiles/Gemfile.rails-3.0.rb +18 -0
  11. data/gemfiles/Gemfile.rails-3.0.rb.lock +52 -0
  12. data/gemfiles/Gemfile.rails-3.1.rb +18 -0
  13. data/gemfiles/Gemfile.rails-3.1.rb.lock +57 -0
  14. data/lib/friendly_id.rb +126 -80
  15. data/lib/friendly_id/active_record_adapter/relation.rb +10 -2
  16. data/lib/friendly_id/active_record_adapter/slugged_model.rb +3 -9
  17. data/lib/friendly_id/base.rb +132 -0
  18. data/lib/friendly_id/configuration.rb +65 -152
  19. data/lib/friendly_id/finder_methods.rb +20 -0
  20. data/lib/friendly_id/history.rb +88 -0
  21. data/lib/friendly_id/migration.rb +18 -0
  22. data/lib/friendly_id/model.rb +22 -0
  23. data/lib/friendly_id/object_utils.rb +40 -0
  24. data/lib/friendly_id/reserved.rb +46 -0
  25. data/lib/friendly_id/scoped.rb +131 -0
  26. data/lib/friendly_id/slug.rb +9 -0
  27. data/lib/friendly_id/slug_sequencer.rb +82 -0
  28. data/lib/friendly_id/slugged.rb +191 -76
  29. data/lib/friendly_id/version.rb +2 -2
  30. data/test/base_test.rb +54 -0
  31. data/test/configuration_test.rb +27 -0
  32. data/test/core_test.rb +30 -0
  33. data/test/databases.yml +19 -0
  34. data/test/helper.rb +88 -0
  35. data/test/history_test.rb +55 -0
  36. data/test/object_utils_test.rb +26 -0
  37. data/test/reserved_test.rb +26 -0
  38. data/test/schema.rb +59 -0
  39. data/test/scoped_test.rb +57 -0
  40. data/test/shared.rb +118 -0
  41. data/test/slugged_test.rb +83 -0
  42. data/test/sti_test.rb +48 -0
  43. metadata +110 -102
  44. data/Contributors.md +0 -46
  45. data/Guide.md +0 -626
  46. data/extras/README.txt +0 -3
  47. data/extras/bench.rb +0 -40
  48. data/extras/extras.rb +0 -38
  49. data/extras/prof.rb +0 -19
  50. data/extras/template-gem.rb +0 -26
  51. data/extras/template-plugin.rb +0 -28
  52. data/generators/friendly_id/friendly_id_generator.rb +0 -30
  53. data/generators/friendly_id/templates/create_slugs.rb +0 -18
  54. data/lib/tasks/friendly_id.rake +0 -19
  55. data/rails/init.rb +0 -2
  56. data/test/active_record_adapter/ar_test_helper.rb +0 -149
  57. data/test/active_record_adapter/basic_slugged_model_test.rb +0 -14
  58. data/test/active_record_adapter/cached_slug_test.rb +0 -76
  59. data/test/active_record_adapter/core.rb +0 -138
  60. data/test/active_record_adapter/custom_normalizer_test.rb +0 -20
  61. data/test/active_record_adapter/custom_table_name_test.rb +0 -22
  62. data/test/active_record_adapter/default_scope_test.rb +0 -30
  63. data/test/active_record_adapter/optimistic_locking_test.rb +0 -18
  64. data/test/active_record_adapter/scoped_model_test.rb +0 -129
  65. data/test/active_record_adapter/simple_test.rb +0 -76
  66. data/test/active_record_adapter/slug_test.rb +0 -34
  67. data/test/active_record_adapter/slugged.rb +0 -33
  68. data/test/active_record_adapter/slugged_status_test.rb +0 -28
  69. data/test/active_record_adapter/sti_test.rb +0 -22
  70. data/test/active_record_adapter/support/database.jdbcsqlite3.yml +0 -2
  71. data/test/active_record_adapter/support/database.mysql.yml +0 -4
  72. data/test/active_record_adapter/support/database.mysql2.yml +0 -4
  73. data/test/active_record_adapter/support/database.postgres.yml +0 -6
  74. data/test/active_record_adapter/support/database.sqlite3.yml +0 -2
  75. data/test/active_record_adapter/support/models.rb +0 -104
  76. data/test/active_record_adapter/tasks_test.rb +0 -82
  77. data/test/compatibility/ancestry/Gemfile.lock +0 -34
  78. data/test/friendly_id_test.rb +0 -96
  79. data/test/test_helper.rb +0 -13
data/bench.rb ADDED
@@ -0,0 +1,63 @@
1
+ require File.expand_path("../test/helper", __FILE__)
2
+ require "ffaker"
3
+
4
+ N = 1000
5
+
6
+ def transaction
7
+ ActiveRecord::Base.transaction { yield ; raise ActiveRecord::Rollback }
8
+ end
9
+
10
+ class Array
11
+ def rand
12
+ self[Kernel.rand(length)]
13
+ end
14
+ end
15
+
16
+ Book = Class.new ActiveRecord::Base
17
+
18
+ class Journalist < ActiveRecord::Base
19
+ extend FriendlyId
20
+ include FriendlyId::Slugged
21
+ friendly_id :name
22
+ end
23
+
24
+ class Manual < ActiveRecord::Base
25
+ extend FriendlyId
26
+ include FriendlyId::History
27
+ friendly_id :name
28
+ end
29
+
30
+ BOOKS = []
31
+ JOURNALISTS = []
32
+ MANUALS = []
33
+
34
+ 100.times do
35
+ name = Faker::Name.name
36
+ BOOKS << (Book.create! :name => name).id
37
+ JOURNALISTS << (Journalist.create! :name => name).friendly_id
38
+ MANUALS << (Manual.create! :name => name).friendly_id
39
+ end
40
+
41
+ Benchmark.bmbm do |x|
42
+ x.report 'find (without FriendlyId)' do
43
+ N.times {Book.find BOOKS.rand}
44
+ end
45
+ x.report 'find (in-table slug)' do
46
+ N.times {Journalist.find JOURNALISTS.rand}
47
+ end
48
+ x.report 'find (external slug)' do
49
+ N.times {Manual.find_by_friendly_id MANUALS.rand}
50
+ end
51
+
52
+ x.report 'insert (without FriendlyId)' do
53
+ N.times {transaction {Book.create :name => Faker::Name.name}}
54
+ end
55
+
56
+ x.report 'insert (in-table-slug)' do
57
+ N.times {transaction {Journalist.create :name => Faker::Name.name}}
58
+ end
59
+
60
+ x.report 'insert (external slug)' do
61
+ N.times {transaction {Manual.create :name => Faker::Name.name}}
62
+ end
63
+ end
@@ -0,0 +1,40 @@
1
+ # encoding: utf-8
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+
4
+ require "friendly_id"
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = "friendly_id"
8
+ s.version = FriendlyId::VERSION
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 "minitest", "~> 2.4.0"
21
+ s.add_development_dependency "mocha", "~> 0.9.12"
22
+ s.add_development_dependency "ffaker", "~> 1.8.0"
23
+ s.add_development_dependency "maruku", "~> 0.6.0"
24
+ s.add_development_dependency "yard", "~> 0.7.2"
25
+
26
+ s.description = <<-EOM
27
+ FriendlyId is the "Swiss Army bulldozer" of slugging and permalink plugins for
28
+ 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
+
32
+ s.post_install_message = <<-EOM
33
+ NOTE: FriendlyId 4.x breaks compatibility with 3.x. If you're upgrading
34
+ from 3.x, please see this document:
35
+
36
+ https://github.com/norman/friendly_id/blob/4.0.0/WhatsNew.md
37
+
38
+ EOM
39
+
40
+ end
@@ -0,0 +1,18 @@
1
+ source :rubygems
2
+
3
+ platform :jruby do
4
+ gem "activerecord-jdbcmysql-adapter"
5
+ gem "activerecord-jdbcpostgresql-adapter"
6
+ gem "activerecord-jdbcsqlite3-adapter"
7
+ end
8
+
9
+ platform :ruby do
10
+ gem "mysql2", "~> 0.2.0"
11
+ gem "pg", "~> 0.11.0"
12
+ gem "sqlite3", "~> 1.3.4"
13
+ end
14
+
15
+ gem "activerecord", "3.0.9"
16
+ gem "minitest", "~> 2.4.0"
17
+ gem "mocha", "~> 0.9.12"
18
+ gem "rake"
@@ -0,0 +1,52 @@
1
+ GEM
2
+ remote: http://rubygems.org/
3
+ specs:
4
+ activemodel (3.0.9)
5
+ activesupport (= 3.0.9)
6
+ builder (~> 2.1.2)
7
+ i18n (~> 0.5.0)
8
+ activerecord (3.0.9)
9
+ activemodel (= 3.0.9)
10
+ activesupport (= 3.0.9)
11
+ arel (~> 2.0.10)
12
+ tzinfo (~> 0.3.23)
13
+ activerecord-jdbc-adapter (1.1.3)
14
+ activerecord-jdbcmysql-adapter (1.1.3)
15
+ activerecord-jdbc-adapter (= 1.1.3)
16
+ jdbc-mysql (~> 5.1.0)
17
+ activerecord-jdbcpostgresql-adapter (1.1.3)
18
+ activerecord-jdbc-adapter (= 1.1.3)
19
+ jdbc-postgres (~> 9.0.0)
20
+ activerecord-jdbcsqlite3-adapter (1.1.3)
21
+ activerecord-jdbc-adapter (= 1.1.3)
22
+ jdbc-sqlite3 (~> 3.7.0)
23
+ activesupport (3.0.9)
24
+ arel (2.0.10)
25
+ builder (2.1.2)
26
+ i18n (0.5.0)
27
+ jdbc-mysql (5.1.13)
28
+ jdbc-postgres (9.0.801)
29
+ jdbc-sqlite3 (3.7.2)
30
+ minitest (2.4.0)
31
+ mocha (0.9.12)
32
+ mysql2 (0.2.11)
33
+ pg (0.11.0)
34
+ rake (0.9.2)
35
+ sqlite3 (1.3.4)
36
+ tzinfo (0.3.29)
37
+
38
+ PLATFORMS
39
+ java
40
+ ruby
41
+
42
+ DEPENDENCIES
43
+ activerecord (= 3.0.9)
44
+ activerecord-jdbcmysql-adapter
45
+ activerecord-jdbcpostgresql-adapter
46
+ activerecord-jdbcsqlite3-adapter
47
+ minitest (~> 2.4.0)
48
+ mocha (~> 0.9.12)
49
+ mysql2 (~> 0.2.0)
50
+ pg (~> 0.11.0)
51
+ rake
52
+ sqlite3 (~> 1.3.4)
@@ -0,0 +1,18 @@
1
+ source :rubygems
2
+
3
+ platform :jruby do
4
+ gem "activerecord-jdbcmysql-adapter"
5
+ gem "activerecord-jdbcpostgresql-adapter"
6
+ gem "activerecord-jdbcsqlite3-adapter"
7
+ end
8
+
9
+ platform :ruby do
10
+ gem "mysql2", "~> 0.3.6"
11
+ gem "pg", "~> 0.11.0"
12
+ gem "sqlite3", "~> 1.3.4"
13
+ end
14
+
15
+ gem "activerecord", "~> 3.1.0.rc5"
16
+ gem "minitest", "~> 2.4.0"
17
+ gem "mocha", "~> 0.9.12"
18
+ gem "rake"
@@ -0,0 +1,57 @@
1
+ GEM
2
+ remote: http://rubygems.org/
3
+ specs:
4
+ activemodel (3.1.0.rc5)
5
+ activesupport (= 3.1.0.rc5)
6
+ bcrypt-ruby (~> 2.1.4)
7
+ builder (~> 3.0.0)
8
+ i18n (~> 0.6)
9
+ activerecord (3.1.0.rc5)
10
+ activemodel (= 3.1.0.rc5)
11
+ activesupport (= 3.1.0.rc5)
12
+ arel (~> 2.1.4)
13
+ tzinfo (~> 0.3.29)
14
+ activerecord-jdbc-adapter (1.1.3)
15
+ activerecord-jdbcmysql-adapter (1.1.3)
16
+ activerecord-jdbc-adapter (= 1.1.3)
17
+ jdbc-mysql (~> 5.1.0)
18
+ activerecord-jdbcpostgresql-adapter (1.1.3)
19
+ activerecord-jdbc-adapter (= 1.1.3)
20
+ jdbc-postgres (~> 9.0.0)
21
+ activerecord-jdbcsqlite3-adapter (1.1.3)
22
+ activerecord-jdbc-adapter (= 1.1.3)
23
+ jdbc-sqlite3 (~> 3.7.0)
24
+ activesupport (3.1.0.rc5)
25
+ multi_json (~> 1.0)
26
+ arel (2.1.4)
27
+ bcrypt-ruby (2.1.4)
28
+ bcrypt-ruby (2.1.4-java)
29
+ builder (3.0.0)
30
+ i18n (0.6.0)
31
+ jdbc-mysql (5.1.13)
32
+ jdbc-postgres (9.0.801)
33
+ jdbc-sqlite3 (3.7.2)
34
+ minitest (2.4.0)
35
+ mocha (0.9.12)
36
+ multi_json (1.0.3)
37
+ mysql2 (0.3.6)
38
+ pg (0.11.0)
39
+ rake (0.9.2)
40
+ sqlite3 (1.3.4)
41
+ tzinfo (0.3.29)
42
+
43
+ PLATFORMS
44
+ java
45
+ ruby
46
+
47
+ DEPENDENCIES
48
+ activerecord (~> 3.1.0.rc5)
49
+ activerecord-jdbcmysql-adapter
50
+ activerecord-jdbcpostgresql-adapter
51
+ activerecord-jdbcsqlite3-adapter
52
+ minitest (~> 2.4.0)
53
+ mocha (~> 0.9.12)
54
+ mysql2 (~> 0.3.6)
55
+ pg (~> 0.11.0)
56
+ rake
57
+ sqlite3 (~> 1.3.4)
data/lib/friendly_id.rb CHANGED
@@ -1,93 +1,139 @@
1
- require "babosa"
2
- require "forwardable"
3
- require "friendly_id/slug_string"
1
+ # encoding: utf-8
2
+ require "thread"
3
+ require "friendly_id/base"
4
+ require "friendly_id/model"
5
+ require "friendly_id/object_utils"
4
6
  require "friendly_id/configuration"
5
- require "friendly_id/status"
6
- require "friendly_id/slugged"
7
-
8
- # FriendlyId is a comprehensive Ruby library for slugging and permalinks with
9
- # ActiveRecord.
10
- # @author Norman Clarke
11
- # @author Emilio Tagua
12
- # @author Adrian Mugnolo
13
- module FriendlyId
7
+ require "friendly_id/finder_methods"
8
+
9
+ =begin
10
+
11
+ == About FriendlyId
12
+
13
+ FriendlyId is an add-on to Ruby's Active Record that allows you to replace ids
14
+ in your URLs with strings:
15
+
16
+ # without FriendlyId
17
+ http://example.com/states/4323454
18
+
19
+ # with FriendlyId
20
+ http://example.com/states/washington
21
+
22
+ It requires few changes to your application code and offers flexibility,
23
+ performance and a well-documented codebase.
24
+
25
+ === Concepts
14
26
 
15
- # An error based on this class is raised when slug generation fails
16
- class SlugGenerationError < StandardError ; end
17
-
18
- # Raised when the slug text is blank.
19
- class BlankError < SlugGenerationError ; end
20
-
21
- # Raised when the slug text is reserved.
22
- class ReservedError < SlugGenerationError ; end
23
-
24
- module Base
25
- # Set up a model to use a friendly_id. This method accepts a hash with
26
- # {FriendlyId::Configuration several possible options}.
27
- #
28
- # @param [#to_sym] method The column or method that should be used as the
29
- # basis of the friendly_id string.
30
- #
31
- # @param [Hash] options For valid configuration options, see
32
- # {FriendlyId::Configuration}.
33
- #
34
- # @example
35
- #
36
- # class User < ActiveRecord::Base
37
- # has_friendly_id :user_name
38
- # end
39
- #
40
- # class Post < ActiveRecord::Base
41
- # has_friendly_id :title, :use_slug => true, :approximate_ascii => true
42
- # end
43
- #
44
- # @see FriendlyId::Configuration
45
- def has_friendly_id(method, options = {})
46
- raise NotImplementedError
27
+ Although FriendlyId helps with URLs, it does all of its work inside your models,
28
+ not your routes.
29
+
30
+ === Simple Models
31
+
32
+ The simplest way to use FriendlyId is with a model that has a uniquely indexed
33
+ column with no spaces or special characters, and that is seldom or never
34
+ updated. The most common example of this is a user name:
35
+
36
+ class User < ActiveRecord::Base
37
+ extend FriendlyId
38
+ friendly_id :login
39
+ validates_format_of :login, :with => /\A[a-z0-9]+\z/i
47
40
  end
48
41
 
49
- # Does the model class use the FriendlyId plugin?
50
- def uses_friendly_id?
51
- respond_to? :friendly_id_config
42
+ @user = User.find "joe" # the old User.find(1) still works, too
43
+ @user.to_param # returns "joe"
44
+ redirect_to @user # the URL will be /users/joe
45
+
46
+ In this case, FriendlyId assumes you want to use the column as-is; it will never
47
+ modify the value of the column, and your application should ensure that the
48
+ value is admissible in a URL:
49
+
50
+ class City < ActiveRecord::Base
51
+ extend FriendlyId
52
+ friendly_id :name
52
53
  end
53
- end
54
54
 
55
- end
55
+ @city.find "Viña del Mar"
56
+ redirect_to @city # the URL will be /cities/Viña%20del%20Mar
56
57
 
57
- class String
58
- def parse_friendly_id(separator = nil)
59
- separator ||= FriendlyId::Configuration::DEFAULTS[:sequence_separator]
60
- name, sequence = split(/#{Regexp.escape(separator)}(\d+)?\z/)
61
- return name, (sequence ||= 1).to_i
62
- end
63
- end
58
+ For this reason, it is often more convenient to use "slugs" rather than a single
59
+ column.
60
+
61
+ === Slugged Models
62
+
63
+ FriendlyId can uses a separate column to store slugs for models which require
64
+ some processing of the friendly_id text. The most common example is a blog
65
+ post's title, which may have spaces, uppercase characters, or other attributes
66
+ you wish to modify to make them more suitable for use in URL's.
64
67
 
65
- class Object
66
-
67
- # Is the object a friendly id? Note that the return value here is
68
- # +false+ if the +id+ is definitely not friendly, and +nil+ if it can
69
- # not be determined.
70
- # The return value will be:
71
- # * +true+ - if the id is definitely friendly (i.e., a string with non-numeric characters)
72
- # * +false+ - if the id is definitely unfriendly (i.e., an Integer, a model instance, etc.)
73
- # * +nil+ - if it can not be determined (i.e., a numeric string like "206".)
74
- # @return [true, false, nil]
75
- # @see #unfriendly?
76
- def friendly_id?
77
- if kind_of?(Integer) or kind_of?(Symbol) or self.class.respond_to? :friendly_id_config
78
- false
79
- elsif to_i.to_s != to_s
80
- true
68
+ class Post < ActiveRecord::Base
69
+ extend FriendlyId
70
+ friendly_id :title, :use => :slugged
81
71
  end
72
+
73
+ @post = Post.create(:title => "This is the first post!")
74
+ @post.friendly_id # returns "this-is-the-first-post"
75
+ redirect_to @post # the URL will be /posts/this-is-the-first-post
76
+
77
+ In general, use slugs by default unless you know for sure you don't need them.
78
+
79
+ @author Norman Clarke
80
+ =end
81
+ module FriendlyId
82
+
83
+ # The current version.
84
+ VERSION = "4.0.0.beta7"
85
+
86
+ @mutex = Mutex.new
87
+
88
+ autoload :History, "friendly_id/history"
89
+ autoload :Reserved, "friendly_id/reserved"
90
+ autoload :Scoped, "friendly_id/scoped"
91
+ autoload :Slugged, "friendly_id/slugged"
92
+
93
+ # FriendlyId takes advantage of `extended` to do basic model setup, primarily
94
+ # extending {FriendlyId::Base} to add {FriendlyId::Base#friendly_id
95
+ # friendly_id} as a class method.
96
+ #
97
+ # Previous versions of FriendlyId simply patched ActiveRecord::Base, but this
98
+ # version tries to be less invasive.
99
+ #
100
+ # In addition to adding {FriendlyId::Base#friendly_id friendly_id}, the class
101
+ # instance variable +@friendly_id_config+ is added. This variable is an
102
+ # instance of an anonymous subclass of {FriendlyId::Configuration}. This
103
+ # allows subsequently loaded modules like {FriendlyId::Slugged} and
104
+ # {FriendlyId::Scoped} to add functionality to the configuration class only
105
+ # for the current class, rather than monkey patching
106
+ # {FriendlyId::Configuration} directly. This isolates other models from large
107
+ # feature changes an addon to FriendlyId could potentially introduce.
108
+ #
109
+ # The upshot of this is, you can htwo Active Record models that both have a
110
+ # @friendly_id_config, but each config object can have different methods and
111
+ # behaviors depending on what modules have been loaded, without conflicts.
112
+ # Keep this in mind if you're hacking on FriendlyId.
113
+ #
114
+ # For examples of this, see the source for {Scoped.included}.
115
+ def self.extended(model_class)
116
+ model_class.instance_eval do
117
+ extend Base
118
+ @friendly_id_config = Class.new(Configuration).new(self)
119
+ FriendlyId.defaults.call @friendly_id_config
120
+ end
121
+ ActiveRecord::Relation.send :include, FinderMethods
82
122
  end
83
123
 
84
- # Is the object a numeric id?
85
- # @return [true, false, nil] +true+ if definitely unfriendly, +false+ if
86
- # definitely friendly, else +nil+.
87
- # @see #friendly?
88
- def unfriendly_id?
89
- val = friendly_id? ; !val unless val.nil?
124
+ # Set global defaults for all models using FriendlyId.
125
+ #
126
+ # The default defaults are to use the +:reserved+ module and nothing else.
127
+ #
128
+ # @example
129
+ # FriendlyId.defaults do |config|
130
+ # config.base = :name
131
+ # config.use :slugged
132
+ # end
133
+ def self.defaults(&block)
134
+ @mutex.synchronize do
135
+ @defaults = block if block_given?
136
+ @defaults ||= lambda {|config| config.use :reserved}
137
+ end
90
138
  end
91
139
  end
92
-
93
- require "friendly_id/railtie" if defined?(Rails) && Rails.version >= "3"