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.
Files changed (77) hide show
  1. data/.gitignore +1 -0
  2. data/Gemfile +8 -0
  3. data/README.textile +297 -0
  4. data/Rakefile +16 -0
  5. data/lib/roar/client/entity_proxy.rb +58 -0
  6. data/lib/roar/client/proxy.rb +14 -0
  7. data/lib/roar/client/transport.rb +29 -0
  8. data/lib/roar/model.rb +36 -0
  9. data/lib/roar/model/representable.rb +31 -0
  10. data/lib/roar/rails.rb +21 -0
  11. data/lib/roar/rails/controller_methods.rb +71 -0
  12. data/lib/roar/rails/representer_methods.rb +52 -0
  13. data/lib/roar/rails/test_case.rb +43 -0
  14. data/lib/roar/representer.rb +72 -0
  15. data/lib/roar/representer/feature/http_verbs.rb +63 -0
  16. data/lib/roar/representer/feature/hypermedia.rb +43 -0
  17. data/lib/roar/representer/feature/model_representing.rb +88 -0
  18. data/lib/roar/representer/json.rb +32 -0
  19. data/lib/roar/representer/xml.rb +43 -0
  20. data/lib/roar/version.rb +1 -1
  21. data/roar.gemspec +10 -1
  22. data/test/Gemfile +6 -0
  23. data/test/dummy/Rakefile +7 -0
  24. data/test/dummy/app/controllers/albums_controller.rb +27 -0
  25. data/test/dummy/app/controllers/application_controller.rb +4 -0
  26. data/test/dummy/app/helpers/application_helper.rb +2 -0
  27. data/test/dummy/app/models/album.rb +6 -0
  28. data/test/dummy/app/models/song.rb +2 -0
  29. data/test/dummy/app/representers/representer/xml/album.rb +19 -0
  30. data/test/dummy/app/representers/representer/xml/song.rb +9 -0
  31. data/test/dummy/app/views/layouts/application.html.erb +14 -0
  32. data/test/dummy/app/views/musician/featured.html.erb +1 -0
  33. data/test/dummy/app/views/musician/featured_with_block.html.erb +4 -0
  34. data/test/dummy/app/views/musician/hamlet.html.haml +1 -0
  35. data/test/dummy/config.ru +4 -0
  36. data/test/dummy/config/application.rb +20 -0
  37. data/test/dummy/config/boot.rb +10 -0
  38. data/test/dummy/config/database.yml +22 -0
  39. data/test/dummy/config/environment.rb +5 -0
  40. data/test/dummy/config/environments/development.rb +16 -0
  41. data/test/dummy/config/environments/production.rb +46 -0
  42. data/test/dummy/config/environments/test.rb +32 -0
  43. data/test/dummy/config/locales/en.yml +5 -0
  44. data/test/dummy/config/routes.rb +7 -0
  45. data/test/dummy/db/development.sqlite3 +0 -0
  46. data/test/dummy/db/migrate/20110514114753_create_albums.rb +14 -0
  47. data/test/dummy/db/migrate/20110514121228_create_songs.rb +14 -0
  48. data/test/dummy/db/schema.rb +29 -0
  49. data/test/dummy/db/test.sqlite3 +0 -0
  50. data/test/dummy/module - (2011-05-14 15:26:19) +5 -0
  51. data/test/dummy/public/404.html +26 -0
  52. data/test/dummy/public/422.html +26 -0
  53. data/test/dummy/public/500.html +26 -0
  54. data/test/dummy/public/favicon.ico +0 -0
  55. data/test/dummy/script/rails +6 -0
  56. data/test/dummy/tmp/app/cells/blog/post/latest.html.erb +7 -0
  57. data/test/dummy/tmp/app/cells/blog/post_cell.rb +7 -0
  58. data/test/fake_server.rb +80 -0
  59. data/test/http_verbs_test.rb +46 -0
  60. data/test/hypermedia_test.rb +35 -0
  61. data/test/integration_test.rb +122 -0
  62. data/test/json_representer_test.rb +101 -0
  63. data/test/model_representing_test.rb +121 -0
  64. data/test/model_test.rb +50 -0
  65. data/test/order_representers.rb +34 -0
  66. data/test/proxy_test.rb +89 -0
  67. data/test/rails/controller_methods_test.rb +147 -0
  68. data/test/rails/rails_representer_methods_test.rb +32 -0
  69. data/test/representable_test.rb +49 -0
  70. data/test/representer_test.rb +25 -0
  71. data/test/ruby_representation_test.rb +144 -0
  72. data/test/test_helper.rb +45 -0
  73. data/test/test_helper_test.rb +59 -0
  74. data/test/transport_test.rb +34 -0
  75. data/test/xml_hypermedia_test.rb +47 -0
  76. data/test/xml_representer_test.rb +238 -0
  77. 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
@@ -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