jpie 0.4.5 → 1.0.0
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/.gitignore +21 -0
- data/.rspec +3 -0
- data/.rubocop.yml +35 -110
- data/.travis.yml +7 -0
- data/Gemfile +21 -0
- data/Gemfile.lock +312 -0
- data/README.md +2072 -140
- data/Rakefile +3 -14
- data/bin/console +15 -0
- data/bin/setup +8 -0
- data/jpie.gemspec +18 -35
- data/kiln/app/resources/user_message_resource.rb +2 -0
- data/lib/jpie.rb +3 -24
- data/lib/json_api/active_storage/deserialization.rb +106 -0
- data/lib/json_api/active_storage/detection.rb +74 -0
- data/lib/json_api/active_storage/serialization.rb +32 -0
- data/lib/json_api/configuration.rb +58 -0
- data/lib/json_api/controllers/base_controller.rb +26 -0
- data/lib/json_api/controllers/concerns/controller_helpers.rb +223 -0
- data/lib/json_api/controllers/concerns/resource_actions.rb +657 -0
- data/lib/json_api/controllers/relationships_controller.rb +504 -0
- data/lib/json_api/controllers/resources_controller.rb +6 -0
- data/lib/json_api/errors/parameter_not_allowed.rb +19 -0
- data/lib/json_api/railtie.rb +75 -0
- data/lib/json_api/resources/active_storage_blob_resource.rb +11 -0
- data/lib/json_api/resources/resource.rb +238 -0
- data/lib/json_api/resources/resource_loader.rb +35 -0
- data/lib/json_api/routing.rb +72 -0
- data/lib/json_api/serialization/deserializer.rb +362 -0
- data/lib/json_api/serialization/serializer.rb +320 -0
- data/lib/json_api/support/active_storage_support.rb +85 -0
- data/lib/json_api/support/collection_query.rb +406 -0
- data/lib/json_api/support/instrumentation.rb +42 -0
- data/lib/json_api/support/param_helpers.rb +51 -0
- data/lib/json_api/support/relationship_guard.rb +16 -0
- data/lib/json_api/support/relationship_helpers.rb +74 -0
- data/lib/json_api/support/resource_identifier.rb +87 -0
- data/lib/json_api/support/responders.rb +100 -0
- data/lib/json_api/support/response_helpers.rb +10 -0
- data/lib/json_api/support/sort_parsing.rb +21 -0
- data/lib/json_api/support/type_conversion.rb +21 -0
- data/lib/json_api/testing/test_helper.rb +76 -0
- data/lib/json_api/testing.rb +3 -0
- data/lib/{jpie → json_api}/version.rb +2 -2
- data/lib/json_api.rb +50 -0
- data/lib/rubocop/cop/custom/hash_value_omission.rb +53 -0
- metadata +50 -169
- data/.cursor/rules/dependencies.mdc +0 -19
- data/.cursor/rules/examples.mdc +0 -16
- data/.cursor/rules/git.mdc +0 -14
- data/.cursor/rules/project_structure.mdc +0 -30
- data/.cursor/rules/publish_gem.mdc +0 -73
- data/.cursor/rules/security.mdc +0 -14
- data/.cursor/rules/style.mdc +0 -15
- data/.cursor/rules/testing.mdc +0 -16
- data/.overcommit.yml +0 -35
- data/CHANGELOG.md +0 -164
- data/LICENSE.txt +0 -21
- data/PUBLISHING.md +0 -111
- data/examples/basic_example.md +0 -146
- data/examples/including_related_resources.md +0 -491
- data/examples/pagination.md +0 -303
- data/examples/relationships.md +0 -114
- data/examples/resource_attribute_configuration.md +0 -147
- data/examples/resource_meta_configuration.md +0 -244
- data/examples/rspec_testing.md +0 -130
- data/examples/single_table_inheritance.md +0 -160
- data/lib/jpie/configuration.rb +0 -12
- data/lib/jpie/controller/crud_actions.rb +0 -141
- data/lib/jpie/controller/error_handling/handler_setup.rb +0 -124
- data/lib/jpie/controller/error_handling/handlers.rb +0 -109
- data/lib/jpie/controller/error_handling.rb +0 -23
- data/lib/jpie/controller/json_api_validation.rb +0 -193
- data/lib/jpie/controller/parameter_parsing.rb +0 -78
- data/lib/jpie/controller/related_actions.rb +0 -45
- data/lib/jpie/controller/relationship_actions.rb +0 -291
- data/lib/jpie/controller/relationship_validation.rb +0 -117
- data/lib/jpie/controller/rendering.rb +0 -154
- data/lib/jpie/controller.rb +0 -45
- data/lib/jpie/deserializer.rb +0 -110
- data/lib/jpie/errors.rb +0 -117
- data/lib/jpie/generators/resource_generator.rb +0 -116
- data/lib/jpie/generators/templates/resource.rb.erb +0 -31
- data/lib/jpie/railtie.rb +0 -42
- data/lib/jpie/resource/attributable.rb +0 -112
- data/lib/jpie/resource/inferrable.rb +0 -43
- data/lib/jpie/resource/sortable.rb +0 -93
- data/lib/jpie/resource.rb +0 -147
- data/lib/jpie/routing.rb +0 -59
- data/lib/jpie/serializer.rb +0 -205
data/lib/jpie/deserializer.rb
DELETED
|
@@ -1,110 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module JPie
|
|
4
|
-
class Deserializer
|
|
5
|
-
attr_reader :resource_class, :options
|
|
6
|
-
|
|
7
|
-
def initialize(resource_class, options = {})
|
|
8
|
-
@resource_class = resource_class
|
|
9
|
-
@options = options
|
|
10
|
-
end
|
|
11
|
-
|
|
12
|
-
def deserialize(json_data, context = {})
|
|
13
|
-
data = json_data.is_a?(String) ? JSON.parse(json_data) : json_data
|
|
14
|
-
|
|
15
|
-
validate_json_api_structure!(data)
|
|
16
|
-
|
|
17
|
-
if data['data'].is_a?(Array)
|
|
18
|
-
deserialize_collection(data['data'], context)
|
|
19
|
-
else
|
|
20
|
-
deserialize_single(data['data'], context)
|
|
21
|
-
end
|
|
22
|
-
rescue JSON::ParserError => e
|
|
23
|
-
raise Errors::BadRequestError.new(detail: "Invalid JSON: #{e.message}")
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
private
|
|
27
|
-
|
|
28
|
-
def deserialize_single(resource_data, context)
|
|
29
|
-
validate_resource_data!(resource_data)
|
|
30
|
-
extract_attributes(resource_data, context)
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
def deserialize_collection(resources_data, context)
|
|
34
|
-
resources_data.map { deserialize_single(it, context) }
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
def extract_attributes(resource_data, _context)
|
|
38
|
-
attributes = resource_data['attributes'] || {}
|
|
39
|
-
type = resource_data['type']
|
|
40
|
-
id = resource_data['id']
|
|
41
|
-
|
|
42
|
-
validate_type!(type) if type
|
|
43
|
-
|
|
44
|
-
model_attributes = transform_attribute_keys(attributes)
|
|
45
|
-
filtered_attributes = filter_allowed_attributes(model_attributes)
|
|
46
|
-
result = deserialize_attribute_values(filtered_attributes)
|
|
47
|
-
|
|
48
|
-
add_id_if_present(result, id)
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
def transform_attribute_keys(attributes)
|
|
52
|
-
attributes.transform_keys { it.to_s.underscore }
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
def filter_allowed_attributes(model_attributes)
|
|
56
|
-
allowed_attributes = resource_class._attributes.map(&:to_s)
|
|
57
|
-
model_attributes.slice(*allowed_attributes)
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
def deserialize_attribute_values(attributes)
|
|
61
|
-
attributes.transform_values { deserialize_value(it) }
|
|
62
|
-
end
|
|
63
|
-
|
|
64
|
-
def add_id_if_present(result, id)
|
|
65
|
-
result['id'] = id if id
|
|
66
|
-
result.with_indifferent_access
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
def deserialize_value(value)
|
|
70
|
-
case value
|
|
71
|
-
when String
|
|
72
|
-
# Only try to parse as datetime if it looks like an ISO8601 string
|
|
73
|
-
if value.match?(/\A\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/)
|
|
74
|
-
begin
|
|
75
|
-
Time.parse(value)
|
|
76
|
-
rescue ArgumentError, TypeError
|
|
77
|
-
value
|
|
78
|
-
end
|
|
79
|
-
else
|
|
80
|
-
value
|
|
81
|
-
end
|
|
82
|
-
else
|
|
83
|
-
value
|
|
84
|
-
end
|
|
85
|
-
end
|
|
86
|
-
|
|
87
|
-
def validate_json_api_structure!(data)
|
|
88
|
-
return if data.is_a?(Hash) && data.key?('data')
|
|
89
|
-
|
|
90
|
-
raise Errors::BadRequestError.new(detail: 'Invalid JSON:API structure. Missing "data" key.')
|
|
91
|
-
end
|
|
92
|
-
|
|
93
|
-
def validate_resource_data!(resource_data)
|
|
94
|
-
raise Errors::BadRequestError.new(detail: 'Invalid resource data structure.') unless resource_data.is_a?(Hash)
|
|
95
|
-
|
|
96
|
-
return if resource_data.key?('type')
|
|
97
|
-
|
|
98
|
-
raise Errors::BadRequestError.new(detail: 'Resource data must include "type".')
|
|
99
|
-
end
|
|
100
|
-
|
|
101
|
-
def validate_type!(type)
|
|
102
|
-
expected_type = resource_class.type
|
|
103
|
-
return if type == expected_type
|
|
104
|
-
|
|
105
|
-
raise Errors::BadRequestError.new(
|
|
106
|
-
detail: "Expected type '#{expected_type}', got '#{type}'"
|
|
107
|
-
)
|
|
108
|
-
end
|
|
109
|
-
end
|
|
110
|
-
end
|
data/lib/jpie/errors.rb
DELETED
|
@@ -1,117 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module JPie
|
|
4
|
-
module Errors
|
|
5
|
-
class Error < StandardError
|
|
6
|
-
attr_reader :status, :code, :title, :detail, :source
|
|
7
|
-
|
|
8
|
-
def initialize(status:, code: nil, title: nil, detail: nil, source: nil)
|
|
9
|
-
@status = status
|
|
10
|
-
@code = code
|
|
11
|
-
@title = title
|
|
12
|
-
@detail = detail
|
|
13
|
-
@source = source
|
|
14
|
-
super(detail || title || 'An error occurred')
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
def to_hash
|
|
18
|
-
{
|
|
19
|
-
status: status.to_s,
|
|
20
|
-
code:,
|
|
21
|
-
title:,
|
|
22
|
-
detail:,
|
|
23
|
-
source:
|
|
24
|
-
}.compact
|
|
25
|
-
end
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
class ValidationError < Error
|
|
29
|
-
def initialize(detail:, source: nil)
|
|
30
|
-
super(status: 422, title: 'Validation Error', detail:, source:)
|
|
31
|
-
end
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
class NotFoundError < Error
|
|
35
|
-
def initialize(detail: 'Resource not found')
|
|
36
|
-
super(status: 404, title: 'Not Found', detail:)
|
|
37
|
-
end
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
class BadRequestError < Error
|
|
41
|
-
def initialize(detail: 'Bad Request')
|
|
42
|
-
super(status: 400, title: 'Bad Request', detail:)
|
|
43
|
-
end
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
class UnauthorizedError < Error
|
|
47
|
-
def initialize(detail: 'Unauthorized')
|
|
48
|
-
super(status: 401, title: 'Unauthorized', detail:)
|
|
49
|
-
end
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
class ForbiddenError < Error
|
|
53
|
-
def initialize(detail: 'Forbidden')
|
|
54
|
-
super(status: 403, title: 'Forbidden', detail:)
|
|
55
|
-
end
|
|
56
|
-
end
|
|
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
|
-
|
|
64
|
-
class InternalServerError < Error
|
|
65
|
-
def initialize(detail: 'Internal Server Error')
|
|
66
|
-
super(status: 500, title: 'Internal Server Error', detail:)
|
|
67
|
-
end
|
|
68
|
-
end
|
|
69
|
-
|
|
70
|
-
class ResourceError < Error
|
|
71
|
-
def initialize(detail:)
|
|
72
|
-
super(status: 500, title: 'Resource Error', detail:)
|
|
73
|
-
end
|
|
74
|
-
end
|
|
75
|
-
|
|
76
|
-
# JSON:API Compliance Errors
|
|
77
|
-
class InvalidJsonApiRequestError < BadRequestError
|
|
78
|
-
def initialize(detail: 'Request is not JSON:API compliant')
|
|
79
|
-
super
|
|
80
|
-
end
|
|
81
|
-
end
|
|
82
|
-
|
|
83
|
-
class UnsupportedIncludeError < BadRequestError
|
|
84
|
-
def initialize(include_path:, supported_includes: [])
|
|
85
|
-
detail = if supported_includes.any?
|
|
86
|
-
"Unsupported include '#{include_path}'. Supported includes: #{supported_includes.join(', ')}"
|
|
87
|
-
else
|
|
88
|
-
"Unsupported include '#{include_path}'. No includes are supported for this resource"
|
|
89
|
-
end
|
|
90
|
-
super(detail: detail)
|
|
91
|
-
end
|
|
92
|
-
end
|
|
93
|
-
|
|
94
|
-
class UnsupportedSortFieldError < BadRequestError
|
|
95
|
-
def initialize(sort_field:, supported_fields: [])
|
|
96
|
-
detail = if supported_fields.any?
|
|
97
|
-
"Unsupported sort field '#{sort_field}'. Supported fields: #{supported_fields.join(', ')}"
|
|
98
|
-
else
|
|
99
|
-
"Unsupported sort field '#{sort_field}'. No sorting is supported for this resource"
|
|
100
|
-
end
|
|
101
|
-
super(detail: detail)
|
|
102
|
-
end
|
|
103
|
-
end
|
|
104
|
-
|
|
105
|
-
class InvalidSortParameterError < BadRequestError
|
|
106
|
-
def initialize(detail: 'Invalid sort parameter format')
|
|
107
|
-
super
|
|
108
|
-
end
|
|
109
|
-
end
|
|
110
|
-
|
|
111
|
-
class InvalidIncludeParameterError < BadRequestError
|
|
112
|
-
def initialize(detail: 'Invalid include parameter format')
|
|
113
|
-
super
|
|
114
|
-
end
|
|
115
|
-
end
|
|
116
|
-
end
|
|
117
|
-
end
|
|
@@ -1,116 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'rails/generators/base'
|
|
4
|
-
|
|
5
|
-
module JPie
|
|
6
|
-
module Generators
|
|
7
|
-
class ResourceGenerator < Rails::Generators::NamedBase
|
|
8
|
-
source_root File.expand_path('templates', __dir__)
|
|
9
|
-
|
|
10
|
-
desc 'Generate a JPie resource class with semantic field definitions'
|
|
11
|
-
|
|
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)'
|
|
19
|
-
|
|
20
|
-
def create_resource_file
|
|
21
|
-
template 'resource.rb.erb', File.join('app/resources', "#{file_name}_resource.rb")
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
private
|
|
25
|
-
|
|
26
|
-
def model_class_name
|
|
27
|
-
options[:model] || class_name
|
|
28
|
-
end
|
|
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
|
-
|
|
41
|
-
def resource_attributes
|
|
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
|
|
90
|
-
|
|
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
|
|
98
|
-
end
|
|
99
|
-
|
|
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
|
|
107
|
-
end
|
|
108
|
-
|
|
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)
|
|
113
|
-
end
|
|
114
|
-
end
|
|
115
|
-
end
|
|
116
|
-
end
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
class <%= class_name %>Resource < JPie::Resource
|
|
4
|
-
<% if needs_explicit_model? -%>
|
|
5
|
-
model <%= model_class_name %>
|
|
6
|
-
<% end -%>
|
|
7
|
-
|
|
8
|
-
<% if resource_attributes.any? -%>
|
|
9
|
-
attributes <%= resource_attributes.map { |attr| ":#{attr}" }.join(', ') %>
|
|
10
|
-
<% else -%>
|
|
11
|
-
# Define your attributes here:
|
|
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
|
|
30
|
-
<% end -%>
|
|
31
|
-
end
|
data/lib/jpie/railtie.rb
DELETED
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'rails/railtie'
|
|
4
|
-
|
|
5
|
-
module JPie
|
|
6
|
-
class Railtie < Rails::Railtie
|
|
7
|
-
railtie_name :jpie
|
|
8
|
-
|
|
9
|
-
config.jpie = ActiveSupport::OrderedOptions.new
|
|
10
|
-
|
|
11
|
-
# Configure Rails inflections to preserve JPie casing
|
|
12
|
-
initializer 'jpie.inflections' do
|
|
13
|
-
ActiveSupport::Inflector.inflections(:en) do |inflect|
|
|
14
|
-
inflect.acronym 'JPie'
|
|
15
|
-
end
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
initializer 'jpie.configure' do |app|
|
|
19
|
-
JPie.configure do |config|
|
|
20
|
-
app.config.jpie.each do |key, value|
|
|
21
|
-
config.public_send("#{key}=", value) if config.respond_to?("#{key}=")
|
|
22
|
-
end
|
|
23
|
-
end
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
initializer 'jpie.action_controller' do
|
|
27
|
-
ActiveSupport.on_load(:action_controller) do
|
|
28
|
-
extend JPie::Controller::ClassMethods if defined?(JPie::Controller::ClassMethods)
|
|
29
|
-
end
|
|
30
|
-
end
|
|
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
|
-
|
|
38
|
-
generators do
|
|
39
|
-
require 'jpie/generators/resource_generator'
|
|
40
|
-
end
|
|
41
|
-
end
|
|
42
|
-
end
|
|
@@ -1,112 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module JPie
|
|
4
|
-
class Resource
|
|
5
|
-
module Attributable
|
|
6
|
-
extend ActiveSupport::Concern
|
|
7
|
-
|
|
8
|
-
included do
|
|
9
|
-
class_attribute :_attributes, :_relationships, :_meta_attributes
|
|
10
|
-
self._attributes = []
|
|
11
|
-
self._relationships = {}
|
|
12
|
-
self._meta_attributes = []
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
class_methods do
|
|
16
|
-
def attribute(name, options = {}, &)
|
|
17
|
-
name = name.to_sym
|
|
18
|
-
_attributes << name unless _attributes.include?(name)
|
|
19
|
-
define_attribute_method(name, options, &)
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
def attributes(*names)
|
|
23
|
-
names.each { attribute(it) }
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
def meta_attribute(name, options = {}, &)
|
|
27
|
-
name = name.to_sym
|
|
28
|
-
_meta_attributes << name unless _meta_attributes.include?(name)
|
|
29
|
-
define_attribute_method(name, options, &)
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
def meta_attributes(*names)
|
|
33
|
-
names.each { meta_attribute(it) }
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
# More concise aliases for modern Rails style
|
|
37
|
-
alias_method :meta, :meta_attribute
|
|
38
|
-
alias_method :metas, :meta_attributes
|
|
39
|
-
|
|
40
|
-
def relationship(name, options = {})
|
|
41
|
-
name = name.to_sym
|
|
42
|
-
_relationships[name] = options
|
|
43
|
-
|
|
44
|
-
# Check if method is already defined (public or private) to allow custom implementations
|
|
45
|
-
return if method_defined?(name) || private_method_defined?(name)
|
|
46
|
-
|
|
47
|
-
define_method(name) do
|
|
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
|
|
56
|
-
end
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
def has_many(name, options = {})
|
|
60
|
-
name = name.to_sym
|
|
61
|
-
resource_class_name = options[:resource] || infer_resource_class_name(name)
|
|
62
|
-
relationship(name, { type: :has_many, resource: resource_class_name }.merge(options))
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
def has_one(name, options = {})
|
|
66
|
-
name = name.to_sym
|
|
67
|
-
resource_class_name = options[:resource] || infer_resource_class_name(name)
|
|
68
|
-
relationship(name, { type: :has_one, resource: resource_class_name }.merge(options))
|
|
69
|
-
end
|
|
70
|
-
|
|
71
|
-
private
|
|
72
|
-
|
|
73
|
-
def define_attribute_method(name, options, &)
|
|
74
|
-
# If a block is provided, use it (existing behavior)
|
|
75
|
-
if block_given?
|
|
76
|
-
define_method(name) do
|
|
77
|
-
instance_exec(&)
|
|
78
|
-
end
|
|
79
|
-
# If options[:block] is provided, use it (existing behavior)
|
|
80
|
-
elsif options[:block]
|
|
81
|
-
define_method(name) do
|
|
82
|
-
instance_exec(&options[:block])
|
|
83
|
-
end
|
|
84
|
-
# If method is not already defined on the resource (public or private), define the default implementation
|
|
85
|
-
elsif !method_defined?(name) && !private_method_defined?(name)
|
|
86
|
-
define_method(name) do
|
|
87
|
-
attr_name = options[:attr] || name
|
|
88
|
-
@object.public_send(attr_name)
|
|
89
|
-
end
|
|
90
|
-
end
|
|
91
|
-
# If method is already defined, don't override it - let the custom method handle it
|
|
92
|
-
end
|
|
93
|
-
|
|
94
|
-
def infer_resource_class_name(relationship_name)
|
|
95
|
-
# Convert relationship name to resource class name
|
|
96
|
-
# :posts -> "PostResource"
|
|
97
|
-
# :user -> "UserResource"
|
|
98
|
-
singularized_name = relationship_name.to_s.singularize
|
|
99
|
-
"#{singularized_name.camelize}Resource"
|
|
100
|
-
end
|
|
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
|
|
110
|
-
end
|
|
111
|
-
end
|
|
112
|
-
end
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module JPie
|
|
4
|
-
class Resource
|
|
5
|
-
module Inferrable
|
|
6
|
-
extend ActiveSupport::Concern
|
|
7
|
-
|
|
8
|
-
included do
|
|
9
|
-
class_attribute :_type, :_model_class
|
|
10
|
-
end
|
|
11
|
-
|
|
12
|
-
class_methods do
|
|
13
|
-
def model(model_class = nil)
|
|
14
|
-
if model_class
|
|
15
|
-
self._model_class = model_class
|
|
16
|
-
else
|
|
17
|
-
_model_class || infer_model_class
|
|
18
|
-
end
|
|
19
|
-
end
|
|
20
|
-
|
|
21
|
-
def type(type_name = nil)
|
|
22
|
-
if type_name
|
|
23
|
-
self._type = type_name.to_s
|
|
24
|
-
else
|
|
25
|
-
_type || infer_type_name
|
|
26
|
-
end
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
private
|
|
30
|
-
|
|
31
|
-
def infer_model_class
|
|
32
|
-
name.chomp('Resource').constantize
|
|
33
|
-
rescue NameError
|
|
34
|
-
nil
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
def infer_type_name
|
|
38
|
-
model&.model_name&.plural || name.chomp('Resource').underscore.pluralize
|
|
39
|
-
end
|
|
40
|
-
end
|
|
41
|
-
end
|
|
42
|
-
end
|
|
43
|
-
end
|
|
@@ -1,93 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module JPie
|
|
4
|
-
class Resource
|
|
5
|
-
module Sortable
|
|
6
|
-
extend ActiveSupport::Concern
|
|
7
|
-
|
|
8
|
-
included do
|
|
9
|
-
class_attribute :_sortable_fields
|
|
10
|
-
self._sortable_fields = {}
|
|
11
|
-
end
|
|
12
|
-
|
|
13
|
-
class_methods do
|
|
14
|
-
# Define a sortable field with optional custom sorting logic
|
|
15
|
-
# Example:
|
|
16
|
-
# sortable_by :name
|
|
17
|
-
# sortable_by :created_at, :created_at_desc
|
|
18
|
-
# sortable_by :popularity do |direction|
|
|
19
|
-
# if direction == :asc
|
|
20
|
-
# model.order(:likes_count)
|
|
21
|
-
# else
|
|
22
|
-
# model.order(likes_count: :desc)
|
|
23
|
-
# end
|
|
24
|
-
# end
|
|
25
|
-
def sortable_by(field, column = nil, &block)
|
|
26
|
-
field = field.to_sym
|
|
27
|
-
_sortable_fields[field] = block || column || field
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
# More concise alias for modern Rails style
|
|
31
|
-
alias_method :sortable, :sortable_by
|
|
32
|
-
|
|
33
|
-
# Apply sorting to a query based on sort parameters
|
|
34
|
-
# sort_fields: array of sort field strings (e.g., ['name', '-created_at'])
|
|
35
|
-
def sort(query, sort_fields)
|
|
36
|
-
return query if sort_fields.blank?
|
|
37
|
-
|
|
38
|
-
sort_fields.each do |sort_field|
|
|
39
|
-
# Parse direction (- prefix means descending)
|
|
40
|
-
if sort_field.start_with?('-')
|
|
41
|
-
field = sort_field[1..].to_sym
|
|
42
|
-
direction = :desc
|
|
43
|
-
else
|
|
44
|
-
field = sort_field.to_sym
|
|
45
|
-
direction = :asc
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
# Check if field is sortable
|
|
49
|
-
unless sortable_field?(field)
|
|
50
|
-
raise JPie::Errors::BadRequestError.new(
|
|
51
|
-
detail: "Invalid sort field: #{field}. Sortable fields are: #{sortable_fields.join(', ')}"
|
|
52
|
-
)
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
# Apply sorting
|
|
56
|
-
query = apply_sort(query, field, direction)
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
query
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
# Get list of all sortable fields (attributes + explicitly defined sortable fields)
|
|
63
|
-
def sortable_fields
|
|
64
|
-
(_attributes + _sortable_fields.keys).uniq.map(&:to_s)
|
|
65
|
-
end
|
|
66
|
-
|
|
67
|
-
# Check if a field is sortable
|
|
68
|
-
def sortable_field?(field)
|
|
69
|
-
field = field.to_sym
|
|
70
|
-
_attributes.include?(field) || _sortable_fields.key?(field)
|
|
71
|
-
end
|
|
72
|
-
|
|
73
|
-
private
|
|
74
|
-
|
|
75
|
-
# Apply a single sort to the query
|
|
76
|
-
def apply_sort(query, field, direction)
|
|
77
|
-
sortable_config = _sortable_fields[field]
|
|
78
|
-
|
|
79
|
-
if sortable_config.is_a?(Proc)
|
|
80
|
-
# Custom sorting block
|
|
81
|
-
instance_exec(query, direction, &sortable_config)
|
|
82
|
-
elsif sortable_config.is_a?(Symbol)
|
|
83
|
-
# Custom column name
|
|
84
|
-
query.order(sortable_config => direction)
|
|
85
|
-
else
|
|
86
|
-
# Default sorting by attribute name
|
|
87
|
-
query.order(field => direction)
|
|
88
|
-
end
|
|
89
|
-
end
|
|
90
|
-
end
|
|
91
|
-
end
|
|
92
|
-
end
|
|
93
|
-
end
|