casefaz / rails-engine

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Rails Engine

A week long solo API-only project that implements endpoints using RESTful convention

Postman Badge JSON Badge Ruby/Rails

Utilizing service-oriented architecture this project exposes the data powering a movie database through RESTful conventions and test driven development. Rails Engine Requirements

Testing

Gems, Ruby, and Rails Versions:

jsonapi-serializer
factory_bot_rails
faker
rspec-rails
shoulda-matchers
simplecov
ruby 2.7.4
rails 5.2.8

Postman:

Screen Shot 2022-07-14 at 6 36 35 PM Screen Shot 2022-07-14 at 6 36 24 PM Screen Shot 2022-07-14 at 6 36 56 PM

Simplecov:

Screen Shot 2022-07-14 at 6 42 14 PM Screen Shot 2022-07-14 at 6 42 53 PM

If time was limitless I would:

  • refactor the test paths for better developer empathy re: encapsulating each CRUD action in one file (better potential for cleaner code on a larger scale)

Return to Header

Endpoints

First Endpoint

Get an Items Merchant

Routes

namespace :api do
  namespace :v1 do
    resources :items do 
      get '/merchant', to: 'merchants#show'
    end
  end
end

Controller

def show
  if Item.exists? && params[:item_id]
    merchant = Item.find(params[:item_id]).merchant
    render json: MerchantSerializer.new(merchant)
  else
    merchant2 = Merchant.find(params[:id])
    render json: MerchantSerializer.new(merchant2)
  end
end

Spec

Happy path: successfully produce the endpoint

merchant = create(:merchant)
item = create(:item, merchant_id: merchant.id)

get "/api/v1/items/#{item.id}/merchant"

expect(response).to be_successful
expect(response).to have_http_status(200)

parsed_merchant = JSON.parse(response.body, symbolize_names: true)

expect(parsed_merchant[:data].keys.count).to eq(3)
expect(parsed_merchant[:data][:attributes].count).to eq(1)
expect(parsed_merchant[:data][:attributes][:name]).to eq(merchant.name)

Sad path: return an error if the id is invalid

it 'returns an error with a bad item id' do
  get '/api/v1/items/45293/merchant'

  expect(response).to have_http_status(404)
end

it 'returns an error if the id comes through as a string' do
  get "/api/v1/items/'45293'/merchant"

  expect(response).to have_http_status(404)
end

Second Endpoint

Find All Items

Routing

Includes all routes

namespace :api do
  namespace :v1 do
    get 'merchants/find', to: 'merchants/search#index'
    get 'items/find_all', to: 'items/search#index'
    resources :merchants, only: %i[index show] do
      resources :items, only: [:index], controller: 'merchant_items'
    end
    resources :items do
      get '/merchant', to: 'merchants#show'
    end
  end
end

Item Model

def self.find_name(query)
  where('name ILIKE ?', "%#{query}%")
end

def self.find_min_price(price)
  where('unit_price >= ?', price.to_s)
end

def self.find_max_price(price)
  where('unit_price <= ?', price.to_s)
end

def self.find_min_and_max_price(min_price, max_price)
  where("unit_price >= #{min_price} AND unit_price <= #{max_price}")
end

Search Controller

def index
      if valid_query
        if params[:name] 
          item = Item.find_name(params[:name])
          render json: ItemSerializer.new(item)
        elsif params[:min_price] && params[:max_price]
          item = Item.find_min_and_max_price(params[:min_price], params[:max_price])
          render json: ItemSerializer.new(item)
        elsif params[:min_price]
          item = Item.find_min_price(params[:min_price])
          render json: ItemSerializer.new(item)
        elsif params[:max_price]
          item = Item.find_max_price(params[:max_price])
          render json: ItemSerializer.new(item)
        end
      else
        render json: { response: 'Bad Request' }, status: :bad_request
      end
    end

    def valid_query
      name = (params[:name] && params[:name] != '') && !params[:min_price] && !params[:max_price]
      min_or_max = !params[:name] && (!params[:min_price].nil? || !params[:max_price].nil?)
      min_and_max_price = !params[:name] && (!params[:min_price].nil? && !params[:max_price].nil?)

      name || min_or_max || min_and_max_price
    end
  end 
end

Find All Spec

Happy path: finds the item by name, min, and max price parameters Name

item1 = create(:item, name: 'white rice')
item2 = create(:item, name: 'brown rice')
item3 = create(:item, name: 'fried rice')
item4 = create(:item, name: 'sesame ball')

get '/api/v1/items/find_all?name=rice'

found_items = JSON.parse(response.body, symbolize_names: true)
expect(found_items[:data].count).to eq(3)

found_items[:data].each do |item|
  expect(item).to have_key(:id)
  expect(item[:attributes].keys.count).to eq(4)
  expect(item[:attributes]).to have_key(:name)
  expect(item[:attributes]).to have_key(:description)
  expect(item[:attributes]).to have_key(:merchant_id)
  expect(item[:attributes]).to have_key(:unit_price)
  expect(item[:attributes]).to_not have_key(:created_at)
end

Price

get '/api/v1/items/find_all?min_price=4'

expect(response).to be_successful
expect(response).to have_http_status(200)

parsed_min = JSON.parse(response.body, symbolize_names: true)
expect(parsed_min[:data].count).to eq(3)

get '/api/v1/items/find_all?max_price=4'

expect(response).to be_successful
expect(response).to have_http_status(200)

parsed_max = JSON.parse(response.body, symbolize_names: true)

expect(parsed_max[:data].count).to eq(2)

get '/api/v1/items/find_all?min_price=1&max_price=4'

expect(response).to be_successful
expect(response).to have_http_status(200)

parsed_min_max = JSON.parse(response.body, symbolize_names: true)

expect(parsed_min_max[:data].count).to eq(2)

Sad path: incorrect parameters

get '/api/v1/items/find_all?name=deckofmanythings'

expect(response).to be_successful
expect(response).to have_http_status(200)

parsed_response = JSON.parse(response.body, symbolize_names: true)
expect(parsed_response[:data]).to eq([])

get '/api/v1/items/find_all?name=coriander&min_price=2'

expect(response).to have_http_status(400)

get '/api/v1/items/find_all?name=coriander&min_price=2&max_price=5'

expect(response).to have_http_status(400)

get '/api/v1/items/find_all'

expect(response).to have_http_status(400)

Return to Header

Potential Refactor

If I had all the time in the world:

  • Refactor Items Merchant into it's own controller
  • Combine all the model methods into one to shorten controller
  • Put repetitive render json: error paths into Module
  • Do edge casing on top of of sad-pathing

Learning Outcomes

  • RESTful API conventions
    • routing
    • encapsulating logic and files in places that promote developer empathy and common sense
  • Abstract logic out of the controllers
    • modules
    • model methods
  • Be patient with yourself
    • Work in progress

About


Languages

Language:Ruby 99.6%Language:HTML 0.4%