jpie 0.3.1 → 0.4.1
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} +14 -2
- data/CHANGELOG.md +51 -0
- data/README.md +179 -844
- data/examples/basic_example.md +146 -0
- data/examples/including_related_resources.md +491 -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 +10 -0
- data/lib/jpie/controller/error_handling.rb +168 -17
- data/lib/jpie/controller/json_api_validation.rb +171 -0
- data/lib/jpie/controller.rb +2 -0
- data/lib/jpie/errors.rb +41 -0
- data/lib/jpie/generators/resource_generator.rb +86 -9
- data/lib/jpie/generators/templates/resource.rb.erb +20 -1
- data/lib/jpie/resource/attributable.rb +21 -2
- data/lib/jpie/resource.rb +26 -0
- data/lib/jpie/version.rb +1 -1
- metadata +9 -3
@@ -6,36 +6,187 @@ module JPie
|
|
6
6
|
extend ActiveSupport::Concern
|
7
7
|
|
8
8
|
included do
|
9
|
-
|
9
|
+
# Use class_attribute to allow easy overriding
|
10
|
+
class_attribute :jpie_error_handlers_enabled, default: true
|
10
11
|
|
11
|
-
|
12
|
-
|
13
|
-
|
12
|
+
# Set up default handlers unless explicitly disabled
|
13
|
+
setup_jpie_error_handlers if jpie_error_handlers_enabled
|
14
|
+
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
|
14
90
|
end
|
15
91
|
end
|
16
92
|
|
17
93
|
private
|
18
94
|
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
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
|
+
)
|
23
102
|
end
|
24
103
|
|
25
|
-
|
26
|
-
|
27
|
-
|
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
|
+
)
|
28
111
|
end
|
29
112
|
|
30
|
-
|
31
|
-
|
32
|
-
|
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
|
+
}
|
33
121
|
end
|
34
122
|
|
35
|
-
render json: { errors: },
|
36
|
-
|
37
|
-
|
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
|
+
)
|
38
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
|
39
190
|
end
|
40
191
|
end
|
41
192
|
end
|
@@ -0,0 +1,171 @@
|
|
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 = request.body.read
|
32
|
+
request.body.rewind # Reset for later reading
|
33
|
+
|
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
|
43
|
+
|
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
|
49
|
+
|
50
|
+
validate_data_structure(parsed_body['data'])
|
51
|
+
end
|
52
|
+
|
53
|
+
# Validate the structure of the data member
|
54
|
+
def validate_data_structure(data)
|
55
|
+
if data.is_a?(Array)
|
56
|
+
data.each { |item| validate_resource_object(item) }
|
57
|
+
elsif data.is_a?(Hash)
|
58
|
+
validate_resource_object(data)
|
59
|
+
elsif !data.nil?
|
60
|
+
raise JPie::Errors::InvalidJsonApiRequestError.new(
|
61
|
+
detail: 'Data member must be an object, array, or null'
|
62
|
+
)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
# Validate individual resource object structure
|
67
|
+
def validate_resource_object(resource)
|
68
|
+
unless resource.is_a?(Hash)
|
69
|
+
raise JPie::Errors::InvalidJsonApiRequestError.new(
|
70
|
+
detail: 'Resource objects must be JSON objects'
|
71
|
+
)
|
72
|
+
end
|
73
|
+
|
74
|
+
unless resource.key?('type')
|
75
|
+
raise JPie::Errors::InvalidJsonApiRequestError.new(
|
76
|
+
detail: 'Resource objects must have a "type" member'
|
77
|
+
)
|
78
|
+
end
|
79
|
+
|
80
|
+
# ID is required for updates but not for creates
|
81
|
+
return unless request.patch? || request.put?
|
82
|
+
return if resource.key?('id')
|
83
|
+
|
84
|
+
raise JPie::Errors::InvalidJsonApiRequestError.new(
|
85
|
+
detail: 'Resource objects must have an "id" member for updates'
|
86
|
+
)
|
87
|
+
end
|
88
|
+
|
89
|
+
# Validate include parameters against supported includes
|
90
|
+
def validate_include_params
|
91
|
+
return if params[:include].blank?
|
92
|
+
|
93
|
+
include_paths = params[:include].to_s.split(',').map(&:strip)
|
94
|
+
supported_includes = resource_class.supported_includes
|
95
|
+
|
96
|
+
include_paths.each do |include_path|
|
97
|
+
next if include_path.blank?
|
98
|
+
|
99
|
+
validate_include_path(include_path, supported_includes)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
# Validate a single include path
|
104
|
+
def validate_include_path(include_path, supported_includes)
|
105
|
+
# Handle nested includes (e.g., "posts.comments")
|
106
|
+
path_parts = include_path.split('.')
|
107
|
+
current_level = supported_includes
|
108
|
+
|
109
|
+
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
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
# Validate sort parameters against supported fields
|
130
|
+
def validate_sort_params
|
131
|
+
return if params[:sort].blank?
|
132
|
+
|
133
|
+
sort_fields = params[:sort].to_s.split(',').map(&:strip)
|
134
|
+
supported_fields = resource_class.supported_sort_fields
|
135
|
+
|
136
|
+
sort_fields.each do |sort_field|
|
137
|
+
next if sort_field.blank?
|
138
|
+
|
139
|
+
validate_sort_field(sort_field, supported_fields)
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
# Validate a single sort field
|
144
|
+
def validate_sort_field(sort_field, supported_fields)
|
145
|
+
# Remove leading minus for descending sort
|
146
|
+
field_name = sort_field.start_with?('-') ? sort_field[1..] : sort_field
|
147
|
+
|
148
|
+
# Validate field name format
|
149
|
+
unless field_name.match?(/\A[a-zA-Z][a-zA-Z0-9_]*\z/)
|
150
|
+
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"
|
152
|
+
)
|
153
|
+
end
|
154
|
+
|
155
|
+
return if supported_fields.include?(field_name.to_sym) || supported_fields.include?(field_name)
|
156
|
+
|
157
|
+
raise JPie::Errors::UnsupportedSortFieldError.new(
|
158
|
+
sort_field: field_name,
|
159
|
+
supported_fields: supported_fields.map(&:to_s)
|
160
|
+
)
|
161
|
+
end
|
162
|
+
|
163
|
+
# Validate all JSON:API request aspects
|
164
|
+
def validate_json_api_compliance
|
165
|
+
validate_json_api_request
|
166
|
+
validate_include_params
|
167
|
+
validate_sort_params
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|
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
|
@@ -5,11 +5,17 @@ require 'rails/generators/base'
|
|
5
5
|
module JPie
|
6
6
|
module Generators
|
7
7
|
class ResourceGenerator < Rails::Generators::NamedBase
|
8
|
-
|
8
|
+
source_root File.expand_path('templates', __dir__)
|
9
9
|
|
10
|
-
|
10
|
+
desc 'Generate a JPie resource class with semantic field definitions'
|
11
11
|
|
12
|
-
|
12
|
+
argument :field_definitions, type: :array, default: [],
|
13
|
+
banner: 'attribute:field meta:field relationship:type:field'
|
14
|
+
|
15
|
+
class_option :model, type: :string,
|
16
|
+
desc: 'Model class to associate with this resource (defaults to inferred model)'
|
17
|
+
class_option :skip_model, type: :boolean, default: false,
|
18
|
+
desc: 'Skip explicit model declaration (use automatic inference)'
|
13
19
|
|
14
20
|
def create_resource_file
|
15
21
|
template 'resource.rb.erb', File.join('app/resources', "#{file_name}_resource.rb")
|
@@ -21,18 +27,89 @@ module JPie
|
|
21
27
|
options[:model] || class_name
|
22
28
|
end
|
23
29
|
|
30
|
+
def needs_explicit_model?
|
31
|
+
# Only need explicit model declaration if:
|
32
|
+
# 1. User explicitly provided a different model name, OR
|
33
|
+
# 2. User didn't use --skip-model flag AND the model name differs from the inferred name
|
34
|
+
return false if options[:skip_model]
|
35
|
+
return true if options[:model] && options[:model] != class_name
|
36
|
+
|
37
|
+
# For standard naming (UserResource -> User), we can skip the explicit declaration
|
38
|
+
false
|
39
|
+
end
|
40
|
+
|
24
41
|
def resource_attributes
|
25
|
-
|
42
|
+
parse_field_definitions.fetch(:attributes, [])
|
43
|
+
end
|
44
|
+
|
45
|
+
def meta_attributes_list
|
46
|
+
parse_field_definitions.fetch(:meta_attributes, [])
|
47
|
+
end
|
48
|
+
|
49
|
+
def relationships_list
|
50
|
+
parse_field_definitions.fetch(:relationships, [])
|
51
|
+
end
|
52
|
+
|
53
|
+
def parse_relationships
|
54
|
+
relationships_list.map do |rel|
|
55
|
+
# rel is already a hash with :type and :name from parse_field_definitions
|
56
|
+
rel
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def parse_field_definitions
|
61
|
+
return @parsed_definitions if @parsed_definitions
|
62
|
+
|
63
|
+
@parsed_definitions = { attributes: [], meta_attributes: [], relationships: [] }
|
64
|
+
|
65
|
+
field_definitions.each do |definition|
|
66
|
+
process_field_definition(definition)
|
67
|
+
end
|
68
|
+
|
69
|
+
@parsed_definitions
|
70
|
+
end
|
71
|
+
|
72
|
+
def process_field_definition(definition)
|
73
|
+
case definition
|
74
|
+
when /^attribute:(.+)$/
|
75
|
+
@parsed_definitions[:attributes] << ::Regexp.last_match(1)
|
76
|
+
when /^meta:(.+)$/
|
77
|
+
@parsed_definitions[:meta_attributes] << ::Regexp.last_match(1)
|
78
|
+
when /^relationship:(.+):(.+)$/, /^(has_many|has_one|belongs_to):(.+)$/
|
79
|
+
add_relationship(::Regexp.last_match(1), ::Regexp.last_match(2))
|
80
|
+
when /^(.+):(.+)$/
|
81
|
+
process_legacy_field(::Regexp.last_match(1))
|
82
|
+
else
|
83
|
+
process_plain_field(definition)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def add_relationship(type, name)
|
88
|
+
@parsed_definitions[:relationships] << { type: type, name: name }
|
89
|
+
end
|
26
90
|
|
27
|
-
|
91
|
+
def process_legacy_field(field_name)
|
92
|
+
# Legacy support: field:type format - treat as attribute and ignore type
|
93
|
+
if meta_attribute_name?(field_name)
|
94
|
+
@parsed_definitions[:meta_attributes] << field_name
|
95
|
+
else
|
96
|
+
@parsed_definitions[:attributes] << field_name
|
97
|
+
end
|
28
98
|
end
|
29
99
|
|
30
|
-
def
|
31
|
-
|
100
|
+
def process_plain_field(field_name)
|
101
|
+
# Plain field name - treat as attribute
|
102
|
+
if meta_attribute_name?(field_name)
|
103
|
+
@parsed_definitions[:meta_attributes] << field_name
|
104
|
+
else
|
105
|
+
@parsed_definitions[:attributes] << field_name
|
106
|
+
end
|
32
107
|
end
|
33
108
|
|
34
|
-
def
|
35
|
-
|
109
|
+
def meta_attribute_name?(name)
|
110
|
+
# Check if field name suggests it's a meta attribute
|
111
|
+
meta_names = %w[created_at updated_at deleted_at published_at archived_at]
|
112
|
+
meta_names.include?(name)
|
36
113
|
end
|
37
114
|
end
|
38
115
|
end
|
@@ -1,12 +1,31 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
class <%= class_name %>Resource < JPie::Resource
|
4
|
+
<% if needs_explicit_model? -%>
|
4
5
|
model <%= model_class_name %>
|
6
|
+
<% end -%>
|
5
7
|
|
6
8
|
<% if resource_attributes.any? -%>
|
7
9
|
attributes <%= resource_attributes.map { |attr| ":#{attr}" }.join(', ') %>
|
8
10
|
<% else -%>
|
9
11
|
# Define your attributes here:
|
10
|
-
# attributes :name, :email, :
|
12
|
+
# attributes :name, :email, :title
|
13
|
+
<% end -%>
|
14
|
+
|
15
|
+
<% if meta_attributes_list.any? -%>
|
16
|
+
meta_attributes <%= meta_attributes_list.map { |attr| ":#{attr}" }.join(', ') %>
|
17
|
+
<% else -%>
|
18
|
+
# Define your meta attributes here:
|
19
|
+
# meta_attributes :created_at, :updated_at
|
20
|
+
<% end -%>
|
21
|
+
|
22
|
+
<% if parse_relationships.any? -%>
|
23
|
+
<% parse_relationships.each do |rel| -%>
|
24
|
+
<%= rel[:type] %> :<%= rel[:name] %>
|
25
|
+
<% end -%>
|
26
|
+
<% else -%>
|
27
|
+
# Define your relationships here:
|
28
|
+
# has_many :posts
|
29
|
+
# has_one :user
|
11
30
|
<% end -%>
|
12
31
|
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,19 @@ 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
|
+
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
|
114
|
+
end
|
96
115
|
end
|
97
116
|
end
|
98
117
|
end
|
data/lib/jpie/resource.rb
CHANGED
@@ -37,6 +37,32 @@ 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
|
+
# Return all attributes and sortable fields as supported sort fields by default
|
55
|
+
fields = (_attributes + _sortable_fields.keys).uniq.map(&:to_s)
|
56
|
+
|
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')
|
60
|
+
|
61
|
+
fields << 'updated_at' if model.column_names.include?('updated_at') && fields.exclude?('updated_at')
|
62
|
+
end
|
63
|
+
|
64
|
+
fields
|
65
|
+
end
|
40
66
|
end
|
41
67
|
|
42
68
|
attr_reader :object, :context
|
data/lib/jpie/version.rb
CHANGED