roar 0.8.0 → 0.8.1
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +2 -2
- data/README.textile +8 -2
- data/Rakefile +1 -1
- data/lib/roar/rails/representer_methods.rb +2 -1
- data/lib/roar/representer/base.rb +65 -0
- data/lib/roar/representer/feature/http_verbs.rb +6 -7
- data/lib/roar/representer/feature/hypermedia.rb +53 -13
- data/lib/roar/representer/feature/model_representing.rb +19 -27
- data/lib/roar/representer/json.rb +44 -14
- data/lib/roar/representer/xml.rb +48 -24
- data/lib/roar/version.rb +1 -1
- data/roar.gemspec +3 -3
- data/test/{http_verbs_test.rb → http_verbs_feature_test.rb} +3 -3
- data/test/hypermedia_feature_test.rb +132 -0
- data/test/integration_test.rb +8 -8
- data/test/json_representer_test.rb +31 -26
- data/test/model_representing_test.rb +21 -23
- data/test/order_representers.rb +5 -2
- data/test/representer_test.rb +35 -5
- data/test/test_helper.rb +28 -7
- data/test/xml_representer_test.rb +60 -90
- metadata +11 -17
- data/lib/roar/model.rb +0 -36
- data/lib/roar/model/representable.rb +0 -31
- data/lib/roar/representer.rb +0 -72
- data/test/hypermedia_test.rb +0 -35
- data/test/model_test.rb +0 -50
- data/test/proxy_test.rb +0 -89
- data/test/representable_test.rb +0 -49
- data/test/ruby_representation_test.rb +0 -144
- data/test/test_helper_test.rb +0 -59
- data/test/xml_hypermedia_test.rb +0 -47
data/Gemfile
CHANGED
@@ -3,10 +3,10 @@ source "http://rubygems.org"
|
|
3
3
|
# Specify your gem's dependencies in roar.gemspec
|
4
4
|
gemspec
|
5
5
|
|
6
|
-
#gem "representable", :path => "
|
6
|
+
#gem "representable", :path => "../representable"
|
7
7
|
#gem "test_xml", :path => "/home/nick/projects/test_xml" #"~> 0.1.0"
|
8
8
|
|
9
9
|
group :test do
|
10
|
-
gem "rails", "~> 3.
|
10
|
+
gem "rails", "~> 3.1.0"
|
11
11
|
gem "sqlite3"
|
12
12
|
end
|
data/README.textile
CHANGED
@@ -3,6 +3,8 @@ h1. ROAR
|
|
3
3
|
_Streamlines the development of RESTful, Resource-Oriented Architectures in Ruby._
|
4
4
|
|
5
5
|
|
6
|
+
Lets make documents suit our models and not models fit to documents
|
7
|
+
|
6
8
|
h2. Introduction
|
7
9
|
|
8
10
|
Roar is a framework for developing distributed applications while using hypermedia as key for application workflow.
|
@@ -63,7 +65,9 @@ h2. Representers
|
|
63
65
|
To render a representational document, the backend service has to define a representer.
|
64
66
|
|
65
67
|
<pre>module JSON
|
66
|
-
class Article
|
68
|
+
class Article
|
69
|
+
include Roar::Representer::JSON
|
70
|
+
|
67
71
|
property :title
|
68
72
|
property :id
|
69
73
|
|
@@ -125,7 +129,9 @@ What if we wanted to check an existing order? We'd @GET http://orders/1@, right?
|
|
125
129
|
Since orders may contain a composition of articles, how would the order service define its representer?
|
126
130
|
|
127
131
|
<pre>module JSON
|
128
|
-
class Order
|
132
|
+
class Order
|
133
|
+
include Roar::Representer::JSON
|
134
|
+
|
129
135
|
property :id
|
130
136
|
property :client_id
|
131
137
|
|
data/Rakefile
CHANGED
@@ -7,7 +7,7 @@ task :default => [:test, :testrails]
|
|
7
7
|
|
8
8
|
Rake::TestTask.new(:test) do |test|
|
9
9
|
test.libs << 'test'
|
10
|
-
test.test_files = FileList['test
|
10
|
+
test.test_files = FileList['test/*_test.rb'] - ['test/integration_test.rb']
|
11
11
|
test.verbose = true
|
12
12
|
end
|
13
13
|
|
@@ -44,7 +44,8 @@ module Roar
|
|
44
44
|
|
45
45
|
super name, options.reverse_merge(
|
46
46
|
:as => "representer/#{namespace}/#{singular_name}".classify.constantize,
|
47
|
-
|
47
|
+
#:tag => singular_name # FIXME: how/where to decide if singular TAG or not?
|
48
|
+
)
|
48
49
|
end
|
49
50
|
end
|
50
51
|
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
require 'representable'
|
2
|
+
|
3
|
+
module Roar
|
4
|
+
module Representer
|
5
|
+
module Base
|
6
|
+
def self.included(base)
|
7
|
+
base.class_eval do
|
8
|
+
include Representable
|
9
|
+
extend ClassMethods
|
10
|
+
|
11
|
+
class << self
|
12
|
+
alias_method :property, :representable_property
|
13
|
+
alias_method :collection, :representable_collection
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
|
19
|
+
module ClassMethods
|
20
|
+
# Creates a representer instance and fills it with +attributes+.
|
21
|
+
def from_attributes(attributes) # DISCUSS: better move to #new? how do we handle the original #new then?
|
22
|
+
new.tap do |representer|
|
23
|
+
yield representer if block_given?
|
24
|
+
attributes.each { |p,v| representer.public_send("#{p}=", v) }
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
|
30
|
+
# Convert representer's attributes to a nested attributes hash.
|
31
|
+
def to_attributes
|
32
|
+
{}.tap do |attributes|
|
33
|
+
self.class.representable_attrs.each do |definition|
|
34
|
+
value = public_send(definition.getter)
|
35
|
+
|
36
|
+
if definition.typed?
|
37
|
+
value = definition.apply(value) do |v|
|
38
|
+
v.to_attributes # applied to each typed attribute (even in collections).
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
attributes[definition.name] = value
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
def before_serialize(*)
|
49
|
+
end
|
50
|
+
|
51
|
+
# Returns block used in #from_json and #from_xml to filter incoming arguments.
|
52
|
+
# This method is subject to change and might be removed, soon.
|
53
|
+
def deserialize_block_for_options(options)
|
54
|
+
return unless props = options[:except] || options[:include]
|
55
|
+
props.collect!{ |name| name.to_s }
|
56
|
+
|
57
|
+
lambda do |bind|
|
58
|
+
res = props.include?(bind.definition.name)
|
59
|
+
options[:include] ? res : !res
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -1,16 +1,15 @@
|
|
1
1
|
require 'roar/client/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.
|
5
5
|
module Representer
|
6
6
|
module Feature
|
7
7
|
module HttpVerbs
|
8
|
-
|
9
|
-
|
10
|
-
included do |base|
|
11
|
-
base.class_attribute :resource_base
|
8
|
+
def self.included(base)
|
9
|
+
base.extend ClassMethods
|
12
10
|
end
|
13
11
|
|
12
|
+
|
14
13
|
module ClassMethods
|
15
14
|
include Client::Transport
|
16
15
|
|
@@ -40,7 +39,7 @@ module Roar
|
|
40
39
|
|
41
40
|
self.class.representable_attrs.each do |definition|
|
42
41
|
|
43
|
-
send(definition.setter, rep.public_send(definition.
|
42
|
+
send(definition.setter, rep.public_send(definition.getter))
|
44
43
|
end # TODO: this sucks. do this with #properties and #replace_properties.
|
45
44
|
end
|
46
45
|
|
@@ -48,7 +47,7 @@ module Roar
|
|
48
47
|
rep = self.class.get(url, format) # TODO: where's the format? why do we need class here?
|
49
48
|
|
50
49
|
self.class.representable_attrs.each do |definition|
|
51
|
-
send(definition.setter, rep.public_send(definition.
|
50
|
+
send(definition.setter, rep.public_send(definition.getter))
|
52
51
|
end # TODO: this sucks. do this with #properties and #replace_properties.
|
53
52
|
end
|
54
53
|
|
@@ -1,21 +1,52 @@
|
|
1
|
-
require "roar/model"
|
2
|
-
|
3
1
|
module Roar
|
4
2
|
module Representer
|
5
3
|
module Feature
|
6
|
-
# Adds
|
7
|
-
#
|
8
|
-
|
9
|
-
|
4
|
+
# Adds #link to the representer to define hypermedia links in the representation.
|
5
|
+
#
|
6
|
+
# Example:
|
7
|
+
#
|
8
|
+
# class Order
|
9
|
+
# include Roar::Representer::JSON
|
10
|
+
#
|
11
|
+
# property :id
|
12
|
+
#
|
13
|
+
# link :self do
|
14
|
+
# "http://orders/#{id}"
|
15
|
+
# end
|
16
|
+
module Hypermedia
|
17
|
+
def self.included(base)
|
18
|
+
base.extend ClassMethods
|
19
|
+
end
|
20
|
+
|
21
|
+
def before_serialize(options={})
|
22
|
+
prepare_links! unless options[:links] == false # DISCUSS: doesn't work when links are already setup (e.g. from #deserialize).
|
23
|
+
super # Representer::Base
|
24
|
+
end
|
10
25
|
|
11
|
-
def links=(
|
12
|
-
|
26
|
+
def links=(link_list)
|
27
|
+
links.replace(link_list)
|
13
28
|
end
|
14
29
|
|
15
30
|
def links
|
16
|
-
@links
|
31
|
+
@links ||= LinkCollection.new
|
17
32
|
end
|
18
33
|
|
34
|
+
protected
|
35
|
+
# Setup hypermedia links by invoking their blocks. Usually called by #serialize.
|
36
|
+
def prepare_links!
|
37
|
+
links_def = self.class.find_links_definition or return
|
38
|
+
links_def.rel2block.each do |link|
|
39
|
+
links << links_def.sought_type.from_attributes({ # create Hyperlink representer.
|
40
|
+
"rel" => link[:rel],
|
41
|
+
"href" => run_link_block(link[:block])})
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def run_link_block(block)
|
46
|
+
instance_exec(&block)
|
47
|
+
end
|
48
|
+
|
49
|
+
|
19
50
|
class LinkCollection < Array
|
20
51
|
def [](rel)
|
21
52
|
link = find { |l| l.rel.to_s == rel.to_s } and return link.href
|
@@ -24,19 +55,28 @@ module Roar
|
|
24
55
|
|
25
56
|
|
26
57
|
module ClassMethods
|
27
|
-
# Defines
|
58
|
+
# Defines a hypermedia link to be embedded in the document.
|
28
59
|
def link(rel, &block)
|
29
|
-
unless links =
|
60
|
+
unless links = find_links_definition
|
30
61
|
links = LinksDefinition.new(:links, links_definition_options)
|
31
62
|
representable_attrs << links
|
32
|
-
#add_reader(links) # TODO: refactor in Roxml.
|
33
|
-
# attr_writer(links.accessor)
|
34
63
|
end
|
35
64
|
|
36
65
|
links.rel2block << {:rel => rel, :block => block}
|
37
66
|
end
|
67
|
+
|
68
|
+
def find_links_definition
|
69
|
+
representable_attrs.find do |d| d.is_a?(LinksDefinition) end
|
70
|
+
end
|
38
71
|
end
|
39
72
|
|
73
|
+
|
74
|
+
class LinksDefinition < Representable::Definition
|
75
|
+
# TODO: hide rel2block in interface.
|
76
|
+
def rel2block
|
77
|
+
@rel2block ||= []
|
78
|
+
end
|
79
|
+
end
|
40
80
|
end
|
41
81
|
end
|
42
82
|
end
|
@@ -39,7 +39,7 @@ module Roar
|
|
39
39
|
# Properties that are mapped to a model attribute.
|
40
40
|
class ModelDefinition < ::Representable::Definition
|
41
41
|
def compute_attribute_for(represented, attributes)
|
42
|
-
value = represented.send(
|
42
|
+
value = represented.send(getter)
|
43
43
|
|
44
44
|
if typed?
|
45
45
|
value = apply(value) do |v|
|
@@ -47,40 +47,32 @@ module Roar
|
|
47
47
|
end
|
48
48
|
end
|
49
49
|
|
50
|
-
attributes[
|
50
|
+
attributes[name] = value
|
51
51
|
end
|
52
52
|
end
|
53
53
|
end
|
54
54
|
|
55
55
|
|
56
56
|
module ActiveRecordMethods
|
57
|
-
def to_nested_attributes # FIXME:
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
57
|
+
def to_nested_attributes # FIXME: extract iterating with #to_attributes.
|
58
|
+
{}.tap do |attributes|
|
59
|
+
self.class.representable_attrs.each do |definition|
|
60
|
+
next unless definition.kind_of?(ModelRepresenting::ModelDefinition)
|
61
|
+
|
62
|
+
value = public_send(definition.getter)
|
63
|
+
|
64
|
+
if definition.typed?
|
65
|
+
value = definition.apply(value) do |v|
|
66
|
+
v.to_nested_attributes # applied to each typed attribute (even in collections).
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
key = definition.name
|
71
|
+
key = "#{key}_attributes" if definition.typed?
|
72
|
+
|
73
|
+
attributes[key] = value
|
70
74
|
end
|
71
75
|
end
|
72
|
-
|
73
|
-
attrs
|
74
|
-
end
|
75
|
-
|
76
|
-
private
|
77
|
-
def clear_attributes(attrs)
|
78
|
-
puts "clearing #{attrs.inspect}"
|
79
|
-
attrs.each do |k,v|
|
80
|
-
attrs.delete(k) if k == "links"
|
81
|
-
|
82
|
-
clear_attributes(v) if v.is_a?(Hash)
|
83
|
-
end
|
84
76
|
end
|
85
77
|
end
|
86
78
|
end
|
@@ -1,32 +1,62 @@
|
|
1
|
-
require 'roar/representer'
|
1
|
+
require 'roar/representer/base'
|
2
2
|
require 'representable/json'
|
3
3
|
|
4
|
-
|
5
4
|
module Roar
|
6
5
|
module Representer
|
7
|
-
|
8
|
-
|
6
|
+
module JSON
|
7
|
+
def self.included(base)
|
8
|
+
base.class_eval do
|
9
|
+
include Base
|
10
|
+
include Representable::JSON
|
11
|
+
|
12
|
+
extend ClassMethods
|
13
|
+
include InstanceMethods # otherwise Representable overrides our #to_json.
|
14
|
+
end
|
15
|
+
end
|
9
16
|
|
10
|
-
|
11
|
-
to_json
|
17
|
+
module InstanceMethods
|
18
|
+
def to_json(*args)
|
19
|
+
before_serialize(*args)
|
20
|
+
super
|
21
|
+
end
|
22
|
+
|
23
|
+
def from_json(document, options={})
|
24
|
+
document ||= "{}" # DISCUSS: provide this for convenience, or better not?
|
25
|
+
|
26
|
+
if block = deserialize_block_for_options(options) and
|
27
|
+
return super(document, &block)
|
28
|
+
end
|
29
|
+
|
30
|
+
super
|
31
|
+
end
|
32
|
+
|
33
|
+
# Generic entry-point for rendering.
|
34
|
+
def serialize(*args)
|
35
|
+
to_json(*args)
|
36
|
+
end
|
12
37
|
end
|
13
38
|
|
14
|
-
|
15
|
-
|
39
|
+
|
40
|
+
module ClassMethods
|
41
|
+
def deserialize(json)
|
42
|
+
from_json(json)
|
43
|
+
end
|
44
|
+
|
45
|
+
# TODO: move to instance method, or remove?
|
46
|
+
def links_definition_options
|
47
|
+
{:as => [Hyperlink]}
|
48
|
+
end
|
16
49
|
end
|
17
50
|
|
51
|
+
|
18
52
|
# Encapsulates a hypermedia link.
|
19
|
-
class Hyperlink
|
53
|
+
class Hyperlink
|
54
|
+
include JSON
|
20
55
|
self.representation_name = :link
|
21
56
|
|
22
57
|
property :rel
|
23
58
|
property :href
|
24
59
|
end
|
25
|
-
|
26
|
-
def self.links_definition_options
|
27
|
-
{:as => [Hyperlink]}
|
28
|
-
end
|
29
60
|
end
|
30
|
-
|
31
61
|
end
|
32
62
|
end
|
data/lib/roar/representer/xml.rb
CHANGED
@@ -1,43 +1,67 @@
|
|
1
|
-
require 'roar/representer'
|
1
|
+
require 'roar/representer/base'
|
2
2
|
require 'representable/xml'
|
3
3
|
|
4
|
-
|
5
4
|
module Roar
|
6
|
-
#
|
7
|
-
#
|
8
|
-
# * recognized elements are stored as representer attributes
|
9
|
-
# out: * attributes in representer are assigned - either as hash in #to_xml, by calling #serialize(represented),
|
10
|
-
# by calling representer's accessors (eg in client?) or whatever else
|
11
|
-
# * representation is compiled from representer only
|
12
|
-
# TODO: make XML a module to include in Hyperlink < Base.
|
5
|
+
# Includes #from_xml and #to_xml into your represented object.
|
6
|
+
# In addition to that, some more options are available when declaring properties.
|
13
7
|
module Representer
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
8
|
+
module XML
|
9
|
+
def self.included(base)
|
10
|
+
base.class_eval do
|
11
|
+
include Base
|
12
|
+
include Representable::XML
|
13
|
+
|
14
|
+
extend ClassMethods
|
15
|
+
include InstanceMethods # otherwise Representable overrides our #to_xml.
|
16
|
+
end
|
19
17
|
end
|
20
18
|
|
21
|
-
|
22
|
-
from_xml(
|
19
|
+
module InstanceMethods
|
20
|
+
def from_xml(document, options={})
|
21
|
+
if block = deserialize_block_for_options(options)
|
22
|
+
return super(document, &block)
|
23
|
+
end
|
24
|
+
|
25
|
+
super
|
26
|
+
end
|
27
|
+
|
28
|
+
def to_xml(*args)
|
29
|
+
before_serialize(*args)
|
30
|
+
super.serialize
|
31
|
+
end
|
32
|
+
|
33
|
+
# Generic entry-point for rendering.
|
34
|
+
def serialize(*args)
|
35
|
+
to_xml(*args)
|
36
|
+
end
|
23
37
|
end
|
24
38
|
|
25
39
|
|
40
|
+
module ClassMethods
|
41
|
+
include Representable::XML::ClassMethods
|
42
|
+
|
43
|
+
def links_definition_options
|
44
|
+
{:from => :link, :as => [Hyperlink]}
|
45
|
+
end
|
46
|
+
|
47
|
+
# Generic entry-point for parsing.
|
48
|
+
def deserialize(*args)
|
49
|
+
from_xml(*args)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
|
26
54
|
# Encapsulates a hypermedia <link ...>.
|
27
|
-
class Hyperlink
|
55
|
+
class Hyperlink
|
56
|
+
# TODO: make XML a module to include in Hyperlink < Base.
|
57
|
+
include XML
|
58
|
+
|
28
59
|
self.representation_name = :link
|
29
60
|
|
30
61
|
property :rel, :from => "@rel"
|
31
62
|
property :href, :from => "@href"
|
32
63
|
end
|
33
64
|
|
34
|
-
|
35
|
-
def self.links_definition_options
|
36
|
-
{:tag => :link, :as => [Hyperlink]}
|
37
|
-
end
|
38
|
-
|
39
|
-
require 'roar/representer/feature/hypermedia'
|
40
|
-
include Feature::Hypermedia
|
41
65
|
end
|
42
66
|
end
|
43
67
|
end
|