roar 0.9.1 → 0.9.2
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGES.markdown +4 -0
- data/README.textile +33 -12
- data/Rakefile +0 -6
- data/lib/roar/representer/feature/http_verbs.rb +1 -1
- data/lib/roar/version.rb +1 -1
- data/roar.gemspec +3 -3
- data/test/Gemfile +0 -3
- data/test/fake_server.rb +0 -2
- data/test/test_helper.rb +6 -43
- data/test/xml_representer_test.rb +2 -6
- metadata +8 -12
- data/lib/roar/rails/controller_methods.rb +0 -71
- data/lib/roar/rails/representer_methods.rb +0 -53
- data/lib/roar/rails/test_case.rb +0 -69
data/CHANGES.markdown
CHANGED
data/README.textile
CHANGED
@@ -4,6 +4,7 @@ _Resource-Oriented Architectures in Ruby._
|
|
4
4
|
|
5
5
|
"Lets make documents suit our models and not models fit to documents."
|
6
6
|
|
7
|
+
Questions? Need help? Free 1st Level Support on irc.freenode.org#roar !
|
7
8
|
|
8
9
|
h2. Introduction
|
9
10
|
|
@@ -61,13 +62,22 @@ Representers are the key ingredience in Roar, so let's check them out!
|
|
61
62
|
|
62
63
|
h2. Representers
|
63
64
|
|
64
|
-
|
65
|
+
Representers are most usable when defined in a module, and then mixed into a host class. In our example, the host class is the article.
|
66
|
+
|
67
|
+
<pre>
|
68
|
+
class Article
|
69
|
+
attr_accessor :title, :id
|
70
|
+
end
|
71
|
+
</pre>
|
72
|
+
|
73
|
+
To render a representational document from the article, the backend service has to define a representer.
|
65
74
|
|
66
75
|
<pre>
|
67
76
|
require 'roar/representer/json'
|
68
77
|
require 'roar/representer/feature/hypermedia'
|
69
78
|
|
70
|
-
|
79
|
+
|
80
|
+
module ArticleRepresenter
|
71
81
|
include Roar::Representer::JSON
|
72
82
|
include Roar::Representer::Feature::Hypermedia
|
73
83
|
|
@@ -75,22 +85,22 @@ class Article
|
|
75
85
|
property :id
|
76
86
|
|
77
87
|
link :self do
|
78
|
-
article_url(
|
88
|
+
article_url(self)
|
79
89
|
end
|
80
90
|
end
|
81
91
|
</pre>
|
82
92
|
|
83
|
-
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!
|
93
|
+
Hooray, we can define plain properties and embedd links easily - and we can even use URL helpers (in Rails, using the "roar-rails gem":https://github.com/apotonick/roar-rails). There's even more, nesting, collections, but more on that later!
|
84
94
|
|
85
95
|
|
86
96
|
h3. Rendering Representations in the Service
|
87
97
|
|
88
98
|
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.
|
89
99
|
|
90
|
-
<pre>Article.new(
|
91
|
-
title
|
92
|
-
id
|
93
|
-
to_json # => "{\"article\":{\"id\":666, ...
|
100
|
+
<pre>loney = Article.new.extend(ArticleRepresenter)
|
101
|
+
loney.title = "Lonestar"
|
102
|
+
loney.id = 666
|
103
|
+
loney.to_json # => "{\"article\":{\"id\":666, ...
|
94
104
|
</pre>
|
95
105
|
|
96
106
|
Articles itself are useless, so they may be placed into orders. This is the next example.
|
@@ -120,10 +130,18 @@ What if we wanted to check an existing order? We'd @GET http://orders/1@, right?
|
|
120
130
|
}
|
121
131
|
</pre>
|
122
132
|
|
133
|
+
The order model is simple.
|
134
|
+
|
135
|
+
<pre>
|
136
|
+
class Order
|
137
|
+
attr_accessor :id, :client_id, :articles
|
138
|
+
end
|
139
|
+
</pre>
|
140
|
+
|
123
141
|
Since orders may contain a composition of articles, how would the order service define its representer?
|
124
142
|
|
125
143
|
<pre>
|
126
|
-
|
144
|
+
module OrderRepresenter
|
127
145
|
include Roar::Representer::JSON
|
128
146
|
include Roar::Representer::Feature::Hypermedia
|
129
147
|
|
@@ -142,6 +160,8 @@ class Order
|
|
142
160
|
end
|
143
161
|
</pre>
|
144
162
|
|
163
|
+
Representers don't have to be in modules, but can be
|
164
|
+
|
145
165
|
The declarative @#collection@ method lets us define compositions of representers.
|
146
166
|
|
147
167
|
|
@@ -153,12 +173,13 @@ If we were to implement an endpoint for creating new orders, we'd allow POST to
|
|
153
173
|
|
154
174
|
<pre>
|
155
175
|
post "/orders" do
|
156
|
-
|
157
|
-
|
176
|
+
order = Order.new.extend(OrderRepresenter)
|
177
|
+
order.from_json(request.body.string)
|
178
|
+
order.to_json
|
158
179
|
end
|
159
180
|
</pre>
|
160
181
|
|
161
|
-
Look how the @#
|
182
|
+
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.
|
162
183
|
|
163
184
|
|
164
185
|
h2. Representers in the Client
|
data/Rakefile
CHANGED
@@ -10,9 +10,3 @@ Rake::TestTask.new(:test) do |test|
|
|
10
10
|
test.test_files = FileList['test/*_test.rb'] - ['test/integration_test.rb', 'test/active_record_integration_test.rb']
|
11
11
|
test.verbose = true
|
12
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
|
@@ -27,7 +27,7 @@ module Roar
|
|
27
27
|
# and updates properties accordingly.
|
28
28
|
def post(url, format)
|
29
29
|
# DISCUSS: what if a redirect happens here?
|
30
|
-
document = http.post_uri(url, serialize, format).body
|
30
|
+
document = http.post_uri(url, serialize(:links => false), format).body
|
31
31
|
deserialize(document)
|
32
32
|
end
|
33
33
|
|
data/lib/roar/version.rb
CHANGED
data/roar.gemspec
CHANGED
@@ -19,9 +19,9 @@ Gem::Specification.new do |s|
|
|
19
19
|
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
20
20
|
s.require_paths = ["lib"]
|
21
21
|
|
22
|
-
s.add_runtime_dependency "representable", "~> 1.
|
22
|
+
s.add_runtime_dependency "representable", "~> 1.1"
|
23
23
|
|
24
24
|
s.add_development_dependency "test_xml"
|
25
|
-
s.add_development_dependency "minitest",
|
26
|
-
s.add_development_dependency "sinatra",
|
25
|
+
s.add_development_dependency "minitest", ">= 2.8.1"
|
26
|
+
s.add_development_dependency "sinatra", "~> 1.2.6"
|
27
27
|
end
|
data/test/Gemfile
CHANGED
data/test/fake_server.rb
CHANGED
data/test/test_helper.rb
CHANGED
@@ -8,29 +8,6 @@ require 'roar/representer'
|
|
8
8
|
require 'roar/representer/feature/hypermedia'
|
9
9
|
require 'roar/representer/feature/http_verbs'
|
10
10
|
|
11
|
-
# TODO: 2BRM.
|
12
|
-
module TestModel
|
13
|
-
def self.included(base)
|
14
|
-
base.extend ClassMethods
|
15
|
-
end
|
16
|
-
|
17
|
-
|
18
|
-
module ClassMethods
|
19
|
-
def accessors(*names)
|
20
|
-
names.each do |name|
|
21
|
-
attr_accessor name
|
22
|
-
end
|
23
|
-
end
|
24
|
-
end
|
25
|
-
|
26
|
-
attr_accessor :attributes
|
27
|
-
|
28
|
-
def initialize(attributes={})
|
29
|
-
attributes.each do |k,v|
|
30
|
-
send("#{k}=", v)
|
31
|
-
end
|
32
|
-
end
|
33
|
-
end
|
34
11
|
|
35
12
|
module AttributesContructor
|
36
13
|
def initialize(attrs={})
|
@@ -40,33 +17,19 @@ module AttributesContructor
|
|
40
17
|
end
|
41
18
|
end
|
42
19
|
|
43
|
-
|
44
|
-
|
45
20
|
class Item
|
46
|
-
include
|
47
|
-
|
48
|
-
|
49
|
-
def self.model_name
|
50
|
-
"item"
|
51
|
-
end
|
21
|
+
include AttributesContructor
|
22
|
+
attr_accessor :value
|
52
23
|
end
|
53
24
|
|
54
25
|
class Position
|
55
|
-
include
|
56
|
-
|
57
|
-
|
58
|
-
def self.model_name
|
59
|
-
:order
|
60
|
-
end
|
26
|
+
include AttributesContructor
|
27
|
+
attr_accessor :id, :item
|
61
28
|
end
|
62
29
|
|
63
30
|
class Order
|
64
|
-
include
|
65
|
-
|
66
|
-
|
67
|
-
def self.model_name
|
68
|
-
:order
|
69
|
-
end
|
31
|
+
include AttributesContructor
|
32
|
+
attr_accessor :id, :items
|
70
33
|
end
|
71
34
|
|
72
35
|
require "test_xml/mini_test"
|
@@ -42,12 +42,8 @@ end
|
|
42
42
|
|
43
43
|
class XMLRepresenterFunctionalTest < MiniTest::Spec
|
44
44
|
class GreedyOrder
|
45
|
-
include
|
46
|
-
|
47
|
-
|
48
|
-
def self.model_name
|
49
|
-
:order
|
50
|
-
end
|
45
|
+
include AttributesContructor
|
46
|
+
attr_accessor :id, :items
|
51
47
|
end
|
52
48
|
|
53
49
|
class TestXmlRepresenter
|
metadata
CHANGED
@@ -5,8 +5,8 @@ version: !ruby/object:Gem::Version
|
|
5
5
|
segments:
|
6
6
|
- 0
|
7
7
|
- 9
|
8
|
-
-
|
9
|
-
version: 0.9.
|
8
|
+
- 2
|
9
|
+
version: 0.9.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:
|
17
|
+
date: 2012-02-06 00:00:00 +01:00
|
18
18
|
default_executable:
|
19
19
|
dependencies:
|
20
20
|
- !ruby/object:Gem::Dependency
|
@@ -27,9 +27,8 @@ dependencies:
|
|
27
27
|
- !ruby/object:Gem::Version
|
28
28
|
segments:
|
29
29
|
- 1
|
30
|
-
- 0
|
31
30
|
- 1
|
32
|
-
version: 1.
|
31
|
+
version: "1.1"
|
33
32
|
type: :runtime
|
34
33
|
version_requirements: *id001
|
35
34
|
- !ruby/object:Gem::Dependency
|
@@ -51,13 +50,13 @@ dependencies:
|
|
51
50
|
requirement: &id003 !ruby/object:Gem::Requirement
|
52
51
|
none: false
|
53
52
|
requirements:
|
54
|
-
- -
|
53
|
+
- - ">="
|
55
54
|
- !ruby/object:Gem::Version
|
56
55
|
segments:
|
56
|
+
- 2
|
57
|
+
- 8
|
57
58
|
- 1
|
58
|
-
|
59
|
-
- 0
|
60
|
-
version: 1.6.0
|
59
|
+
version: 2.8.1
|
61
60
|
type: :development
|
62
61
|
version_requirements: *id003
|
63
62
|
- !ruby/object:Gem::Dependency
|
@@ -93,9 +92,6 @@ files:
|
|
93
92
|
- TODO.markdown
|
94
93
|
- lib/roar.rb
|
95
94
|
- lib/roar/rails.rb
|
96
|
-
- lib/roar/rails/controller_methods.rb
|
97
|
-
- lib/roar/rails/representer_methods.rb
|
98
|
-
- lib/roar/rails/test_case.rb
|
99
95
|
- lib/roar/representer.rb
|
100
96
|
- lib/roar/representer/feature/http_verbs.rb
|
101
97
|
- lib/roar/representer/feature/hypermedia.rb
|
@@ -1,71 +0,0 @@
|
|
1
|
-
require 'active_support/core_ext/class/attribute'
|
2
|
-
|
3
|
-
module Roar
|
4
|
-
module Rails
|
5
|
-
module ControllerMethods
|
6
|
-
extend ActiveSupport::Concern
|
7
|
-
|
8
|
-
included do |base|
|
9
|
-
base.responder = Responder
|
10
|
-
base.class_attribute :represented_class
|
11
|
-
end
|
12
|
-
|
13
|
-
module ClassMethods
|
14
|
-
# Sets the represented class for the controller.
|
15
|
-
def represents(model_class)
|
16
|
-
self.represented_class = model_class
|
17
|
-
end
|
18
|
-
end
|
19
|
-
|
20
|
-
#private
|
21
|
-
def representer_class_for(model_class, format)
|
22
|
-
# DISCUSS: upcase and static namespace is not cool, but works for now.
|
23
|
-
"Representer::#{format.to_s.upcase}::#{model_class}".constantize
|
24
|
-
end
|
25
|
-
|
26
|
-
# Returns a representer instance that has parsed the request body.
|
27
|
-
def incoming
|
28
|
-
representer = representer_class_for(self.class.represented_class, formats.first).deserialize(request.raw_post)
|
29
|
-
end
|
30
|
-
|
31
|
-
|
32
|
-
# Returns the deserialized representation as a hash suitable for #create and #update_attributes.
|
33
|
-
def representation
|
34
|
-
incoming.to_nested_attributes
|
35
|
-
end
|
36
|
-
|
37
|
-
|
38
|
-
class Responder < ActionController::Responder
|
39
|
-
def display(resource, given_options={})
|
40
|
-
# TODO: find the correct representer for #format.
|
41
|
-
# TODO: should we infer the represented class per default?
|
42
|
-
# TODO: unit-test this method.
|
43
|
-
#representer = controller.representer_class_for(resource.class, format)
|
44
|
-
representer = controller.representer_class_for(controller.represented_class, format)
|
45
|
-
|
46
|
-
# DISCUSS: do that here?
|
47
|
-
#representer.extend(RepresenterMethods::ClassMethods)
|
48
|
-
|
49
|
-
controller.render given_options.merge!(options).merge!(
|
50
|
-
format => representer.serialize_model_with_controller(resource, controller)
|
51
|
-
)
|
52
|
-
end
|
53
|
-
|
54
|
-
# This is the common behavior for formats associated with APIs, such as :xml and :json.
|
55
|
-
def api_behavior(error)
|
56
|
-
if has_errors?
|
57
|
-
controller.render :text => resource.errors, :status => :unprocessable_entity # TODO: which media format? use an ErrorRepresenter shipped with Roar.
|
58
|
-
elsif get?
|
59
|
-
display resource
|
60
|
-
elsif post?
|
61
|
-
display resource, :status => :created, :location => api_location
|
62
|
-
elsif put?
|
63
|
-
display resource
|
64
|
-
else
|
65
|
-
head :ok
|
66
|
-
end
|
67
|
-
end
|
68
|
-
end
|
69
|
-
end
|
70
|
-
end
|
71
|
-
end
|
@@ -1,53 +0,0 @@
|
|
1
|
-
module Roar
|
2
|
-
module Rails
|
3
|
-
# Makes Rails URL helpers work in representers. Dependent on Rails.application.
|
4
|
-
module RepresenterMethods
|
5
|
-
extend ActiveSupport::Concern
|
6
|
-
|
7
|
-
included do |base|
|
8
|
-
base.class_eval do
|
9
|
-
attr_accessor :_controller
|
10
|
-
delegate :request, :env, :to => :_controller
|
11
|
-
|
12
|
-
include ActionController::UrlFor
|
13
|
-
include ::Rails.application.routes.url_helpers
|
14
|
-
|
15
|
-
extend Conventions
|
16
|
-
end
|
17
|
-
end
|
18
|
-
|
19
|
-
module ClassMethods
|
20
|
-
# TODO: test?
|
21
|
-
def for_model_with_controller(represented, controller)
|
22
|
-
# DISCUSS: use #for_model_attributes for overriding?
|
23
|
-
from_attributes(compute_attributes_for(represented)) do |rep|
|
24
|
-
rep.represented = represented
|
25
|
-
rep._controller = controller
|
26
|
-
end
|
27
|
-
end
|
28
|
-
|
29
|
-
# TODO: test?
|
30
|
-
def serialize_model_with_controller(represented, controller)
|
31
|
-
for_model_with_controller(represented, controller).serialize
|
32
|
-
end
|
33
|
-
end
|
34
|
-
|
35
|
-
# Introduces strongly opinionated convenience methods in Representer.
|
36
|
-
module Conventions
|
37
|
-
def representation_name
|
38
|
-
super.to_s.singularize
|
39
|
-
end
|
40
|
-
|
41
|
-
def collection(name, options={})
|
42
|
-
namespace = self.name.split("::")[-2] # FIXME: this assumption is pretty opinionated.
|
43
|
-
singular_name = name.to_s.singularize
|
44
|
-
|
45
|
-
super name, options.reverse_merge(
|
46
|
-
:class => "representer/#{namespace}/#{singular_name}".classify.constantize,
|
47
|
-
#:tag => singular_name # FIXME: how/where to decide if singular TAG or not?
|
48
|
-
)
|
49
|
-
end
|
50
|
-
end
|
51
|
-
end
|
52
|
-
end
|
53
|
-
end
|
data/lib/roar/rails/test_case.rb
DELETED
@@ -1,69 +0,0 @@
|
|
1
|
-
#require 'test_xml/test_unit'
|
2
|
-
require 'action_controller/test_case'
|
3
|
-
|
4
|
-
module Roar::Rails
|
5
|
-
module TestCase
|
6
|
-
def process(action, *args)
|
7
|
-
raise
|
8
|
-
if args.first.is_a?(String)
|
9
|
-
puts "YO"
|
10
|
-
request.env['RAW_POST_DATA'] = args.shift
|
11
|
-
method = args.pop
|
12
|
-
args << nil
|
13
|
-
args << method
|
14
|
-
end
|
15
|
-
|
16
|
-
super
|
17
|
-
end
|
18
|
-
|
19
|
-
def assert_response(status, headers={}) # FIXME: allow message.
|
20
|
-
super
|
21
|
-
|
22
|
-
if headers.is_a?(Hash)
|
23
|
-
assert_headers(headers)
|
24
|
-
else
|
25
|
-
assert_body(headers)
|
26
|
-
end
|
27
|
-
end
|
28
|
-
|
29
|
-
def assert_headers(headers)
|
30
|
-
headers.each_pair do |k,v|
|
31
|
-
assert_equal v, @response.headers[k]
|
32
|
-
end
|
33
|
-
end
|
34
|
-
|
35
|
-
def assert_body(body, options={})
|
36
|
-
return assert_xml_equal body, @response.body if options[:format] == :xml # FIXME: how do we know whether assert_xml is appropriate?
|
37
|
-
assert_equal body, @response.body
|
38
|
-
end
|
39
|
-
end
|
40
|
-
end
|
41
|
-
|
42
|
-
|
43
|
-
ActionController::TestCase::Behavior.class_eval do
|
44
|
-
# FIXME: ugly monkey-patching.
|
45
|
-
# TODO: test:
|
46
|
-
# put :create
|
47
|
-
# put :create, :format => :xml
|
48
|
-
# put :create, "<order/>", :format => :xml
|
49
|
-
# put :create, "<order/>"
|
50
|
-
|
51
|
-
|
52
|
-
include Roar::Rails::TestCase
|
53
|
-
end
|
54
|
-
|
55
|
-
RSpec::Rails::ControllerExampleGroup.class_eval do
|
56
|
-
#include Roar::Rails::TestCase
|
57
|
-
# FIXME: include module!
|
58
|
-
|
59
|
-
def process(action, *args)
|
60
|
-
if args.first.is_a?(String)
|
61
|
-
request.env['RAW_POST_DATA'] = args.shift
|
62
|
-
method = args.pop
|
63
|
-
args << nil
|
64
|
-
args << method
|
65
|
-
end
|
66
|
-
|
67
|
-
super
|
68
|
-
end
|
69
|
-
end
|