Skip to content

Commit 4bcf1e6

Browse files
authored
Release v19.12.2023
Release v19.12.2023
2 parents 49641ed + 4c5afaa commit 4bcf1e6

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+1426
-40
lines changed

Gemfile

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,16 @@ gem "devise"
4646

4747
gem 'devise-jwt'
4848

49+
gem 'grape'
50+
51+
gem 'appsignal'
52+
53+
gem 'grape-swagger'
54+
55+
gem 'grape_logging'
56+
57+
gem 'faker'
58+
4959
# Use Sass to process CSS
5060
# gem "sassc-rails"
5161

@@ -55,6 +65,7 @@ gem 'devise-jwt'
5565
group :development, :test do
5666
# See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem
5767
gem "debug", platforms: %i[ mri mingw x64_mingw ]
68+
gem 'byebug'
5869
gem 'rspec-rails'
5970
gem 'factory_bot_rails'
6071
end

Gemfile.lock

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,11 +68,14 @@ GEM
6868
tzinfo (~> 2.0)
6969
addressable (2.8.6)
7070
public_suffix (>= 2.0.2, < 6.0)
71+
appsignal (3.5.0)
72+
rack
7173
bcrypt (3.1.20)
7274
bindex (0.8.1)
7375
bootsnap (1.17.0)
7476
msgpack (~> 1.2)
7577
builder (3.2.4)
78+
byebug (11.1.3)
7679
capybara (3.39.0)
7780
addressable
7881
matrix
@@ -107,14 +110,40 @@ GEM
107110
dry-core (1.0.1)
108111
concurrent-ruby (~> 1.0)
109112
zeitwerk (~> 2.6)
113+
dry-inflector (1.0.0)
114+
dry-logic (1.5.0)
115+
concurrent-ruby (~> 1.0)
116+
dry-core (~> 1.0, < 2)
117+
zeitwerk (~> 2.6)
118+
dry-types (1.7.1)
119+
concurrent-ruby (~> 1.0)
120+
dry-core (~> 1.0)
121+
dry-inflector (~> 1.0)
122+
dry-logic (~> 1.4)
123+
zeitwerk (~> 2.6)
110124
erubi (1.12.0)
111125
factory_bot (6.4.2)
112126
activesupport (>= 5.0.0)
113127
factory_bot_rails (6.4.2)
114128
factory_bot (~> 6.4)
115129
railties (>= 5.0.0)
130+
faker (3.2.2)
131+
i18n (>= 1.8.11, < 2)
116132
globalid (1.2.1)
117133
activesupport (>= 6.1)
134+
grape (2.0.0)
135+
activesupport (>= 5)
136+
builder
137+
dry-types (>= 1.1)
138+
mustermann-grape (~> 1.0.0)
139+
rack (>= 1.3.0)
140+
rack-accept
141+
grape-swagger (2.0.0)
142+
grape (>= 1.7, < 3.0)
143+
rack-test (~> 2)
144+
grape_logging (1.8.4)
145+
grape
146+
rack
118147
i18n (1.14.1)
119148
concurrent-ruby (~> 1.0)
120149
importmap-rails (1.1.5)
@@ -143,6 +172,10 @@ GEM
143172
mini_portile2 (2.8.5)
144173
minitest (5.20.0)
145174
msgpack (1.7.2)
175+
mustermann (3.0.0)
176+
ruby2_keywords (~> 0.0.1)
177+
mustermann-grape (1.0.2)
178+
mustermann (>= 1.0.0)
146179
net-imap (0.4.7)
147180
date
148181
net-protocol
@@ -164,6 +197,8 @@ GEM
164197
nio4r (~> 2.0)
165198
racc (1.7.3)
166199
rack (2.2.8)
200+
rack-accept (0.4.5)
201+
rack (>= 0.4)
167202
rack-test (2.1.0)
168203
rack (>= 1.3)
169204
rails (7.0.4.3)
@@ -220,6 +255,7 @@ GEM
220255
rspec-mocks (~> 3.12)
221256
rspec-support (~> 3.12)
222257
rspec-support (3.12.1)
258+
ruby2_keywords (0.0.5)
223259
rubyzip (2.3.2)
224260
selenium-webdriver (4.10.0)
225261
rexml (~> 3.2, >= 3.2.5)
@@ -273,12 +309,18 @@ PLATFORMS
273309
ruby
274310

275311
DEPENDENCIES
312+
appsignal
276313
bootsnap
314+
byebug
277315
capybara
278316
debug
279317
devise
280318
devise-jwt
281319
factory_bot_rails
320+
faker
321+
grape
322+
grape-swagger
323+
grape_logging
282324
importmap-rails
283325
jbuilder
284326
puma (~> 5.0)

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
# Awesome Blog API
1+
# CodeLearn API
22

3-
Welcome to the Awesome Blog API repository! This project is built using Ruby on Rails, Grape API, JWT (JSON Web Tokens), and Swagger. It provides a robust backend for a web blog application, allowing you to create, retrieve, update, and delete blog posts, comments, and more through a RESTful API.
3+
Welcome to the CodeLearn API repository! This project is built using Ruby on Rails, Grape API, JWT (JSON Web Tokens), and Swagger. It provides a robust backend for a web blog application, allowing you to create, retrieve, update, and delete blog posts, comments, and more through a RESTful API.
44

55
## Table of Contents
66

@@ -57,7 +57,7 @@ This project uses JSON Web Tokens (JWT) for user authentication. Make sure to in
5757
```
5858

5959
## Swagger Documentation
60-
Explore the API endpoints and test them using the Swagger documentation at `http://localhost:3000/doc`.
60+
Explore the API endpoints and test them using the Swagger documentation at `http://localhost:2106/api/web/doc`.
6161

6262
## Contributing
6363
We welcome contributions! If you find a bug or have an enhancement in mind, please open an issue or submit a pull request.

app/api/application_api.rb

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# frozen_string_literal: true
2+
3+
require 'appsignal/integrations/grape'
4+
require 'grape-swagger'
5+
require 'grape_logging'
6+
7+
class ApplicationAPI < Grape::API
8+
format :json
9+
10+
helpers ::Helpers::AuthenticationHelper
11+
helpers do
12+
def unauthorized_error!
13+
error!('Unauthorized', 401)
14+
end
15+
end
16+
17+
rescue_from Grape::Exceptions::ValidationErrors do |e|
18+
error!(e, 400)
19+
end
20+
21+
rescue_from ActiveRecord::RecordNotFound do |e|
22+
error!(e, 404)
23+
end
24+
25+
rescue_from NotImplementedError, TypeError, StandardError do |e|
26+
status_code = e.respond_to?(:status_code) ? e.status_code : 500
27+
Appsignal.set_error(e) if status_code == 500
28+
error!(e, status_code)
29+
end
30+
31+
mount WebAPI
32+
end
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
module Helpers::AuthenticationHelper
2+
extend Grape::API::Helpers
3+
4+
def authenticate_user!
5+
error!('Unauthorized. Invalid or expired token.', 401) unless current_user
6+
end
7+
8+
def current_user
9+
@current_user ||=
10+
begin
11+
token = request.headers['Authorization'].split(' ').last
12+
User.find_by(id: JWT.decode(token, Rails.application.credentials.devise_jwt_secret_key)[0]['user_id']) if token
13+
rescue JWT::DecodeError
14+
nil
15+
end
16+
end
17+
18+
end

app/api/json_error_formatter.rb

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
# frozen_string_literal: true
2+
# typed: true
3+
4+
# Custom error formatter for Grape API
5+
class JsonErrorFormatter
6+
class << self
7+
8+
ERRORS_MAPPER = {
9+
StandardError: 'E999',
10+
'Errors::APIFailures::AccessDeniedError': 'E401',
11+
'Grape::Exceptions::ValidationErrors': 'E400',
12+
'Errors::AccessDeniedError': 'E1001',
13+
'ActiveRecord::RecordNotFound': 'E1002',
14+
'AASM::InvalidTransition': 'E1003',
15+
'ActiveModel::Errors': 'E1004',
16+
ArgumentError: 'E1005',
17+
'Tpl::Services::Base::InvalidKeyError': 'E1006',
18+
'NonUpdatable::NonUpdatableError': 'E1007',
19+
'Errors::StockChecks::AlreadyStartedError': 'E1008',
20+
'Errors::StockChecks::HasNotStartedError': 'E1009',
21+
'Errors::StockChecks::AlreadyEndedError': 'E1010',
22+
'Errors::StockChecks::NotAllLocationsChecked': 'E1011',
23+
'Errors::OverstockCarts::CheckingProductIsNotUnderstockError': 'E1013',
24+
'Errors::OverstockCarts::SupplyOverflowError': 'E1014',
25+
'Errors::OverstockCarts::DiffStockNotMatchedExceedingStockError': 'E1015',
26+
'Errors::OverstockCarts::NotEnoughStockOnCart': 'E1016',
27+
'Errors::SharedAccessKeys::InvalidKeyError': 'E1017',
28+
'Errors::APIFailures::ServiceFailure': 'E1018',
29+
'Errors::ProductUnits::AlreadyGtinCodeError': 'E1019',
30+
}.freeze
31+
32+
private_constant :ERRORS_MAPPER
33+
34+
# Return the error response in JSON format
35+
def call(messages, _backtrace, _options, _env, _original_exception)
36+
error_code = ERRORS_MAPPER[messages.class.to_s.to_sym] || ERRORS_MAPPER[messages.to_s.to_sym]
37+
error_code ||= ERRORS_MAPPER[:StandardError]
38+
39+
{
40+
error: error_code,
41+
messages: parse_messages(messages).uniq,
42+
debug: debug_data(messages),
43+
}.to_json
44+
end
45+
46+
# Adding the debug data to the error response
47+
def debug_data(messages)
48+
return {} if Rails.env.production?
49+
50+
{
51+
inspect: messages.try(:inspect)&.first(100),
52+
backtrace: messages.try(:backtrace)&.first(30),
53+
}
54+
end
55+
56+
# Return the error messages
57+
def parse_messages(exception) # rubocop:disable Metrics/MethodLength
58+
case exception
59+
when ActiveModel::Errors, Grape::Exceptions::ValidationErrors
60+
exception.full_messages
61+
when ActiveRecord::RecordNotFound
62+
Array(I18n.t('api.errors.not_found', model: exception.model))
63+
when Errors::APIFailures::ServiceFailure
64+
exception.errors.full_messages
65+
when ActiveRecord::RecordInvalid, StandardError
66+
Array(exception.message)
67+
when Array
68+
exception
69+
else
70+
Array(exception)
71+
end
72+
end
73+
end
74+
end

app/api/json_formatter.rb

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# frozen_string_literal: true
2+
# typed: true
3+
4+
# Custom formatter for Grape API
5+
class JsonFormatter
6+
class << self
7+
# Return the response in JSON format
8+
def call(obj, env)
9+
case obj
10+
when ServiceResult
11+
success_entity = get_success_entity(env)
12+
to_json(success_entity.represent(success_entity.try(:collection?) ? obj.records : obj.record), env)
13+
when API::Result
14+
success_entity = get_success_entity(env)
15+
to_json(success_entity.represent(obj.data), env)
16+
else
17+
to_json(obj, env)
18+
end
19+
end
20+
21+
private
22+
23+
def to_json(obj, env)
24+
Grape::Formatter::Json.call(obj, env)
25+
end
26+
27+
def get_success_entity(env)
28+
success_entity = env.dig('grape.routing_args', :route_info).options[:success]
29+
success_entity || raise(Errors::APIFailures::InvalidSuccessConfigError)
30+
end
31+
end
32+
end
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# frozen_string_literal: true
2+
3+
# GteDateValidator for Grape params
4+
class Validators::GteDateValidator < Grape::Validations::Validators::Base
5+
FORM_INPUT_TIME = 10.minutes
6+
private_constant :FORM_INPUT_TIME
7+
8+
# validate_param! method is required
9+
def validate_param!(param_name, params)
10+
case @option
11+
when :now
12+
validate_current_date!(param_name, params)
13+
when Symbol
14+
validate_greater_than!(param_name, params)
15+
else
16+
error_message = I18n.t('validators.un_supported', option: param_name, class: 'GteDateValidator')
17+
raise(error_message)
18+
end
19+
end
20+
21+
private
22+
23+
def validate_greater_than!(param_name, params)
24+
return if params[@option]&.value.blank?
25+
return if params[param_name].value >= params[@option].value
26+
27+
raise(Grape::Exceptions::Validation.new(params: [@scope.full_name(param_name)],
28+
message: I18n.t('validators.invalid_greater_than_date', date: @option)))
29+
end
30+
31+
def validate_current_date!(param_name, params)
32+
return if @option.blank?
33+
return if params[param_name].value > FORM_INPUT_TIME.ago
34+
35+
raise(Grape::Exceptions::Validation.new(params: [@scope.full_name(param_name)],
36+
message: I18n.t('validators.invalid_greater_than_date', date: 'current date')))
37+
end
38+
end
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# frozen_string_literal: true
2+
3+
# LengthValidator for Grape params
4+
class Validators::LengthValidator < Grape::Validations::Validators::Base
5+
# validate_param! method is required
6+
def validate_param!(attr_name, params)
7+
return if params[attr_name].blank?
8+
return if params[attr_name].length.in?(limited_length)
9+
10+
raise(Grape::Exceptions::Validation.new(params: [@scope.full_name(attr_name)], message: error_message(attr_name)))
11+
end
12+
13+
private
14+
15+
def limited_length
16+
case @option
17+
when Range
18+
@option
19+
when Hash
20+
@option[:minimun]..@option[:maximum]
21+
when Integer
22+
@option..@option
23+
else
24+
raise(Grape::Exceptions::Validation.new(params: [@scope.full_name(attr_name)], message: 'Length is invalid'))
25+
end
26+
end
27+
28+
def error_message(attr_name)
29+
case @option
30+
when Range
31+
"#{attr_name} length must be in #{@option}"
32+
when Hash
33+
"#{attr_name} length must be in #{Integer(@option[:minimun], 10)}..#{Integer(@option[:maximum], 10)}"
34+
when Integer
35+
"#{attr_name} length must be #{@option}"
36+
end
37+
end
38+
end

0 commit comments

Comments
 (0)