Skip to content

Instantly share code, notes, and snippets.

@webuilder240
Last active May 14, 2023 00:33
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save webuilder240/da94ab9661fbbf5b89f8ca11e094fcd7 to your computer and use it in GitHub Desktop.
Save webuilder240/da94ab9661fbbf5b89f8ca11e094fcd7 to your computer and use it in GitHub Desktop.
require_relative 'errors'
module ApiModel
module Base
extend ActiveSupport::Concern
include ActiveModel::Model
include ActiveModel::Attributes
include ActiveModel::Serializers::JSON
included do
private
attr_accessor :response
end
class_methods do
def api_url
'http://localhost:3000'
end
def client
Faraday.new(url: api_url) do |faraday|
faraday.adapter Faraday.default_adapter
faraday.request :json
faraday.response :json, parser_options: { symbolize_names: true }
end
end
def raise_api_exception(response)
case response.status
when 404
raise ApiNotFound.new(response), "Record not found"
when 422
raise ApiInvalidError.new(response), "Invalid data"
when 400..499
raise HttpClientError.new(response), "Client error"
when 500..599
raise HttpServerError.new(response), "Server error"
else
raise ApiError.new(response), "Api Error"
end
end
def request(method, *args, &block)
response = client.public_send(method, *args) do |req|
yield req if block_given?
end
if response.success?
response
else
raise_api_exception(response)
end
end
end
def has_error?
errors.any?
end
def validate!
raise RecordInvalidError.new(self) unless valid?
end
private
def request(method, *args, &block)
@response = self.class.client.public_send(method, *args) do |req|
yield req if block_given?
end
if response.success?
true
else
handle_api_errors
end
end
def request!(method, *args, &block)
@response = self.class.client.public_send(method, *args) do |req|
yield req if block_given?
end
if response.success?
self
else
raise_api_exception
end
end
def raise_api_exception
case response.status
when 404
raise ApiNotFound.new(response), "Record not found"
when 422
add_api_errors
raise RecordInvalidError.new(self, response)
when 400..499
raise HttpClientError.new(response), "Client error"
when 500..599
raise HttpServerError.new(response), "Server error"
else
raise ApiError.new(response), "Api Error"
end
end
def handle_api_errors
raise_api_exception
rescue RecordInvalidError
false
end
def add_api_errors
return unless response.status == 422
error_message = :unknown_error
# Get the custom error message if available, otherwise use the default message
custom_error_key = "activemodel.errors.models.#{self.class.name.underscore}.response.#{error_message}"
common_error_key = "activemodel.errors.api_record.response.#{error_message}"
error_message_text = I18n.t(custom_error_key, default: I18n.t(common_error_key))
errors.add(:response, error_message_text)
error_response = response.body
if error_response.is_a?(Hash)
error_response.each do |key, error_details|
error_details.each do |message|
errors.add(key, message)
end
end
end
end
end
end
module ApiModel
class RecordInvalidError < StandardError
attr_reader :record, :response
def initialize(record, response = nil)
@record = record
@response = response
message = "Validation failed: #{record.errors.full_messages.join(', ')}"
super(message)
end
end
class ApiError < StandardError
attr_reader :response
def initialize(response)
@response = response
end
end
class HttpClientError < ApiError; end
class HttpServerError < ApiError; end
class ApiNotFound < HttpClientError; end
end
module Api
class Payment
include ApiModel::Base
attribute :user
attribute :card_number, :integer
validates :card_number, presence: true, numericality: { only_integer: true, greater_than: 0 }
validates :user, presence: true
def authorize
return false unless valid?
params = {
user_id: user.id,
card_number: card_number
}
request(:post, "/users/#{user.id}/payments", params)
end
def authorize!
validate!
params = {
user_id: user.id,
card_number: card_number
}
request!(:post, "/users/#{user.id}/payments", params)
end
end
end
module Api
class Product
include ApiRecord::Base
attribute :id, :integer
attribute :title, :string
attribute :description, :string
attribute :price, :integer
attribute :discount_percentage, :decimal
attribute :rating, :decimal
attribute :stock, :integer
attribute :brand, :string
attribute :category, :string
attribute :thumbnail, :string
attribute :images
validates :id, presence: true, numericality: { only_integer: true }
validates :title, presence: true, length: { maximum: 255 }
validates :description, presence: true
validates :price, presence: true, numericality: { greater_than_or_equal_to: 0 }
validates :discount_percentage, presence: true, numericality: { greater_than_or_equal_to: 0, less_than_or_equal_to: 100 }
validates :rating, presence: true, numericality: { greater_than_or_equal_to: 0, less_than_or_equal_to: 5 }
validates :stock, presence: true, numericality: { only_integer: true, greater_than_or_equal_to: 0 }
validates :brand, presence: true, length: { maximum: 255 }
validates :category, presence: true, length: { maximum: 255 }
validates :thumbnail, presence: true, format: { with: /\Ahttps?:\/\/.+\z/ }
def request_params(param)
param
end
def self.api_url
"https://dummyjson.com"
end
def self.index
response = request(:get, "/products", {})
if response.success?
body = response.body['products']
ApiRecord::Relation.new(body.map do |attributes|
new(attributes.deep_transform_keys!(&:underscore))
end, response)
else
raise_api_exception(response)
end
end
def self.create(params)
record = new(params)
record.save
record
end
def self.create!(params)
record = new(params)
record.save!
record
end
def self.update!(params)
self.attributes = params
save!
end
def self.update(params)
self.attributes = params
save
end
def save
return false unless valid?
result = perform_save_request
if result
if response.body.present?
body = response.body.deep_transform_keys!(&:underscore)
self.attributes = body
end
end
result
end
def save!
validate!
perform_save_request!
if response.body.present?
body = response.body.deep_transform_keys!(&:underscore)
self.attributes = body
end
self
end
def destroy
request(:delete, "/products/#{id}")
if response.success?
true
else
handle_api_errors()
false
end
end
def destroy!
request(:delete, "/products/#{id}")
if response.success?
self
else
raise_api_exception()
end
end
private
def perform_save_request!
params = self.class.request_params(attributes)
params.delete("id")
if new_record?
request!(:post, "/products", params)
else
request!(:put, "/products/#{id}", params)
end
end
def perform_save_request
params = self.class.request_params(attributes)
params.delete("id")
if new_record?
request(:post, "/products", params)
else
request(:put, "/products/#{id}", params)
end
end
end
end
class ProductsController < ApplicationController
before_action :set_product, only: [:show, :update, :destroy]
def index
@products = Api::Product.index.data
end
def show
end
def create
@product = Api::Product.new(params)
if @product.save
render :show, status: 201
else
render json: { @product.errors }, status: 422
end
end
def update
if @product.update(params)
render :show
else
render json: { @product.errors }, status: 422
end
end
def destroy
@product.destroy
head :no_content
end
private
def set_product
@product = Api::Product.find(params[:id])
end
# Only allow a list of trusted parameters through.
def product_params
params.require(:product).permit(:title)
end
end
module ApiRecord
Relation = Struct.new(:data, :response)
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment