jat 0.0.3 → 0.0.5
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/lib/jat/attribute.rb +36 -4
- data/lib/jat/plugins/_activerecord_preloads/_activerecord_preloads.rb +7 -3
- data/lib/jat/plugins/_activerecord_preloads/lib/preloader.rb +7 -13
- data/lib/jat/plugins/_lower_camel_case/_lower_camel_case.rb +32 -0
- data/lib/jat/plugins/_preloads/_preloads.rb +8 -2
- data/lib/jat/plugins/cache/cache.rb +9 -5
- data/lib/jat/plugins/json_api/json_api.rb +145 -105
- data/lib/jat/plugins/json_api/lib/fields_param_parser.rb +40 -0
- data/lib/jat/plugins/json_api/lib/include_param_parser.rb +84 -0
- data/lib/jat/plugins/json_api/lib/map.rb +92 -27
- data/lib/jat/plugins/json_api/lib/params/fields/validate.rb +8 -5
- data/lib/jat/plugins/json_api/lib/response.rb +84 -196
- data/lib/jat/plugins/json_api/lib/response_piece.rb +166 -0
- data/lib/jat/plugins/json_api_activerecord/json_api_activerecord.rb +31 -0
- data/lib/jat/plugins/{_json_api_activerecord → json_api_activerecord}/lib/preloads.rb +16 -24
- data/lib/jat/plugins/json_api_lower_camel_case/json_api_lower_camel_case.rb +30 -0
- data/lib/jat/plugins/json_api_maps_cache/json_api_maps_cache.rb +54 -0
- data/lib/jat/plugins/json_api_validate_params/json_api_validate_params.rb +57 -0
- data/lib/jat/plugins/json_api_validate_params/lib/params_error.rb +6 -0
- data/lib/jat/plugins/json_api_validate_params/lib/validate_fields_param.rb +59 -0
- data/lib/jat/plugins/json_api_validate_params/lib/validate_include_param.rb +33 -0
- data/lib/jat/plugins/simple_api/lib/fields_param_parser.rb +97 -0
- data/lib/jat/plugins/simple_api/lib/map.rb +80 -10
- data/lib/jat/plugins/simple_api/lib/response.rb +78 -89
- data/lib/jat/plugins/simple_api/lib/response_piece.rb +84 -0
- data/lib/jat/plugins/simple_api/simple_api.rb +83 -24
- data/lib/jat/plugins/simple_api_activerecord/lib/preloads.rb +55 -0
- data/lib/jat/plugins/simple_api_activerecord/simple_api_activerecord.rb +31 -0
- data/lib/jat/plugins/simple_api_lower_camel_case/simple_api_lower_camel_case.rb +30 -0
- data/lib/jat/plugins/simple_api_maps_cache/simple_api_maps_cache.rb +48 -0
- data/lib/jat/plugins/simple_api_validate_params/lib/fields_error.rb +6 -0
- data/lib/jat/plugins/simple_api_validate_params/lib/validate_fields_param.rb +45 -0
- data/lib/jat/plugins/simple_api_validate_params/simple_api_validate_params.rb +45 -0
- data/lib/jat/plugins/to_str/to_str.rb +10 -4
- data/lib/jat/plugins.rb +3 -16
- data/lib/jat.rb +28 -30
- data/test/lib/jat/attribute_test.rb +15 -5
- data/test/lib/jat/plugins/_activerecord_preloads/_activerecord_preloads_test.rb +34 -15
- data/test/lib/jat/plugins/_activerecord_preloads/lib/preloader_test.rb +10 -24
- data/test/lib/jat/plugins/_camel_lower/_camel_lower_test.rb +26 -0
- data/test/lib/jat/plugins/_preloads/lib/format_user_preloads_test.rb +1 -1
- data/test/lib/jat/plugins/_preloads/lib/preloads_with_path_test.rb +1 -1
- data/test/lib/jat/plugins/cache/cache_test.rb +11 -11
- data/test/lib/jat/plugins/json_api/json_api_test.rb +63 -47
- data/test/lib/jat/plugins/json_api/lib/{params/fields_test.rb → fields_param_parser_test.rb} +7 -6
- data/test/lib/jat/plugins/json_api/lib/{params/include_test.rb → include_param_parser_test.rb} +4 -4
- data/test/lib/jat/plugins/json_api/lib/map_test.rb +150 -79
- data/test/lib/jat/plugins/json_api/lib/response_test.rb +32 -32
- data/test/lib/jat/plugins/{_json_api_activerecord/_json_api_activerecord_test.rb → json_api_activerecord/json_api_activerecord_test.rb} +14 -5
- data/test/lib/jat/plugins/{_json_api_activerecord → json_api_activerecord}/lib/preloads_test.rb +11 -10
- data/test/lib/jat/plugins/json_api_camel_lower/json_api_camel_lower_test.rb +79 -0
- data/test/lib/jat/plugins/json_api_maps_cache/json_api_maps_cache_test.rb +107 -0
- data/test/lib/jat/plugins/json_api_validate_params/json_api_validate_params_test.rb +84 -0
- data/test/lib/jat/plugins/simple_api/lib/{params/parse_test.rb → fields_param_parser_test.rb} +10 -4
- data/test/lib/jat/plugins/simple_api/lib/map_test.rb +111 -34
- data/test/lib/jat/plugins/simple_api/lib/response_test.rb +80 -74
- data/test/lib/jat/plugins/simple_api/simple_api_test.rb +91 -25
- data/test/lib/jat/plugins/simple_api_activerecord/lib/preloads_test.rb +135 -0
- data/test/lib/jat/plugins/simple_api_activerecord/simple_api_activerecord_test.rb +38 -0
- data/test/lib/jat/plugins/simple_api_camel_lower/simple_api_camel_lower_test.rb +48 -0
- data/test/lib/jat/plugins/simple_api_maps_cache/simple_api_maps_cache_test.rb +95 -0
- data/test/lib/jat/plugins/simple_api_validate_params/simple_api_validate_params_test.rb +89 -0
- data/test/lib/jat/plugins/to_str/to_str_test.rb +3 -3
- data/test/lib/jat_test.rb +47 -24
- data/test/lib/plugin_test.rb +3 -3
- data/test/test_helper.rb +0 -3
- data/test/test_plugin.rb +9 -12
- metadata +60 -71
- data/CHANGELOG.md +0 -7
- data/README.md +0 -21
- data/jat.gemspec +0 -37
- data/lib/jat/plugins/_json_api_activerecord/_json_api_activerecord.rb +0 -22
- data/lib/jat/plugins/camel_lower/camel_lower.rb +0 -18
- data/lib/jat/plugins/json_api/lib/construct_traversal_map.rb +0 -91
- data/lib/jat/plugins/json_api/lib/presenters/document_links_presenter.rb +0 -48
- data/lib/jat/plugins/json_api/lib/presenters/document_meta_presenter.rb +0 -48
- data/lib/jat/plugins/json_api/lib/presenters/jsonapi_presenter.rb +0 -48
- data/lib/jat/plugins/json_api/lib/presenters/links_presenter.rb +0 -48
- data/lib/jat/plugins/json_api/lib/presenters/meta_presenter.rb +0 -48
- data/lib/jat/plugins/json_api/lib/presenters/relationship_links_presenter.rb +0 -53
- data/lib/jat/plugins/json_api/lib/presenters/relationship_meta_presenter.rb +0 -53
- data/lib/jat/plugins/json_api/lib/traversal_map.rb +0 -34
- data/lib/jat/plugins/simple_api/lib/construct_traversal_map.rb +0 -45
- data/lib/jat/plugins/simple_api/lib/params/parse.rb +0 -68
- data/lib/jat/presenter.rb +0 -51
- data/test/lib/jat/plugins/camel_lower/camel_lower_test.rb +0 -78
- data/test/lib/jat/plugins/json_api/lib/construct_traversal_map_test.rb +0 -119
- data/test/lib/jat/plugins/json_api/lib/params/fields/parse_test.rb +0 -24
- data/test/lib/jat/plugins/json_api/lib/params/fields/validate_test.rb +0 -47
- data/test/lib/jat/plugins/json_api/lib/params/include/parse_test.rb +0 -46
- data/test/lib/jat/plugins/json_api/lib/params/include/validate_test.rb +0 -51
- data/test/lib/jat/plugins/json_api/lib/presenters/document_links_presenter_test.rb +0 -69
- data/test/lib/jat/plugins/json_api/lib/presenters/document_meta_presenter_test.rb +0 -69
- data/test/lib/jat/plugins/json_api/lib/presenters/jsonapi_presenter_test.rb +0 -69
- data/test/lib/jat/plugins/json_api/lib/presenters/links_presenter_test.rb +0 -69
- data/test/lib/jat/plugins/json_api/lib/presenters/meta_presenter_test.rb +0 -69
- data/test/lib/jat/plugins/json_api/lib/presenters/relationship_links_presenter_test.rb +0 -75
- data/test/lib/jat/plugins/json_api/lib/presenters/relationship_meta_presenter_test.rb +0 -75
- data/test/lib/jat/plugins/json_api/lib/traversal_map_test.rb +0 -58
- data/test/lib/jat/plugins/simple_api/lib/construct_traversal_map_test.rb +0 -100
- data/test/lib/jat/presenter_test.rb +0 -61
@@ -0,0 +1,166 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Jat
|
4
|
+
module Plugins
|
5
|
+
module JsonApi
|
6
|
+
class ResponsePiece
|
7
|
+
module ClassMethods
|
8
|
+
# Returns the Jat class that this ResponsePiece class is namespaced under.
|
9
|
+
attr_accessor :jat_class
|
10
|
+
|
11
|
+
# Since ResponsePiece is anonymously subclassed when Jat is subclassed,
|
12
|
+
# and then assigned to a constant of the Jat subclass, make inspect
|
13
|
+
# reflect the likely name for the class.
|
14
|
+
def inspect
|
15
|
+
"#{jat_class.inspect}::ResponsePiece"
|
16
|
+
end
|
17
|
+
|
18
|
+
def call(object, context, map, includes)
|
19
|
+
new(object, context, map, includes).to_h
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
module InstanceMethods
|
24
|
+
attr_reader :jat_class, :object, :context, :map, :type_map, :includes
|
25
|
+
|
26
|
+
def initialize(object, context, map, includes)
|
27
|
+
@jat_class = self.class.jat_class
|
28
|
+
@object = object
|
29
|
+
@context = context
|
30
|
+
@map = map
|
31
|
+
@type_map = map.fetch(jat_class.get_type)
|
32
|
+
@includes = includes
|
33
|
+
end
|
34
|
+
|
35
|
+
def to_h
|
36
|
+
return unless object
|
37
|
+
|
38
|
+
attributes = get_attributes
|
39
|
+
relationships = get_relationships
|
40
|
+
links = get_links
|
41
|
+
meta = get_meta
|
42
|
+
|
43
|
+
result = uid
|
44
|
+
result[:attributes] = attributes if attributes
|
45
|
+
result[:relationships] = relationships if relationships
|
46
|
+
result[:links] = links if links.any?
|
47
|
+
result[:meta] = meta if meta.any?
|
48
|
+
result
|
49
|
+
end
|
50
|
+
|
51
|
+
def uid
|
52
|
+
{type: jat_class.get_type, id: jat_class.get_id.block.call(object, context)}
|
53
|
+
end
|
54
|
+
|
55
|
+
private
|
56
|
+
|
57
|
+
def get_attributes
|
58
|
+
attributes_names = type_map[:attributes]
|
59
|
+
return if attributes_names.empty?
|
60
|
+
|
61
|
+
attributes_names.each_with_object({}) do |name, attrs|
|
62
|
+
attribute = jat_class.attributes[name]
|
63
|
+
attrs[name] = attribute.block.call(object, context)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def get_relationships
|
68
|
+
relationships_names = type_map[:relationships]
|
69
|
+
return if relationships_names.empty?
|
70
|
+
|
71
|
+
relationships_names.each_with_object({}) do |name, rels|
|
72
|
+
rel_attribute = jat_class.attributes[name]
|
73
|
+
rel_object = rel_attribute.block.call(object, context)
|
74
|
+
|
75
|
+
rel_serializer = rel_attribute.serializer.call
|
76
|
+
rel_links = get_relationship_links(rel_serializer, rel_object)
|
77
|
+
rel_meta = get_relationship_meta(rel_serializer, rel_object)
|
78
|
+
rel_data =
|
79
|
+
if many?(rel_attribute, rel_object)
|
80
|
+
many_relationships_data(rel_serializer, rel_object)
|
81
|
+
else
|
82
|
+
one_relationship_data(rel_serializer, rel_object)
|
83
|
+
end
|
84
|
+
|
85
|
+
result = {}
|
86
|
+
result[:data] = rel_data
|
87
|
+
result[:links] = rel_links if rel_links.any?
|
88
|
+
result[:meta] = rel_meta if rel_meta.any?
|
89
|
+
rels[name] = result
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def many_relationships_data(rel_serializer, rel_objects)
|
94
|
+
return FROZEN_EMPTY_ARRAY if rel_objects.empty?
|
95
|
+
|
96
|
+
rel_objects.map { |rel_object| add_relationship_data(rel_serializer, rel_object) }
|
97
|
+
end
|
98
|
+
|
99
|
+
def one_relationship_data(rel_serializer, rel_object)
|
100
|
+
return unless rel_object
|
101
|
+
|
102
|
+
add_relationship_data(rel_serializer, rel_object)
|
103
|
+
end
|
104
|
+
|
105
|
+
def add_relationship_data(rel_serializer, rel_object)
|
106
|
+
rel_response_data = rel_serializer::ResponsePiece.new(rel_object, context, map, includes)
|
107
|
+
rel_uid = rel_response_data.uid
|
108
|
+
includes[rel_uid] ||= rel_response_data.to_h
|
109
|
+
rel_uid
|
110
|
+
end
|
111
|
+
|
112
|
+
def many?(attribute, object)
|
113
|
+
is_many = attribute.many?
|
114
|
+
|
115
|
+
# handle boolean
|
116
|
+
return is_many if (is_many == true) || (is_many == false)
|
117
|
+
|
118
|
+
# handle nil
|
119
|
+
object.is_a?(Enumerable)
|
120
|
+
end
|
121
|
+
|
122
|
+
def get_links
|
123
|
+
jat_class
|
124
|
+
.object_links
|
125
|
+
.transform_values { |attr| attr.block.call(object, context) }
|
126
|
+
.tap(&:compact!)
|
127
|
+
end
|
128
|
+
|
129
|
+
def get_meta
|
130
|
+
jat_class
|
131
|
+
.added_object_meta
|
132
|
+
.transform_values { |attr| attr.block.call(object, context) }
|
133
|
+
.tap(&:compact!)
|
134
|
+
end
|
135
|
+
|
136
|
+
def get_relationship_links(rel_serializer, rel_object)
|
137
|
+
links = rel_serializer.relationship_links
|
138
|
+
return FROZEN_EMPTY_HASH unless links
|
139
|
+
|
140
|
+
context[:parent_object] = object
|
141
|
+
|
142
|
+
links
|
143
|
+
.transform_values { |attr| attr.block.call(rel_object, context) }
|
144
|
+
.tap(&:compact!)
|
145
|
+
.tap { context.delete(:parent_object) }
|
146
|
+
end
|
147
|
+
|
148
|
+
def get_relationship_meta(rel_serializer, rel_object)
|
149
|
+
meta = rel_serializer.added_relationship_meta
|
150
|
+
return FROZEN_EMPTY_HASH unless meta
|
151
|
+
|
152
|
+
context[:parent_object] = object
|
153
|
+
|
154
|
+
meta
|
155
|
+
.transform_values { |attr| attr.block.call(rel_object, context) }
|
156
|
+
.tap(&:compact!)
|
157
|
+
.tap { context.delete(:parent_object) }
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
extend ClassMethods
|
162
|
+
include InstanceMethods
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "./lib/preloads"
|
4
|
+
|
5
|
+
class Jat
|
6
|
+
module Plugins
|
7
|
+
module JsonApiActiverecord
|
8
|
+
def self.before_load(jat_class, **opts)
|
9
|
+
return if jat_class.plugin_used?(:json_api)
|
10
|
+
raise Error, "Please load :json_api plugin first"
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.load(jat_class, **_opts)
|
14
|
+
jat_class.include(InstanceMethods)
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.after_load(jat_class, **opts)
|
18
|
+
jat_class.plugin :_preloads, **opts
|
19
|
+
jat_class.plugin :_activerecord_preloads, **opts
|
20
|
+
end
|
21
|
+
|
22
|
+
module InstanceMethods
|
23
|
+
def preloads
|
24
|
+
@preloads ||= Preloads.call(self)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
register_plugin(:json_api_activerecord, JsonApiActiverecord)
|
30
|
+
end
|
31
|
+
end
|
@@ -1,60 +1,52 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require "set"
|
4
|
+
|
3
5
|
class Jat
|
4
6
|
module Plugins
|
5
7
|
module JsonApiActiverecord
|
6
8
|
class Preloads
|
7
9
|
class << self
|
8
10
|
def call(jat)
|
9
|
-
new(jat.
|
11
|
+
new(jat.map).for(jat.class)
|
10
12
|
end
|
11
13
|
end
|
12
14
|
|
13
|
-
attr_reader :initial_result
|
14
|
-
|
15
15
|
def initialize(current_map)
|
16
|
+
@used = Set.new
|
16
17
|
@current_map = current_map
|
17
|
-
@initial_result = {}
|
18
18
|
end
|
19
19
|
|
20
20
|
def for(jat_class)
|
21
|
-
|
22
|
-
append(
|
23
|
-
|
24
|
-
rescue SystemStackError
|
25
|
-
raise Error, "Stack level too deep, recursive preloads detected: #{initial_result}"
|
21
|
+
result = {}
|
22
|
+
append(result, jat_class)
|
23
|
+
result
|
26
24
|
end
|
27
25
|
|
28
26
|
private
|
29
27
|
|
30
|
-
attr_reader :current_map
|
28
|
+
attr_reader :current_map, :used
|
31
29
|
|
32
30
|
def append(result, jat_class)
|
33
|
-
attrs = current_map[jat_class.
|
34
|
-
attributes_names = attrs[:attributes] + attrs[:relationships]
|
31
|
+
attrs = current_map[jat_class.get_type]
|
35
32
|
|
36
|
-
add_attributes(result, jat_class,
|
33
|
+
add_attributes(result, jat_class, attrs[:attributes])
|
34
|
+
add_attributes(result, jat_class, attrs[:relationships])
|
37
35
|
end
|
38
36
|
|
39
37
|
def add_attributes(result, jat_class, attributes_names)
|
40
38
|
attributes_names.each do |name|
|
39
|
+
next unless used.add?([jat_class, name]) # Protection from recursive preloads
|
40
|
+
|
41
41
|
attribute = jat_class.attributes[name]
|
42
42
|
preloads = attribute.preloads
|
43
|
-
next unless preloads # we should not
|
43
|
+
next unless preloads # we should not add preloads and nested preloads when nil provided
|
44
44
|
|
45
|
-
|
45
|
+
merge(result, deep_dup(preloads)) unless preloads.empty?
|
46
|
+
add_nested_preloads(result, attribute) if attribute.relation?
|
46
47
|
end
|
47
48
|
end
|
48
49
|
|
49
|
-
def add_preloads(result, preloads, attribute)
|
50
|
-
unless preloads.empty?
|
51
|
-
preloads = deep_dup(preloads)
|
52
|
-
merge(result, preloads)
|
53
|
-
end
|
54
|
-
|
55
|
-
add_nested_preloads(result, attribute) if attribute.relation?
|
56
|
-
end
|
57
|
-
|
58
50
|
def add_nested_preloads(result, attribute)
|
59
51
|
path = attribute.preloads_path
|
60
52
|
nested_result = nested(result, path)
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Jat
|
4
|
+
module Plugins
|
5
|
+
module JsonApiLowerCamelCase
|
6
|
+
def self.before_load(jat_class, **_opts)
|
7
|
+
raise Error, "Please load :json_api plugin first" unless jat_class.plugin_used?(:json_api)
|
8
|
+
|
9
|
+
jat_class.plugin :_lower_camel_case
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.load(jat_class, **_opts)
|
13
|
+
jat_class::Response.include(ResponseInstanceMethods)
|
14
|
+
end
|
15
|
+
|
16
|
+
module ResponseInstanceMethods
|
17
|
+
private
|
18
|
+
|
19
|
+
def context_attr_transform(*)
|
20
|
+
result = super
|
21
|
+
return result if result.empty?
|
22
|
+
|
23
|
+
result.transform_keys! { |key| Jat::LowerCamelCaseTransformation.call(key) }
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
register_plugin(:json_api_lower_camel_case, JsonApiLowerCamelCase)
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Jat
|
4
|
+
module Plugins
|
5
|
+
module JsonApiMapsCache
|
6
|
+
def self.before_load(jat_class, **opts)
|
7
|
+
return if jat_class.plugin_used?(:json_api)
|
8
|
+
raise Error, "Please load :json_api plugin first"
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.load(jat_class, **_opts)
|
12
|
+
jat_class::Map.extend(MapsCacheClassMethods)
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.after_load(jat_class, **opts)
|
16
|
+
jat_class.config[:cached_maps_count] = opts[:cached_maps_count] || 100
|
17
|
+
end
|
18
|
+
|
19
|
+
module MapsCacheClassMethods
|
20
|
+
# Caches up to `:cached_maps_count` maps for each serializer.
|
21
|
+
# Removes earliest value if new value exceeds limit.
|
22
|
+
|
23
|
+
def maps_cache
|
24
|
+
@maps_cache ||= Hash.new do |cache, cache_key|
|
25
|
+
cache.shift if cache.length >= jat_class.config[:cached_maps_count] # protect from memory leak
|
26
|
+
cache[cache_key] = EnumDeepFreeze.call(yield)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def construct_map(exposed, fields, includes)
|
33
|
+
cache = maps_cache { super }
|
34
|
+
cache_key = cache_key(exposed, fields, includes)
|
35
|
+
cache[cache_key]
|
36
|
+
end
|
37
|
+
|
38
|
+
def cache_key(exposed, fields, includes)
|
39
|
+
key = "exposed:#{exposed}:"
|
40
|
+
key += "includes:#{includes}:" if includes
|
41
|
+
|
42
|
+
if fields
|
43
|
+
key += "fields:"
|
44
|
+
fields.each { |type, attrs| key += "#{type}:#{attrs}:" }
|
45
|
+
end
|
46
|
+
|
47
|
+
key
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
register_plugin(:json_api_maps_cache, JsonApiMapsCache)
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "./lib/params_error"
|
4
|
+
require_relative "./lib/validate_fields_param"
|
5
|
+
require_relative "./lib/validate_include_param"
|
6
|
+
|
7
|
+
class Jat
|
8
|
+
module Plugins
|
9
|
+
module JsonApiValidateParams
|
10
|
+
def self.before_load(jat_class, **_opts)
|
11
|
+
return if jat_class.plugin_used?(:json_api)
|
12
|
+
raise Error, "Please load :json_api plugin first"
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.load(jat_class, **_opts)
|
16
|
+
jat_class.extend(ClassMethods)
|
17
|
+
jat_class.include(InstanceMethods)
|
18
|
+
|
19
|
+
jat_class::FieldsParamParser.extend(FieldsParamParserClassMethods)
|
20
|
+
jat_class::IncludeParamParser.extend(IncludeParamParserClassMethods)
|
21
|
+
end
|
22
|
+
|
23
|
+
module InstanceMethods
|
24
|
+
def validate
|
25
|
+
@validate ||= self.class.validate(context)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
module ClassMethods
|
30
|
+
def validate(context)
|
31
|
+
# Generate map for current context.
|
32
|
+
# Params are valid if no errors were raised
|
33
|
+
map(context)
|
34
|
+
true
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
module IncludeParamParserClassMethods
|
39
|
+
private
|
40
|
+
|
41
|
+
def parse_to_nested_hash(*)
|
42
|
+
super.tap { |result| ValidateIncludeParam.call(jat_class, result) }
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
module FieldsParamParserClassMethods
|
47
|
+
private
|
48
|
+
|
49
|
+
def parse_to_nested_hash(*)
|
50
|
+
super.tap { |result| ValidateFieldsParam.call(jat_class, result) }
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
register_plugin(:json_api_validate_params, JsonApiValidateParams)
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Jat
|
4
|
+
module Plugins
|
5
|
+
module JsonApiValidateParams
|
6
|
+
class ValidateFieldsParam
|
7
|
+
def self.call(jat_class, fields)
|
8
|
+
full_map = jat_class.map_full
|
9
|
+
|
10
|
+
fields.each do |type, attributes_names|
|
11
|
+
new(type, full_map).validate(attributes_names)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
attr_reader :type, :full_map
|
16
|
+
|
17
|
+
def initialize(type, full_map)
|
18
|
+
@type = type
|
19
|
+
@full_map = full_map
|
20
|
+
end
|
21
|
+
|
22
|
+
def validate(attributes_names)
|
23
|
+
check_fields_type
|
24
|
+
check_attributes_names(attributes_names)
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def check_fields_type
|
30
|
+
return if full_map.key?(type)
|
31
|
+
|
32
|
+
allowed_types = "'#{full_map.keys.join("', '")}'"
|
33
|
+
|
34
|
+
raise JsonApiParamsError, <<~ERROR.strip
|
35
|
+
Response does not have resources with type '#{type}'. Existing types are: #{allowed_types}
|
36
|
+
ERROR
|
37
|
+
end
|
38
|
+
|
39
|
+
def check_attributes_names(attributes_names)
|
40
|
+
attributes_names.each do |attribute_name|
|
41
|
+
check_attribute_name(attribute_name)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def check_attribute_name(attribute_name)
|
46
|
+
type_data = full_map.fetch(type)
|
47
|
+
type_serializer = type_data.fetch(:serializer)
|
48
|
+
return if type_serializer.attributes.key?(attribute_name)
|
49
|
+
|
50
|
+
allowed_attributes = "'#{type_serializer.attributes.keys.join("', '")}'"
|
51
|
+
|
52
|
+
raise JsonApiParamsError, <<~ERROR.strip
|
53
|
+
No attribute '#{attribute_name}' in resource type '#{type}'. Existing attributes are: #{allowed_attributes}
|
54
|
+
ERROR
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Jat
|
4
|
+
module Plugins
|
5
|
+
module JsonApiValidateParams
|
6
|
+
class ValidateIncludeParam
|
7
|
+
class << self
|
8
|
+
def call(jat_class, includes)
|
9
|
+
includes.each do |name, nested_includes|
|
10
|
+
attribute = jat_class.attributes[name]
|
11
|
+
raise_error(jat_class, name) if !attribute || !attribute.relation?
|
12
|
+
|
13
|
+
nested_serializer = attribute.serializer.call
|
14
|
+
call(nested_serializer, nested_includes)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def raise_error(jat_class, name)
|
21
|
+
type = jat_class.get_type
|
22
|
+
allowed_relationships = jat_class.attributes.each_value.select(&:relation?).map!(&:name)
|
23
|
+
allowed_relationships = "'#{allowed_relationships.join("', '")}'"
|
24
|
+
|
25
|
+
raise JsonApiParamsError,
|
26
|
+
"Type '#{type}' has no included '#{name}' relationship. " \
|
27
|
+
"Existing relationships are: #{allowed_relationships}"
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,97 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Jat
|
4
|
+
module Plugins
|
5
|
+
module SimpleApi
|
6
|
+
class FieldsParamParser
|
7
|
+
module ClassMethods
|
8
|
+
# Returns the Jat class that this FieldsParamParser class is namespaced under.
|
9
|
+
attr_accessor :jat_class
|
10
|
+
|
11
|
+
# Since FieldsParamParser is anonymously subclassed when Jat is subclassed,
|
12
|
+
# and then assigned to a constant of the Jat subclass, make inspect
|
13
|
+
# reflect the likely name for the class.
|
14
|
+
def inspect
|
15
|
+
"#{jat_class.inspect}::FieldsParamParser"
|
16
|
+
end
|
17
|
+
|
18
|
+
def parse(fields)
|
19
|
+
return FROZEN_EMPTY_HASH unless fields
|
20
|
+
|
21
|
+
new(fields).parse
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
module InstanceMethods
|
26
|
+
COMMA = ","
|
27
|
+
OPEN_BRACKET = "("
|
28
|
+
CLOSE_BRACKET = ")"
|
29
|
+
|
30
|
+
def initialize(fields)
|
31
|
+
@fields = fields
|
32
|
+
end
|
33
|
+
|
34
|
+
# user => { user: {} }
|
35
|
+
# user(id) => { user: { id: {} } }
|
36
|
+
# user(id,name) => { user: { id: {}, name: {} } }
|
37
|
+
# user,comments => { user: {}, comments: {} }
|
38
|
+
# user(comments(text)) => { user: { comments: { text: {} } } }
|
39
|
+
def parse
|
40
|
+
current_attr = nil
|
41
|
+
|
42
|
+
fields.each_char do |char|
|
43
|
+
case char
|
44
|
+
when COMMA
|
45
|
+
next unless current_attr
|
46
|
+
|
47
|
+
add_attribute(current_attr)
|
48
|
+
current_attr = nil
|
49
|
+
when CLOSE_BRACKET
|
50
|
+
if current_attr
|
51
|
+
add_attribute(current_attr)
|
52
|
+
current_attr = nil
|
53
|
+
end
|
54
|
+
|
55
|
+
route.pop
|
56
|
+
when OPEN_BRACKET
|
57
|
+
next unless current_attr
|
58
|
+
|
59
|
+
attribute_name = add_attribute(current_attr, {})
|
60
|
+
route << attribute_name
|
61
|
+
current_attr = nil
|
62
|
+
else
|
63
|
+
current_attr = current_attr ? current_attr.insert(-1, char) : char
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
add_attribute(current_attr) if current_attr
|
68
|
+
|
69
|
+
res
|
70
|
+
end
|
71
|
+
|
72
|
+
private
|
73
|
+
|
74
|
+
attr_reader :fields
|
75
|
+
|
76
|
+
def add_attribute(current_attr, nested_attrs = FROZEN_EMPTY_HASH)
|
77
|
+
current_resource = route.empty? ? res : res.dig(*route)
|
78
|
+
attribute_name = current_attr.strip.to_sym
|
79
|
+
current_resource[attribute_name] = nested_attrs
|
80
|
+
attribute_name
|
81
|
+
end
|
82
|
+
|
83
|
+
def res
|
84
|
+
@res ||= {}
|
85
|
+
end
|
86
|
+
|
87
|
+
def route
|
88
|
+
@route ||= []
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
extend ClassMethods
|
93
|
+
include InstanceMethods
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|