oat 0.0.1 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +68 -5
- data/lib/oat/adapter.rb +3 -1
- data/lib/oat/adapters/hal.rb +1 -1
- data/lib/oat/adapters/json_api.rb +13 -6
- data/lib/oat/adapters/siren.rb +50 -2
- data/lib/oat/serializer.rb +14 -1
- data/lib/oat/version.rb +1 -1
- data/oat.gemspec +1 -1
- data/spec/adapters/hal_spec.rb +28 -1
- data/spec/adapters/json_api_spec.rb +32 -1
- data/spec/adapters/siren_spec.rb +43 -1
- data/spec/fixtures.rb +14 -5
- metadata +3 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8bb1271b6a91bda9b273a02d29e48f999e240a35
|
4
|
+
data.tar.gz: 6ac4f8595d78f974ed688182ef021bc688958e65
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5a4d8281a3a0a38e6a21556388df68c43c20dcd41c2e8df4cbd57e695a2d43d966361e3475b58a233f055d4afd1f433b9e73b571d3cad1f1638da73745505296
|
7
|
+
data.tar.gz: df432c06d285565c54a7102d96cb48cd13c4470bf283f23d94b91d2b809c900611140b139a299bc8cfd884a429702cf7d3fed43916313ee7b52db9eb1b0c9b7e
|
data/README.md
CHANGED
@@ -1,10 +1,10 @@
|
|
1
1
|
# Oat [![Build Status](https://travis-ci.org/ismasan/oat.png)](https://travis-ci.org/ismasan/oat)
|
2
2
|
|
3
|
-
Adapters-based API serializers with Hypermedia support for Ruby apps.
|
3
|
+
Adapters-based API serializers with Hypermedia support for Ruby apps. Read [the blog post](http://new-bamboo.co.uk/blog/2013/11/21/oat-explicit-media-type-serializers-in-ruby) for context and motivation.
|
4
4
|
|
5
5
|
## What
|
6
6
|
|
7
|
-
Oat lets you design your API payloads succinctly while conforming to your *media type* of choice (hypermedia or not).
|
7
|
+
Oat lets you design your API payloads succinctly while conforming to your *media type* of choice (hypermedia or not).
|
8
8
|
The details of the media type are dealt with by pluggable adapters.
|
9
9
|
|
10
10
|
Oat ships with adapters for HAL, Siren and JsonAPI, and it's easy to write your own.
|
@@ -23,6 +23,7 @@ class ProductSerializer < Oat::Serializer
|
|
23
23
|
schema do
|
24
24
|
type "product"
|
25
25
|
link :self, href: product_url(item)
|
26
|
+
|
26
27
|
properties do |props|
|
27
28
|
props.title item.title
|
28
29
|
props.price item.price
|
@@ -48,6 +49,68 @@ The full serializer signature is `item`, `context`, `adapter_class`.
|
|
48
49
|
* `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
50
|
* `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
|
|
52
|
+
### Defining Properties
|
53
|
+
|
54
|
+
There are a few different ways of defining properties on a serializer.
|
55
|
+
|
56
|
+
Properties can be added explicitly using `property`. In this case, you can map an arbitrary value to an arbitrary key:
|
57
|
+
|
58
|
+
```ruby
|
59
|
+
require 'oat/adapters/hal'
|
60
|
+
class ProductSerializer < Oat::Serializer
|
61
|
+
adapter Oat::Adapters::HAL
|
62
|
+
|
63
|
+
schema do
|
64
|
+
type "product"
|
65
|
+
link :self, href: product_url(item)
|
66
|
+
|
67
|
+
property :title, item.title
|
68
|
+
property :price, item.price
|
69
|
+
property :description, item.blurb
|
70
|
+
property :the_number_one, 1
|
71
|
+
end
|
72
|
+
end
|
73
|
+
```
|
74
|
+
|
75
|
+
Similarly, properties can be added within a block using `properties` to be more concise or make the code more readable. Again, these will set arbitrary values for arbitrary keys:
|
76
|
+
|
77
|
+
```ruby
|
78
|
+
require 'oat/adapters/hal'
|
79
|
+
class ProductSerializer < Oat::Serializer
|
80
|
+
adapter Oat::Adapters::HAL
|
81
|
+
|
82
|
+
schema do
|
83
|
+
type "product"
|
84
|
+
link :self, href: product_url(item)
|
85
|
+
|
86
|
+
properties do |p|
|
87
|
+
p.title item.title
|
88
|
+
p.price item.price
|
89
|
+
p.description item.blurb
|
90
|
+
p.the_number_one 1
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
```
|
95
|
+
|
96
|
+
In many cases, you will want to simply map the properties of `item` to a property in the serializer. This can be easily done using `map_properties`. This method takes a list of method or attribute names to which `item` will respond. Note that you cannot assign arbitrary values and keys using `map_properties` - the serializer will simply add a key and call that method on `item` to assign the value.
|
97
|
+
|
98
|
+
```ruby
|
99
|
+
require 'oat/adapters/hal'
|
100
|
+
class ProductSerializer < Oat::Serializer
|
101
|
+
adapter Oat::Adapters::HAL
|
102
|
+
|
103
|
+
schema do
|
104
|
+
type "product"
|
105
|
+
link :self, href: product_url(item)
|
106
|
+
|
107
|
+
map_properties :title, :price
|
108
|
+
property :description, item.blurb
|
109
|
+
property :the_number_one, 1
|
110
|
+
end
|
111
|
+
end
|
112
|
+
```
|
113
|
+
|
51
114
|
## Adapters
|
52
115
|
|
53
116
|
Using the included [HAL](http://stateless.co/hal_specification.html) adapter, the `ProductSerializer` above would render the following JSON:
|
@@ -264,7 +327,7 @@ end
|
|
264
327
|
|
265
328
|
## URLs
|
266
329
|
|
267
|
-
Hypermedia is all about the URLs linking your resources together. Oat adapters can have methods to declare links in your entity
|
330
|
+
Hypermedia is all about the URLs linking your resources together. Oat adapters can have methods to declare links in your entity schema but it's up to your code/framework how to create those links.
|
268
331
|
A simple stand-alone implementation could be:
|
269
332
|
|
270
333
|
```ruby
|
@@ -277,7 +340,7 @@ class ProductSerializer < Oat::Serializer
|
|
277
340
|
end
|
278
341
|
|
279
342
|
protected
|
280
|
-
|
343
|
+
|
281
344
|
# helper URL method
|
282
345
|
def product_url(id)
|
283
346
|
"https://api.com/products/#{id}"
|
@@ -355,7 +418,7 @@ NOTE: Rails URL helpers could be handled by a separate oat-rails gem.
|
|
355
418
|
|
356
419
|
An adapter's primary concern is to abstract away the details of specific media types.
|
357
420
|
|
358
|
-
Methods defined in an adapter are exposed as `schema` setters in your serializers.
|
421
|
+
Methods defined in an adapter are exposed as `schema` setters in your serializers.
|
359
422
|
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
423
|
|
361
424
|
* `type` The type of the entity. Renders as "class" in Siren, root node name in JsonAPI, not used in HAL.
|
data/lib/oat/adapter.rb
CHANGED
@@ -22,6 +22,8 @@ module Oat
|
|
22
22
|
end
|
23
23
|
|
24
24
|
def serializer_from_block_or_class(obj, serializer_class = nil, &block)
|
25
|
+
return nil if obj.nil?
|
26
|
+
|
25
27
|
if block_given?
|
26
28
|
serializer_class = Class.new(serializer.class)
|
27
29
|
serializer_class.adapter self.class
|
@@ -33,4 +35,4 @@ module Oat
|
|
33
35
|
end
|
34
36
|
end
|
35
37
|
end
|
36
|
-
end
|
38
|
+
end
|
data/lib/oat/adapters/hal.rb
CHANGED
@@ -26,9 +26,12 @@ module Oat
|
|
26
26
|
end
|
27
27
|
|
28
28
|
def entity(name, obj, serializer_class = nil, &block)
|
29
|
+
@entities[name.to_s.pluralize.to_sym] ||= []
|
29
30
|
ent = entity_without_root(obj, serializer_class, &block)
|
30
|
-
|
31
|
-
|
31
|
+
if ent
|
32
|
+
link name, href: ent[:id]
|
33
|
+
@entities[name.to_s.pluralize.to_sym] << ent
|
34
|
+
end
|
32
35
|
end
|
33
36
|
|
34
37
|
def entities(name, collection, serializer_class = nil, &block)
|
@@ -36,9 +39,12 @@ module Oat
|
|
36
39
|
data[:links][link_name] = []
|
37
40
|
|
38
41
|
collection.each do |obj|
|
42
|
+
@entities[link_name] ||= []
|
39
43
|
ent = entity_without_root(obj, serializer_class, &block)
|
40
|
-
|
41
|
-
|
44
|
+
if ent
|
45
|
+
data[:links][link_name] << ent[:id]
|
46
|
+
@entities[link_name] << ent
|
47
|
+
end
|
42
48
|
end
|
43
49
|
end
|
44
50
|
|
@@ -54,9 +60,10 @@ module Oat
|
|
54
60
|
attr_reader :root_name
|
55
61
|
|
56
62
|
def entity_without_root(obj, serializer_class = nil, &block)
|
57
|
-
serializer_from_block_or_class(obj, serializer_class, &block)
|
63
|
+
ent = serializer_from_block_or_class(obj, serializer_class, &block)
|
64
|
+
ent.values.first.first if ent
|
58
65
|
end
|
59
66
|
|
60
67
|
end
|
61
68
|
end
|
62
|
-
end
|
69
|
+
end
|
data/lib/oat/adapters/siren.rb
CHANGED
@@ -7,6 +7,7 @@ module Oat
|
|
7
7
|
super
|
8
8
|
data[:links] = []
|
9
9
|
data[:entities] = []
|
10
|
+
data[:actions] = []
|
10
11
|
end
|
11
12
|
|
12
13
|
def type(*types)
|
@@ -26,7 +27,8 @@ module Oat
|
|
26
27
|
end
|
27
28
|
|
28
29
|
def entity(name, obj, serializer_class = nil, &block)
|
29
|
-
|
30
|
+
ent = serializer_from_block_or_class(obj, serializer_class, &block)
|
31
|
+
data[:entities] << ent if ent
|
30
32
|
end
|
31
33
|
|
32
34
|
def entities(name, collection, serializer_class = nil, &block)
|
@@ -35,6 +37,52 @@ module Oat
|
|
35
37
|
end
|
36
38
|
end
|
37
39
|
|
40
|
+
def action(name, &block)
|
41
|
+
action = Action.new(name)
|
42
|
+
block.call(action)
|
43
|
+
|
44
|
+
data[:actions] << action.data
|
45
|
+
end
|
46
|
+
|
47
|
+
class Action
|
48
|
+
attr_reader :data
|
49
|
+
|
50
|
+
def initialize(name)
|
51
|
+
@data = { name: name, class: [], fields: [] }
|
52
|
+
end
|
53
|
+
|
54
|
+
def class(value)
|
55
|
+
data[:class] << value
|
56
|
+
end
|
57
|
+
|
58
|
+
def field(name, &block)
|
59
|
+
field = Field.new(name)
|
60
|
+
block.call(field)
|
61
|
+
|
62
|
+
data[:fields] << field.data
|
63
|
+
end
|
64
|
+
|
65
|
+
%w(href method title).each do |attribute|
|
66
|
+
define_method(attribute) do |value|
|
67
|
+
data[attribute.to_sym] = value
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
class Field
|
72
|
+
attr_reader :data
|
73
|
+
|
74
|
+
def initialize(name)
|
75
|
+
@data = { name: name }
|
76
|
+
end
|
77
|
+
|
78
|
+
%w(type value).each do |attribute|
|
79
|
+
define_method(attribute) do |value|
|
80
|
+
data[attribute.to_sym] = value
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
38
86
|
end
|
39
87
|
end
|
40
|
-
end
|
88
|
+
end
|
data/lib/oat/serializer.rb
CHANGED
@@ -39,6 +39,10 @@ module Oat
|
|
39
39
|
end
|
40
40
|
end
|
41
41
|
|
42
|
+
def respond_to_missing?(method_name, include_private = false)
|
43
|
+
adapter.respond_to? method_name
|
44
|
+
end
|
45
|
+
|
42
46
|
def to_hash
|
43
47
|
@to_hash ||= (
|
44
48
|
self.instance_eval &self.class.schema
|
@@ -46,5 +50,14 @@ module Oat
|
|
46
50
|
)
|
47
51
|
end
|
48
52
|
|
53
|
+
def map_properties(*args)
|
54
|
+
args.each { |name| map_property name }
|
55
|
+
end
|
56
|
+
|
57
|
+
def map_property(name)
|
58
|
+
value = item.send(name)
|
59
|
+
property name, value
|
60
|
+
end
|
61
|
+
|
49
62
|
end
|
50
|
-
end
|
63
|
+
end
|
data/lib/oat/version.rb
CHANGED
data/oat.gemspec
CHANGED
@@ -10,7 +10,7 @@ Gem::Specification.new do |spec|
|
|
10
10
|
spec.email = ["ismaelct@gmail.com"]
|
11
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
12
|
spec.summary = %q{Adapters-based serializers with Hypermedia support}
|
13
|
-
spec.homepage = ""
|
13
|
+
spec.homepage = "https://github.com/ismasan/oat"
|
14
14
|
spec.license = "MIT"
|
15
15
|
|
16
16
|
spec.files = `git ls-files`.split($/)
|
data/spec/adapters/hal_spec.rb
CHANGED
@@ -35,5 +35,32 @@ describe Oat::Adapters::HAL do
|
|
35
35
|
end
|
36
36
|
end
|
37
37
|
end
|
38
|
+
|
39
|
+
context 'with a nil entity relationship' do
|
40
|
+
let(:manager) { nil }
|
41
|
+
|
42
|
+
it 'produces a HAL-compliant hash' do
|
43
|
+
subject.to_hash.tap do |h|
|
44
|
+
# properties
|
45
|
+
h[:id].should == user.id
|
46
|
+
h[:name].should == user.name
|
47
|
+
h[:age].should == user.age
|
48
|
+
h[:controller_name].should == 'some_controller'
|
49
|
+
# links
|
50
|
+
h[:_links][:self][:href].should == "http://foo.bar.com/#{user.id}"
|
51
|
+
# embedded manager
|
52
|
+
h[:_embedded].fetch(:manager).should be_nil
|
53
|
+
# embedded friends
|
54
|
+
h[:_embedded][:friends].size.should == 1
|
55
|
+
h[:_embedded][:friends][0].tap do |f|
|
56
|
+
f[:id].should == friend.id
|
57
|
+
f[:name].should == friend.name
|
58
|
+
f[:age].should == friend.age
|
59
|
+
f[:controller_name].should == 'some_controller'
|
60
|
+
f[:_links][:self][:href].should == "http://foo.bar.com/#{friend.id}"
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
38
65
|
end
|
39
|
-
end
|
66
|
+
end
|
@@ -39,5 +39,36 @@ describe Oat::Adapters::JsonAPI do
|
|
39
39
|
h[:links][:friends].should == [friend.id]
|
40
40
|
end
|
41
41
|
end
|
42
|
+
|
43
|
+
context 'with a nil entity relationship' do
|
44
|
+
let(:manager) { nil }
|
45
|
+
|
46
|
+
it 'produces a JSON-API compliant hash' do
|
47
|
+
payload = subject.to_hash
|
48
|
+
# embedded friends
|
49
|
+
payload[:linked][:friends][0].tap do |f|
|
50
|
+
f[:id].should == friend.id
|
51
|
+
f[:name].should == friend.name
|
52
|
+
f[:age].should == friend.age
|
53
|
+
f[:controller_name].should == 'some_controller'
|
54
|
+
f[:links][:self].should == "http://foo.bar.com/#{friend.id}"
|
55
|
+
end
|
56
|
+
|
57
|
+
# embedded manager
|
58
|
+
payload[:linked].fetch(:managers).should be_empty
|
59
|
+
|
60
|
+
payload[:users][0].tap do |h|
|
61
|
+
h[:id].should == user.id
|
62
|
+
h[:name].should == user.name
|
63
|
+
h[:age].should == user.age
|
64
|
+
h[:controller_name].should == 'some_controller'
|
65
|
+
# links
|
66
|
+
h[:links][:self].should == "http://foo.bar.com/#{user.id}"
|
67
|
+
# these links are added by embedding entities
|
68
|
+
h[:links].should_not include(:manager)
|
69
|
+
h[:links][:friends].should == [friend.id]
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
42
73
|
end
|
43
|
-
end
|
74
|
+
end
|
data/spec/adapters/siren_spec.rb
CHANGED
@@ -39,7 +39,49 @@ describe Oat::Adapters::Siren do
|
|
39
39
|
f[:links][0][:rel].should == [:self]
|
40
40
|
f[:links][0][:href].should == "http://foo.bar.com/#{friend.id}"
|
41
41
|
end
|
42
|
+
# action close_account
|
43
|
+
h[:actions][0].tap do |a|
|
44
|
+
a[:name].should == :close_account
|
45
|
+
a[:href].should == "http://foo.bar.com/#{user.id}/close_account"
|
46
|
+
a[:class].should == ['danger', 'irreversible']
|
47
|
+
a[:method].should == 'DELETE'
|
48
|
+
a[:fields][0].tap do |f|
|
49
|
+
f[:name].should == :current_password
|
50
|
+
f[:type].should == :password
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
context 'with a nil entity relationship' do
|
57
|
+
let(:manager) { nil }
|
58
|
+
|
59
|
+
it 'produces a Siren-compliant hash' do
|
60
|
+
subject.to_hash.tap do |h|
|
61
|
+
#siren class
|
62
|
+
h[:class].should == ['user']
|
63
|
+
# properties
|
64
|
+
h[:properties][:id].should == user.id
|
65
|
+
h[:properties][:name].should == user.name
|
66
|
+
h[:properties][:age].should == user.age
|
67
|
+
h[:properties][:controller_name].should == 'some_controller'
|
68
|
+
# links
|
69
|
+
h[:links][0][:rel].should == [:self]
|
70
|
+
h[:links][0][:href].should == "http://foo.bar.com/#{user.id}"
|
71
|
+
# embedded manager
|
72
|
+
h[:entities].any?{|o| o[:class].include?("manager")}.should be_false
|
73
|
+
# embedded friends
|
74
|
+
h[:entities][0].tap do |f|
|
75
|
+
f[:class].should == ['user']
|
76
|
+
f[:properties][:id].should == friend.id
|
77
|
+
f[:properties][:name].should == friend.name
|
78
|
+
f[:properties][:age].should == friend.age
|
79
|
+
f[:properties][:controller_name].should == 'some_controller'
|
80
|
+
f[:links][0][:rel].should == [:self]
|
81
|
+
f[:links][0][:href].should == "http://foo.bar.com/#{friend.id}"
|
82
|
+
end
|
83
|
+
end
|
42
84
|
end
|
43
85
|
end
|
44
86
|
end
|
45
|
-
end
|
87
|
+
end
|
data/spec/fixtures.rb
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
module Fixtures
|
2
|
-
|
2
|
+
|
3
3
|
def self.included(base)
|
4
4
|
base.let(:user_class) { Struct.new(:name, :age, :id, :friends, :manager) }
|
5
5
|
base.let(:friend) { user_class.new('Joe', 33, 2, []) }
|
@@ -14,9 +14,8 @@ module Fixtures
|
|
14
14
|
link :self, href: url_for(item.id)
|
15
15
|
|
16
16
|
property :id, item.id
|
17
|
+
map_properties :name, :age
|
17
18
|
properties do |attrs|
|
18
|
-
attrs.name item.name
|
19
|
-
attrs.age item.age
|
20
19
|
attrs.controller_name context[:name]
|
21
20
|
end
|
22
21
|
|
@@ -30,7 +29,17 @@ module Fixtures
|
|
30
29
|
attrs.name manager.name
|
31
30
|
attrs.age manager.age
|
32
31
|
end
|
33
|
-
end
|
32
|
+
end
|
33
|
+
|
34
|
+
action :close_account do |action|
|
35
|
+
action.href "http://foo.bar.com/#{item.id}/close_account"
|
36
|
+
action.class 'danger'
|
37
|
+
action.class 'irreversible'
|
38
|
+
action.method 'DELETE'
|
39
|
+
action.field :current_password do |field|
|
40
|
+
field.type :password
|
41
|
+
end
|
42
|
+
end
|
34
43
|
end
|
35
44
|
|
36
45
|
def url_for(id)
|
@@ -39,4 +48,4 @@ module Fixtures
|
|
39
48
|
end
|
40
49
|
end
|
41
50
|
end
|
42
|
-
end
|
51
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: oat
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0
|
4
|
+
version: 0.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Ismael Celis
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2014-01-14 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|
@@ -98,7 +98,7 @@ files:
|
|
98
98
|
- spec/fixtures.rb
|
99
99
|
- spec/serializer_spec.rb
|
100
100
|
- spec/spec_helper.rb
|
101
|
-
homepage:
|
101
|
+
homepage: https://github.com/ismasan/oat
|
102
102
|
licenses:
|
103
103
|
- MIT
|
104
104
|
metadata: {}
|