roar 0.0.1.alpha1 → 0.8.0
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/.gitignore +1 -0
- data/Gemfile +8 -0
- data/README.textile +297 -0
- data/Rakefile +16 -0
- data/lib/roar/client/entity_proxy.rb +58 -0
- data/lib/roar/client/proxy.rb +14 -0
- data/lib/roar/client/transport.rb +29 -0
- data/lib/roar/model.rb +36 -0
- data/lib/roar/model/representable.rb +31 -0
- data/lib/roar/rails.rb +21 -0
- data/lib/roar/rails/controller_methods.rb +71 -0
- data/lib/roar/rails/representer_methods.rb +52 -0
- data/lib/roar/rails/test_case.rb +43 -0
- data/lib/roar/representer.rb +72 -0
- data/lib/roar/representer/feature/http_verbs.rb +63 -0
- data/lib/roar/representer/feature/hypermedia.rb +43 -0
- data/lib/roar/representer/feature/model_representing.rb +88 -0
- data/lib/roar/representer/json.rb +32 -0
- data/lib/roar/representer/xml.rb +43 -0
- data/lib/roar/version.rb +1 -1
- data/roar.gemspec +10 -1
- data/test/Gemfile +6 -0
- data/test/dummy/Rakefile +7 -0
- data/test/dummy/app/controllers/albums_controller.rb +27 -0
- data/test/dummy/app/controllers/application_controller.rb +4 -0
- data/test/dummy/app/helpers/application_helper.rb +2 -0
- data/test/dummy/app/models/album.rb +6 -0
- data/test/dummy/app/models/song.rb +2 -0
- data/test/dummy/app/representers/representer/xml/album.rb +19 -0
- data/test/dummy/app/representers/representer/xml/song.rb +9 -0
- data/test/dummy/app/views/layouts/application.html.erb +14 -0
- data/test/dummy/app/views/musician/featured.html.erb +1 -0
- data/test/dummy/app/views/musician/featured_with_block.html.erb +4 -0
- data/test/dummy/app/views/musician/hamlet.html.haml +1 -0
- data/test/dummy/config.ru +4 -0
- data/test/dummy/config/application.rb +20 -0
- data/test/dummy/config/boot.rb +10 -0
- data/test/dummy/config/database.yml +22 -0
- data/test/dummy/config/environment.rb +5 -0
- data/test/dummy/config/environments/development.rb +16 -0
- data/test/dummy/config/environments/production.rb +46 -0
- data/test/dummy/config/environments/test.rb +32 -0
- data/test/dummy/config/locales/en.yml +5 -0
- data/test/dummy/config/routes.rb +7 -0
- data/test/dummy/db/development.sqlite3 +0 -0
- data/test/dummy/db/migrate/20110514114753_create_albums.rb +14 -0
- data/test/dummy/db/migrate/20110514121228_create_songs.rb +14 -0
- data/test/dummy/db/schema.rb +29 -0
- data/test/dummy/db/test.sqlite3 +0 -0
- data/test/dummy/module - (2011-05-14 15:26:19) +5 -0
- data/test/dummy/public/404.html +26 -0
- data/test/dummy/public/422.html +26 -0
- data/test/dummy/public/500.html +26 -0
- data/test/dummy/public/favicon.ico +0 -0
- data/test/dummy/script/rails +6 -0
- data/test/dummy/tmp/app/cells/blog/post/latest.html.erb +7 -0
- data/test/dummy/tmp/app/cells/blog/post_cell.rb +7 -0
- data/test/fake_server.rb +80 -0
- data/test/http_verbs_test.rb +46 -0
- data/test/hypermedia_test.rb +35 -0
- data/test/integration_test.rb +122 -0
- data/test/json_representer_test.rb +101 -0
- data/test/model_representing_test.rb +121 -0
- data/test/model_test.rb +50 -0
- data/test/order_representers.rb +34 -0
- data/test/proxy_test.rb +89 -0
- data/test/rails/controller_methods_test.rb +147 -0
- data/test/rails/rails_representer_methods_test.rb +32 -0
- data/test/representable_test.rb +49 -0
- data/test/representer_test.rb +25 -0
- data/test/ruby_representation_test.rb +144 -0
- data/test/test_helper.rb +45 -0
- data/test/test_helper_test.rb +59 -0
- data/test/transport_test.rb +34 -0
- data/test/xml_hypermedia_test.rb +47 -0
- data/test/xml_representer_test.rb +238 -0
- metadata +181 -13
@@ -0,0 +1,31 @@
|
|
1
|
+
module Roar
|
2
|
+
module Model
|
3
|
+
module Representable
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
included do |base|
|
7
|
+
base.extend Hooks::InheritableAttribute
|
8
|
+
base.inheritable_attr :representable
|
9
|
+
base.representable = {} # FIXME: doesn't that break inheritance?
|
10
|
+
end
|
11
|
+
|
12
|
+
module ClassMethods
|
13
|
+
def represents(mime_type, options)
|
14
|
+
self.representable[mime_type] = options[:with]
|
15
|
+
end
|
16
|
+
|
17
|
+
def representer_class_for(mime_type)
|
18
|
+
representable[mime_type]
|
19
|
+
end
|
20
|
+
|
21
|
+
def from(mime_type, representation)
|
22
|
+
representer_class_for(mime_type).deserialize(self, mime_type, representation)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def to(mime_type)
|
27
|
+
self.class.representer_class_for(mime_type).new.serialize(self, mime_type)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
data/lib/roar/rails.rb
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'roar/rails/controller_methods'
|
2
|
+
require 'roar/representer'
|
3
|
+
require 'roar/representer/xml' # TODO: make dynamically.
|
4
|
+
require 'roar/representer/json' # TODO: make dynamically.
|
5
|
+
require 'roar/rails/representer_methods'
|
6
|
+
require 'roar/representer/feature/model_representing'
|
7
|
+
|
8
|
+
module Roar
|
9
|
+
module Rails
|
10
|
+
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
# FIXME: don't like monkey-patching:
|
15
|
+
# TODO: assure we don't overwrite anything here, as people might want to do things without AR/DM.
|
16
|
+
|
17
|
+
Roar::Representer::XML.class_eval do # FIXME: why in XML, only? #definition_class is defined in Representable.
|
18
|
+
include Roar::Representer::Feature::ModelRepresenting
|
19
|
+
include Roar::Representer::Feature::ActiveRecordMethods # to_nested_attributes
|
20
|
+
include Roar::Rails::RepresenterMethods
|
21
|
+
end
|
@@ -0,0 +1,71 @@
|
|
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
|
@@ -0,0 +1,52 @@
|
|
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
|
+
:as => "representer/#{namespace}/#{singular_name}".classify.constantize,
|
47
|
+
:tag => singular_name)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
require 'test_xml/test_unit'
|
2
|
+
|
3
|
+
ActionController::TestCase.class_eval do
|
4
|
+
# FIXME: ugly monkey-patching.
|
5
|
+
# TODO: test:
|
6
|
+
# put :create
|
7
|
+
# put :create, :format => :xml
|
8
|
+
# put :create, "<order/>", :format => :xml
|
9
|
+
# put :create, "<order/>"
|
10
|
+
def process(action, *args)
|
11
|
+
if args.first.is_a?(String)
|
12
|
+
request.env['RAW_POST_DATA'] = args.shift
|
13
|
+
method = args.pop
|
14
|
+
args << nil
|
15
|
+
args << method
|
16
|
+
end
|
17
|
+
|
18
|
+
super
|
19
|
+
end
|
20
|
+
|
21
|
+
def assert_response(status, headers={}) # FIXME: allow message.
|
22
|
+
super
|
23
|
+
|
24
|
+
if headers.is_a?(Hash)
|
25
|
+
assert_headers(headers)
|
26
|
+
else
|
27
|
+
assert_body(headers)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def assert_headers(headers)
|
32
|
+
headers.each_pair do |k,v|
|
33
|
+
assert_equal v, @response.headers[k]
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def assert_body(body, options={})
|
38
|
+
return assert_xml_equal body, @response.body if options[:format] == :xml # FIXME: how do we know whether assert_xml is appropriate?
|
39
|
+
assert_equal body, @response.body
|
40
|
+
end
|
41
|
+
|
42
|
+
|
43
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
require 'representable'
|
2
|
+
|
3
|
+
module Roar
|
4
|
+
module Representer
|
5
|
+
class Base
|
6
|
+
include Representable
|
7
|
+
|
8
|
+
class << self
|
9
|
+
alias_method :property, :representable_property
|
10
|
+
alias_method :collection, :representable_collection
|
11
|
+
|
12
|
+
# Creates a representer instance and fills it with +attributes+.
|
13
|
+
def from_attributes(attributes)
|
14
|
+
new.tap do |representer|
|
15
|
+
yield representer if block_given?
|
16
|
+
|
17
|
+
representable_attrs.each do |definition|
|
18
|
+
definition.populate(representer, attributes)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
|
25
|
+
def initialize(properties={})
|
26
|
+
properties.each { |p,v| send("#{p}=", v) } # DISCUSS: check if valid property?
|
27
|
+
end
|
28
|
+
|
29
|
+
# Convert representer's attributes to a nested attributes hash.
|
30
|
+
def to_attributes
|
31
|
+
{}.tap do |attributes|
|
32
|
+
self.class.representable_attrs.each do |definition|
|
33
|
+
value = public_send(definition.accessor)
|
34
|
+
|
35
|
+
if definition.typed?
|
36
|
+
value = definition.apply(value) do |v|
|
37
|
+
v.to_attributes # applied to each typed attribute (even in collections).
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
attributes[definition.accessor] = value
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
class LinksDefinition < Representable::Definition
|
48
|
+
def rel2block
|
49
|
+
@rel2block ||= []
|
50
|
+
end
|
51
|
+
|
52
|
+
def populate(representer, *args)
|
53
|
+
representer.links ||= []
|
54
|
+
|
55
|
+
rel2block.each do |link|
|
56
|
+
representer.links << sought_type.from_attributes({ # create Hyperlink representer.
|
57
|
+
"rel" => link[:rel],
|
58
|
+
"href" => representer.instance_exec(&link[:block])}) # DISCUSS: run block in representer context?
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
# FIXME: move to some init asset.
|
67
|
+
Representable::Definition.class_eval do
|
68
|
+
# Populate the representer's attribute with the right value.
|
69
|
+
def populate(representer, attributes)
|
70
|
+
representer.public_send("#{accessor}=", attributes[accessor])
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
require 'roar/client/transport'
|
2
|
+
|
3
|
+
module Roar
|
4
|
+
# Used in Models as convenience, ActiveResource-like methods. # FIXME: currently this is meant for clients like Representers.
|
5
|
+
module Representer
|
6
|
+
module Feature
|
7
|
+
module HttpVerbs
|
8
|
+
extend ActiveSupport::Concern
|
9
|
+
|
10
|
+
included do |base|
|
11
|
+
base.class_attribute :resource_base
|
12
|
+
end
|
13
|
+
|
14
|
+
module ClassMethods
|
15
|
+
include Client::Transport
|
16
|
+
|
17
|
+
def get(url, format) # TODO: test me!
|
18
|
+
#url = resource_base + variable_path.to_s
|
19
|
+
representation = get_uri(url, format).body
|
20
|
+
deserialize(representation)
|
21
|
+
end
|
22
|
+
|
23
|
+
def post(url, body, format)
|
24
|
+
representation = post_uri(url, body, format).body
|
25
|
+
deserialize(representation)
|
26
|
+
end
|
27
|
+
|
28
|
+
|
29
|
+
def put(url, body, format)
|
30
|
+
representation = put_uri(url, body, format).body
|
31
|
+
deserialize(representation)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def post(url, format)
|
36
|
+
self.class.post(url, serialize, format)
|
37
|
+
end
|
38
|
+
def post!(*args)
|
39
|
+
rep = post(*args) # TODO: make this better.
|
40
|
+
|
41
|
+
self.class.representable_attrs.each do |definition|
|
42
|
+
|
43
|
+
send(definition.setter, rep.public_send(definition.accessor))
|
44
|
+
end # TODO: this sucks. do this with #properties and #replace_properties.
|
45
|
+
end
|
46
|
+
|
47
|
+
def get!(url, format) # FIXME: abstract to #replace_properties
|
48
|
+
rep = self.class.get(url, format) # TODO: where's the format? why do we need class here?
|
49
|
+
|
50
|
+
self.class.representable_attrs.each do |definition|
|
51
|
+
send(definition.setter, rep.public_send(definition.accessor))
|
52
|
+
end # TODO: this sucks. do this with #properties and #replace_properties.
|
53
|
+
end
|
54
|
+
|
55
|
+
def put(url, format)
|
56
|
+
self.class.put(url, serialize, format)
|
57
|
+
end
|
58
|
+
|
59
|
+
# TODO: implement delete, patch.
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
require "roar/model"
|
2
|
+
|
3
|
+
module Roar
|
4
|
+
module Representer
|
5
|
+
module Feature
|
6
|
+
# Adds links methods to the model which can then be used for hypermedia links when
|
7
|
+
# representing the model.
|
8
|
+
module Hypermedia # TODO: test me.
|
9
|
+
extend ActiveSupport::Concern
|
10
|
+
|
11
|
+
def links=(links)
|
12
|
+
@links = LinkCollection.new(links)
|
13
|
+
end
|
14
|
+
|
15
|
+
def links
|
16
|
+
@links
|
17
|
+
end
|
18
|
+
|
19
|
+
class LinkCollection < Array
|
20
|
+
def [](rel)
|
21
|
+
link = find { |l| l.rel.to_s == rel.to_s } and return link.href
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
|
26
|
+
module ClassMethods
|
27
|
+
# Defines an embedded hypermedia link.
|
28
|
+
def link(rel, &block)
|
29
|
+
unless links = representable_attrs.find { |d| d.is_a?(LinksDefinition)}
|
30
|
+
links = LinksDefinition.new(:links, links_definition_options)
|
31
|
+
representable_attrs << links
|
32
|
+
#add_reader(links) # TODO: refactor in Roxml.
|
33
|
+
# attr_writer(links.accessor)
|
34
|
+
end
|
35
|
+
|
36
|
+
links.rel2block << {:rel => rel, :block => block}
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
module Roar
|
2
|
+
module Representer
|
3
|
+
module Feature
|
4
|
+
module ModelRepresenting
|
5
|
+
attr_accessor :represented
|
6
|
+
|
7
|
+
def self.included(base)
|
8
|
+
base.extend ClassMethods
|
9
|
+
end
|
10
|
+
|
11
|
+
module ClassMethods
|
12
|
+
def for_model(represented)
|
13
|
+
# DISCUSS: split that into #for_model_attributes so it's easier overridable?
|
14
|
+
from_attributes(compute_attributes_for(represented)) { |rep| rep.represented = represented }
|
15
|
+
end
|
16
|
+
|
17
|
+
def serialize_model(represented)
|
18
|
+
for_model(represented).serialize
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
def definition_class
|
23
|
+
ModelDefinition
|
24
|
+
end
|
25
|
+
|
26
|
+
# Called in for_model.
|
27
|
+
def compute_attributes_for(represented)
|
28
|
+
{}.tap do |attributes|
|
29
|
+
self.representable_attrs.each do |definition|
|
30
|
+
next unless definition.kind_of?(ModelDefinition) # for now, really only use "our" model attributes.
|
31
|
+
definition.compute_attribute_for(represented, attributes)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
|
37
|
+
end # ClassMethods
|
38
|
+
|
39
|
+
# Properties that are mapped to a model attribute.
|
40
|
+
class ModelDefinition < ::Representable::Definition
|
41
|
+
def compute_attribute_for(represented, attributes)
|
42
|
+
value = represented.send(accessor)
|
43
|
+
|
44
|
+
if typed?
|
45
|
+
value = apply(value) do |v|
|
46
|
+
sought_type.for_model(v) # applied to each typed attribute (even in collections).
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
attributes[accessor] = value
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
|
56
|
+
module ActiveRecordMethods
|
57
|
+
def to_nested_attributes # FIXME: works on first level only, doesn't check if we really need to suffix _attributes and is horriby implemented. just for protoyping.
|
58
|
+
attrs = {}
|
59
|
+
|
60
|
+
to_attributes.each do |k,v|
|
61
|
+
|
62
|
+
|
63
|
+
|
64
|
+
#next if k.to_s == "links" # FIXME: how to skip virtual attributes that are not mapped in a model?
|
65
|
+
clear_attributes(attrs)
|
66
|
+
|
67
|
+
attrs[k] = v
|
68
|
+
if v.is_a?(Hash) or v.is_a?(Array)
|
69
|
+
attrs["#{k}_attributes"] = attrs.delete(k)
|
70
|
+
end
|
71
|
+
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
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|