roar 0.0.1.alpha1 → 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (77) hide show
  1. data/.gitignore +1 -0
  2. data/Gemfile +8 -0
  3. data/README.textile +297 -0
  4. data/Rakefile +16 -0
  5. data/lib/roar/client/entity_proxy.rb +58 -0
  6. data/lib/roar/client/proxy.rb +14 -0
  7. data/lib/roar/client/transport.rb +29 -0
  8. data/lib/roar/model.rb +36 -0
  9. data/lib/roar/model/representable.rb +31 -0
  10. data/lib/roar/rails.rb +21 -0
  11. data/lib/roar/rails/controller_methods.rb +71 -0
  12. data/lib/roar/rails/representer_methods.rb +52 -0
  13. data/lib/roar/rails/test_case.rb +43 -0
  14. data/lib/roar/representer.rb +72 -0
  15. data/lib/roar/representer/feature/http_verbs.rb +63 -0
  16. data/lib/roar/representer/feature/hypermedia.rb +43 -0
  17. data/lib/roar/representer/feature/model_representing.rb +88 -0
  18. data/lib/roar/representer/json.rb +32 -0
  19. data/lib/roar/representer/xml.rb +43 -0
  20. data/lib/roar/version.rb +1 -1
  21. data/roar.gemspec +10 -1
  22. data/test/Gemfile +6 -0
  23. data/test/dummy/Rakefile +7 -0
  24. data/test/dummy/app/controllers/albums_controller.rb +27 -0
  25. data/test/dummy/app/controllers/application_controller.rb +4 -0
  26. data/test/dummy/app/helpers/application_helper.rb +2 -0
  27. data/test/dummy/app/models/album.rb +6 -0
  28. data/test/dummy/app/models/song.rb +2 -0
  29. data/test/dummy/app/representers/representer/xml/album.rb +19 -0
  30. data/test/dummy/app/representers/representer/xml/song.rb +9 -0
  31. data/test/dummy/app/views/layouts/application.html.erb +14 -0
  32. data/test/dummy/app/views/musician/featured.html.erb +1 -0
  33. data/test/dummy/app/views/musician/featured_with_block.html.erb +4 -0
  34. data/test/dummy/app/views/musician/hamlet.html.haml +1 -0
  35. data/test/dummy/config.ru +4 -0
  36. data/test/dummy/config/application.rb +20 -0
  37. data/test/dummy/config/boot.rb +10 -0
  38. data/test/dummy/config/database.yml +22 -0
  39. data/test/dummy/config/environment.rb +5 -0
  40. data/test/dummy/config/environments/development.rb +16 -0
  41. data/test/dummy/config/environments/production.rb +46 -0
  42. data/test/dummy/config/environments/test.rb +32 -0
  43. data/test/dummy/config/locales/en.yml +5 -0
  44. data/test/dummy/config/routes.rb +7 -0
  45. data/test/dummy/db/development.sqlite3 +0 -0
  46. data/test/dummy/db/migrate/20110514114753_create_albums.rb +14 -0
  47. data/test/dummy/db/migrate/20110514121228_create_songs.rb +14 -0
  48. data/test/dummy/db/schema.rb +29 -0
  49. data/test/dummy/db/test.sqlite3 +0 -0
  50. data/test/dummy/module - (2011-05-14 15:26:19) +5 -0
  51. data/test/dummy/public/404.html +26 -0
  52. data/test/dummy/public/422.html +26 -0
  53. data/test/dummy/public/500.html +26 -0
  54. data/test/dummy/public/favicon.ico +0 -0
  55. data/test/dummy/script/rails +6 -0
  56. data/test/dummy/tmp/app/cells/blog/post/latest.html.erb +7 -0
  57. data/test/dummy/tmp/app/cells/blog/post_cell.rb +7 -0
  58. data/test/fake_server.rb +80 -0
  59. data/test/http_verbs_test.rb +46 -0
  60. data/test/hypermedia_test.rb +35 -0
  61. data/test/integration_test.rb +122 -0
  62. data/test/json_representer_test.rb +101 -0
  63. data/test/model_representing_test.rb +121 -0
  64. data/test/model_test.rb +50 -0
  65. data/test/order_representers.rb +34 -0
  66. data/test/proxy_test.rb +89 -0
  67. data/test/rails/controller_methods_test.rb +147 -0
  68. data/test/rails/rails_representer_methods_test.rb +32 -0
  69. data/test/representable_test.rb +49 -0
  70. data/test/representer_test.rb +25 -0
  71. data/test/ruby_representation_test.rb +144 -0
  72. data/test/test_helper.rb +45 -0
  73. data/test/test_helper_test.rb +59 -0
  74. data/test/transport_test.rb +34 -0
  75. data/test/xml_hypermedia_test.rb +47 -0
  76. data/test/xml_representer_test.rb +238 -0
  77. metadata +181 -13
data/.gitignore CHANGED
@@ -1,3 +1,4 @@
1
1
  pkg/*
2
2
  *.gem
3
3
  .bundle
4
+ Gemfile.lock
data/Gemfile CHANGED
@@ -2,3 +2,11 @@ source "http://rubygems.org"
2
2
 
3
3
  # Specify your gem's dependencies in roar.gemspec
4
4
  gemspec
5
+
6
+ #gem "representable", :path => "/home/nick/projects/representable"
7
+ #gem "test_xml", :path => "/home/nick/projects/test_xml" #"~> 0.1.0"
8
+
9
+ group :test do
10
+ gem "rails", "~> 3.0.0"
11
+ gem "sqlite3"
12
+ end
@@ -0,0 +1,297 @@
1
+ h1. ROAR
2
+
3
+ _Streamlines the development of RESTful, Resource-Oriented Architectures in Ruby._
4
+
5
+
6
+ h2. Introduction
7
+
8
+ Roar is a framework for developing distributed applications while using hypermedia as key for application workflow.
9
+
10
+ REST is an architectural style for distributed systems. However, many implementations forget about the _distributed_ part of REST and simply map CRUD operations to HTTP verbs in a monolithic application.
11
+
12
+
13
+ h2. Features
14
+
15
+ * Roar worries about incoming and outgoing *representation documents*.
16
+ * Representers let you declaratively *define your representations*.
17
+ * Representations now are *OOP instances* with accessors, methods and behaviour.
18
+ * Both *rendering and parsing* representations is handled by representers which keeps knowledge in one place.
19
+ * Features as *hypermedia* support and *HTTP* methods can be mixed in dynamically.
20
+ * Representers are packagable in gems for *distribution to services and clients*.
21
+ * *Framework agnostic*, runs with Sinatra, Rails & Co.
22
+ * Extra support for *Rails*.
23
+ * Makes *testing distributed REST systems* as easy as possible.
24
+
25
+ h2. Example
26
+
27
+ 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.
28
+
29
+
30
+ h2. Representations
31
+
32
+ Representations are the pivotal elements of REST. Work in a REST system means working with representations, which can be put down to parsing or extracting representations and rendering the like.
33
+
34
+ Roar makes it easy to render and parse representations of resources after defining the formats.
35
+
36
+
37
+ h3. Creating Representations
38
+
39
+ Why not GET a particular article, what about a good beer?
40
+
41
+ @GET http://articles/lonestarbeer@
42
+
43
+ It's cheap and it's good. The response of a GET is a representation of the requested resource. A *representation* is always a *document*. In this example, it's a bit of JSON.
44
+
45
+ pre. { "article": {
46
+ "title": "Lonestar Beer",
47
+ "id": 4711,
48
+ "links":[
49
+ { "rel": "self",
50
+ "href": "http://articles/lonestarbeer"}
51
+ ]}
52
+ }
53
+
54
+ p. In addition to boring article data, there's a _link_ embedded in the document. This is *hypermedia*, yeah! We will learn more about that shortly.
55
+
56
+ So, how did the service render that JSON document? It could use an ERB template, @#to_json@, or maybe another gem. The document could also be created by a *representer*.
57
+
58
+ Representers are the key ingredience in Roar, so let's check them out!
59
+
60
+
61
+ h2. Representers
62
+
63
+ To render a representational document, the backend service has to define a representer.
64
+
65
+ <pre>module JSON
66
+ class Article < Roar::Representer::JSON
67
+ property :title
68
+ property :id
69
+
70
+ link :self do
71
+ article_url(represented)
72
+ end
73
+ end
74
+ end
75
+ </pre>
76
+
77
+ Hooray, we can define plain properties and embedd links easily - and we can even use URL helpers (in Rails). There's even more, nesting, collections, but more on that later!
78
+
79
+
80
+ h3. Rendering Representations in the Service
81
+
82
+ 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.
83
+
84
+ <pre>JSON::Article.new(
85
+ :title => "Lonestar",
86
+ :id => 666).
87
+ serialize # => "{\"article\":{\"id\":666, ...
88
+ </pre>
89
+
90
+ Using the @ModelRepresenting@ feature module we can take a shortcut.
91
+
92
+ <pre>@beer = Article.find_by_title("Lonestar")
93
+
94
+ JSON::Article.from_model(@beer).
95
+ serialize # => "{\"article\":{\"id\":666, ...
96
+ </pre>
97
+
98
+ Articles itself are useless, so they may be placed into orders. This is the next example.
99
+
100
+
101
+ h3. Nesting Representations
102
+
103
+ What if we wanted to check an existing order? We'd @GET http://orders/1@, right?
104
+
105
+ <pre>{ "order": {
106
+ "id": 1,
107
+ "client_id": "815",
108
+ "articles": [
109
+ {"title": "Lonestar Beer",
110
+ "id": 666,
111
+ "links":[
112
+ { "rel": "self",
113
+ "href": "http://articles/lonestarbeer"}
114
+ ]}
115
+ ],
116
+ "links":[
117
+ { "rel": "self",
118
+ "href": "http://orders/1"},
119
+ { "rel": "items",
120
+ "href": "http://orders/1/items"}
121
+ ]}
122
+ }
123
+ </pre>
124
+
125
+ Since orders may contain a composition of articles, how would the order service define its representer?
126
+
127
+ <pre>module JSON
128
+ class Order < Roar::Representer::JSON
129
+ property :id
130
+ property :client_id
131
+
132
+ collection :articles, :as => Article
133
+
134
+ link :self do
135
+ order_url(represented)
136
+ end
137
+
138
+ link :items do
139
+ items_url
140
+ end
141
+ end
142
+ end
143
+ </pre>
144
+
145
+ The declarative @#collection@ method lets us define compositions of representers.
146
+
147
+
148
+ h3. Parsing Documents in the Service
149
+
150
+ Rendering stuff is easy: Representers allow defining the layout and serializing documents for us. However, representers can do more. They work _bi-directional_ in terms of rendering outgoing _and_ parsing incoming representation documents.
151
+
152
+ If we were to implement an endpoint for creating new orders, we'd allow POST to @http://orders/@. Let's explore the service code for parsing and creation.
153
+
154
+ <pre>
155
+ post "/orders" do
156
+ incoming = JSON::Order.deserialize(request.body.string)
157
+ puts incoming.to_attributes #=> {:client_id => 815}
158
+ </pre>
159
+
160
+ Again, the @ModelRepresenting@ module comes in handy for creating a new database record.
161
+
162
+ <pre>
163
+ post "/orders" do
164
+ # ...
165
+ @order = Order.create(incoming.to_nested_attributes)
166
+
167
+ JSON::Order.for_model(@order).serialize
168
+ </pre>
169
+
170
+ 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.
171
+
172
+
173
+ h2. Representers in the Client
174
+
175
+ The new representer abstraction layer seems complex and irritating first, where you used @params[]@ and @#to_json@ is a new OOP instance now. But... the cool thing is: You can package representers in gems and distribute them to your client layer as well. In our example, the web frontend can take advantage of the representers, too.
176
+
177
+
178
+ h3. Using HTTP
179
+
180
+ Communication between REST clients and services happens via HTTP - clients request, services respond. There are plenty of great gems helping out, namely Restfulie, HTTParty, etc. Representers in Roar provide support for HTTP as well, given you mix in the @HTTPVerbs@ feature module!
181
+
182
+ To create a new order, the frontend needs to POST to the REST backend. Here's how this could happen using a representer on HTTP.
183
+
184
+
185
+ <pre>
186
+ order = JSON::Order.new(:client_id => current_user.id)
187
+ order.post!("http://orders/")
188
+ </pre>
189
+
190
+ A couple of noteworthy steps happen here.
191
+
192
+ # Using the constructor a blank order document is created.
193
+ # Initial values like the client's id are passed as arguments and placed in the document.
194
+ # The filled-out document is POSTed to the given URL.
195
+ # The backend service creates an actual order record and sends back the representation.
196
+ # In the @#post!@ call, the returned document is parsed and attributes in the representer instance are updated accordingly,
197
+
198
+ After the HTTP roundtrip, the order instance keeps all the information we need for proceeding the ordering workflow.
199
+
200
+ <pre>
201
+ order.id #=> 42
202
+ </pre>
203
+
204
+ h3. Discovering Hypermedia
205
+
206
+ Now that we got a fresh order, let's place some items! The system's API allows adding articles to an existing order by POSTing articles to a specific resource. This endpoint is propagated in the order document using *hypermedia*.
207
+
208
+ Where and what is this hypermedia?
209
+
210
+ First, check the JSON document we get back from the POST.
211
+
212
+ <pre>{ "order": {
213
+ "id": 42,
214
+ "client_id": 1337,
215
+ "articles": [],
216
+ "links":[
217
+ { "rel": "self",
218
+ "href": "http://orders/42"},
219
+ { "rel": "items",
220
+ "href": "http://orders/42/items"}
221
+ ]}
222
+ }
223
+ </pre>
224
+
225
+ Two hypermedia links are embedded in this representation, both feature a @rel@ attribute for defining a link semantic - a "meaning" - and a @href@ attribute for a network address. Isn't that great?
226
+
227
+ * The @self@ link refers to the actual resource. It's a REST best practice and representations should always refer to their resource address.
228
+ * The @items@ link is what we want. The address @http://orders/42/items@ is what we have to refer to when adding articles to this order. Why? Cause we decided that!
229
+
230
+
231
+ h3. Using Hypermedia
232
+
233
+ Let the frontend add the delicious "Lonestar" beer to our order, now!
234
+
235
+ <pre>
236
+ beer = JSON::Article.new(:title => "Lonestar Beer")
237
+ beer.post(order.links[:items])
238
+ </pre>
239
+
240
+ That's all we need to do.
241
+
242
+ # First, we create an appropriate article representation.
243
+ # Then, the @#links@ method helps extracting the @items@ link URL from the order document.
244
+ # A simple POST to the respective address places the item in the order.
245
+
246
+ The @order@ instance in the frontend is now stale - it doesn't contain articles, yet, since it is still the document from the first post to @http://orders/@.
247
+
248
+ <pre>
249
+ order.items #=> []
250
+ </pre>
251
+
252
+ To update attributes, a GET is needed.
253
+
254
+ <pre>
255
+ order.get!(order.links[:self])
256
+ </pre>
257
+
258
+ Again, we use hypermedia to retrieve the order's URL. And now, the added article is included in the order.
259
+
260
+ [*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.]
261
+
262
+ <pre>
263
+ order.to_attributes #=> {:id => 42, :client_id => 1337,
264
+ :articles => [{:title => "Lonestar Beer", :id => 666}]}
265
+ </pre>
266
+
267
+ This is cool, we used REST representers and hypermedia to create an order and fill it with articles. It's time for a beer, isn't it?
268
+
269
+
270
+ h3. Using Accessors
271
+
272
+ 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!
273
+
274
+ Here's what could happen in the frontend.
275
+
276
+ <pre>
277
+ beer = JSON::Article.new(:title => "Lonestar Beer")
278
+ order.items << beer
279
+ order.post!(order.links[:self])
280
+ </pre>
281
+
282
+ This was dead simple since representations can be composed of different documents in Roar.
283
+
284
+
285
+ h2. Current Status
286
+
287
+ Please note that Roar is still in conception, the API might change as well as concepts do.
288
+
289
+
290
+ h2. What is REST about?
291
+
292
+ Making that system RESTful basically means
293
+
294
+ # The frontend knows one _single entry point_ URL to the REST services. This is @http://orders@.
295
+ # Do _not_ let the frontend compute any URLs to further actions.
296
+ # Showing articles, creating a new order, adding articles to it and finally placing the order - this all requires further URLs. These URLs are embedded as _hypermedia_ in the representations sent by the REST backend.
297
+
data/Rakefile CHANGED
@@ -1,2 +1,18 @@
1
1
  require 'bundler'
2
2
  Bundler::GemHelper.install_tasks
3
+
4
+ require 'rake/testtask'
5
+
6
+ task :default => [:test, :testrails]
7
+
8
+ Rake::TestTask.new(:test) do |test|
9
+ test.libs << 'test'
10
+ test.test_files = FileList['test/xml_representer_test.rb', 'test/model_representing_test.rb', 'test/representer_test.rb', 'test/transport_test.rb', 'test/http_verbs_test.rb', 'test/integration_test.rb', 'test/json_representer_test.rb', 'test/xml_hypermedia_test.rb', 'test/hypermedia_test.rb']
11
+ test.verbose = true
12
+ end
13
+
14
+ Rake::TestTask.new(:testrails) do |test|
15
+ test.libs << 'test'
16
+ test.test_files = FileList['test/rails/*_test.rb']
17
+ test.verbose = true
18
+ end
@@ -0,0 +1,58 @@
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
@@ -0,0 +1,14 @@
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
@@ -0,0 +1,29 @@
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
@@ -0,0 +1,36 @@
1
+ module Roar
2
+ # Basic methods needed to implement the ActiveModel API. Gives your model +#attributes+ and +model_name+.
3
+ # Include this for quickly converting an object to a ROAR-compatible monster.
4
+ #
5
+ # #DISCUSS: are Models used both on client- and server-side? I'd say hell yeah.
6
+ module Model
7
+ extend ActiveSupport::Concern
8
+
9
+ module ClassMethods
10
+ def model_name
11
+ ActiveSupport::Inflector.underscore(self) # We don't use AM::Naming for now.
12
+ end
13
+
14
+ def accessors(*names)
15
+ names.each do |name|
16
+ class_eval %Q{
17
+ def #{name}=(v)
18
+ attributes["#{name}"] = v
19
+ end
20
+
21
+ def #{name}
22
+ attributes["#{name}"]
23
+ end
24
+ }
25
+ end
26
+ end
27
+ end
28
+
29
+
30
+ attr_accessor :attributes
31
+
32
+ def initialize(attributes={})
33
+ @attributes = attributes
34
+ end
35
+ end
36
+ end