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/resource.rb
DELETED
|
@@ -1,147 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require_relative 'resource/attributable'
|
|
4
|
-
require_relative 'resource/inferrable'
|
|
5
|
-
require_relative 'resource/sortable'
|
|
6
|
-
require_relative 'errors'
|
|
7
|
-
|
|
8
|
-
module JPie
|
|
9
|
-
class Resource
|
|
10
|
-
include ActiveSupport::Configurable
|
|
11
|
-
include Attributable
|
|
12
|
-
include Inferrable
|
|
13
|
-
include Sortable
|
|
14
|
-
|
|
15
|
-
class << self
|
|
16
|
-
def inherited(subclass)
|
|
17
|
-
super
|
|
18
|
-
subclass._attributes = _attributes.dup
|
|
19
|
-
subclass._relationships = _relationships.dup
|
|
20
|
-
subclass._meta_attributes = _meta_attributes.dup
|
|
21
|
-
subclass._sortable_fields = _sortable_fields.dup
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
# Default scope method that returns all records
|
|
25
|
-
# Override this in your resource classes to provide authorization scoping
|
|
26
|
-
# Example:
|
|
27
|
-
# def self.scope(context)
|
|
28
|
-
# current_user = context[:current_user]
|
|
29
|
-
# return model.none unless current_user
|
|
30
|
-
#
|
|
31
|
-
# if current_user.admin?
|
|
32
|
-
# model.all
|
|
33
|
-
# else
|
|
34
|
-
# model.where(user: current_user)
|
|
35
|
-
# end
|
|
36
|
-
# end
|
|
37
|
-
def scope(_context = {})
|
|
38
|
-
model.all
|
|
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
|
|
80
|
-
end
|
|
81
|
-
|
|
82
|
-
attr_reader :object, :context
|
|
83
|
-
|
|
84
|
-
def initialize(object, context = {})
|
|
85
|
-
@object = object
|
|
86
|
-
@context = context
|
|
87
|
-
end
|
|
88
|
-
|
|
89
|
-
delegate :id, to: :@object
|
|
90
|
-
|
|
91
|
-
delegate :type, to: :class
|
|
92
|
-
|
|
93
|
-
def attributes_hash
|
|
94
|
-
self.class._attributes.index_with do
|
|
95
|
-
send(it)
|
|
96
|
-
end
|
|
97
|
-
end
|
|
98
|
-
|
|
99
|
-
def meta_hash
|
|
100
|
-
# Start with meta attributes from the macro
|
|
101
|
-
base_meta = self.class._meta_attributes.index_with do
|
|
102
|
-
send(it)
|
|
103
|
-
end
|
|
104
|
-
|
|
105
|
-
# Check if the resource defines a custom meta method
|
|
106
|
-
if respond_to?(:meta, true) && method(:meta).owner != JPie::Resource
|
|
107
|
-
custom_meta = meta
|
|
108
|
-
|
|
109
|
-
# Validate that meta method returns a hash
|
|
110
|
-
unless custom_meta.is_a?(Hash)
|
|
111
|
-
raise JPie::Errors::ResourceError.new(
|
|
112
|
-
detail: "meta method must return a Hash, got #{custom_meta.class}"
|
|
113
|
-
)
|
|
114
|
-
end
|
|
115
|
-
|
|
116
|
-
# Merge custom meta with base meta (custom meta takes precedence)
|
|
117
|
-
base_meta.merge(custom_meta)
|
|
118
|
-
else
|
|
119
|
-
base_meta
|
|
120
|
-
end
|
|
121
|
-
end
|
|
122
|
-
|
|
123
|
-
protected
|
|
124
|
-
|
|
125
|
-
# Default meta method that returns the meta attributes
|
|
126
|
-
# This can be overridden in subclasses
|
|
127
|
-
def meta
|
|
128
|
-
self.class._meta_attributes.index_with do
|
|
129
|
-
send(it)
|
|
130
|
-
end
|
|
131
|
-
end
|
|
132
|
-
|
|
133
|
-
private
|
|
134
|
-
|
|
135
|
-
def method_missing(method_name, *, &)
|
|
136
|
-
if @object.respond_to?(method_name)
|
|
137
|
-
@object.public_send(method_name, *, &)
|
|
138
|
-
else
|
|
139
|
-
super
|
|
140
|
-
end
|
|
141
|
-
end
|
|
142
|
-
|
|
143
|
-
def respond_to_missing?(method_name, include_private = false)
|
|
144
|
-
@object.respond_to?(method_name, include_private) || super
|
|
145
|
-
end
|
|
146
|
-
end
|
|
147
|
-
end
|
data/lib/jpie/routing.rb
DELETED
|
@@ -1,59 +0,0 @@
|
|
|
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/serializer.rb
DELETED
|
@@ -1,205 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module JPie
|
|
4
|
-
class Serializer
|
|
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 serialize(objects, context = {}, includes: [])
|
|
13
|
-
return { data: nil } if objects.nil?
|
|
14
|
-
|
|
15
|
-
resources = build_resources(objects, context)
|
|
16
|
-
result = serialize_data(objects, resources)
|
|
17
|
-
add_included_data(result, resources, includes, context) if should_include_data?(includes, result)
|
|
18
|
-
result
|
|
19
|
-
end
|
|
20
|
-
|
|
21
|
-
private
|
|
22
|
-
|
|
23
|
-
def build_resources(objects, context)
|
|
24
|
-
Array(objects).filter_map { |obj| obj ? resource_class.new(obj, context) : nil }
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
def serialize_data(objects, resources)
|
|
28
|
-
if objects.is_a?(Array) || objects.respond_to?(:each)
|
|
29
|
-
serialize_collection(resources)
|
|
30
|
-
else
|
|
31
|
-
resources.first ? serialize_single(resources.first) : { data: nil }
|
|
32
|
-
end
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
def should_include_data?(includes, result)
|
|
36
|
-
includes.any? && result[:data]
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
def add_included_data(result, resources, includes, context)
|
|
40
|
-
included_data = collect_included_data(resources, includes, context)
|
|
41
|
-
result[:included] = included_data
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
def serialize_single(resource)
|
|
45
|
-
{
|
|
46
|
-
data: serialize_resource_data(resource)
|
|
47
|
-
}
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
def serialize_collection(resources)
|
|
51
|
-
{
|
|
52
|
-
data: resources.map { serialize_resource_data(it) }
|
|
53
|
-
}
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
def serialize_resource_data(resource)
|
|
57
|
-
data = {
|
|
58
|
-
id: resource.id.to_s,
|
|
59
|
-
type: resource.type,
|
|
60
|
-
attributes: serialize_attributes(resource)
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
meta_data = serialize_meta(resource)
|
|
64
|
-
data[:meta] = meta_data if meta_data.any?
|
|
65
|
-
|
|
66
|
-
data.compact
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
def serialize_attributes(resource)
|
|
70
|
-
attributes = resource.attributes_hash
|
|
71
|
-
return {} if attributes.empty?
|
|
72
|
-
|
|
73
|
-
attributes.transform_keys { it.to_s.underscore }
|
|
74
|
-
.transform_values { serialize_value(it) }
|
|
75
|
-
end
|
|
76
|
-
|
|
77
|
-
def serialize_meta(resource)
|
|
78
|
-
meta_attributes = resource.meta_hash
|
|
79
|
-
return {} if meta_attributes.empty?
|
|
80
|
-
|
|
81
|
-
meta_attributes.transform_keys { it.to_s.underscore }
|
|
82
|
-
.transform_values { serialize_value(it) }
|
|
83
|
-
end
|
|
84
|
-
|
|
85
|
-
def serialize_value(value)
|
|
86
|
-
value.respond_to?(:iso8601) ? value.iso8601 : value
|
|
87
|
-
end
|
|
88
|
-
|
|
89
|
-
def collect_included_data(resources, includes, context)
|
|
90
|
-
processor = IncludeProcessor.new(self, context)
|
|
91
|
-
processor.process(resources, includes)
|
|
92
|
-
end
|
|
93
|
-
|
|
94
|
-
def parse_nested_includes(includes)
|
|
95
|
-
result = {}
|
|
96
|
-
|
|
97
|
-
includes.each do |include_path|
|
|
98
|
-
parts = include_path.split('.')
|
|
99
|
-
top_level = parts.first
|
|
100
|
-
nested_path = parts[1..].join('.') if parts.length > 1
|
|
101
|
-
|
|
102
|
-
result[top_level] ||= []
|
|
103
|
-
result[top_level] << nested_path if nested_path.present?
|
|
104
|
-
end
|
|
105
|
-
|
|
106
|
-
result
|
|
107
|
-
end
|
|
108
|
-
|
|
109
|
-
# Helper class to manage include processing state and reduce parameter passing
|
|
110
|
-
class IncludeProcessor
|
|
111
|
-
attr_reader :serializer, :context, :included, :processed_includes
|
|
112
|
-
|
|
113
|
-
def initialize(serializer, context)
|
|
114
|
-
@serializer = serializer
|
|
115
|
-
@context = context
|
|
116
|
-
@included = []
|
|
117
|
-
@processed_includes = {}
|
|
118
|
-
end
|
|
119
|
-
|
|
120
|
-
def process(resources, includes)
|
|
121
|
-
parsed_includes = serializer.send(:parse_nested_includes, includes)
|
|
122
|
-
parsed_includes.each do |include_name, nested_includes|
|
|
123
|
-
process_single_include(include_name, nested_includes, resources)
|
|
124
|
-
end
|
|
125
|
-
included
|
|
126
|
-
end
|
|
127
|
-
|
|
128
|
-
private
|
|
129
|
-
|
|
130
|
-
def process_single_include(include_name, nested_includes, resources)
|
|
131
|
-
include_sym = include_name.to_sym
|
|
132
|
-
relationship_options = serializer.resource_class._relationships[include_sym]
|
|
133
|
-
return unless relationship_options
|
|
134
|
-
|
|
135
|
-
resources.each do |resource|
|
|
136
|
-
process_resource_relationships(resource, include_sym, relationship_options, nested_includes)
|
|
137
|
-
end
|
|
138
|
-
end
|
|
139
|
-
|
|
140
|
-
def process_resource_relationships(resource, include_sym, relationship_options, nested_includes)
|
|
141
|
-
related_objects = resource.public_send(include_sym)
|
|
142
|
-
return unless related_objects
|
|
143
|
-
|
|
144
|
-
Array(related_objects).each do |related_object|
|
|
145
|
-
process_related_object(related_object, relationship_options, nested_includes)
|
|
146
|
-
end
|
|
147
|
-
end
|
|
148
|
-
|
|
149
|
-
def process_related_object(related_object, relationship_options, nested_includes)
|
|
150
|
-
related_resource_class = serializer.send(:determine_resource_class, related_object, relationship_options)
|
|
151
|
-
return unless related_resource_class
|
|
152
|
-
|
|
153
|
-
related_resource = related_resource_class.new(related_object, context)
|
|
154
|
-
resource_data = serializer.send(:serialize_resource_data, related_resource)
|
|
155
|
-
|
|
156
|
-
add_to_included_if_unique(resource_data)
|
|
157
|
-
process_nested_includes_for_resource(related_resource_class, related_resource, nested_includes)
|
|
158
|
-
end
|
|
159
|
-
|
|
160
|
-
def add_to_included_if_unique(resource_data)
|
|
161
|
-
key = [resource_data[:type], resource_data[:id]]
|
|
162
|
-
return if processed_includes[key]
|
|
163
|
-
|
|
164
|
-
processed_includes[key] = true
|
|
165
|
-
included << resource_data
|
|
166
|
-
end
|
|
167
|
-
|
|
168
|
-
def process_nested_includes_for_resource(related_resource_class, related_resource, nested_includes)
|
|
169
|
-
return unless nested_includes.any?
|
|
170
|
-
|
|
171
|
-
nested_serializer = JPie::Serializer.new(related_resource_class)
|
|
172
|
-
nested_included = nested_serializer.send(:collect_included_data, [related_resource], nested_includes, context)
|
|
173
|
-
|
|
174
|
-
nested_included.each do |nested_item|
|
|
175
|
-
add_to_included_if_unique(nested_item)
|
|
176
|
-
end
|
|
177
|
-
end
|
|
178
|
-
end
|
|
179
|
-
|
|
180
|
-
def determine_resource_class(object, relationship_options)
|
|
181
|
-
# First try the explicitly specified resource class
|
|
182
|
-
if relationship_options[:resource]
|
|
183
|
-
begin
|
|
184
|
-
return relationship_options[:resource].constantize
|
|
185
|
-
rescue NameError
|
|
186
|
-
# If the resource class doesn't exist, it might be a polymorphic relationship
|
|
187
|
-
# Fall through to polymorphic detection
|
|
188
|
-
end
|
|
189
|
-
end
|
|
190
|
-
|
|
191
|
-
# For polymorphic relationships, determine resource class from object class
|
|
192
|
-
if object&.class
|
|
193
|
-
resource_class_name = "#{object.class.name}Resource"
|
|
194
|
-
begin
|
|
195
|
-
return resource_class_name.constantize
|
|
196
|
-
rescue NameError
|
|
197
|
-
# Resource class doesn't exist for this object type
|
|
198
|
-
return nil
|
|
199
|
-
end
|
|
200
|
-
end
|
|
201
|
-
|
|
202
|
-
nil
|
|
203
|
-
end
|
|
204
|
-
end
|
|
205
|
-
end
|