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.
Files changed (91) 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 -24
  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 -169
  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/publish_gem.mdc +0 -73
  54. data/.cursor/rules/security.mdc +0 -14
  55. data/.cursor/rules/style.mdc +0 -15
  56. data/.cursor/rules/testing.mdc +0 -16
  57. data/.overcommit.yml +0 -35
  58. data/CHANGELOG.md +0 -164
  59. data/LICENSE.txt +0 -21
  60. data/PUBLISHING.md +0 -111
  61. data/examples/basic_example.md +0 -146
  62. data/examples/including_related_resources.md +0 -491
  63. data/examples/pagination.md +0 -303
  64. data/examples/relationships.md +0 -114
  65. data/examples/resource_attribute_configuration.md +0 -147
  66. data/examples/resource_meta_configuration.md +0 -244
  67. data/examples/rspec_testing.md +0 -130
  68. data/examples/single_table_inheritance.md +0 -160
  69. data/lib/jpie/configuration.rb +0 -12
  70. data/lib/jpie/controller/crud_actions.rb +0 -141
  71. data/lib/jpie/controller/error_handling/handler_setup.rb +0 -124
  72. data/lib/jpie/controller/error_handling/handlers.rb +0 -109
  73. data/lib/jpie/controller/error_handling.rb +0 -23
  74. data/lib/jpie/controller/json_api_validation.rb +0 -193
  75. data/lib/jpie/controller/parameter_parsing.rb +0 -78
  76. data/lib/jpie/controller/related_actions.rb +0 -45
  77. data/lib/jpie/controller/relationship_actions.rb +0 -291
  78. data/lib/jpie/controller/relationship_validation.rb +0 -117
  79. data/lib/jpie/controller/rendering.rb +0 -154
  80. data/lib/jpie/controller.rb +0 -45
  81. data/lib/jpie/deserializer.rb +0 -110
  82. data/lib/jpie/errors.rb +0 -117
  83. data/lib/jpie/generators/resource_generator.rb +0 -116
  84. data/lib/jpie/generators/templates/resource.rb.erb +0 -31
  85. data/lib/jpie/railtie.rb +0 -42
  86. data/lib/jpie/resource/attributable.rb +0 -112
  87. data/lib/jpie/resource/inferrable.rb +0 -43
  88. data/lib/jpie/resource/sortable.rb +0 -93
  89. data/lib/jpie/resource.rb +0 -147
  90. data/lib/jpie/routing.rb +0 -59
  91. 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
@@ -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