hati-jsonapi-error 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: f5c0d7b15ad75fc5e3cc6671e4468ad84d34414d7806f5fb08f7f27d03cdad0f
4
+ data.tar.gz: 2dfc99d96da5c7f1f65023d7120755deac9ecb42a62f5bc5b49fe36c584c77ff
5
+ SHA512:
6
+ metadata.gz: 2d320265c9f28b71b6421fe04e6341e7d877c53ac99ff7c2667e3fb2f96d4d24cc094db3509b3ee2fcc851aa053632ed422730da3e89c620cf011751d6946f60
7
+ data.tar.gz: d5e54f2fd110a5ab0e0e13abd51b1d54bc9cf8f0b7060332cff16c1ceeff447cac7616621b4b2b89262ae1bdc8047bcddfdf492d23b94ff27a06a5e41faabc56
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 hackico.ai
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,391 @@
1
+ # Hati JSON:API Error
2
+
3
+ [![Gem Version](https://badge.fury.io/rb/hati-jsonapi-error.svg)](https://badge.fury.io/rb/hati-jsonapi-error)
4
+ [![Ruby](https://img.shields.io/badge/ruby-%3E%3D%203.0.0-ruby.svg)](https://ruby-lang.org)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
6
+ [![Test Coverage](https://img.shields.io/badge/coverage-100%25-brightgreen.svg)](https://github.com/hackico-ai/ruby-hati-jsonapi-error)
7
+
8
+ > **Production-ready JSON:API-compliant error responses for professional Web APIs**
9
+
10
+ Transform inconsistent error handling into standardized, traceable responses. Built for Ruby applications requiring enterprise-grade error management.
11
+
12
+ ## Why Standardized Error Handling Matters
13
+
14
+ ### The Problem: Inconsistent Error Responses
15
+
16
+ Different controllers returning different error formats creates maintenance nightmares:
17
+
18
+ ```ruby
19
+ # Three different error formats in one application
20
+ class UsersController
21
+ def show
22
+ render json: { error: "User not found" }, status: 404
23
+ end
24
+ end
25
+
26
+ class OrdersController
27
+ def create
28
+ render json: { message: "Validation failed", details: errors }, status: 422
29
+ end
30
+ end
31
+
32
+ class PaymentsController
33
+ def process
34
+ render json: { errors: errors, error_code: "INVALID", status: "failure" }, status: 400
35
+ end
36
+ end
37
+ ```
38
+
39
+ This forces frontend developers to handle multiple error formats:
40
+
41
+ ```javascript
42
+ // Unmaintainable error handling
43
+ if (data.error) {
44
+ showError(data.error); // Users format
45
+ } else if (data.message && data.details) {
46
+ showError(`${data.message}: ${data.details.join(", ")}`); // Orders format
47
+ } else if (data.errors && data.error_code) {
48
+ showError(`${data.error_code}: ${data.errors.join(", ")}`); // Payments format
49
+ }
50
+ ```
51
+
52
+ ### The Impact
53
+
54
+ - **API Documentation**: Each endpoint needs custom error documentation
55
+ - **Error Tracking**: Different structures break centralized logging
56
+ - **Client SDKs**: Cannot provide consistent error handling
57
+ - **Testing**: Each format requires separate test cases
58
+ - **Team Coordination**: New developers must learn multiple patterns
59
+
60
+ ### The Solution: JSON:API Standard
61
+
62
+ **One format across all endpoints:**
63
+
64
+ ```ruby
65
+ raise HatiJsonapiError::UnprocessableEntity.new(
66
+ detail: "Email address is required",
67
+ source: { pointer: "/data/attributes/email" }
68
+ )
69
+ ```
70
+
71
+ **Always produces standardized output:**
72
+
73
+ ```json
74
+ {
75
+ "errors": [
76
+ {
77
+ "status": 422,
78
+ "code": "unprocessable_entity",
79
+ "title": "Validation Failed",
80
+ "detail": "Email address is required",
81
+ "source": { "pointer": "/data/attributes/email" }
82
+ }
83
+ ]
84
+ }
85
+ ```
86
+
87
+ ## ✨ Features
88
+
89
+ - **JSON:API Compliant** - Follows the official [JSON:API error specification](https://jsonapi.org/format/#errors)
90
+ - **Auto-Generated Error Classes** - Dynamic HTTP status code error classes (400-511)
91
+ - **Rich Error Context** - Support for `id`, `code`, `title`, `detail`, `status`, `meta`, `links`, `source`
92
+ - **Error Registry** - Map custom exceptions to standardized responses
93
+ - **Controller Integration** - Helper methods for Rails, Sinatra, and other frameworks
94
+ - **100% Test Coverage** - Comprehensive RSpec test suite
95
+ - **Zero Dependencies** - Lightweight and fast
96
+ - **Production Ready** - Thread-safe and memory efficient
97
+
98
+ ## Installation
99
+
100
+ ```ruby
101
+ # Gemfile
102
+ gem 'hati-jsonapi-error'
103
+ ```
104
+
105
+ ```bash
106
+ bundle install
107
+ ```
108
+
109
+ ## Quick Start
110
+
111
+ ### 1. Configuration
112
+
113
+ ```ruby
114
+ # config/initializers/hati_jsonapi_error.rb
115
+ HatiJsonapiError::Config.configure do |config|
116
+ config.load_errors!
117
+
118
+ config.map_errors = {
119
+ ActiveRecord::RecordNotFound => :not_found,
120
+ ActiveRecord::RecordInvalid => :unprocessable_entity,
121
+ ArgumentError => :bad_request
122
+ }
123
+
124
+ config.use_unexpected = HatiJsonapiError::InternalServerError
125
+ end
126
+ ```
127
+
128
+ ### 2. Basic Usage
129
+
130
+ ```ruby
131
+ # Simple error raising
132
+ raise HatiJsonapiError::NotFound.new
133
+ raise HatiJsonapiError::BadRequest.new
134
+ raise HatiJsonapiError::Unauthorized.new
135
+ ```
136
+
137
+ ## Usage Examples
138
+
139
+ ### Basic Error Handling
140
+
141
+ **Access errors multiple ways:**
142
+
143
+ ```ruby
144
+ # By class name
145
+ raise HatiJsonapiError::NotFound.new
146
+
147
+ # By status code
148
+ api_err = HatiJsonapiError::Helpers::ApiErr
149
+ raise api_err[404]
150
+
151
+ # By error code
152
+ raise api_err[:not_found]
153
+ ```
154
+
155
+ ### Rich Error Context
156
+
157
+ **Add debugging information:**
158
+
159
+ ```ruby
160
+ HatiJsonapiError::NotFound.new(
161
+ id: 'user_lookup_failed',
162
+ detail: 'User with email john@example.com was not found',
163
+ source: { pointer: '/data/attributes/email' },
164
+ meta: {
165
+ searched_email: 'john@example.com',
166
+ suggestion: 'Verify the email address is correct'
167
+ }
168
+ )
169
+ ```
170
+
171
+ ### Multiple Validation Errors
172
+
173
+ **Collect and return multiple errors:**
174
+
175
+ ```ruby
176
+ errors = []
177
+ errors << HatiJsonapiError::UnprocessableEntity.new(
178
+ detail: "Email format is invalid",
179
+ source: { pointer: '/data/attributes/email' }
180
+ )
181
+ errors << HatiJsonapiError::UnprocessableEntity.new(
182
+ detail: "Password too short",
183
+ source: { pointer: '/data/attributes/password' }
184
+ )
185
+
186
+ resolver = HatiJsonapiError::Resolver.new(errors)
187
+ render json: resolver.to_json, status: resolver.status
188
+ ```
189
+
190
+ ## Controller Integration
191
+
192
+ ```ruby
193
+ class ApiController < ApplicationController
194
+ include HatiJsonapiError::Helpers
195
+
196
+ rescue_from StandardError, with: :handle_error
197
+
198
+ def show
199
+ # ActiveRecord::RecordNotFound automatically mapped to JSON:API NotFound
200
+ user = User.find(params[:id])
201
+ render json: user
202
+ end
203
+
204
+ def create
205
+ user = User.new(user_params)
206
+
207
+ unless user.save
208
+ validation_error = HatiJsonapiError::UnprocessableEntity.new(
209
+ detail: user.errors.full_messages.join(', '),
210
+ source: { pointer: '/data/attributes' },
211
+ meta: { validation_errors: user.errors.messages }
212
+ )
213
+
214
+ return render_error(validation_error)
215
+ end
216
+
217
+ render json: user, status: :created
218
+ end
219
+ end
220
+ ```
221
+
222
+ ### Custom Error Classes
223
+
224
+ **Domain-specific errors:**
225
+
226
+ ```ruby
227
+ class PaymentRequiredError < HatiJsonapiError::PaymentRequired
228
+ def initialize(amount:, currency: 'USD')
229
+ super(
230
+ detail: "Payment of #{amount} #{currency} required",
231
+ meta: {
232
+ required_amount: amount,
233
+ currency: currency,
234
+ payment_methods: ['credit_card', 'paypal']
235
+ },
236
+ links: {
237
+ payment_page: "https://app.com/billing/upgrade?amount=#{amount}"
238
+ }
239
+ )
240
+ end
241
+ end
242
+
243
+ # Usage
244
+ raise PaymentRequiredError.new(amount: 29.99)
245
+ ```
246
+
247
+ ## Functional Programming Integration
248
+
249
+ Perfect for functional programming patterns with [hati-operation gem](https://github.com/hackico-ai/ruby-hati-operation):
250
+
251
+ ```ruby
252
+ require 'hati_operation'
253
+
254
+ class Api::User::CreateOperation < Hati::Operation
255
+ ApiErr = HatiJsonapiError::Helpers::ApiErr
256
+
257
+ def call(params)
258
+ user_params = step validate_params(params), err: ApiErr[422]
259
+ user = step create_user(user_params), err: ApiErr[409]
260
+ profile = step create_profile(user), err: ApiErr[503]
261
+
262
+ Success(profile)
263
+ end
264
+
265
+ private
266
+
267
+ def validate_params(params)
268
+ return Failure('Invalid parameters') unless params[:name]
269
+ Success(params)
270
+ end
271
+ end
272
+ ```
273
+
274
+ ## Configuration
275
+
276
+ ### Error Mapping
277
+
278
+ ```ruby
279
+ HatiJsonapiError::Config.configure do |config|
280
+ config.map_errors = {
281
+ # Rails exceptions
282
+ ActiveRecord::RecordNotFound => :not_found,
283
+ ActiveRecord::RecordInvalid => :unprocessable_entity,
284
+
285
+ # Custom exceptions
286
+ AuthenticationError => :unauthorized,
287
+ RateLimitError => :too_many_requests,
288
+
289
+ # Infrastructure exceptions
290
+ Redis::TimeoutError => :service_unavailable,
291
+ Net::ReadTimeout => :gateway_timeout
292
+ }
293
+
294
+ config.use_unexpected = HatiJsonapiError::InternalServerError
295
+ end
296
+ ```
297
+
298
+ ## Available Error Classes
299
+
300
+ **Quick Reference - Most Common:**
301
+
302
+ | Status | Class | Code |
303
+ | ------ | --------------------- | ----------------------- |
304
+ | 400 | `BadRequest` | `bad_request` |
305
+ | 401 | `Unauthorized` | `unauthorized` |
306
+ | 403 | `Forbidden` | `forbidden` |
307
+ | 404 | `NotFound` | `not_found` |
308
+ | 422 | `UnprocessableEntity` | `unprocessable_entity` |
309
+ | 429 | `TooManyRequests` | `too_many_requests` |
310
+ | 500 | `InternalServerError` | `internal_server_error` |
311
+ | 502 | `BadGateway` | `bad_gateway` |
312
+ | 503 | `ServiceUnavailable` | `service_unavailable` |
313
+
314
+ **[Complete list of all 39 HTTP status codes →](HTTP_STATUS_CODES.md)**
315
+
316
+ ## Testing
317
+
318
+ ### RSpec Integration
319
+
320
+ ```ruby
321
+ # Shared examples for JSON:API compliance
322
+ RSpec.shared_examples 'JSON:API error response' do |expected_status, expected_code|
323
+ it 'returns proper JSON:API error format' do
324
+ json = JSON.parse(response.body)
325
+
326
+ expect(response).to have_http_status(expected_status)
327
+ expect(json['errors'].first['status']).to eq(expected_status)
328
+ expect(json['errors'].first['code']).to eq(expected_code)
329
+ end
330
+ end
331
+
332
+ # Usage in specs
333
+ describe 'GET #show' do
334
+ context 'when user not found' do
335
+ subject { get :show, params: { id: 'nonexistent' } }
336
+ include_examples 'JSON:API error response', 404, 'not_found'
337
+ end
338
+ end
339
+ ```
340
+
341
+ ### Unit Testing
342
+
343
+ ```ruby
344
+ RSpec.describe HatiJsonapiError::NotFound do
345
+ it 'has correct default attributes' do
346
+ error = described_class.new
347
+
348
+ expect(error.status).to eq(404)
349
+ expect(error.code).to eq(:not_found)
350
+ expect(error.to_h[:title]).to eq('Not Found')
351
+ end
352
+ end
353
+ ```
354
+
355
+ ## Benefits
356
+
357
+ **For Development Teams:**
358
+
359
+ - Reduced development time with single error pattern
360
+ - Easier onboarding for new developers
361
+ - Better testing with standardized structure
362
+ - Improved debugging with consistent error tracking
363
+
364
+ **For Frontend/Mobile Teams:**
365
+
366
+ - One error parser for entire API
367
+ - Rich error context for better user experience
368
+ - Easier SDK development
369
+
370
+ **For Operations:**
371
+
372
+ - Centralized monitoring and alerting
373
+ - Consistent error analysis
374
+ - Simplified documentation
375
+
376
+ ## Contributing
377
+
378
+ ```bash
379
+ git clone https://github.com/hackico-ai/ruby-hati-jsonapi-error.git
380
+ cd ruby-hati-jsonapi-error
381
+ bundle install
382
+ bundle exec rspec
383
+ ```
384
+
385
+ ## License
386
+
387
+ MIT License - see [LICENSE](LICENSE) file.
388
+
389
+ ---
390
+
391
+ **Professional error handling for professional APIs**
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ lib = File.expand_path('lib', __dir__)
4
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
+
6
+ require 'hati_jsonapi_error/version'
7
+
8
+ Gem::Specification.new do |spec|
9
+ spec.name = 'hati-jsonapi-error'
10
+ spec.version = HatiJsonapiError::VERSION
11
+ spec.authors = ['Marie Giy']
12
+ spec.email = %w[giy.mariya@gmail.com]
13
+ spec.license = 'MIT'
14
+
15
+ spec.summary = 'Standardized JSON: API-compliant error responses made easy for your Web API.'
16
+ spec.description = 'hati-jsonapi-error is a Ruby gem for Standardized JSON Error.'
17
+ spec.homepage = "https://github.com/hackico-ai/#{spec.name}"
18
+
19
+ spec.required_ruby_version = '>= 3.0.0'
20
+
21
+ spec.files = Dir['CHANGELOG.md', 'LICENSE', 'README.md', 'hati-jsonapi-error.gemspec', 'lib/**/*']
22
+ spec.bindir = 'bin'
23
+ spec.executables = []
24
+ spec.require_paths = ['lib']
25
+
26
+ spec.metadata['repo_homepage'] = spec.homepage
27
+ spec.metadata['allowed_push_host'] = 'https://rubygems.org'
28
+
29
+ spec.metadata['homepage_uri'] = spec.homepage
30
+ spec.metadata['changelog_uri'] = "#{spec.homepage}/blob/main/CHANGELOG.md"
31
+ spec.metadata['source_code_uri'] = spec.homepage
32
+ spec.metadata['bug_tracker_uri'] = "#{spec.homepage}/issues"
33
+
34
+ spec.metadata['rubygems_mfa_required'] = 'true'
35
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HatiJsonapiError
4
+ # This is the base error class for all errors in the HatiJsonapiError gem.
5
+ class BaseError < ::StandardError
6
+ STR = ''
7
+ OBJ = {}.freeze
8
+
9
+ attr_accessor :id, :code, :title, :detail, :status, :meta, :links, :source
10
+
11
+ def initialize(**attrs)
12
+ @id = attrs[:id] || STR
13
+ @code = attrs[:code] || STR
14
+ @title = attrs[:title] || STR
15
+ @detail = attrs[:detail] || STR
16
+ @status = attrs[:status] || STR
17
+
18
+ @links = build_links(attrs[:links])
19
+ @source = build_source(attrs[:source])
20
+ @meta = attrs[:meta] || OBJ
21
+
22
+ super(error_message)
23
+ end
24
+
25
+ # NOTE: used in lib/hati_jsonapi_error/payload_adapter.rb
26
+ def to_h
27
+ {
28
+ id: id,
29
+ links: links.to_h,
30
+ status: status,
31
+ code: code,
32
+ title: title,
33
+ detail: detail,
34
+ source: source.to_h,
35
+ meta: meta
36
+ }
37
+ end
38
+
39
+ def to_s
40
+ to_h.to_s
41
+ end
42
+
43
+ def serializable_hash
44
+ to_h
45
+ end
46
+
47
+ def to_json(*_args)
48
+ serializable_hash.to_json
49
+ end
50
+
51
+ private
52
+
53
+ def build_links(links_attrs)
54
+ links_attrs ? Links.new(**links_attrs) : OBJ
55
+ end
56
+
57
+ def build_source(source_attrs)
58
+ source_attrs ? Source.new(**source_attrs) : OBJ
59
+ end
60
+
61
+ def error_message
62
+ @detail.empty? ? @title : @detail
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HatiJsonapiError
4
+ # rubocop:disable Layout/LineLength
5
+ CLIENT = {
6
+ 400 => { name: 'BadRequest', code: :bad_request, message: 'Bad Request' },
7
+ 401 => { name: 'Unauthorized', code: :unauthorized, message: 'Unauthorized' },
8
+ 402 => { name: 'PaymentRequired', code: :payment_required, message: 'Payment Required' },
9
+ 403 => { name: 'Forbidden', code: :forbidden, message: 'Forbidden' },
10
+ 404 => { name: 'NotFound', code: :not_found, message: 'Not Found' },
11
+ 405 => { name: 'MethodNotAllowed', code: :method_not_allowed, message: 'Method Not Allowed' },
12
+ 406 => { name: 'NotAcceptable', code: :not_acceptable, message: 'Not Acceptable' },
13
+ 407 => { name: 'ProxyAuthenticationRequired', code: :proxy_authentication_required, message: 'Proxy Authentication Required' },
14
+ 408 => { name: 'RequestTimeout', code: :request_timeout, message: 'Request Timeout' },
15
+ 409 => { name: 'Conflict', code: :conflict, message: 'Conflict' },
16
+ 410 => { name: 'Gone', code: :gone, message: 'Gone' },
17
+ 411 => { name: 'LengthRequired', code: :length_required, message: 'Length Required' },
18
+ 412 => { name: 'PreconditionFailed', code: :precondition_failed, message: 'Precondition Failed' },
19
+ 413 => { name: 'RequestEntityTooLarge', code: :request_entity_too_large, message: 'Request Entity Too Large' },
20
+ 414 => { name: 'RequestUriTooLong', code: :request_uri_too_long, message: 'Request Uri Too Long' },
21
+ 415 => { name: 'UnsupportedMediaType', code: :unsupported_media_type, message: 'Unsupported Media Type' },
22
+ 416 => { name: 'RequestedRangeNotSatisfiable', code: :requested_range_not_satisfiable, message: 'Requested Range Not Satisfiable' },
23
+ 417 => { name: 'ExpectationFailed', code: :expectation_failed, message: 'Expectation Failed' },
24
+ 421 => { name: 'MisdirectedRequest', code: :misdirected_request, message: 'Misdirected Request' },
25
+ 422 => { name: 'UnprocessableEntity', code: :unprocessable_entity, message: 'Unprocessable Entity' },
26
+ 423 => { name: 'Locked', code: :locked, message: 'Locked' },
27
+ 424 => { name: 'FailedDependency', code: :failed_dependency, message: 'Failed Dependency' },
28
+ 425 => { name: 'TooEarly', code: :too_early, message: 'Too Early' },
29
+ 426 => { name: 'UpgradeRequired', code: :upgrade_required, message: 'Upgrade Required' },
30
+ 428 => { name: 'PreconditionRequired', code: :precondition_required, message: 'Precondition Required' },
31
+ 429 => { name: 'TooManyRequests', code: :too_many_requests, message: 'Too Many Requests' },
32
+ 431 => { name: 'RequestHeaderFieldsTooLarge', code: :request_header_fields_too_large, message: 'Request Header Fields Too Large' },
33
+ 451 => { name: 'UnavailableForLegalReasons', code: :unavailable_for_legal_reasons, message: 'Unavailable for Legal Reasons' }
34
+ }.freeze
35
+
36
+ SERVER = {
37
+ 500 => { name: 'InternalServerError', code: :internal_server_error, message: 'Internal Server Error' },
38
+ 501 => { name: 'NotImplemented', code: :not_implemented, message: 'Not Implemented' },
39
+ 502 => { name: 'BadGateway', code: :bad_gateway, message: 'Bad Gateway' },
40
+ 503 => { name: 'ServiceUnavailable', code: :service_unavailable, message: 'Service Unavailable' },
41
+ 504 => { name: 'GatewayTimeout', code: :gateway_timeout, message: 'Gateway Timeout' },
42
+ 505 => { name: 'HttpVersionNotSupported', code: :http_version_not_supported, message: 'HTTP Version Not Supported' },
43
+ 506 => { name: 'VariantAlsoNegotiates', code: :variant_also_negotiates, message: 'Variant Also Negotiates' },
44
+ 507 => { name: 'InsufficientStorage', code: :insufficient_storage, message: 'Insufficient Storage' },
45
+ 508 => { name: 'LoopDetected', code: :loop_detected, message: 'Loop Detected' },
46
+ 509 => { name: 'BandwidthLimitExceeded', code: :bandwidth_limit_exceeded, message: 'Bandwidth Limit Exceeded' },
47
+ 510 => { name: 'NotExtended', code: :not_extended, message: 'Not Extended' },
48
+ 511 => { name: 'NetworkAuthenticationRequired', code: :network_authentication_required, message: 'Network Authentication Required' }
49
+ }.freeze
50
+ # rubocop:enable Layout/LineLength
51
+
52
+ STATUS_MAP = CLIENT.merge(SERVER)
53
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HatiJsonapiError
4
+ # This class is used to build the links object for the error response.
5
+ class Links
6
+ STR = ''
7
+
8
+ attr_accessor :about, :type
9
+
10
+ def initialize(about: STR, type: STR)
11
+ @about = about
12
+ @type = type
13
+ end
14
+
15
+ def to_h
16
+ {
17
+ about: about,
18
+ type: type
19
+ }
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HatiJsonapiError
4
+ # This class is used to build the source object for the error response.
5
+ class Source
6
+ STR = ''
7
+
8
+ attr_accessor :pointer, :parameter, :header
9
+
10
+ def initialize(pointer: STR, parameter: STR, header: STR)
11
+ @pointer = pointer
12
+ @parameter = parameter
13
+ @header = header
14
+ end
15
+
16
+ def to_h
17
+ {
18
+ pointer: pointer,
19
+ parameter: parameter,
20
+ header: header
21
+ }
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,42 @@
1
+ module HatiJsonapiError
2
+ class Config
3
+ # HatiJsonapiError::Config.configure do |config|
4
+ # config.load_error!
5
+ # config.map_errors = {
6
+ # ActiveRecord::RecordNotFound => ApiError::NotFound,
7
+ # ActiveRecord::RecordInvalid => ApiError::UnprocessableEntity
8
+ # ActiveRecord::RecordNotUnique => :conflict
9
+ # ActiveRecord::Unauthorized => 401
10
+ # }
11
+ # config.use_unexpected = InternalServerError
12
+ # end
13
+
14
+ # TODO: preload rails rescue responses
15
+ # - what to do about order ???order of loading is important
16
+ # - what to do about rails app?
17
+ # - what to do about rails app not loaded?
18
+ # - what to do about rails app not loaded?
19
+ class << self
20
+ def configure
21
+ yield self if block_given?
22
+ end
23
+
24
+ def use_unexpected=(fallback_error)
25
+ HatiJsonapiError::Registry.fallback = fallback_error
26
+ end
27
+
28
+ def map_errors=(error_map)
29
+ HatiJsonapiError::Registry.error_map = error_map
30
+ end
31
+
32
+ def error_map
33
+ HatiJsonapiError::Registry.error_map
34
+ end
35
+
36
+ # TODO: check if double defintion of errors
37
+ def load_errors!
38
+ HatiJsonapiError::Kigen.load_errors!
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HatiJsonapiError
4
+ # WIP: draft
5
+ module Errors
6
+ class HelpersHandleError < StandardError
7
+ def initialize(message = 'Invalid Helpers:handle_error')
8
+ super
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HatiJsonapiError
4
+ # WIP: draft
5
+ module Errors
6
+ class HelpersRenderError < StandardError
7
+ def initialize(message = 'Invalid Helpers:render_error')
8
+ super
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HatiJsonapiError
4
+ # WIP: draft
5
+ module Errors
6
+ class NotDefinedErrorClassError < StandardError
7
+ def initialize(message = 'Error class not defined')
8
+ super
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HatiJsonapiError
4
+ # WIP: draft
5
+ module Errors
6
+ class NotLoadedError < StandardError
7
+ def initialize(message = 'HatiJsonapiError::Kigen not loaded')
8
+ super
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HatiJsonapiError
4
+ # class ApiController < ApplicationController
5
+ # rescue_from ::StandardError, with: ->(e) { handle_error(e) }
6
+ # end
7
+
8
+ # This module contains helper methods for rendering errors in a JSON API format.
9
+ module Helpers
10
+ HatiErrs = HatiJsonapiError::Errors
11
+
12
+ def render_error(error, status: nil, short: false)
13
+ error_instance = error.is_a?(Class) ? error.new : error
14
+
15
+ unless error_instance.class <= HatiJsonapiError::BaseError
16
+ msg = "Supported only explicit type of HatiJsonapiError::BaseError, got: #{error_instance.class.name}"
17
+ raise HatiErrs::HelpersRenderError, msg
18
+ end
19
+
20
+ resolver = HatiJsonapiError::Resolver.new(error_instance)
21
+
22
+ unless defined?(render)
23
+ msg = 'Render not defined'
24
+ raise HatiErrs::HelpersRenderError, msg
25
+ end
26
+
27
+ render json: resolver.to_json(short: short), status: status || resolver.status
28
+ end
29
+
30
+ # with_original: oneOf: [false, true, :full_trace]
31
+ def handle_error(error, with_original: false)
32
+ error_class = error if error.class <= HatiJsonapiError::BaseError
33
+ error_class ||= HatiJsonapiError::Registry.lookup_error(error)
34
+
35
+ unless error_class
36
+ msg = 'Used handle_error but no mapping of default error set'
37
+ raise HatiErrs::HelpersHandleError, msg
38
+ end
39
+
40
+ # Fix: if error_class is already an instance, use it directly, otherwise create new instance
41
+ api_err = error_class.is_a?(Class) ? error_class.new : error_class
42
+ if with_original
43
+ api_err.meta = {
44
+ original_error: error.class,
45
+ trace: error.backtrace[0],
46
+ message: error.message
47
+ }
48
+ api_err.meta.merge!(backtrace: error.backtrace.join("\n")) if with_original == :full_trace
49
+ end
50
+
51
+ render_error(api_err)
52
+ end
53
+
54
+ # shorthand for API errors
55
+ # raise ApiErr[404] # => ApiError::NotFound
56
+ # raise ApiErr[:not_found] # => ApiError::NotFound
57
+ class ApiErr
58
+ class << self
59
+ def [](error)
60
+ call(error)
61
+ end
62
+
63
+ def call(error)
64
+ raise HatiErrs::NotLoadedError unless HatiJsonapiError::Kigen.loaded?
65
+
66
+ err = HatiJsonapiError::Kigen.fetch_err(error)
67
+
68
+ unless err
69
+ msg = "Error #{error} not defined on load_errors!. Check kigen.rb and api_error/error_const.rb"
70
+ raise HatiErrs::NotDefinedErrorClassError, msg
71
+ end
72
+
73
+ err
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HatiJsonapiError
4
+ # This class is used to load all errors from STATUS_MAP in api_error/error_const.rb
5
+ class Kigen
6
+ class << self
7
+ # loads all errors from STATUS_MAP in api_error/error_const.rb
8
+ # HatiJsonapiError::NotFound
9
+ # HatiJsonapiError::BadRequest
10
+ # HatiJsonapiError::Unauthorized
11
+ # HatiJsonapiError::Forbidden
12
+ # etc.
13
+ def load_errors!
14
+ return if loaded?
15
+
16
+ HatiJsonapiError::STATUS_MAP.each do |status, value|
17
+ next if HatiJsonapiError.const_defined?(value[:name])
18
+
19
+ err_klass = create_error_class(status, value)
20
+
21
+ status_klass_map[status] = err_klass
22
+ code_klass_map[value[:code]] = err_klass
23
+ end
24
+
25
+ @loaded = true
26
+ end
27
+
28
+ # HatiJsonapiError::Kigen.fetch_err(400) # => HatiJsonapiError::BadRequest
29
+ # HatiJsonapiError::Kigen.fetch_err(:bad_request)
30
+ def fetch_err(err)
31
+ return unless loaded?
32
+
33
+ status_klass_map[err] || code_klass_map[err]
34
+ end
35
+
36
+ # HatiJsonapiError::Kigen[400] # => HatiJsonapiError::BadRequest
37
+ def [](err)
38
+ fetch_err(err)
39
+ end
40
+
41
+ def loaded?
42
+ @loaded
43
+ end
44
+
45
+ def status_klass_map
46
+ @status_klass_map ||= {}
47
+ end
48
+
49
+ def code_klass_map
50
+ @code_klass_map ||= {}
51
+ end
52
+
53
+ private
54
+
55
+ def create_error_class(status, value)
56
+ HatiJsonapiError.const_set(value[:name], Class.new(HatiJsonapiError::BaseError) do
57
+ define_method :initialize do |**attrs|
58
+ defaults = {
59
+ code: value[:code],
60
+ message: value[:message],
61
+ title: value[:message],
62
+ status: status
63
+ }
64
+ super(**defaults.merge(attrs))
65
+ end
66
+ end)
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HatiJsonapiError
4
+ # This class is used to serialize errors to a JSON API format.
5
+ class PoroSerializer
6
+ SHORT_KEYS = %i[status title detail source].freeze
7
+
8
+ def initialize(error)
9
+ @errors = normalized_errors(error)
10
+ end
11
+
12
+ def serialize_to_json(short: false)
13
+ serializable_hash(short: short).to_json
14
+ end
15
+
16
+ def serializable_hash(short: false)
17
+ if short
18
+ { errors: errors.map { |error| error.to_h.slice(*SHORT_KEYS) } }
19
+ else
20
+ { errors: errors.map(&:to_h) }
21
+ end
22
+ end
23
+
24
+ private
25
+
26
+ attr_reader :errors
27
+
28
+ def normalized_errors(error)
29
+ error.is_a?(Array) ? error : [error]
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HatiJsonapiError
4
+ # This class is used to register errors and provide a fallback error.
5
+ class Registry
6
+ class << self
7
+ def fallback=(err)
8
+ @fallback = loaded_error?(err) ? err : fetch_error(err)
9
+ end
10
+
11
+ def fallback
12
+ @fallback ||= nil
13
+ end
14
+
15
+ # Base.loaded? # => true
16
+ # Registry.error_map = {
17
+ # ActiveRecord::RecordNotFound => :not_found,
18
+ # ActiveRecord::RecordInvalid => 422
19
+ # }
20
+ def error_map=(error_map)
21
+ error_map.each do |error, mapped_error|
22
+ next if loaded_error?(mapped_error)
23
+
24
+ error_map[error] = fetch_error(mapped_error)
25
+ end
26
+
27
+ @error_map = error_map
28
+ end
29
+
30
+ def error_map
31
+ @error_map ||= {}
32
+ end
33
+
34
+ def lookup_error(error)
35
+ error_map[error.class] || fallback
36
+ end
37
+
38
+ private
39
+
40
+ def loaded_error?(error)
41
+ error.is_a?(Class) && error <= HatiJsonapiError::BaseError
42
+ end
43
+
44
+ def fetch_error(error)
45
+ err = HatiJsonapiError::Kigen.fetch_err(error)
46
+ unless err
47
+ msg = "Error #{error} definition not found in lib/hati_jsonapi_error/api_error/error_const.rb"
48
+ raise HatiJsonapiError::Errors::NotDefinedErrorClassError, msg
49
+ end
50
+
51
+ err
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HatiJsonapiError
4
+ # This class is used to resolve errors and serialize them to a JSON API format.
5
+ class Resolver
6
+ attr_reader :errors, :serializer
7
+
8
+ def initialize(api_error, serializer: PoroSerializer)
9
+ @errors = error_arr(api_error)
10
+ @serializer = serializer.new(errors)
11
+ end
12
+
13
+ def status
14
+ errors.first.status
15
+ end
16
+
17
+ def to_json(*_args)
18
+ serializer.serialize_to_json
19
+ end
20
+
21
+ private
22
+
23
+ def error_arr(api_error)
24
+ api_error.is_a?(Array) ? api_error : [api_error]
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HatiJsonapiError
4
+ VERSION = '0.1.0'
5
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'hati_jsonapi_error/version'
4
+
5
+ # errors
6
+ require 'hati_jsonapi_error/errors/helpers_handle_error'
7
+ require 'hati_jsonapi_error/errors/helpers_render_error'
8
+ require 'hati_jsonapi_error/errors/not_defined_error_class_error'
9
+ require 'hati_jsonapi_error/errors/not_loaded_error'
10
+
11
+ # api_error/*
12
+ require 'hati_jsonapi_error/api_error/base_error'
13
+ require 'hati_jsonapi_error/api_error/error_const'
14
+ require 'hati_jsonapi_error/api_error/links'
15
+ require 'hati_jsonapi_error/api_error/source'
16
+
17
+ # logic
18
+ require 'hati_jsonapi_error/config'
19
+ require 'hati_jsonapi_error/kigen'
20
+ require 'hati_jsonapi_error/helpers'
21
+ require 'hati_jsonapi_error/poro_serializer'
22
+ require 'hati_jsonapi_error/registry'
23
+ require 'hati_jsonapi_error/resolver'
metadata ADDED
@@ -0,0 +1,67 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: hati-jsonapi-error
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Marie Giy
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies: []
12
+ description: hati-jsonapi-error is a Ruby gem for Standardized JSON Error.
13
+ email:
14
+ - giy.mariya@gmail.com
15
+ executables: []
16
+ extensions: []
17
+ extra_rdoc_files: []
18
+ files:
19
+ - LICENSE
20
+ - README.md
21
+ - hati-jsonapi-error.gemspec
22
+ - lib/hati_jsonapi_error.rb
23
+ - lib/hati_jsonapi_error/api_error/base_error.rb
24
+ - lib/hati_jsonapi_error/api_error/error_const.rb
25
+ - lib/hati_jsonapi_error/api_error/links.rb
26
+ - lib/hati_jsonapi_error/api_error/source.rb
27
+ - lib/hati_jsonapi_error/config.rb
28
+ - lib/hati_jsonapi_error/errors/helpers_handle_error.rb
29
+ - lib/hati_jsonapi_error/errors/helpers_render_error.rb
30
+ - lib/hati_jsonapi_error/errors/not_defined_error_class_error.rb
31
+ - lib/hati_jsonapi_error/errors/not_loaded_error.rb
32
+ - lib/hati_jsonapi_error/helpers.rb
33
+ - lib/hati_jsonapi_error/kigen.rb
34
+ - lib/hati_jsonapi_error/poro_serializer.rb
35
+ - lib/hati_jsonapi_error/registry.rb
36
+ - lib/hati_jsonapi_error/resolver.rb
37
+ - lib/hati_jsonapi_error/version.rb
38
+ homepage: https://github.com/hackico-ai/hati-jsonapi-error
39
+ licenses:
40
+ - MIT
41
+ metadata:
42
+ repo_homepage: https://github.com/hackico-ai/hati-jsonapi-error
43
+ allowed_push_host: https://rubygems.org
44
+ homepage_uri: https://github.com/hackico-ai/hati-jsonapi-error
45
+ changelog_uri: https://github.com/hackico-ai/hati-jsonapi-error/blob/main/CHANGELOG.md
46
+ source_code_uri: https://github.com/hackico-ai/hati-jsonapi-error
47
+ bug_tracker_uri: https://github.com/hackico-ai/hati-jsonapi-error/issues
48
+ rubygems_mfa_required: 'true'
49
+ rdoc_options: []
50
+ require_paths:
51
+ - lib
52
+ required_ruby_version: !ruby/object:Gem::Requirement
53
+ requirements:
54
+ - - ">="
55
+ - !ruby/object:Gem::Version
56
+ version: 3.0.0
57
+ required_rubygems_version: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ requirements: []
63
+ rubygems_version: 3.6.9
64
+ specification_version: 4
65
+ summary: 'Standardized JSON: API-compliant error responses made easy for your Web
66
+ API.'
67
+ test_files: []