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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 265248fecbfa3adf9fae15b6c9124a452fe8f285
4
- data.tar.gz: aac3cc104b933d261d8f8ca858e038e5e7cef835
3
+ metadata.gz: 8bb1271b6a91bda9b273a02d29e48f999e240a35
4
+ data.tar.gz: 6ac4f8595d78f974ed688182ef021bc688958e65
5
5
  SHA512:
6
- metadata.gz: 29f80fbffde75401c81cf69de1f59a5f1091d173abddf404c902ce90ada9c8060b950cfd56a3cc713daf5e961cac417376eb8e68d8ebeae169bfa5bfcbb9a2e9
7
- data.tar.gz: fa5ecb844b738eed0344bc513ae083f2928e40eef0099c3e8bd282d0b466e1727cd23598577eb080977a06050e3d6c58764b387d2b5be0565683fea9d51a9985
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 schemae but it's up to your code/framework how to create those links.
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
@@ -26,4 +26,4 @@ module Oat
26
26
 
27
27
  end
28
28
  end
29
- end
29
+ end
@@ -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
- link name, href: ent[:id]
31
- (@entities[name.to_s.pluralize.to_sym] ||= []) << ent
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
- data[:links][link_name] << ent[:id]
41
- (@entities[link_name] ||= []) << ent
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).values.first.first
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
@@ -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
- data[:entities] << serializer_from_block_or_class(obj, serializer_class, &block)
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
@@ -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
@@ -1,3 +1,3 @@
1
1
  module Oat
2
- VERSION = "0.0.1"
2
+ VERSION = "0.1.0"
3
3
  end
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($/)
@@ -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
@@ -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 if item.manager
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.1
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: 2013-11-18 00:00:00.000000000 Z
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: {}