oat 0.0.1 → 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 +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 [](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: {}
|