roar 0.9.1 → 0.9.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 +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
|