vanity_slug 0.0.1

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 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