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.
Files changed (102) hide show
  1. checksums.yaml +4 -4
  2. data/lib/jat/attribute.rb +36 -4
  3. data/lib/jat/plugins/_activerecord_preloads/_activerecord_preloads.rb +7 -3
  4. data/lib/jat/plugins/_activerecord_preloads/lib/preloader.rb +7 -13
  5. data/lib/jat/plugins/_lower_camel_case/_lower_camel_case.rb +32 -0
  6. data/lib/jat/plugins/_preloads/_preloads.rb +8 -2
  7. data/lib/jat/plugins/cache/cache.rb +9 -5
  8. data/lib/jat/plugins/json_api/json_api.rb +145 -105
  9. data/lib/jat/plugins/json_api/lib/fields_param_parser.rb +40 -0
  10. data/lib/jat/plugins/json_api/lib/include_param_parser.rb +84 -0
  11. data/lib/jat/plugins/json_api/lib/map.rb +92 -27
  12. data/lib/jat/plugins/json_api/lib/params/fields/validate.rb +8 -5
  13. data/lib/jat/plugins/json_api/lib/response.rb +84 -196
  14. data/lib/jat/plugins/json_api/lib/response_piece.rb +166 -0
  15. data/lib/jat/plugins/json_api_activerecord/json_api_activerecord.rb +31 -0
  16. data/lib/jat/plugins/{_json_api_activerecord → json_api_activerecord}/lib/preloads.rb +16 -24
  17. data/lib/jat/plugins/json_api_lower_camel_case/json_api_lower_camel_case.rb +30 -0
  18. data/lib/jat/plugins/json_api_maps_cache/json_api_maps_cache.rb +54 -0
  19. data/lib/jat/plugins/json_api_validate_params/json_api_validate_params.rb +57 -0
  20. data/lib/jat/plugins/json_api_validate_params/lib/params_error.rb +6 -0
  21. data/lib/jat/plugins/json_api_validate_params/lib/validate_fields_param.rb +59 -0
  22. data/lib/jat/plugins/json_api_validate_params/lib/validate_include_param.rb +33 -0
  23. data/lib/jat/plugins/simple_api/lib/fields_param_parser.rb +97 -0
  24. data/lib/jat/plugins/simple_api/lib/map.rb +80 -10
  25. data/lib/jat/plugins/simple_api/lib/response.rb +78 -89
  26. data/lib/jat/plugins/simple_api/lib/response_piece.rb +84 -0
  27. data/lib/jat/plugins/simple_api/simple_api.rb +83 -24
  28. data/lib/jat/plugins/simple_api_activerecord/lib/preloads.rb +55 -0
  29. data/lib/jat/plugins/simple_api_activerecord/simple_api_activerecord.rb +31 -0
  30. data/lib/jat/plugins/simple_api_lower_camel_case/simple_api_lower_camel_case.rb +30 -0
  31. data/lib/jat/plugins/simple_api_maps_cache/simple_api_maps_cache.rb +48 -0
  32. data/lib/jat/plugins/simple_api_validate_params/lib/fields_error.rb +6 -0
  33. data/lib/jat/plugins/simple_api_validate_params/lib/validate_fields_param.rb +45 -0
  34. data/lib/jat/plugins/simple_api_validate_params/simple_api_validate_params.rb +45 -0
  35. data/lib/jat/plugins/to_str/to_str.rb +10 -4
  36. data/lib/jat/plugins.rb +3 -16
  37. data/lib/jat.rb +28 -30
  38. data/test/lib/jat/attribute_test.rb +15 -5
  39. data/test/lib/jat/plugins/_activerecord_preloads/_activerecord_preloads_test.rb +34 -15
  40. data/test/lib/jat/plugins/_activerecord_preloads/lib/preloader_test.rb +10 -24
  41. data/test/lib/jat/plugins/_camel_lower/_camel_lower_test.rb +26 -0
  42. data/test/lib/jat/plugins/_preloads/lib/format_user_preloads_test.rb +1 -1
  43. data/test/lib/jat/plugins/_preloads/lib/preloads_with_path_test.rb +1 -1
  44. data/test/lib/jat/plugins/cache/cache_test.rb +11 -11
  45. data/test/lib/jat/plugins/json_api/json_api_test.rb +63 -47
  46. data/test/lib/jat/plugins/json_api/lib/{params/fields_test.rb → fields_param_parser_test.rb} +7 -6
  47. data/test/lib/jat/plugins/json_api/lib/{params/include_test.rb → include_param_parser_test.rb} +4 -4
  48. data/test/lib/jat/plugins/json_api/lib/map_test.rb +150 -79
  49. data/test/lib/jat/plugins/json_api/lib/response_test.rb +32 -32
  50. data/test/lib/jat/plugins/{_json_api_activerecord/_json_api_activerecord_test.rb → json_api_activerecord/json_api_activerecord_test.rb} +14 -5
  51. data/test/lib/jat/plugins/{_json_api_activerecord → json_api_activerecord}/lib/preloads_test.rb +11 -10
  52. data/test/lib/jat/plugins/json_api_camel_lower/json_api_camel_lower_test.rb +79 -0
  53. data/test/lib/jat/plugins/json_api_maps_cache/json_api_maps_cache_test.rb +107 -0
  54. data/test/lib/jat/plugins/json_api_validate_params/json_api_validate_params_test.rb +84 -0
  55. data/test/lib/jat/plugins/simple_api/lib/{params/parse_test.rb → fields_param_parser_test.rb} +10 -4
  56. data/test/lib/jat/plugins/simple_api/lib/map_test.rb +111 -34
  57. data/test/lib/jat/plugins/simple_api/lib/response_test.rb +80 -74
  58. data/test/lib/jat/plugins/simple_api/simple_api_test.rb +91 -25
  59. data/test/lib/jat/plugins/simple_api_activerecord/lib/preloads_test.rb +135 -0
  60. data/test/lib/jat/plugins/simple_api_activerecord/simple_api_activerecord_test.rb +38 -0
  61. data/test/lib/jat/plugins/simple_api_camel_lower/simple_api_camel_lower_test.rb +48 -0
  62. data/test/lib/jat/plugins/simple_api_maps_cache/simple_api_maps_cache_test.rb +95 -0
  63. data/test/lib/jat/plugins/simple_api_validate_params/simple_api_validate_params_test.rb +89 -0
  64. data/test/lib/jat/plugins/to_str/to_str_test.rb +3 -3
  65. data/test/lib/jat_test.rb +47 -24
  66. data/test/lib/plugin_test.rb +3 -3
  67. data/test/test_helper.rb +0 -3
  68. data/test/test_plugin.rb +9 -12
  69. metadata +60 -71
  70. data/CHANGELOG.md +0 -7
  71. data/README.md +0 -21
  72. data/jat.gemspec +0 -37
  73. data/lib/jat/plugins/_json_api_activerecord/_json_api_activerecord.rb +0 -22
  74. data/lib/jat/plugins/camel_lower/camel_lower.rb +0 -18
  75. data/lib/jat/plugins/json_api/lib/construct_traversal_map.rb +0 -91
  76. data/lib/jat/plugins/json_api/lib/presenters/document_links_presenter.rb +0 -48
  77. data/lib/jat/plugins/json_api/lib/presenters/document_meta_presenter.rb +0 -48
  78. data/lib/jat/plugins/json_api/lib/presenters/jsonapi_presenter.rb +0 -48
  79. data/lib/jat/plugins/json_api/lib/presenters/links_presenter.rb +0 -48
  80. data/lib/jat/plugins/json_api/lib/presenters/meta_presenter.rb +0 -48
  81. data/lib/jat/plugins/json_api/lib/presenters/relationship_links_presenter.rb +0 -53
  82. data/lib/jat/plugins/json_api/lib/presenters/relationship_meta_presenter.rb +0 -53
  83. data/lib/jat/plugins/json_api/lib/traversal_map.rb +0 -34
  84. data/lib/jat/plugins/simple_api/lib/construct_traversal_map.rb +0 -45
  85. data/lib/jat/plugins/simple_api/lib/params/parse.rb +0 -68
  86. data/lib/jat/presenter.rb +0 -51
  87. data/test/lib/jat/plugins/camel_lower/camel_lower_test.rb +0 -78
  88. data/test/lib/jat/plugins/json_api/lib/construct_traversal_map_test.rb +0 -119
  89. data/test/lib/jat/plugins/json_api/lib/params/fields/parse_test.rb +0 -24
  90. data/test/lib/jat/plugins/json_api/lib/params/fields/validate_test.rb +0 -47
  91. data/test/lib/jat/plugins/json_api/lib/params/include/parse_test.rb +0 -46
  92. data/test/lib/jat/plugins/json_api/lib/params/include/validate_test.rb +0 -51
  93. data/test/lib/jat/plugins/json_api/lib/presenters/document_links_presenter_test.rb +0 -69
  94. data/test/lib/jat/plugins/json_api/lib/presenters/document_meta_presenter_test.rb +0 -69
  95. data/test/lib/jat/plugins/json_api/lib/presenters/jsonapi_presenter_test.rb +0 -69
  96. data/test/lib/jat/plugins/json_api/lib/presenters/links_presenter_test.rb +0 -69
  97. data/test/lib/jat/plugins/json_api/lib/presenters/meta_presenter_test.rb +0 -69
  98. data/test/lib/jat/plugins/json_api/lib/presenters/relationship_links_presenter_test.rb +0 -75
  99. data/test/lib/jat/plugins/json_api/lib/presenters/relationship_meta_presenter_test.rb +0 -75
  100. data/test/lib/jat/plugins/json_api/lib/traversal_map_test.rb +0 -58
  101. data/test/lib/jat/plugins/simple_api/lib/construct_traversal_map_test.rb +0 -100
  102. 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.traversal_map.current).for(jat.class)
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
- @initial_result = {}
22
- append(initial_result, jat_class)
23
- initial_result
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.type]
34
- attributes_names = attrs[:attributes] + attrs[:relationships]
31
+ attrs = current_map[jat_class.get_type]
35
32
 
36
- add_attributes(result, jat_class, attributes_names)
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 addd preloads and nested preloads when nil provided
43
+ next unless preloads # we should not add preloads and nested preloads when nil provided
44
44
 
45
- add_preloads(result, preloads, attribute)
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,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Jat
4
+ class JsonApiParamsError < Error
5
+ end
6
+ 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