Wednesday, August 18, 21 Challenge Ruby

Simpel Challenge To Test Programmer

For a few days ago, I saw a good twit from someone to discuss about testing a programmer is easier with build and API server but without using any database to make the data is not persistent for everytime we create a new instance of the apps.

Ternyata ngetes pemrogram di Indonesia yang ngerti cara mrogram ga susah-susah amat: suruh bikin API server atau aplikasi web tapi ga boleh pake basis data dengan limitasi yang sudah diterima: kalo servernya mati, ya datanya ilang.. - @lynxluna

This is the original twit you can read by yourself.

So, today I wanna try to solve this challenge by my knowledge, but that we have to find out the requirements in more detail. As we can see the comments on his post, he just wanna test the programmer to create an API or web apps to handle normal CURD (Create, Update, Read, and Delete) operation but without database. Sounds simple doesn’t it? Of course, if you’re not a fan of frameworks and depend on them to solve all your problem.

I’ll using a Sinatra to solve this problem because I’m a person who use Ruby as my daily tool to build a web service. Why Sinatra? Not Rack or native Net/HTTP lib? Simply is to make it easier to handle all routing process.

I’ll do it with a simple approach that using a class variable to store all users and using struct to handle user attributes. This is my user class:

# frozen_string_literal: true

# Struct to store user attributes
UserEntity = Struct.new(:id, :name, :email, keyword_init: true) do
  # update method used to update user attribute
  # user.update(params)
  def update(params)
    self.each_pair { |key, _| self[key] = params[key] unless key == :id }
    self
  end

  def to_api
    {
      id: id,
      name: name,
      email: email
    }
  end
end

# User model
class User
  # users stored
  @@users = []

  class << self
    # User.all used for get all value from @@users
    # and return as API format
    def all
      @@users.map(&:to_api)
    end

    # Used for create a new user struct and stored to @@users
    # User.create(params)
    def create(params)
      user = UserEntity.new(params)
      user.id = @@users.length + 1
      @@users.push(user)
      user
    end

    # Used for find user by their id
    # User.find(id)
    def find(id)
      @@users.find { |struct| struct.id == id.to_i }
    end

    # Used for destroy a user by id
    # User.destroy(id)
    def destroy(id)
      @@users.delete_if { |struct| struct.id == id.to_i }
    end
  end
end

I’m sure you’ll ask me why I create a User class when I can put them into main file. I created a User class to encapsulate all action for user attribute. It can be more cleaner and easy to understand what we done. I’ll not explain all method for User class, we just focused on @@users variable used to store all user entries.

@@users in Ruby known as a Class Variable. This variable will useable in the class where it defined and also can inherited to their child. You can find out more detail about variable in Ruby here.

Now this code is how I handle all CRUD operation from client request. Looks simple right? imagine if we put the code in the user class here, then what will happen is our code becomes bigger and lazy to read. I was added any comments to my code, so you can understand what have I written.

# frozen_string_literal: true

require 'sinatra'
require 'sinatra/json'
require_relative 'user'

# simple helper to parsed reqeust body and convert to json format
helpers do
  def json_response
    JSON.parse(request.body.read, symbolize_names: true)
  rescue JSON::ParserError
    {}
  end
end

# @test curl -X GET localhost:port/users
# @params nil
# @return { "data": [] }
get '/users' do
  json data: User.all
end

# @test curl -X POST localhost:port/users
# @params { "name: "name", "email": "email" }
# @returns { "data": { "id": 1, "name: "name", "email": "email" } }
post '/users' do
  user = User.create(json_response)
  json data: user.to_api
end

# @test curl -X GET localhost:port/users/:id
# @params id
# @returns { "data": { "id": 1, "name: "name", "email": "email" } }
get '/users/:id' do
  user = User.find(params[:id])
  halt 404 if user.nil?

  json data: user.to_api
end

# @test curl -X GET localhost:port/users/:id
# @params id, { "name: "name", "email": "email" }
# @returns { "data": { "id": 1, "name: "name", "email": "email" } }
patch '/users/:id' do
  user = User.find(params[:id])
  halt 404 if user.nil?

  user.update(json_response)
  json data: user.to_api
end

# @test curl -X GET localhost:port/users/:id
# @params id
# @return status 204
delete '/users/:id' do
  halt 404 if User.find(params[:id]).nil?

  User.destroy(params[:id])
  status 204
end

I also made a simple test to validate our app is work. Currently, I forced myself to make the test as a habit and it had to be done. Of course with the appropriate condition whether we need it or not. So, here my test code.

# frozen_string_literal: true

require 'spec_helper'
require_relative '../user'
require_relative '../users_controller'

RSpec.describe 'UserController', type: :request do
  # We need to init the sinatra app or controller to test
  def app
    Sinatra::Application
  end

  # initial variable to store user params
  let!(:params) { { name: 'Jhon Doe', email: 'jhon_doe@gmail.com' } }

  describe 'GET /users' do
    it 'should return 3 users' do
      3.times { |index| User.create(name: "test-#{index + 1}", email: "test-#{index + 1}@mail.com") }
      get '/users'
      expect(json_response[:data].length).to eq(3)
    end
  end

  describe 'POST /users' do
    it 'should return new user' do
      post '/users', params.to_json
      expect(json_response.dig(:data, :name)).to eq('Jhon Doe')
    end
  end

  describe 'GET /users/:id' do
    it 'should return existing user' do
      get '/users/4'
      expect(json_response.dig(:data, :id)).to eq(4)
    end

     it 'should return existing user' do
      get '/users/1337'
      expect(last_response.status).to eq(404)
    end
  end

  describe 'PATCH /users/:id' do
    it 'should return existing user' do
      patch '/users/4', { name: 'Boy Pablo' }.to_json
      expect(json_response.dig(:data, :name)).to eq('Boy Pablo')
    end

     it 'should return existing user' do
      patch '/users/1337'
      expect(last_response.status).to eq(404)
    end
  end

  describe 'DELETE /users/:id' do
    it 'should return status 204' do
      delete '/users/4'
      expect(last_response.status).to eq(204)
    end
  end
end

Conclusion

We don’t have to depend on framework to solve all our problem. But we must know what tool is suitable for the problem we encounter. We have to know the fundamental (even though I don’t fully understand it too) because it is very helpful in making the right decision.

Thanks for your coming. Please give me your feedback to make me better and better.