jat 0.0.1 → 0.0.3

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 (88) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +7 -0
  3. data/README.md +21 -0
  4. data/jat.gemspec +37 -0
  5. data/lib/jat/attribute.rb +85 -0
  6. data/lib/jat/config.rb +38 -0
  7. data/lib/jat/plugins/_activerecord_preloads/_activerecord_preloads.rb +29 -0
  8. data/lib/jat/plugins/_activerecord_preloads/lib/preloader.rb +89 -0
  9. data/lib/jat/plugins/_json_api_activerecord/_json_api_activerecord.rb +22 -0
  10. data/lib/jat/plugins/_json_api_activerecord/lib/preloads.rb +84 -0
  11. data/lib/jat/plugins/_preloads/_preloads.rb +53 -0
  12. data/lib/jat/plugins/_preloads/lib/format_user_preloads.rb +52 -0
  13. data/lib/jat/plugins/_preloads/lib/preloads_with_path.rb +78 -0
  14. data/lib/jat/plugins/cache/cache.rb +39 -0
  15. data/lib/jat/plugins/camel_lower/camel_lower.rb +18 -0
  16. data/lib/jat/plugins/json_api/json_api.rb +207 -0
  17. data/lib/jat/plugins/json_api/lib/construct_traversal_map.rb +91 -0
  18. data/lib/jat/plugins/json_api/lib/map.rb +54 -0
  19. data/lib/jat/plugins/json_api/lib/params/fields/parse.rb +27 -0
  20. data/lib/jat/plugins/json_api/lib/params/fields/validate.rb +55 -0
  21. data/lib/jat/plugins/json_api/lib/params/fields.rb +23 -0
  22. data/lib/jat/plugins/json_api/lib/params/include/parse.rb +55 -0
  23. data/lib/jat/plugins/json_api/lib/params/include/validate.rb +29 -0
  24. data/lib/jat/plugins/json_api/lib/params/include.rb +49 -0
  25. data/lib/jat/plugins/json_api/lib/presenters/document_links_presenter.rb +48 -0
  26. data/lib/jat/plugins/json_api/lib/presenters/document_meta_presenter.rb +48 -0
  27. data/lib/jat/plugins/json_api/lib/presenters/jsonapi_presenter.rb +48 -0
  28. data/lib/jat/plugins/json_api/lib/presenters/links_presenter.rb +48 -0
  29. data/lib/jat/plugins/json_api/lib/presenters/meta_presenter.rb +48 -0
  30. data/lib/jat/plugins/json_api/lib/presenters/relationship_links_presenter.rb +53 -0
  31. data/lib/jat/plugins/json_api/lib/presenters/relationship_meta_presenter.rb +53 -0
  32. data/lib/jat/plugins/json_api/lib/response.rb +239 -0
  33. data/lib/jat/plugins/json_api/lib/traversal_map.rb +34 -0
  34. data/lib/jat/plugins/simple_api/lib/construct_traversal_map.rb +45 -0
  35. data/lib/jat/plugins/simple_api/lib/map.rb +29 -0
  36. data/lib/jat/plugins/simple_api/lib/params/parse.rb +68 -0
  37. data/lib/jat/plugins/simple_api/lib/response.rb +134 -0
  38. data/lib/jat/plugins/simple_api/simple_api.rb +65 -0
  39. data/lib/jat/plugins/to_str/to_str.rb +44 -0
  40. data/lib/jat/plugins.rb +39 -0
  41. data/lib/jat/presenter.rb +51 -0
  42. data/lib/jat/utils/enum_deep_dup.rb +29 -0
  43. data/lib/jat/utils/enum_deep_freeze.rb +19 -0
  44. data/lib/jat.rb +66 -144
  45. data/test/lib/jat/attribute_test.rb +142 -0
  46. data/test/lib/jat/config_test.rb +57 -0
  47. data/test/lib/jat/plugins/_activerecord_preloads/_activerecord_preloads_test.rb +40 -0
  48. data/test/lib/jat/plugins/_activerecord_preloads/lib/preloader_test.rb +98 -0
  49. data/test/lib/jat/plugins/_json_api_activerecord/_json_api_activerecord_test.rb +29 -0
  50. data/test/lib/jat/plugins/_json_api_activerecord/lib/preloads_test.rb +191 -0
  51. data/test/lib/jat/plugins/_preloads/_preloads_test.rb +68 -0
  52. data/test/lib/jat/plugins/_preloads/lib/format_user_preloads_test.rb +47 -0
  53. data/test/lib/jat/plugins/_preloads/lib/preloads_with_path_test.rb +33 -0
  54. data/test/lib/jat/plugins/cache/cache_test.rb +82 -0
  55. data/test/lib/jat/plugins/camel_lower/camel_lower_test.rb +78 -0
  56. data/test/lib/jat/plugins/json_api/json_api_test.rb +154 -0
  57. data/test/lib/jat/plugins/json_api/lib/construct_traversal_map_test.rb +119 -0
  58. data/test/lib/jat/plugins/json_api/lib/map_test.rb +117 -0
  59. data/test/lib/jat/plugins/json_api/lib/params/fields/parse_test.rb +24 -0
  60. data/test/lib/jat/plugins/json_api/lib/params/fields/validate_test.rb +47 -0
  61. data/test/lib/jat/plugins/json_api/lib/params/fields_test.rb +37 -0
  62. data/test/lib/jat/plugins/json_api/lib/params/include/parse_test.rb +46 -0
  63. data/test/lib/jat/plugins/json_api/lib/params/include/validate_test.rb +51 -0
  64. data/test/lib/jat/plugins/json_api/lib/params/include_test.rb +41 -0
  65. data/test/lib/jat/plugins/json_api/lib/presenters/document_links_presenter_test.rb +69 -0
  66. data/test/lib/jat/plugins/json_api/lib/presenters/document_meta_presenter_test.rb +69 -0
  67. data/test/lib/jat/plugins/json_api/lib/presenters/jsonapi_presenter_test.rb +69 -0
  68. data/test/lib/jat/plugins/json_api/lib/presenters/links_presenter_test.rb +69 -0
  69. data/test/lib/jat/plugins/json_api/lib/presenters/meta_presenter_test.rb +69 -0
  70. data/test/lib/jat/plugins/json_api/lib/presenters/relationship_links_presenter_test.rb +75 -0
  71. data/test/lib/jat/plugins/json_api/lib/presenters/relationship_meta_presenter_test.rb +75 -0
  72. data/test/lib/jat/plugins/json_api/lib/response_test.rb +489 -0
  73. data/test/lib/jat/plugins/json_api/lib/traversal_map_test.rb +58 -0
  74. data/test/lib/jat/plugins/simple_api/lib/construct_traversal_map_test.rb +100 -0
  75. data/test/lib/jat/plugins/simple_api/lib/map_test.rb +56 -0
  76. data/test/lib/jat/plugins/simple_api/lib/params/parse_test.rb +71 -0
  77. data/test/lib/jat/plugins/simple_api/lib/response_test.rb +342 -0
  78. data/test/lib/jat/plugins/simple_api/simple_api_test.rb +81 -0
  79. data/test/lib/jat/plugins/to_str/to_str_test.rb +52 -0
  80. data/test/lib/jat/presenter_test.rb +61 -0
  81. data/test/lib/jat/utils/enum_deep_dup_test.rb +31 -0
  82. data/test/lib/jat/utils/enum_deep_freeze_test.rb +28 -0
  83. data/test/lib/jat_test.rb +120 -0
  84. data/test/lib/plugin_test.rb +49 -0
  85. data/test/support/activerecord.rb +24 -0
  86. data/test/test_helper.rb +16 -0
  87. data/test/test_plugin.rb +59 -0
  88. metadata +240 -11
@@ -0,0 +1,207 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "./lib/response"
4
+ require_relative "./lib/traversal_map"
5
+ require_relative "./lib/presenters/document_links_presenter"
6
+ require_relative "./lib/presenters/document_meta_presenter"
7
+ require_relative "./lib/presenters/jsonapi_presenter"
8
+ require_relative "./lib/presenters/links_presenter"
9
+ require_relative "./lib/presenters/meta_presenter"
10
+ require_relative "./lib/presenters/relationship_links_presenter"
11
+ require_relative "./lib/presenters/relationship_meta_presenter"
12
+
13
+ # Serializes to JSON-API format
14
+ class Jat
15
+ module Plugins
16
+ module JsonApi
17
+ def self.after_load(jat_class, **opts)
18
+ jat_class.plugin(:_json_api_activerecord, **opts) if opts[:activerecord]
19
+
20
+ jat_class.const_set(:JsonapiPresenter, Class.new(Jat::Presenters::JsonapiPresenter))
21
+ jat_class::JsonapiPresenter.jat_class = jat_class
22
+
23
+ jat_class.const_set(:LinksPresenter, Class.new(Jat::Presenters::LinksPresenter))
24
+ jat_class::LinksPresenter.jat_class = jat_class
25
+
26
+ jat_class.const_set(:DocumentLinksPresenter, Class.new(Jat::Presenters::DocumentLinksPresenter))
27
+ jat_class::DocumentLinksPresenter.jat_class = jat_class
28
+
29
+ jat_class.const_set(:RelationshipLinksPresenter, Class.new(Jat::Presenters::RelationshipLinksPresenter))
30
+ jat_class::RelationshipLinksPresenter.jat_class = jat_class
31
+
32
+ jat_class.const_set(:MetaPresenter, Class.new(Jat::Presenters::MetaPresenter))
33
+ jat_class::MetaPresenter.jat_class = jat_class
34
+
35
+ jat_class.const_set(:DocumentMetaPresenter, Class.new(Jat::Presenters::DocumentMetaPresenter))
36
+ jat_class::DocumentMetaPresenter.jat_class = jat_class
37
+
38
+ jat_class.const_set(:RelationshipMetaPresenter, Class.new(Jat::Presenters::RelationshipMetaPresenter))
39
+ jat_class::RelationshipMetaPresenter.jat_class = jat_class
40
+ end
41
+
42
+ module InstanceMethods
43
+ def to_h
44
+ Response.new(self).response
45
+ end
46
+
47
+ def traversal_map
48
+ @traversal_map ||= TraversalMap.new(self)
49
+ end
50
+ end
51
+
52
+ module ClassMethods
53
+ def inherited(subclass)
54
+ subclass.type(@type) if defined?(@type)
55
+
56
+ # Initialize JsonApi data Presenters
57
+ jsonapi_data_presenter_class = Class.new(self::Presenters::JsonapiPresenter)
58
+ jsonapi_data_presenter_class.jat_class = subclass
59
+ subclass.const_set(:JsonapiPresenter, jsonapi_data_presenter_class)
60
+ jsonapi_data.each { |key, block| subclass.jsonapi(key, &block) }
61
+
62
+ # Initialize Links Presenters
63
+ links_presenter_class = Class.new(self::Presenters::LinksPresenter)
64
+ links_presenter_class.jat_class = subclass
65
+ subclass.const_set(:LinksPresenter, links_presenter_class)
66
+ object_links.each { |key, block| subclass.object_link(key, &block) }
67
+
68
+ # Initialize DocumentLinks Presenters
69
+ document_links_presenter_class = Class.new(self::Presenters::DocumentLinksPresenter)
70
+ document_links_presenter_class.jat_class = subclass
71
+ subclass.const_set(:DocumentLinksPresenter, document_links_presenter_class)
72
+ document_links.each { |key, block| subclass.document_link(key, &block) }
73
+
74
+ # Initialize RelationshipLinks Presenters
75
+ relationship_links_presenter_class = Class.new(self::Presenters::RelationshipLinksPresenter)
76
+ relationship_links_presenter_class.jat_class = subclass
77
+ subclass.const_set(:RelationshipLinksPresenter, relationship_links_presenter_class)
78
+ relationship_links.each { |key, block| subclass.relationship_link(key, &block) }
79
+
80
+ # Initialize Meta Presenters
81
+ meta_presenter_class = Class.new(self::Presenters::MetaPresenter)
82
+ meta_presenter_class.jat_class = subclass
83
+ subclass.const_set(:MetaPresenter, meta_presenter_class)
84
+ added_object_meta.each { |key, block| subclass.object_meta(key, &block) }
85
+
86
+ # Initialize DocumentMeta Presenters
87
+ document_meta_presenter_class = Class.new(self::Presenters::DocumentMetaPresenter)
88
+ document_meta_presenter_class.jat_class = subclass
89
+ subclass.const_set(:DocumentMetaPresenter, document_meta_presenter_class)
90
+ added_document_meta.each { |key, block| subclass.document_meta(key, &block) }
91
+
92
+ # Initialize RelationshipMeta Presenters
93
+ relationship_meta_presenter_class = Class.new(self::Presenters::RelationshipMetaPresenter)
94
+ relationship_meta_presenter_class.jat_class = subclass
95
+ subclass.const_set(:RelationshipMetaPresenter, relationship_meta_presenter_class)
96
+ added_relationship_meta.each { |key, block| subclass.relationship_meta(key, &block) }
97
+
98
+ super
99
+ end
100
+
101
+ def type(new_type = nil)
102
+ return (defined?(@type) && @type) || raise(Error, "#{self} has no defined type") unless new_type
103
+
104
+ new_type = new_type.to_sym
105
+ @type = new_type
106
+ end
107
+
108
+ def relationship(name, serializer:, **opts, &block)
109
+ attribute(name, serializer: serializer, **opts, &block)
110
+ end
111
+
112
+ # JSON API block values
113
+ #
114
+ # https://jsonapi.org/format/#document-jsonapi-object
115
+ def jsonapi_data(value = nil)
116
+ @jsonapi_data ||= {}
117
+ end
118
+
119
+ def jsonapi(key, &block)
120
+ jsonapi_data[key.to_sym] = block
121
+ self::JsonapiPresenter.add_method(key, block)
122
+ block
123
+ end
124
+
125
+ # Links related to the resource
126
+ #
127
+ # https://jsonapi.org/format/#document-resource-object-links
128
+ def object_links
129
+ @object_links ||= {}
130
+ end
131
+
132
+ def object_link(key, &block)
133
+ object_links[key.to_sym] = block
134
+ self::LinksPresenter.add_method(key, block)
135
+ block
136
+ end
137
+
138
+ # Top-level document links
139
+ #
140
+ # https://jsonapi.org/format/#document-top-level
141
+ def document_links
142
+ @document_links ||= {}
143
+ end
144
+
145
+ def document_link(key, &block)
146
+ document_links[key.to_sym] = block
147
+ self::DocumentLinksPresenter.add_method(key, block)
148
+ block
149
+ end
150
+
151
+ # Relationship links
152
+ #
153
+ # https://jsonapi.org/format/#document-resource-object-linkage
154
+ def relationship_links
155
+ @relationship_links ||= {}
156
+ end
157
+
158
+ def relationship_link(key, &block)
159
+ relationship_links[key.to_sym] = block
160
+ self::RelationshipLinksPresenter.add_method(key, block)
161
+ block
162
+ end
163
+
164
+ # Object meta
165
+ #
166
+ # https://jsonapi.org/format/#document-resource-objects
167
+ def added_object_meta
168
+ @added_object_meta ||= {}
169
+ end
170
+
171
+ def object_meta(key, &block)
172
+ added_object_meta[key.to_sym] = block
173
+ self::MetaPresenter.add_method(key, block)
174
+ block
175
+ end
176
+
177
+ # Top-level document meta
178
+ #
179
+ # https://jsonapi.org/format/#document-meta
180
+ def added_document_meta
181
+ @added_document_meta ||= {}
182
+ end
183
+
184
+ def document_meta(key, &block)
185
+ added_document_meta[key.to_sym] = block
186
+ self::DocumentMetaPresenter.add_method(key, block)
187
+ block
188
+ end
189
+
190
+ # Relationship meta
191
+ #
192
+ # https://jsonapi.org/format/#document-resource-object-relationships
193
+ def added_relationship_meta
194
+ @added_relationship_meta ||= {}
195
+ end
196
+
197
+ def relationship_meta(key, &block)
198
+ added_relationship_meta[key.to_sym] = block
199
+ self::RelationshipMetaPresenter.add_method(key, block)
200
+ block
201
+ end
202
+ end
203
+ end
204
+
205
+ register_plugin(:json_api, JsonApi)
206
+ end
207
+ end
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ ##
4
+ # Combines all serializer and its realtions exposed fields into one hash.
5
+ # Returns Hash
6
+ # {
7
+ # type1 => {
8
+ # serializer: ser1,
9
+ # attributes: [attr1, attr2, ...],
10
+ # relationships: [attr1, attr2, ...]
11
+ # },
12
+ #
13
+ class Jat
14
+ module Plugins
15
+ module JsonApi
16
+ class ConstructTraversalMap
17
+ attr_reader :jat_class, :result, :exposed, :manually_exposed
18
+
19
+ EXPOSED_VALUES = {all: :all, exposed: :exposed, manual: :manual}.freeze
20
+
21
+ def initialize(jat_class, exposed, manually_exposed: {})
22
+ @jat_class = jat_class
23
+ @exposed = EXPOSED_VALUES.fetch(exposed)
24
+ @manually_exposed = manually_exposed
25
+ end
26
+
27
+ def to_h
28
+ @result = {}
29
+ append(jat_class)
30
+ result
31
+ end
32
+
33
+ private
34
+
35
+ def append(jat_class)
36
+ type = jat_class.type
37
+ return result if result.key?(type)
38
+
39
+ result[type] = {
40
+ serializer: jat_class,
41
+ attributes: [],
42
+ relationships: []
43
+ }
44
+
45
+ fill(jat_class)
46
+ end
47
+
48
+ def fill(jat_class)
49
+ type = jat_class.type
50
+ type_result = result[type]
51
+
52
+ jat_class.attributes.each_value do |attribute|
53
+ next unless expose?(type, attribute)
54
+
55
+ fill_attr(type_result, attribute)
56
+ end
57
+ end
58
+
59
+ def fill_attr(type_result, attribute)
60
+ name = attribute.name
61
+
62
+ if attribute.relation?
63
+ type_result[:relationships] << name
64
+ append(attribute.serializer.call)
65
+ else
66
+ type_result[:attributes] << name
67
+ end
68
+ end
69
+
70
+ def expose?(type, attribute)
71
+ return false if attribute.name == :id
72
+
73
+ case exposed
74
+ when :all then true
75
+ when :manual then manually_exposed?(type, attribute)
76
+ else attribute.exposed? || manually_exposed?(type, attribute)
77
+ end
78
+ end
79
+
80
+ # Return `attribute.exposed?` when type is not provided
81
+ # Checks type is in exposed attributes
82
+ def manually_exposed?(type, attribute)
83
+ return attribute.exposed? unless manually_exposed.key?(type)
84
+
85
+ exposed_attrs = manually_exposed[type]
86
+ exposed_attrs.include?(attribute.name)
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "./params/fields"
4
+ require_relative "./params/include"
5
+ require_relative "./construct_traversal_map"
6
+
7
+ class Jat
8
+ module Plugins
9
+ module JsonApi
10
+ class Map
11
+ class << self
12
+ # Returns structure like
13
+ # {
14
+ # type => {
15
+ # attributes: [attr1, attr2, ...],
16
+ # relationships: [rel1, rel2, ...]
17
+ # }
18
+ # }
19
+ def call(jat)
20
+ params = jat.context[:params]
21
+ fields = params && (params[:fields] || params["fields"])
22
+ includes = params && (params[:include] || params["include"])
23
+
24
+ default_attrs = jat.traversal_map.exposed
25
+ includes_attrs = requested_includes_fields(jat, includes)
26
+ fields_attrs = requested_fields(jat, fields)
27
+
28
+ default_attrs.merge!(includes_attrs).merge!(fields_attrs)
29
+ end
30
+
31
+ private
32
+
33
+ def requested_includes_fields(jat, includes)
34
+ return {} unless includes
35
+
36
+ include_types = Params::Include.call(jat, includes)
37
+ ConstructTraversalMap
38
+ .new(jat.class, :exposed, manually_exposed: include_types)
39
+ .to_h
40
+ end
41
+
42
+ def requested_fields(jat, fields)
43
+ return {} unless fields
44
+
45
+ fields_types = Params::Fields.call(jat, fields)
46
+ ConstructTraversalMap
47
+ .new(jat.class, :manual, manually_exposed: fields_types)
48
+ .to_h
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Jat
4
+ module Plugins
5
+ module JsonApi
6
+ module Params
7
+ class Fields
8
+ class Parse
9
+ COMMA = ","
10
+
11
+ class << self
12
+ # returns Hash { type => [attr1, attr2] }
13
+ def call(fields)
14
+ return {} unless fields
15
+
16
+ fields.each_with_object({}) do |(type, attrs_string), obj|
17
+ attrs = attrs_string.split(COMMA).map!(&:to_sym)
18
+ obj[type.to_sym] = attrs
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Jat
4
+ module Plugins
5
+ module JsonApi
6
+ module Params
7
+ class Fields
8
+ class Validate
9
+ class << self
10
+ def call(jat, fields)
11
+ fields.each do |type, attributes_names|
12
+ new(jat, type).validate(attributes_names)
13
+ end
14
+ end
15
+ end
16
+
17
+ attr_reader :jat, :type
18
+
19
+ def initialize(jat, type)
20
+ @jat = jat
21
+ @type = type
22
+ end
23
+
24
+ def validate(attributes_names)
25
+ check_fields_type
26
+ check_attributes_names(attributes_names)
27
+ end
28
+
29
+ private
30
+
31
+ def check_fields_type
32
+ return if jat.traversal_map.full.key?(type)
33
+
34
+ raise Error, "#{jat.class} and its children have no requested type `#{type}`"
35
+ end
36
+
37
+ def check_attributes_names(attributes_names)
38
+ attributes_names.each do |attribute_name|
39
+ check_attribute_name(attribute_name)
40
+ end
41
+ end
42
+
43
+ def check_attribute_name(attribute_name)
44
+ type_data = jat.traversal_map.full.fetch(type)
45
+ type_serializer = type_data.fetch(:serializer)
46
+ return if type_serializer.attributes.key?(attribute_name)
47
+
48
+ raise Error, "#{type_serializer} has no requested attribute or relationship `#{attribute_name}`"
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "./fields/parse"
4
+ require_relative "./fields/validate"
5
+
6
+ class Jat
7
+ module Plugins
8
+ module JsonApi
9
+ module Params
10
+ class Fields
11
+ class << self
12
+ # returns Hash { type => [key1, key2] }
13
+ def call(jat, fields)
14
+ parsed_fields = Parse.call(fields)
15
+ Validate.call(jat, parsed_fields)
16
+ parsed_fields
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Jat
4
+ module Plugins
5
+ module JsonApi
6
+ module Params
7
+ class Include
8
+ class Parse
9
+ COMMA = ","
10
+ DOT = "."
11
+
12
+ class << self
13
+ def call(includes_string)
14
+ return {} unless includes_string
15
+
16
+ string_to_hash(includes_string)
17
+ end
18
+
19
+ private
20
+
21
+ def string_to_hash(includes_string)
22
+ includes_string.split(COMMA).each_with_object({}) do |part, obj|
23
+ includes = parse_part(part)
24
+ deep_merge!(obj, includes)
25
+ end
26
+ end
27
+
28
+ def parse_part(part)
29
+ val = {}
30
+
31
+ part.split(DOT).reverse_each do |inc|
32
+ val = {inc.to_sym => val}
33
+ end
34
+
35
+ val
36
+ end
37
+
38
+ def deep_merge!(this_hash, other_hash)
39
+ this_hash.merge!(other_hash) do |_key, this_val, other_val|
40
+ deep_merge(this_val, other_val)
41
+ end
42
+ end
43
+
44
+ def deep_merge(this_hash, other_hash)
45
+ this_hash.merge(other_hash) do |_key, this_val, other_val|
46
+ deep_merge(this_val, other_val)
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Jat
4
+ module Plugins
5
+ module JsonApi
6
+ module Params
7
+ class Include
8
+ class Validate
9
+ class << self
10
+ def call(jat_class, includes)
11
+ includes.each do |name, nested_includes|
12
+ attribute = jat_class.attributes[name]
13
+ raise_error(jat_class, name) if !attribute || !attribute.relation?
14
+
15
+ nested_serializer = attribute.serializer.call
16
+ call(nested_serializer, nested_includes)
17
+ end
18
+ end
19
+
20
+ def raise_error(jat_class, name)
21
+ raise Error, "#{jat_class} has no `#{name}` relationship"
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "./include/parse"
4
+ require_relative "./include/validate"
5
+
6
+ class Jat
7
+ module Plugins
8
+ module JsonApi
9
+ module Params
10
+ class Include
11
+ class << self
12
+ # returns Hash { type => [attr1, attr2] }
13
+ def call(jat, includes_string)
14
+ return {} unless includes_string
15
+
16
+ jat_class = jat.class
17
+ includes = Parse.call(includes_string)
18
+ Validate.call(jat_class, includes)
19
+
20
+ typed_includes(jat_class, includes, {})
21
+ end
22
+
23
+ private
24
+
25
+ def typed_includes(jat_class, includes, result)
26
+ includes.each do |included_attr_name, nested_includes|
27
+ add_include(result, jat_class, included_attr_name)
28
+
29
+ nested_serializer = jat_class.attributes.fetch(included_attr_name).serializer.call
30
+ typed_includes(nested_serializer, nested_includes, result)
31
+ end
32
+
33
+ result
34
+ end
35
+
36
+ def add_include(result, serializer, included_attr_name)
37
+ type = serializer.type
38
+
39
+ includes = result[type] || []
40
+ includes |= [included_attr_name]
41
+
42
+ result[type] = includes
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Jat
4
+ module Presenters
5
+ class DocumentLinksPresenter
6
+ module InstanceMethods
7
+ # Presented object
8
+ attr_reader :object
9
+
10
+ # Presented context
11
+ attr_reader :context
12
+
13
+ def initialize(object, context)
14
+ @object = object
15
+ @context = context
16
+ end
17
+ end
18
+
19
+ module ClassMethods
20
+ attr_accessor :jat_class
21
+
22
+ def inspect
23
+ "#{jat_class.inspect}::Presenters::DocumentLinksPresenter"
24
+ end
25
+
26
+ def add_method(name, block)
27
+ # Warning-free method redefinition
28
+ remove_method(name) if method_defined?(name, false)
29
+ define_method(name, &block_without_args(block))
30
+ end
31
+
32
+ private
33
+
34
+ def block_without_args(block)
35
+ case block.parameters.count
36
+ when 0 then block
37
+ when 1 then -> { instance_exec(object, &block) }
38
+ when 2 then -> { instance_exec(object, context, &block) }
39
+ else raise Error, "Invalid block arguments count"
40
+ end
41
+ end
42
+ end
43
+
44
+ extend ClassMethods
45
+ include InstanceMethods
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Jat
4
+ module Presenters
5
+ class DocumentMetaPresenter
6
+ module InstanceMethods
7
+ # Presented object
8
+ attr_reader :object
9
+
10
+ # Presented context
11
+ attr_reader :context
12
+
13
+ def initialize(object, context)
14
+ @object = object
15
+ @context = context
16
+ end
17
+ end
18
+
19
+ module ClassMethods
20
+ attr_accessor :jat_class
21
+
22
+ def inspect
23
+ "#{jat_class.inspect}::Presenters::DocumentMetaPresenter"
24
+ end
25
+
26
+ def add_method(name, block)
27
+ # Warning-free method redefinition
28
+ remove_method(name) if method_defined?(name, false)
29
+ define_method(name, &block_without_args(block))
30
+ end
31
+
32
+ private
33
+
34
+ def block_without_args(block)
35
+ case block.parameters.count
36
+ when 0 then block
37
+ when 1 then -> { instance_exec(object, &block) }
38
+ when 2 then -> { instance_exec(object, context, &block) }
39
+ else raise Error, "Invalid block arguments count"
40
+ end
41
+ end
42
+ end
43
+
44
+ extend ClassMethods
45
+ include InstanceMethods
46
+ end
47
+ end
48
+ end