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.
@@ -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"
@@ -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
- if options[:attr]
108
- # Custom attribute name was provided - use it
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
- # Return all attributes and sortable fields as supported sort fields by default
55
- fields = (_attributes + _sortable_fields.keys).uniq.map(&:to_s)
54
+ base_fields = extract_base_sort_fields
55
+ timestamp_fields = extract_timestamp_fields
56
+ (base_fields + timestamp_fields).uniq
57
+ end
56
58
 
57
- # Add common model timestamp fields if the model supports them
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
- fields << 'updated_at' if model.column_names.include?('updated_at') && fields.exclude?('updated_at')
62
- end
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
 
@@ -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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module JPie
4
- VERSION = '0.4.1'
4
+ VERSION = '0.4.3'
5
5
  end
data/lib/jpie.rb CHANGED
@@ -11,6 +11,7 @@ module JPie
11
11
  autoload :Controller, 'jpie/controller'
12
12
  autoload :Configuration, 'jpie/configuration'
13
13
  autoload :Errors, 'jpie/errors'
14
+ autoload :Routing, 'jpie/routing'
14
15
 
15
16
  class << self
16
17
  def configuration
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.1
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-25 00:00:00.000000000 Z
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