roar 0.11.4 → 0.11.5
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.
- data/CHANGES.markdown +5 -1
- data/Gemfile +1 -1
- data/README.textile +22 -21
- data/lib/roar/representer.rb +1 -1
- data/lib/roar/representer/feature/client.rb +3 -3
- data/lib/roar/representer/feature/coercion.rb +1 -1
- data/lib/roar/representer/feature/http_verbs.rb +8 -9
- data/lib/roar/representer/feature/hypermedia.rb +83 -63
- data/lib/roar/representer/json.rb +12 -12
- data/lib/roar/representer/json/hal.rb +98 -25
- data/lib/roar/representer/transport/faraday.rb +5 -11
- data/lib/roar/representer/transport/net_http.rb +5 -5
- data/lib/roar/representer/xml.rb +11 -11
- data/lib/roar/version.rb +1 -1
- data/roar.gemspec +2 -2
- data/test/client_test.rb +2 -2
- data/test/coercion_feature_test.rb +2 -2
- data/test/dummy/app/controllers/albums_controller.rb +6 -6
- data/test/dummy/app/models/album.rb +1 -1
- data/test/dummy/app/representers/representer/xml/album.rb +3 -3
- data/test/dummy/app/representers/representer/xml/song.rb +1 -1
- data/test/dummy/config/boot.rb +1 -1
- data/test/fake_server.rb +14 -15
- data/test/faraday_http_transport_test.rb +25 -10
- data/test/hal_json_test.rb +73 -31
- data/test/http_verbs_feature_test.rb +12 -12
- data/test/hypermedia_feature_test.rb +38 -145
- data/test/hypermedia_test.rb +100 -0
- data/test/integration_test.rb +22 -22
- data/test/json_representer_test.rb +31 -31
- data/test/net_http_transport_test.rb +24 -10
- data/test/order_representers.rb +11 -11
- data/test/rails/controller_methods_test.rb +25 -25
- data/test/rails/rails_representer_methods_test.rb +3 -3
- data/test/representer_test.rb +6 -6
- data/test/test_helper.rb +33 -4
- data/test/xml_representer_test.rb +47 -47
- metadata +3 -3
- data/lib/roar/rails.rb +0 -21
data/CHANGES.markdown
CHANGED
@@ -1,3 +1,7 @@
|
|
1
|
+
## 0.11.5
|
2
|
+
|
3
|
+
* Introducing HAL::links method to map arrays of link objects in the HAL format. This completes the HAL/JSON specification.
|
4
|
+
|
1
5
|
## 0.11.4
|
2
6
|
|
3
7
|
* Links can now return a hash of attributes as `link :self do {:href => fruit_path(self), :title => "Yummy stuff"} end`.
|
@@ -21,7 +25,7 @@
|
|
21
25
|
* You can include nil values now in your representations since #property respects :represent_nil => true.
|
22
26
|
|
23
27
|
* The `:except` option got deprecated in favor of `:exclude`.
|
24
|
-
* Hyperlinks can now have arbitrary attributes. To render, just provide `#link` with the options
|
28
|
+
* Hyperlinks can now have arbitrary attributes. To render, just provide `#link` with the options
|
25
29
|
<code>link :self, :title => "Mee!", "data-remote" => true</code>
|
26
30
|
When parsing, the options are avaible via `OpenStruct` compliant readers.
|
27
31
|
<code>link = Hyperlink.from_json({\"rel\":\"self\",\"data-url\":\"http://self\"} )
|
data/Gemfile
CHANGED
data/README.textile
CHANGED
@@ -6,10 +6,10 @@ h2. Introduction
|
|
6
6
|
|
7
7
|
Roar is a framework for parsing and rendering REST documents. Nothing more. With Roar, REST documents - also known as representations - are defined using a new concept called representers. Both syntax and semantics are declared in Ruby modules that can be mixed into your domain models, following clean OOP patterns.
|
8
8
|
|
9
|
-
Roar comes with built-in JSON, JSON::HAL and XML support. It exposes a highly modular architecture and makes it very simple to add new media types and functionality where needed. Additional features include client HTTP support, coercion, client-side caching, awesome hypermedia support and more. Representers
|
9
|
+
Roar comes with built-in JSON, JSON::HAL and XML support. It exposes a highly modular architecture and makes it very simple to add new media types and functionality where needed. Additional features include client HTTP support, coercion, client-side caching, awesome hypermedia support and more. Representers fit pretty well in DCI environments, too.
|
10
|
+
|
11
|
+
Roar is completely framework-agnostic and loves being used in web kits like Rails, Webmachine, Sinatra, Padrino, etc. Actually, Roar makes it fun designing real, hypermedia-driven, and resource-oriented systems that will even make Steve sleep happily at night so he finally gets some REST!
|
10
12
|
|
11
|
-
Roar is completely framework-agnostic and loves being used in web kits like Rails, Webmachine, Sinatra, Padrino, etc. Actually, Roar makes it fun designing real, hypermedia-driven, and resource-oriented systems that will even make Steve sleep happily at night so he finally gets some REST!
|
12
|
-
|
13
13
|
h2. Example
|
14
14
|
|
15
15
|
Say your webshop consists of two completely separated apps. The REST backend, a Sinatra app, serves articles and processes orders. The frontend, being browsed by your clients, is a rich Rails application. It queries the services for articles, renders them nicely and reads or writes orders with REST calls. That being said, the frontend turns out to be a pure REST client.
|
@@ -34,7 +34,7 @@ pre. { "article": {
|
|
34
34
|
"title": "Lonestar Beer",
|
35
35
|
"id": 4711,
|
36
36
|
"links":[
|
37
|
-
{ "rel": "self",
|
37
|
+
{ "rel": "self",
|
38
38
|
"href": "http://articles/lonestarbeer"}
|
39
39
|
]}
|
40
40
|
}
|
@@ -66,13 +66,13 @@ require 'roar/representer/feature/hypermedia'
|
|
66
66
|
module ArticleRepresenter
|
67
67
|
include Roar::Representer::JSON
|
68
68
|
include Roar::Representer::Feature::Hypermedia
|
69
|
-
|
69
|
+
|
70
70
|
property :title
|
71
71
|
property :id
|
72
|
-
|
72
|
+
|
73
73
|
link :self do
|
74
74
|
article_url(self)
|
75
|
-
end
|
75
|
+
end
|
76
76
|
end
|
77
77
|
</pre>
|
78
78
|
|
@@ -103,14 +103,14 @@ What if we wanted to check an existing order? We'd @GET http://orders/1@, right?
|
|
103
103
|
{"title": "Lonestar Beer",
|
104
104
|
"id": 666,
|
105
105
|
"links":[
|
106
|
-
{ "rel": "self",
|
106
|
+
{ "rel": "self",
|
107
107
|
"href": "http://articles/lonestarbeer"}
|
108
108
|
]}
|
109
109
|
],
|
110
110
|
"links":[
|
111
|
-
{ "rel": "self",
|
111
|
+
{ "rel": "self",
|
112
112
|
"href": "http://orders/1"},
|
113
|
-
{ "rel": "items",
|
113
|
+
{ "rel": "items",
|
114
114
|
"href": "http://orders/1/items"}
|
115
115
|
]}
|
116
116
|
}
|
@@ -133,20 +133,20 @@ module OrderRepresenter
|
|
133
133
|
|
134
134
|
property :id
|
135
135
|
property :client_id
|
136
|
-
|
136
|
+
|
137
137
|
collection :articles, :class => Article
|
138
|
-
|
138
|
+
|
139
139
|
link :self do
|
140
140
|
order_url(represented)
|
141
141
|
end
|
142
|
-
|
142
|
+
|
143
143
|
link :items do
|
144
144
|
items_url
|
145
|
-
end
|
145
|
+
end
|
146
146
|
end
|
147
147
|
</pre>
|
148
148
|
|
149
|
-
Representers don't have to be in modules, but can be
|
149
|
+
Representers don't have to be in modules, but can be
|
150
150
|
|
151
151
|
The declarative @#collection@ method lets us define compositions of representers.
|
152
152
|
|
@@ -164,7 +164,7 @@ If we were to implement an endpoint for creating new orders, we'd allow POST to
|
|
164
164
|
order.to_json
|
165
165
|
end
|
166
166
|
</pre>
|
167
|
-
|
167
|
+
|
168
168
|
Look how the @#from_json@ method helps extracting data from the incoming document and, again, @#to_json@ returns the freshly created order's representation. Roar's representers are truely working in both directions, rendering and parsing and thus prevent you from redundant knowledge sharing.
|
169
169
|
|
170
170
|
|
@@ -212,9 +212,9 @@ First, check the JSON document we get back from the POST.
|
|
212
212
|
"client_id": 1337,
|
213
213
|
"articles": [],
|
214
214
|
"links":[
|
215
|
-
{ "rel": "self",
|
215
|
+
{ "rel": "self",
|
216
216
|
"href": "http://orders/42"},
|
217
|
-
{ "rel": "items",
|
217
|
+
{ "rel": "items",
|
218
218
|
"href": "http://orders/42/items"}
|
219
219
|
]}
|
220
220
|
}
|
@@ -258,7 +258,7 @@ Again, we use hypermedia to retrieve the order's URL. And now, the added article
|
|
258
258
|
[*Note:* If this looks clumsy - It's just the raw API for representers. You might be interested in the upcoming DSL that simplifys frequent workflows as updating a representer.]
|
259
259
|
|
260
260
|
<pre>
|
261
|
-
order.to_attributes #=> {:id => 42, :client_id => 1337,
|
261
|
+
order.to_attributes #=> {:id => 42, :client_id => 1337,
|
262
262
|
:articles => [{:title => "Lonestar Beer", :id => 666}]}
|
263
263
|
</pre>
|
264
264
|
|
@@ -266,7 +266,7 @@ This is cool, we used REST representers and hypermedia to create an order and fi
|
|
266
266
|
|
267
267
|
|
268
268
|
h3. Using Accessors
|
269
|
-
|
269
|
+
|
270
270
|
What if the ordering API is going a different way? What if we had to place articles into the order document ourselves, and then PUT this representation to @http://orders/42@? No problem with representers!
|
271
271
|
|
272
272
|
Here's what could happen in the frontend.
|
@@ -283,7 +283,8 @@ h2. More Features
|
|
283
283
|
|
284
284
|
Be sure to check out the bundled features.
|
285
285
|
|
286
|
-
# *Coercion* transforms values to typed objects when parsing a document. Uses virtus.
|
286
|
+
# *Coercion* transforms values to typed objects when parsing a document. Uses virtus.
|
287
|
+
# *Faraday* support, if you want to use it install the Faraday gem and require 'roar/representer/transport/faraday' and configure 'Roar::Representer::Feature::HttpVerbs.transport_engine = Roar::Representer::Transport::Faraday'
|
287
288
|
|
288
289
|
h2. What is REST about?
|
289
290
|
|
data/lib/roar/representer.rb
CHANGED
@@ -6,19 +6,19 @@ module Roar
|
|
6
6
|
module Feature
|
7
7
|
module Client
|
8
8
|
include HttpVerbs
|
9
|
-
|
9
|
+
|
10
10
|
def self.extended(base)
|
11
11
|
base.instance_eval do
|
12
12
|
representable_attrs.each do |attr|
|
13
13
|
next unless attr.instance_of? Representable::Definition # ignore hyperlinks etc for now.
|
14
14
|
name = attr.name
|
15
|
-
|
15
|
+
|
16
16
|
# TODO: could anyone please make this better?
|
17
17
|
instance_eval %Q{
|
18
18
|
def #{name}=(v)
|
19
19
|
@#{name} = v
|
20
20
|
end
|
21
|
-
|
21
|
+
|
22
22
|
def #{name}
|
23
23
|
@#{name}
|
24
24
|
end
|
@@ -1,5 +1,4 @@
|
|
1
1
|
require 'roar/representer/transport/net_http'
|
2
|
-
require 'roar/representer/transport/faraday' # Do not require here
|
3
2
|
|
4
3
|
module Roar
|
5
4
|
# Gives HTTP-power to representers. They can serialize, send, process and deserialize HTTP-requests.
|
@@ -9,13 +8,13 @@ module Roar
|
|
9
8
|
|
10
9
|
class << self
|
11
10
|
attr_accessor :transport_engine
|
12
|
-
|
11
|
+
|
13
12
|
def included(base)
|
14
13
|
base.extend ClassMethods
|
15
14
|
end
|
16
15
|
end
|
17
16
|
self.transport_engine = ::Roar::Representer::Transport::NetHTTP
|
18
|
-
|
17
|
+
|
19
18
|
|
20
19
|
module ClassMethods
|
21
20
|
# GETs +url+ with +format+ and returns deserialized represented object.
|
@@ -23,26 +22,26 @@ module Roar
|
|
23
22
|
new.get(*args)
|
24
23
|
end
|
25
24
|
end
|
26
|
-
|
27
|
-
|
25
|
+
|
26
|
+
|
28
27
|
attr_writer :transport_engine
|
29
28
|
def transport_engine
|
30
29
|
@transport_engine || HttpVerbs.transport_engine
|
31
30
|
end
|
32
|
-
|
31
|
+
|
33
32
|
# Serializes the object, POSTs it to +url+ with +format+, deserializes the returned document
|
34
33
|
# and updates properties accordingly.
|
35
34
|
def post(url, format)
|
36
35
|
response = http.post_uri(url, serialize, format)
|
37
36
|
handle_response(response)
|
38
37
|
end
|
39
|
-
|
38
|
+
|
40
39
|
# GETs +url+ with +format+, deserializes the returned document and updates properties accordingly.
|
41
40
|
def get(url, format)
|
42
41
|
response = http.get_uri(url, format)
|
43
42
|
handle_response(response)
|
44
43
|
end
|
45
|
-
|
44
|
+
|
46
45
|
# Serializes the object, PUTs it to +url+ with +format+, deserializes the returned document
|
47
46
|
# and updates properties accordingly.
|
48
47
|
def put(url, format)
|
@@ -50,7 +49,7 @@ module Roar
|
|
50
49
|
handle_response(response)
|
51
50
|
self
|
52
51
|
end
|
53
|
-
|
52
|
+
|
54
53
|
def patch(url, format)
|
55
54
|
response = http.patch_uri(url, serialize, format)
|
56
55
|
handle_response(response)
|
@@ -33,65 +33,87 @@ module Roar
|
|
33
33
|
# "http://orders/#{opts[:id]}"
|
34
34
|
# end
|
35
35
|
#
|
36
|
-
# model.to_json(:id => 1)
|
36
|
+
# model.to_json(:id => 1)
|
37
37
|
module Hypermedia
|
38
38
|
def self.included(base)
|
39
39
|
base.extend ClassMethods
|
40
40
|
end
|
41
|
-
|
41
|
+
|
42
|
+
# TODO: the whole declarative setup in representable should happen on instance level so we don't need to share methods between class and instance.
|
43
|
+
module LinksDefinitionMethods
|
44
|
+
private
|
45
|
+
def create_links_definition
|
46
|
+
representable_attrs << links = LinksDefinition.new(*links_definition_options)
|
47
|
+
links
|
48
|
+
end
|
49
|
+
|
50
|
+
def links_definition
|
51
|
+
representable_attrs.find { |d| d.is_a?(LinksDefinition) } or create_links_definition
|
52
|
+
end
|
53
|
+
end
|
54
|
+
include LinksDefinitionMethods
|
55
|
+
def links_definition_options # FIXME: make this unnecessary.
|
56
|
+
self.class.links_definition_options
|
57
|
+
end
|
58
|
+
|
42
59
|
def before_serialize(options={})
|
43
60
|
prepare_links!(options) unless options[:links] == false # DISCUSS: doesn't work when links are already setup (e.g. from #deserialize).
|
44
61
|
super # Representer::Base
|
45
62
|
end
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
end
|
50
|
-
|
63
|
+
|
64
|
+
attr_writer :links
|
65
|
+
|
51
66
|
def links
|
52
67
|
@links ||= LinkCollection.new
|
53
68
|
end
|
54
|
-
|
55
|
-
|
69
|
+
|
70
|
+
def links_array
|
71
|
+
links.values # FIXME: move to LinkCollection#to_a.
|
72
|
+
end
|
73
|
+
|
74
|
+
def links_array=(ary)
|
75
|
+
# FIXME: move to LinkCollection
|
76
|
+
self.links= LinkCollection.new
|
77
|
+
ary.each do |lnk|
|
78
|
+
self.links[lnk.rel.to_s] = lnk
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
private
|
56
83
|
# Setup hypermedia links by invoking their blocks. Usually called by #serialize.
|
57
84
|
def prepare_links!(*args)
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
href
|
63
|
-
options.merge! href.is_a?(Hash) ? href : {:href => href}
|
64
|
-
|
65
|
-
links.update_link(Feature::Hypermedia::Hyperlink.new(options))
|
85
|
+
links_definition.each do |config| # config is [{..}, block]
|
86
|
+
options, block = config.first, config.last
|
87
|
+
href = run_link_block(block, *args) or next
|
88
|
+
|
89
|
+
links.add(prepare_link_for(href, options))
|
66
90
|
end
|
67
91
|
end
|
68
|
-
|
92
|
+
|
93
|
+
def prepare_link_for(href, options)
|
94
|
+
options.merge! href.is_a?(Hash) ? href : {:href => href}
|
95
|
+
Hyperlink.new(options)
|
96
|
+
end
|
97
|
+
|
69
98
|
def run_link_block(block, *args)
|
70
99
|
instance_exec(*args, &block)
|
71
100
|
end
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
class LinkCollection < Array
|
101
|
+
|
102
|
+
|
103
|
+
class LinkCollection < Hash
|
104
|
+
# DISCUSS: make Link#rel return string always.
|
79
105
|
def [](rel)
|
80
|
-
|
106
|
+
self.fetch(rel.to_s, nil)
|
81
107
|
end
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
def update_link(link)
|
86
|
-
if i = find_index { |l| l.rel.to_s == link.rel.to_s }
|
87
|
-
return self[i] = link
|
88
|
-
end
|
89
|
-
self << link
|
108
|
+
|
109
|
+
def add(link) # FIXME: use Hash API.
|
110
|
+
self[link.rel.to_s] = link
|
90
111
|
end
|
91
112
|
end
|
92
|
-
|
93
|
-
|
113
|
+
|
114
|
+
|
94
115
|
module ClassMethods
|
116
|
+
include LinksDefinitionMethods
|
95
117
|
# Declares a hypermedia link in the document.
|
96
118
|
#
|
97
119
|
# Example:
|
@@ -103,48 +125,46 @@ module Roar
|
|
103
125
|
# The block is executed in instance context, so you may call properties or other accessors.
|
104
126
|
# Note that you're free to put decider logic into #link blocks, too.
|
105
127
|
def link(options, &block)
|
106
|
-
links = find_links_definition || create_links
|
107
|
-
|
108
128
|
options = {:rel => options} if options.is_a?(Symbol)
|
109
|
-
|
110
|
-
end
|
111
|
-
|
112
|
-
def find_links_definition
|
113
|
-
representable_attrs.find { |d| d.is_a?(LinksDefinition) }
|
114
|
-
end
|
115
|
-
|
116
|
-
private
|
117
|
-
def create_links
|
118
|
-
LinksDefinition.new(*links_definition_options).tap do |links|
|
119
|
-
representable_attrs << links
|
120
|
-
end
|
129
|
+
links_definition << [options, block]
|
121
130
|
end
|
122
131
|
end
|
123
|
-
|
124
|
-
|
132
|
+
|
133
|
+
|
125
134
|
class LinksDefinition < Representable::Definition
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
def
|
130
|
-
|
135
|
+
include Enumerable
|
136
|
+
|
137
|
+
attr_accessor :rel2block
|
138
|
+
def initialize(*)
|
139
|
+
super
|
140
|
+
@rel2block = []
|
141
|
+
end
|
142
|
+
|
143
|
+
def <<(args)
|
144
|
+
rel2block << args
|
145
|
+
end
|
146
|
+
|
147
|
+
def each(*args, &block)
|
148
|
+
rel2block.each(*args, &block)
|
131
149
|
end
|
132
|
-
|
150
|
+
|
151
|
+
# DISCUSS: where do we need this?
|
133
152
|
def clone
|
134
153
|
super.tap { |d| d.rel2block = rel2block.clone }
|
135
154
|
end
|
136
155
|
end
|
137
|
-
|
138
|
-
|
156
|
+
|
157
|
+
|
139
158
|
require "ostruct"
|
140
159
|
# An abstract hypermedia link with arbitrary attributes.
|
141
160
|
class Hyperlink < OpenStruct
|
142
161
|
include Enumerable
|
143
|
-
|
162
|
+
|
144
163
|
def each(*args, &block)
|
145
164
|
marshal_dump.each(*args, &block)
|
146
165
|
end
|
147
|
-
|
166
|
+
|
167
|
+
# FIXME: do we need this method any longer?
|
148
168
|
def replace(hash)
|
149
169
|
# #marshal_load requires symbol keys: http://apidock.com/ruby/v1_9_3_125/OpenStruct/marshal_load
|
150
170
|
marshal_load(hash.inject({}) { |h, (k,v)| h[k.to_sym] = v; h })
|
@@ -153,4 +173,4 @@ module Roar
|
|
153
173
|
end
|
154
174
|
end
|
155
175
|
end
|
156
|
-
end
|
176
|
+
end
|