Skip to content

Commit

Permalink
Implement API endpoints for Categories and Products (#4)
Browse files Browse the repository at this point in the history
* chore: Switch from jbuilder to jsonapi-serializer

This change came about after a number of research. I found that JSON:API
 standard follows best practices and it's something I want to become
 very good at.

 Also, based on the benchmark tests, it is way faster than jbuilder and
 the alternatives I found.

* feat: Update schema to make category_id optional on products

This update makes the `category_id` on the `products` to be optional so
that it doesn't need to be provided when a product does not fit into a
category yet.

It also ensures that when a category is deleted, the products that
depend will have their `category_id` field set to null.

Finally, `available` and `currency` have been introduced to the
`products` to table to help know the availability status of a product
and the currency it uses.

* test: Add tests for updates and deletion for products and category
models

* chore: Add default URLs to test and development environments

* feat: Add helper to retrieve response body

* test: Add test for controller routing to endpoints

* chore: Include RequestsHelper in spec_helper.rb file

* feat: Add routes for version 1 of categories and unmatched endpoints

* feat: Add error handling and JsonResponse helper for API responses

* chore: Add coverage for tests

* chore: Ignore coverage directory

* chore: Add 'simplecov' configuration to spec_helper.rb

* refactor: Update error handling for controllers

* feat: Add serializer for Category model

* feat: Add JsonResponse and PaginationHelper for API responses

* test: Add unit tests for CategoriesController to validate all actions

* feat: Add controller for category API operations

* style: Update code style

* test: Add unit tests for validating filtering by name and search
keywords

* feat: Implement filtering based on name and search keywords

* refactor: Move authentication validation to application controller

* refactor: Update error handling responses and include Authentication
mixin

* feat: Add an authentication module to validate dev tokens and user IDs

* feat: Add endpoints for products API operations

* chore: Add database cleaner for active record

* refactor: Update application_controller.rb to handle validation error

* refactor: Update developer token and user ID retrieval

* refactor: Add status code success metadata for successful JSON responses

* refactor: Update to mock the valid_developer_token to return true always

* feat: Add redis and HTTParty

* test: Fix test cases for categories_controller.rb

* feat: Update schema to include app ID

* feat: Update authentication.rb to use the user service for validations

* refactor: Improve pagination response

* feat: Implement a method to handle bad requests.

* feat: Add Redis as the caching store

* feat: Add caching for single results

* test: Add tests for route matching for products API endpoints

* feat: Implement user service client to validate user, app and developer
details

* test: Add tests for UserServiceClient

* feat: Add database_cleaner and shared_context.rb files

* feat: Fix failing tests after adding mandatory app_id field

* feat: Add authentication helper for request to spec_helper.rb

* refactor: Move authentication helper to a separate module

* feat: Update shared_contexts.rb to retrieve valid data

* style: Rubocop style fixes

* feat: Add logic for filtering products

* feat: Add the AuthenticationHelper for request type tests

* test: Add tests for product operation endpoints

* feat: Add controller actions for Product endpoints

* feat: Add serializer for products

* style: Rubocop style fixes
  • Loading branch information
nanafox authored Sep 16, 2024
1 parent 1cfde7e commit c859dc4
Show file tree
Hide file tree
Showing 33 changed files with 2,646 additions and 23 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,4 @@
# Ignore master key for decrypting credentials and more.
/config/master.key
.idea/
/coverage/
16 changes: 13 additions & 3 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,8 @@ gem 'rails', '~> 7.2.1'
gem 'pg', '~> 1.1'
# Use the Puma web server [https://github.com/puma/puma]
gem 'puma', '>= 5.0'
# Build JSON APIs with ease [https://github.com/rails/jbuilder]
gem 'jbuilder'
# Use Redis adapter to run Action Cable in production
# gem "redis", ">= 4.0.1"
gem 'redis', '>= 4.0.1'

# Use Kredis to get higher-level data types in Redis [https://github.com/rails/kredis]
# gem "kredis"
Expand Down Expand Up @@ -46,9 +44,21 @@ group :development, :test do
gem 'factory_bot_rails'

gem 'dotenv-rails'

# code coverage
gem 'simplecov', require: false
end

gem 'uuid7', '~> 0.2.0'

# for pagination
gem 'kaminari'

# for JSON response serialization
gem 'jsonapi-serializer'

group :test do
gem 'database_cleaner-active_record'
end

gem 'httparty', '~> 0.22.0'
35 changes: 30 additions & 5 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -82,11 +82,17 @@ GEM
concurrent-ruby (1.3.4)
connection_pool (2.4.1)
crass (1.0.6)
csv (3.3.0)
database_cleaner-active_record (2.2.0)
activerecord (>= 5.a)
database_cleaner-core (~> 2.0.0)
database_cleaner-core (2.0.1)
date (3.3.4)
debug (1.9.2)
irb (~> 1.10)
reline (>= 0.3.8)
diff-lcs (1.5.1)
docile (1.4.1)
dotenv (3.1.2)
dotenv-rails (3.1.2)
dotenv (= 3.1.2)
Expand All @@ -100,16 +106,19 @@ GEM
railties (>= 5.0.0)
globalid (1.2.1)
activesupport (>= 6.1)
i18n (1.14.6)
httparty (0.22.0)
csv
mini_mime (>= 1.0.0)
multi_xml (>= 0.5.2)
i18n (1.14.5)
concurrent-ruby (~> 1.0)
io-console (0.7.2)
irb (1.14.0)
rdoc (>= 4.0.0)
reline (>= 0.4.2)
jbuilder (2.13.0)
actionview (>= 5.0.0)
activesupport (>= 5.0.0)
json (2.7.2)
jsonapi-serializer (2.2.0)
activesupport (>= 4.2)
kaminari (1.2.2)
activesupport (>= 4.1.0)
kaminari-actionview (= 1.2.2)
Expand All @@ -136,6 +145,8 @@ GEM
mini_mime (1.1.5)
minitest (5.25.1)
msgpack (1.7.2)
multi_xml (0.7.1)
bigdecimal (~> 3.1)
net-imap (0.4.16)
date
net-protocol
Expand Down Expand Up @@ -209,6 +220,10 @@ GEM
rake (13.2.1)
rdoc (6.7.0)
psych (>= 4.0.0)
redis (5.3.0)
redis-client (>= 0.22.0)
redis-client (0.22.2)
connection_pool
regexp_parser (2.9.2)
reline (0.5.10)
io-console (~> 0.5)
Expand Down Expand Up @@ -259,6 +274,12 @@ GEM
rubocop-rails
ruby-progressbar (1.13.0)
securerandom (0.3.1)
simplecov (0.22.0)
docile (~> 1.1)
simplecov-html (~> 0.11)
simplecov_json_formatter (~> 0.1)
simplecov-html (0.12.3)
simplecov_json_formatter (0.1.4)
stringio (3.1.1)
thor (1.3.2)
timeout (0.4.1)
Expand All @@ -285,16 +306,20 @@ PLATFORMS
DEPENDENCIES
bootsnap
brakeman
database_cleaner-active_record
debug
dotenv-rails
factory_bot_rails
jbuilder
httparty (~> 0.22.0)
jsonapi-serializer
kaminari
pg (~> 1.1)
puma (>= 5.0)
rails (~> 7.2.1)
redis (>= 4.0.1)
rspec-rails (~> 7.0)
rubocop-rails-omakase
simplecov
tzinfo-data
uuid7 (~> 0.2.0)

Expand Down
128 changes: 128 additions & 0 deletions app/controllers/api/v1/categories_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
# frozen_string_literal: true

module Api
module V1
# Controller for version of Categories side of Product Management Service
# This controller handles CRUD operations for categories.
class CategoriesController < ApplicationController
# Before actions to set up authorization and category object
before_action :set_api_v1_category, only: %i[show update destroy]

# GET /api/v1/categories
# GET /api/v1/categories.json
# Retrieves a paginated list of categories filtered by developer token
def index
categories = Category.all

categories = perform_filtering(categories)

paginated_categories = categories.page(params[:page]).per(page_size)

render json: json_response(
paginated_categories, serializer:,
message: 'Categories retrieved successfully'
)
end

# GET /api/v1/categories/:id
# GET /api/v1/categories/:id.json
# Retrieves a specific category by ID
def show
# Render the JSON response with the category
render json: json_response(
category, serializer:,
message: 'Category retrieved successfully'
)
end

# POST /api/v1/categories
# POST /api/v1/categories.json
# Creates a new category
def create
@category = Category.new(api_v1_category_params)

@category.save! # Raise an error when the category could not be saved

# Render the JSON response with the created category
render json: json_response(@category,
message: 'Category created successfully',
serializer:), status: :created
end

# PATCH/PUT /api/v1/categories/:id
# PATCH/PUT /api/v1/categories/:id.json
# Updates an existing category
def update
if @category.update!(api_v1_category_params)
# Render the JSON response with the updated category
render json: json_response(@category,
serializer:,
message: 'Category updated successfully')
else
# Render an error response if the category could not be updated
render json: category.errors, status: :unprocessable_content
end
end

# DELETE /api/v1/categories/:id
# DELETE /api/v1/categories/:id.json
#
# Deletes a specific category by ID
def destroy
@category.destroy!
# Respond with no content status
head :no_content
end

private

# Reader method for the category instance variable
attr_reader :category

# Sets the category instance variable based on the ID and developer
# token
def set_api_v1_category
cache_key = "category/#{params[:id]}_#{developer_id}"
@category = Rails.cache.fetch(cache_key, expires_in: 12.hours) do
Category.find_by!(
id: params[:id],
developer_id:
)
end
end

# Only allow a list of trusted parameters through.
# Permits the name and description parameters and merges the developer
# token
def api_v1_category_params
params.require(:category)
.permit(:name, :description)
.merge(developer_id:)
end

# Returns the serializer class for the category
def serializer
CategorySerializer
end

def perform_filtering(categories)
# Filter categories by developer token
categories = categories.where(developer_id:)

# Filter categories by name
if params[:name].present?
categories = categories.where('name ILIKE ?', "%#{params[:name]}%")
end

# filter categories by the search term, so any category that
# has the search term in its name or description will be returned
return categories if params[:search].blank?

categories.where(
'name ILIKE :search OR description ILIKE :search',
search: "%#{params[:search]}%"
)
end
end
end
end
Loading

0 comments on commit c859dc4

Please sign in to comment.