sanger-jsonapi-resources 0.1.0
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 +7 -0
- data/LICENSE.txt +22 -0
- data/README.md +53 -0
- data/lib/generators/jsonapi/USAGE +13 -0
- data/lib/generators/jsonapi/controller_generator.rb +14 -0
- data/lib/generators/jsonapi/resource_generator.rb +14 -0
- data/lib/generators/jsonapi/templates/jsonapi_controller.rb +4 -0
- data/lib/generators/jsonapi/templates/jsonapi_resource.rb +4 -0
- data/lib/jsonapi/acts_as_resource_controller.rb +320 -0
- data/lib/jsonapi/cached_resource_fragment.rb +127 -0
- data/lib/jsonapi/callbacks.rb +51 -0
- data/lib/jsonapi/compiled_json.rb +36 -0
- data/lib/jsonapi/configuration.rb +258 -0
- data/lib/jsonapi/error.rb +47 -0
- data/lib/jsonapi/error_codes.rb +60 -0
- data/lib/jsonapi/exceptions.rb +563 -0
- data/lib/jsonapi/formatter.rb +169 -0
- data/lib/jsonapi/include_directives.rb +100 -0
- data/lib/jsonapi/link_builder.rb +152 -0
- data/lib/jsonapi/mime_types.rb +41 -0
- data/lib/jsonapi/naive_cache.rb +30 -0
- data/lib/jsonapi/operation.rb +24 -0
- data/lib/jsonapi/operation_dispatcher.rb +88 -0
- data/lib/jsonapi/operation_result.rb +65 -0
- data/lib/jsonapi/operation_results.rb +35 -0
- data/lib/jsonapi/paginator.rb +209 -0
- data/lib/jsonapi/processor.rb +328 -0
- data/lib/jsonapi/relationship.rb +94 -0
- data/lib/jsonapi/relationship_builder.rb +167 -0
- data/lib/jsonapi/request_parser.rb +678 -0
- data/lib/jsonapi/resource.rb +1255 -0
- data/lib/jsonapi/resource_controller.rb +5 -0
- data/lib/jsonapi/resource_controller_metal.rb +16 -0
- data/lib/jsonapi/resource_serializer.rb +531 -0
- data/lib/jsonapi/resources/version.rb +5 -0
- data/lib/jsonapi/response_document.rb +135 -0
- data/lib/jsonapi/routing_ext.rb +262 -0
- data/lib/jsonapi-resources.rb +27 -0
- metadata +223 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: bf908bb5423dd9c31362ed50c78a5089fd02fa120278d543d90d83d99690acb1
|
4
|
+
data.tar.gz: 1118019c84568daf2ee120327d821dc1e930d210947bf7c9ab0ff6006adec87e
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: bfff352e247bb4b77eab4df52557e91768498155f60084cb20b0e1e66b38a50f4199c07a27af97df97d9672394ff2cf7ef320bde343de7ebd206bb6fa899b4c0
|
7
|
+
data.tar.gz: cf361701487602881c1f2271165be8295f187cf520efcf3324a47b2265efc2926b5642dc33d6cf89da6a63daf196a863aa472ee59436db04ecd5647a64abb5d0
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2014 Larry Gebhardt
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,53 @@
|
|
1
|
+
# JSONAPI::Resources [](https://badge.fury.io/rb/jsonapi-resources) [](http://travis-ci.org/cerebris/jsonapi-resources) [](https://codeclimate.com/github/cerebris/jsonapi-resources)
|
2
|
+
|
3
|
+
[](https://gitter.im/cerebris/jsonapi-resources?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
4
|
+
|
5
|
+
`JSONAPI::Resources`, or "JR", provides a framework for developing an API server that complies with the
|
6
|
+
[JSON:API](http://jsonapi.org/) specification.
|
7
|
+
|
8
|
+
Like JSON:API itself, JR's design is focused on the resources served by an API. JR needs little more than a definition
|
9
|
+
of your resources, including their attributes and relationships, to make your server compliant with JSON API.
|
10
|
+
|
11
|
+
JR is designed to work with Rails 4.2+, and provides custom routes, controllers, and serializers. JR's resources may be
|
12
|
+
backed by ActiveRecord models or by custom objects.
|
13
|
+
|
14
|
+
## Documentation
|
15
|
+
|
16
|
+
Full documentation can be found at [http://jsonapi-resources.com](http://jsonapi-resources.com), including the [v0.9 beta Guide](http://jsonapi-resources.com/v0.9/guide/) specific to this version.
|
17
|
+
|
18
|
+
## Demo App
|
19
|
+
|
20
|
+
We have a simple demo app, called [Peeps](https://github.com/cerebris/peeps), available to show how JR is used.
|
21
|
+
|
22
|
+
## Client Libraries
|
23
|
+
|
24
|
+
JSON:API maintains a (non-verified) listing of [client libraries](http://jsonapi.org/implementations/#client-libraries)
|
25
|
+
which *should* be compatible with JSON:API compliant server implementations such as JR.
|
26
|
+
|
27
|
+
## Installation
|
28
|
+
|
29
|
+
Add JR to your application's `Gemfile`:
|
30
|
+
|
31
|
+
gem 'jsonapi-resources'
|
32
|
+
|
33
|
+
And then execute:
|
34
|
+
|
35
|
+
$ bundle
|
36
|
+
|
37
|
+
Or install it yourself as:
|
38
|
+
|
39
|
+
$ gem install jsonapi-resources
|
40
|
+
|
41
|
+
**For further usage see the [v0.9 beta Guide](http://jsonapi-resources.com/v0.9/guide/)**
|
42
|
+
|
43
|
+
## Contributing
|
44
|
+
|
45
|
+
1. Fork it ( http://github.com/cerebris/jsonapi-resources/fork )
|
46
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
47
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
48
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
49
|
+
5. Create a new Pull Request
|
50
|
+
|
51
|
+
## License
|
52
|
+
|
53
|
+
Copyright 2014-2016 Cerebris Corporation. MIT License (see LICENSE for details).
|
@@ -0,0 +1,13 @@
|
|
1
|
+
Description:
|
2
|
+
Generator for JSONAPI Resources
|
3
|
+
|
4
|
+
Examples:
|
5
|
+
rails generate jsonapi:resource Post
|
6
|
+
|
7
|
+
This will create:
|
8
|
+
app/resources/post_resource.rb
|
9
|
+
|
10
|
+
rails generate jsonapi:controller Post
|
11
|
+
|
12
|
+
This will create:
|
13
|
+
app/controllers/posts_controller.rb
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module Jsonapi
|
2
|
+
class ControllerGenerator < Rails::Generators::NamedBase
|
3
|
+
source_root File.expand_path('../templates', __FILE__)
|
4
|
+
|
5
|
+
def create_resource
|
6
|
+
template_file = File.join(
|
7
|
+
'app/controllers',
|
8
|
+
class_path,
|
9
|
+
"#{file_name.pluralize}_controller.rb"
|
10
|
+
)
|
11
|
+
template 'jsonapi_controller.rb', template_file
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module Jsonapi
|
2
|
+
class ResourceGenerator < Rails::Generators::NamedBase
|
3
|
+
source_root File.expand_path('../templates', __FILE__)
|
4
|
+
|
5
|
+
def create_resource
|
6
|
+
template_file = File.join(
|
7
|
+
'app/resources',
|
8
|
+
class_path,
|
9
|
+
"#{file_name.singularize}_resource.rb"
|
10
|
+
)
|
11
|
+
template 'jsonapi_resource.rb', template_file
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,320 @@
|
|
1
|
+
require 'csv'
|
2
|
+
|
3
|
+
module JSONAPI
|
4
|
+
module ActsAsResourceController
|
5
|
+
MEDIA_TYPE_MATCHER = /.+".+"[^,]*|[^,]+/
|
6
|
+
ALL_MEDIA_TYPES = '*/*'
|
7
|
+
|
8
|
+
def self.included(base)
|
9
|
+
base.extend ClassMethods
|
10
|
+
base.include Callbacks
|
11
|
+
base.cattr_reader :server_error_callbacks
|
12
|
+
base.define_jsonapi_resources_callbacks :process_operations
|
13
|
+
end
|
14
|
+
|
15
|
+
def index
|
16
|
+
process_request
|
17
|
+
end
|
18
|
+
|
19
|
+
def show
|
20
|
+
process_request
|
21
|
+
end
|
22
|
+
|
23
|
+
def show_relationship
|
24
|
+
process_request
|
25
|
+
end
|
26
|
+
|
27
|
+
def create
|
28
|
+
return unless verify_content_type_header
|
29
|
+
process_request
|
30
|
+
end
|
31
|
+
|
32
|
+
def create_relationship
|
33
|
+
return unless verify_content_type_header
|
34
|
+
process_request
|
35
|
+
end
|
36
|
+
|
37
|
+
def update_relationship
|
38
|
+
return unless verify_content_type_header
|
39
|
+
process_request
|
40
|
+
end
|
41
|
+
|
42
|
+
def update
|
43
|
+
return unless verify_content_type_header
|
44
|
+
process_request
|
45
|
+
end
|
46
|
+
|
47
|
+
def destroy
|
48
|
+
process_request
|
49
|
+
end
|
50
|
+
|
51
|
+
def destroy_relationship
|
52
|
+
process_request
|
53
|
+
end
|
54
|
+
|
55
|
+
def get_related_resource
|
56
|
+
process_request
|
57
|
+
end
|
58
|
+
|
59
|
+
def get_related_resources
|
60
|
+
process_request
|
61
|
+
end
|
62
|
+
|
63
|
+
def process_request
|
64
|
+
return unless verify_accept_header
|
65
|
+
|
66
|
+
@request = JSONAPI::RequestParser.new(params, context: context,
|
67
|
+
key_formatter: key_formatter,
|
68
|
+
server_error_callbacks: (self.class.server_error_callbacks || []))
|
69
|
+
|
70
|
+
unless @request.errors.empty?
|
71
|
+
render_errors(@request.errors)
|
72
|
+
else
|
73
|
+
operations = @request.operations
|
74
|
+
unless JSONAPI.configuration.resource_cache.nil?
|
75
|
+
operations.each {|op| op.options[:cache_serializer] = resource_serializer }
|
76
|
+
end
|
77
|
+
results = process_operations(operations)
|
78
|
+
render_results(results)
|
79
|
+
end
|
80
|
+
rescue => e
|
81
|
+
handle_exceptions(e)
|
82
|
+
end
|
83
|
+
|
84
|
+
def process_operations(operations)
|
85
|
+
run_callbacks :process_operations do
|
86
|
+
operation_dispatcher.process(operations)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def transaction
|
91
|
+
lambda { |&block|
|
92
|
+
ActiveRecord::Base.transaction do
|
93
|
+
block.yield
|
94
|
+
end
|
95
|
+
}
|
96
|
+
end
|
97
|
+
|
98
|
+
def rollback
|
99
|
+
lambda {
|
100
|
+
fail ActiveRecord::Rollback
|
101
|
+
}
|
102
|
+
end
|
103
|
+
|
104
|
+
def operation_dispatcher
|
105
|
+
@operation_dispatcher ||= JSONAPI::OperationDispatcher.new(transaction: transaction,
|
106
|
+
rollback: rollback,
|
107
|
+
server_error_callbacks: @request.server_error_callbacks)
|
108
|
+
end
|
109
|
+
|
110
|
+
private
|
111
|
+
|
112
|
+
def resource_klass
|
113
|
+
@resource_klass ||= resource_klass_name.safe_constantize
|
114
|
+
end
|
115
|
+
|
116
|
+
def resource_serializer_klass
|
117
|
+
@resource_serializer_klass ||= JSONAPI::ResourceSerializer
|
118
|
+
end
|
119
|
+
|
120
|
+
def resource_serializer
|
121
|
+
@resource_serializer ||= resource_serializer_klass.new(
|
122
|
+
resource_klass,
|
123
|
+
include_directives: @request ? @request.include_directives : nil,
|
124
|
+
fields: @request ? @request.fields : {},
|
125
|
+
base_url: base_url,
|
126
|
+
key_formatter: key_formatter,
|
127
|
+
route_formatter: route_formatter,
|
128
|
+
serialization_options: serialization_options
|
129
|
+
)
|
130
|
+
@resource_serializer
|
131
|
+
end
|
132
|
+
|
133
|
+
def base_url
|
134
|
+
@base_url ||= request.protocol + request.host_with_port
|
135
|
+
end
|
136
|
+
|
137
|
+
def resource_klass_name
|
138
|
+
@resource_klass_name ||= "#{self.class.name.underscore.sub(/_controller$/, '').singularize}_resource".camelize
|
139
|
+
end
|
140
|
+
|
141
|
+
def verify_content_type_header
|
142
|
+
unless request.content_type == JSONAPI::MEDIA_TYPE
|
143
|
+
fail JSONAPI::Exceptions::UnsupportedMediaTypeError.new(request.content_type)
|
144
|
+
end
|
145
|
+
true
|
146
|
+
rescue => e
|
147
|
+
handle_exceptions(e)
|
148
|
+
false
|
149
|
+
end
|
150
|
+
|
151
|
+
def verify_accept_header
|
152
|
+
unless valid_accept_media_type?
|
153
|
+
fail JSONAPI::Exceptions::NotAcceptableError.new(request.accept)
|
154
|
+
end
|
155
|
+
true
|
156
|
+
rescue => e
|
157
|
+
handle_exceptions(e)
|
158
|
+
false
|
159
|
+
end
|
160
|
+
|
161
|
+
def valid_accept_media_type?
|
162
|
+
media_types = media_types_for('Accept')
|
163
|
+
|
164
|
+
media_types.blank? ||
|
165
|
+
media_types.any? do |media_type|
|
166
|
+
(media_type == JSONAPI::MEDIA_TYPE || media_type.start_with?(ALL_MEDIA_TYPES))
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
def media_types_for(header)
|
171
|
+
(request.headers[header] || '')
|
172
|
+
.scan(MEDIA_TYPE_MATCHER)
|
173
|
+
.to_a
|
174
|
+
.map(&:strip)
|
175
|
+
end
|
176
|
+
|
177
|
+
# override to set context
|
178
|
+
def context
|
179
|
+
{}
|
180
|
+
end
|
181
|
+
|
182
|
+
def serialization_options
|
183
|
+
{}
|
184
|
+
end
|
185
|
+
|
186
|
+
# Control by setting in an initializer:
|
187
|
+
# JSONAPI.configuration.json_key_format = :camelized_key
|
188
|
+
# JSONAPI.configuration.route = :camelized_route
|
189
|
+
#
|
190
|
+
# Override if you want to set a per controller key format.
|
191
|
+
# Must return an instance of a class derived from KeyFormatter.
|
192
|
+
def key_formatter
|
193
|
+
JSONAPI.configuration.key_formatter
|
194
|
+
end
|
195
|
+
|
196
|
+
def route_formatter
|
197
|
+
JSONAPI.configuration.route_formatter
|
198
|
+
end
|
199
|
+
|
200
|
+
def base_response_meta
|
201
|
+
{}
|
202
|
+
end
|
203
|
+
|
204
|
+
def base_meta
|
205
|
+
if @request.nil? || @request.warnings.empty?
|
206
|
+
base_response_meta
|
207
|
+
else
|
208
|
+
base_response_meta.merge(warnings: @request.warnings)
|
209
|
+
end
|
210
|
+
end
|
211
|
+
|
212
|
+
def base_response_links
|
213
|
+
{}
|
214
|
+
end
|
215
|
+
|
216
|
+
def render_errors(errors)
|
217
|
+
operation_results = JSONAPI::OperationResults.new
|
218
|
+
result = JSONAPI::ErrorsOperationResult.new(errors[0].status, errors)
|
219
|
+
operation_results.add_result(result)
|
220
|
+
|
221
|
+
render_results(operation_results)
|
222
|
+
end
|
223
|
+
|
224
|
+
def render_results(operation_results)
|
225
|
+
response_doc = create_response_document(operation_results)
|
226
|
+
content = response_doc.contents
|
227
|
+
|
228
|
+
render_options = {}
|
229
|
+
if operation_results.has_errors?
|
230
|
+
render_options[:json] = content
|
231
|
+
else
|
232
|
+
# Bypasing ActiveSupport allows us to use CompiledJson objects for cached response fragments
|
233
|
+
render_options[:body] = JSON.generate(content)
|
234
|
+
end
|
235
|
+
|
236
|
+
render_options[:location] = content[:data]["links"][:self] if (
|
237
|
+
response_doc.status == :created && content[:data].class != Array
|
238
|
+
)
|
239
|
+
|
240
|
+
# For whatever reason, `render` ignores :status and :content_type when :body is set.
|
241
|
+
# But, we can just set those values directly in the Response object instead.
|
242
|
+
response.status = response_doc.status
|
243
|
+
response.headers['Content-Type'] = JSONAPI::MEDIA_TYPE
|
244
|
+
|
245
|
+
render(render_options)
|
246
|
+
end
|
247
|
+
|
248
|
+
def create_response_document(operation_results)
|
249
|
+
JSONAPI::ResponseDocument.new(
|
250
|
+
operation_results,
|
251
|
+
operation_results.has_errors? ? nil : resource_serializer,
|
252
|
+
key_formatter: key_formatter,
|
253
|
+
base_meta: base_meta,
|
254
|
+
base_links: base_response_links,
|
255
|
+
request: @request
|
256
|
+
)
|
257
|
+
end
|
258
|
+
|
259
|
+
# override this to process other exceptions
|
260
|
+
# Note: Be sure to either call super(e) or handle JSONAPI::Exceptions::Error and raise unhandled exceptions
|
261
|
+
def handle_exceptions(e)
|
262
|
+
case e
|
263
|
+
when JSONAPI::Exceptions::Error
|
264
|
+
render_errors(e.errors)
|
265
|
+
else
|
266
|
+
if JSONAPI.configuration.exception_class_whitelisted?(e)
|
267
|
+
fail e
|
268
|
+
else
|
269
|
+
(self.class.server_error_callbacks || []).each { |callback|
|
270
|
+
safe_run_callback(callback, e)
|
271
|
+
}
|
272
|
+
|
273
|
+
internal_server_error = JSONAPI::Exceptions::InternalServerError.new(e)
|
274
|
+
Rails.logger.error { "Internal Server Error: #{e.message} #{e.backtrace.join("\n")}" }
|
275
|
+
render_errors(internal_server_error.errors)
|
276
|
+
end
|
277
|
+
end
|
278
|
+
end
|
279
|
+
|
280
|
+
def safe_run_callback(callback, error)
|
281
|
+
begin
|
282
|
+
callback.call(error)
|
283
|
+
rescue => e
|
284
|
+
Rails.logger.error { "Error in error handling callback: #{e.message} #{e.backtrace.join("\n")}" }
|
285
|
+
internal_server_error = JSONAPI::Exceptions::InternalServerError.new(e)
|
286
|
+
render_errors(internal_server_error.errors)
|
287
|
+
end
|
288
|
+
end
|
289
|
+
|
290
|
+
# Pass in a methods or a block to be run when an exception is
|
291
|
+
# caught that is not a JSONAPI::Exceptions::Error
|
292
|
+
# Useful for additional logging or notification configuration that
|
293
|
+
# would normally depend on rails catching and rendering an exception.
|
294
|
+
# Ignores whitelist exceptions from config
|
295
|
+
|
296
|
+
module ClassMethods
|
297
|
+
|
298
|
+
def on_server_error(*args, &callback_block)
|
299
|
+
callbacks ||= []
|
300
|
+
|
301
|
+
if callback_block
|
302
|
+
callbacks << callback_block
|
303
|
+
end
|
304
|
+
|
305
|
+
method_callbacks = args.map do |method|
|
306
|
+
->(error) do
|
307
|
+
if self.respond_to? method
|
308
|
+
send(method, error)
|
309
|
+
else
|
310
|
+
Rails.logger.warn("#{method} not defined on #{self}, skipping error callback")
|
311
|
+
end
|
312
|
+
end
|
313
|
+
end.compact
|
314
|
+
callbacks += method_callbacks
|
315
|
+
self.class_variable_set :@@server_error_callbacks, callbacks
|
316
|
+
end
|
317
|
+
|
318
|
+
end
|
319
|
+
end
|
320
|
+
end
|
@@ -0,0 +1,127 @@
|
|
1
|
+
module JSONAPI
|
2
|
+
class CachedResourceFragment
|
3
|
+
def self.fetch_fragments(resource_klass, serializer, context, cache_ids)
|
4
|
+
serializer_config_key = serializer.config_key(resource_klass).gsub("/", "_")
|
5
|
+
context_json = resource_klass.attribute_caching_context(context).to_json
|
6
|
+
context_b64 = JSONAPI.configuration.resource_cache_digest_function.call(context_json)
|
7
|
+
context_key = "ATTR-CTX-#{context_b64.gsub("/", "_")}"
|
8
|
+
|
9
|
+
results = self.lookup(resource_klass, serializer_config_key, context, context_key, cache_ids)
|
10
|
+
|
11
|
+
miss_ids = results.select{|k,v| v.nil? }.keys
|
12
|
+
unless miss_ids.empty?
|
13
|
+
find_filters = {resource_klass._primary_key => miss_ids.uniq}
|
14
|
+
find_options = {context: context}
|
15
|
+
resource_klass.find(find_filters, find_options).each do |resource|
|
16
|
+
(id, cr) = write(resource_klass, resource, serializer, serializer_config_key, context, context_key)
|
17
|
+
results[id] = cr
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
if JSONAPI.configuration.resource_cache_usage_report_function
|
22
|
+
JSONAPI.configuration.resource_cache_usage_report_function.call(
|
23
|
+
resource_klass.name,
|
24
|
+
cache_ids.size - miss_ids.size,
|
25
|
+
miss_ids.size
|
26
|
+
)
|
27
|
+
end
|
28
|
+
|
29
|
+
return results
|
30
|
+
end
|
31
|
+
|
32
|
+
attr_reader :resource_klass, :id, :type, :context, :fetchable_fields, :relationships,
|
33
|
+
:links_json, :attributes_json, :meta_json,
|
34
|
+
:preloaded_fragments
|
35
|
+
|
36
|
+
def initialize(resource_klass, id, type, context, fetchable_fields, relationships,
|
37
|
+
links_json, attributes_json, meta_json)
|
38
|
+
@resource_klass = resource_klass
|
39
|
+
@id = id
|
40
|
+
@type = type
|
41
|
+
@context = context
|
42
|
+
@fetchable_fields = Set.new(fetchable_fields)
|
43
|
+
|
44
|
+
# Relationships left uncompiled because we'll often want to insert included ids on retrieval
|
45
|
+
@relationships = relationships
|
46
|
+
|
47
|
+
@links_json = CompiledJson.of(links_json)
|
48
|
+
@attributes_json = CompiledJson.of(attributes_json)
|
49
|
+
@meta_json = CompiledJson.of(meta_json)
|
50
|
+
|
51
|
+
# A hash of hashes
|
52
|
+
@preloaded_fragments ||= Hash.new
|
53
|
+
end
|
54
|
+
|
55
|
+
def to_cache_value
|
56
|
+
{
|
57
|
+
id: id,
|
58
|
+
type: type,
|
59
|
+
fetchable: fetchable_fields,
|
60
|
+
rels: relationships,
|
61
|
+
links: links_json.try(:to_s),
|
62
|
+
attrs: attributes_json.try(:to_s),
|
63
|
+
meta: meta_json.try(:to_s)
|
64
|
+
}
|
65
|
+
end
|
66
|
+
|
67
|
+
def to_real_resource
|
68
|
+
rs = Resource.resource_for(self.type).find_by_keys([self.id], {context: self.context})
|
69
|
+
return rs.try(:first)
|
70
|
+
end
|
71
|
+
|
72
|
+
private
|
73
|
+
|
74
|
+
def self.lookup(resource_klass, serializer_config_key, context, context_key, cache_ids)
|
75
|
+
type = resource_klass._type
|
76
|
+
|
77
|
+
keys = cache_ids.map do |(id, cache_key)|
|
78
|
+
[type, id, cache_key, serializer_config_key, context_key]
|
79
|
+
end
|
80
|
+
|
81
|
+
hits = JSONAPI.configuration.resource_cache.read_multi(*keys).reject{|_,v| v.nil? }
|
82
|
+
return keys.each_with_object({}) do |key, hash|
|
83
|
+
(_, id, _, _) = key
|
84
|
+
if hits.has_key?(key)
|
85
|
+
hash[id] = self.from_cache_value(resource_klass, context, hits[key])
|
86
|
+
else
|
87
|
+
hash[id] = nil
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def self.from_cache_value(resource_klass, context, h)
|
93
|
+
new(
|
94
|
+
resource_klass,
|
95
|
+
h.fetch(:id),
|
96
|
+
h.fetch(:type),
|
97
|
+
context,
|
98
|
+
h.fetch(:fetchable),
|
99
|
+
h.fetch(:rels, nil),
|
100
|
+
h.fetch(:links, nil),
|
101
|
+
h.fetch(:attrs, nil),
|
102
|
+
h.fetch(:meta, nil)
|
103
|
+
)
|
104
|
+
end
|
105
|
+
|
106
|
+
def self.write(resource_klass, resource, serializer, serializer_config_key, context, context_key)
|
107
|
+
(id, cache_key) = resource.cache_id
|
108
|
+
json = serializer.object_hash(resource) # No inclusions passed to object_hash
|
109
|
+
cr = self.new(
|
110
|
+
resource_klass,
|
111
|
+
json['id'],
|
112
|
+
json['type'],
|
113
|
+
context,
|
114
|
+
resource.fetchable_fields,
|
115
|
+
json['relationships'],
|
116
|
+
json['links'],
|
117
|
+
json['attributes'],
|
118
|
+
json['meta']
|
119
|
+
)
|
120
|
+
|
121
|
+
key = [resource_klass._type, id, cache_key, serializer_config_key, context_key]
|
122
|
+
JSONAPI.configuration.resource_cache.write(key, cr.to_cache_value)
|
123
|
+
return [id, cr]
|
124
|
+
end
|
125
|
+
|
126
|
+
end
|
127
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
require 'active_support/callbacks'
|
2
|
+
|
3
|
+
module JSONAPI
|
4
|
+
module Callbacks
|
5
|
+
def self.included(base)
|
6
|
+
base.class_eval do
|
7
|
+
include ActiveSupport::Callbacks
|
8
|
+
base.extend ClassMethods
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
module ClassMethods
|
13
|
+
def define_jsonapi_resources_callbacks(*callbacks)
|
14
|
+
options = callbacks.extract_options!
|
15
|
+
options = {
|
16
|
+
only: [:before, :around, :after]
|
17
|
+
}.merge!(options)
|
18
|
+
|
19
|
+
types = Array(options.delete(:only))
|
20
|
+
|
21
|
+
callbacks.each do |callback|
|
22
|
+
define_callbacks(callback, options)
|
23
|
+
|
24
|
+
types.each do |type|
|
25
|
+
send("_define_#{type}_callback", self, callback)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def _define_before_callback(klass, callback) #:nodoc:
|
33
|
+
klass.define_singleton_method("before_#{callback}") do |*args, &block|
|
34
|
+
set_callback(:"#{callback}", :before, *args, &block)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def _define_around_callback(klass, callback) #:nodoc:
|
39
|
+
klass.define_singleton_method("around_#{callback}") do |*args, &block|
|
40
|
+
set_callback(:"#{callback}", :around, *args, &block)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def _define_after_callback(klass, callback) #:nodoc:
|
45
|
+
klass.define_singleton_method("after_#{callback}") do |*args, &block|
|
46
|
+
set_callback(:"#{callback}", :after, *args, &block)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module JSONAPI
|
2
|
+
class CompiledJson
|
3
|
+
def self.compile(h)
|
4
|
+
new(JSON.generate(h), h)
|
5
|
+
end
|
6
|
+
|
7
|
+
def self.of(obj)
|
8
|
+
case obj
|
9
|
+
when NilClass then nil
|
10
|
+
when CompiledJson then obj
|
11
|
+
when String then CompiledJson.new(obj)
|
12
|
+
when Hash then CompiledJson.compile(obj)
|
13
|
+
else raise "Can't figure out how to turn #{obj.inspect} into CompiledJson"
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def initialize(json, h = nil)
|
18
|
+
@json = json
|
19
|
+
@h = h
|
20
|
+
end
|
21
|
+
|
22
|
+
def to_json(*args)
|
23
|
+
@json
|
24
|
+
end
|
25
|
+
|
26
|
+
def to_s
|
27
|
+
@json
|
28
|
+
end
|
29
|
+
|
30
|
+
def to_h
|
31
|
+
@h ||= JSON.parse(@json)
|
32
|
+
end
|
33
|
+
|
34
|
+
undef_method :as_json
|
35
|
+
end
|
36
|
+
end
|