roar 0.8.0 → 0.8.1
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/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
|