jsonapi-serializers 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 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