Skip to content

Commit

Permalink
Merge pull request #8 from andrecego/feat/add-candidates-list
Browse files Browse the repository at this point in the history
Add PaginateService and update dependencies
  • Loading branch information
andrecego authored Feb 9, 2024
2 parents 9d574b0 + 1e65381 commit 14a4d6c
Show file tree
Hide file tree
Showing 14 changed files with 270 additions and 16 deletions.
13 changes: 13 additions & 0 deletions .rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,16 @@ Style/Documentation:
RSpec/EmptyExampleGroup:
Exclude:
- "**/*swagger_spec.rb"

RSpec/NestedGroups:
Max: 4

RSpec/ExampleLength:
Enabled: false

Layout/FirstHashElementIndentation:
EnforcedStyle: consistent

Layout/HashAlignment:
EnforcedHashRocketStyle: table
EnforcedColonStyle: table
2 changes: 1 addition & 1 deletion Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ git_source(:github) { |repo| "https://github.com/#{repo}.git" }
ruby '2.7.7'

gem 'bootsnap', '>= 1.4.4', require: false
gem 'jbuilder', '~> 2.7'
gem 'mysql2', '~> 0.5'
gem 'oj'
gem 'puma', '~> 5.0'
gem 'rails', '~> 6.1.7', '>= 6.1.7.3'
gem 'rswag'
Expand Down
8 changes: 4 additions & 4 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ GEM
addressable (2.8.6)
public_suffix (>= 2.0.2, < 6.0)
ast (2.4.2)
bigdecimal (3.1.6)
bindex (0.8.1)
bootsnap (1.16.0)
msgpack (~> 1.2)
Expand All @@ -85,9 +86,6 @@ GEM
activesupport (>= 5.0)
i18n (1.12.0)
concurrent-ruby (~> 1.0)
jbuilder (2.11.5)
actionview (>= 5.0.0)
activesupport (>= 5.0.0)
json (2.6.3)
json-schema (4.1.1)
addressable (>= 2.8)
Expand Down Expand Up @@ -118,6 +116,8 @@ GEM
nio4r (2.5.8)
nokogiri (1.14.2-x86_64-linux)
racc (~> 1.4)
oj (3.16.3)
bigdecimal (>= 3.0)
parallel (1.22.1)
parser (3.2.1.1)
ast (~> 2.4.1)
Expand Down Expand Up @@ -275,8 +275,8 @@ DEPENDENCIES
byebug
factory_bot_rails (~> 6.0.0)
faker (~> 3.2)
jbuilder (~> 2.7)
mysql2 (~> 0.5)
oj
pry-byebug
puma (~> 5.0)
rack-mini-profiler (~> 2.0)
Expand Down
13 changes: 13 additions & 0 deletions app/controllers/api/v1/candidates_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,19 @@
module Api
module V1
class CandidatesController < ApiController
def index
paginated_candidates = PaginateService.new(
relation: Candidate.all,
page: params[:page]&.to_i,
per_page: params[:per_page]&.to_i
)

render json: {
candidates: paginated_candidates.call,
meta: paginated_candidates.meta
}
end

def create
candidate = Candidates::CreateUseCase.new(candidate_params).call
return head :unprocessable_entity unless candidate
Expand Down
4 changes: 4 additions & 0 deletions app/models/candidate.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
# frozen_string_literal: true

class Candidate < ApplicationRecord
def as_json(options = {})
serializable_fields = %i[id name email birthdate]
super(options.merge(only: serializable_fields))
end
end
49 changes: 49 additions & 0 deletions app/services/paginate_service.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# frozen_string_literal: true

# PaginateService is responsible for paginating a relation and returning metadata about the pagination.
class PaginateService
DEFAULT_PAGE = 1
DEFAULT_PER_PAGE = 20
MAX_PER_PAGE = 100

# Initializes a new instance of PaginateService.
#
# @param relation [Object] The relation object to paginate.
# @param page [Integer] The page number to retrieve (default: 1).
# @param per_page [Integer] The number of records per page (default: 20).
def initialize(relation:, page: nil, per_page: nil)
@relation = relation
@page = page || DEFAULT_PAGE
@per_page = [per_page || DEFAULT_PER_PAGE, MAX_PER_PAGE].min
end

# Paginates the relation based on the specified page and per_page values.
#
# @return [Object] The paginated relation.
def call
@relation.offset(offset).limit(per_page)
end

# Returns metadata about the pagination.
#
# @return [Hash] The metadata hash containing page, per_page, total_count, and total_pages.
def meta
{ page: page, per_page: per_page, total_count: total_count, total_pages: total_pages }
end

private

attr_reader :relation, :page, :per_page

def offset
(page - 1) * per_page
end

def total_pages
(total_count.to_f / per_page).ceil
end

def total_count
@total_count ||= relation.offset(nil).limit(nil).count
end
end
4 changes: 2 additions & 2 deletions app/use_cases/candidates/create_use_case.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ def candidate

def candidate_params
{
name: @params[:name],
email: @params[:email],
name: @params[:name],
email: @params[:email],
birthdate: @params[:birthdate]
}
end
Expand Down
4 changes: 4 additions & 0 deletions config/initializers/oj.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# frozen_string_literal: true

# Optimize JSON serialization https://github.com/ohler55/oj/blob/develop/pages/Rails.md
Oj.optimize_rails
2 changes: 1 addition & 1 deletion config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

namespace :api do
namespace :v1 do
resources :candidates, only: %i[create]
resources :candidates, only: %i[create index]
end
end

Expand Down
48 changes: 48 additions & 0 deletions spec/requests/api/v1/candidates_request_spec.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,54 @@
# frozen_string_literal: true

RSpec.describe 'Api::V1::Candidates' do
describe 'GET /' do
subject(:get_candidates) { get '/api/v1/candidates', params: params }

let(:params) { {} }

it 'returns http success' do
get_candidates
expect(response).to have_http_status(:success)
end

it 'returns a list of candidates' do
candidates = create_list(:candidate, 2)
get_candidates
expect(response.parsed_body).to eq({
candidates: [{
id: candidates.first.id,
name: candidates.first.name,
email: candidates.first.email,
birthdate: candidates.first.birthdate.to_s
}, {
id: candidates.second.id,
name: candidates.second.name,
email: candidates.second.email,
birthdate: candidates.second.birthdate.to_s
}],
meta: { page: 1, per_page: 20, total_pages: 1, total_count: 2 }
}.deep_stringify_keys)
end

context 'with pagination' do
let(:params) { { page: 2, per_page: 2 } }

it 'returns only paginated elements' do
candidates = create_list(:candidate, 3)
get_candidates
expect(response.parsed_body).to eq({
candidates: [{
id: candidates.third.id,
name: candidates.third.name,
email: candidates.third.email,
birthdate: candidates.third.birthdate.to_s
}],
meta: { page: 2, per_page: 2, total_pages: 2, total_count: 3 }
}.deep_stringify_keys)
end
end
end

describe 'POST /create' do
subject(:create_candidate) { post '/api/v1/candidates', params: params }

Expand Down
46 changes: 42 additions & 4 deletions spec/requests/api/v1/candidates_request_swagger_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,55 @@

describe 'Candidates API' do
path '/api/v1/candidates' do
get 'Retrieves a list of candidates' do
tags 'Candidates'
produces 'application/json'
parameter name: :page, in: :query, type: :integer
parameter name: :per_page, in: :query, type: :integer

response '200', 'Candidates list' do
schema type: :object,
properties: {
candidates: {
type: :array,
items: {
type: :object,
properties: {
id: { type: :integer },
name: { type: :string },
email: { type: :string },
birthdate: { type: :string, format: :date }
}
}
},
meta: {
type: :object,
properties: {
page: { type: :integer },
per_page: { type: :integer },
total_pages: { type: :integer },
total_count: { type: :integer }
}
}
}

let(:page) { 1 }
let(:per_page) { 20 }
run_test!
end
end

post 'Creates a candidate' do
tags 'Candidates'
consumes 'application/json'
parameter name: :candidate, in: :body, schema: {
type: :object,
type: :object,
properties: {
name: { type: :string, example: 'Andre' },
email: { type: :string, format: :email, example: 'andre@haistack.ai' },
name: { type: :string, example: 'Andre' },
email: { type: :string, format: :email, example: 'andre@haistack.ai' },
birthdate: { type: :string, format: :date, example: '1992-04-27' }
},
required: %w[name email birthdate]
required: %w[name email birthdate]
}

response '201', 'Candidate created' do
Expand Down
39 changes: 39 additions & 0 deletions spec/services/paginate_service_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# frozen_string_literal: true

RSpec.describe PaginateService do
describe '#call' do
subject(:paginate_service) { described_class.new(relation: relation, page: page, per_page: per_page) }

let(:relation) { Candidate.all }
let(:page) { 1 }
let(:per_page) { 1 }

context 'when there are records of the given relation' do
before { create_list(:candidate, 2) }

it 'returns first page of results' do
expect(paginate_service.call.order(:id).map(&:attributes)).to eq([Candidate.first.attributes])
end

it 'returns only the number of records specified by per_page' do
expect(paginate_service.call.count).to eq(1)
end

context 'when on second page' do
let(:page) { 2 }

it 'returns second page of results' do
expect(paginate_service.call.order(:id).map(&:attributes)).to eq([Candidate.last.attributes])
end
end
end

context 'when the relation is empty' do
let(:relation) { Candidate.all }

it 'returns the relation' do
expect(paginate_service.call).to eq([])
end
end
end
end
8 changes: 4 additions & 4 deletions spec/swagger_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,14 @@
config.openapi_specs = {
'v1/swagger.yaml' => {
openapi: '3.0.1',
info: {
title: 'API V1',
info: {
title: 'API V1',
version: 'v1'
},
paths: {},
paths: {},
servers: [
{
url: 'http://localhost:3000',
url: 'http://localhost:3000',
description: 'The local server'
}
]
Expand Down
46 changes: 46 additions & 0 deletions swagger/v1/swagger.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,52 @@ info:
version: v1
paths:
"/api/v1/candidates":
get:
summary: Retrieves a list of candidates
tags:
- Candidates
parameters:
- name: page
in: query
schema:
type: integer
- name: per_page
in: query
schema:
type: integer
responses:
'200':
description: Candidates list
content:
application/json:
schema:
type: object
properties:
candidates:
type: array
items:
type: object
properties:
id:
type: integer
name:
type: string
email:
type: string
birthdate:
type: string
format: date
meta:
type: object
properties:
page:
type: integer
per_page:
type: integer
total_pages:
type: integer
total_count:
type: integer
post:
summary: Creates a candidate
tags:
Expand Down

0 comments on commit 14a4d6c

Please sign in to comment.