jat 0.0.1 → 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
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