jpie 1.0.0 → 1.0.2
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/.cursor/rules/release.mdc +62 -0
- data/.gitignore +5 -0
- data/.rubocop.yml +82 -38
- data/Gemfile +13 -10
- data/Gemfile.lock +18 -1
- data/README.md +675 -1235
- data/Rakefile +22 -0
- data/jpie.gemspec +15 -15
- data/kiln/app/resources/user_message_resource.rb +2 -0
- data/lib/jpie.rb +0 -1
- data/lib/json_api/active_storage/deserialization.rb +32 -22
- data/lib/json_api/active_storage/detection.rb +36 -41
- data/lib/json_api/active_storage/serialization.rb +13 -11
- data/lib/json_api/configuration.rb +4 -5
- data/lib/json_api/controllers/base_controller.rb +3 -3
- data/lib/json_api/controllers/concerns/controller_helpers/authorization.rb +30 -0
- data/lib/json_api/controllers/concerns/controller_helpers/document_meta.rb +20 -0
- data/lib/json_api/controllers/concerns/controller_helpers/error_rendering.rb +64 -0
- data/lib/json_api/controllers/concerns/controller_helpers/parsing.rb +127 -0
- data/lib/json_api/controllers/concerns/controller_helpers/resource_setup.rb +38 -0
- data/lib/json_api/controllers/concerns/controller_helpers.rb +11 -215
- data/lib/json_api/controllers/concerns/relationships/active_storage_removal.rb +65 -0
- data/lib/json_api/controllers/concerns/relationships/events.rb +44 -0
- data/lib/json_api/controllers/concerns/relationships/removal.rb +92 -0
- data/lib/json_api/controllers/concerns/relationships/response_helpers.rb +55 -0
- data/lib/json_api/controllers/concerns/relationships/serialization.rb +72 -0
- data/lib/json_api/controllers/concerns/relationships/sorting.rb +114 -0
- data/lib/json_api/controllers/concerns/relationships/updating.rb +73 -0
- data/lib/json_api/controllers/concerns/resource_actions/crud_helpers.rb +93 -0
- data/lib/json_api/controllers/concerns/resource_actions/field_validation.rb +114 -0
- data/lib/json_api/controllers/concerns/resource_actions/filter_validation.rb +91 -0
- data/lib/json_api/controllers/concerns/resource_actions/pagination.rb +51 -0
- data/lib/json_api/controllers/concerns/resource_actions/preloading.rb +64 -0
- data/lib/json_api/controllers/concerns/resource_actions/resource_loading.rb +71 -0
- data/lib/json_api/controllers/concerns/resource_actions/serialization.rb +63 -0
- data/lib/json_api/controllers/concerns/resource_actions/type_validation.rb +75 -0
- data/lib/json_api/controllers/concerns/resource_actions.rb +51 -602
- data/lib/json_api/controllers/relationships_controller.rb +26 -422
- data/lib/json_api/errors/parameter_not_allowed.rb +1 -1
- data/lib/json_api/railtie.rb +46 -9
- data/lib/json_api/resources/concerns/attributes_dsl.rb +69 -0
- data/lib/json_api/resources/concerns/filters_dsl.rb +32 -0
- data/lib/json_api/resources/concerns/meta_dsl.rb +23 -0
- data/lib/json_api/resources/concerns/model_class_helpers.rb +37 -0
- data/lib/json_api/resources/concerns/relationships_dsl.rb +71 -0
- data/lib/json_api/resources/concerns/sortable_fields_dsl.rb +36 -0
- data/lib/json_api/resources/resource.rb +13 -219
- data/lib/json_api/routing.rb +56 -47
- data/lib/json_api/serialization/concerns/attributes_deserialization.rb +27 -0
- data/lib/json_api/serialization/concerns/attributes_serialization.rb +50 -0
- data/lib/json_api/serialization/concerns/deserialization_helpers.rb +115 -0
- data/lib/json_api/serialization/concerns/includes_serialization.rb +82 -0
- data/lib/json_api/serialization/concerns/links_serialization.rb +33 -0
- data/lib/json_api/serialization/concerns/meta_serialization.rb +60 -0
- data/lib/json_api/serialization/concerns/model_attributes_transformation.rb +69 -0
- data/lib/json_api/serialization/concerns/relationship_processing.rb +119 -0
- data/lib/json_api/serialization/concerns/relationships_deserialization.rb +47 -0
- data/lib/json_api/serialization/concerns/relationships_serialization.rb +81 -0
- data/lib/json_api/serialization/deserializer.rb +10 -346
- data/lib/json_api/serialization/serializer.rb +17 -260
- data/lib/json_api/support/active_storage_support.rb +10 -13
- data/lib/json_api/support/collection_query.rb +14 -370
- data/lib/json_api/support/concerns/condition_building.rb +57 -0
- data/lib/json_api/support/concerns/nested_filters.rb +130 -0
- data/lib/json_api/support/concerns/pagination.rb +30 -0
- data/lib/json_api/support/concerns/polymorphic_filters.rb +75 -0
- data/lib/json_api/support/concerns/regular_filters.rb +81 -0
- data/lib/json_api/support/concerns/sorting.rb +88 -0
- data/lib/json_api/support/instrumentation.rb +13 -12
- data/lib/json_api/support/param_helpers.rb +9 -6
- data/lib/json_api/support/relationship_helpers.rb +4 -2
- data/lib/json_api/support/resource_identifier.rb +29 -29
- data/lib/json_api/support/responders.rb +5 -5
- data/lib/json_api/version.rb +1 -1
- metadata +44 -1
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JSONAPI
|
|
4
|
+
module Resources
|
|
5
|
+
module MetaDsl
|
|
6
|
+
extend ActiveSupport::Concern
|
|
7
|
+
|
|
8
|
+
class_methods do
|
|
9
|
+
def meta(hash = nil, &block)
|
|
10
|
+
@meta = hash || block
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def resource_meta
|
|
14
|
+
if instance_variable_defined?(:@meta)
|
|
15
|
+
@meta
|
|
16
|
+
elsif superclass != JSONAPI::Resource && superclass.respond_to?(:resource_meta)
|
|
17
|
+
superclass.resource_meta
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JSONAPI
|
|
4
|
+
module Resources
|
|
5
|
+
module ModelClassHelpers
|
|
6
|
+
extend ActiveSupport::Concern
|
|
7
|
+
|
|
8
|
+
class_methods do
|
|
9
|
+
def resource_for_model(model_class)
|
|
10
|
+
resource_const = "#{model_class.name}Resource"
|
|
11
|
+
resource_const.safe_constantize if resource_const.respond_to?(:safe_constantize) || defined?(ActiveSupport)
|
|
12
|
+
rescue NameError
|
|
13
|
+
nil
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def model_class
|
|
17
|
+
name.sub(/Resource$/, "").classify.constantize
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def safe_model_class
|
|
21
|
+
return nil unless respond_to?(:name) && name
|
|
22
|
+
return nil unless defined?(ActiveSupport)
|
|
23
|
+
|
|
24
|
+
name.sub(/Resource$/, "").classify.safe_constantize
|
|
25
|
+
rescue NoMethodError
|
|
26
|
+
nil
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def reflection_model_class
|
|
30
|
+
model_class
|
|
31
|
+
rescue StandardError
|
|
32
|
+
safe_model_class
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JSONAPI
|
|
4
|
+
module Resources
|
|
5
|
+
module RelationshipsDsl
|
|
6
|
+
extend ActiveSupport::Concern
|
|
7
|
+
|
|
8
|
+
class_methods do
|
|
9
|
+
def has_one(name, meta: nil, **options)
|
|
10
|
+
@relationships ||= []
|
|
11
|
+
detect_polymorphic(name, options)
|
|
12
|
+
@relationships << { name: name.to_sym, type: :has_one, meta:, options: }
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def has_many(name, meta: nil, **options)
|
|
16
|
+
@relationships ||= []
|
|
17
|
+
validate_append_only_options!(options)
|
|
18
|
+
detect_polymorphic(name, options)
|
|
19
|
+
@relationships << { name: name.to_sym, type: :has_many, meta:, options: }
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def belongs_to(name, meta: nil, **options)
|
|
23
|
+
@relationships ||= []
|
|
24
|
+
detect_polymorphic(name, options)
|
|
25
|
+
@relationships << { name: name.to_sym, type: :belongs_to, meta:, options: }
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def relationship_definitions
|
|
29
|
+
declared_relationships = instance_variable_defined?(:@relationships)
|
|
30
|
+
rels = @relationships || []
|
|
31
|
+
rels = superclass.relationship_definitions + rels if should_inherit_relationships?(declared_relationships)
|
|
32
|
+
rels.uniq { |r| r[:name] }
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def relationship_names
|
|
36
|
+
relationship_definitions.map { |r| r[:name] }
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
module RelationshipHelperMethods
|
|
41
|
+
def validate_append_only_options!(options)
|
|
42
|
+
if options[:append_only] && options[:purge_on_nil] == true
|
|
43
|
+
raise ArgumentError, "Cannot use append_only: true with purge_on_nil: true"
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
options[:purge_on_nil] = false if options[:append_only] && !options.key?(:purge_on_nil)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def detect_polymorphic(name, options)
|
|
50
|
+
return if options.key?(:polymorphic)
|
|
51
|
+
|
|
52
|
+
model_klass = reflection_model_class
|
|
53
|
+
return unless model_klass.respond_to?(:reflect_on_association)
|
|
54
|
+
|
|
55
|
+
reflection = model_klass.reflect_on_association(name)
|
|
56
|
+
options[:polymorphic] = reflection&.polymorphic?
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def should_inherit_relationships?(declared_relationships)
|
|
60
|
+
!declared_relationships &&
|
|
61
|
+
superclass != JSONAPI::Resource &&
|
|
62
|
+
superclass.respond_to?(:relationship_definitions)
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
included do
|
|
67
|
+
extend RelationshipHelperMethods
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JSONAPI
|
|
4
|
+
module Resources
|
|
5
|
+
module SortableFieldsDsl
|
|
6
|
+
extend ActiveSupport::Concern
|
|
7
|
+
|
|
8
|
+
class_methods do
|
|
9
|
+
def sortable_fields(*field_names)
|
|
10
|
+
@sortable_fields ||= []
|
|
11
|
+
@sortable_fields.concat(field_names.map(&:to_sym))
|
|
12
|
+
@sortable_fields.uniq!
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def permitted_sortable_fields
|
|
16
|
+
sort_fields = @sortable_fields || []
|
|
17
|
+
sort_fields = inherited_sort_only_fields + sort_fields if should_inherit_sortable_fields?
|
|
18
|
+
(permitted_attributes + sort_fields).uniq
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def should_inherit_sortable_fields?
|
|
22
|
+
!instance_variable_defined?(:@sortable_fields) &&
|
|
23
|
+
!instance_variable_defined?(:@attributes) &&
|
|
24
|
+
superclass != JSONAPI::Resource &&
|
|
25
|
+
superclass.respond_to?(:permitted_sortable_fields)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def inherited_sort_only_fields
|
|
29
|
+
parent_sort_fields = superclass.permitted_sortable_fields
|
|
30
|
+
parent_attributes = superclass.permitted_attributes
|
|
31
|
+
parent_sort_fields - parent_attributes
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -1,236 +1,30 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "concerns/attributes_dsl"
|
|
4
|
+
require_relative "concerns/filters_dsl"
|
|
5
|
+
require_relative "concerns/sortable_fields_dsl"
|
|
6
|
+
require_relative "concerns/relationships_dsl"
|
|
7
|
+
require_relative "concerns/meta_dsl"
|
|
8
|
+
require_relative "concerns/model_class_helpers"
|
|
9
|
+
|
|
3
10
|
module JSONAPI
|
|
4
11
|
class Resource
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
end
|
|
12
|
-
|
|
13
|
-
def attributes(*attrs)
|
|
14
|
-
@attributes ||= []
|
|
15
|
-
@attributes.concat(attrs.map(&:to_sym))
|
|
16
|
-
@attributes.uniq!
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
def creatable_fields(*fields)
|
|
20
|
-
@creatable_fields ||= []
|
|
21
|
-
@creatable_fields.concat(fields.map(&:to_sym))
|
|
22
|
-
@creatable_fields.uniq!
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
def updatable_fields(*fields)
|
|
26
|
-
@updatable_fields ||= []
|
|
27
|
-
@updatable_fields.concat(fields.map(&:to_sym))
|
|
28
|
-
@updatable_fields.uniq!
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
def filters(*filter_names)
|
|
32
|
-
@filters ||= []
|
|
33
|
-
@filters.concat(filter_names.map(&:to_sym))
|
|
34
|
-
@filters.uniq!
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
def permitted_filters_through
|
|
38
|
-
relationship_names
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
def sortable_fields(*field_names)
|
|
42
|
-
@sortable_fields ||= []
|
|
43
|
-
@sortable_fields.concat(field_names.map(&:to_sym))
|
|
44
|
-
@sortable_fields.uniq!
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
def meta(hash = nil, &block)
|
|
48
|
-
@meta = hash || block
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
def resource_meta
|
|
52
|
-
if instance_variable_defined?(:@meta)
|
|
53
|
-
@meta
|
|
54
|
-
elsif superclass != JSONAPI::Resource && superclass.respond_to?(:resource_meta)
|
|
55
|
-
superclass.resource_meta
|
|
56
|
-
end
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
def has_one(name, meta: nil, **options)
|
|
60
|
-
@relationships ||= []
|
|
61
|
-
|
|
62
|
-
unless options.key?(:polymorphic)
|
|
63
|
-
model_klass = reflection_model_class
|
|
64
|
-
if model_klass.respond_to?(:reflect_on_association)
|
|
65
|
-
reflection = model_klass.reflect_on_association(name)
|
|
66
|
-
options[:polymorphic] = reflection&.polymorphic?
|
|
67
|
-
end
|
|
68
|
-
end
|
|
69
|
-
|
|
70
|
-
@relationships << { name: name.to_sym, type: :has_one, meta:, options: }
|
|
71
|
-
end
|
|
72
|
-
|
|
73
|
-
def has_many(name, meta: nil, **options)
|
|
74
|
-
@relationships ||= []
|
|
75
|
-
|
|
76
|
-
if options[:append_only] && options[:purge_on_nil] == true
|
|
77
|
-
raise ArgumentError, "Cannot use append_only: true with purge_on_nil: true"
|
|
78
|
-
end
|
|
79
|
-
|
|
80
|
-
options[:purge_on_nil] = false if options[:append_only] && !options.key?(:purge_on_nil)
|
|
81
|
-
|
|
82
|
-
unless options.key?(:polymorphic)
|
|
83
|
-
model_klass = reflection_model_class
|
|
84
|
-
if model_klass.respond_to?(:reflect_on_association)
|
|
85
|
-
reflection = model_klass.reflect_on_association(name)
|
|
86
|
-
options[:polymorphic] = reflection&.polymorphic?
|
|
87
|
-
end
|
|
88
|
-
end
|
|
89
|
-
|
|
90
|
-
@relationships << { name: name.to_sym, type: :has_many, meta:, options: }
|
|
91
|
-
end
|
|
92
|
-
|
|
93
|
-
def belongs_to(name, meta: nil, **options)
|
|
94
|
-
@relationships ||= []
|
|
95
|
-
|
|
96
|
-
unless options.key?(:polymorphic)
|
|
97
|
-
model_klass = reflection_model_class
|
|
98
|
-
if model_klass.respond_to?(:reflect_on_association)
|
|
99
|
-
reflection = model_klass.reflect_on_association(name)
|
|
100
|
-
options[:polymorphic] = reflection&.polymorphic?
|
|
101
|
-
end
|
|
102
|
-
end
|
|
103
|
-
|
|
104
|
-
@relationships << { name: name.to_sym, type: :belongs_to, meta:, options: }
|
|
105
|
-
end
|
|
106
|
-
|
|
107
|
-
def permitted_attributes
|
|
108
|
-
# For STI subclasses, merge with parent class attributes unless the subclass declares its own
|
|
109
|
-
declared_attributes = instance_variable_defined?(:@attributes)
|
|
110
|
-
attrs = @attributes || []
|
|
111
|
-
if !declared_attributes &&
|
|
112
|
-
superclass != JSONAPI::Resource &&
|
|
113
|
-
superclass.respond_to?(:permitted_attributes)
|
|
114
|
-
attrs = superclass.permitted_attributes + attrs
|
|
115
|
-
end
|
|
116
|
-
attrs.uniq
|
|
117
|
-
end
|
|
118
|
-
|
|
119
|
-
def permitted_creatable_fields
|
|
120
|
-
if instance_variable_defined?(:@creatable_fields)
|
|
121
|
-
fields = @creatable_fields || []
|
|
122
|
-
elsif superclass != JSONAPI::Resource &&
|
|
123
|
-
superclass.respond_to?(:permitted_creatable_fields) &&
|
|
124
|
-
superclass.instance_variable_defined?(:@creatable_fields)
|
|
125
|
-
parent_fields = superclass.permitted_creatable_fields
|
|
126
|
-
fields = parent_fields
|
|
127
|
-
else
|
|
128
|
-
fields = permitted_attributes
|
|
129
|
-
end
|
|
130
|
-
|
|
131
|
-
fields.uniq
|
|
132
|
-
end
|
|
133
|
-
|
|
134
|
-
def permitted_updatable_fields
|
|
135
|
-
if instance_variable_defined?(:@updatable_fields)
|
|
136
|
-
fields = @updatable_fields || []
|
|
137
|
-
elsif superclass != JSONAPI::Resource &&
|
|
138
|
-
superclass.respond_to?(:permitted_updatable_fields) &&
|
|
139
|
-
superclass.instance_variable_defined?(:@updatable_fields)
|
|
140
|
-
parent_fields = superclass.permitted_updatable_fields
|
|
141
|
-
fields = parent_fields
|
|
142
|
-
else
|
|
143
|
-
fields = permitted_attributes
|
|
144
|
-
end
|
|
145
|
-
|
|
146
|
-
fields.uniq
|
|
147
|
-
end
|
|
148
|
-
|
|
149
|
-
def permitted_filters
|
|
150
|
-
# For STI subclasses, merge with parent class filters unless the subclass declares its own
|
|
151
|
-
declared_filters = instance_variable_defined?(:@filters)
|
|
152
|
-
filter_list = @filters || []
|
|
153
|
-
if !declared_filters &&
|
|
154
|
-
superclass != JSONAPI::Resource &&
|
|
155
|
-
superclass.respond_to?(:permitted_filters)
|
|
156
|
-
filter_list = superclass.permitted_filters + filter_list
|
|
157
|
-
end
|
|
158
|
-
filter_list.uniq
|
|
159
|
-
end
|
|
160
|
-
|
|
161
|
-
def permitted_sortable_fields
|
|
162
|
-
# Include both attributes (which are sortable) and sort-only fields
|
|
163
|
-
# For STI subclasses, merge with parent class sortable fields when no subclass DSL is declared
|
|
164
|
-
declared_sortable = instance_variable_defined?(:@sortable_fields)
|
|
165
|
-
declared_attributes = instance_variable_defined?(:@attributes)
|
|
166
|
-
sort_fields = @sortable_fields || []
|
|
167
|
-
if !declared_sortable &&
|
|
168
|
-
!declared_attributes &&
|
|
169
|
-
superclass != JSONAPI::Resource &&
|
|
170
|
-
superclass.respond_to?(:permitted_sortable_fields)
|
|
171
|
-
parent_sort_fields = superclass.permitted_sortable_fields
|
|
172
|
-
# Only merge parent's sort-only fields, not attributes (attributes are already included via permitted_attributes)
|
|
173
|
-
parent_attributes = superclass.permitted_attributes
|
|
174
|
-
parent_sort_only = parent_sort_fields - parent_attributes
|
|
175
|
-
sort_fields = parent_sort_only + sort_fields
|
|
176
|
-
end
|
|
177
|
-
# Combine with attributes (all attributes are sortable)
|
|
178
|
-
(permitted_attributes + sort_fields).uniq
|
|
179
|
-
end
|
|
180
|
-
|
|
181
|
-
def relationship_definitions
|
|
182
|
-
# For STI subclasses, merge with parent class relationships
|
|
183
|
-
declared_relationships = instance_variable_defined?(:@relationships)
|
|
184
|
-
rels = @relationships || []
|
|
185
|
-
if !declared_relationships &&
|
|
186
|
-
superclass != JSONAPI::Resource &&
|
|
187
|
-
superclass.respond_to?(:relationship_definitions)
|
|
188
|
-
rels = superclass.relationship_definitions + rels
|
|
189
|
-
end
|
|
190
|
-
rels.uniq { |r| r[:name] }
|
|
191
|
-
end
|
|
192
|
-
|
|
193
|
-
def relationship_names
|
|
194
|
-
relationship_definitions.map { |r| r[:name] }
|
|
195
|
-
end
|
|
196
|
-
|
|
197
|
-
def model_class
|
|
198
|
-
name.sub(/Resource$/, "").classify.constantize
|
|
199
|
-
end
|
|
200
|
-
|
|
201
|
-
def safe_model_class
|
|
202
|
-
return nil unless respond_to?(:name) && name
|
|
203
|
-
return nil unless defined?(ActiveSupport)
|
|
204
|
-
|
|
205
|
-
name.sub(/Resource$/, "").classify.safe_constantize
|
|
206
|
-
rescue NoMethodError
|
|
207
|
-
nil
|
|
208
|
-
end
|
|
209
|
-
|
|
210
|
-
def reflection_model_class
|
|
211
|
-
model_class
|
|
212
|
-
rescue NameError
|
|
213
|
-
safe_model_class
|
|
214
|
-
rescue StandardError
|
|
215
|
-
safe_model_class
|
|
216
|
-
end
|
|
217
|
-
end
|
|
12
|
+
include Resources::AttributesDsl
|
|
13
|
+
include Resources::FiltersDsl
|
|
14
|
+
include Resources::SortableFieldsDsl
|
|
15
|
+
include Resources::RelationshipsDsl
|
|
16
|
+
include Resources::MetaDsl
|
|
17
|
+
include Resources::ModelClassHelpers
|
|
218
18
|
|
|
219
|
-
# Instance method to initialize resource with model instance
|
|
220
|
-
# This allows the serializer to instantiate resources directly
|
|
221
19
|
def initialize(record = nil, context = {})
|
|
222
20
|
@record = record
|
|
223
21
|
@context = context
|
|
224
22
|
@transformed_params = {}
|
|
225
23
|
end
|
|
226
24
|
|
|
227
|
-
# Accessor for the underlying model instance
|
|
228
|
-
# @record is the preferred internal name, but :resource is kept for backward compatibility
|
|
229
25
|
attr_reader :record
|
|
230
26
|
alias resource record
|
|
231
27
|
|
|
232
|
-
# Returns transformed params accumulated by resource setters during deserialization
|
|
233
|
-
# Resources can override this method or use the default implementation
|
|
234
28
|
def transformed_params
|
|
235
29
|
@transformed_params || {}
|
|
236
30
|
end
|
data/lib/json_api/routing.rb
CHANGED
|
@@ -2,71 +2,80 @@
|
|
|
2
2
|
|
|
3
3
|
module JSONAPI
|
|
4
4
|
module Routing
|
|
5
|
-
def jsonapi_resources(resource, controller: nil, defaults: {}, sti: false, **options, &
|
|
5
|
+
def jsonapi_resources(resource, controller: nil, defaults: {}, sti: false, **options, &)
|
|
6
6
|
resource_name = resource.to_s
|
|
7
|
+
controller = detect_controller(resource_name) if controller.nil?
|
|
7
8
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
# If NOT found, default to the generic "json_api/resources".
|
|
12
|
-
if controller.nil?
|
|
13
|
-
scoped_module = @scope[:module]
|
|
14
|
-
potential_controller_name = if scoped_module
|
|
15
|
-
"#{scoped_module.to_s.camelize}::#{resource_name.pluralize.camelize}Controller"
|
|
16
|
-
else
|
|
17
|
-
"#{resource_name.pluralize.camelize}Controller"
|
|
18
|
-
end
|
|
9
|
+
JSONAPI::ResourceLoader.find(resource_name)
|
|
10
|
+
defaults = defaults.merge(format: :jsonapi, resource_type: resource_name)
|
|
11
|
+
options[:only] = :index if sti
|
|
19
12
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
# It exists! Leave controller as nil so Rails uses standard routing conventions
|
|
24
|
-
rescue NameError
|
|
25
|
-
# It doesn't exist, fallback to generic controller
|
|
26
|
-
controller = "json_api/resources"
|
|
27
|
-
end
|
|
28
|
-
end
|
|
13
|
+
define_resource_routes(resource, controller, defaults, options, &)
|
|
14
|
+
define_sti_routes(resource, resource_name, defaults, sti)
|
|
15
|
+
end
|
|
29
16
|
|
|
30
|
-
|
|
31
|
-
JSONAPI::ResourceLoader.find(resource_name)
|
|
17
|
+
private
|
|
32
18
|
|
|
33
|
-
|
|
19
|
+
def detect_controller(resource_name)
|
|
20
|
+
potential_controller_name = build_controller_name(resource_name)
|
|
21
|
+
potential_controller_name.constantize
|
|
22
|
+
nil
|
|
23
|
+
rescue NameError
|
|
24
|
+
"json_api/resources"
|
|
25
|
+
end
|
|
34
26
|
|
|
35
|
-
|
|
27
|
+
def build_controller_name(resource_name)
|
|
28
|
+
scoped_module = @scope[:module]
|
|
29
|
+
base_name = "#{resource_name.pluralize.camelize}Controller"
|
|
30
|
+
return base_name unless scoped_module
|
|
36
31
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
get "relationships/:relationship_name", to: "json_api/relationships#show", as: :relationship
|
|
40
|
-
patch "relationships/:relationship_name", to: "json_api/relationships#update"
|
|
41
|
-
delete "relationships/:relationship_name", to: "json_api/relationships#destroy"
|
|
42
|
-
end
|
|
32
|
+
"#{scoped_module.to_s.camelize}::#{base_name}"
|
|
33
|
+
end
|
|
43
34
|
|
|
35
|
+
def define_resource_routes(resource, controller, defaults, options, &block)
|
|
36
|
+
resources(resource, controller:, defaults:, **options) do
|
|
37
|
+
define_relationship_routes
|
|
44
38
|
instance_eval(&block) if block
|
|
45
39
|
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def define_relationship_routes
|
|
43
|
+
member do
|
|
44
|
+
get "relationships/:relationship_name", to: "json_api/relationships#show", as: :relationship
|
|
45
|
+
patch "relationships/:relationship_name", to: "json_api/relationships#update"
|
|
46
|
+
delete "relationships/:relationship_name", to: "json_api/relationships#destroy"
|
|
47
|
+
end
|
|
48
|
+
end
|
|
46
49
|
|
|
50
|
+
def define_sti_routes(resource, resource_name, defaults, sti)
|
|
47
51
|
return unless sti
|
|
48
52
|
|
|
49
53
|
if sti.is_a?(Array)
|
|
50
|
-
sti
|
|
51
|
-
|
|
52
|
-
end
|
|
54
|
+
define_explicit_sti_routes(sti,
|
|
55
|
+
defaults,)
|
|
53
56
|
else
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
+
define_auto_sti_routes(resource, resource_name,
|
|
58
|
+
defaults,)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def define_explicit_sti_routes(sti_resources, defaults)
|
|
63
|
+
sti_resources.each { |sub_resource_name| jsonapi_resources(sub_resource_name, defaults:) }
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def define_auto_sti_routes(resource, resource_name, defaults)
|
|
67
|
+
resource_class = JSONAPI::ResourceLoader.find(resource_name)
|
|
68
|
+
model_class = resource_class.model_class
|
|
69
|
+
return unless model_class.respond_to?(:descendants)
|
|
57
70
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
next if sub_resource_name == resource.to_sym
|
|
71
|
+
model_class.descendants.each do |subclass|
|
|
72
|
+
sub_resource_name = subclass.name.underscore.pluralize.to_sym
|
|
73
|
+
next if sub_resource_name == resource.to_sym
|
|
62
74
|
|
|
63
|
-
|
|
64
|
-
end
|
|
65
|
-
end
|
|
66
|
-
rescue NameError, JSONAPI::ResourceLoader::MissingResourceClass
|
|
67
|
-
# Silently skip if model/resource not found
|
|
68
|
-
end
|
|
75
|
+
jsonapi_resources(sub_resource_name, defaults:)
|
|
69
76
|
end
|
|
77
|
+
rescue NameError, JSONAPI::ResourceLoader::MissingResourceClass
|
|
78
|
+
nil
|
|
70
79
|
end
|
|
71
80
|
end
|
|
72
81
|
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JSONAPI
|
|
4
|
+
module Serialization
|
|
5
|
+
module AttributesDeserialization
|
|
6
|
+
def attributes
|
|
7
|
+
attrs = extract_attributes_from_params
|
|
8
|
+
attrs = attrs.transform_keys(&:to_sym) if attrs.respond_to?(:transform_keys)
|
|
9
|
+
permitted_attrs = permitted_attributes_for_action
|
|
10
|
+
attrs.slice(*permitted_attrs)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def extract_attributes_from_params
|
|
14
|
+
@params.dig(:data, :attributes) || @params[:attributes] || {}
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def permitted_attributes_for_action
|
|
18
|
+
fields = if @action == :create
|
|
19
|
+
@definition.permitted_creatable_fields
|
|
20
|
+
else
|
|
21
|
+
@definition.permitted_updatable_fields
|
|
22
|
+
end
|
|
23
|
+
fields.map(&:to_sym)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JSONAPI
|
|
4
|
+
module Serialization
|
|
5
|
+
module AttributesSerialization
|
|
6
|
+
def serialize_attributes(fields = {})
|
|
7
|
+
type_fields = extract_type_fields(fields)
|
|
8
|
+
return {} if type_fields.empty? && fields.any?
|
|
9
|
+
|
|
10
|
+
attributes = build_attributes_hash
|
|
11
|
+
return attributes if type_fields.empty?
|
|
12
|
+
|
|
13
|
+
attributes.slice(*type_fields.map(&:to_sym))
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
def extract_type_fields(fields)
|
|
19
|
+
fields[record_type.to_sym] || []
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def build_attributes_hash
|
|
23
|
+
permitted_attrs = definition.permitted_attributes.map(&:to_sym)
|
|
24
|
+
attributes = {}
|
|
25
|
+
definition_instance = definition.new(record, {})
|
|
26
|
+
|
|
27
|
+
permitted_attrs.each do |attr_sym|
|
|
28
|
+
attributes[attr_sym] = get_attribute_value(definition_instance, attr_sym)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
attributes.compact
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def get_attribute_value(definition_instance, attr_sym)
|
|
35
|
+
return definition_instance.public_send(attr_sym) if definition_instance.respond_to?(attr_sym, false)
|
|
36
|
+
|
|
37
|
+
attr_name = attr_sym.to_s
|
|
38
|
+
return record.attributes[attr_name] if model_has_attribute?(attr_name)
|
|
39
|
+
|
|
40
|
+
nil
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def model_has_attribute?(attr_name)
|
|
44
|
+
record.respond_to?(:attributes) &&
|
|
45
|
+
record.attributes.is_a?(Hash) &&
|
|
46
|
+
record.attributes.key?(attr_name)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|