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,193 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module JPie
|
4
|
+
module Controller
|
5
|
+
module JsonApiValidation
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
|
8
|
+
private
|
9
|
+
|
10
|
+
# Validate that the request is JSON:API compliant
|
11
|
+
def validate_json_api_request
|
12
|
+
validate_content_type if request.post? || request.patch? || request.put?
|
13
|
+
validate_json_api_structure if request.post? || request.patch? || request.put?
|
14
|
+
end
|
15
|
+
|
16
|
+
# Validate Content-Type header for write operations
|
17
|
+
def validate_content_type
|
18
|
+
# Only validate content type for write operations
|
19
|
+
return unless request.post? || request.patch? || request.put?
|
20
|
+
|
21
|
+
content_type = request.content_type
|
22
|
+
return if content_type&.include?('application/vnd.api+json')
|
23
|
+
|
24
|
+
raise JPie::Errors::InvalidJsonApiRequestError.new(
|
25
|
+
detail: 'Content-Type must be application/vnd.api+json for JSON:API requests'
|
26
|
+
)
|
27
|
+
end
|
28
|
+
|
29
|
+
# Validate basic JSON:API request structure
|
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
|
40
|
+
request_body = request.body.read
|
41
|
+
request.body.rewind # Reset for later reading
|
42
|
+
request_body
|
43
|
+
end
|
44
|
+
|
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
|
52
|
+
|
53
|
+
def validate_top_level_structure(parsed_body)
|
54
|
+
return if parsed_body.is_a?(Hash) && parsed_body.key?('data')
|
55
|
+
|
56
|
+
raise JPie::Errors::InvalidJsonApiRequestError.new(
|
57
|
+
detail: 'JSON:API request must have a top-level "data" member'
|
58
|
+
)
|
59
|
+
end
|
60
|
+
|
61
|
+
# Validate the structure of the data member
|
62
|
+
def validate_data_structure(data)
|
63
|
+
if data.is_a?(Array)
|
64
|
+
data.each { |item| validate_resource_object(item) }
|
65
|
+
elsif data.is_a?(Hash)
|
66
|
+
validate_resource_object(data)
|
67
|
+
elsif !data.nil?
|
68
|
+
raise JPie::Errors::InvalidJsonApiRequestError.new(
|
69
|
+
detail: 'Data member must be an object, array, or null'
|
70
|
+
)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
# Validate individual resource object structure
|
75
|
+
def validate_resource_object(resource)
|
76
|
+
unless resource.is_a?(Hash)
|
77
|
+
raise JPie::Errors::InvalidJsonApiRequestError.new(
|
78
|
+
detail: 'Resource objects must be JSON objects'
|
79
|
+
)
|
80
|
+
end
|
81
|
+
|
82
|
+
unless resource.key?('type')
|
83
|
+
raise JPie::Errors::InvalidJsonApiRequestError.new(
|
84
|
+
detail: 'Resource objects must have a "type" member'
|
85
|
+
)
|
86
|
+
end
|
87
|
+
|
88
|
+
# ID is required for updates but not for creates
|
89
|
+
return unless request.patch? || request.put?
|
90
|
+
return if resource.key?('id')
|
91
|
+
|
92
|
+
raise JPie::Errors::InvalidJsonApiRequestError.new(
|
93
|
+
detail: 'Resource objects must have an "id" member for updates'
|
94
|
+
)
|
95
|
+
end
|
96
|
+
|
97
|
+
# Validate include parameters against supported includes
|
98
|
+
def validate_include_params
|
99
|
+
return if params[:include].blank?
|
100
|
+
|
101
|
+
include_paths = params[:include].to_s.split(',').map(&:strip)
|
102
|
+
supported_includes = resource_class.supported_includes
|
103
|
+
|
104
|
+
include_paths.each do |include_path|
|
105
|
+
next if include_path.blank?
|
106
|
+
|
107
|
+
validate_include_path(include_path, supported_includes)
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
# Validate a single include path
|
112
|
+
def validate_include_path(include_path, supported_includes)
|
113
|
+
path_parts = include_path.split('.')
|
114
|
+
current_level = supported_includes
|
115
|
+
|
116
|
+
path_parts.each_with_index do |part, index|
|
117
|
+
validate_include_part(part, current_level, path_parts, index)
|
118
|
+
current_level = move_to_next_include_level(part, current_level)
|
119
|
+
end
|
120
|
+
end
|
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
|
+
|
150
|
+
# Validate sort parameters against supported fields
|
151
|
+
def validate_sort_params
|
152
|
+
return if params[:sort].blank?
|
153
|
+
|
154
|
+
sort_fields = params[:sort].to_s.split(',').map(&:strip)
|
155
|
+
supported_fields = resource_class.supported_sort_fields
|
156
|
+
|
157
|
+
sort_fields.each do |sort_field|
|
158
|
+
next if sort_field.blank?
|
159
|
+
|
160
|
+
validate_sort_field(sort_field, supported_fields)
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
# Validate a single sort field
|
165
|
+
def validate_sort_field(sort_field, supported_fields)
|
166
|
+
# Remove leading minus for descending sort
|
167
|
+
field_name = sort_field.start_with?('-') ? sort_field[1..] : sort_field
|
168
|
+
|
169
|
+
# Validate field name format
|
170
|
+
unless field_name.match?(/\A[a-zA-Z][a-zA-Z0-9_]*\z/)
|
171
|
+
raise JPie::Errors::InvalidSortParameterError.new(
|
172
|
+
detail: "Invalid sort field format: '#{sort_field}'. " \
|
173
|
+
'Field names must start with a letter and contain only letters, numbers, and underscores'
|
174
|
+
)
|
175
|
+
end
|
176
|
+
|
177
|
+
return if supported_fields.include?(field_name.to_sym) || supported_fields.include?(field_name)
|
178
|
+
|
179
|
+
raise JPie::Errors::UnsupportedSortFieldError.new(
|
180
|
+
sort_field: field_name,
|
181
|
+
supported_fields: supported_fields.map(&:to_s)
|
182
|
+
)
|
183
|
+
end
|
184
|
+
|
185
|
+
# Validate all JSON:API request aspects
|
186
|
+
def validate_json_api_compliance
|
187
|
+
validate_json_api_request
|
188
|
+
validate_include_params
|
189
|
+
validate_sort_params
|
190
|
+
end
|
191
|
+
end
|
192
|
+
end
|
193
|
+
end
|
@@ -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
|
@@ -23,9 +23,15 @@ module JPie
|
|
23
23
|
end
|
24
24
|
|
25
25
|
# More concise method names following Rails conventions
|
26
|
-
def render_jsonapi(resource_or_resources, status: :ok, meta: nil)
|
26
|
+
def render_jsonapi(resource_or_resources, status: :ok, meta: nil, pagination: nil, original_scope: nil)
|
27
27
|
includes = parse_include_params
|
28
28
|
json_data = serializer.serialize(resource_or_resources, context, includes: includes)
|
29
|
+
|
30
|
+
# Add pagination metadata and links if pagination is provided and valid
|
31
|
+
if pagination && pagination[:per_page]
|
32
|
+
add_pagination_metadata(json_data, resource_or_resources, pagination, original_scope)
|
33
|
+
end
|
34
|
+
|
29
35
|
json_data[:meta] = meta if meta
|
30
36
|
|
31
37
|
render json: json_data, status:, content_type: 'application/vnd.api+json'
|
@@ -37,6 +43,94 @@ module JPie
|
|
37
43
|
|
38
44
|
private
|
39
45
|
|
46
|
+
def add_pagination_metadata(json_data, resources, pagination, original_scope)
|
47
|
+
page = pagination[:page] || 1
|
48
|
+
per_page = pagination[:per_page]
|
49
|
+
|
50
|
+
# Get total count from the original scope before pagination
|
51
|
+
total_count = get_total_count(resources, original_scope)
|
52
|
+
total_pages = (total_count.to_f / per_page).ceil
|
53
|
+
|
54
|
+
# Add pagination metadata
|
55
|
+
json_data[:meta] ||= {}
|
56
|
+
json_data[:meta][:pagination] = {
|
57
|
+
page: page,
|
58
|
+
per_page: per_page,
|
59
|
+
total_pages: total_pages,
|
60
|
+
total_count: total_count
|
61
|
+
}
|
62
|
+
|
63
|
+
# Add pagination links
|
64
|
+
json_data[:links] = build_pagination_links(page, per_page, total_pages)
|
65
|
+
end
|
66
|
+
|
67
|
+
def get_total_count(resources, original_scope)
|
68
|
+
# Use original scope if provided, otherwise fall back to resources
|
69
|
+
scope_to_count = original_scope || resources
|
70
|
+
|
71
|
+
# If scope is an ActiveRecord relation, get the count
|
72
|
+
# Otherwise, if it's an array, get the length
|
73
|
+
if scope_to_count.respond_to?(:count) && !scope_to_count.loaded?
|
74
|
+
scope_to_count.count
|
75
|
+
elsif scope_to_count.respond_to?(:size)
|
76
|
+
scope_to_count.size
|
77
|
+
else
|
78
|
+
0
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def build_pagination_links(page, per_page, total_pages)
|
83
|
+
url_components = extract_url_components
|
84
|
+
pagination_data = { page: page, per_page: per_page, total_pages: total_pages }
|
85
|
+
|
86
|
+
links = build_base_pagination_links(url_components, pagination_data)
|
87
|
+
add_conditional_pagination_links(links, url_components, pagination_data)
|
88
|
+
|
89
|
+
links
|
90
|
+
end
|
91
|
+
|
92
|
+
def extract_url_components
|
93
|
+
base_url = request.respond_to?(:base_url) ? request.base_url : 'http://example.com'
|
94
|
+
path = request.respond_to?(:path) ? request.path : '/resources'
|
95
|
+
query_params = request.respond_to?(:query_parameters) ? request.query_parameters.except('page') : {}
|
96
|
+
|
97
|
+
{ base_url: base_url, path: path, query_params: query_params }
|
98
|
+
end
|
99
|
+
|
100
|
+
def build_base_pagination_links(url_components, pagination_data)
|
101
|
+
full_url = url_components[:base_url] + url_components[:path]
|
102
|
+
query_params = url_components[:query_params]
|
103
|
+
page = pagination_data[:page]
|
104
|
+
per_page = pagination_data[:per_page]
|
105
|
+
total_pages = pagination_data[:total_pages]
|
106
|
+
|
107
|
+
{
|
108
|
+
self: build_page_url(full_url, query_params, page, per_page),
|
109
|
+
first: build_page_url(full_url, query_params, 1, per_page),
|
110
|
+
last: build_page_url(full_url, query_params, total_pages, per_page)
|
111
|
+
}
|
112
|
+
end
|
113
|
+
|
114
|
+
def add_conditional_pagination_links(links, url_components, pagination_data)
|
115
|
+
full_url = url_components[:base_url] + url_components[:path]
|
116
|
+
query_params = url_components[:query_params]
|
117
|
+
page = pagination_data[:page]
|
118
|
+
per_page = pagination_data[:per_page]
|
119
|
+
total_pages = pagination_data[:total_pages]
|
120
|
+
|
121
|
+
links[:prev] = build_page_url(full_url, query_params, page - 1, per_page) if page > 1
|
122
|
+
links[:next] = build_page_url(full_url, query_params, page + 1, per_page) if page < total_pages
|
123
|
+
end
|
124
|
+
|
125
|
+
def build_page_url(base_url, query_params, page_num, per_page)
|
126
|
+
params = query_params.merge(
|
127
|
+
'page' => page_num.to_s,
|
128
|
+
'per_page' => per_page.to_s
|
129
|
+
)
|
130
|
+
query_string = params.respond_to?(:to_query) ? params.to_query : params.map { |k, v| "#{k}=#{v}" }.join('&')
|
131
|
+
"#{base_url}?#{query_string}"
|
132
|
+
end
|
133
|
+
|
40
134
|
def infer_resource_class
|
41
135
|
# Convert controller name to resource class name
|
42
136
|
# e.g., "UsersController" -> "UserResource"
|
data/lib/jpie/controller.rb
CHANGED
@@ -5,6 +5,7 @@ require_relative 'controller/error_handling'
|
|
5
5
|
require_relative 'controller/parameter_parsing'
|
6
6
|
require_relative 'controller/rendering'
|
7
7
|
require_relative 'controller/crud_actions'
|
8
|
+
require_relative 'controller/json_api_validation'
|
8
9
|
|
9
10
|
module JPie
|
10
11
|
module Controller
|
@@ -14,5 +15,6 @@ module JPie
|
|
14
15
|
include ParameterParsing
|
15
16
|
include Rendering
|
16
17
|
include CrudActions
|
18
|
+
include JsonApiValidation
|
17
19
|
end
|
18
20
|
end
|
data/lib/jpie/errors.rb
CHANGED
@@ -66,5 +66,46 @@ module JPie
|
|
66
66
|
super(status: 500, title: 'Resource Error', detail:)
|
67
67
|
end
|
68
68
|
end
|
69
|
+
|
70
|
+
# JSON:API Compliance Errors
|
71
|
+
class InvalidJsonApiRequestError < BadRequestError
|
72
|
+
def initialize(detail: 'Request is not JSON:API compliant')
|
73
|
+
super
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
class UnsupportedIncludeError < BadRequestError
|
78
|
+
def initialize(include_path:, supported_includes: [])
|
79
|
+
detail = if supported_includes.any?
|
80
|
+
"Unsupported include '#{include_path}'. Supported includes: #{supported_includes.join(', ')}"
|
81
|
+
else
|
82
|
+
"Unsupported include '#{include_path}'. No includes are supported for this resource"
|
83
|
+
end
|
84
|
+
super(detail: detail)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
class UnsupportedSortFieldError < BadRequestError
|
89
|
+
def initialize(sort_field:, supported_fields: [])
|
90
|
+
detail = if supported_fields.any?
|
91
|
+
"Unsupported sort field '#{sort_field}'. Supported fields: #{supported_fields.join(', ')}"
|
92
|
+
else
|
93
|
+
"Unsupported sort field '#{sort_field}'. No sorting is supported for this resource"
|
94
|
+
end
|
95
|
+
super(detail: detail)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
class InvalidSortParameterError < BadRequestError
|
100
|
+
def initialize(detail: 'Invalid sort parameter format')
|
101
|
+
super
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
class InvalidIncludeParameterError < BadRequestError
|
106
|
+
def initialize(detail: 'Invalid include parameter format')
|
107
|
+
super
|
108
|
+
end
|
109
|
+
end
|
69
110
|
end
|
70
111
|
end
|
@@ -45,8 +45,14 @@ module JPie
|
|
45
45
|
return if method_defined?(name) || private_method_defined?(name)
|
46
46
|
|
47
47
|
define_method(name) do
|
48
|
-
|
49
|
-
|
48
|
+
# Handle through associations
|
49
|
+
if options[:through]
|
50
|
+
handle_through_association(name, options)
|
51
|
+
else
|
52
|
+
# Standard association handling
|
53
|
+
attr_name = options[:attr] || name
|
54
|
+
@object.public_send(attr_name)
|
55
|
+
end
|
50
56
|
end
|
51
57
|
end
|
52
58
|
|
@@ -93,6 +99,14 @@ module JPie
|
|
93
99
|
"#{singularized_name.camelize}Resource"
|
94
100
|
end
|
95
101
|
end
|
102
|
+
|
103
|
+
private
|
104
|
+
|
105
|
+
# Handle through associations by calling the appropriate association method
|
106
|
+
def handle_through_association(name, options)
|
107
|
+
attr_name = options[:attr] || name
|
108
|
+
@object.public_send(attr_name)
|
109
|
+
end
|
96
110
|
end
|
97
111
|
end
|
98
112
|
end
|
data/lib/jpie/resource.rb
CHANGED
@@ -37,6 +37,46 @@ module JPie
|
|
37
37
|
def scope(_context = {})
|
38
38
|
model.all
|
39
39
|
end
|
40
|
+
|
41
|
+
# Return supported include paths for validation
|
42
|
+
# Override this method to customize supported includes
|
43
|
+
def supported_includes
|
44
|
+
# Return relationship names as supported includes by default
|
45
|
+
_relationships.keys.map(&:to_s)
|
46
|
+
|
47
|
+
# Convert to nested hash format for complex includes
|
48
|
+
# For simple includes, return array format
|
49
|
+
end
|
50
|
+
|
51
|
+
# Return supported sort fields for validation
|
52
|
+
# Override this method to customize supported sort fields
|
53
|
+
def supported_sort_fields
|
54
|
+
base_fields = extract_base_sort_fields
|
55
|
+
timestamp_fields = extract_timestamp_fields
|
56
|
+
(base_fields + timestamp_fields).uniq
|
57
|
+
end
|
58
|
+
|
59
|
+
private
|
60
|
+
|
61
|
+
def extract_base_sort_fields
|
62
|
+
(_attributes + _sortable_fields.keys).uniq.map(&:to_s)
|
63
|
+
end
|
64
|
+
|
65
|
+
def extract_timestamp_fields
|
66
|
+
return [] unless model.respond_to?(:column_names)
|
67
|
+
|
68
|
+
timestamp_fields = []
|
69
|
+
add_timestamp_field(timestamp_fields, 'created_at')
|
70
|
+
add_timestamp_field(timestamp_fields, 'updated_at')
|
71
|
+
timestamp_fields
|
72
|
+
end
|
73
|
+
|
74
|
+
def add_timestamp_field(fields, field_name)
|
75
|
+
return unless model.column_names.include?(field_name)
|
76
|
+
return if fields.include?(field_name)
|
77
|
+
|
78
|
+
fields << field_name
|
79
|
+
end
|
40
80
|
end
|
41
81
|
|
42
82
|
attr_reader :object, :context
|
data/lib/jpie/version.rb
CHANGED
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: jpie
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.4.
|
4
|
+
version: 0.4.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Emil Kampp
|
8
8
|
bindir: exe
|
9
9
|
cert_chain: []
|
10
|
-
date: 2025-05-
|
10
|
+
date: 2025-05-26 00:00:00.000000000 Z
|
11
11
|
dependencies:
|
12
12
|
- !ruby/object:Gem::Dependency
|
13
13
|
name: activesupport
|
@@ -169,18 +169,28 @@ executables: []
|
|
169
169
|
extensions: []
|
170
170
|
extra_rdoc_files: []
|
171
171
|
files:
|
172
|
-
- ".
|
172
|
+
- ".cursorrules"
|
173
|
+
- ".overcommit.yml"
|
173
174
|
- ".rubocop.yml"
|
174
175
|
- CHANGELOG.md
|
175
176
|
- LICENSE.txt
|
176
177
|
- README.md
|
177
178
|
- Rakefile
|
179
|
+
- examples/basic_example.md
|
180
|
+
- examples/including_related_resources.md
|
181
|
+
- examples/pagination.md
|
182
|
+
- examples/resource_attribute_configuration.md
|
183
|
+
- examples/resource_meta_configuration.md
|
184
|
+
- examples/single_table_inheritance.md
|
178
185
|
- jpie.gemspec
|
179
186
|
- lib/jpie.rb
|
180
187
|
- lib/jpie/configuration.rb
|
181
188
|
- lib/jpie/controller.rb
|
182
189
|
- lib/jpie/controller/crud_actions.rb
|
183
190
|
- lib/jpie/controller/error_handling.rb
|
191
|
+
- lib/jpie/controller/error_handling/handler_setup.rb
|
192
|
+
- lib/jpie/controller/error_handling/handlers.rb
|
193
|
+
- lib/jpie/controller/json_api_validation.rb
|
184
194
|
- lib/jpie/controller/parameter_parsing.rb
|
185
195
|
- lib/jpie/controller/rendering.rb
|
186
196
|
- lib/jpie/deserializer.rb
|