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 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 => "/home/nick/projects/representable"
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.0.0"
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 < Roar::Representer::JSON
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 < Roar::Representer::JSON
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/xml_representer_test.rb', 'test/model_representing_test.rb', 'test/representer_test.rb', 'test/transport_test.rb', 'test/http_verbs_test.rb', 'test/integration_test.rb', 'test/json_representer_test.rb', 'test/xml_hypermedia_test.rb', 'test/hypermedia_test.rb']
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
- :tag => singular_name)
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
- # Used in Models as convenience, ActiveResource-like methods. # FIXME: currently this is meant for clients like Representers.
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
- extend ActiveSupport::Concern
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.accessor))
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.accessor))
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 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
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=(links)
12
- @links = LinkCollection.new(links)
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 an embedded hypermedia link.
58
+ # Defines a hypermedia link to be embedded in the document.
28
59
  def link(rel, &block)
29
- unless links = representable_attrs.find { |d| d.is_a?(LinksDefinition)}
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(accessor)
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[accessor] = value
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: 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)
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
- class JSON < Base
8
- include Representable::JSON
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
- def serialize
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
- def self.deserialize(json)
15
- from_json(json)
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 < self
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
@@ -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
- # Basic work-flow
7
- # in: * representer parses representation
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
- class XML < Base
15
- include Representable::XML
16
-
17
- def serialize
18
- to_xml.serialize
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
- def self.deserialize(xml)
22
- from_xml(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 < self
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