roar 0.11.4 → 0.11.5

Sign up to get free protection for your applications and to get access to all the features.
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