jsonapi-serializers 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 035748d359ee3c492b7867f9dd5548960df3cb78
4
+ data.tar.gz: 2d6e4b038d482c9de6b9d3972778082c74b36ba9
5
+ SHA512:
6
+ metadata.gz: 79ac938adcc1fdcc64a43c7237c4b6d1fed837f4d8325312f9a901bd0faf3b06296425e4c2c83b3da518b7813d148f2532c234687f39ceba5e8ab824836993ab
7
+ data.tar.gz: 167c5d8970366dce05af73431498cc5d5a3d0a0dc585437bc5a4e4e3f126106fec046e03578593031d5976f0cd4bfda7204d9658dc01688b992251c511253f21
data/.gitignore ADDED
@@ -0,0 +1,15 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ *.bundle
11
+ *.so
12
+ *.o
13
+ *.a
14
+ mkmf.log
15
+ .DS_Store
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --color
2
+ --format d
3
+ --require spec_helper
data/.travis.yml ADDED
@@ -0,0 +1,7 @@
1
+ language: ruby
2
+ rvm:
3
+ - 1.9.3
4
+ - 2.1.1
5
+ - 2.2.2
6
+ - ruby-head
7
+ script: bundle exec rspec
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in jsonapi-serializers.gemspec
4
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2015 Mike Fotinakis
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,417 @@
1
+ # JSONAPI::Serializers
2
+
3
+ JSONAPI::Serializers is a simple library for serializing Ruby objects and their relationships into the [JSON:API format](http://jsonapi.org/format/).
4
+
5
+ As of writing, the JSON:API spec is approaching v1 and still undergoing changes. This library supports RC3+ and aims to keep up with the continuing development changes.
6
+
7
+ * [Features](#features)
8
+ * [Installation](#installation)
9
+ * [Usage](#usage)
10
+ * [Define a serializer](#define-a-serializer)
11
+ * [Serialize an object](#serialize-an-object)
12
+ * [Serialize a collection](#serialize-a-collection)
13
+ * [Null handling](#null-handling)
14
+ * [Custom attributes](#custom-attributes)
15
+ * [More customizations](#more-customizations)
16
+ * [Relationships](#relationships)
17
+ * [Compound documents and includes](#compound-documents-and-includes)
18
+ * [Relationship path handling](#relationship-path-handling)
19
+ * [Rails example](#rails-example)
20
+ * [Unfinished business](#unfinished-business)
21
+ * [Contributing](#contributing)
22
+
23
+ ## Features
24
+
25
+ * Works with **any Ruby web framework**, including Rails, Sinatra, etc. This is a pure Ruby library.
26
+ * Supports the readonly features of the JSON:API spec.
27
+ * **Full support for compound documents** ("side-loading") and the `include` parameter.
28
+ * Similar interface to ActiveModel::Serializers, should provide an easy migration path.
29
+ * Intentionally unopinionated and simple, allows you to structure your app however you would like and then serialize the objects at the end.
30
+
31
+ JSONAPI::Serializers was built as an intentionally simple serialization interface. It makes no assumptions about your database structure or routes and it does not provide controllers or any create/update interface to the objects. It is a library, not a framework. You will probably still need to do work to make your API fully compliant with the nuances of the [JSON:API spec](http://jsonapi.org/format/), for things like supporting `/links` routes and for supporting write actions like creating or updating objects. If you are looking for a more complete and opinionated framework, see the [jsonapi-resources](https://github.com/cerebris/jsonapi-resources) project.
32
+
33
+ ## Installation
34
+
35
+ Add this line to your application's Gemfile:
36
+
37
+ ```ruby
38
+ gem 'jsonapi-serializers'
39
+ ```
40
+
41
+ Or install directly with `gem install jsonapi-serializers`.
42
+
43
+ ## Usage
44
+
45
+ ### Define a serializer
46
+
47
+ ```ruby
48
+ require 'jsonapi-serializers'
49
+
50
+ class PostSerializer
51
+ include JSONAPI::Serializer
52
+
53
+ attribute :title
54
+ attribute :content
55
+ end
56
+ ```
57
+
58
+ ### Serialize an object
59
+
60
+ ```ruby
61
+ JSONAPI::Serializer.serialize(post)
62
+ ```
63
+
64
+ Returns a hash:
65
+ ```json
66
+ {
67
+ "data": {
68
+ "id": "1",
69
+ "type": "posts",
70
+ "attributes": {
71
+ "title": "Hello World",
72
+ "content": "Your first post"
73
+ },
74
+ "links": {
75
+ "self": "/posts/1"
76
+ }
77
+ }
78
+ }
79
+ ```
80
+
81
+ ### Serialize a collection
82
+
83
+ ```ruby
84
+ JSONAPI::Serializer.serialize(posts, is_collection: true)
85
+ ```
86
+
87
+ Returns:
88
+
89
+ ```json
90
+ {
91
+ "data": [
92
+ {
93
+ "id": "1",
94
+ "type": "posts",
95
+ "attributes": {
96
+ "title": "Hello World",
97
+ "content": "Your first post"
98
+ },
99
+ "links": {
100
+ "self": "/posts/1"
101
+ }
102
+ },
103
+ {
104
+ "id": "2",
105
+ "type": "posts",
106
+ "attributes": {
107
+ "title": "Hello World again",
108
+ "content": "Your second post"
109
+ },
110
+ "links": {
111
+ "self": "/posts/2"
112
+ }
113
+ }
114
+ ]
115
+ }
116
+ ```
117
+
118
+ You must always pass `is_collection: true` when serializing a collection, see [Null handling](#null-handling).
119
+
120
+ ### Null handling
121
+
122
+ ```ruby
123
+ JSONAPI::Serializer.serialize(nil)
124
+ ```
125
+
126
+ Returns:
127
+ ```json
128
+ {
129
+ "data": null
130
+ }
131
+ ```
132
+
133
+ And serializing an empty collection:
134
+ ```ruby
135
+ JSONAPI::Serializer.serialize([], is_collection: true)
136
+ ```
137
+
138
+ Returns:
139
+ ```json
140
+ {
141
+ "data": []
142
+ }
143
+ ```
144
+
145
+ Note that the JSON:API spec distinguishes in how null/empty is handled for single objects vs. collections, so you must always provide `is_collection: true` when serializing multiple objects. If you attempt to serialize multiple objects without this flag (or a single object with it on) a `JSONAPI::Serializer::AmbiguousCollectionError` will be raised.
146
+
147
+ ### Custom attributes
148
+
149
+ By default the serializer looks for the same name of the attribute on the object it is given. You can customize this behavior by providing a block to the attribute:
150
+
151
+ ```ruby
152
+ attribute :content do
153
+ object.body
154
+ end
155
+ ```
156
+
157
+ The block is evaluated within the serializer instance, so it has access to the `object` and `context` instance variables.
158
+
159
+ ### More customizations
160
+
161
+ Many other formatting and customizations are possible by overriding any of the following instance methods on your serializers.
162
+
163
+ ```ruby
164
+ # Override this to customize the JSON:API "id" for this object.
165
+ # Always return a string from this method to conform with the JSON:API spec.
166
+ def id
167
+ object.id.to_s
168
+ end
169
+ ```
170
+ ```ruby
171
+ # Override this to customize the JSON:API "type" for this object.
172
+ # By default, the type is the object's class name lowercased, pluralized, and dasherized,
173
+ # per the spec naming recommendations: http://jsonapi.org/recommendations/#naming
174
+ # For example, 'MyApp::LongCommment' will become the 'long-comments' type.
175
+ def type
176
+ object.class.name.demodulize.tableize.dasherize
177
+ end
178
+ ```
179
+ ```ruby
180
+ # Override this to customize how attribute names are formatted.
181
+ # By default, attribute names are dasherized per the spec naming recommendations:
182
+ # http://jsonapi.org/recommendations/#naming
183
+ def format_name(attribute_name)
184
+ attribute_name.to_s.dasherize
185
+ end
186
+ ```
187
+ ```ruby
188
+ # The opposite of format_name. Override this if you override format_name.
189
+ def unformat_name(attribute_name)
190
+ attribute_name.to_s.underscore
191
+ end
192
+ ```
193
+ ```ruby
194
+ # Override this to provide resource-object metadata.
195
+ # http://jsonapi.org/format/#document-structure-resource-objects
196
+ def meta
197
+ end
198
+ ```
199
+ ```ruby
200
+ def self_link
201
+ "/#{type}/#{id}"
202
+ end
203
+ ```
204
+ ```ruby
205
+ def relationship_self_link(attribute_name)
206
+ "#{self_link}/links/#{format_name(attribute_name)}"
207
+ end
208
+ ```
209
+ ```ruby
210
+ def relationship_related_link(attribute_name)
211
+ "#{self_link}/#{format_name(attribute_name)}"
212
+ end
213
+ ```
214
+
215
+ ## Relationships
216
+
217
+ You can easily specify relationships with the `has_one` and `has_many` directives.
218
+
219
+ ```ruby
220
+ class BaseSerializer
221
+ include JSONAPI::Serializer
222
+ end
223
+
224
+ class PostSerializer < BaseSerializer
225
+ attribute :title
226
+ attribute :content
227
+
228
+ has_one :author
229
+ has_many :comments
230
+ end
231
+
232
+ class UserSerializer < BaseSerializer
233
+ attribute :name
234
+ end
235
+
236
+ class CommentSerializer < BaseSerializer
237
+ attribute :content
238
+
239
+ has_one :user
240
+ end
241
+ ```
242
+
243
+ Note that when serializing a post, the `author` association will come from the `author` attribute on the `Post` instance, no matter what type it is (in this case it is a `User`). This will work just fine, because JSONAPI::Serializers automatically finds serializer classes by appending `Serializer` to the object's class name. This behavior can be customized.
244
+
245
+ Because the full class name is used when discovering serializers, JSONAPI::Serializers works with any custom namespaces you might have, like a Rails Engine or standard Ruby module namespace.
246
+
247
+ ### Compound documents and includes
248
+
249
+ > To reduce the number of HTTP requests, servers MAY allow responses that include related resources along with the requested primary resources. Such responses are called "compound documents".
250
+ > [JSON:API Compound Documents](http://jsonapi.org/format/#document-structure-compound-documents)
251
+
252
+ JSONAPI::Serializers supports compound documents with a simple `include` parameter.
253
+
254
+ For example:
255
+
256
+ ```ruby
257
+ JSONAPI::Serializer.serialize(post, include: ['author', 'comments', 'comments.user'])
258
+ ```
259
+
260
+ Returns:
261
+
262
+ ```json
263
+
264
+ "data": {
265
+ "id": "1",
266
+ "type": "posts",
267
+ "attributes": {
268
+ "title": "Hello World",
269
+ "content": "Your first post"
270
+ },
271
+ "links": {
272
+ "self": "/posts/1",
273
+ "author": {
274
+ "self": "/posts/1/links/author",
275
+ "related": "/posts/1/author",
276
+ "linkage": {
277
+ "type": "users",
278
+ "id": "1"
279
+ }
280
+ },
281
+ "comments": {
282
+ "self": "/posts/1/links/comments",
283
+ "related": "/posts/1/comments",
284
+ "linkage": [
285
+ {
286
+ "type": "comments",
287
+ "id": "1"
288
+ }
289
+ ]
290
+ }
291
+ }
292
+ },
293
+ "included": [
294
+ {
295
+ "id": "1",
296
+ "type": "users",
297
+ "attributes": {
298
+ "name": "Post Author"
299
+ },
300
+ "links": {
301
+ "self": "/users/1"
302
+ }
303
+ },
304
+ {
305
+ "id": "1",
306
+ "type": "comments",
307
+ "attributes": {
308
+ "content": "Have no fear, sers, your king is safe."
309
+ },
310
+ "links": {
311
+ "self": "/comments/1",
312
+ "user": {
313
+ "self": "/comments/1/links/user",
314
+ "related": "/comments/1/user",
315
+ "linkage": {
316
+ "type": "users",
317
+ "id": "2"
318
+ }
319
+ }
320
+ }
321
+ },
322
+ {
323
+ "id": "2",
324
+ "type": "users",
325
+ "attributes": {
326
+ "name": "Barristan Selmy"
327
+ },
328
+ "links": {
329
+ "self": "/users/2"
330
+ }
331
+ }
332
+ ]
333
+ }
334
+ ```
335
+
336
+ Notice a few things:
337
+ * The [primary data](http://jsonapi.org/format/#document-structure-top-level) now includes "linkage" information for each relationship that was included.
338
+ * The related objects themselves are loaded in the top-level `included` member.
339
+ * The related objects _also_ include "linkage" information when a deeper relationship is also present in the compound document. This is a very powerful feature of the JSON:API spec, and allows you to deeply link complicated relationships all in the same document and in a single HTTP response. JSONAPI::Serializers automatically includes the correct linkage information for whatever `include` paths you specify. This conforms to this part of the spec:
340
+
341
+ > Note: Resource linkage in a compound document allows a client to link together all of the included resource objects without having to GET any relationship URLs.
342
+ > [JSON:API Resource Relationships](http://jsonapi.org/format/#document-structure-resource-relationships)
343
+
344
+ #### Relationship path handling
345
+
346
+ The `include` param also accepts a string of [relationship paths](http://jsonapi.org/format/#fetching-includes), ie. `include: 'author,comments,comments.user'` so you can pass an `?include` query param directly through to the serialize method. Be aware that letting users pass arbitrary relationship paths might introduce security issues depending on your authorization setup, where a user could `include` a relationship they might not be authorized to see directly. Be aware of what you allow API users to include.
347
+
348
+ ## Rails example
349
+
350
+ ```ruby
351
+ # app/serializers/base_serializer.rb
352
+ class BaseSerializer
353
+ include JSONAPI::Serializer
354
+
355
+ def self_link
356
+ "/api/v1#{super}"
357
+ end
358
+ end
359
+
360
+ # app/serializers/post_serializer.rb
361
+ class PostSerializer < BaseSerializer
362
+ attribute :title
363
+ attribute :content
364
+ end
365
+
366
+ # app/controllers/api/v1/base_controller.rb
367
+ class Api::V1::BaseController < ActionController::Base
368
+ # Convenience methods for serializing models:
369
+ def serialize_model(model, options = {})
370
+ options[:is_collection] = false
371
+ JSONAPI::Serializer.serialize(model, options)
372
+ end
373
+
374
+ def serialize_models(models, options = {})
375
+ options[:is_collection] = true
376
+ JSONAPI::Serializer.serialize(models, options)
377
+ end
378
+ end
379
+
380
+ # app/controllers/api/v1/posts_controller.rb
381
+ class Api::V1::ReposController < Api::V1::BaseController
382
+ def index
383
+ posts = Post.all
384
+ render json: serialize_models(posts)
385
+ end
386
+
387
+ def show
388
+ post = Post.find(params[:id])
389
+ render json: serialize_model(post)
390
+ end
391
+ end
392
+
393
+ # lib/jsonapi_mimetypes.rb
394
+ # Without this mimetype registration, controllers will not automatically parse JSON API params.
395
+ module JSONAPI
396
+ MIMETYPE = "application/vnd.api+json"
397
+ end
398
+ Mime::Type.register(JSONAPI::MIMETYPE, :api_json)
399
+ ActionDispatch::ParamsParser::DEFAULT_PARSERS[Mime::Type.lookup(JSONAPI::MIMETYPE)] = lambda do |body|
400
+ JSON.parse(body)
401
+ end
402
+ ```
403
+
404
+ ## Unfinished business
405
+
406
+ * Support for passing `context` through to serializers is partially complete, but needs more work.
407
+ * Support for a `serializer_class` attribute on objects that overrides serializer discovery, would love a PR contribution for this.
408
+ * Support for the `fields` spec is planned, would love a PR contribution for this.
409
+ * Support for pagination/sorting is unlikely to be supported because it would likely involve coupling to ActiveRecord, but please open an issue if you have ideas of how to support this generically.
410
+
411
+ ## Contributing
412
+
413
+ 1. Fork it ( https://github.com/fotinakis/jsonapi-serializers/fork )
414
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
415
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
416
+ 4. Push to the branch (`git push origin my-new-feature`)
417
+ 5. Create a new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,2 @@
1
+ require "bundler/gem_tasks"
2
+
@@ -0,0 +1,26 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'jsonapi-serializers/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "jsonapi-serializers"
8
+ spec.version = JSONAPI::Serializer::VERSION
9
+ spec.authors = ["Mike Fotinakis"]
10
+ spec.email = ["mike@fotinakis.com"]
11
+ spec.summary = %q{Pure Ruby serializers conforming to the JSON:API spec.}
12
+ spec.description = %q{}
13
+ spec.homepage = ""
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files -z`.split("\x0")
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_dependency "activesupport"
22
+ spec.add_development_dependency "bundler", "~> 1.7"
23
+ spec.add_development_dependency "rake", "~> 10.0"
24
+ spec.add_development_dependency "rspec", "~> 3.2"
25
+ spec.add_development_dependency "factory_girl", "~> 4.5"
26
+ end
@@ -0,0 +1,58 @@
1
+ module JSONAPI
2
+ module Attributes
3
+ def self.included(target)
4
+ target.send(:include, InstanceMethods)
5
+ target.extend ClassMethods
6
+ end
7
+
8
+ module InstanceMethods
9
+ end
10
+
11
+ module ClassMethods
12
+ attr_accessor :attributes_map
13
+ attr_accessor :to_one_associations
14
+ attr_accessor :to_many_associations
15
+
16
+ def attribute(name, options = {}, &block)
17
+ add_attribute(name, options, &block)
18
+ end
19
+
20
+ def has_one(name, options = {})
21
+ add_to_one_association(name, options)
22
+ end
23
+
24
+ def has_many(name, options = {})
25
+ add_to_many_association(name, options)
26
+ end
27
+
28
+ def add_attribute(name, options = {}, &block)
29
+ # Blocks are optional and can override the default attribute discovery. They are just
30
+ # stored here, but evaluated by the Serializer within the instance context.
31
+ @attributes_map ||= {}
32
+ @attributes_map[name] = {
33
+ attr_or_block: block_given? ? block : name,
34
+ options: options,
35
+ }
36
+ end
37
+ private :add_attribute
38
+
39
+ def add_to_one_association(name, options = {}, &block)
40
+ @to_one_associations ||= {}
41
+ @to_one_associations[name] = {
42
+ attr_or_block: block_given? ? block : name,
43
+ options: options,
44
+ }
45
+ end
46
+ private :add_to_one_association
47
+
48
+ def add_to_many_association(name, options = {}, &block)
49
+ @to_many_associations ||= {}
50
+ @to_many_associations[name] = {
51
+ attr_or_block: block_given? ? block : name,
52
+ options: options,
53
+ }
54
+ end
55
+ private :add_to_many_association
56
+ end
57
+ end
58
+ end