Skip to content

Commit

Permalink
Merge pull request #13 from turingschool/jm/link_shortening
Browse files Browse the repository at this point in the history
Link Shortening Endpoints
  • Loading branch information
noahdurbin authored Aug 23, 2024
2 parents a48d4f2 + beeffe7 commit 84825bc
Show file tree
Hide file tree
Showing 8 changed files with 331 additions and 3 deletions.
134 changes: 133 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,25 @@ This is the backend API repository for TurLink. TurLink is a link shortener app
"id": "1",
"type": "user",
"attributes": {
"email": "[email protected]"
"email": "[email protected]",
"links": [
{
"id": 1,
"original": "testlink.com",
"short": "tur.link/4a7c204baeacaf2c",
"user_id": 1,
"created_at": "2024-08-23T15:51:38.866Z",
"updated_at": "2024-08-23T15:51:38.866Z"
},
{
"id": 2,
"original": "testlink.com",
"short": "tur.link/67c758fc",
"user_id": 1,
"created_at": "2024-08-23T15:53:08.573Z",
"updated_at": "2024-08-23T15:53:08.573Z"
},
]
}
}
}
Expand Down Expand Up @@ -89,3 +107,117 @@ This is the backend API repository for TurLink. TurLink is a link shortener app
]
}
```
### Shorten a Link
- **POST** `/api/v1/users/:id/links?link={original link}`
- Description: Creates a shortened link associated with an exisiting user
- Example Request: POST `https://turlink-be-53ba7254a7c1.herokuapp.com/users/1/links?link=testlink.com`
- Successful Response (200 OK):
```json
{
"data": {
"id": "1",
"type": "link",
"attributes": {
"original": "testlink.com",
"short": "tur.link/4a7c204baeacaf2c",
"user_id": 1
}
}
}
```
- Error Response (422 Unprocessable Entity) -- when original link isn't entered:
```json
{
"errors": [
{
"status": "unprocessable_entity",
"message": "Original can't be blank"
}
]
}
```
- Error Response (404 Not Found) -- when user_id is not valid:
```json
{
"errors": [
{
"status": "unprocessable_entity",
"message": "User must exist"
}
]
}
```

### All Links for a User
- **GET** `/api/v1/users/:id/links`
- Description: Gets an index of all links for a user
- Example Request: GET `https://turlink-be-53ba7254a7c1.herokuapp.com/users/1/links`
- Successful Response (200 OK):
```json
{
"data": {
"id": "1",
"type": "user",
"attributes": {
"email": "[email protected]",
"links": [
{
"id": 1,
"original": "testlink.com",
"short": "tur.link/4a7c204baeacaf2c",
"user_id": 1,
"created_at": "2024-08-23T15:51:38.866Z",
"updated_at": "2024-08-23T15:51:38.866Z"
},
{
"id": 2,
"original": "testlink.com",
"short": "tur.link/67c758fc",
"user_id": 1,
"created_at": "2024-08-23T15:53:08.573Z",
"updated_at": "2024-08-23T15:53:08.573Z"
},
]
}
}
}
```
- Error Response (404 Not Found) -- when user_id is not valid:
```json
{
"errors": [
{
"message": "Record not found"
}
]
}
```

### Return full link when short link is given
- **GET** `/api/v1/links?short={shortened link}`
- Description: Gets full link object when given shortened link
- Example Request: GET `https://turlink-be-53ba7254a7c1.herokuapp.com/links?short=tur.link/4a7c204baeacaf2c`
- Successful Response (200 OK):
```json
{
"data": {
"id": "1",
"type": "link",
"attributes": {
"original": "testlink.com",
"short": "tur.link/4a7c204baeacaf2c",
"user_id": 1
}
}
}
```
- Error Response (404 Not Found) -- when shortened link is not entered or does not exist:
```json
{
"errors": [
{
"message": "Record not found"
}
]
}
```
34 changes: 34 additions & 0 deletions app/controllers/api/v1/links_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
class Api::V1::LinksController < ApplicationController

rescue_from ActiveRecord::RecordNotFound, with: :not_found_error

def create
link = Link.create_new(params[:user_id], params[:link])
if link.save
render json: LinkSerializer.new(link)
else
render json: ErrorSerializer.new(link, :unprocessable_entity).serialize, status: :unprocessable_entity
end
end

def index
user = User.find(params[:user_id])
render json: UserSerializer.new(user)
end

def show
link = Link.find_by(short: params[:short])
if link != nil
render json: LinkSerializer.new(link)
else
not_found_error
end
end

private

def not_found_error
render json: { errors: [{ message: 'Record not found' }] }, status: :not_found
end

end
11 changes: 11 additions & 0 deletions app/models/link.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,18 @@
require 'securerandom'

class Link < ApplicationRecord
validates_presence_of :user_id
validates_presence_of :original
validates :short, uniqueness: true, presence: true

belongs_to :user

def self.create_new(user_id, original)
short = Link.create_short_link
Link.new(user_id: user_id, original: original, short: short)
end

def self.create_short_link
"tur.link/#{SecureRandom.hex(4)}"
end
end
4 changes: 4 additions & 0 deletions app/serializers/link_serializer.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
class LinkSerializer
include JSONAPI::Serializer
attributes :original, :short, :user_id
end
2 changes: 1 addition & 1 deletion app/serializers/user_serializer.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
class UserSerializer
include JSONAPI::Serializer
attributes :email
attributes :email, :links
end
5 changes: 4 additions & 1 deletion config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,11 @@

namespace :api do
namespace :v1 do
resources :users, only: %i[create]
resources :users, only: %i[create] do
resources :links, only: %i[create index]
end
resources :sessions, only: %i[create]
resources :links, only: %i[index], action: :show
end
end
end
14 changes: 14 additions & 0 deletions spec/models/link_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,18 @@
describe 'relationships' do
it { should belong_to :user }
end

describe 'class methods' do
describe 'create_new' do
it 'can create a new link' do
user1 = User.create(email: "[email protected]", password: "user123")
link = Link.create_new(user1.id, "long-link-example.com")

expect(link).to be_a Link
expect(link.user_id).to eq(user1.id)
expect(link.original).to eq("long-link-example.com")
expect(link.short).to be_a String
end
end
end
end
130 changes: 130 additions & 0 deletions spec/requests/api/v1/links_request_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
require 'rails_helper'

RSpec.describe 'links requests', type: :request do
describe 'POST /users/:id/links' do
it 'creates a new Link record with a shortened link' do
user1 = User.create(email: "[email protected]", password: "user123")

post "/api/v1/users/#{user1.id}/links?link=long-link-example.com"

expect(response).to be_successful
expect(response.status).to eq(200)

link = JSON.parse(response.body, symbolize_names: true)[:data]

expect(link).to be_a Hash
expect(link).to have_key :id
expect(link[:type]).to eq("link")
attributes = link[:attributes]
expect(attributes[:original]).to eq("long-link-example.com")
expect(attributes[:user_id]).to eq(user1.id)
expect(attributes[:short]).to be_a String
end

describe 'sad path' do
it "will return 422 if link param is not entered" do
user1 = User.create(email: "[email protected]", password: "user123")

post "/api/v1/users/#{user1.id}/links"

expect(response).to_not be_successful
expect(response.status).to eq(422)

error = JSON.parse(response.body, symbolize_names: true)[:errors][0]

expect(error[:status]).to eq("unprocessable_entity")
expect(error[:message]).to eq("Original can't be blank")
end

it "will return 404 if user_id does not exist" do
post "/api/v1/users/99999999/links?link=this-wont-work.com"

expect(response).to_not be_successful
expect(response.status).to eq(422)

error = JSON.parse(response.body, symbolize_names: true)[:errors][0]

expect(error[:status]).to eq("unprocessable_entity")
expect(error[:message]).to eq("User must exist")
end
end
end

describe 'GET /users/:id/links' do
it "retrieves the user object with all associated links as attributes" do
user1 = User.create(email: "[email protected]", password: "user123")

post "/api/v1/users/#{user1.id}/links?link=long-link-example.com"
post "/api/v1/users/#{user1.id}/links?link=another-long-link.com"

get "/api/v1/users/#{user1.id}/links"

expect(response).to be_successful
expect(response.status).to eq(200)

user = JSON.parse(response.body, symbolize_names: true)[:data]

expect(user).to have_key :id
expect(user[:type]).to eq("user")
expect(user[:attributes][:email]).to eq("[email protected]")
links = user[:attributes][:links]
expect(links.count).to eq(2)
first_link = links[0]
expect(first_link).to have_key :id
expect(first_link[:original]).to eq("long-link-example.com")
expect(first_link[:short]).to be_a String
expect(first_link[:user_id]).to eq(user1.id)
end

describe "sad path" do
it "will return 404 if user does not exist" do
get "/api/v1/users/999999/links"

expect(response).to_not be_successful
expect(response.status).to eq(404)

error = JSON.parse(response.body, symbolize_names: true)[:errors][0]

expect(error[:message]).to eq("Record not found")
end
end
end

describe 'GET /links' do
it "can return a the json for a link (including full url) when given shortened link" do
user1 = User.create(email: "[email protected]", password: "user123")

post "/api/v1/users/#{user1.id}/links?link=long-link-example.com"

link = JSON.parse(response.body, symbolize_names: true)[:data]

short = link[:attributes][:short]

get "/api/v1/links?short=#{short}"

expect(response).to be_successful
expect(response.status).to eq(200)

link2 = JSON.parse(response.body, symbolize_names: true)[:data]

expect(link2).to have_key :id
expect(link2[:type]).to eq("link")
expect(link2[:attributes][:original]).to eq("long-link-example.com")
expect(link2[:attributes][:short]).to eq(short)
expect(link2[:attributes][:user_id]).to eq(user1.id)
end

describe "sad path" do
it "will return a 404 if shortened link doesn't exist" do
get "/api/v1/links?short=tur.link/12345678"

expect(response).to_not be_successful
expect(response.status).to eq(404)

error = JSON.parse(response.body, symbolize_names: true)[:errors][0]

expect(error[:message]).to eq("Record not found")
end
end
end
end

0 comments on commit 84825bc

Please sign in to comment.