jpie 0.4.4 → 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.
Files changed (89) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +21 -0
  3. data/.rspec +3 -0
  4. data/.rubocop.yml +35 -110
  5. data/.travis.yml +7 -0
  6. data/Gemfile +21 -0
  7. data/Gemfile.lock +312 -0
  8. data/README.md +2072 -140
  9. data/Rakefile +3 -14
  10. data/bin/console +15 -0
  11. data/bin/setup +8 -0
  12. data/jpie.gemspec +18 -35
  13. data/kiln/app/resources/user_message_resource.rb +2 -0
  14. data/lib/jpie.rb +3 -28
  15. data/lib/json_api/active_storage/deserialization.rb +106 -0
  16. data/lib/json_api/active_storage/detection.rb +74 -0
  17. data/lib/json_api/active_storage/serialization.rb +32 -0
  18. data/lib/json_api/configuration.rb +58 -0
  19. data/lib/json_api/controllers/base_controller.rb +26 -0
  20. data/lib/json_api/controllers/concerns/controller_helpers.rb +223 -0
  21. data/lib/json_api/controllers/concerns/resource_actions.rb +657 -0
  22. data/lib/json_api/controllers/relationships_controller.rb +504 -0
  23. data/lib/json_api/controllers/resources_controller.rb +6 -0
  24. data/lib/json_api/errors/parameter_not_allowed.rb +19 -0
  25. data/lib/json_api/railtie.rb +75 -0
  26. data/lib/json_api/resources/active_storage_blob_resource.rb +11 -0
  27. data/lib/json_api/resources/resource.rb +238 -0
  28. data/lib/json_api/resources/resource_loader.rb +35 -0
  29. data/lib/json_api/routing.rb +72 -0
  30. data/lib/json_api/serialization/deserializer.rb +362 -0
  31. data/lib/json_api/serialization/serializer.rb +320 -0
  32. data/lib/json_api/support/active_storage_support.rb +85 -0
  33. data/lib/json_api/support/collection_query.rb +406 -0
  34. data/lib/json_api/support/instrumentation.rb +42 -0
  35. data/lib/json_api/support/param_helpers.rb +51 -0
  36. data/lib/json_api/support/relationship_guard.rb +16 -0
  37. data/lib/json_api/support/relationship_helpers.rb +74 -0
  38. data/lib/json_api/support/resource_identifier.rb +87 -0
  39. data/lib/json_api/support/responders.rb +100 -0
  40. data/lib/json_api/support/response_helpers.rb +10 -0
  41. data/lib/json_api/support/sort_parsing.rb +21 -0
  42. data/lib/json_api/support/type_conversion.rb +21 -0
  43. data/lib/json_api/testing/test_helper.rb +76 -0
  44. data/lib/json_api/testing.rb +3 -0
  45. data/lib/{jpie → json_api}/version.rb +2 -2
  46. data/lib/json_api.rb +50 -0
  47. data/lib/rubocop/cop/custom/hash_value_omission.rb +53 -0
  48. metadata +50 -167
  49. data/.cursor/rules/dependencies.mdc +0 -19
  50. data/.cursor/rules/examples.mdc +0 -16
  51. data/.cursor/rules/git.mdc +0 -14
  52. data/.cursor/rules/project_structure.mdc +0 -30
  53. data/.cursor/rules/security.mdc +0 -14
  54. data/.cursor/rules/style.mdc +0 -15
  55. data/.cursor/rules/testing.mdc +0 -16
  56. data/.overcommit.yml +0 -35
  57. data/CHANGELOG.md +0 -164
  58. data/LICENSE.txt +0 -21
  59. data/examples/basic_example.md +0 -146
  60. data/examples/including_related_resources.md +0 -491
  61. data/examples/pagination.md +0 -303
  62. data/examples/relationships.md +0 -114
  63. data/examples/resource_attribute_configuration.md +0 -147
  64. data/examples/resource_meta_configuration.md +0 -244
  65. data/examples/single_table_inheritance.md +0 -160
  66. data/lib/jpie/configuration.rb +0 -12
  67. data/lib/jpie/controller/crud_actions.rb +0 -141
  68. data/lib/jpie/controller/error_handling/handler_setup.rb +0 -124
  69. data/lib/jpie/controller/error_handling/handlers.rb +0 -109
  70. data/lib/jpie/controller/error_handling.rb +0 -23
  71. data/lib/jpie/controller/json_api_validation.rb +0 -193
  72. data/lib/jpie/controller/parameter_parsing.rb +0 -78
  73. data/lib/jpie/controller/related_actions.rb +0 -45
  74. data/lib/jpie/controller/relationship_actions.rb +0 -291
  75. data/lib/jpie/controller/relationship_validation.rb +0 -117
  76. data/lib/jpie/controller/rendering.rb +0 -154
  77. data/lib/jpie/controller.rb +0 -45
  78. data/lib/jpie/deserializer.rb +0 -110
  79. data/lib/jpie/errors.rb +0 -117
  80. data/lib/jpie/generators/resource_generator.rb +0 -116
  81. data/lib/jpie/generators/templates/resource.rb.erb +0 -31
  82. data/lib/jpie/railtie.rb +0 -42
  83. data/lib/jpie/resource/attributable.rb +0 -112
  84. data/lib/jpie/resource/inferrable.rb +0 -43
  85. data/lib/jpie/resource/sortable.rb +0 -93
  86. data/lib/jpie/resource.rb +0 -147
  87. data/lib/jpie/routing.rb +0 -59
  88. data/lib/jpie/rspec.rb +0 -71
  89. 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/rspec.rb DELETED
@@ -1,71 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'rspec/expectations'
4
-
5
- module JPie
6
- # RSpec matchers and helpers for testing JPie resources
7
- module RSpec
8
- # Configure RSpec with JPie helpers and matchers
9
- def self.configure!
10
- ::RSpec.configure do |config|
11
- config.include JPie::RSpec::Matchers
12
- config.include JPie::RSpec::Helpers
13
- end
14
- end
15
-
16
- # Custom matchers for JPie resources
17
- module Matchers
18
- extend ::RSpec::Matchers::DSL
19
-
20
- matcher :have_attribute do |attribute_name|
21
- match do |actual|
22
- actual.respond_to?(attribute_name) &&
23
- actual.attributes.key?(attribute_name.to_s)
24
- end
25
-
26
- failure_message do |actual|
27
- "expected #{actual.inspect} to have attribute '#{attribute_name}'"
28
- end
29
- end
30
-
31
- matcher :have_relationship do |relationship_name|
32
- match do |actual|
33
- actual.respond_to?(relationship_name) &&
34
- actual.relationships.key?(relationship_name.to_s)
35
- end
36
-
37
- failure_message do |actual|
38
- "expected #{actual.inspect} to have relationship '#{relationship_name}'"
39
- end
40
- end
41
- end
42
-
43
- # Helper methods for testing JPie resources
44
- module Helpers
45
- # Build a JPie resource without saving it
46
- def build_jpie_resource(type, attributes = {}, relationships = {})
47
- JPie::Resource.new(
48
- type: type,
49
- attributes: attributes,
50
- relationships: relationships
51
- )
52
- end
53
-
54
- # Create a JPie resource and save it
55
- def create_jpie_resource(type, attributes = {}, relationships = {})
56
- resource = build_jpie_resource(type, attributes, relationships)
57
- resource.save
58
- resource
59
- end
60
-
61
- # Clean up test data after specs
62
- def cleanup_jpie_resources(resources)
63
- Array(resources).each do |resource|
64
- resource.destroy if resource.persisted?
65
- rescue StandardError => e
66
- warn "Failed to cleanup resource #{resource.inspect}: #{e.message}"
67
- end
68
- end
69
- end
70
- end
71
- end
@@ -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