oat 0.0.1

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: 265248fecbfa3adf9fae15b6c9124a452fe8f285
4
+ data.tar.gz: aac3cc104b933d261d8f8ca858e038e5e7cef835
5
+ SHA512:
6
+ metadata.gz: 29f80fbffde75401c81cf69de1f59a5f1091d173abddf404c902ce90ada9c8060b950cfd56a3cc713daf5e961cac417376eb8e68d8ebeae169bfa5bfcbb9a2e9
7
+ data.tar.gz: fa5ecb844b738eed0344bc513ae083f2928e40eef0099c3e8bd282d0b466e1727cd23598577eb080977a06050e3d6c58764b387d2b5be0565683fea9d51a9985
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --format documentation
2
+ --color
data/.travis.yml ADDED
@@ -0,0 +1,3 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.0.0
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in oat.gemspec
4
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2013 Ismael Celis
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
6
+ this software and associated documentation files (the "Software"), to deal in
7
+ the Software without restriction, including without limitation the rights to
8
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
9
+ the Software, and to permit persons to whom the Software is furnished to do so,
10
+ subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
17
+ FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18
+ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
19
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Ismael Celis
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,510 @@
1
+ # Oat [![Build Status](https://travis-ci.org/ismasan/oat.png)](https://travis-ci.org/ismasan/oat)
2
+
3
+ Adapters-based API serializers with Hypermedia support for Ruby apps.
4
+
5
+ ## What
6
+
7
+ Oat lets you design your API payloads succinctly while conforming to your *media type* of choice (hypermedia or not).
8
+ The details of the media type are dealt with by pluggable adapters.
9
+
10
+ Oat ships with adapters for HAL, Siren and JsonAPI, and it's easy to write your own.
11
+
12
+ ## Serializers
13
+
14
+ A serializer describes one or more of your API's *entities*.
15
+
16
+ You extend from [Oat::Serializer](https://github.com/ismasan/oat/blob/master/lib/oat/serializer.rb) to define your own serializers.
17
+
18
+ ```ruby
19
+ require 'oat/adapters/hal'
20
+ class ProductSerializer < Oat::Serializer
21
+ adapter Oat::Adapters::HAL
22
+
23
+ schema do
24
+ type "product"
25
+ link :self, href: product_url(item)
26
+ properties do |props|
27
+ props.title item.title
28
+ props.price item.price
29
+ props.description item.blurb
30
+ end
31
+ end
32
+
33
+ end
34
+ ```
35
+
36
+ Then in your app (for example a Rails controller)
37
+
38
+ ```ruby
39
+ product = Product.find(params[:id])
40
+ render json: ProductSerializer.new(product)
41
+ ```
42
+
43
+ Serializers require a single object as argument, which can be a model instance, a presenter or any other domain object.
44
+
45
+ The full serializer signature is `item`, `context`, `adapter_class`.
46
+
47
+ * `item` a model or presenter instance. It is available in your serializer's schema as `item`.
48
+ * `context` (optional) a context object or hash that is passed to the serializer and sub-serializers as the `context` variable. Useful if you need to pass request-specific data.
49
+ * `adapter_class` (optional) A serializer's adapter can be configured at class-level or passed here to the initializer. Useful if you want to switch adapters based on request data. More on this below.
50
+
51
+ ## Adapters
52
+
53
+ Using the included [HAL](http://stateless.co/hal_specification.html) adapter, the `ProductSerializer` above would render the following JSON:
54
+
55
+ ```json
56
+ {
57
+ "_links": {
58
+ "self": {"href": "http://example.com/products/1"}
59
+ },
60
+ "title": "Some product",
61
+ "price": 1000,
62
+ "description": "..."
63
+ }
64
+ ```
65
+
66
+ You can easily swap adapters. The same `ProductSerializer`, this time using the [Siren](https://github.com/kevinswiber/siren) adapter:
67
+
68
+ ```ruby
69
+ adapter Oat::Adapters::Siren
70
+ ```
71
+
72
+ ... Renders this JSON:
73
+
74
+ ```json
75
+ {
76
+ "class": ["product"],
77
+ "links": [
78
+ { "rel": [ "self" ], "href": "http://example.com/products/1" }
79
+ ],
80
+ "properties": {
81
+ "title": "Some product",
82
+ "price": 1000,
83
+ "description": "..."
84
+ }
85
+ }
86
+ ```
87
+ At the moment Oat ships with adapters for [HAL](http://stateless.co/hal_specification.html), [Siren](https://github.com/kevinswiber/siren) and [JsonAPI](http://jsonapi.org/), but it's easy to write your own.
88
+
89
+ Note: Oat adapters are not *required* by default. Your code should explicitely require the ones it needs:
90
+
91
+ ```ruby
92
+ # HAL
93
+ require 'oat/adapters/hal'
94
+ # Siren
95
+ require 'oat/adapters/siren'
96
+ # JsonAPI
97
+ require 'oat/adapters/json_api'
98
+ ```
99
+
100
+ ## Switching adapters dynamically
101
+
102
+ Adapters can also be passed as an argument to serializer instances.
103
+
104
+ ```ruby
105
+ ProductSerializer.new(product, nil, Oat::Adapters::HAL)
106
+ ```
107
+
108
+ That means that your app could switch adapters on run time depending, for example, on the request's `Accept` header or anything you need.
109
+
110
+ Note: a different library could be written to make adapter-switching auto-magical for different frameworks, for example using [Responders](http://api.rubyonrails.org/classes/ActionController/Responder.html) in Rails.
111
+
112
+ ## Nested serializers
113
+
114
+ It's common for a media type to include "embedded" entities within a payload. For example an `account` entity may have many `users`. An Oat serializer can inline such relationships:
115
+
116
+ ```ruby
117
+ class AccountSerializer < Oat::Serializer
118
+ adapter Oat::Adapters::HAL
119
+
120
+ schema do
121
+ property :id, item.id
122
+ property :status, item.status
123
+ # user entities
124
+ entities :users, item.users do |user, user_serializer|
125
+ user_serializer.name user.name
126
+ user_serializer.email user.email
127
+ end
128
+ end
129
+ end
130
+ ```
131
+
132
+ Another, more reusable option is to use a nested serializer. Instead of a block, you pass another serializer class that will handle serializing `user` entities.
133
+
134
+ ```ruby
135
+ class AccountSerializer < Oat::Serializer
136
+ adapter Oat::Adapters::HAL
137
+
138
+ schema do
139
+ property :id, item.id
140
+ property :status, item.status
141
+ # user entities
142
+ entities :users, item.users, UserSerializer
143
+ end
144
+ end
145
+ ```
146
+
147
+ And the `UserSerializer` may look like this:
148
+
149
+ ```ruby
150
+ class UserSerializer < Oat::Serializer
151
+ adapter Oat::Adapters::HAL
152
+
153
+ schema do
154
+ property :name, item.name
155
+ property :email, item.name
156
+ end
157
+ end
158
+ ```
159
+
160
+ In the user serializer, `item` refers to the user instance being wrapped by the serializer.
161
+
162
+ The bundled hypermedia adapters ship with an `entities` method to add arrays of entities, and an `entity` method to add a single entity.
163
+
164
+ ```ruby
165
+ # single entity
166
+ entity :child, item.child do |child, s|
167
+ s.name child.name
168
+ s.id child.id
169
+ end
170
+
171
+ # list of entities
172
+ entities :children, item.children do |child, s|
173
+ s.name child.name
174
+ s.id child.id
175
+ end
176
+ ```
177
+
178
+ Both can be expressed using a separate serializer:
179
+
180
+ ```ruby
181
+ # single entity
182
+ entity :child, item.child, ChildSerializer
183
+
184
+ # list of entities
185
+ entities :children, item.children, ChildSerializer
186
+ ```
187
+
188
+ The way sub-entities are rendered in the final payload is up to the adapter. In HAL the example above would be:
189
+
190
+ ```json
191
+ {
192
+ ...,
193
+ "_embedded": {
194
+ "child": {"name": "child's name", "id": 1},
195
+ "children": [
196
+ {"name": "child 2 name", "id": 2},
197
+ {"name": "child 3 name", "id": 3},
198
+ ...
199
+ ]
200
+ }
201
+ }
202
+ ```
203
+
204
+ ## Subclassing
205
+
206
+ Serializers can be subclassed, for example if you want all your serializers to share the same adapter or add shared helper methods.
207
+
208
+ ```ruby
209
+ class MyAppSerializer < Oat::Serializer
210
+ adapter Oat::Adapters::HAL
211
+
212
+ protected
213
+
214
+ def format_price(price)
215
+ Money.new(price, 'GBP').format
216
+ end
217
+ end
218
+ ```
219
+
220
+ ```ruby
221
+ class ProductSerializer < MyAppSerializer
222
+ schema do
223
+ property :title, item.title
224
+ property :price, format_price(item.price)
225
+ end
226
+ end
227
+ ```
228
+
229
+ This is useful if you want your serializers to better express your app's domain. For example, a serializer for a social app:
230
+
231
+ ```ruby
232
+ class UserSerializer < SocialSerializer
233
+ schema do
234
+ name item.name
235
+ email item.email
236
+ # friend entities
237
+ friends item.friends
238
+ end
239
+ end
240
+ ```
241
+
242
+ The superclass defines the methods `name`, `email` and `friends`, which in turn delegate to the adapter's setters.
243
+
244
+ ```ruby
245
+ class SocialSerializer < Oat::Serializer
246
+ adapter Oat::Adapters::HAL # or whatever
247
+
248
+ # friendly setters
249
+ protected
250
+
251
+ def name(value)
252
+ property :name, value
253
+ end
254
+
255
+ def email(value)
256
+ property :email, value
257
+ end
258
+
259
+ def friends(objects)
260
+ entities :friends, objects, FriendSerializer
261
+ end
262
+ end
263
+ ```
264
+
265
+ ## URLs
266
+
267
+ Hypermedia is all about the URLs linking your resources together. Oat adapters can have methods to declare links in your entity schemae but it's up to your code/framework how to create those links.
268
+ A simple stand-alone implementation could be:
269
+
270
+ ```ruby
271
+ class ProductSerializer < Oat::Serializer
272
+ adapter Oat::Adapters::HAL
273
+
274
+ schema do
275
+ link :self, href: product_url(item.id)
276
+ ...
277
+ end
278
+
279
+ protected
280
+
281
+ # helper URL method
282
+ def product_url(id)
283
+ "https://api.com/products/#{id}"
284
+ end
285
+ end
286
+ ```
287
+
288
+ In frameworks like Rails, you'll probably want to use the URL helpers created by the `routes.rb` file. Two options:
289
+
290
+ ### Pass a context object to serializers
291
+
292
+ You can pass a context object as second argument to serializers. This object will be passed to nested serializers too. For example, you can pass the controller instance itself.
293
+
294
+ ```ruby
295
+ # users_controller.rb
296
+
297
+ def show
298
+ user = User.find(params[:id])
299
+ render json: UserSerializer.new(user, self)
300
+ end
301
+ ```
302
+
303
+ Then, in the `UserSerializer`:
304
+ ```ruby
305
+ class ProductSerializer < Oat::Serializer
306
+ adapter Oat::Adapters::HAL
307
+
308
+ schema do
309
+ # `context` is the controller, which responds to URL helpers.
310
+ link :self, href: context.product_url(item)
311
+ ...
312
+ end
313
+ end
314
+ ```
315
+
316
+ ### Mixin Rails' routing module
317
+
318
+ Alternatively, you can mix in Rails routing helpers directly into your serializers.
319
+
320
+ ```ruby
321
+ class MyAppParentSerializer < Oat::Serializer
322
+ include ActionDispatch::Routing::UrlFor
323
+ include Rails.application.routes.url_helpers
324
+ def self.default_url_options
325
+ Rails.application.routes.default_url_options
326
+ end
327
+
328
+ adapter Oat::Adapters::HAL
329
+ end
330
+ ```
331
+
332
+ Then your serializer sub-classes can just use the URL helpers
333
+
334
+ ```ruby
335
+ class ProductSerializer < MyAppParentSerializer
336
+ schema do
337
+ # `product_url` is mixed in from Rails' routing system.
338
+ link :self, href: product_url(item)
339
+ ...
340
+ end
341
+ end
342
+ ```
343
+
344
+ However, since serializers don't have access to the current request, for this to work you must configure each environment's base host. In `config/environments/production.rb`:
345
+
346
+ ```ruby
347
+ config.after_initialize do
348
+ Rails.application.routes.default_url_options[:host] = 'api.com'
349
+ end
350
+ ```
351
+
352
+ NOTE: Rails URL helpers could be handled by a separate oat-rails gem.
353
+
354
+ ## Custom adapters.
355
+
356
+ An adapter's primary concern is to abstract away the details of specific media types.
357
+
358
+ Methods defined in an adapter are exposed as `schema` setters in your serializers.
359
+ Ideally different adapters should expose the same methods so your serializers can switch adapters without loosing compatibility. For example all bundled adapters expose the following methods:
360
+
361
+ * `type` The type of the entity. Renders as "class" in Siren, root node name in JsonAPI, not used in HAL.
362
+ * `link` Add a link with `rel` and `href`. Renders inside "_links" in HAL, "links" in Siren and JsonAP.
363
+ * `property` Add a property to the entity. Top level attributes in HAL and JsonAPI, "properties" node in Siren.
364
+ * `properties` Yield a properties object to set many properties at once.
365
+ * `entity` Add a single sub-entity. "_embedded" node in HAL, "entities" in Siren, "linked" in JsonAPI.
366
+ * `entities` Add a collection of sub-entities.
367
+
368
+ You can define these in your own custom adapters if you're using your own media type or need to implement a different spec.
369
+
370
+ ```ruby
371
+ class CustomAdapter < Oat::Adapter
372
+
373
+ def type(*types)
374
+ data[:rel] = types
375
+ end
376
+
377
+ def property(name, value)
378
+ data[:attr][name] = value
379
+ end
380
+
381
+ def entity(name, obj, serializer_class = nil, &block)
382
+ data[:nested_documents] = serializer_from_block_or_class(obj, serializer_class, &block)
383
+ end
384
+
385
+ ... etc
386
+ end
387
+ ```
388
+
389
+ An adapter class provides a `data` object (just a Hash) that stores your data in the structure you want. An adapter's public methods are exposed to your serializers.
390
+
391
+ ## Unconventional or domain specific adapters
392
+
393
+ Although adapters should in general comply with a common interface, you can still create your own domain-specific adapters if you need to.
394
+
395
+ Let's say you're working on a media-type specification specializing in describing social networks and want your payload definitions to express the concept of "friendship". You want your serializers to look like:
396
+
397
+ ```ruby
398
+ class UserSerializer < Oat::Serializer
399
+ adapter SocialAdapter
400
+
401
+ schema do
402
+ name item.name
403
+ email item.email
404
+
405
+ # Friend entity
406
+ friends item.friends do |friend, friend_serializer|
407
+ friend_serializer.name friend.name
408
+ friend_serializer.email friend.email
409
+ end
410
+ end
411
+ end
412
+ ```
413
+
414
+ A custom media type could return JSON looking looking like this:
415
+
416
+ ```json
417
+ {
418
+ "name": "Joe",
419
+ "email": "joe@email.com",
420
+ "friends": [
421
+ {"name": "Jane", "email":"jane@email.com"},
422
+ ...
423
+ ]
424
+ }
425
+ ```
426
+
427
+ The adapter for that would be:
428
+
429
+ ```ruby
430
+ class SocialAdapter < Oat::Adapter
431
+
432
+ def name(value)
433
+ data[:name] = value
434
+ end
435
+
436
+ def email(value)
437
+ data[:email] = value
438
+ end
439
+
440
+ def friends(friend_list, serializer_class = nil, &block)
441
+ data[:friends] = friend_list.map do |obj|
442
+ serializer_from_block_or_class(obj, serializer_class, &block)
443
+ end
444
+ end
445
+ end
446
+ ```
447
+
448
+ But you can easily write an adapter that turns your domain-specific serializers into HAL-compliant JSON.
449
+
450
+ ```ruby
451
+ class SocialHalAdapter < Oat::Adapters::HAL
452
+
453
+ def name(value)
454
+ property :name, value
455
+ end
456
+
457
+ def email(value)
458
+ property :email, value
459
+ end
460
+
461
+ def friends(friend_list, serializer_class = nil, &block)
462
+ entities :friends, friend_list, serializer_class, &block
463
+ end
464
+ end
465
+ ```
466
+
467
+ The result for the SocialHalAdapter is:
468
+
469
+ ```json
470
+ {
471
+ "name": "Joe",
472
+ "email": "joe@email.com",
473
+ "_embedded": {
474
+ "friends": [
475
+ {"name": "Jane", "email":"jane@email.com"},
476
+ ...
477
+ ]
478
+ }
479
+ }
480
+ ```
481
+
482
+ You can take a look at [the built-in Hypermedia adapters](https://github.com/ismasan/oat/tree/master/lib/oat/adapters) for guidance.
483
+
484
+ ## Installation
485
+
486
+ Add this line to your application's Gemfile:
487
+
488
+ gem 'oat'
489
+
490
+ And then execute:
491
+
492
+ $ bundle
493
+
494
+ Or install it yourself as:
495
+
496
+ $ gem install oat
497
+
498
+ ## TODO / contributions welcome
499
+
500
+ * Siren actions.
501
+ * JsonAPI URL and ID modes, top-level links
502
+ * testing module that can be used for testing spec-compliance in user apps?
503
+
504
+ ## Contributing
505
+
506
+ 1. Fork it
507
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
508
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
509
+ 4. Push to the branch (`git push origin my-new-feature`)
510
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
@@ -0,0 +1,36 @@
1
+ require 'oat/props'
2
+ module Oat
3
+ class Adapter
4
+
5
+ def initialize(serializer)
6
+ @serializer = serializer
7
+ @data = Hash.new{|h,k| h[k] = {}}
8
+ end
9
+
10
+ def to_hash
11
+ data
12
+ end
13
+
14
+ protected
15
+
16
+ attr_reader :data, :serializer
17
+
18
+ def yield_props(&block)
19
+ props = Props.new
20
+ serializer.instance_exec(props, &block)
21
+ props.to_hash
22
+ end
23
+
24
+ def serializer_from_block_or_class(obj, serializer_class = nil, &block)
25
+ if block_given?
26
+ serializer_class = Class.new(serializer.class)
27
+ serializer_class.adapter self.class
28
+ s = serializer_class.new(obj, serializer.context, serializer.adapter_class, serializer.top)
29
+ serializer.top.instance_exec(obj, s, &block)
30
+ s.to_hash
31
+ else
32
+ serializer_class.new(obj, serializer.context, serializer.adapter_class).to_hash
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,29 @@
1
+ # http://stateless.co/hal_specification.html
2
+ module Oat
3
+ module Adapters
4
+ class HAL < Oat::Adapter
5
+ def link(rel, opts = {})
6
+ data[:_links][rel] = opts
7
+ end
8
+
9
+ def properties(&block)
10
+ data.merge! yield_props(&block)
11
+ end
12
+
13
+ def property(key, value)
14
+ data[key] = value
15
+ end
16
+
17
+ def entity(name, obj, serializer_class = nil, &block)
18
+ data[:_embedded][name] = serializer_from_block_or_class(obj, serializer_class, &block)
19
+ end
20
+
21
+ def entities(name, collection, serializer_class = nil, &block)
22
+ data[:_embedded][name] = collection.map do |obj|
23
+ serializer_from_block_or_class(obj, serializer_class, &block)
24
+ end
25
+ end
26
+
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,62 @@
1
+ # http://jsonapi.org/format/#url-based-json-api
2
+ require 'active_support/core_ext/string/inflections'
3
+ module Oat
4
+ module Adapters
5
+ class JsonAPI < Oat::Adapter
6
+
7
+ def initialize(*args)
8
+ super
9
+ @entities = {}
10
+ end
11
+
12
+ def type(*types)
13
+ @root_name = types.first.to_s
14
+ end
15
+
16
+ def link(rel, opts = {})
17
+ data[:links][rel] = opts[:href]
18
+ end
19
+
20
+ def properties(&block)
21
+ data.merge! yield_props(&block)
22
+ end
23
+
24
+ def property(key, value)
25
+ data[key] = value
26
+ end
27
+
28
+ def entity(name, obj, serializer_class = nil, &block)
29
+ ent = entity_without_root(obj, serializer_class, &block)
30
+ link name, href: ent[:id]
31
+ (@entities[name.to_s.pluralize.to_sym] ||= []) << ent
32
+ end
33
+
34
+ def entities(name, collection, serializer_class = nil, &block)
35
+ link_name = name.to_s.pluralize.to_sym
36
+ data[:links][link_name] = []
37
+
38
+ collection.each do |obj|
39
+ ent = entity_without_root(obj, serializer_class, &block)
40
+ data[:links][link_name] << ent[:id]
41
+ (@entities[link_name] ||= []) << ent
42
+ end
43
+ end
44
+
45
+ def to_hash
46
+ raise "JSON API entities MUST define a type. Use type 'user' in your serializers" unless root_name
47
+ h = {root_name.pluralize.to_sym => [data]}
48
+ h[:linked] = @entities if @entities.keys.any?
49
+ h
50
+ end
51
+
52
+ protected
53
+
54
+ attr_reader :root_name
55
+
56
+ def entity_without_root(obj, serializer_class = nil, &block)
57
+ serializer_from_block_or_class(obj, serializer_class, &block).values.first.first
58
+ end
59
+
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,40 @@
1
+ # https://github.com/kevinswiber/siren
2
+ module Oat
3
+ module Adapters
4
+ class Siren < Oat::Adapter
5
+
6
+ def initialize(*args)
7
+ super
8
+ data[:links] = []
9
+ data[:entities] = []
10
+ end
11
+
12
+ def type(*types)
13
+ data[:class] = types
14
+ end
15
+
16
+ def link(rel, opts = {})
17
+ data[:links] << {rel: [rel]}.merge(opts)
18
+ end
19
+
20
+ def properties(&block)
21
+ data[:properties].merge! yield_props(&block)
22
+ end
23
+
24
+ def property(key, value)
25
+ data[:properties][key] = value
26
+ end
27
+
28
+ def entity(name, obj, serializer_class = nil, &block)
29
+ data[:entities] << serializer_from_block_or_class(obj, serializer_class, &block)
30
+ end
31
+
32
+ def entities(name, collection, serializer_class = nil, &block)
33
+ collection.each do |obj|
34
+ entity name, obj, serializer_class, &block
35
+ end
36
+ end
37
+
38
+ end
39
+ end
40
+ end
data/lib/oat/props.rb ADDED
@@ -0,0 +1,21 @@
1
+ module Oat
2
+ class Props < BasicObject
3
+
4
+ def initialize
5
+ @attributes = {}
6
+ end
7
+
8
+ def _from(data)
9
+ @attributes = data.to_hash
10
+ end
11
+
12
+ def method_missing(name, value)
13
+ @attributes[name] = value
14
+ end
15
+
16
+ def to_hash
17
+ @attributes
18
+ end
19
+
20
+ end
21
+ end
@@ -0,0 +1,50 @@
1
+ require 'active_support/core_ext/class/attribute'
2
+ module Oat
3
+ class Serializer
4
+
5
+ class_attribute :_adapter, :logger
6
+
7
+ def self.schema(&block)
8
+ @schema = block if block_given?
9
+ @schema || Proc.new{}
10
+ end
11
+
12
+ def self.adapter(adapter_class = nil)
13
+ self._adapter = adapter_class if adapter_class
14
+ self._adapter
15
+ end
16
+
17
+ def self.warn(msg)
18
+ logger ? logger.warning(msg) : puts(msg)
19
+ end
20
+
21
+ attr_reader :item, :context, :adapter_class, :adapter
22
+
23
+ def initialize(item, context = nil, _adapter_class = nil, parent_serializer = nil)
24
+ @item, @context = item, context
25
+ @parent_serializer = parent_serializer
26
+ @adapter_class = _adapter_class || self.class.adapter
27
+ @adapter = @adapter_class.new(self)
28
+ end
29
+
30
+ def top
31
+ @top ||= @parent_serializer || self
32
+ end
33
+
34
+ def method_missing(name, *args, &block)
35
+ if adapter.respond_to?(name)
36
+ adapter.send(name, *args, &block)
37
+ else
38
+ self.class.warn "[#{adapter.class}] does not implement ##{name}. Called with #{args}"
39
+ end
40
+ end
41
+
42
+ def to_hash
43
+ @to_hash ||= (
44
+ self.instance_eval &self.class.schema
45
+ adapter.to_hash
46
+ )
47
+ end
48
+
49
+ end
50
+ end
@@ -0,0 +1,3 @@
1
+ module Oat
2
+ VERSION = "0.0.1"
3
+ end
data/lib/oat.rb ADDED
@@ -0,0 +1,6 @@
1
+ require "oat/version"
2
+
3
+ module Oat
4
+ require 'oat/serializer'
5
+ require 'oat/adapter'
6
+ end
data/oat.gemspec ADDED
@@ -0,0 +1,25 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'oat/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "oat"
8
+ spec.version = Oat::VERSION
9
+ spec.authors = ["Ismael Celis"]
10
+ spec.email = ["ismaelct@gmail.com"]
11
+ spec.description = %q{Oat helps you separate your API schema definitions from the underlying media type. Media types can be plugged or swapped on demand globally or on the content-negotiation phase}
12
+ spec.summary = %q{Adapters-based serializers with Hypermedia support}
13
+ spec.homepage = ""
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files`.split($/)
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", ">= 4.0"
22
+ spec.add_development_dependency "bundler", "~> 1.3"
23
+ spec.add_development_dependency "rake"
24
+ spec.add_development_dependency "rspec"
25
+ end
@@ -0,0 +1,39 @@
1
+ require 'spec_helper'
2
+ require 'oat/adapters/hal'
3
+
4
+ describe Oat::Adapters::HAL do
5
+
6
+ include Fixtures
7
+
8
+ subject{ serializer_class.new(user, {name: 'some_controller'}, Oat::Adapters::HAL) }
9
+
10
+ describe '#to_hash' do
11
+ it 'produces a HAL-compliant hash' do
12
+ subject.to_hash.tap do |h|
13
+ # properties
14
+ h[:id].should == user.id
15
+ h[:name].should == user.name
16
+ h[:age].should == user.age
17
+ h[:controller_name].should == 'some_controller'
18
+ # links
19
+ h[:_links][:self][:href].should == "http://foo.bar.com/#{user.id}"
20
+ # embedded manager
21
+ h[:_embedded][:manager].tap do |m|
22
+ m[:id].should == manager.id
23
+ m[:name].should == manager.name
24
+ m[:age].should == manager.age
25
+ m[:_links][:self][:href].should == "http://foo.bar.com/#{manager.id}"
26
+ end
27
+ # embedded friends
28
+ h[:_embedded][:friends].size.should == 1
29
+ h[:_embedded][:friends][0].tap do |f|
30
+ f[:id].should == friend.id
31
+ f[:name].should == friend.name
32
+ f[:age].should == friend.age
33
+ f[:controller_name].should == 'some_controller'
34
+ f[:_links][:self][:href].should == "http://foo.bar.com/#{friend.id}"
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,43 @@
1
+ require 'spec_helper'
2
+ require 'oat/adapters/json_api'
3
+
4
+ describe Oat::Adapters::JsonAPI do
5
+
6
+ include Fixtures
7
+
8
+ subject{ serializer_class.new(user, {name: 'some_controller'}, Oat::Adapters::JsonAPI) }
9
+
10
+ describe '#to_hash' do
11
+ it 'produces a JSON-API compliant hash' do
12
+ payload = subject.to_hash
13
+ # embedded friends
14
+ payload[:linked][:friends][0].tap do |f|
15
+ f[:id].should == friend.id
16
+ f[:name].should == friend.name
17
+ f[:age].should == friend.age
18
+ f[:controller_name].should == 'some_controller'
19
+ f[:links][:self].should == "http://foo.bar.com/#{friend.id}"
20
+ end
21
+
22
+ # embedded manager
23
+ payload[:linked][:managers][0].tap do |m|
24
+ m[:id].should == manager.id
25
+ m[:name].should == manager.name
26
+ m[:age].should == manager.age
27
+ m[:links][:self].should == "http://foo.bar.com/#{manager.id}"
28
+ end
29
+
30
+ payload[:users][0].tap do |h|
31
+ h[:id].should == user.id
32
+ h[:name].should == user.name
33
+ h[:age].should == user.age
34
+ h[:controller_name].should == 'some_controller'
35
+ # links
36
+ h[:links][:self].should == "http://foo.bar.com/#{user.id}"
37
+ # these links are added by embedding entities
38
+ h[:links][:manager].should == manager.id
39
+ h[:links][:friends].should == [friend.id]
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,45 @@
1
+ require 'spec_helper'
2
+ require 'oat/adapters/siren'
3
+
4
+ describe Oat::Adapters::Siren do
5
+
6
+ include Fixtures
7
+
8
+ subject{ serializer_class.new(user, {name: 'some_controller'}, Oat::Adapters::Siren) }
9
+
10
+ describe '#to_hash' do
11
+ it 'produces a Siren-compliant hash' do
12
+ subject.to_hash.tap do |h|
13
+ #siren class
14
+ h[:class].should == ['user']
15
+ # properties
16
+ h[:properties][:id].should == user.id
17
+ h[:properties][:name].should == user.name
18
+ h[:properties][:age].should == user.age
19
+ h[:properties][:controller_name].should == 'some_controller'
20
+ # links
21
+ h[:links][0][:rel].should == [:self]
22
+ h[:links][0][:href].should == "http://foo.bar.com/#{user.id}"
23
+ # embedded manager
24
+ h[:entities][1].tap do |m|
25
+ m[:class].should == ['manager']
26
+ m[:properties][:id].should == manager.id
27
+ m[:properties][:name].should == manager.name
28
+ m[:properties][:age].should == manager.age
29
+ m[:links][0][:rel].should == [:self]
30
+ m[:links][0][:href].should == "http://foo.bar.com/#{manager.id}"
31
+ end
32
+ # embedded friends
33
+ h[:entities][0].tap do |f|
34
+ f[:class].should == ['user']
35
+ f[:properties][:id].should == friend.id
36
+ f[:properties][:name].should == friend.name
37
+ f[:properties][:age].should == friend.age
38
+ f[:properties][:controller_name].should == 'some_controller'
39
+ f[:links][0][:rel].should == [:self]
40
+ f[:links][0][:href].should == "http://foo.bar.com/#{friend.id}"
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
data/spec/fixtures.rb ADDED
@@ -0,0 +1,42 @@
1
+ module Fixtures
2
+
3
+ def self.included(base)
4
+ base.let(:user_class) { Struct.new(:name, :age, :id, :friends, :manager) }
5
+ base.let(:friend) { user_class.new('Joe', 33, 2, []) }
6
+ base.let(:manager) { user_class.new('Jane', 29, 3, []) }
7
+ base.let(:user) { user_class.new('Ismael', 35, 1, [friend], manager) }
8
+ base.let(:serializer_class) do
9
+ Class.new(Oat::Serializer) do
10
+ klass = self
11
+
12
+ schema do
13
+ type 'user'
14
+ link :self, href: url_for(item.id)
15
+
16
+ property :id, item.id
17
+ properties do |attrs|
18
+ attrs.name item.name
19
+ attrs.age item.age
20
+ attrs.controller_name context[:name]
21
+ end
22
+
23
+ entities :friends, item.friends, klass
24
+
25
+ entity :manager, item.manager do |manager, s|
26
+ s.type 'manager'
27
+ s.link :self, href: url_for(manager.id)
28
+ s.properties do |attrs|
29
+ attrs.id manager.id
30
+ attrs.name manager.name
31
+ attrs.age manager.age
32
+ end
33
+ end if item.manager
34
+ end
35
+
36
+ def url_for(id)
37
+ "http://foo.bar.com/#{id}"
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,69 @@
1
+ require 'spec_helper'
2
+
3
+ describe Oat::Serializer do
4
+
5
+ before do
6
+ @adapter_class = Class.new(Oat::Adapter) do
7
+ def attributes(&block)
8
+ data[:attributes].merge!(yield_props(&block))
9
+ end
10
+
11
+ def attribute(key, value)
12
+ data[:attributes][key] = value
13
+ end
14
+
15
+ def link(rel, url)
16
+ data[:links][rel] = url
17
+ end
18
+ end
19
+
20
+ @sc = Class.new(Oat::Serializer) do
21
+
22
+ schema do
23
+ my_attribute 'Hello'
24
+ attribute :id, item.id
25
+ attributes do |attrs|
26
+ attrs.name item.name
27
+ attrs.age item.age
28
+ attrs.controller_name context[:name]
29
+ end
30
+ link :self, url_for(item.id)
31
+ end
32
+
33
+ def url_for(id)
34
+ "http://foo.bar.com/#{id}"
35
+ end
36
+
37
+ def my_attribute(value)
38
+ attribute :special, value
39
+ end
40
+ end
41
+
42
+ @sc.adapter @adapter_class
43
+ end
44
+
45
+ let(:user_class) do
46
+ Struct.new(:name, :age, :id, :friends)
47
+ end
48
+
49
+ let(:user1) { user_class.new('Ismael', 35, 1, []) }
50
+
51
+ it 'should have a version number' do
52
+ Oat::VERSION.should_not be_nil
53
+ end
54
+
55
+ describe '#to_hash' do
56
+ it 'builds Hash from item and context with attributes as defined in adapter' do
57
+ serializer = @sc.new(user1, name: 'some_controller')
58
+ serializer.to_hash.tap do |h|
59
+ h[:attributes][:special].should == 'Hello'
60
+ h[:attributes][:id].should == user1.id
61
+ h[:attributes][:name].should == user1.name
62
+ h[:attributes][:age].should == user1.age
63
+ h[:attributes][:controller_name].should == 'some_controller'
64
+ h[:links][:self].should == "http://foo.bar.com/#{user1.id}"
65
+ end
66
+ end
67
+ end
68
+
69
+ end
@@ -0,0 +1,3 @@
1
+ $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
2
+ require 'oat'
3
+ require 'fixtures'
metadata ADDED
@@ -0,0 +1,131 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: oat
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Ismael Celis
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2013-11-18 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activesupport
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - '>='
18
+ - !ruby/object:Gem::Version
19
+ version: '4.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - '>='
25
+ - !ruby/object:Gem::Version
26
+ version: '4.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: bundler
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ~>
32
+ - !ruby/object:Gem::Version
33
+ version: '1.3'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ~>
39
+ - !ruby/object:Gem::Version
40
+ version: '1.3'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - '>='
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - '>='
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rspec
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - '>='
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ description: Oat helps you separate your API schema definitions from the underlying
70
+ media type. Media types can be plugged or swapped on demand globally or on the content-negotiation
71
+ phase
72
+ email:
73
+ - ismaelct@gmail.com
74
+ executables: []
75
+ extensions: []
76
+ extra_rdoc_files: []
77
+ files:
78
+ - .gitignore
79
+ - .rspec
80
+ - .travis.yml
81
+ - Gemfile
82
+ - LICENSE
83
+ - LICENSE.txt
84
+ - README.md
85
+ - Rakefile
86
+ - lib/oat.rb
87
+ - lib/oat/adapter.rb
88
+ - lib/oat/adapters/hal.rb
89
+ - lib/oat/adapters/json_api.rb
90
+ - lib/oat/adapters/siren.rb
91
+ - lib/oat/props.rb
92
+ - lib/oat/serializer.rb
93
+ - lib/oat/version.rb
94
+ - oat.gemspec
95
+ - spec/adapters/hal_spec.rb
96
+ - spec/adapters/json_api_spec.rb
97
+ - spec/adapters/siren_spec.rb
98
+ - spec/fixtures.rb
99
+ - spec/serializer_spec.rb
100
+ - spec/spec_helper.rb
101
+ homepage: ''
102
+ licenses:
103
+ - MIT
104
+ metadata: {}
105
+ post_install_message:
106
+ rdoc_options: []
107
+ require_paths:
108
+ - lib
109
+ required_ruby_version: !ruby/object:Gem::Requirement
110
+ requirements:
111
+ - - '>='
112
+ - !ruby/object:Gem::Version
113
+ version: '0'
114
+ required_rubygems_version: !ruby/object:Gem::Requirement
115
+ requirements:
116
+ - - '>='
117
+ - !ruby/object:Gem::Version
118
+ version: '0'
119
+ requirements: []
120
+ rubyforge_project:
121
+ rubygems_version: 2.0.3
122
+ signing_key:
123
+ specification_version: 4
124
+ summary: Adapters-based serializers with Hypermedia support
125
+ test_files:
126
+ - spec/adapters/hal_spec.rb
127
+ - spec/adapters/json_api_spec.rb
128
+ - spec/adapters/siren_spec.rb
129
+ - spec/fixtures.rb
130
+ - spec/serializer_spec.rb
131
+ - spec/spec_helper.rb