roar 0.8.1 → 0.8.2
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 +8 -0
- data/Gemfile +0 -6
- data/README.textile +46 -67
- data/TODO.markdown +1 -0
- data/lib/roar/representer/feature/http_verbs.rb +2 -2
- data/lib/roar/representer/feature/transport.rb +43 -0
- data/lib/roar/version.rb +1 -1
- data/roar.gemspec +1 -3
- data/test/transport_test.rb +6 -6
- metadata +14 -44
- data/lib/roar/client/entity_proxy.rb +0 -58
- data/lib/roar/client/proxy.rb +0 -14
- data/lib/roar/client/transport.rb +0 -29
data/CHANGES.markdown
ADDED
data/Gemfile
CHANGED
data/README.textile
CHANGED
@@ -1,28 +1,27 @@
|
|
1
1
|
h1. ROAR
|
2
2
|
|
3
|
-
|
3
|
+
_Resource-Oriented Architectures in Ruby._
|
4
4
|
|
5
|
+
"Lets make documents suit our models and not models fit to documents."
|
5
6
|
|
6
|
-
Lets make documents suit our models and not models fit to documents
|
7
7
|
|
8
8
|
h2. Introduction
|
9
9
|
|
10
|
-
|
10
|
+
REST is about representations. Representations are documents. Documents are pushed back and forth between clients and REST services. Roar focuses on providing object-oriented representations.
|
11
11
|
|
12
|
-
REST
|
12
|
+
Most REST gems provide a neat HTTP interface, automatic parsing of documents and a more or less convenient way for rendering representations. Roar is different. The central concept of Roar are *representers* - object-oriented documents suitable for parsing _and_ rendering, extendable at runtime and with hypermedia support. The Representer concept is the answer to the missing REST abstraction layer in most frameworks.
|
13
13
|
|
14
14
|
|
15
15
|
h2. Features
|
16
16
|
|
17
|
-
*
|
18
|
-
*
|
19
|
-
*
|
20
|
-
*
|
21
|
-
*
|
22
|
-
*
|
23
|
-
*
|
24
|
-
|
25
|
-
* Makes *testing distributed REST systems* as easy as possible.
|
17
|
+
* OOP access to documents.
|
18
|
+
* Parsing _and_ rendering of representations in one place.
|
19
|
+
* Declaratively define document syntax and semantics.
|
20
|
+
* Hypermedia support.
|
21
|
+
* ActiveResource-like client support.
|
22
|
+
* Useable in both client _and_ server.
|
23
|
+
* Framework agnostic, runs with sinatra, Rails, webmachine and friends.
|
24
|
+
|
26
25
|
|
27
26
|
h2. Example
|
28
27
|
|
@@ -64,17 +63,16 @@ h2. Representers
|
|
64
63
|
|
65
64
|
To render a representational document, the backend service has to define a representer.
|
66
65
|
|
67
|
-
<pre>
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
end
|
66
|
+
<pre>
|
67
|
+
class Article
|
68
|
+
include Roar::Representer::JSON
|
69
|
+
|
70
|
+
property :title
|
71
|
+
property :id
|
72
|
+
|
73
|
+
link :self do
|
74
|
+
article_url(represented)
|
75
|
+
end
|
78
76
|
end
|
79
77
|
</pre>
|
80
78
|
|
@@ -85,18 +83,10 @@ h3. Rendering Representations in the Service
|
|
85
83
|
|
86
84
|
In order to *render* an actual document, the backend service would have to do a few steps: creating a representer, filling in data, and then serialize it.
|
87
85
|
|
88
|
-
<pre>
|
89
|
-
:
|
90
|
-
:
|
91
|
-
|
92
|
-
</pre>
|
93
|
-
|
94
|
-
Using the @ModelRepresenting@ feature module we can take a shortcut.
|
95
|
-
|
96
|
-
<pre>@beer = Article.find_by_title("Lonestar")
|
97
|
-
|
98
|
-
JSON::Article.from_model(@beer).
|
99
|
-
serialize # => "{\"article\":{\"id\":666, ...
|
86
|
+
<pre>Article.new(
|
87
|
+
title: "Lonestar",
|
88
|
+
id: 666).
|
89
|
+
to_json # => "{\"article\":{\"id\":666, ...
|
100
90
|
</pre>
|
101
91
|
|
102
92
|
Articles itself are useless, so they may be placed into orders. This is the next example.
|
@@ -128,23 +118,22 @@ What if we wanted to check an existing order? We'd @GET http://orders/1@, right?
|
|
128
118
|
|
129
119
|
Since orders may contain a composition of articles, how would the order service define its representer?
|
130
120
|
|
131
|
-
<pre>
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
end
|
143
|
-
|
144
|
-
link :items do
|
145
|
-
items_url
|
146
|
-
end
|
121
|
+
<pre>
|
122
|
+
class Order
|
123
|
+
include Roar::Representer::JSON
|
124
|
+
|
125
|
+
property :id
|
126
|
+
property :client_id
|
127
|
+
|
128
|
+
collection :articles, :as => Article
|
129
|
+
|
130
|
+
link :self do
|
131
|
+
order_url(represented)
|
147
132
|
end
|
133
|
+
|
134
|
+
link :items do
|
135
|
+
items_url
|
136
|
+
end
|
148
137
|
end
|
149
138
|
</pre>
|
150
139
|
|
@@ -159,21 +148,11 @@ If we were to implement an endpoint for creating new orders, we'd allow POST to
|
|
159
148
|
|
160
149
|
<pre>
|
161
150
|
post "/orders" do
|
162
|
-
incoming =
|
151
|
+
incoming = Order.deserialize(request.body.string)
|
163
152
|
puts incoming.to_attributes #=> {:client_id => 815}
|
164
153
|
</pre>
|
165
154
|
|
166
|
-
|
167
|
-
|
168
|
-
<pre>
|
169
|
-
post "/orders" do
|
170
|
-
# ...
|
171
|
-
@order = Order.create(incoming.to_nested_attributes)
|
172
|
-
|
173
|
-
JSON::Order.for_model(@order).serialize
|
174
|
-
</pre>
|
175
|
-
|
176
|
-
Look how the @#to_nested_attributes@ method helps extracting data from the incoming document and, again, @#serialize@ 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.
|
155
|
+
Look how the @#to_attributes@ 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.
|
177
156
|
|
178
157
|
|
179
158
|
h2. Representers in the Client
|
@@ -189,7 +168,7 @@ To create a new order, the frontend needs to POST to the REST backend. Here's ho
|
|
189
168
|
|
190
169
|
|
191
170
|
<pre>
|
192
|
-
order =
|
171
|
+
order = Order.new(:client_id => current_user.id)
|
193
172
|
order.post!("http://orders/")
|
194
173
|
</pre>
|
195
174
|
|
@@ -239,7 +218,7 @@ h3. Using Hypermedia
|
|
239
218
|
Let the frontend add the delicious "Lonestar" beer to our order, now!
|
240
219
|
|
241
220
|
<pre>
|
242
|
-
beer =
|
221
|
+
beer = Article.new(:title => "Lonestar Beer")
|
243
222
|
beer.post(order.links[:items])
|
244
223
|
</pre>
|
245
224
|
|
@@ -280,7 +259,7 @@ What if the ordering API is going a different way? What if we had to place artic
|
|
280
259
|
Here's what could happen in the frontend.
|
281
260
|
|
282
261
|
<pre>
|
283
|
-
beer =
|
262
|
+
beer = Article.new(:title => "Lonestar Beer")
|
284
263
|
order.items << beer
|
285
264
|
order.post!(order.links[:self])
|
286
265
|
</pre>
|
data/TODO.markdown
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
* Add proxies, so nested models can be lazy-loaded.
|
@@ -1,4 +1,4 @@
|
|
1
|
-
require 'roar/
|
1
|
+
require 'roar/representer/feature/transport'
|
2
2
|
|
3
3
|
module Roar
|
4
4
|
# Gives HTTP-power to representers where those can automatically serialize, send, process and deserialize HTTP-requests.
|
@@ -11,7 +11,7 @@ module Roar
|
|
11
11
|
|
12
12
|
|
13
13
|
module ClassMethods
|
14
|
-
include
|
14
|
+
include Transport
|
15
15
|
|
16
16
|
def get(url, format) # TODO: test me!
|
17
17
|
#url = resource_base + variable_path.to_s
|
@@ -0,0 +1,43 @@
|
|
1
|
+
require "net/http"
|
2
|
+
require "uri"
|
3
|
+
|
4
|
+
module Roar
|
5
|
+
module Representer
|
6
|
+
module Feature
|
7
|
+
# Implements the HTTP verbs with Net::HTTP.
|
8
|
+
module Transport
|
9
|
+
# TODO: generically handle return codes/let Restfulie do it.
|
10
|
+
def get_uri(uri, as)
|
11
|
+
do_request(Net::HTTP::Get, uri, as)
|
12
|
+
end
|
13
|
+
|
14
|
+
def post_uri(uri, body, as)
|
15
|
+
do_request(Net::HTTP::Post, uri, as)
|
16
|
+
end
|
17
|
+
|
18
|
+
def put_uri(uri, body, as)
|
19
|
+
do_request(Net::HTTP::Put, uri, as)
|
20
|
+
end
|
21
|
+
|
22
|
+
def patch_uri(uri, body, as)
|
23
|
+
do_request(Net::HTTP::Patch, uri, as)
|
24
|
+
end
|
25
|
+
|
26
|
+
def delete_uri(uri, as)
|
27
|
+
do_request(Net::HTTP::Delete, uri, as)
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
def do_request(what, uri, as, body="")
|
32
|
+
# DISCUSS: can this be made easier?
|
33
|
+
uri = URI.parse(uri)
|
34
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
35
|
+
req = what.new(uri.request_uri)
|
36
|
+
req.content_type = as
|
37
|
+
req.body = body if body
|
38
|
+
http.request(req)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
data/lib/roar/version.rb
CHANGED
data/roar.gemspec
CHANGED
@@ -20,11 +20,9 @@ Gem::Specification.new do |s|
|
|
20
20
|
s.require_paths = ["lib"]
|
21
21
|
|
22
22
|
s.add_runtime_dependency "representable", "~> 0.9.2"
|
23
|
-
s.add_runtime_dependency "
|
24
|
-
s.add_runtime_dependency "hooks", "~> 0.1.4"
|
23
|
+
s.add_runtime_dependency "hooks", "~> 0.1.4"
|
25
24
|
|
26
25
|
s.add_development_dependency "test_xml"
|
27
26
|
s.add_development_dependency "minitest", "~> 1.6.0"
|
28
27
|
s.add_development_dependency "sinatra", "~> 1.2.6"
|
29
|
-
s.add_development_dependency "sinatra-reloader", "~> 0.5.0"
|
30
28
|
end
|
data/test/transport_test.rb
CHANGED
@@ -1,28 +1,28 @@
|
|
1
1
|
require 'test_helper'
|
2
|
-
require 'roar/
|
2
|
+
require 'roar/representer/feature/transport'
|
3
3
|
|
4
4
|
class TransportTest < MiniTest::Spec
|
5
5
|
describe "Transport" do
|
6
6
|
before do
|
7
7
|
@klass = Class.new(Object) do
|
8
|
-
include Roar::
|
8
|
+
include Roar::Representer::Feature::Transport
|
9
9
|
end
|
10
10
|
@o = @klass.new
|
11
11
|
end
|
12
12
|
|
13
|
-
it "#get_uri returns
|
13
|
+
it "#get_uri returns response" do
|
14
14
|
assert_equal "<method>get</method>", @o.get_uri("http://localhost:9999/method", "application/xml").body
|
15
15
|
end
|
16
16
|
|
17
|
-
it "#post_uri returns
|
17
|
+
it "#post_uri returns response" do
|
18
18
|
assert_equal "<method>post</method>", @o.post_uri("http://localhost:9999/method", "booty", "application/xml").body
|
19
19
|
end
|
20
20
|
|
21
|
-
it "#put_uri returns
|
21
|
+
it "#put_uri returns response" do
|
22
22
|
assert_equal "<method>put</method>", @o.put_uri("http://localhost:9999/method", "booty", "application/xml").body
|
23
23
|
end
|
24
24
|
|
25
|
-
it "#delete_uri returns
|
25
|
+
it "#delete_uri returns response" do
|
26
26
|
assert_equal "<method>delete</method>", @o.delete_uri("http://localhost:9999/method", "application/xml").body
|
27
27
|
end
|
28
28
|
|
metadata
CHANGED
@@ -5,8 +5,8 @@ version: !ruby/object:Gem::Version
|
|
5
5
|
segments:
|
6
6
|
- 0
|
7
7
|
- 8
|
8
|
-
-
|
9
|
-
version: 0.8.
|
8
|
+
- 2
|
9
|
+
version: 0.8.2
|
10
10
|
platform: ruby
|
11
11
|
authors:
|
12
12
|
- Nick Sutterer
|
@@ -14,7 +14,7 @@ autorequire:
|
|
14
14
|
bindir: bin
|
15
15
|
cert_chain: []
|
16
16
|
|
17
|
-
date: 2011-11-
|
17
|
+
date: 2011-11-27 00:00:00 +01:00
|
18
18
|
default_executable:
|
19
19
|
dependencies:
|
20
20
|
- !ruby/object:Gem::Dependency
|
@@ -32,25 +32,10 @@ dependencies:
|
|
32
32
|
version: 0.9.2
|
33
33
|
type: :runtime
|
34
34
|
version_requirements: *id001
|
35
|
-
- !ruby/object:Gem::Dependency
|
36
|
-
name: restfulie
|
37
|
-
prerelease: false
|
38
|
-
requirement: &id002 !ruby/object:Gem::Requirement
|
39
|
-
none: false
|
40
|
-
requirements:
|
41
|
-
- - ~>
|
42
|
-
- !ruby/object:Gem::Version
|
43
|
-
segments:
|
44
|
-
- 1
|
45
|
-
- 0
|
46
|
-
- 0
|
47
|
-
version: 1.0.0
|
48
|
-
type: :runtime
|
49
|
-
version_requirements: *id002
|
50
35
|
- !ruby/object:Gem::Dependency
|
51
36
|
name: hooks
|
52
37
|
prerelease: false
|
53
|
-
requirement: &
|
38
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
54
39
|
none: false
|
55
40
|
requirements:
|
56
41
|
- - ~>
|
@@ -61,11 +46,11 @@ dependencies:
|
|
61
46
|
- 4
|
62
47
|
version: 0.1.4
|
63
48
|
type: :runtime
|
64
|
-
version_requirements: *
|
49
|
+
version_requirements: *id002
|
65
50
|
- !ruby/object:Gem::Dependency
|
66
51
|
name: test_xml
|
67
52
|
prerelease: false
|
68
|
-
requirement: &
|
53
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
69
54
|
none: false
|
70
55
|
requirements:
|
71
56
|
- - ">="
|
@@ -74,11 +59,11 @@ dependencies:
|
|
74
59
|
- 0
|
75
60
|
version: "0"
|
76
61
|
type: :development
|
77
|
-
version_requirements: *
|
62
|
+
version_requirements: *id003
|
78
63
|
- !ruby/object:Gem::Dependency
|
79
64
|
name: minitest
|
80
65
|
prerelease: false
|
81
|
-
requirement: &
|
66
|
+
requirement: &id004 !ruby/object:Gem::Requirement
|
82
67
|
none: false
|
83
68
|
requirements:
|
84
69
|
- - ~>
|
@@ -89,11 +74,11 @@ dependencies:
|
|
89
74
|
- 0
|
90
75
|
version: 1.6.0
|
91
76
|
type: :development
|
92
|
-
version_requirements: *
|
77
|
+
version_requirements: *id004
|
93
78
|
- !ruby/object:Gem::Dependency
|
94
79
|
name: sinatra
|
95
80
|
prerelease: false
|
96
|
-
requirement: &
|
81
|
+
requirement: &id005 !ruby/object:Gem::Requirement
|
97
82
|
none: false
|
98
83
|
requirements:
|
99
84
|
- - ~>
|
@@ -104,22 +89,7 @@ dependencies:
|
|
104
89
|
- 6
|
105
90
|
version: 1.2.6
|
106
91
|
type: :development
|
107
|
-
version_requirements: *
|
108
|
-
- !ruby/object:Gem::Dependency
|
109
|
-
name: sinatra-reloader
|
110
|
-
prerelease: false
|
111
|
-
requirement: &id007 !ruby/object:Gem::Requirement
|
112
|
-
none: false
|
113
|
-
requirements:
|
114
|
-
- - ~>
|
115
|
-
- !ruby/object:Gem::Version
|
116
|
-
segments:
|
117
|
-
- 0
|
118
|
-
- 5
|
119
|
-
- 0
|
120
|
-
version: 0.5.0
|
121
|
-
type: :development
|
122
|
-
version_requirements: *id007
|
92
|
+
version_requirements: *id005
|
123
93
|
description: Streamlines the development of RESTful, resource-oriented architectures in Ruby.
|
124
94
|
email:
|
125
95
|
- apotonick@gmail.com
|
@@ -131,13 +101,12 @@ extra_rdoc_files: []
|
|
131
101
|
|
132
102
|
files:
|
133
103
|
- .gitignore
|
104
|
+
- CHANGES.markdown
|
134
105
|
- Gemfile
|
135
106
|
- README.textile
|
136
107
|
- Rakefile
|
108
|
+
- TODO.markdown
|
137
109
|
- lib/roar.rb
|
138
|
-
- lib/roar/client/entity_proxy.rb
|
139
|
-
- lib/roar/client/proxy.rb
|
140
|
-
- lib/roar/client/transport.rb
|
141
110
|
- lib/roar/rails.rb
|
142
111
|
- lib/roar/rails/controller_methods.rb
|
143
112
|
- lib/roar/rails/representer_methods.rb
|
@@ -146,6 +115,7 @@ files:
|
|
146
115
|
- lib/roar/representer/feature/http_verbs.rb
|
147
116
|
- lib/roar/representer/feature/hypermedia.rb
|
148
117
|
- lib/roar/representer/feature/model_representing.rb
|
118
|
+
- lib/roar/representer/feature/transport.rb
|
149
119
|
- lib/roar/representer/json.rb
|
150
120
|
- lib/roar/representer/xml.rb
|
151
121
|
- lib/roar/version.rb
|
@@ -1,58 +0,0 @@
|
|
1
|
-
require "roar/model"
|
2
|
-
require "roar/representer/xml"
|
3
|
-
require "active_support/core_ext/module/attr_internal"
|
4
|
-
require "roar/client/proxy"
|
5
|
-
|
6
|
-
module Roar
|
7
|
-
module Client
|
8
|
-
# Wraps associated objects, they can be retrieved with #finalize!
|
9
|
-
# Used e.g. in Representer::Xml.has_proxied.
|
10
|
-
class EntityProxy
|
11
|
-
# FIXME: where to move me? i do Representable and i use Transport. however, i'm only for clients.
|
12
|
-
include Model
|
13
|
-
include Representer::Xml # FIXME: why does EntityProxy know about xml? get this from Representable or so.
|
14
|
-
extend Proxy
|
15
|
-
|
16
|
-
attr_internal :proxied_resource
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
class << self
|
21
|
-
attr_accessor :options
|
22
|
-
|
23
|
-
def class_for(options)
|
24
|
-
Class.new(self).tap { |k| k.options = options }
|
25
|
-
end
|
26
|
-
|
27
|
-
def model_name
|
28
|
-
options[:class].model_name # proxy!
|
29
|
-
end
|
30
|
-
|
31
|
-
def from_attributes(attrs) # FIXME: move to Representable or so.
|
32
|
-
new(attrs)
|
33
|
-
end
|
34
|
-
end
|
35
|
-
|
36
|
-
# Get the actual proxied resource.
|
37
|
-
def finalize!(*)
|
38
|
-
# TODO: move to class.
|
39
|
-
# DISCUSS: how to compute uri? what if NO uri passed? exception?
|
40
|
-
self.proxied_resource = self.class.get_model(original_attributes["uri"], self.class.options[:class])
|
41
|
-
end
|
42
|
-
|
43
|
-
def attributes
|
44
|
-
# DISCUSS: delegate all unknown methods to the proxied object?
|
45
|
-
proxied_resource.attributes # proxy!
|
46
|
-
end
|
47
|
-
|
48
|
-
def attributes_for_xml(*options)
|
49
|
-
original_attributes
|
50
|
-
end
|
51
|
-
|
52
|
-
private
|
53
|
-
def original_attributes
|
54
|
-
@attributes
|
55
|
-
end
|
56
|
-
end
|
57
|
-
end
|
58
|
-
end
|
data/lib/roar/client/proxy.rb
DELETED
@@ -1,14 +0,0 @@
|
|
1
|
-
module Roar
|
2
|
-
module Client
|
3
|
-
|
4
|
-
module Proxy
|
5
|
-
include Transport
|
6
|
-
|
7
|
-
def get_model(uri, klass) # DISCUSS: not directly used in models. USED in EntityProxy.
|
8
|
-
body = get_uri(uri).body
|
9
|
-
klass.from_xml(body) # DISCUSS: knows about DE-serialization and representation-type!
|
10
|
-
end
|
11
|
-
end
|
12
|
-
end
|
13
|
-
|
14
|
-
end
|
@@ -1,29 +0,0 @@
|
|
1
|
-
require "restfulie"
|
2
|
-
|
3
|
-
module Roar
|
4
|
-
module Client
|
5
|
-
# Implements the HTTP verbs by abstracting Restfulie.
|
6
|
-
module Transport
|
7
|
-
# TODO: generically handle return codes/let Restfulie do it.
|
8
|
-
def get_uri(uri, as)
|
9
|
-
Restfulie.at(uri).accepts(as).get # TODO: debugging/logging here.
|
10
|
-
end
|
11
|
-
|
12
|
-
def post_uri(uri, body, as)
|
13
|
-
Restfulie.at(uri).as(as).post(body)
|
14
|
-
end
|
15
|
-
|
16
|
-
def put_uri(uri, body, as)
|
17
|
-
Restfulie.at(uri).as(as).put(body)
|
18
|
-
end
|
19
|
-
|
20
|
-
def patch_uri(uri, body, as)
|
21
|
-
Restfulie.at(uri).as(as).patch(body)
|
22
|
-
end
|
23
|
-
|
24
|
-
def delete_uri(uri, as)
|
25
|
-
Restfulie.at(uri).accepts(as).delete # TODO: debugging/logging here.
|
26
|
-
end
|
27
|
-
end
|
28
|
-
end
|
29
|
-
end
|