jpie 0.4.0 → 0.4.2
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 +4 -4
- data/{.aiconfig → .cursorrules} +21 -5
- data/.overcommit.yml +35 -0
- data/CHANGELOG.md +33 -0
- data/README.md +97 -1063
- data/examples/basic_example.md +146 -0
- data/examples/including_related_resources.md +491 -0
- data/examples/pagination.md +303 -0
- data/examples/resource_attribute_configuration.md +147 -0
- data/examples/resource_meta_configuration.md +244 -0
- data/examples/single_table_inheritance.md +160 -0
- data/lib/jpie/controller/crud_actions.rb +33 -2
- data/lib/jpie/controller/error_handling/handler_setup.rb +124 -0
- data/lib/jpie/controller/error_handling/handlers.rb +109 -0
- data/lib/jpie/controller/error_handling.rb +10 -28
- data/lib/jpie/controller/json_api_validation.rb +193 -0
- data/lib/jpie/controller/parameter_parsing.rb +43 -0
- data/lib/jpie/controller/rendering.rb +95 -1
- data/lib/jpie/controller.rb +2 -0
- data/lib/jpie/errors.rb +41 -0
- data/lib/jpie/resource/attributable.rb +16 -2
- data/lib/jpie/resource.rb +40 -0
- data/lib/jpie/version.rb +1 -1
- metadata +13 -3
@@ -0,0 +1,160 @@
|
|
1
|
+
# Single Table Inheritance (STI) Example
|
2
|
+
|
3
|
+
This example demonstrates the minimal setup required to implement Single Table Inheritance with JPie resources and controllers.
|
4
|
+
|
5
|
+
## Setup
|
6
|
+
|
7
|
+
### 1. Base Model (`app/models/vehicle.rb`)
|
8
|
+
```ruby
|
9
|
+
class Vehicle < ApplicationRecord
|
10
|
+
validates :name, presence: true
|
11
|
+
validates :brand, presence: true
|
12
|
+
validates :year, presence: true
|
13
|
+
end
|
14
|
+
```
|
15
|
+
|
16
|
+
### 2. STI Models (`app/models/car.rb`, `app/models/truck.rb`)
|
17
|
+
```ruby
|
18
|
+
class Car < Vehicle
|
19
|
+
validates :engine_size, presence: true
|
20
|
+
end
|
21
|
+
|
22
|
+
class Truck < Vehicle
|
23
|
+
validates :cargo_capacity, presence: true
|
24
|
+
end
|
25
|
+
```
|
26
|
+
|
27
|
+
### 3. Base Resource (`app/resources/vehicle_resource.rb`)
|
28
|
+
```ruby
|
29
|
+
class VehicleResource < JPie::Resource
|
30
|
+
attributes :name, :brand, :year
|
31
|
+
end
|
32
|
+
```
|
33
|
+
|
34
|
+
### 4. STI Resources (`app/resources/car_resource.rb`, `app/resources/truck_resource.rb`)
|
35
|
+
```ruby
|
36
|
+
class CarResource < VehicleResource
|
37
|
+
attributes :engine_size
|
38
|
+
end
|
39
|
+
|
40
|
+
class TruckResource < VehicleResource
|
41
|
+
attributes :cargo_capacity
|
42
|
+
end
|
43
|
+
```
|
44
|
+
|
45
|
+
### 5. Controller (`app/controllers/vehicles_controller.rb`)
|
46
|
+
```ruby
|
47
|
+
class VehiclesController < ApplicationController
|
48
|
+
include JPie::Controller
|
49
|
+
end
|
50
|
+
```
|
51
|
+
|
52
|
+
### 6. Routes (`config/routes.rb`)
|
53
|
+
```ruby
|
54
|
+
Rails.application.routes.draw do
|
55
|
+
resources :vehicles
|
56
|
+
end
|
57
|
+
```
|
58
|
+
|
59
|
+
## HTTP Examples
|
60
|
+
|
61
|
+
### Create Car
|
62
|
+
```http
|
63
|
+
POST /vehicles
|
64
|
+
Content-Type: application/vnd.api+json
|
65
|
+
|
66
|
+
{
|
67
|
+
"data": {
|
68
|
+
"type": "cars",
|
69
|
+
"attributes": {
|
70
|
+
"name": "Civic",
|
71
|
+
"brand": "Honda",
|
72
|
+
"year": 2024,
|
73
|
+
"engine_size": 1500
|
74
|
+
}
|
75
|
+
}
|
76
|
+
}
|
77
|
+
|
78
|
+
HTTP/1.1 201 Created
|
79
|
+
Content-Type: application/vnd.api+json
|
80
|
+
|
81
|
+
{
|
82
|
+
"data": {
|
83
|
+
"id": "1",
|
84
|
+
"type": "cars",
|
85
|
+
"attributes": {
|
86
|
+
"name": "Civic",
|
87
|
+
"brand": "Honda",
|
88
|
+
"year": 2024,
|
89
|
+
"engine_size": 1500
|
90
|
+
}
|
91
|
+
}
|
92
|
+
}
|
93
|
+
```
|
94
|
+
|
95
|
+
### Update Car
|
96
|
+
```http
|
97
|
+
PATCH /vehicles/1
|
98
|
+
Content-Type: application/vnd.api+json
|
99
|
+
|
100
|
+
{
|
101
|
+
"data": {
|
102
|
+
"id": "1",
|
103
|
+
"type": "cars",
|
104
|
+
"attributes": {
|
105
|
+
"name": "Civic Hybrid",
|
106
|
+
"engine_size": 1800
|
107
|
+
}
|
108
|
+
}
|
109
|
+
}
|
110
|
+
|
111
|
+
HTTP/1.1 200 OK
|
112
|
+
Content-Type: application/vnd.api+json
|
113
|
+
|
114
|
+
{
|
115
|
+
"data": {
|
116
|
+
"id": "1",
|
117
|
+
"type": "cars",
|
118
|
+
"attributes": {
|
119
|
+
"name": "Civic Hybrid",
|
120
|
+
"brand": "Honda",
|
121
|
+
"year": 2024,
|
122
|
+
"engine_size": 1800
|
123
|
+
}
|
124
|
+
}
|
125
|
+
}
|
126
|
+
```
|
127
|
+
|
128
|
+
### Get Mixed Vehicles
|
129
|
+
```http
|
130
|
+
GET /vehicles
|
131
|
+
Accept: application/vnd.api+json
|
132
|
+
|
133
|
+
HTTP/1.1 200 OK
|
134
|
+
Content-Type: application/vnd.api+json
|
135
|
+
|
136
|
+
{
|
137
|
+
"data": [
|
138
|
+
{
|
139
|
+
"id": "1",
|
140
|
+
"type": "cars",
|
141
|
+
"attributes": {
|
142
|
+
"name": "Civic",
|
143
|
+
"brand": "Honda",
|
144
|
+
"year": 2024,
|
145
|
+
"engine_size": 1500
|
146
|
+
}
|
147
|
+
},
|
148
|
+
{
|
149
|
+
"id": "2",
|
150
|
+
"type": "trucks",
|
151
|
+
"attributes": {
|
152
|
+
"name": "F-150",
|
153
|
+
"brand": "Ford",
|
154
|
+
"year": 2024,
|
155
|
+
"cargo_capacity": 1000
|
156
|
+
}
|
157
|
+
}
|
158
|
+
]
|
159
|
+
}
|
160
|
+
```
|
@@ -34,15 +34,23 @@ module JPie
|
|
34
34
|
|
35
35
|
def define_index_method(resource_class)
|
36
36
|
define_method :index do
|
37
|
+
validate_include_params
|
38
|
+
validate_sort_params
|
37
39
|
resources = resource_class.scope(context)
|
38
40
|
sort_fields = parse_sort_params
|
39
41
|
resources = resource_class.sort(resources, sort_fields) if sort_fields.any?
|
40
|
-
|
42
|
+
|
43
|
+
pagination_params = parse_pagination_params
|
44
|
+
original_resources = resources
|
45
|
+
resources = apply_pagination(resources, pagination_params)
|
46
|
+
|
47
|
+
render_jsonapi(resources, pagination: pagination_params, original_scope: original_resources)
|
41
48
|
end
|
42
49
|
end
|
43
50
|
|
44
51
|
def define_show_method(resource_class)
|
45
52
|
define_method :show do
|
53
|
+
validate_include_params
|
46
54
|
resource = resource_class.scope(context).find(params[:id])
|
47
55
|
render_jsonapi(resource)
|
48
56
|
end
|
@@ -50,6 +58,7 @@ module JPie
|
|
50
58
|
|
51
59
|
def define_create_method(resource_class)
|
52
60
|
define_method :create do
|
61
|
+
validate_json_api_request
|
53
62
|
attributes = deserialize_params
|
54
63
|
resource = resource_class.model.create!(attributes)
|
55
64
|
render_jsonapi(resource, status: :created)
|
@@ -58,6 +67,7 @@ module JPie
|
|
58
67
|
|
59
68
|
def define_update_method(resource_class)
|
60
69
|
define_method :update do
|
70
|
+
validate_json_api_request
|
61
71
|
resource = resource_class.scope(context).find(params[:id])
|
62
72
|
attributes = deserialize_params
|
63
73
|
resource.update!(attributes)
|
@@ -76,24 +86,34 @@ module JPie
|
|
76
86
|
|
77
87
|
# These methods can still be called manually or used to override defaults
|
78
88
|
def index
|
89
|
+
validate_include_params
|
90
|
+
validate_sort_params
|
79
91
|
resources = resource_class.scope(context)
|
80
92
|
sort_fields = parse_sort_params
|
81
93
|
resources = resource_class.sort(resources, sort_fields) if sort_fields.any?
|
82
|
-
|
94
|
+
|
95
|
+
pagination_params = parse_pagination_params
|
96
|
+
original_resources = resources
|
97
|
+
resources = apply_pagination(resources, pagination_params)
|
98
|
+
|
99
|
+
render_jsonapi(resources, pagination: pagination_params, original_scope: original_resources)
|
83
100
|
end
|
84
101
|
|
85
102
|
def show
|
103
|
+
validate_include_params
|
86
104
|
resource = resource_class.scope(context).find(params[:id])
|
87
105
|
render_jsonapi(resource)
|
88
106
|
end
|
89
107
|
|
90
108
|
def create
|
109
|
+
validate_json_api_request
|
91
110
|
attributes = deserialize_params
|
92
111
|
resource = model_class.create!(attributes)
|
93
112
|
render_jsonapi(resource, status: :created)
|
94
113
|
end
|
95
114
|
|
96
115
|
def update
|
116
|
+
validate_json_api_request
|
97
117
|
resource = resource_class.scope(context).find(params[:id])
|
98
118
|
attributes = deserialize_params
|
99
119
|
resource.update!(attributes)
|
@@ -105,6 +125,17 @@ module JPie
|
|
105
125
|
resource.destroy!
|
106
126
|
head :no_content
|
107
127
|
end
|
128
|
+
|
129
|
+
private
|
130
|
+
|
131
|
+
def apply_pagination(resources, pagination_params)
|
132
|
+
return resources unless pagination_params[:per_page]
|
133
|
+
|
134
|
+
page = pagination_params[:page] || 1
|
135
|
+
per_page = pagination_params[:per_page]
|
136
|
+
|
137
|
+
resources.limit(per_page).offset((page - 1) * per_page)
|
138
|
+
end
|
108
139
|
end
|
109
140
|
end
|
110
141
|
end
|
@@ -0,0 +1,124 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module JPie
|
4
|
+
module Controller
|
5
|
+
module ErrorHandling
|
6
|
+
module HandlerSetup
|
7
|
+
extend ActiveSupport::Concern
|
8
|
+
|
9
|
+
class_methods do
|
10
|
+
# Allow applications to easily disable all JPie error handlers
|
11
|
+
def disable_jpie_error_handlers
|
12
|
+
self.jpie_error_handlers_enabled = false
|
13
|
+
# Remove any already-added handlers
|
14
|
+
remove_jpie_handlers
|
15
|
+
end
|
16
|
+
|
17
|
+
# Allow applications to enable specific handlers
|
18
|
+
def enable_jpie_error_handler(error_class, method_name = nil)
|
19
|
+
method_name ||= :"handle_#{error_class.name.demodulize.underscore}"
|
20
|
+
rescue_from error_class, with: method_name
|
21
|
+
end
|
22
|
+
|
23
|
+
# Check for application-defined error handlers
|
24
|
+
def rescue_handler?(exception_class)
|
25
|
+
# Use Rails' rescue_handlers method to check for existing handlers
|
26
|
+
return false unless respond_to?(:rescue_handlers, true)
|
27
|
+
|
28
|
+
begin
|
29
|
+
rescue_handlers.any? { |handler| handler.first == exception_class.name }
|
30
|
+
rescue NoMethodError
|
31
|
+
false
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def setup_jpie_error_handlers
|
38
|
+
setup_jpie_specific_handlers
|
39
|
+
end
|
40
|
+
|
41
|
+
def setup_jpie_specific_handlers
|
42
|
+
setup_core_error_handlers
|
43
|
+
setup_activerecord_handlers
|
44
|
+
setup_json_api_compliance_handlers
|
45
|
+
end
|
46
|
+
|
47
|
+
def setup_core_error_handlers
|
48
|
+
return if rescue_handler?(JPie::Errors::Error)
|
49
|
+
|
50
|
+
rescue_from JPie::Errors::Error, with: :handle_jpie_error
|
51
|
+
end
|
52
|
+
|
53
|
+
def setup_activerecord_handlers
|
54
|
+
setup_not_found_handler
|
55
|
+
setup_invalid_record_handler
|
56
|
+
end
|
57
|
+
|
58
|
+
def setup_not_found_handler
|
59
|
+
return if rescue_handler?(ActiveRecord::RecordNotFound)
|
60
|
+
|
61
|
+
rescue_from ActiveRecord::RecordNotFound, with: :handle_record_not_found
|
62
|
+
end
|
63
|
+
|
64
|
+
def setup_invalid_record_handler
|
65
|
+
return if rescue_handler?(ActiveRecord::RecordInvalid)
|
66
|
+
|
67
|
+
rescue_from ActiveRecord::RecordInvalid, with: :handle_record_invalid
|
68
|
+
end
|
69
|
+
|
70
|
+
def setup_json_api_compliance_handlers
|
71
|
+
setup_json_api_request_handler
|
72
|
+
setup_include_handlers
|
73
|
+
setup_sort_handlers
|
74
|
+
end
|
75
|
+
|
76
|
+
def setup_json_api_request_handler
|
77
|
+
return if rescue_handler?(JPie::Errors::InvalidJsonApiRequestError)
|
78
|
+
|
79
|
+
rescue_from JPie::Errors::InvalidJsonApiRequestError, with: :handle_invalid_json_api_request
|
80
|
+
end
|
81
|
+
|
82
|
+
def setup_include_handlers
|
83
|
+
setup_unsupported_include_handler
|
84
|
+
setup_invalid_include_handler
|
85
|
+
end
|
86
|
+
|
87
|
+
def setup_unsupported_include_handler
|
88
|
+
return if rescue_handler?(JPie::Errors::UnsupportedIncludeError)
|
89
|
+
|
90
|
+
rescue_from JPie::Errors::UnsupportedIncludeError, with: :handle_unsupported_include
|
91
|
+
end
|
92
|
+
|
93
|
+
def setup_invalid_include_handler
|
94
|
+
return if rescue_handler?(JPie::Errors::InvalidIncludeParameterError)
|
95
|
+
|
96
|
+
rescue_from JPie::Errors::InvalidIncludeParameterError, with: :handle_invalid_include_parameter
|
97
|
+
end
|
98
|
+
|
99
|
+
def setup_sort_handlers
|
100
|
+
setup_unsupported_sort_handler
|
101
|
+
setup_invalid_sort_handler
|
102
|
+
end
|
103
|
+
|
104
|
+
def setup_unsupported_sort_handler
|
105
|
+
return if rescue_handler?(JPie::Errors::UnsupportedSortFieldError)
|
106
|
+
|
107
|
+
rescue_from JPie::Errors::UnsupportedSortFieldError, with: :handle_unsupported_sort_field
|
108
|
+
end
|
109
|
+
|
110
|
+
def setup_invalid_sort_handler
|
111
|
+
return if rescue_handler?(JPie::Errors::InvalidSortParameterError)
|
112
|
+
|
113
|
+
rescue_from JPie::Errors::InvalidSortParameterError, with: :handle_invalid_sort_parameter
|
114
|
+
end
|
115
|
+
|
116
|
+
def remove_jpie_handlers
|
117
|
+
# This is a placeholder - Rails doesn't provide an easy way to remove specific handlers
|
118
|
+
# In practice, applications should use the disable_jpie_error_handlers before including
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
@@ -0,0 +1,109 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module JPie
|
4
|
+
module Controller
|
5
|
+
module ErrorHandling
|
6
|
+
module Handlers
|
7
|
+
extend ActiveSupport::Concern
|
8
|
+
|
9
|
+
private
|
10
|
+
|
11
|
+
# Handle JPie-specific errors
|
12
|
+
def handle_jpie_error(error)
|
13
|
+
render_json_api_error(
|
14
|
+
status: error.status,
|
15
|
+
title: error.title,
|
16
|
+
detail: error.detail
|
17
|
+
)
|
18
|
+
end
|
19
|
+
|
20
|
+
# Handle ActiveRecord::RecordNotFound
|
21
|
+
def handle_record_not_found(error)
|
22
|
+
render_json_api_error(
|
23
|
+
status: 404,
|
24
|
+
title: 'Not Found',
|
25
|
+
detail: error.message
|
26
|
+
)
|
27
|
+
end
|
28
|
+
|
29
|
+
# Handle ActiveRecord::RecordInvalid
|
30
|
+
def handle_record_invalid(error)
|
31
|
+
errors = error.record.errors.full_messages.map do |message|
|
32
|
+
{
|
33
|
+
status: '422',
|
34
|
+
title: 'Validation Error',
|
35
|
+
detail: message
|
36
|
+
}
|
37
|
+
end
|
38
|
+
|
39
|
+
render json: { errors: errors }, status: :unprocessable_content
|
40
|
+
end
|
41
|
+
|
42
|
+
# Render a single JSON:API error
|
43
|
+
def render_json_api_error(status:, title:, detail:)
|
44
|
+
render json: {
|
45
|
+
errors: [{
|
46
|
+
status: status.to_s,
|
47
|
+
title: title,
|
48
|
+
detail: detail
|
49
|
+
}]
|
50
|
+
}, status: status
|
51
|
+
end
|
52
|
+
|
53
|
+
# Handle JSON:API compliance errors
|
54
|
+
def handle_invalid_json_api_request(error)
|
55
|
+
render_json_api_error(
|
56
|
+
status: error.status,
|
57
|
+
title: error.title || 'Invalid JSON:API Request',
|
58
|
+
detail: error.detail
|
59
|
+
)
|
60
|
+
end
|
61
|
+
|
62
|
+
def handle_unsupported_include(error)
|
63
|
+
render_json_api_error(
|
64
|
+
status: error.status,
|
65
|
+
title: error.title || 'Unsupported Include',
|
66
|
+
detail: error.detail
|
67
|
+
)
|
68
|
+
end
|
69
|
+
|
70
|
+
def handle_unsupported_sort_field(error)
|
71
|
+
render_json_api_error(
|
72
|
+
status: error.status,
|
73
|
+
title: error.title || 'Unsupported Sort Field',
|
74
|
+
detail: error.detail
|
75
|
+
)
|
76
|
+
end
|
77
|
+
|
78
|
+
def handle_invalid_sort_parameter(error)
|
79
|
+
render_json_api_error(
|
80
|
+
status: error.status,
|
81
|
+
title: error.title || 'Invalid Sort Parameter',
|
82
|
+
detail: error.detail
|
83
|
+
)
|
84
|
+
end
|
85
|
+
|
86
|
+
def handle_invalid_include_parameter(error)
|
87
|
+
render_json_api_error(
|
88
|
+
status: error.status,
|
89
|
+
title: error.title || 'Invalid Include Parameter',
|
90
|
+
detail: error.detail
|
91
|
+
)
|
92
|
+
end
|
93
|
+
|
94
|
+
# Backward compatibility aliases
|
95
|
+
alias jpie_handle_error handle_jpie_error
|
96
|
+
alias jpie_handle_not_found handle_record_not_found
|
97
|
+
alias jpie_handle_invalid handle_record_invalid
|
98
|
+
|
99
|
+
# Legacy method name aliases
|
100
|
+
alias render_jpie_error handle_jpie_error
|
101
|
+
alias render_jpie_not_found_error handle_record_not_found
|
102
|
+
alias render_jpie_validation_error handle_record_invalid
|
103
|
+
alias render_jsonapi_error handle_jpie_error
|
104
|
+
alias render_not_found_error handle_record_not_found
|
105
|
+
alias render_validation_error handle_record_invalid
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
@@ -1,40 +1,22 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require_relative 'error_handling/handler_setup'
|
4
|
+
require_relative 'error_handling/handlers'
|
5
|
+
|
3
6
|
module JPie
|
4
7
|
module Controller
|
5
8
|
module ErrorHandling
|
6
9
|
extend ActiveSupport::Concern
|
7
10
|
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
if defined?(ActiveRecord)
|
12
|
-
rescue_from ActiveRecord::RecordNotFound, with: :render_not_found_error
|
13
|
-
rescue_from ActiveRecord::RecordInvalid, with: :render_validation_error
|
14
|
-
end
|
15
|
-
end
|
16
|
-
|
17
|
-
private
|
11
|
+
include HandlerSetup
|
12
|
+
include Handlers
|
18
13
|
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
content_type: 'application/vnd.api+json'
|
23
|
-
end
|
24
|
-
|
25
|
-
def render_not_found_error(error)
|
26
|
-
json_error = JPie::Errors::NotFoundError.new(detail: error.message)
|
27
|
-
render_jsonapi_error(json_error)
|
28
|
-
end
|
29
|
-
|
30
|
-
def render_validation_error(error)
|
31
|
-
errors = error.record.errors.full_messages.map do
|
32
|
-
JPie::Errors::ValidationError.new(detail: it).to_hash
|
33
|
-
end
|
14
|
+
included do
|
15
|
+
# Use class_attribute to allow easy overriding
|
16
|
+
class_attribute :jpie_error_handlers_enabled, default: true
|
34
17
|
|
35
|
-
|
36
|
-
|
37
|
-
content_type: 'application/vnd.api+json'
|
18
|
+
# Set up default handlers unless explicitly disabled
|
19
|
+
setup_jpie_error_handlers if jpie_error_handlers_enabled
|
38
20
|
end
|
39
21
|
end
|
40
22
|
end
|