Giter Club home page Giter Club logo

shorten's Introduction

Sinatra Url Shortener

Learning Competencies

  • Test-driven development using rspec and rack-test
  • Map the flow of data through a web application
  • Use redirect
  • Use Active Record callbacks
  • Implement a user authorization scheme to limit unauthorized access to specific pages in a web application
  • Deploy your application to Heroku

Summary

We're going to build a simple link shortener, a la bitly.

You'll have one model named Url. The table for this model stores a list of URLs that people have entered.

Releases

Release 0: Simple Shortener

Start with the empty Sinatra skeleton.

We have one resource: Urls. For our controllers, we have a route that lists Url objects and another route that, when POSTed to, creates a Url object.

Let's test-drive these routes!

Create a file in the spec/ directory that corresponds to your controller. Maybe call it something like index_controller_spec.rb. Open this file in your editor and write a controller spec for GET /urls using the syntax provided by rack-test. The Sinatra testing documentation also has documentation about how to test a controller.

Write a failing test first. After you've made it pass by implementing the route, write a failing test for the POST /urls route. Keep the

  1. Arrange
  2. Act
  3. Assert

steps in mind, since they'll help you set up the test for your POST route. Here's the general format for an empty controller test:

require 'spec_helper'

describe "index_controller" do
  # specs go here!
end

We'll also need a route that redirects us to the full (unshortened) URL. As always, write a test first. If you've never used bitly, use it now to get a feel for how it works.

Now that you've written tested controllers, the controller methods should look like this:

get '/' do
  # let user create new short URL, display a list of shortened URLs
end

post '/urls' do
  # create a new Url
end

# e.g., /q6bda
get '/:short_url' do
  # redirect to appropriate "long" URL
end

Use a before_save callback in the Url model to generate the short URL. Write a test for the model method you created that generates the short URL, but don't worry about testing that the before_save callback fires. We'll trust that ActiveRecord has been thoroughly tested.

Release 1: Add a Counter!

Add a click_count field to your urls table, which keeps track of how many times someone has visited the shortened URL. Add code to the appropriate place in your controller code so that any time someone hits a short URL the counter for the appropriate Url is incremented by 1. You can (and should) also write a test for this.

Release 2: User Authentication

Now it's time for things to get complicated. Let's implement a login and logout feature for our URL shortener!

The core flow of the app should remain the same, however a person might choose to log in or create an account. In the event that they're logged in when they shorten a URL, this Url should now be associated with their user account. In other words, a Url belongs to a User and a User has many Urls.

Note: Don't worry about implement the above user-centric logic yet. First get all the URL shortening and user authentication code working. Feel free to spike on this and forego TDD while you figure out user authentication, we'll come back to repair broken tests in a bit. Make sure people can log in and can shorten URLs regardless of whether they're logged in or not.

There's going to be a lot going on in your controller! Try to keep the code as readable and organized as possible. A file structure like this might make sense:

  1. app/controllers/urls.rb, which contains the routes related to listing, creating, and redirecting Url objects
  2. app/controllers/sessions.rb, which contains the routes related to logging in and logging out
  3. app/controllers/users.rb, which contains the routes related to creating, displaying, and editing users

Release 3: Conditional Logic

Now that you've got basic user authentication and URL shortening working, it's time to stitch these features together. Depending on whether a user is signed in, your site will change dynamically.

People should be able to create short URLs regardless of whether they're logged in or not (that is, the user_id field on the urls table could possibly be NULL).

However, if a user is logged in, when we create a URL it should set the user_id to whatever the user_id of the currently logged-in user is. This information should not be a part of the form that a user submits — it would be trivial for someone to change the content of the form and submit as any user. The information should, instead, be stored in the users' sessions.

Users should now be able to view their URLs on their profile page, which should look like this:

get '/users/:id' do
end

This should display a user's profile, which lists all the links that a particular user has created. If I'm viewing my own profile page, show the number of clicks next to each link so I can see how awesome my link-sharing skills are.

Since you'll be checking the session in every route, your routes are probably pretty repetetive. But theres a better way! You can create helper methods to DRY up your controller and view code. You could use something like this:

helpers do
  # This will return the current user, if they exist
  # Replace with code that works with your application
  def current_user
    if session[:user_id]
      @current_user ||= User.find_by_id(session[:user_id])
    end
  end

  # Returns true if current_user exists, false otherwise
  def logged_in?
    !current_user.nil?
  end
end

Your controller can now call current_user to get the currently authenticated user, if they exist. This means we don't have to rely on user-submitted data to determine what user created a short URL.

Release 4: Fix Your Broken Tests and Write New Ones!

All of this dynamic content has probably caused your tests to break. But that's ok! They can be fixed! You can fake a user's session using rack-test. The 'GET' and 'POST' methods take an optional third argument that corresponds to the rack environment hash (where information about sessions is stored). It looks something like this:

it "should create a new post" do
  fake_params = { title: "some title", content: "some content" }
  fake_session = { 'rack.session' => { user_id: 20 } }
  expect{
    post '/some_route', fake_params, fake_session
  }.to change{ Post.count }.by(1)
end

You should also write tests to check each branch of your conditionals (if/else) that correspond to user authentication.

Release 5: Add Validations

Test-drive adding a validation to your Url model so that only Urls with valid URLs are saved to the database. Read up on ActiveRecord validations. You can use ActiveRecord's valid? instance method in your test to check if the validations are working.

What constitutes a "valid URL" is up to you. It's a sliding scale, from validations that would permit lots of invalid URLs to validations that might reject lots of valid URLs. When you get into it you'll see that expressing the fact "x is a valid URL" in Ruby Land or SQL Land is never perfect.

For example, the valid URL could range across:

A valid URL is...

  • Any non-empty string
  • Any non-empty string that starts with "http://" or "https://"
  • Any string that the Ruby URI module says is valid
  • Any URL-looking thing which responds to a HTTP request, i.e., we actually check to see if the URL is accessible via HTTP

Some of these are easily expressible in SQL Land. Some of these are hard to express in SQL Land, but ActiveRecord comes with pre-built validation helpers that make it easy to express in Ruby Land. Others require custom validations that express logic unique to our application domain.

The rule of thumb is that where we can, we want to always express constraints in Ruby Land and also express them in SQL Land where feasible.

Release 5: Add Error Handling

When you try to save (create or update) an ActiveRecord object that has invalid data, ActiveRecord will fail. Some methods like create! and save! throw an exception. Others like create (without the !, the bang) return the resulting object whether the object was saved successfully to the database or not, while save will return false if perform_validation is true and any validations fail. See create and save for more information.

Remember, you can call valid? or invalid? on an ActiveRecord object to see whether its data is valid or invalid.

Use valid?, invalid?, and the errors method to display a helpful error message if a user enters an invalid URL. This will give them a change to correct their error.

Optimize Your Learning

More on Validations, Constraints, and Database Consistency

We often want to put constraints on what sort of data can go into our database. This way we can guarantee that all data in the database conforms to certain standards, e.g., there are no users missing an email address. Guarantees of this kind — ensuring that the data in our database is never confusing or contradictory or partially changed or otherwise invalid — are called consistency.

If we think of this as a fact from Fact Land, these constraints look like:

  • A user must have a first_name
  • A user must have an email
  • Two user's can't have the same email address, or equivalently, each user's email must be unique
  • A Craigslist post's URL must be a valid URL, for some reasonable definition of valid

These facts can be recorded in both SQL Land and in Ruby Land, like this:

Fact Land SQL Land Ruby Land
A user must have an email address NOT NULL constraint on email field validates :email, :presence => true
A user must have a first name NOT NULL constraint on first_name field validates :first_name, :presence => true
A user's email address must be unique UNIQUE INDEX on email field validates :email, :uniqueness => true

Release 7: Deployment

You're feature complete with 100% test coverage. It's time to push this thing into production! Get the app up on Heroku, and call it a night.

Resources

shorten's People

Contributors

lkroner avatar

Watchers

James Cloos avatar

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.