jpie 0.4.1 → 0.4.3
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/.cursorrules +8 -3
- data/.overcommit.yml +35 -0
- data/CHANGELOG.md +14 -0
- data/README.md +58 -198
- data/examples/pagination.md +303 -0
- data/examples/relationships.md +114 -0
- data/lib/jpie/controller/crud_actions.rb +23 -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 +6 -175
- data/lib/jpie/controller/json_api_validation.rb +55 -33
- data/lib/jpie/controller/parameter_parsing.rb +43 -0
- data/lib/jpie/controller/related_actions.rb +45 -0
- data/lib/jpie/controller/relationship_actions.rb +291 -0
- data/lib/jpie/controller/relationship_validation.rb +117 -0
- data/lib/jpie/controller/rendering.rb +95 -1
- data/lib/jpie/controller.rb +25 -0
- data/lib/jpie/errors.rb +6 -0
- data/lib/jpie/railtie.rb +6 -0
- data/lib/jpie/resource/attributable.rb +4 -9
- data/lib/jpie/resource.rb +22 -8
- data/lib/jpie/routing.rb +59 -0
- data/lib/jpie/version.rb +1 -1
- data/lib/jpie.rb +1 -0
- metadata +11 -2
@@ -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
@@ -6,6 +6,8 @@ require_relative 'controller/parameter_parsing'
|
|
6
6
|
require_relative 'controller/rendering'
|
7
7
|
require_relative 'controller/crud_actions'
|
8
8
|
require_relative 'controller/json_api_validation'
|
9
|
+
require_relative 'controller/relationship_actions'
|
10
|
+
require_relative 'controller/related_actions'
|
9
11
|
|
10
12
|
module JPie
|
11
13
|
module Controller
|
@@ -16,5 +18,28 @@ module JPie
|
|
16
18
|
include Rendering
|
17
19
|
include CrudActions
|
18
20
|
include JsonApiValidation
|
21
|
+
include RelationshipActions
|
22
|
+
include RelatedActions
|
23
|
+
|
24
|
+
# Relationship route actions
|
25
|
+
def show_relationship
|
26
|
+
relationship_show
|
27
|
+
end
|
28
|
+
|
29
|
+
def update_relationship
|
30
|
+
relationship_update
|
31
|
+
end
|
32
|
+
|
33
|
+
def create_relationship
|
34
|
+
relationship_create
|
35
|
+
end
|
36
|
+
|
37
|
+
def destroy_relationship
|
38
|
+
relationship_destroy
|
39
|
+
end
|
40
|
+
|
41
|
+
def show_related
|
42
|
+
related_show
|
43
|
+
end
|
19
44
|
end
|
20
45
|
end
|
data/lib/jpie/errors.rb
CHANGED
@@ -55,6 +55,12 @@ module JPie
|
|
55
55
|
end
|
56
56
|
end
|
57
57
|
|
58
|
+
class UnsupportedMediaTypeError < Error
|
59
|
+
def initialize(detail: 'Unsupported Media Type')
|
60
|
+
super(status: 415, title: 'Unsupported Media Type', detail:)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
58
64
|
class InternalServerError < Error
|
59
65
|
def initialize(detail: 'Internal Server Error')
|
60
66
|
super(status: 500, title: 'Internal Server Error', detail:)
|
data/lib/jpie/railtie.rb
CHANGED
@@ -29,6 +29,12 @@ module JPie
|
|
29
29
|
end
|
30
30
|
end
|
31
31
|
|
32
|
+
initializer 'jpie.routing' do
|
33
|
+
ActiveSupport.on_load(:action_dispatch) do
|
34
|
+
ActionDispatch::Routing::Mapper.include JPie::Routing
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
32
38
|
generators do
|
33
39
|
require 'jpie/generators/resource_generator'
|
34
40
|
end
|
@@ -59,13 +59,13 @@ module JPie
|
|
59
59
|
def has_many(name, options = {})
|
60
60
|
name = name.to_sym
|
61
61
|
resource_class_name = options[:resource] || infer_resource_class_name(name)
|
62
|
-
relationship(name, { resource: resource_class_name }.merge(options))
|
62
|
+
relationship(name, { type: :has_many, resource: resource_class_name }.merge(options))
|
63
63
|
end
|
64
64
|
|
65
65
|
def has_one(name, options = {})
|
66
66
|
name = name.to_sym
|
67
67
|
resource_class_name = options[:resource] || infer_resource_class_name(name)
|
68
|
-
relationship(name, { resource: resource_class_name }.merge(options))
|
68
|
+
relationship(name, { type: :has_one, resource: resource_class_name }.merge(options))
|
69
69
|
end
|
70
70
|
|
71
71
|
private
|
@@ -104,13 +104,8 @@ module JPie
|
|
104
104
|
|
105
105
|
# Handle through associations by calling the appropriate association method
|
106
106
|
def handle_through_association(name, options)
|
107
|
-
|
108
|
-
|
109
|
-
@object.public_send(options[:attr])
|
110
|
-
else
|
111
|
-
# Use the relationship name directly - Rails will handle the through association
|
112
|
-
@object.public_send(name)
|
113
|
-
end
|
107
|
+
attr_name = options[:attr] || name
|
108
|
+
@object.public_send(attr_name)
|
114
109
|
end
|
115
110
|
end
|
116
111
|
end
|
data/lib/jpie/resource.rb
CHANGED
@@ -51,17 +51,31 @@ module JPie
|
|
51
51
|
# Return supported sort fields for validation
|
52
52
|
# Override this method to customize supported sort fields
|
53
53
|
def supported_sort_fields
|
54
|
-
|
55
|
-
|
54
|
+
base_fields = extract_base_sort_fields
|
55
|
+
timestamp_fields = extract_timestamp_fields
|
56
|
+
(base_fields + timestamp_fields).uniq
|
57
|
+
end
|
56
58
|
|
57
|
-
|
58
|
-
if model.respond_to?(:column_names)
|
59
|
-
fields << 'created_at' if model.column_names.include?('created_at') && fields.exclude?('created_at')
|
59
|
+
private
|
60
60
|
|
61
|
-
|
62
|
-
|
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)
|
63
77
|
|
64
|
-
fields
|
78
|
+
fields << field_name
|
65
79
|
end
|
66
80
|
end
|
67
81
|
|
data/lib/jpie/routing.rb
ADDED
@@ -0,0 +1,59 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module JPie
|
4
|
+
module Routing
|
5
|
+
# Add jpie_resources method to Rails routing DSL that creates JSON:API compliant routes
|
6
|
+
def jpie_resources(*resources)
|
7
|
+
options = resources.extract_options!
|
8
|
+
merged_options = build_merged_options(options)
|
9
|
+
|
10
|
+
# Create standard RESTful routes for the resource
|
11
|
+
resources(*resources, merged_options) do
|
12
|
+
yield if block_given?
|
13
|
+
add_jsonapi_relationship_routes(merged_options) if relationship_routes_allowed?(merged_options)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def build_merged_options(options)
|
20
|
+
default_options = {
|
21
|
+
defaults: { format: :json },
|
22
|
+
constraints: { format: :json }
|
23
|
+
}
|
24
|
+
default_options.merge(options)
|
25
|
+
end
|
26
|
+
|
27
|
+
def relationship_routes_allowed?(merged_options)
|
28
|
+
only_actions = merged_options[:only]
|
29
|
+
except_actions = merged_options[:except]
|
30
|
+
|
31
|
+
if only_actions
|
32
|
+
# If only specific actions are allowed, don't add relationship routes
|
33
|
+
# unless multiple member actions (show, update, destroy) are included
|
34
|
+
(only_actions & %i[show update destroy]).size >= 2
|
35
|
+
elsif except_actions
|
36
|
+
# If actions are excluded, only add if member actions aren't excluded
|
37
|
+
!except_actions.intersect?(%i[show update destroy])
|
38
|
+
else
|
39
|
+
true
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def add_jsonapi_relationship_routes(_merged_options)
|
44
|
+
# These routes handle relationship management as per JSON:API spec
|
45
|
+
member do
|
46
|
+
# Routes for fetching and updating relationships
|
47
|
+
# Pattern: /resources/:id/relationships/:relationship_name
|
48
|
+
get 'relationships/*relationship_name', action: :show_relationship, as: :relationship
|
49
|
+
patch 'relationships/*relationship_name', action: :update_relationship
|
50
|
+
post 'relationships/*relationship_name', action: :create_relationship
|
51
|
+
delete 'relationships/*relationship_name', action: :destroy_relationship
|
52
|
+
|
53
|
+
# Routes for fetching related resources
|
54
|
+
# Pattern: /resources/:id/:relationship_name
|
55
|
+
get '*relationship_name', action: :show_related, as: :related
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
data/lib/jpie/version.rb
CHANGED
data/lib/jpie.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.3
|
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
|
@@ -170,6 +170,7 @@ 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
|
@@ -177,6 +178,8 @@ files:
|
|
177
178
|
- Rakefile
|
178
179
|
- examples/basic_example.md
|
179
180
|
- examples/including_related_resources.md
|
181
|
+
- examples/pagination.md
|
182
|
+
- examples/relationships.md
|
180
183
|
- examples/resource_attribute_configuration.md
|
181
184
|
- examples/resource_meta_configuration.md
|
182
185
|
- examples/single_table_inheritance.md
|
@@ -186,8 +189,13 @@ files:
|
|
186
189
|
- lib/jpie/controller.rb
|
187
190
|
- lib/jpie/controller/crud_actions.rb
|
188
191
|
- lib/jpie/controller/error_handling.rb
|
192
|
+
- lib/jpie/controller/error_handling/handler_setup.rb
|
193
|
+
- lib/jpie/controller/error_handling/handlers.rb
|
189
194
|
- lib/jpie/controller/json_api_validation.rb
|
190
195
|
- lib/jpie/controller/parameter_parsing.rb
|
196
|
+
- lib/jpie/controller/related_actions.rb
|
197
|
+
- lib/jpie/controller/relationship_actions.rb
|
198
|
+
- lib/jpie/controller/relationship_validation.rb
|
191
199
|
- lib/jpie/controller/rendering.rb
|
192
200
|
- lib/jpie/deserializer.rb
|
193
201
|
- lib/jpie/errors.rb
|
@@ -198,6 +206,7 @@ files:
|
|
198
206
|
- lib/jpie/resource/attributable.rb
|
199
207
|
- lib/jpie/resource/inferrable.rb
|
200
208
|
- lib/jpie/resource/sortable.rb
|
209
|
+
- lib/jpie/routing.rb
|
201
210
|
- lib/jpie/serializer.rb
|
202
211
|
- lib/jpie/version.rb
|
203
212
|
homepage: https://github.com/emk-klaay/jpie
|