roar 0.11.4 → 0.11.5

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 (39) hide show
  1. data/CHANGES.markdown +5 -1
  2. data/Gemfile +1 -1
  3. data/README.textile +22 -21
  4. data/lib/roar/representer.rb +1 -1
  5. data/lib/roar/representer/feature/client.rb +3 -3
  6. data/lib/roar/representer/feature/coercion.rb +1 -1
  7. data/lib/roar/representer/feature/http_verbs.rb +8 -9
  8. data/lib/roar/representer/feature/hypermedia.rb +83 -63
  9. data/lib/roar/representer/json.rb +12 -12
  10. data/lib/roar/representer/json/hal.rb +98 -25
  11. data/lib/roar/representer/transport/faraday.rb +5 -11
  12. data/lib/roar/representer/transport/net_http.rb +5 -5
  13. data/lib/roar/representer/xml.rb +11 -11
  14. data/lib/roar/version.rb +1 -1
  15. data/roar.gemspec +2 -2
  16. data/test/client_test.rb +2 -2
  17. data/test/coercion_feature_test.rb +2 -2
  18. data/test/dummy/app/controllers/albums_controller.rb +6 -6
  19. data/test/dummy/app/models/album.rb +1 -1
  20. data/test/dummy/app/representers/representer/xml/album.rb +3 -3
  21. data/test/dummy/app/representers/representer/xml/song.rb +1 -1
  22. data/test/dummy/config/boot.rb +1 -1
  23. data/test/fake_server.rb +14 -15
  24. data/test/faraday_http_transport_test.rb +25 -10
  25. data/test/hal_json_test.rb +73 -31
  26. data/test/http_verbs_feature_test.rb +12 -12
  27. data/test/hypermedia_feature_test.rb +38 -145
  28. data/test/hypermedia_test.rb +100 -0
  29. data/test/integration_test.rb +22 -22
  30. data/test/json_representer_test.rb +31 -31
  31. data/test/net_http_transport_test.rb +24 -10
  32. data/test/order_representers.rb +11 -11
  33. data/test/rails/controller_methods_test.rb +25 -25
  34. data/test/rails/rails_representer_methods_test.rb +3 -3
  35. data/test/representer_test.rb +6 -6
  36. data/test/test_helper.rb +33 -4
  37. data/test/xml_representer_test.rb +47 -47
  38. metadata +3 -3
  39. data/lib/roar/rails.rb +0 -21
@@ -1,3 +1,7 @@
1
+ ## 0.11.5
2
+
3
+ * Introducing HAL::links method to map arrays of link objects in the HAL format. This completes the HAL/JSON specification.
4
+
1
5
  ## 0.11.4
2
6
 
3
7
  * Links can now return a hash of attributes as `link :self do {:href => fruit_path(self), :title => "Yummy stuff"} end`.
@@ -21,7 +25,7 @@
21
25
  * You can include nil values now in your representations since #property respects :represent_nil => true.
22
26
 
23
27
  * The `:except` option got deprecated in favor of `:exclude`.
24
- * Hyperlinks can now have arbitrary attributes. To render, just provide `#link` with the options
28
+ * Hyperlinks can now have arbitrary attributes. To render, just provide `#link` with the options
25
29
  <code>link :self, :title => "Mee!", "data-remote" => true</code>
26
30
  When parsing, the options are avaible via `OpenStruct` compliant readers.
27
31
  <code>link = Hyperlink.from_json({\"rel\":\"self\",\"data-url\":\"http://self\"} )
data/Gemfile CHANGED
@@ -3,7 +3,7 @@ source "http://rubygems.org"
3
3
  # Specify your gem's dependencies in roar.gemspec
4
4
  gemspec
5
5
 
6
- #gem "representable", :path => "../representable"
6
+ gem "representable", "~> 1.2.9"
7
7
 
8
8
 
9
9
  group :test do
@@ -6,10 +6,10 @@ h2. Introduction
6
6
 
7
7
  Roar is a framework for parsing and rendering REST documents. Nothing more. With Roar, REST documents - also known as representations - are defined using a new concept called representers. Both syntax and semantics are declared in Ruby modules that can be mixed into your domain models, following clean OOP patterns.
8
8
 
9
- Roar comes with built-in JSON, JSON::HAL and XML support. It exposes a highly modular architecture and makes it very simple to add new media types and functionality where needed. Additional features include client HTTP support, coercion, client-side caching, awesome hypermedia support and more. Representers feel pretty well in DCI environments, too.
9
+ Roar comes with built-in JSON, JSON::HAL and XML support. It exposes a highly modular architecture and makes it very simple to add new media types and functionality where needed. Additional features include client HTTP support, coercion, client-side caching, awesome hypermedia support and more. Representers fit pretty well in DCI environments, too.
10
+
11
+ Roar is completely framework-agnostic and loves being used in web kits like Rails, Webmachine, Sinatra, Padrino, etc. Actually, Roar makes it fun designing real, hypermedia-driven, and resource-oriented systems that will even make Steve sleep happily at night so he finally gets some REST!
10
12
 
11
- Roar is completely framework-agnostic and loves being used in web kits like Rails, Webmachine, Sinatra, Padrino, etc. Actually, Roar makes it fun designing real, hypermedia-driven, and resource-oriented systems that will even make Steve sleep happily at night so he finally gets some REST!
12
-
13
13
  h2. Example
14
14
 
15
15
  Say your webshop consists of two completely separated apps. The REST backend, a Sinatra app, serves articles and processes orders. The frontend, being browsed by your clients, is a rich Rails application. It queries the services for articles, renders them nicely and reads or writes orders with REST calls. That being said, the frontend turns out to be a pure REST client.
@@ -34,7 +34,7 @@ pre. { "article": {
34
34
  "title": "Lonestar Beer",
35
35
  "id": 4711,
36
36
  "links":[
37
- { "rel": "self",
37
+ { "rel": "self",
38
38
  "href": "http://articles/lonestarbeer"}
39
39
  ]}
40
40
  }
@@ -66,13 +66,13 @@ require 'roar/representer/feature/hypermedia'
66
66
  module ArticleRepresenter
67
67
  include Roar::Representer::JSON
68
68
  include Roar::Representer::Feature::Hypermedia
69
-
69
+
70
70
  property :title
71
71
  property :id
72
-
72
+
73
73
  link :self do
74
74
  article_url(self)
75
- end
75
+ end
76
76
  end
77
77
  </pre>
78
78
 
@@ -103,14 +103,14 @@ What if we wanted to check an existing order? We'd @GET http://orders/1@, right?
103
103
  {"title": "Lonestar Beer",
104
104
  "id": 666,
105
105
  "links":[
106
- { "rel": "self",
106
+ { "rel": "self",
107
107
  "href": "http://articles/lonestarbeer"}
108
108
  ]}
109
109
  ],
110
110
  "links":[
111
- { "rel": "self",
111
+ { "rel": "self",
112
112
  "href": "http://orders/1"},
113
- { "rel": "items",
113
+ { "rel": "items",
114
114
  "href": "http://orders/1/items"}
115
115
  ]}
116
116
  }
@@ -133,20 +133,20 @@ module OrderRepresenter
133
133
 
134
134
  property :id
135
135
  property :client_id
136
-
136
+
137
137
  collection :articles, :class => Article
138
-
138
+
139
139
  link :self do
140
140
  order_url(represented)
141
141
  end
142
-
142
+
143
143
  link :items do
144
144
  items_url
145
- end
145
+ end
146
146
  end
147
147
  </pre>
148
148
 
149
- Representers don't have to be in modules, but can be
149
+ Representers don't have to be in modules, but can be
150
150
 
151
151
  The declarative @#collection@ method lets us define compositions of representers.
152
152
 
@@ -164,7 +164,7 @@ If we were to implement an endpoint for creating new orders, we'd allow POST to
164
164
  order.to_json
165
165
  end
166
166
  </pre>
167
-
167
+
168
168
  Look how the @#from_json@ method helps extracting data from the incoming document and, again, @#to_json@ returns the freshly created order's representation. Roar's representers are truely working in both directions, rendering and parsing and thus prevent you from redundant knowledge sharing.
169
169
 
170
170
 
@@ -212,9 +212,9 @@ First, check the JSON document we get back from the POST.
212
212
  "client_id": 1337,
213
213
  "articles": [],
214
214
  "links":[
215
- { "rel": "self",
215
+ { "rel": "self",
216
216
  "href": "http://orders/42"},
217
- { "rel": "items",
217
+ { "rel": "items",
218
218
  "href": "http://orders/42/items"}
219
219
  ]}
220
220
  }
@@ -258,7 +258,7 @@ Again, we use hypermedia to retrieve the order's URL. And now, the added article
258
258
  [*Note:* If this looks clumsy - It's just the raw API for representers. You might be interested in the upcoming DSL that simplifys frequent workflows as updating a representer.]
259
259
 
260
260
  <pre>
261
- order.to_attributes #=> {:id => 42, :client_id => 1337,
261
+ order.to_attributes #=> {:id => 42, :client_id => 1337,
262
262
  :articles => [{:title => "Lonestar Beer", :id => 666}]}
263
263
  </pre>
264
264
 
@@ -266,7 +266,7 @@ This is cool, we used REST representers and hypermedia to create an order and fi
266
266
 
267
267
 
268
268
  h3. Using Accessors
269
-
269
+
270
270
  What if the ordering API is going a different way? What if we had to place articles into the order document ourselves, and then PUT this representation to @http://orders/42@? No problem with representers!
271
271
 
272
272
  Here's what could happen in the frontend.
@@ -283,7 +283,8 @@ h2. More Features
283
283
 
284
284
  Be sure to check out the bundled features.
285
285
 
286
- # *Coercion* transforms values to typed objects when parsing a document. Uses virtus.
286
+ # *Coercion* transforms values to typed objects when parsing a document. Uses virtus.
287
+ # *Faraday* support, if you want to use it install the Faraday gem and require 'roar/representer/transport/faraday' and configure 'Roar::Representer::Feature::HttpVerbs.transport_engine = Roar::Representer::Transport::Faraday'
287
288
 
288
289
  h2. What is REST about?
289
290
 
@@ -7,7 +7,7 @@ module Roar
7
7
  include Representable
8
8
  end
9
9
  end
10
-
10
+
11
11
  private
12
12
  def before_serialize(*)
13
13
  end
@@ -6,19 +6,19 @@ module Roar
6
6
  module Feature
7
7
  module Client
8
8
  include HttpVerbs
9
-
9
+
10
10
  def self.extended(base)
11
11
  base.instance_eval do
12
12
  representable_attrs.each do |attr|
13
13
  next unless attr.instance_of? Representable::Definition # ignore hyperlinks etc for now.
14
14
  name = attr.name
15
-
15
+
16
16
  # TODO: could anyone please make this better?
17
17
  instance_eval %Q{
18
18
  def #{name}=(v)
19
19
  @#{name} = v
20
20
  end
21
-
21
+
22
22
  def #{name}
23
23
  @#{name}
24
24
  end
@@ -6,7 +6,7 @@ module Roar::Representer::Feature
6
6
  # class ImmigrantSong
7
7
  # include Roar::Representer::JSON
8
8
  # include Roar::Representer::Feature::Coercion
9
- #
9
+ #
10
10
  # property :composed_at, :type => DateTime, :default => "May 12th, 2012"
11
11
  # end
12
12
  module Coercion
@@ -1,5 +1,4 @@
1
1
  require 'roar/representer/transport/net_http'
2
- require 'roar/representer/transport/faraday' # Do not require here
3
2
 
4
3
  module Roar
5
4
  # Gives HTTP-power to representers. They can serialize, send, process and deserialize HTTP-requests.
@@ -9,13 +8,13 @@ module Roar
9
8
 
10
9
  class << self
11
10
  attr_accessor :transport_engine
12
-
11
+
13
12
  def included(base)
14
13
  base.extend ClassMethods
15
14
  end
16
15
  end
17
16
  self.transport_engine = ::Roar::Representer::Transport::NetHTTP
18
-
17
+
19
18
 
20
19
  module ClassMethods
21
20
  # GETs +url+ with +format+ and returns deserialized represented object.
@@ -23,26 +22,26 @@ module Roar
23
22
  new.get(*args)
24
23
  end
25
24
  end
26
-
27
-
25
+
26
+
28
27
  attr_writer :transport_engine
29
28
  def transport_engine
30
29
  @transport_engine || HttpVerbs.transport_engine
31
30
  end
32
-
31
+
33
32
  # Serializes the object, POSTs it to +url+ with +format+, deserializes the returned document
34
33
  # and updates properties accordingly.
35
34
  def post(url, format)
36
35
  response = http.post_uri(url, serialize, format)
37
36
  handle_response(response)
38
37
  end
39
-
38
+
40
39
  # GETs +url+ with +format+, deserializes the returned document and updates properties accordingly.
41
40
  def get(url, format)
42
41
  response = http.get_uri(url, format)
43
42
  handle_response(response)
44
43
  end
45
-
44
+
46
45
  # Serializes the object, PUTs it to +url+ with +format+, deserializes the returned document
47
46
  # and updates properties accordingly.
48
47
  def put(url, format)
@@ -50,7 +49,7 @@ module Roar
50
49
  handle_response(response)
51
50
  self
52
51
  end
53
-
52
+
54
53
  def patch(url, format)
55
54
  response = http.patch_uri(url, serialize, format)
56
55
  handle_response(response)
@@ -33,65 +33,87 @@ module Roar
33
33
  # "http://orders/#{opts[:id]}"
34
34
  # end
35
35
  #
36
- # model.to_json(:id => 1)
36
+ # model.to_json(:id => 1)
37
37
  module Hypermedia
38
38
  def self.included(base)
39
39
  base.extend ClassMethods
40
40
  end
41
-
41
+
42
+ # TODO: the whole declarative setup in representable should happen on instance level so we don't need to share methods between class and instance.
43
+ module LinksDefinitionMethods
44
+ private
45
+ def create_links_definition
46
+ representable_attrs << links = LinksDefinition.new(*links_definition_options)
47
+ links
48
+ end
49
+
50
+ def links_definition
51
+ representable_attrs.find { |d| d.is_a?(LinksDefinition) } or create_links_definition
52
+ end
53
+ end
54
+ include LinksDefinitionMethods
55
+ def links_definition_options # FIXME: make this unnecessary.
56
+ self.class.links_definition_options
57
+ end
58
+
42
59
  def before_serialize(options={})
43
60
  prepare_links!(options) unless options[:links] == false # DISCUSS: doesn't work when links are already setup (e.g. from #deserialize).
44
61
  super # Representer::Base
45
62
  end
46
-
47
- def links=(link_list)
48
- links.replace(link_list)
49
- end
50
-
63
+
64
+ attr_writer :links
65
+
51
66
  def links
52
67
  @links ||= LinkCollection.new
53
68
  end
54
-
55
- protected
69
+
70
+ def links_array
71
+ links.values # FIXME: move to LinkCollection#to_a.
72
+ end
73
+
74
+ def links_array=(ary)
75
+ # FIXME: move to LinkCollection
76
+ self.links= LinkCollection.new
77
+ ary.each do |lnk|
78
+ self.links[lnk.rel.to_s] = lnk
79
+ end
80
+ end
81
+
82
+ private
56
83
  # Setup hypermedia links by invoking their blocks. Usually called by #serialize.
57
84
  def prepare_links!(*args)
58
- links_def = find_links_definition or return
59
-
60
- links_def.rel2block.each do |config| # config is [{..}, block]
61
- options = config.first
62
- href = run_link_block(config.last, *args) or next
63
- options.merge! href.is_a?(Hash) ? href : {:href => href}
64
-
65
- links.update_link(Feature::Hypermedia::Hyperlink.new(options))
85
+ links_definition.each do |config| # config is [{..}, block]
86
+ options, block = config.first, config.last
87
+ href = run_link_block(block, *args) or next
88
+
89
+ links.add(prepare_link_for(href, options))
66
90
  end
67
91
  end
68
-
92
+
93
+ def prepare_link_for(href, options)
94
+ options.merge! href.is_a?(Hash) ? href : {:href => href}
95
+ Hyperlink.new(options)
96
+ end
97
+
69
98
  def run_link_block(block, *args)
70
99
  instance_exec(*args, &block)
71
100
  end
72
-
73
- def find_links_definition
74
- representable_attrs.find { |d| d.is_a?(LinksDefinition) }
75
- end
76
-
77
-
78
- class LinkCollection < Array
101
+
102
+
103
+ class LinkCollection < Hash
104
+ # DISCUSS: make Link#rel return string always.
79
105
  def [](rel)
80
- link = find { |l| l.rel.to_s == rel.to_s } and return link
106
+ self.fetch(rel.to_s, nil)
81
107
  end
82
-
83
- # Checks if the link is already contained by querying for its +rel+.
84
- # If so, it gets replaced. Otherwise, the new link gets appended.
85
- def update_link(link)
86
- if i = find_index { |l| l.rel.to_s == link.rel.to_s }
87
- return self[i] = link
88
- end
89
- self << link
108
+
109
+ def add(link) # FIXME: use Hash API.
110
+ self[link.rel.to_s] = link
90
111
  end
91
112
  end
92
-
93
-
113
+
114
+
94
115
  module ClassMethods
116
+ include LinksDefinitionMethods
95
117
  # Declares a hypermedia link in the document.
96
118
  #
97
119
  # Example:
@@ -103,48 +125,46 @@ module Roar
103
125
  # The block is executed in instance context, so you may call properties or other accessors.
104
126
  # Note that you're free to put decider logic into #link blocks, too.
105
127
  def link(options, &block)
106
- links = find_links_definition || create_links
107
-
108
128
  options = {:rel => options} if options.is_a?(Symbol)
109
- links.rel2block << [options, block]
110
- end
111
-
112
- def find_links_definition
113
- representable_attrs.find { |d| d.is_a?(LinksDefinition) }
114
- end
115
-
116
- private
117
- def create_links
118
- LinksDefinition.new(*links_definition_options).tap do |links|
119
- representable_attrs << links
120
- end
129
+ links_definition << [options, block]
121
130
  end
122
131
  end
123
-
124
-
132
+
133
+
125
134
  class LinksDefinition < Representable::Definition
126
- # TODO: hide rel2block in interface.
127
- attr_writer :rel2block
128
-
129
- def rel2block
130
- @rel2block ||= []
135
+ include Enumerable
136
+
137
+ attr_accessor :rel2block
138
+ def initialize(*)
139
+ super
140
+ @rel2block = []
141
+ end
142
+
143
+ def <<(args)
144
+ rel2block << args
145
+ end
146
+
147
+ def each(*args, &block)
148
+ rel2block.each(*args, &block)
131
149
  end
132
-
150
+
151
+ # DISCUSS: where do we need this?
133
152
  def clone
134
153
  super.tap { |d| d.rel2block = rel2block.clone }
135
154
  end
136
155
  end
137
-
138
-
156
+
157
+
139
158
  require "ostruct"
140
159
  # An abstract hypermedia link with arbitrary attributes.
141
160
  class Hyperlink < OpenStruct
142
161
  include Enumerable
143
-
162
+
144
163
  def each(*args, &block)
145
164
  marshal_dump.each(*args, &block)
146
165
  end
147
-
166
+
167
+ # FIXME: do we need this method any longer?
148
168
  def replace(hash)
149
169
  # #marshal_load requires symbol keys: http://apidock.com/ruby/v1_9_3_125/OpenStruct/marshal_load
150
170
  marshal_load(hash.inject({}) { |h, (k,v)| h[k.to_sym] = v; h })
@@ -153,4 +173,4 @@ module Roar
153
173
  end
154
174
  end
155
175
  end
156
- end
176
+ end