Skip to content

Commit

Permalink
Merge pull request #7 from jpmcgrath/develop
Browse files Browse the repository at this point in the history
Syncs develop with upstream changes
  • Loading branch information
antonivanopoulos committed Feb 7, 2024
2 parents dcd2cb4 + 95d06a5 commit be4fe01
Show file tree
Hide file tree
Showing 40 changed files with 612 additions and 166 deletions.
69 changes: 69 additions & 0 deletions .github/workflows/ruby.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
name: Ruby

on:
push:
branches: [ develop ]
pull_request:
branches: [ develop ]

jobs:
test:
runs-on: ubuntu-latest
strategy:
fail-fast: true
matrix:
ruby-version:
- '2.4'
- '2.7'
- '3.0'
- '3.2'
gemfile:
- rails_4.2.gemfile
- rails_5.2.gemfile
- rails_6.0.gemfile
- rails_6.1.gemfile
- rails_7.0.gemfile
- rails_7.1.gemfile
include:
- ruby-version: '2.4'
gemfile: rails_4.2.gemfile
bundler-version: 1
exclude:
- ruby-version: '2.7'
gemfile: rails_4.2.gemfile
- ruby-version: '3.0'
gemfile: rails_4.2.gemfile
- ruby-version: '3.2'
gemfile: rails_4.2.gemfile
- ruby-version: '3.0'
gemfile: rails_5.2.gemfile
- ruby-version: '3.2'
gemfile: rails_5.2.gemfile
- ruby-version: '2.4'
gemfile: rails_6.0.gemfile
- ruby-version: '3.2'
gemfile: rails_6.0.gemfile
- ruby-version: '2.4'
gemfile: rails_6.1.gemfile
- ruby-version: '3.2'
gemfile: rails_6.1.gemfile
- ruby-version: '2.4'
gemfile: rails_7.0.gemfile
- ruby-version: '2.4'
gemfile: rails_7.1.gemfile
- ruby-version: '2.7'
gemfile: rails_7.1.gemfile

env:
BUNDLE_GEMFILE: ${{ github.workspace }}/gemfiles/${{ matrix.gemfile }}

steps:
- uses: actions/checkout@v3
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: ${{ matrix.ruby-version }}
bundler: ${{ matrix.bundler-version }}
bundler-cache: true
- name: Run tests
run: bundle exec rake
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,8 @@ bin/*
*.gem
*.swp
Gemfile.lock
.ruby-version
.rvmrc
.redcar/*
.redcar/*
gemfiles/*.lock
spec/dummy/tmp
5 changes: 0 additions & 5 deletions .travis.yml

This file was deleted.

27 changes: 27 additions & 0 deletions Appraisals
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
appraise "rails_4.2" do
gem "rails", "~> 4.2.10"
gem "rspec-rails", "~> 3.0"
gem "sqlite3", "~> 1.3.6"
gem "loofah", "~> 2.20.0"
end

appraise "rails_5.2" do
gem "rails", "~> 5.2.0"
gem "loofah", "~> 2.20.0"
end

appraise "rails_6.0" do
gem "rails", "~> 6.0.0"
end

appraise "rails_6.1" do
gem "rails", "~> 6.1.0"
end

appraise "rails_7.0" do
gem "rails", "~> 7.0.0"
end

appraise "rails_7.1" do
gem "rails", "~> 7.1.0"
end
2 changes: 1 addition & 1 deletion Gemfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
source "http://rubygems.org"
source "https://rubygems.org"

# Specify any dependencies in the gemspec
gemspec
120 changes: 85 additions & 35 deletions README.rdoc
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
{<img src="https://secure.travis-ci.org/jpmcgrath/shortener.png?branch=master" alt="Build Status" />}[http://travis-ci.org/jpmcgrath/shortener]
{<img src="https://github.com/jpmcgrath/shortener/actions/workflows/ruby.yml/badge.svg" alt="Build Status" />}[https://github.com/jpmcgrath/shortener/actions]
{<img src="https://codeclimate.com/github/jpmcgrath/shortener/badges/gpa.svg" />}[https://codeclimate.com/github/jpmcgrath/shortener]
{<img src="https://badge.fury.io/rb/shortener.svg" alt="Gem version" />}[http://badge.fury.io/rb/shortener]

= Shortener

Expand All @@ -14,7 +16,7 @@ The majority of the Shortener consists of three parts:

=== Dependencies

Shortener is designed to work from within a Rails 3 and Rails 4 applications. It has dependancies Rails core components like ActiveRecord, ActionController, the rails routing engine and more.
Shortener is designed to work from within a Ruby on Rail applications. It has dependancies Rails core components like ActiveRecord, ActionController, the rails routing engine and more.

=== Ruby Version Support

Expand All @@ -33,29 +35,37 @@ v0.5.2 introduced the ability to set an expiration date for a shortened URL. The
are stored in a expires_at column in the database, which can be added to your schema with the following
migration:

class AddExpiresAtToShortenedUrl < ActiveRecord::Migration
class AddExpiresAtToShortenedUrl < ActiveRecord::Migration[4.2]
def change
add_column :shortened_urls, :expires_at, :datetime
end
end

=== v0.5.6 to v0.6.1
v0.6.1 introduced the ability to categorize a shortened URL. The category value
is stored in a string column in the database, which must be added to your schema with the following
migration:

bundle exec rails g migration add_category_to_shortened_url category:string:index


class AddCategoryToShortenedUrl < ActiveRecord::Migration[4.2]
def change
add_column :shortened_urls, :category, :string
add_index :shortened_urls, :category
end
end

=== Some niceities of Shortener:

* The controller does a 301 redirect, which is the recommended type of redirect for maintaining maximum google juice to the original URL;
* 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;
* The link records a count of how many times it has been “un-shortened”;
* The link can be associated with a user, this allows for stats of the link usage for a particular user and other interesting things;
* The controller spawns a new thread to record information to the database, allowing the redirect to happen as quickly as possible;

=== Future improvements:

* 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;
* The system could pre-generate unique keys in advance, avoiding the database penalty when checking that a newly generated key is unique;
* The system could store the shortened URL if the url is to be continually rendered;

== Installation

Shortener is compatible with Rails v3 and v4. To install, add to your Gemfile:
Shortener is compatible with Rails v4, v5, & v6. To install, add to your Gemfile:

gem 'shortener'

Expand All @@ -65,6 +75,8 @@ After you install Shortener run the generator:

This generator will create a migration to create the shortened_urls table where your shortened URLs will be stored.

*Note:* The default length of the url field in the generated migration is 2083. This is because MySQL requires fixed length indicies and browsers have a defacto limit on URL length. This may not be right for you or your database. The discussion can be seen here https://github.com/jpmcgrath/shortener/pull/98

Then add to your routes:

get '/:id' => "shortener/shortened_urls#show"
Expand All @@ -85,6 +97,15 @@ the upper and lower case charset, by including the following:

Shortener.charset = :alphanumcase

If you want to use a custom charset, you can create your own combination by creating an array of possible values, such as allowing underscore and dashes:

Shortener.charset = ("a".."z").to_a + (0..9).to_a + ["-", "_"]

By default, <b>Shortener assumes URLs to be valid web URLs</b> and normalizes them in an effort to make sure there are no duplicate records generated for effectively same URLs with differences of only non-effective slash etc.
You can control this option if it interferes for any of your logic. One common case is for mobile app links or universal links where normalization can corrupt the URLs of form <tt>appname://some_route</tt>

Shortener.auto_clean_url = true

== Usage

To generate a Shortened URL object for the URL "http://example.com" within your controller / models do the following:
Expand Down Expand Up @@ -127,7 +148,9 @@ And to access those URLs:

You can pass in your own key when generating a shortened URL. This should be unique.

Shortener::ShortenedUrl.generate("example.com", owner: user, custom_key: "my-key")
*Important:* Custom keys can't contain characters other than those defined in *Shortener.charset*. Default is numbers and lowercase a-z (See *Configuration*).

Shortener::ShortenedUrl.generate("example.com", owner: user, custom_key: "mykey")

short_url("http://example.com", custom_key: 'yourkey')

Expand All @@ -136,11 +159,11 @@ You can pass in your own key when generating a shortened URL. This should be uni
You can create expirable URLs.
Probably, most of the time it would be used with owner:

Shortener::ShortenedUrl.generate("example.com/page", user, expires_at: 24.hours.since)
Shortener::ShortenedUrl.generate("example.com/page", owner: user, expires_at: 24.hours.since)

You can omit owner passing nil instead:
You can omit owner:

Shortener::ShortenedUrl.generate("example.com/page", nil, expires_at: 24.hours.since)
Shortener::ShortenedUrl.generate("example.com/page", expires_at: 24.hours.since)

=== Fresh Links

Expand All @@ -154,18 +177,43 @@ to create a fresh record, you can pass the following argument:
=== Forbidden keys

You can ensure that records with forbidden keys will not be generated.
In rails you can put next line into config/initializers/shortener.rb
In Rails you can put next line into config/initializers/shortener.rb

Shortener.forbidden_keys.concat %w(terms promo)

=== Ignoring Robots

By default Shortener will count all visits to a shortened url, including any crawler
robots like the Google Web Crawler, or Twitter's link unshortening bot. To ignore
these visits, Shortener makes use of the excellent voight_kampff gem to identify
web robots. This feature is disabled by default. To enable add the following to
your shortener configuration:

Shortener.ignore_robots = true

=== Mounting on a Subdomain

If you want to constrain the shortener route to a subdomain, the following config will
prevent the subdomain parameter from leaking in to shortened URLs if it matches the configured subdomain.

Within config/initializers/shortener.rb

Shortener.subdomain = 's'

Within config/routes.rb

constraints subdomain: 's' do
get '/:id' => "shortener/shortened_urls#show"
end

=== URL Parameters

Parameters are passed though from the shortened url, to the destination URL. If the destination
URL has the same parameters as the destination URL, the parameters on the shortened url take
precedence over those on the destination URL.

For example, if we have an orginal URL of:
> http://destination.com?test=yes&happy=defo (idenfified with token ABCDEF)
> http://destination.com?test=yes&happy=defo (identified with token ABCDEF)
Which is shortened into:
> http://coolapp.io/s/ABCDEF?test=no&why=not
Then, the resulting URL will be:
Expand Down Expand Up @@ -208,32 +256,34 @@ If you want more things to happen when a user accesses one of your short urls, y

*note:* If no shortened URL is found, the url will be `default_redirect` or `/`

== Origins

For a bit of backstory to Shortener see this {blog post}[http://jamespmcgrath.com/a-simple-link-shortener-in-rails/].

== In The Wild
=== Configuring a different database for shortened_urls table

Shortener is used in a number of production systems, including, but not limited to:
You can store a `shortened_urls` table in another database and connecting to it by creating a initializer with the following:

{Doorkeeper - An Event Management Tool}[http://www.doorkeeperhq.com/]
```ruby
ActiveSupport.on_load(:shortener_record) do
connects_to(database: { writing: :dbname, reading: :dbname_replica })
end
```

If you are using Shortener in your project and would like to be added to this list, please get in touch!

== Authors

* {James McGrath}[https://github.com/jpmcgrath]
* {Michael Reinsch}[https://github.com/mreinsch]
**Note:** Please, replace `dbname` and `dbname_replica` to match your database configuration.

== Contributing

New feature requests are welcome, code is more welcome still. Code with Specs is the
most welcomiest there is!
We welcome new contributors. Because we're all busy people, and because Shortener
is used/relied upon by many projects, it is essential that new Pull Requests
are opened with good spec coverage, and a passing build on supported ruby versions
and Rails versions. Please create a single PR per feature/contribution, with as
few changes per PR as possible to make it easier to review.

To contribute:

1. Fork it
2. Create your feature branch (git checkout -b my-new-feature)
3. Commit your changes (git commit -am 'Add some feature')
4. Push to the branch (git push origin my-new-feature)
5. Create a new Pull Request
3. Write spec coverage of changes
4. Commit your changes (git commit -am 'Add some feature')
5. Push to the branch (git push origin my-new-feature)
6. Create a new Pull Request
7. Ensure the build is passing

Note: We adhere to the community driven Ruby style guide: https://github.com/bbatsov/ruby-style-guide
11 changes: 8 additions & 3 deletions app/controllers/shortener/shortened_urls_controller.rb
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
class Shortener::ShortenedUrlsController < ActionController::Base
class Shortener::ShortenedUrlsController < ActionController::Metal
include ActionController::StrongParameters
include ActionController::Redirecting
include ActionController::Instrumentation
include Rails.application.routes.url_helpers
include Shortener

def show
token = ::Shortener::ShortenedUrl.extract_token(params[:id])
url = ::Shortener::ShortenedUrl.fetch_with_token(token: token, additional_params: params)
track = Shortener.ignore_robots.blank? || request.human?
url = ::Shortener::ShortenedUrl.fetch_with_token(token: token, additional_params: params, track: track)
redirect_to url[:url], status: :moved_permanently
end

end
22 changes: 14 additions & 8 deletions app/helpers/shortener/shortener_helper.rb
Original file line number Diff line number Diff line change
@@ -1,16 +1,22 @@
module Shortener::ShortenerHelper

# generate a url from a url string
def short_url(url, owner: nil, custom_key: nil, expires_at: nil, fresh: false, url_options: {})
short_url = Shortener::ShortenedUrl.generate(url,
owner: owner,
custom_key: custom_key,
expires_at: expires_at,
fresh: fresh
)
def short_url(url, owner: nil, custom_key: nil, expires_at: nil, fresh: false, category: nil, url_options: {})
short_url = Shortener::ShortenedUrl.generate(
url,
owner: owner,
custom_key: custom_key,
expires_at: expires_at,
fresh: fresh,
category: category
)

if short_url
options = { controller: :"shortener/shortened_urls", action: :show, id: short_url.unique_key, only_path: false }.merge(url_options)
if subdomain = Shortener.subdomain
url_options = url_options.merge(subdomain: subdomain)
end

options = { controller: :"/shortener/shortened_urls", action: :show, id: short_url.unique_key, only_path: false }.merge(url_options)
url_for(options)
else
url
Expand Down
5 changes: 5 additions & 0 deletions app/models/shortener/record.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
class Shortener::Record < ActiveRecord::Base #:nodoc:
self.abstract_class = true
end

ActiveSupport.run_load_hooks :shortener_record, Shortener::Record
Loading

0 comments on commit be4fe01

Please sign in to comment.