jsonapi-materializer 1.0.0.rc2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,21 @@
1
+ module JSONAPI
2
+ module Materializer
3
+ class Configuration
4
+ include(ActiveModel::Model)
5
+
6
+ attr_accessor(:default_origin)
7
+ attr_accessor(:default_identifier)
8
+ attr_accessor(:default_missing_accept_exception)
9
+ attr_accessor(:default_invalid_accept_exception)
10
+
11
+ validates_presence_of(:default_missing_accept_exception)
12
+ validates_presence_of(:default_invalid_accept_exception)
13
+
14
+ def initialize(**keyword_arguments)
15
+ super(**keyword_arguments)
16
+
17
+ validate!
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,8 @@
1
+ module JSONAPI
2
+ module Materializer
3
+ module Context
4
+ extend(ActiveSupport::Concern)
5
+ include(ActiveModel::Model)
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,13 @@
1
+ module JSONAPI
2
+ module Materializer
3
+ module Controller
4
+ private def reject_missing_accept_header
5
+ raise(JSONAPI::Materializer.configuration.default_missing_accept_exception) unless request.headers.key?("Accept")
6
+ end
7
+
8
+ private def reject_invalid_accept_header
9
+ raise(JSONAPI::Materializer.configuration.default_invalid_accept_exception) unless request.headers.fetch("Accept").include?(JSONAPI::MEDIA_TYPE)
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,12 @@
1
+ module JSONAPI
2
+ module Materializer
3
+ class Error < StandardError
4
+ include(ActiveModel::Model)
5
+
6
+ require_relative("error/invalid_accept_header")
7
+ require_relative("error/missing_accept_header")
8
+ require_relative("error/resource_attribute_not_found")
9
+ require_relative("error/resource_relationship_not_found")
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,8 @@
1
+ module JSONAPI
2
+ module Materializer
3
+ class Error
4
+ class InvalidAcceptHeader < Error
5
+ end
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,8 @@
1
+ module JSONAPI
2
+ module Materializer
3
+ class Error
4
+ class MissingAcceptHeader < Error
5
+ end
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,14 @@
1
+ module JSONAPI
2
+ module Materializer
3
+ class Error
4
+ class ResourceAttributeNotFound < Error
5
+ attr_accessor(:name)
6
+ attr_accessor(:materializer)
7
+
8
+ def message
9
+ "#{materializer} doesn't define the attribute #{name}"
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,14 @@
1
+ module JSONAPI
2
+ module Materializer
3
+ class Error
4
+ class ResourceRelationshipNotFound < Error
5
+ attr_accessor(:name)
6
+ attr_accessor(:materializer)
7
+
8
+ def message
9
+ "#{materializer} doesn't define the relationship #{name}"
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,231 @@
1
+ module JSONAPI
2
+ module Materializer
3
+ module Resource
4
+ require_relative("resource/attribute")
5
+ require_relative("resource/relation")
6
+ require_relative("resource/relationship")
7
+ require_relative("resource/configuration")
8
+
9
+ extend(ActiveSupport::Concern)
10
+ include(ActiveModel::Model)
11
+
12
+ MIXIN_HOOK = ->(*) do
13
+ @attributes = {}
14
+ @relations = {}
15
+
16
+ unless const_defined?("Collection")
17
+ self::Collection = Class.new do
18
+ include(JSONAPI::Materializer::Collection)
19
+ end
20
+ end
21
+
22
+ unless const_defined?("Context")
23
+ self::Context = Class.new do
24
+ include(JSONAPI::Materializer::Context)
25
+
26
+ def initialize(**keyword_arguments)
27
+ keyword_arguments.keys.each(&singleton_class.method(:attr_accessor))
28
+
29
+ super(**keyword_arguments)
30
+ end
31
+ end
32
+ end
33
+
34
+ validates_presence_of(:object)
35
+
36
+ origin(JSONAPI::Materializer.configuration.default_origin)
37
+ identifier(JSONAPI::Materializer.configuration.default_identifier)
38
+
39
+ has(JSONAPI::Materializer.configuration.default_identifier)
40
+ end
41
+
42
+ attr_accessor(:object)
43
+ attr_writer(:selects)
44
+ attr_writer(:includes)
45
+ attr_writer(:context)
46
+ attr_reader(:raw)
47
+
48
+ def initialize(**keyword_arguments)
49
+ super(**keyword_arguments)
50
+
51
+ @raw = keyword_arguments
52
+
53
+ context.validate!
54
+ validate!
55
+ end
56
+
57
+ def as_data
58
+ {
59
+ :id => id,
60
+ :type => type,
61
+ :attributes => exposed(attributes.except(:id)).
62
+ transform_values {|attribute| object.public_send(attribute.from)},
63
+ :relationships => exposed(relations).
64
+ transform_values {|relation| relation.using(self).as_json},
65
+ :links => {
66
+ :self => links_self
67
+ }
68
+ }.transform_values(&:presence).compact
69
+ end
70
+
71
+ private def exposed(mapping)
72
+ if selects.any?
73
+ mapping.
74
+ select {|_, value| value.visible?(self)}.
75
+ slice(*selects.dig(type))
76
+ else
77
+ mapping.
78
+ select {|_, value| value.visible?(self)}
79
+ end
80
+ end
81
+
82
+ def as_json(*)
83
+ {
84
+ :links => {
85
+ :self => links_self
86
+ },
87
+ :data => as_data,
88
+ :included => included
89
+ }.transform_values(&:presence).compact
90
+ end
91
+
92
+ private def id
93
+ object.public_send(identifier).to_s
94
+ end
95
+
96
+ def type
97
+ self.class.configuration.type.to_s
98
+ end
99
+
100
+ private def attributes
101
+ self.class.configuration.attributes
102
+ end
103
+
104
+ private def origin
105
+ self.class.configuration.origin
106
+ end
107
+
108
+ private def identifier
109
+ self.class.configuration.identifier
110
+ end
111
+
112
+ private def relations
113
+ self.class.configuration.relations
114
+ end
115
+
116
+ def attribute(name)
117
+ self.class.attribute(name)
118
+ end
119
+
120
+ def relation(name)
121
+ self.class.relation(name)
122
+ end
123
+
124
+ def links_self
125
+ Addressable::Template.new(
126
+ "#{origin}/#{type}/#{object.public_send(identifier)}"
127
+ ).pattern
128
+ end
129
+
130
+ def selects
131
+ (@selects || {}).transform_values {|list| list.map(&:to_sym)}
132
+ end
133
+
134
+ def includes
135
+ @includes || []
136
+ end
137
+
138
+ def context
139
+ self.class.const_get("Context").new(**@context || {})
140
+ end
141
+
142
+ private def included
143
+ @included ||= includes.flat_map do |path|
144
+ path.reduce(self) do |subject, key|
145
+ if subject.is_a?(Array)
146
+ subject.map {|related_subject| related_subject.relation(key).for(subject)}
147
+ else
148
+ subject.relation(key).for(subject)
149
+ end
150
+ end
151
+ end.map(&:as_data)
152
+ end
153
+
154
+ included do
155
+ class_eval(&MIXIN_HOOK) unless @abstract_class
156
+ end
157
+
158
+ class_methods do
159
+ def inherited(object)
160
+ object.class_eval(&MIXIN_HOOK) unless object.instance_variable_defined?(:@abstract_class)
161
+ end
162
+
163
+ def identifier(value)
164
+ @identifier = value.to_sym
165
+ end
166
+
167
+ def origin(value)
168
+ @origin = value
169
+ end
170
+
171
+ def type(value)
172
+ @type = value.to_sym
173
+ end
174
+
175
+ def has(name, from: name, visible: true)
176
+ @attributes[name] = Attribute.new(
177
+ :owner => self,
178
+ :name => name,
179
+ :from => from,
180
+ :visible => visible
181
+ )
182
+ end
183
+
184
+ def has_one(name, from: name, class_name:, visible: true)
185
+ @relations[name] = Relation.new(
186
+ :owner => self,
187
+ :type => :one,
188
+ :name => name,
189
+ :from => from,
190
+ :class_name => class_name,
191
+ :visible => visible
192
+ )
193
+ end
194
+
195
+ def has_many(name, from: name, class_name:, visible: true)
196
+ @relations[name] = Relation.new(
197
+ :owner => self,
198
+ :type => :many,
199
+ :name => name,
200
+ :from => from,
201
+ :class_name => class_name,
202
+ :visible => visible
203
+ )
204
+ end
205
+
206
+ def context
207
+ const_get("Context")
208
+ end
209
+
210
+ def configuration
211
+ @configuration ||= Configuration.new(
212
+ :owner => self,
213
+ :type => @type,
214
+ :origin => @origin,
215
+ :identifier => @identifier,
216
+ :attributes => @attributes,
217
+ :relations => @relations
218
+ )
219
+ end
220
+
221
+ def attribute(name)
222
+ configuration.attributes.fetch(name.to_sym) {raise(Error::ResourceRelationshipNotFound, :name => name, :materializer => self)}
223
+ end
224
+
225
+ def relation(name)
226
+ configuration.relations.fetch(name.to_sym) {raise(Error::ResourceRelationshipNotFound, :name => name, :materializer => self)}
227
+ end
228
+ end
229
+ end
230
+ end
231
+ end
@@ -0,0 +1,49 @@
1
+ module JSONAPI
2
+ module Materializer
3
+ module Resource
4
+ class Attribute
5
+ include(ActiveModel::Model)
6
+
7
+ attr_accessor(:owner)
8
+ attr_accessor(:name)
9
+ attr_accessor(:from)
10
+ attr_accessor(:visible)
11
+
12
+ validates_presence_of(:owner)
13
+ validates_presence_of(:name)
14
+ validates_presence_of(:from)
15
+ validate(:visible_callable)
16
+
17
+ def initialize(**keyword_arguments)
18
+ super(**keyword_arguments)
19
+
20
+ validate!
21
+ end
22
+
23
+ def for(subject)
24
+ subject.object.public_send(from)
25
+ end
26
+
27
+ def visible?(subject)
28
+ return visible if [true, false].include?(visible)
29
+ return subject.send(visible, self) if visible.is_a?(Symbol)
30
+ return visible.call(self) if visible.respond_to?(:call)
31
+
32
+ true
33
+ end
34
+
35
+ private def materializer_class
36
+ class_name.constantize
37
+ end
38
+
39
+ private def visible_callable
40
+ return if [true, false].include?(visible)
41
+ return if visible.is_a?(Symbol)
42
+ return if visible.respond_to?(:call)
43
+
44
+ errors.add(:visible, "not callable or boolean")
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,27 @@
1
+ module JSONAPI
2
+ module Materializer
3
+ module Resource
4
+ class Configuration
5
+ include(ActiveModel::Model)
6
+
7
+ attr_accessor(:owner)
8
+ attr_accessor(:type)
9
+ attr_accessor(:origin)
10
+ attr_accessor(:identifier)
11
+ attr_accessor(:attributes)
12
+ attr_accessor(:relations)
13
+
14
+ validates_presence_of(:owner)
15
+ validates_presence_of(:type)
16
+ validates_presence_of(:origin)
17
+ validates_presence_of(:identifier)
18
+
19
+ def initialize(**keyword_arguments)
20
+ super(**keyword_arguments)
21
+
22
+ validate!
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,107 @@
1
+ module JSONAPI
2
+ module Materializer
3
+ module Resource
4
+ class Relation
5
+ include(ActiveModel::Model)
6
+
7
+ attr_accessor(:owner)
8
+ attr_accessor(:name)
9
+ attr_accessor(:type)
10
+ attr_accessor(:from)
11
+ attr_accessor(:class_name)
12
+ attr_accessor(:visible)
13
+
14
+ validates_presence_of(:owner)
15
+ validates_presence_of(:name)
16
+ validates_presence_of(:type)
17
+ validates_presence_of(:from)
18
+ validates_presence_of(:class_name)
19
+ validate(:visible_callable)
20
+
21
+ def initialize(**keyword_arguments)
22
+ super(**keyword_arguments)
23
+
24
+ validate!
25
+ end
26
+
27
+ def for(subject)
28
+ @for ||= {}
29
+ @for[checksum(subject)] ||= case type
30
+ when :many then
31
+ unlessing(fetch_relation(subject), -> {subject.includes.any? {|included| included.include?(from.to_s)} || fetch_relation(subject).loaded?}) do |subject|
32
+ subject.select(:id)
33
+ end.map do |related_object|
34
+ materializer_class.new(
35
+ **subject.raw,
36
+ :object => related_object
37
+ )
38
+ end
39
+ when :one then
40
+ if fetch_relation(subject).present?
41
+ materializer_class.new(
42
+ **subject.raw,
43
+ :object => fetch_relation(subject)
44
+ )
45
+ end
46
+ end
47
+ end
48
+
49
+ def visible?(subject)
50
+ return visible if [true, false].include?(visible)
51
+ return subject.send(visible, self) if visible.is_a?(Symbol)
52
+ return visible.call(self) if visible.respond_to?(:call)
53
+
54
+ true
55
+ end
56
+
57
+ private def fetch_relation(subject)
58
+ @fetch_relationship ||= {}
59
+ @fetch_relationship[checksum(subject)] ||= subject.object.public_send(from)
60
+ end
61
+
62
+ def using(parent)
63
+ Resource::Relationship.new(:related => self, :parent => parent)
64
+ end
65
+
66
+ def many?
67
+ type == :many
68
+ end
69
+
70
+ def one?
71
+ type == :one
72
+ end
73
+
74
+ private def materializer_class
75
+ class_name.constantize
76
+ end
77
+
78
+ private def visible_callable
79
+ return if [true, false].include?(visible)
80
+ return if visible.is_a?(Symbol)
81
+ return if visible.respond_to?(:call)
82
+
83
+ errors.add(:visible, "not callable or boolean")
84
+ end
85
+
86
+ private def unlessing(object, proc)
87
+ unless proc.call()
88
+ yield(object)
89
+ else
90
+ object
91
+ end
92
+ end
93
+
94
+ private def checksum(subject)
95
+ [
96
+ from,
97
+ materializer_class,
98
+ name,
99
+ owner,
100
+ subject,
101
+ type
102
+ ].hash
103
+ end
104
+ end
105
+ end
106
+ end
107
+ end