roar 0.12.9 → 1.0.0.beta1

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 (64) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/.travis.yml +8 -10
  4. data/CHANGES.markdown +24 -2
  5. data/Gemfile +5 -2
  6. data/README.markdown +132 -28
  7. data/TODO.markdown +9 -7
  8. data/examples/example.rb +2 -0
  9. data/examples/example_server.rb +19 -3
  10. data/gemfiles/Gemfile.representable-2.0 +5 -0
  11. data/gemfiles/Gemfile.representable-2.1 +5 -0
  12. data/lib/roar.rb +1 -1
  13. data/lib/roar/client.rb +38 -0
  14. data/lib/roar/{representer/feature/coercion.rb → coercion.rb} +2 -1
  15. data/lib/roar/decorator.rb +3 -11
  16. data/lib/roar/http_verbs.rb +88 -0
  17. data/lib/roar/hypermedia.rb +174 -0
  18. data/lib/roar/json.rb +55 -0
  19. data/lib/roar/json/collection.rb +3 -0
  20. data/lib/roar/{representer/json → json}/collection_json.rb +20 -20
  21. data/lib/roar/{representer/json → json}/hal.rb +33 -31
  22. data/lib/roar/json/hash.rb +3 -0
  23. data/lib/roar/json/json_api.rb +208 -0
  24. data/lib/roar/representer.rb +3 -36
  25. data/lib/roar/transport/faraday.rb +49 -0
  26. data/lib/roar/transport/net_http.rb +57 -0
  27. data/lib/roar/transport/net_http/request.rb +72 -0
  28. data/lib/roar/version.rb +1 -1
  29. data/lib/roar/xml.rb +54 -0
  30. data/roar.gemspec +5 -4
  31. data/test/client_test.rb +3 -3
  32. data/test/coercion_feature_test.rb +6 -3
  33. data/test/collection_json_test.rb +8 -10
  34. data/test/decorator_test.rb +27 -15
  35. data/test/faraday_http_transport_test.rb +13 -15
  36. data/test/hal_json_test.rb +16 -16
  37. data/test/hal_links_test.rb +3 -3
  38. data/test/http_verbs_test.rb +17 -22
  39. data/test/hypermedia_feature_test.rb +23 -45
  40. data/test/hypermedia_test.rb +11 -23
  41. data/test/integration/band_representer.rb +2 -2
  42. data/test/integration/runner.rb +4 -3
  43. data/test/integration/server.rb +13 -2
  44. data/test/integration/ssl_server.rb +1 -1
  45. data/test/json_api_test.rb +336 -0
  46. data/test/json_representer_test.rb +16 -12
  47. data/test/lib/runner.rb +134 -0
  48. data/test/lonely_test.rb +9 -0
  49. data/test/net_http_transport_test.rb +4 -4
  50. data/test/representer_test.rb +2 -2
  51. data/test/{lib/roar/representer/transport/net_http/request_test.rb → ssl_client_certs_test.rb} +43 -5
  52. data/test/test_helper.rb +12 -5
  53. data/test/xml_representer_test.rb +26 -166
  54. metadata +49 -29
  55. data/gemfiles/Gemfile.representable-1.7 +0 -6
  56. data/gemfiles/Gemfile.representable-1.8 +0 -6
  57. data/lib/roar/representer/feature/client.rb +0 -39
  58. data/lib/roar/representer/feature/http_verbs.rb +0 -95
  59. data/lib/roar/representer/feature/hypermedia.rb +0 -175
  60. data/lib/roar/representer/json.rb +0 -67
  61. data/lib/roar/representer/transport/faraday.rb +0 -50
  62. data/lib/roar/representer/transport/net_http.rb +0 -59
  63. data/lib/roar/representer/transport/net_http/request.rb +0 -75
  64. data/lib/roar/representer/xml.rb +0 -61
@@ -57,4 +57,4 @@ class SslServer < Sinatra::Base
57
57
  end
58
58
  server = ::Rack::Handler::WEBrick
59
59
 
60
- server.run(SslServer, webrick_options)
60
+ server.run(SslServer, webrick_options)
@@ -0,0 +1,336 @@
1
+ require 'test_helper'
2
+ require 'roar/json/json_api'
3
+
4
+ class JSONAPITest < MiniTest::Spec
5
+ let(:song) {
6
+ s = OpenStruct.new(
7
+ bla: "halo",
8
+ id: "1",
9
+ title: 'Computadores Fazem Arte',
10
+ album: OpenStruct.new(id: 9, title: "Hackers"),
11
+ :album_id => "9",
12
+ :musician_ids => ["1","2"],
13
+ :composer_id => "10",
14
+ :listener_ids => ["8"],
15
+ musicians: [OpenStruct.new(id: 1, name: "Eddie Van Halen"), OpenStruct.new(id: 2, name: "Greg Howe")]
16
+ )
17
+
18
+ }
19
+
20
+ # minimal resource, singular
21
+ module MinimalSingular
22
+ include Roar::JSON::JSONAPI
23
+ type :songs
24
+
25
+ property :id
26
+ end
27
+
28
+ class MinimalSingularDecorator < Roar::Decorator
29
+ include Roar::JSON::JSONAPI
30
+ type :songs
31
+
32
+ property :id
33
+ end
34
+
35
+ [MinimalSingular, MinimalSingularDecorator].each do |representer|
36
+ describe "minimal singular with #{representer}" do
37
+ subject { representer.prepare(song) }
38
+
39
+ it { subject.to_json.must_equal "{\"songs\":{\"id\":\"1\"}}" }
40
+ it { subject.from_json("{\"songs\":{\"id\":\"2\"}}").id.must_equal "2" }
41
+ end
42
+ end
43
+
44
+
45
+
46
+ module Singular
47
+ include Roar::JSON::JSONAPI
48
+ type :songs
49
+
50
+ property :id
51
+ property :title
52
+
53
+ # local per-model "id" links
54
+ links do
55
+ property :album_id, :as => :album
56
+ collection :musician_ids, :as => :musicians
57
+ end
58
+ has_one :composer
59
+ has_many :listeners
60
+
61
+
62
+ # global document links.
63
+ link "songs.album" do
64
+ {
65
+ type: "album",
66
+ href: "http://example.com/albums/{songs.album}"
67
+ }
68
+ end
69
+
70
+ compound do
71
+ property :album do
72
+ property :title
73
+ end
74
+
75
+ collection :musicians do
76
+ property :name
77
+ end
78
+ end
79
+ end
80
+
81
+ class SingularDecorator < Roar::Decorator
82
+ include Roar::JSON::JSONAPI
83
+ type :songs
84
+
85
+ property :id
86
+ property :title
87
+
88
+ # local per-model "id" links
89
+ links do
90
+ property :album_id, :as => :album
91
+ collection :musician_ids, :as => :musicians
92
+ end
93
+ has_one :composer
94
+ has_many :listeners
95
+
96
+
97
+ # global document links.
98
+ link "songs.album" do
99
+ {
100
+ type: "album",
101
+ href: "http://example.com/albums/{songs.album}"
102
+ }
103
+ end
104
+
105
+ compound do
106
+ property :album do
107
+ property :title
108
+ end
109
+
110
+ collection :musicians do
111
+ property :name
112
+ end
113
+ end
114
+ end
115
+
116
+ [Singular, SingularDecorator].each do |representer|
117
+ describe "singular with #{representer}" do
118
+ subject { song.extend(Singular) }
119
+
120
+ let (:document) do
121
+ {
122
+ "songs" => {
123
+ "id" => "1",
124
+ "title" => "Computadores Fazem Arte",
125
+ "links" => {
126
+ "album" => "9",
127
+ "musicians" => [ "1", "2" ],
128
+ "composer"=>"10",
129
+ "listeners"=>["8"]
130
+ }
131
+ },
132
+ "links" => {
133
+ "songs.album"=> {
134
+ "href"=>"http://example.com/albums/{songs.album}", "type"=>"album"
135
+ }
136
+ },
137
+ "linked" => {
138
+ "album"=> [{"title"=>"Hackers"}],
139
+ "musicians"=> [
140
+ {"name"=>"Eddie Van Halen"},
141
+ {"name"=>"Greg Howe"}
142
+ ]
143
+ }
144
+ }
145
+ end
146
+
147
+ # to_hash
148
+ it do
149
+ subject.to_hash.must_equal document
150
+ end
151
+
152
+ # #to_json
153
+ it do
154
+ subject.to_json.must_equal JSON.generate(document)
155
+ end
156
+
157
+ # #from_json
158
+ it do
159
+ song = OpenStruct.new.extend(Singular)
160
+ song.from_json(
161
+ JSON.generate(
162
+ {
163
+ "songs" => {
164
+ "id" => "1",
165
+ "title" => "Computadores Fazem Arte",
166
+ "links" => {
167
+ "album" => "9",
168
+ "musicians" => [ "1", "2" ],
169
+ "composer"=>"10",
170
+ "listeners"=>["8"]
171
+ }
172
+ },
173
+ "links" => {
174
+ "songs.album"=> {
175
+ "href"=>"http://example.com/albums/{songs.album}", "type"=>"album"
176
+ }
177
+ }
178
+ }
179
+ )
180
+ )
181
+
182
+ song.id.must_equal "1"
183
+ song.title.must_equal "Computadores Fazem Arte"
184
+ song.album_id.must_equal "9"
185
+ song.musician_ids.must_equal ["1", "2"]
186
+ song.composer_id.must_equal "10"
187
+ song.listener_ids.must_equal ["8"]
188
+ end
189
+ end
190
+ end
191
+
192
+
193
+ # collection with links
194
+ [Singular, SingularDecorator].each do |representer|
195
+ describe "collection with links and compound" do
196
+ subject { Singular.for_collection.prepare([song, song]) }
197
+
198
+ let (:document) do
199
+ {
200
+ "songs" => [
201
+ {
202
+ "id" => "1",
203
+ "title" => "Computadores Fazem Arte",
204
+ "links" => {
205
+ "album" => "9",
206
+ "musicians" => [ "1", "2" ],
207
+ "composer"=>"10",
208
+ "listeners"=>["8"]
209
+ }
210
+ }, {
211
+ "id" => "1",
212
+ "title" => "Computadores Fazem Arte",
213
+ "links" => {
214
+ "album" => "9",
215
+ "musicians" => [ "1", "2" ],
216
+ "composer"=>"10",
217
+ "listeners"=>["8"]
218
+ }
219
+ }
220
+ ],
221
+ "links" => {
222
+ "songs.album" => {
223
+ "href" => "http://example.com/albums/{songs.album}",
224
+ "type" => "album" # DISCUSS: does that have to be albums ?
225
+ },
226
+ },
227
+ "linked"=>{
228
+ "album" =>[{"title"=>"Hackers"}], # only once!
229
+ "musicians"=>[{"name"=>"Eddie Van Halen"}, {"name"=>"Greg Howe"}]
230
+ }
231
+ }
232
+ end
233
+
234
+ # to_hash
235
+ it do
236
+ subject.to_hash.must_equal document
237
+ end
238
+
239
+ # #to_json
240
+ it { subject.to_json.must_equal JSON.generate(document) }
241
+ end
242
+
243
+
244
+ # from_json
245
+ it do
246
+ song1, song2 = Singular.for_collection.prepare([OpenStruct.new, OpenStruct.new]).from_json(
247
+ JSON.generate(
248
+ {
249
+ "songs" => [
250
+ {
251
+ "id" => "1",
252
+ "title" => "Computadores Fazem Arte",
253
+ "links" => {
254
+ "album" => "9",
255
+ "musicians" => [ "1", "2" ],
256
+ "composer"=>"10",
257
+ "listeners"=>["8"]
258
+ },
259
+ },
260
+ {
261
+ "id" => "2",
262
+ "title" => "Talking To Remind Me",
263
+ "links" => {
264
+ "album" => "1",
265
+ "musicians" => [ "3", "4" ],
266
+ "composer"=>"2",
267
+ "listeners"=>["6"]
268
+ }
269
+ },
270
+ ],
271
+ "links" => {
272
+ "songs.album"=> {
273
+ "href"=>"http://example.com/albums/{songs.album}", "type"=>"album"
274
+ }
275
+ }
276
+ }
277
+ )
278
+ )
279
+
280
+ song1.id.must_equal "1"
281
+ song1.title.must_equal "Computadores Fazem Arte"
282
+ song1.album_id.must_equal "9"
283
+ song1.musician_ids.must_equal ["1", "2"]
284
+ song1.composer_id.must_equal "10"
285
+ song1.listener_ids.must_equal ["8"]
286
+
287
+ song2.id.must_equal "2"
288
+ song2.title.must_equal "Talking To Remind Me"
289
+ song2.album_id.must_equal "1"
290
+ song2.musician_ids.must_equal ["3", "4"]
291
+ song2.composer_id.must_equal "2"
292
+ song2.listener_ids.must_equal ["6"]
293
+ end
294
+ end
295
+
296
+
297
+ class CollectionWithoutCompound < self
298
+ module Representer
299
+ include Roar::JSON::JSONAPI
300
+ type :songs
301
+
302
+ property :id
303
+ property :title
304
+
305
+ # local per-model "id" links
306
+ links do
307
+ property :album_id, :as => :album
308
+ collection :musician_ids, :as => :musicians
309
+ end
310
+ has_one :composer
311
+ has_many :listeners
312
+
313
+
314
+ # global document links.
315
+ link "songs.album" do
316
+ {
317
+ type: "album",
318
+ href: "http://example.com/albums/{songs.album}"
319
+ }
320
+ end
321
+ end
322
+
323
+ subject { [song, song].extend(Singular.for_collection) }
324
+
325
+ # to_json
326
+ it do
327
+ subject.extend(Representer.for_collection).to_hash.must_equal(
328
+ {
329
+ "songs"=>[{"id"=>"1", "title"=>"Computadores Fazem Arte", "links"=>{"album"=>"9", "musicians"=>["1", "2"], "composer"=>"10", "listeners"=>["8"]}}, {"id"=>"1", "title"=>"Computadores Fazem Arte", "links"=>{"album"=>"9", "musicians"=>["1", "2"], "composer"=>"10", "listeners"=>["8"]}}],
330
+ "links"=>{"songs.album"=>{"href"=>"http://example.com/albums/{songs.album}", "type"=>"album"}
331
+ }
332
+ }
333
+ )
334
+ end
335
+ end
336
+ end
@@ -1,14 +1,18 @@
1
1
  require 'test_helper'
2
2
 
3
3
  require "test_xml/mini_test"
4
- require "roar/representer/json"
4
+ require "roar/json"
5
5
 
6
6
  class JsonRepresenterTest < MiniTest::Spec
7
7
  class Order
8
- include Roar::Representer::JSON
8
+ include Roar::JSON
9
9
  property :id
10
10
  property :pending
11
11
  attr_accessor :id, :pending
12
+
13
+ def id=(v) # in ruby 2.2, #id= is not there, all at sudden. what *is* that?
14
+ @id=v
15
+ end
12
16
  end
13
17
 
14
18
 
@@ -63,7 +67,7 @@ class JsonRepresenterTest < MiniTest::Spec
63
67
 
64
68
  describe "JSON.from_json" do
65
69
  it "is aliased by #deserialize" do
66
- @order = Order.deserialize('{"id":1}')
70
+ @order = Order.new.deserialize('{"id":1}')
67
71
  assert_equal 1, @order.id
68
72
  end
69
73
  end
@@ -74,7 +78,7 @@ end
74
78
  class JsonHyperlinkRepresenterTest
75
79
  describe "API" do
76
80
  before do
77
- @link = Roar::Representer::Feature::Hypermedia::Hyperlink.new.extend(Roar::Representer::JSON::HyperlinkRepresenter).from_json(
81
+ @link = Roar::Hypermedia::Hyperlink.new.extend(Roar::JSON::HyperlinkRepresenter).from_json(
78
82
  '{"rel":"self", "href":"http://roar.apotomo.de", "media":"web"}')
79
83
  end
80
84
 
@@ -101,8 +105,8 @@ class JsonHypermediaTest
101
105
  before do
102
106
  @c = Class.new do
103
107
  include AttributesConstructor
104
- include Roar::Representer::JSON
105
- include Roar::Representer::Feature::Hypermedia
108
+ include Roar::JSON
109
+ include Roar::Hypermedia
106
110
  attr_accessor :id, :self, :next
107
111
 
108
112
  property :id
@@ -115,14 +119,14 @@ class JsonHypermediaTest
115
119
  end
116
120
 
117
121
  it "responds to #links" do
118
- @r.links.must_equal({})
122
+ @r.links.must_equal nil
119
123
  end
120
124
 
121
125
  it "extracts links from JSON" do
122
- r = @c.from_json('{"links":[{"rel":"self","href":"http://self"}]}')
126
+ r = @r.from_json('{"links":[{"rel":"self","href":"http://self"}]}')
123
127
 
124
- assert_equal 1, r.links_array.size
125
- link = r.links_array.first
128
+ assert_equal 1, r.links.size
129
+ link = r.links["self"]
126
130
  assert_equal(["self", "http://self"], [link.rel, link.href])
127
131
  end
128
132
 
@@ -132,8 +136,8 @@ class JsonHypermediaTest
132
136
 
133
137
  it "doesn't render links when empty" do
134
138
  assert_equal("{\"links\":[]}", Class.new do
135
- include Roar::Representer::JSON
136
- include Roar::Representer::Feature::Hypermedia
139
+ include Roar::JSON
140
+ include Roar::Hypermedia
137
141
 
138
142
  link :self do nil end
139
143
  link :next do false end
@@ -0,0 +1,134 @@
1
+ require 'open-uri'
2
+ require 'net/http'
3
+ require 'timeout'
4
+
5
+ # Helps you spinning up and shutting down your own sinatra app. This is especially helpful for running
6
+ # real network tests against a sinatra backend.
7
+ #
8
+ # The backend server could look like the following (in test/server.rb).
9
+ #
10
+ # require "sinatra"
11
+ #
12
+ # get "/" do
13
+ # "Cheers from test server"
14
+ # end
15
+ #
16
+ # get "/ping" do
17
+ # "1"
18
+ # end
19
+ #
20
+ # Note that you need to implement a ping action for internal use.
21
+ #
22
+ # Next, you need to write your runner.
23
+ #
24
+ # require 'sinatra/runner'
25
+ #
26
+ # class Runner < Sinatra::Runner
27
+ # def app_file
28
+ # File.expand_path("../server.rb", __FILE__)
29
+ # end
30
+ # end
31
+ #
32
+ # Override Runner#app_file, #command, #port, #protocol and #ping_path for customization.
33
+ #
34
+ # Whereever you need this test backend, here's how you manage it. The following example assumes you
35
+ # have a test in your app that needs to be run against your test backend.
36
+ #
37
+ # runner = ServerRunner.new
38
+ # runner.run
39
+ #
40
+ # # ..tests against localhost:4567 here..
41
+ #
42
+ # runner.kill
43
+ #
44
+ # For an example, check https://github.com/apotonick/roar/blob/master/test/test_helper.rb
45
+ module Sinatra
46
+ class Runner
47
+ def app_file
48
+ File.expand_path("../server.rb", __FILE__)
49
+ end
50
+
51
+ def run
52
+ #puts command
53
+ @pipe = start
54
+ @started = Time.now
55
+ warn "#{server} up and running on port #{port}" if ping
56
+ end
57
+
58
+ def kill
59
+ return unless pipe
60
+ Process.kill("KILL", pipe.pid)
61
+ rescue NotImplementedError
62
+ system "kill -9 #{pipe.pid}"
63
+ rescue Errno::ESRCH
64
+ end
65
+
66
+ def get(url)
67
+ Timeout.timeout(1) { get_url("#{protocol}://127.0.0.1:#{port}#{url}") }
68
+ end
69
+
70
+ def log
71
+ @log ||= ""
72
+ loop { @log << pipe.read_nonblock(1) }
73
+ rescue Exception
74
+ @log
75
+ end
76
+
77
+ private
78
+ attr_accessor :pipe
79
+
80
+ def start
81
+ IO.popen(command)
82
+ end
83
+
84
+ def command # to be overwritten
85
+ "bundle exec ruby #{app_file} -p #{port} -e production"
86
+ end
87
+
88
+ def ping(timeout=30)
89
+ loop do
90
+ return if alive?
91
+ if Time.now - @started > timeout
92
+ $stderr.puts command, log
93
+ fail "timeout"
94
+ else
95
+ sleep 0.1
96
+ end
97
+ end
98
+ end
99
+
100
+ def alive?
101
+ 3.times { get(ping_path) }
102
+ true
103
+ rescue Errno::ECONNREFUSED, Errno::ECONNRESET, EOFError, SystemCallError, OpenURI::HTTPError, Timeout::Error
104
+ false
105
+ end
106
+
107
+ def ping_path # to be overwritten
108
+ '/ping'
109
+ end
110
+
111
+ def port # to be overwritten
112
+ 4567
113
+ end
114
+
115
+ def protocol
116
+ "http"
117
+ end
118
+
119
+ def get_url(url)
120
+ uri = URI.parse(url)
121
+
122
+ return uri.read unless protocol == "https"
123
+ get_https_url(uri)
124
+ end
125
+
126
+ def get_https_url(uri)
127
+ http = Net::HTTP.new(uri.host, uri.port)
128
+ http.use_ssl = true
129
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE
130
+ request = Net::HTTP::Get.new(uri.request_uri)
131
+ http.request(request).body
132
+ end
133
+ end
134
+ end