vanity_slug 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,18 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ .DS_Store
data/Gemfile ADDED
@@ -0,0 +1,12 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in vanity_slug.gemspec
4
+ gemspec
5
+
6
+ group :test do
7
+ gem 'rspec'
8
+ gem 'debugger'
9
+ gem 'activerecord'
10
+ gem 'sqlite3'
11
+ gem 'simplecov', require: false
12
+ end
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 Nick Merwin
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,84 @@
1
+ # VanitySlug
2
+
3
+ Add unique vanity urls to any model without use of redirects.
4
+ Routing trickery via middleware like so:
5
+
6
+ "/my-post-title" => "/posts/1"
7
+ "/the-category" => "/category/2"
8
+
9
+ ## Installation
10
+
11
+ Add this line to your application's Gemfile:
12
+
13
+ gem 'vanity_slug'
14
+
15
+ And then execute:
16
+
17
+ $ bundle
18
+
19
+ Or install it yourself as:
20
+
21
+ $ gem install vanity_slug
22
+
23
+ ## Usage
24
+
25
+ has_vanity_slug
26
+
27
+ ### Options
28
+
29
+ * action: route vanity slug will resolve to, "/posts/:id"
30
+ * field_to_slug: which column to use in vanity slug generation
31
+ * slug_field: which column to store generated slug
32
+ * uniqueness_scope: method or attribute to use as uniqueness check in slug
33
+ generation
34
+
35
+ #### Config
36
+
37
+ ```ruby
38
+ VanitySlug.path_scope Proc.new { |env| { } }
39
+ ```
40
+
41
+ Use to scope the finder based on rack env, i.e. host parameter.
42
+
43
+ ## Examples:
44
+
45
+ ```ruby
46
+ class Post < ActiveRecord::Base
47
+ attr_accessible :title, :site
48
+
49
+ has_vanity_slug action: "/posts/:id",
50
+ field_to_slug: :title,
51
+ uniqueness_scope: :site_id
52
+
53
+ belongs_to :site
54
+ end
55
+
56
+ class Category < ActiveRecord::Base
57
+ attr_accessible :name, :site
58
+
59
+ has_vanity_slug action: "/categories/:id/slug",
60
+ slug_field: :permalink
61
+
62
+ belongs_to :site
63
+ end
64
+
65
+ class Site < ActiveRecord::Base
66
+ attr_accessible :domain
67
+ end
68
+ ```
69
+
70
+ ### Initializer:
71
+
72
+ ```ruby
73
+ VanitySlug.path_scope = Proc.new{|env|
74
+ { organization_id: Organization.find_by_host(env["HTTP_HOST"]).try(:id) }
75
+ }
76
+ ```
77
+
78
+ ## Contributing
79
+
80
+ 1. Fork it
81
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
82
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
83
+ 4. Push to the branch (`git push origin my-new-feature`)
84
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -0,0 +1,10 @@
1
+ require "vanity_slug/version"
2
+ require "vanity_slug/active_record"
3
+
4
+ if defined?(Rails)
5
+ require 'vanity_slug/vanity_router'
6
+ require 'vanity_slug/railtie'
7
+ end
8
+
9
+ module VanitySlug
10
+ end
@@ -0,0 +1,136 @@
1
+ ActiveSupport.on_load :active_record do
2
+ module VanitySlug
3
+ module ActiveRecord
4
+ def has_vanity_slug(opts={})
5
+ unless opts[:do_not_set]
6
+ validate :check_vanity_slug
7
+ before_validation :set_vanity_slug, on: :create
8
+ end
9
+
10
+ opts = {
11
+ field_to_slug: :name,
12
+ slug_field: :slug
13
+ }.merge opts
14
+
15
+ class_attribute :has_vanity_slug_opts
16
+ self.has_vanity_slug_opts = opts
17
+
18
+ class_attribute :field_to_slug
19
+ self.field_to_slug = opts[:field_to_slug]
20
+
21
+ class_attribute :slug_field
22
+ self.slug_field = opts[:slug_field]
23
+
24
+ class_attribute :vanity_action
25
+ self.vanity_action = opts[:vanity_action]
26
+
27
+ VanitySlug.add_class self
28
+
29
+ include InstanceMethods
30
+ end
31
+
32
+ module InstanceMethods
33
+
34
+ def get_vanity_action
35
+ action = self.class.has_vanity_slug_opts[:action]
36
+
37
+ if action.is_a?(Proc)
38
+ self.instance_eval &action
39
+ else
40
+ action.gsub(/:(.+?)(?=\/|$)/){ |s| send s[1..-1] }
41
+ end
42
+ end
43
+
44
+ def set_vanity_slug
45
+ potential_slug = send(self.class.field_to_slug).parameterize
46
+ i = 1
47
+
48
+ if vanity_slug_exists?(potential_slug)
49
+ potential_slug += "-#{i}"
50
+
51
+ while vanity_slug_exists?(potential_slug)
52
+ potential_slug.gsub!(/-\d$/, "-#{i += 1}")
53
+ end
54
+ end
55
+
56
+ self.send "#{self.class.slug_field}=", potential_slug
57
+ end
58
+
59
+ def check_vanity_slug
60
+ slug_to_check = send(self.class.slug_field)
61
+
62
+ exists = VanitySlug.classes.any? do |klass|
63
+ scope = klass.has_vanity_slug_opts[:uniqueness_scope]
64
+ conditions = scope ? { scope => send(scope) } : {}
65
+
66
+ finder = klass.where(conditions.merge({
67
+ klass.slug_field => slug_to_check
68
+ }))
69
+
70
+ finder = finder.where("id != ?", id) if klass == self.class
71
+ finder.count > 0
72
+ end
73
+
74
+ if exists
75
+ errors.add :slug, "already exists"
76
+ end
77
+
78
+ if defined?(Rails)
79
+ begin
80
+ if Rails.application.routes.recognize_path(slug)
81
+ errors.add :slug, "conflicts with another url"
82
+ end
83
+ rescue ActionController::RoutingError
84
+ end
85
+ end
86
+ end
87
+
88
+ def vanity_slug_exists?(potential_slug)
89
+ if defined?(Rails)
90
+ begin
91
+ return true if Rails.application.routes.recognize_path(potential_slug)
92
+ rescue ActionController::RoutingError
93
+ end
94
+ end
95
+
96
+ VanitySlug.classes.any? do |klass|
97
+ scope = klass.has_vanity_slug_opts[:uniqueness_scope]
98
+ conditions = scope ? { scope => send(scope) } : {}
99
+ klass.exists? conditions.merge({ klass.slug_field => potential_slug })
100
+ end
101
+ end
102
+ end
103
+ end
104
+
105
+ class << self
106
+ attr_accessor :path_scope
107
+ attr_accessor :classes
108
+
109
+ def add_class(klass)
110
+ @classes ||= []
111
+ @classes << klass unless @classes.include?(klass)
112
+ end
113
+
114
+ def find(env)
115
+ path = env["PATH_INFO"]
116
+ base_path = File.basename path, ".*"
117
+
118
+ slug = base_path.gsub(/^\//,'')
119
+
120
+ conditions = @path_scope ? @path_scope.call(env) : {}
121
+
122
+ obj = nil
123
+ @classes.any? do |klass|
124
+ obj = klass.where(conditions.merge({ klass.slug_field => slug }))
125
+ .first
126
+ end
127
+ return false unless obj
128
+
129
+ obj.get_vanity_action + File.extname(path)
130
+ end
131
+ end
132
+
133
+ end
134
+
135
+ ActiveRecord::Base.extend VanitySlug::ActiveRecord
136
+ end
@@ -0,0 +1,7 @@
1
+ module VanitySlug
2
+ class Railtie < Rails::Railtie
3
+ initializer "vanity_slug.configure_rails_initialization" do |app|
4
+ app.middleware.use VanitySlug::VanityRouter
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,19 @@
1
+ module VanitySlug
2
+ class VanityRouter
3
+ def initialize(app)
4
+ @app = app
5
+ end
6
+
7
+ def call(env)
8
+ begin
9
+ Rails.application.routes.recognize_path env["PATH_INFO"]
10
+ rescue
11
+ if path = VanitySlug.find(env)
12
+ env["PATH_INFO"] = path
13
+ end
14
+ end
15
+
16
+ @app.call(env)
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,3 @@
1
+ module VanitySlug
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,25 @@
1
+ require 'rubygems'
2
+ require 'bundler/setup'
3
+
4
+ require 'simplecov'
5
+ SimpleCov.start
6
+
7
+ require 'active_support/all'
8
+ require 'vanity_slug'
9
+
10
+ RSpec.configure do |config|
11
+ config.around do |example|
12
+ ActiveRecord::Base.transaction do
13
+ example.run
14
+ raise ActiveRecord::Rollback
15
+ end
16
+ end
17
+ end
18
+
19
+ def silence
20
+ return yield if ENV['silence'] == 'false'
21
+
22
+ silence_stream(STDOUT) do
23
+ yield
24
+ end
25
+ end
@@ -0,0 +1,24 @@
1
+ require 'active_record'
2
+
3
+ silence do
4
+ ActiveRecord::Base.establish_connection adapter: "sqlite3", database: ":memory:"
5
+
6
+ ActiveRecord::Migration.create_table :posts do |t|
7
+ t.string :title
8
+ t.string :slug
9
+ t.integer :site_id
10
+ t.timestamps
11
+ end
12
+
13
+ ActiveRecord::Migration.create_table :categories do |t|
14
+ t.string :name
15
+ t.string :permalink
16
+ t.integer :site_id
17
+ t.timestamps
18
+ end
19
+
20
+ ActiveRecord::Migration.create_table :sites do |t|
21
+ t.string :domain
22
+ t.timestamps
23
+ end
24
+ end
@@ -0,0 +1,84 @@
1
+ require 'spec_helper'
2
+ require 'support/active_record'
3
+
4
+ VanitySlug.path_scope = Proc.new{|env|
5
+ { site_id: Site.find_by_domain(env["HTTP_HOST"]).try(:id) }
6
+ }
7
+
8
+ class Post < ActiveRecord::Base
9
+ attr_accessible :title, :site
10
+
11
+ has_vanity_slug action: "/posts/:id",
12
+ field_to_slug: :title,
13
+ uniqueness_scope: :site_id
14
+
15
+ belongs_to :site
16
+ end
17
+
18
+ class Category < ActiveRecord::Base
19
+ attr_accessible :name, :site
20
+
21
+ has_vanity_slug action: "/categories/:id/slug",
22
+ slug_field: :permalink
23
+
24
+ belongs_to :site
25
+ end
26
+
27
+ class Site < ActiveRecord::Base
28
+ attr_accessible :domain
29
+ end
30
+
31
+ describe VanitySlug do
32
+ context "setting slugs" do
33
+ let(:str) { "slug me" }
34
+ let(:str_slugged) { "slug-me" }
35
+
36
+ before do
37
+ @site_1 = Site.create domain: "a.com"
38
+ @site_2 = Site.create domain: "b.com"
39
+
40
+ @post_1 = Post.create title: str, site: @site_1
41
+ @post_2 = Post.create title: str, site: @site_1
42
+ @post_3 = Post.create title: str, site: @site_2
43
+
44
+ @category_1 = Category.create name: str, site: @site_1
45
+ @category_2 = Category.create name: str, site: @site_2
46
+ end
47
+
48
+ it { @post_1.slug.should eq str_slugged }
49
+ it { @post_2.slug.should eq str_slugged+"-1" }
50
+ it { @post_3.slug.should eq str_slugged }
51
+ it { @category_1.permalink.should eq str_slugged+"-2" }
52
+ it { @category_2.permalink.should eq str_slugged+"-1" }
53
+
54
+ it do
55
+ env = {"HTTP_HOST" => @site_1.domain, "PATH_INFO" => "/#{@post_1.slug}"}
56
+ VanitySlug.find(env).should eq "/posts/#{@post_1.id}"
57
+ end
58
+
59
+ it do
60
+ env = {"HTTP_HOST" => @site_1.domain, "PATH_INFO" => "/#{@post_2.slug}"}
61
+ VanitySlug.find(env).should eq "/posts/#{@post_2.id}"
62
+ end
63
+
64
+ it do
65
+ env = {"HTTP_HOST" => @site_2.domain, "PATH_INFO" => "/#{@post_3.slug}"}
66
+ VanitySlug.find(env).should eq "/posts/#{@post_3.id}"
67
+ end
68
+
69
+ it do
70
+ env = {"HTTP_HOST" => @site_1.domain, "PATH_INFO" => "/#{@category_1.permalink}"}
71
+ VanitySlug.find(env).should eq "/categories/#{@category_1.id}/slug"
72
+ end
73
+
74
+ it do
75
+ env = {"HTTP_HOST" => @site_2.domain, "PATH_INFO" => "/#{@category_2.permalink}"}
76
+ VanitySlug.find(env).should eq "/categories/#{@category_2.id}/slug"
77
+ end
78
+
79
+ it do
80
+ env = {"HTTP_HOST" => "c.com", "PATH_INFO" => "/#{@category_2.permalink}"}
81
+ VanitySlug.find(env).should be_false
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,19 @@
1
+ # -*- encoding: utf-8 -*-
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'vanity_slug/version'
5
+
6
+ Gem::Specification.new do |gem|
7
+ gem.name = "vanity_slug"
8
+ gem.version = VanitySlug::VERSION
9
+ gem.authors = ["Nick Merwin"]
10
+ gem.email = ["nick@lemurheavy.com"]
11
+ gem.description = %q{root level Vanity Slug for any model}
12
+ gem.summary = %q{easily add vanity slugs}
13
+ gem.homepage = ""
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
+ end
metadata ADDED
@@ -0,0 +1,62 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: vanity_slug
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Nick Merwin
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-11-13 00:00:00.000000000 Z
13
+ dependencies: []
14
+ description: root level Vanity Slug for any model
15
+ email:
16
+ - nick@lemurheavy.com
17
+ executables: []
18
+ extensions: []
19
+ extra_rdoc_files: []
20
+ files:
21
+ - .gitignore
22
+ - Gemfile
23
+ - LICENSE.txt
24
+ - README.md
25
+ - Rakefile
26
+ - lib/vanity_slug.rb
27
+ - lib/vanity_slug/active_record.rb
28
+ - lib/vanity_slug/railtie.rb
29
+ - lib/vanity_slug/vanity_router.rb
30
+ - lib/vanity_slug/version.rb
31
+ - spec/spec_helper.rb
32
+ - spec/support/active_record.rb
33
+ - spec/vanity_slug_spec.rb
34
+ - vanity_slug.gemspec
35
+ homepage: ''
36
+ licenses: []
37
+ post_install_message:
38
+ rdoc_options: []
39
+ require_paths:
40
+ - lib
41
+ required_ruby_version: !ruby/object:Gem::Requirement
42
+ none: false
43
+ requirements:
44
+ - - ! '>='
45
+ - !ruby/object:Gem::Version
46
+ version: '0'
47
+ required_rubygems_version: !ruby/object:Gem::Requirement
48
+ none: false
49
+ requirements:
50
+ - - ! '>='
51
+ - !ruby/object:Gem::Version
52
+ version: '0'
53
+ requirements: []
54
+ rubyforge_project:
55
+ rubygems_version: 1.8.23
56
+ signing_key:
57
+ specification_version: 3
58
+ summary: easily add vanity slugs
59
+ test_files:
60
+ - spec/spec_helper.rb
61
+ - spec/support/active_record.rb
62
+ - spec/vanity_slug_spec.rb