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.
@@ -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
- render_jsonapi(resources)
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
- render_jsonapi(resources)
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
- included do
9
- rescue_from JPie::Errors::Error, with: :render_jsonapi_error
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
- def render_jsonapi_error(error)
20
- render json: { errors: [error.to_hash] },
21
- status: error.status,
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
- render json: { errors: },
36
- status: :unprocessable_entity,
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