jpie 0.4.1 → 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.
@@ -39,7 +39,12 @@ module JPie
39
39
  resources = resource_class.scope(context)
40
40
  sort_fields = parse_sort_params
41
41
  resources = resource_class.sort(resources, sort_fields) if sort_fields.any?
42
- 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)
43
48
  end
44
49
  end
45
50
 
@@ -86,7 +91,12 @@ module JPie
86
91
  resources = resource_class.scope(context)
87
92
  sort_fields = parse_sort_params
88
93
  resources = resource_class.sort(resources, sort_fields) if sort_fields.any?
89
- 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)
90
100
  end
91
101
 
92
102
  def show
@@ -115,6 +125,17 @@ module JPie
115
125
  resource.destroy!
116
126
  head :no_content
117
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
118
139
  end
119
140
  end
120
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,10 +1,16 @@
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
 
11
+ include HandlerSetup
12
+ include Handlers
13
+
8
14
  included do
9
15
  # Use class_attribute to allow easy overriding
10
16
  class_attribute :jpie_error_handlers_enabled, default: true
@@ -12,181 +18,6 @@ module JPie
12
18
  # Set up default handlers unless explicitly disabled
13
19
  setup_jpie_error_handlers if jpie_error_handlers_enabled
14
20
  end
15
-
16
- class_methods do
17
- # Allow applications to easily disable all JPie error handlers
18
- def disable_jpie_error_handlers
19
- self.jpie_error_handlers_enabled = false
20
- # Remove any already-added handlers
21
- remove_jpie_handlers
22
- end
23
-
24
- # Allow applications to enable specific handlers
25
- def enable_jpie_error_handler(error_class, method_name = nil)
26
- method_name ||= :"handle_#{error_class.name.demodulize.underscore}"
27
- rescue_from error_class, with: method_name
28
- end
29
-
30
- # Check for application-defined error handlers
31
- def rescue_handler?(exception_class)
32
- # Use Rails' rescue_handlers method to check for existing handlers
33
- return false unless respond_to?(:rescue_handlers, true)
34
-
35
- begin
36
- rescue_handlers.any? { |handler| handler.first == exception_class.name }
37
- rescue NoMethodError
38
- false
39
- end
40
- end
41
-
42
- private
43
-
44
- def setup_jpie_error_handlers
45
- setup_jpie_specific_handlers
46
- end
47
-
48
- def setup_jpie_specific_handlers
49
- # Only add handlers if they don't already exist
50
- rescue_from JPie::Errors::Error, with: :handle_jpie_error unless rescue_handler?(JPie::Errors::Error)
51
- unless rescue_handler?(ActiveRecord::RecordNotFound)
52
- rescue_from ActiveRecord::RecordNotFound,
53
- with: :handle_record_not_found
54
- end
55
- return if rescue_handler?(ActiveRecord::RecordInvalid)
56
-
57
- rescue_from ActiveRecord::RecordInvalid,
58
- with: :handle_record_invalid
59
-
60
- # JSON:API compliance error handlers
61
- unless rescue_handler?(JPie::Errors::InvalidJsonApiRequestError)
62
- rescue_from JPie::Errors::InvalidJsonApiRequestError,
63
- with: :handle_invalid_json_api_request
64
- end
65
-
66
- unless rescue_handler?(JPie::Errors::UnsupportedIncludeError)
67
- rescue_from JPie::Errors::UnsupportedIncludeError,
68
- with: :handle_unsupported_include
69
- end
70
-
71
- unless rescue_handler?(JPie::Errors::UnsupportedSortFieldError)
72
- rescue_from JPie::Errors::UnsupportedSortFieldError,
73
- with: :handle_unsupported_sort_field
74
- end
75
-
76
- unless rescue_handler?(JPie::Errors::InvalidSortParameterError)
77
- rescue_from JPie::Errors::InvalidSortParameterError,
78
- with: :handle_invalid_sort_parameter
79
- end
80
-
81
- return if rescue_handler?(JPie::Errors::InvalidIncludeParameterError)
82
-
83
- rescue_from JPie::Errors::InvalidIncludeParameterError,
84
- with: :handle_invalid_include_parameter
85
- end
86
-
87
- def remove_jpie_handlers
88
- # This is a placeholder - Rails doesn't provide an easy way to remove specific handlers
89
- # In practice, applications should use the disable_jpie_error_handlers before including
90
- end
91
- end
92
-
93
- private
94
-
95
- # Handle JPie-specific errors
96
- def handle_jpie_error(error)
97
- render_json_api_error(
98
- status: error.status,
99
- title: error.title,
100
- detail: error.detail
101
- )
102
- end
103
-
104
- # Handle ActiveRecord::RecordNotFound
105
- def handle_record_not_found(error)
106
- render_json_api_error(
107
- status: 404,
108
- title: 'Not Found',
109
- detail: error.message
110
- )
111
- end
112
-
113
- # Handle ActiveRecord::RecordInvalid
114
- def handle_record_invalid(error)
115
- errors = error.record.errors.full_messages.map do |message|
116
- {
117
- status: '422',
118
- title: 'Validation Error',
119
- detail: message
120
- }
121
- end
122
-
123
- render json: { errors: errors }, status: :unprocessable_content
124
- end
125
-
126
- # Render a single JSON:API error
127
- def render_json_api_error(status:, title:, detail:)
128
- render json: {
129
- errors: [{
130
- status: status.to_s,
131
- title: title,
132
- detail: detail
133
- }]
134
- }, status: status
135
- end
136
-
137
- # Handle JSON:API compliance errors
138
- def handle_invalid_json_api_request(error)
139
- render_json_api_error(
140
- status: error.status,
141
- title: error.title || 'Invalid JSON:API Request',
142
- detail: error.detail
143
- )
144
- end
145
-
146
- def handle_unsupported_include(error)
147
- render_json_api_error(
148
- status: error.status,
149
- title: error.title || 'Unsupported Include',
150
- detail: error.detail
151
- )
152
- end
153
-
154
- def handle_unsupported_sort_field(error)
155
- render_json_api_error(
156
- status: error.status,
157
- title: error.title || 'Unsupported Sort Field',
158
- detail: error.detail
159
- )
160
- end
161
-
162
- def handle_invalid_sort_parameter(error)
163
- render_json_api_error(
164
- status: error.status,
165
- title: error.title || 'Invalid Sort Parameter',
166
- detail: error.detail
167
- )
168
- end
169
-
170
- def handle_invalid_include_parameter(error)
171
- render_json_api_error(
172
- status: error.status,
173
- title: error.title || 'Invalid Include Parameter',
174
- detail: error.detail
175
- )
176
- end
177
-
178
- # Backward compatibility aliases
179
- alias jpie_handle_error handle_jpie_error
180
- alias jpie_handle_not_found handle_record_not_found
181
- alias jpie_handle_invalid handle_record_invalid
182
-
183
- # Legacy method name aliases
184
- alias render_jpie_error handle_jpie_error
185
- alias render_jpie_not_found_error handle_record_not_found
186
- alias render_jpie_validation_error handle_record_invalid
187
- alias render_jsonapi_error handle_jpie_error
188
- alias render_not_found_error handle_record_not_found
189
- alias render_validation_error handle_record_invalid
190
21
  end
191
22
  end
192
23
  end
@@ -28,26 +28,34 @@ module JPie
28
28
 
29
29
  # Validate basic JSON:API request structure
30
30
  def validate_json_api_structure
31
+ request_body = read_request_body
32
+ return if request_body.blank?
33
+
34
+ parsed_body = parse_json_body(request_body)
35
+ validate_top_level_structure(parsed_body)
36
+ validate_data_structure(parsed_body['data'])
37
+ end
38
+
39
+ def read_request_body
31
40
  request_body = request.body.read
32
41
  request.body.rewind # Reset for later reading
42
+ request_body
43
+ end
33
44
 
34
- return if request_body.blank?
35
-
36
- begin
37
- parsed_body = JSON.parse(request_body)
38
- rescue JSON::ParserError => e
39
- raise JPie::Errors::InvalidJsonApiRequestError.new(
40
- detail: "Invalid JSON: #{e.message}"
41
- )
42
- end
45
+ def parse_json_body(request_body)
46
+ JSON.parse(request_body)
47
+ rescue JSON::ParserError => e
48
+ raise JPie::Errors::InvalidJsonApiRequestError.new(
49
+ detail: "Invalid JSON: #{e.message}"
50
+ )
51
+ end
43
52
 
44
- unless parsed_body.is_a?(Hash) && parsed_body.key?('data')
45
- raise JPie::Errors::InvalidJsonApiRequestError.new(
46
- detail: 'JSON:API request must have a top-level "data" member'
47
- )
48
- end
53
+ def validate_top_level_structure(parsed_body)
54
+ return if parsed_body.is_a?(Hash) && parsed_body.key?('data')
49
55
 
50
- validate_data_structure(parsed_body['data'])
56
+ raise JPie::Errors::InvalidJsonApiRequestError.new(
57
+ detail: 'JSON:API request must have a top-level "data" member'
58
+ )
51
59
  end
52
60
 
53
61
  # Validate the structure of the data member
@@ -102,30 +110,43 @@ module JPie
102
110
 
103
111
  # Validate a single include path
104
112
  def validate_include_path(include_path, supported_includes)
105
- # Handle nested includes (e.g., "posts.comments")
106
113
  path_parts = include_path.split('.')
107
114
  current_level = supported_includes
108
115
 
109
116
  path_parts.each_with_index do |part, index|
110
- unless current_level.include?(part.to_sym) || current_level.include?(part)
111
- current_path = path_parts[0..index].join('.')
112
- available_at_level = current_level.is_a?(Hash) ? current_level.keys : current_level
113
-
114
- raise JPie::Errors::UnsupportedIncludeError.new(
115
- include_path: current_path,
116
- supported_includes: available_at_level.map(&:to_s)
117
- )
118
- end
119
-
120
- # Move to next level for nested validation
121
- if current_level.is_a?(Hash) && current_level[part.to_sym].is_a?(Hash)
122
- current_level = current_level[part.to_sym]
123
- elsif current_level.is_a?(Hash) && current_level[part].is_a?(Hash)
124
- current_level = current_level[part]
125
- end
117
+ validate_include_part(part, current_level, path_parts, index)
118
+ current_level = move_to_next_include_level(part, current_level)
126
119
  end
127
120
  end
128
121
 
122
+ def validate_include_part(part, current_level, path_parts, index)
123
+ return if include_part_supported?(part, current_level)
124
+
125
+ current_path = path_parts[0..index].join('.')
126
+ available_at_level = extract_available_includes(current_level)
127
+
128
+ raise JPie::Errors::UnsupportedIncludeError.new(
129
+ include_path: current_path,
130
+ supported_includes: available_at_level.map(&:to_s)
131
+ )
132
+ end
133
+
134
+ def include_part_supported?(part, current_level)
135
+ current_level.include?(part.to_sym) || current_level.include?(part)
136
+ end
137
+
138
+ def extract_available_includes(current_level)
139
+ current_level.is_a?(Hash) ? current_level.keys : current_level
140
+ end
141
+
142
+ def move_to_next_include_level(part, current_level)
143
+ return current_level unless current_level.is_a?(Hash)
144
+
145
+ current_level[part.to_sym] if current_level[part.to_sym].is_a?(Hash)
146
+ current_level[part] if current_level[part].is_a?(Hash)
147
+ current_level
148
+ end
149
+
129
150
  # Validate sort parameters against supported fields
130
151
  def validate_sort_params
131
152
  return if params[:sort].blank?
@@ -148,7 +169,8 @@ module JPie
148
169
  # Validate field name format
149
170
  unless field_name.match?(/\A[a-zA-Z][a-zA-Z0-9_]*\z/)
150
171
  raise JPie::Errors::InvalidSortParameterError.new(
151
- detail: "Invalid sort field format: '#{sort_field}'. Field names must start with a letter and contain only letters, numbers, and underscores"
172
+ detail: "Invalid sort field format: '#{sort_field}'. " \
173
+ 'Field names must start with a letter and contain only letters, numbers, and underscores'
152
174
  )
153
175
  end
154
176
 
@@ -11,6 +11,16 @@ module JPie
11
11
  params[:sort]&.split(',')&.map(&:strip) || []
12
12
  end
13
13
 
14
+ def parse_pagination_params
15
+ page_params = params[:page] || {}
16
+ per_page_param = params[:per_page]
17
+
18
+ {
19
+ page: extract_page_number(page_params, per_page_param),
20
+ per_page: extract_per_page_size(page_params, per_page_param)
21
+ }
22
+ end
23
+
14
24
  def deserialize_params
15
25
  deserializer.deserialize(request.body.read, context)
16
26
  rescue JSON::ParserError => e
@@ -30,6 +40,39 @@ module JPie
30
40
  action: action_name
31
41
  }
32
42
  end
43
+
44
+ def extract_page_number(page_params, per_page_param)
45
+ page_number = determine_page_number(page_params)
46
+ page_number = '1' if page_number.nil? && per_page_param.present?
47
+ return 1 if page_number.blank?
48
+
49
+ parsed_page = page_number.to_i
50
+ parsed_page.positive? ? parsed_page : 1
51
+ end
52
+
53
+ def extract_per_page_size(page_params, per_page_param)
54
+ per_page_size = determine_per_page_size(page_params, per_page_param)
55
+ return nil if per_page_size.blank?
56
+
57
+ parsed_size = per_page_size.to_i
58
+ parsed_size.positive? ? parsed_size : nil
59
+ end
60
+
61
+ def determine_page_number(page_params)
62
+ if page_params.is_a?(String) || page_params.is_a?(Integer)
63
+ page_params
64
+ else
65
+ page_params[:number] || page_params['number']
66
+ end
67
+ end
68
+
69
+ def determine_per_page_size(page_params, per_page_param)
70
+ if page_params.is_a?(String) || page_params.is_a?(Integer)
71
+ per_page_param
72
+ else
73
+ page_params[:size] || page_params['size'] || per_page_param
74
+ end
75
+ end
33
76
  end
34
77
  end
35
78
  end