shortener 0.0.2 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (59) hide show
  1. data/.gitignore +3 -24
  2. data/README.rdoc +40 -11
  3. data/Rakefile +7 -21
  4. data/app/controllers/shortener/shortened_urls_controller.rb +22 -23
  5. data/app/helpers/shortener/shortener_helper.rb +6 -21
  6. data/app/models/shortener/shortened_url.rb +66 -74
  7. data/lib/generators/shortener/templates/migration.rb +17 -14
  8. data/lib/shortener.rb +22 -11
  9. data/lib/shortener/active_record_extension.rb +5 -0
  10. data/lib/shortener/engine.rb +3 -7
  11. data/lib/shortener/railtie.rb +10 -0
  12. data/lib/shortener/version.rb +2 -2
  13. data/shortener.gemspec +7 -5
  14. data/spec/controllers/shortened_urls_controller_spec.rb +19 -0
  15. data/spec/dummy/.gitignore +1 -0
  16. data/{test → spec}/dummy/Rakefile +0 -0
  17. data/{test → spec}/dummy/app/controllers/application_controller.rb +0 -0
  18. data/{test → spec}/dummy/app/helpers/application_helper.rb +0 -0
  19. data/spec/dummy/app/models/user.rb +3 -0
  20. data/{test → spec}/dummy/app/views/layouts/application.html.erb +0 -0
  21. data/{test → spec}/dummy/config.ru +0 -0
  22. data/{test → spec}/dummy/config/application.rb +0 -1
  23. data/{test → spec}/dummy/config/boot.rb +0 -0
  24. data/{test → spec}/dummy/config/database.yml +0 -0
  25. data/{test → spec}/dummy/config/environment.rb +0 -0
  26. data/{test → spec}/dummy/config/environments/development.rb +0 -3
  27. data/{test → spec}/dummy/config/environments/test.rb +0 -5
  28. data/{test → spec}/dummy/config/initializers/backtrace_silencers.rb +0 -0
  29. data/{test → spec}/dummy/config/initializers/inflections.rb +0 -0
  30. data/{test → spec}/dummy/config/initializers/mime_types.rb +0 -0
  31. data/{test → spec}/dummy/config/initializers/secret_token.rb +0 -0
  32. data/{test → spec}/dummy/config/initializers/session_store.rb +0 -0
  33. data/{test → spec}/dummy/config/locales/en.yml +0 -0
  34. data/spec/dummy/config/routes.rb +4 -0
  35. data/{test/dummy/public/stylesheets → spec/dummy/db}/.gitkeep +0 -0
  36. data/spec/dummy/db/migrate/20120213084304_create_shortened_urls_table.rb +25 -0
  37. data/spec/dummy/db/migrate/20120214023758_create_users.rb +8 -0
  38. data/spec/dummy/db/schema.rb +34 -0
  39. data/{test → spec}/dummy/public/favicon.ico +0 -0
  40. data/{test → spec}/dummy/script/rails +0 -0
  41. data/spec/helpers/shortener_helper_spec.rb +12 -0
  42. data/spec/models/shortened_url_spec.rb +68 -0
  43. data/spec/spec_helper.rb +12 -0
  44. metadata +71 -49
  45. data/test/dummy/config/environments/production.rb +0 -49
  46. data/test/dummy/config/routes.rb +0 -58
  47. data/test/dummy/public/404.html +0 -26
  48. data/test/dummy/public/422.html +0 -26
  49. data/test/dummy/public/500.html +0 -26
  50. data/test/dummy/public/javascripts/application.js +0 -2
  51. data/test/dummy/public/javascripts/controls.js +0 -965
  52. data/test/dummy/public/javascripts/dragdrop.js +0 -974
  53. data/test/dummy/public/javascripts/effects.js +0 -1123
  54. data/test/dummy/public/javascripts/prototype.js +0 -6001
  55. data/test/dummy/public/javascripts/rails.js +0 -191
  56. data/test/integration/navigation_test.rb +0 -7
  57. data/test/shortener_test.rb +0 -7
  58. data/test/support/integration_case.rb +0 -5
  59. data/test/test_helper.rb +0 -22
data/.gitignore CHANGED
@@ -2,33 +2,12 @@
2
2
  log/*
3
3
  tmp/**/*
4
4
  bin/*
5
-
6
- config/database.yml
7
- db/*.sqlite3
8
- *~
9
- public/photos/*
10
- \#*\#
11
- #Ignore all log files and process ID files
12
5
  *.log
13
- *.pid
14
-
15
- #ignore all generated pshinx config files
16
- *.sphinx.conf
17
-
18
- #ignore all sphinx DB files
19
- *.spa
20
- *.spd
21
- *.sph
22
- *.spi
23
- *.spk
24
- *.spl
25
- *.spm
26
- *.spp
27
-
28
- #ignore radrails files and temp files
6
+ *~
29
7
  .project
30
8
  .loadpath
31
9
  ._*
32
-
33
10
  .bundle
34
11
  *.gem
12
+ *.swp
13
+ Gemfile.lock
data/README.rdoc CHANGED
@@ -1,29 +1,32 @@
1
1
  = Shortener
2
2
 
3
- Shortener makes it easy to create shortened URLs for your rails application.
3
+ Shortener is a Rails Engine Gem that makes it easy to create and interpret shortened URLs on your own domain from within your Rails application. Once installed Shortener will generate, store URLS and "unshorten" shortened URLs for your applications visitors, all whilst collecting basic usage metrics.
4
+
5
+ ---
4
6
 
5
7
  == Overview
6
8
 
7
- The majority of the solution consists of two parts:
9
+ The majority of the Shortener consists of three parts:
8
10
 
9
- * a model for storing the details of the shortened link (including the user the shortened link belongs to and counter that increments as the link is clicked);
10
- * a controller to accept incoming requests, grab the shortened link data out of the database and redirecting the visitors request to the target URL;
11
+ * a model for storing the details of the shortened link;
12
+ * a controller to accept incoming requests and redirecting them to the target URL;
13
+ * a helper for generating shortened URLs from controllers and views.
11
14
 
12
- === Some niceities of shortener:
15
+ === Some niceities of Shortener:
13
16
 
14
17
  * The controller does a 301 redirect, which is the recommended type of redirect for maintaining maximum google juice to the original URL;
15
- * A unique code of is generated for each shortened link, instead of using the id of the shortened link record. This means that we can get more unique combinations than if we just used numbers;
18
+ * A unique alphanumeric code of generated for each shortened link, this means that we can get more unique combinations than if we just used numbers;
16
19
  * The link records a count of how many times it has been “un-shortened”;
17
- * The link can be linked to a user, this allows for stats of the link usage for a particular user and other interesting things;
20
+ * The link can be associated with a user, this allows for stats of the link usage for a particular user and other interesting things;
18
21
  * The controller spawns a new thread to record information to the database, allowing the redirect to happen as quickly as possible;
19
22
 
20
- === Future (possible) improvements:
23
+ === Future improvements:
21
24
 
22
25
  * There has not been an attempt to remove ambiguous characters (i.e. 1 l and capital i, or 0 and O etc.) from the unique key generated for the link. This means people might copy the link incorrectly if copying the link by hand;
23
26
  * The shortened links are found with a case-insensitive search on the unique key. This means that the system can’t take advantage of upper and lower case to increase the number of unique combinations. This may have an effect for people copying the link by hand;
24
27
  * The system could pre-generate unique keys in advance, avoiding the database penalty when checking that a newly generated key is unique;
25
28
  * The system could store the shortened URL if the url is to be continually rendered;
26
- * Some implementations might want duplicate links to be generated each time a user requests it.
29
+ * Some implementations might want duplicate links to be generated each time a user request a shortened link.
27
30
 
28
31
 
29
32
  == Installation
@@ -38,6 +41,10 @@ After you install Shortener run the generator:
38
41
 
39
42
  This generator will create a migration to create the shortened_urls table where your shortened URLs will be stored.
40
43
 
44
+ Then add to your routes:
45
+
46
+ match '/:id' => "shortener/shortened_urls#show"
47
+
41
48
  == Usage
42
49
 
43
50
  To generate a Shortened URL object for the URL "http://dealush.com" within your controller / models do the following:
@@ -54,6 +61,28 @@ To generate and display a shortened URL in your application use the helper metho
54
61
 
55
62
  This will generate a shortened URL. store it to the db and return a string representing the shortened URL.
56
63
 
57
- == Notes
64
+ === Shortened URLs with owner
65
+
66
+ You can link shortened URLs to an owner, to scope them. To do so, add the following line to the models which will act as owners:
67
+
68
+ class User < ActiveRecord::Base
69
+ has_shortened_urls
70
+ end
71
+
72
+ This will allow you to pass the owner when generating URLs:
73
+
74
+ Shortener::ShortenedURL.generate("dealush.com", user)
75
+
76
+ And to access those URLs:
77
+
78
+ user.shortened_urls
79
+
80
+ == Origins
81
+
82
+ Shortener is based on code from Dealush[http://dealush.com], for a bit of backstory to Shortener see this {blog post}[http://jamespmcgrath.com/a-simple-link-shortener-in-rails/].
83
+
84
+ == Authors
85
+
86
+ * {James McGrath}[https://github.com/jpmcgrath]
87
+ * {Michael Reinsch}[https://github.com/mreinsch]
58
88
 
59
- This is the first release and still has some bugs. I will be releasing fixes for these bugs soon.
data/Rakefile CHANGED
@@ -1,26 +1,12 @@
1
- # encoding: UTF-8
2
- require 'rubygems'
3
- begin
4
- require 'bundler/setup'
5
- rescue LoadError
6
- puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
7
- end
8
-
9
- require 'rake'
10
- require 'rake/rdoctask'
11
-
12
- require 'rake/testtask'
13
-
14
- Rake::TestTask.new(:test) do |t|
15
- t.libs << 'lib'
16
- t.libs << 'test'
17
- t.pattern = 'test/**/*_test.rb'
18
- t.verbose = false
19
- end
1
+ require 'bundler'
2
+ require 'rspec/core/rake_task'
3
+ require 'rdoc/task'
4
+ Bundler::GemHelper.install_tasks
20
5
 
21
- task :default => :test
6
+ task :default => :spec
7
+ RSpec::Core::RakeTask.new
22
8
 
23
- Rake::RDocTask.new(:rdoc) do |rdoc|
9
+ RDoc::Task.new :rdoc do |rdoc|
24
10
  rdoc.rdoc_dir = 'rdoc'
25
11
  rdoc.title = 'Shortener'
26
12
  rdoc.options << '--line-numbers' << '--inline-source'
@@ -1,27 +1,26 @@
1
- module Shortener
2
- class ShortenedUrlsController < ::ApplicationController
3
-
4
- # find the real link for the shortened link key and redirect
5
- def translate
6
- # pull the link out of the db
7
- sl = ShortenedUrl.find_by_unique_key(params[:unique_key])
8
-
9
- if sl
10
- # don't want to wait for the increment to happen, make it snappy!
11
- # this is the place to enhance the metrics captured
12
- # for the system. You could log the request origin
13
- # browser type, ip address etc.
14
- Thread.new do
15
- sl.increment!(:use_count)
16
- end
17
- # do a 301 redirect to the destination url
18
- head :moved_permanently, :location => sl.url
19
- else
20
- # if we don't find the shortened link, redirect to the root
21
- # make this configurable in future versions
22
- head :moved_permanently, :location => root_url
1
+ class Shortener::ShortenedUrlsController < ::ApplicationController
2
+
3
+ # find the real link for the shortened link key and redirect
4
+ def show
5
+ # pull the link out of the db
6
+ sl = ::Shortener::ShortenedUrl.find_by_unique_key(params[:id])
7
+
8
+ if sl
9
+ # don't want to wait for the increment to happen, make it snappy!
10
+ # this is the place to enhance the metrics captured
11
+ # for the system. You could log the request origin
12
+ # browser type, ip address etc.
13
+ Thread.new do
14
+ sl.increment!(:use_count)
15
+ ActiveRecord::Base.connection.close
23
16
  end
17
+ # do a 301 redirect to the destination url
18
+ redirect_to sl.url, :status => :moved_permanently
19
+ else
20
+ # if we don't find the shortened link, redirect to the root
21
+ # make this configurable in future versions
22
+ redirect_to '/'
24
23
  end
25
-
26
24
  end
25
+
27
26
  end
@@ -1,24 +1,9 @@
1
1
  module Shortener::ShortenerHelper
2
-
3
- # generate a url from either a url string, or a shortened url object
4
- def shortened_url(url_object, user=nil)
5
-
6
- short_url = nil
7
-
8
- if url_object.class != String #== ShortenedUrl
9
- if user.nil?
10
- short_url = url_object
11
- else
12
- # if the user has passed in a shortened url, with a user, then
13
- # work out the link for the shortened url and make another with the
14
- # passed user
15
- short_url = Shortener::ShortenedUrl.generate(shortened_url(url_object), user)
16
- end
17
- else
18
- short_url = Shortener::ShortenedUrl.generate(url_object, user)
19
- end
20
-
21
- return short_url.nil? ? nil : shortener_translate_url(short_url.unique_key)
2
+
3
+ # generate a url from a url string
4
+ def short_url(url, owner=nil)
5
+ short_url = Shortener::ShortenedUrl.generate(url, owner)
6
+ short_url ? url_for(:controller => :"shortener/shortened_urls", :action => :show, :id => short_url.unique_key, :only_path => false) : url
22
7
  end
23
8
 
24
- end
9
+ end
@@ -1,80 +1,72 @@
1
- module Shortener
2
- class ShortenedUrl < ActiveRecord::Base
3
-
4
- UNIQUE_KEY_LENGTH = 5
5
- URL_PROTOCOL_HTTP = "http://"
6
-
7
- REGEX_HTTP_URL = /^\s*(http[s]?:\/\/)?[a-z0-9]+([-.]{1}[a-z0-9]+)*\.[a-z]{2,5}(([0-9]{1,5})?\/.*)?\s*$/i
8
- REGEX_LINK_HAS_PROTOCOL = Regexp.new('\Ahttp:\/\/|\Ahttps:\/\/', Regexp::IGNORECASE)
9
- REGEX_EMAIL = /^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/i
10
-
11
- validates_format_of :url, :with => REGEX_HTTP_URL, :allow_blank => true
12
- validates_presence_of :url
13
- validates_uniqueness_of :unique_key
14
-
15
- belongs_to :user # allows the shortened link to be associated with a user
16
-
17
- before_validation :clean_destination_url, :init_unique_key, :on => :create
18
-
19
-
20
- # ensure the url starts with it protocol
21
- def clean_destination_url
22
- if !self.url.blank? and self.url !~ REGEX_LINK_HAS_PROTOCOL
23
- self.url.insert(0, URL_PROTOCOL_HTTP)
24
- end
25
- end
26
-
27
- def init_unique_key
28
- # generate a unique key for the link
29
- begin
30
- # has about 50 million possible combos
31
- self.unique_key = ShortenedUrl::generate_unique_key
32
- end while ShortenedUrl::find_by_unique_key self.unique_key
1
+ class Shortener::ShortenedUrl < ActiveRecord::Base
2
+
3
+ URL_PROTOCOL_HTTP = "http://"
4
+ REGEX_LINK_HAS_PROTOCOL = Regexp.new('\Ahttp:\/\/|\Ahttps:\/\/', Regexp::IGNORECASE)
5
+
6
+ validates :url, :presence => true
7
+
8
+ # allows the shortened link to be associated with a user
9
+ belongs_to :owner, :polymorphic => true
10
+
11
+ # ensure the url starts with it protocol and is normalized
12
+ def self.clean_url(url)
13
+ return nil if url.blank?
14
+ url = URL_PROTOCOL_HTTP + url.strip unless url =~ REGEX_LINK_HAS_PROTOCOL
15
+ URI.parse(url).normalize.to_s
16
+ end
17
+
18
+ # generate a shortened link from a url
19
+ # link to a user if one specified
20
+ # throw an exception if anything goes wrong
21
+ def self.generate!(orig_url, owner=nil)
22
+ # if we get a shortened_url object with a different owner, generate
23
+ # new one for the new owner. Otherwise return same object
24
+ if orig_url.is_a?(Shortener::ShortenedUrl)
25
+ return orig_url.owner == owner ? orig_url : generate!(orig_url.url, owner)
33
26
  end
34
-
35
- # generate a shortened link from a url
36
- # link to a user if one specified
37
- # throw an exception if anything goes wrong
38
- def self.generate!(orig_url, user=nil)
39
- # don't want to generate the link if it has already been generated
40
- # so check the datastore
41
- uid = user.nil? ? nil : user.id
42
- sl = ShortenedUrl.find_by_url_and_user_id(orig_url, uid)
43
-
44
- return sl if sl
45
-
46
- # create the shortened link, storing it
47
- sl = ShortenedUrl.create!(:url => orig_url, :user => user)
48
-
49
- # return the url
50
- return sl
27
+
28
+ # don't want to generate the link if it has already been generated
29
+ # so check the datastore
30
+ cleaned_url = clean_url(orig_url)
31
+ scope = owner ? owner.shortened_urls : self
32
+ scope.find_or_create_by_url(cleaned_url)
33
+ end
34
+
35
+ # return shortened url on success, nil on failure
36
+ def self.generate(orig_url, owner=nil)
37
+ begin
38
+ generate!(orig_url, owner)
39
+ rescue
40
+ nil
51
41
  end
52
-
53
- # return shortened url on success, nil on failure
54
- def self.generate(orig_url, user=nil)
55
-
56
- sl = nil
57
-
58
- begin
59
- sl = ShortenedUrl::generate!(orig_url, user)
60
- rescue
61
- sl = nil
42
+ end
43
+
44
+ private
45
+
46
+ # we'll rely on the DB to make sure the unique key is really unique.
47
+ # if it isn't unique, the unique index will catch this and raise an error
48
+ def create
49
+ count = 0
50
+ begin
51
+ self.unique_key = generate_unique_key
52
+ super
53
+ rescue ActiveRecord::RecordNotUnique, ActiveRecord::StatementInvalid => err
54
+ if (count +=1) < 5
55
+ logger.info("retrying with different unique key")
56
+ retry
57
+ else
58
+ logger.info("too many retries, giving up")
59
+ raise
62
60
  end
63
-
64
- return sl
65
- end
66
-
67
-
68
-
69
- private
70
-
71
- # generate a random string
72
- # future mod to allow specifying a more expansive charst, like utf-8 chinese
73
- def self.generate_unique_key(size = UNIQUE_KEY_LENGTH)
74
- # not doing uppercase as url is case insensitive
75
- charset = ('a'..'z').to_a + (0..9).to_a
76
- (0...size).map{ charset.to_a[rand(charset.size)] }.join
77
61
  end
78
-
79
62
  end
63
+
64
+ # generate a random string
65
+ # future mod to allow specifying a more expansive charst, like utf-8 chinese
66
+ def generate_unique_key
67
+ # not doing uppercase as url is case insensitive
68
+ charset = ::Shortener.key_chars
69
+ (0...::Shortener.unique_key_length).map{ charset[rand(charset.size)] }.join
70
+ end
71
+
80
72
  end
@@ -1,22 +1,25 @@
1
1
  class CreateShortenedUrlsTable < ActiveRecord::Migration
2
- def self.up
2
+ def change
3
3
  create_table :shortened_urls do |t|
4
-
5
- t.integer :user_id # we can link this to a user for interesting things
6
- t.string :url, :null => false # the real url that we will redirect to
7
- t.string :unique_key, :null => false # the unique key
8
- t.integer :use_count, :null => false, :default => 0 # how many times the link has been clicked
4
+ # we can link this to a user for interesting things
5
+ t.integer :owner_id
6
+ t.string :owner_type, :limit => 20
7
+
8
+ # the real url that we will redirect to
9
+ t.string :url, :null => false
10
+
11
+ # the unique key
12
+ t.string :unique_key, :limit => 10, :null => false
13
+
14
+ # how many times the link has been clicked
15
+ t.integer :use_count, :null => false, :default => 0
9
16
 
10
17
  t.timestamps
11
18
  end
12
-
13
- add_index :shortened_urls, :unique_key # we will lookup the links in the db with this
14
- add_index :shortened_urls, :user_id # and this
15
- end
16
19
 
17
- def self.down
18
- remove_index :shortened_urls, :unique_key
19
- remove_index :shortened_urls, :user_id
20
- drop_table :shortened_urls
20
+ # we will lookup the links in the db by key and owners.
21
+ # also make sure the unique keys are actually unique
22
+ add_index :shortened_urls, :unique_key, :unique => true
23
+ add_index :shortened_urls, [:owner_id, :owner_type]
21
24
  end
22
25
  end
data/lib/shortener.rb CHANGED
@@ -1,17 +1,28 @@
1
1
  require "active_support/dependencies"
2
2
 
3
3
  module Shortener
4
-
5
- # Our host application root path
6
- # We set this when the engine is initialized
7
- mattr_accessor :app_root
8
-
9
- # Yield self on setup for nice config blocks
10
- def self.setup
11
- yield self
4
+
5
+ autoload :ActiveRecordExtension, "shortener/active_record_extension"
6
+
7
+ CHARSETS = {
8
+ :alphanum => ('a'..'z').to_a + (0..9).to_a,
9
+ :alphanumcase => ('a'..'z').to_a + ('A'..'Z').to_a + (0..9).to_a }
10
+
11
+ # default key length: 5 characters
12
+ mattr_accessor :unique_key_length
13
+ self.unique_key_length = 5
14
+
15
+ # character set to chose from:
16
+ # :alphanum - a-z0-9 - has about 60 million possible combos
17
+ # :alphanumcase - a-zA-Z0-9 - has about 900 million possible combos
18
+ mattr_accessor :charset
19
+ self.charset = :alphanum
20
+
21
+ def self.key_chars
22
+ CHARSETS[charset]
12
23
  end
13
-
14
24
  end
15
25
 
16
- # Require our engine
17
- require "shortener/engine"
26
+ # Require our railtie and engine
27
+ require "shortener/railtie"
28
+ require "shortener/engine"