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 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