oat 0.0.1

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: 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