alf-rest 0.14.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (44) hide show
  1. data/CHANGELOG.md +5 -0
  2. data/Gemfile +21 -0
  3. data/Gemfile.lock +71 -0
  4. data/LICENCE.md +22 -0
  5. data/Manifest.txt +12 -0
  6. data/README.md +11 -0
  7. data/Rakefile +11 -0
  8. data/lib/alf-rest.rb +1 -0
  9. data/lib/alf/rest.rb +30 -0
  10. data/lib/alf/rest/alf-ext/renderer.rb +16 -0
  11. data/lib/alf/rest/alf-ext/unit_of_work.rb +3 -0
  12. data/lib/alf/rest/alf-ext/unit_of_work/delete.rb +21 -0
  13. data/lib/alf/rest/alf-ext/unit_of_work/insert.rb +22 -0
  14. data/lib/alf/rest/alf-ext/unit_of_work/update.rb +22 -0
  15. data/lib/alf/rest/config.rb +55 -0
  16. data/lib/alf/rest/errors.rb +5 -0
  17. data/lib/alf/rest/helpers.rb +68 -0
  18. data/lib/alf/rest/loader.rb +5 -0
  19. data/lib/alf/rest/middleware.rb +19 -0
  20. data/lib/alf/rest/payload.rb +12 -0
  21. data/lib/alf/rest/payload/client.rb +22 -0
  22. data/lib/alf/rest/request.rb +43 -0
  23. data/lib/alf/rest/response.rb +21 -0
  24. data/lib/alf/rest/test.rb +16 -0
  25. data/lib/alf/rest/test/client.rb +83 -0
  26. data/lib/alf/rest/test/ext.rb +7 -0
  27. data/lib/alf/rest/test/steps.rb +286 -0
  28. data/lib/alf/rest/version.rb +16 -0
  29. data/lib/sinatra/alf-rest.rb +73 -0
  30. data/spec/fixtures/sap.db +0 -0
  31. data/spec/integration/sinatra/rest_get/test_accept.rb +98 -0
  32. data/spec/integration/spec_helper.rb +27 -0
  33. data/spec/test_rest.rb +10 -0
  34. data/spec/unit/config/test_database.rb +28 -0
  35. data/spec/unit/config/test_viewpoint.rb +18 -0
  36. data/spec/unit/ext/renderer/test_from_http_accept.rb +50 -0
  37. data/spec/unit/ext/renderer/test_supported_media_types.rb +10 -0
  38. data/spec/unit/middleware/test_behavior.rb +55 -0
  39. data/spec/unit/request/test_to_relation.rb +56 -0
  40. data/spec/unit/spec_helper.rb +35 -0
  41. data/spec/unit/test_rest.rb +10 -0
  42. data/tasks/gem.rake +8 -0
  43. data/tasks/test.rake +17 -0
  44. metadata +251 -0
@@ -0,0 +1,12 @@
1
+ require_relative 'payload/client'
2
+ module Alf
3
+ module Rest
4
+ class Payload
5
+
6
+ def initialize(*args)
7
+ raise "Payload has been removed from Alf::Rest"
8
+ end
9
+
10
+ end # class Payload
11
+ end # module Rest
12
+ end # module Alf
@@ -0,0 +1,22 @@
1
+ module Alf
2
+ module Rest
3
+ class Payload
4
+ module Client
5
+
6
+ def payload
7
+ JSON::load(last_response.body)
8
+ end
9
+
10
+ def to_payload(h)
11
+ case c = headers["Content-Type"]
12
+ when /urlencoded/ then URI.escape(h.map{|k,v| "#{k}=#{v}"}.join('&'))
13
+ when /json/ then ::JSON.dump(body)
14
+ else
15
+ raise "Unable to generate payload for Content-Type `#{c}`"
16
+ end
17
+ end
18
+
19
+ end # module Client
20
+ end # class Payload
21
+ end # module Rest
22
+ end # module Alf
@@ -0,0 +1,43 @@
1
+ module Alf
2
+ module Rest
3
+ class Request < Rack::Request
4
+
5
+ def initialize(env, heading)
6
+ super(env)
7
+ @heading = Heading.coerce(heading)
8
+ end
9
+ attr_reader :heading
10
+
11
+ def to_relation
12
+ relation = Relation.coerce(each.to_a)
13
+ commons = heading.to_attr_list & relation.heading.to_attr_list
14
+ relation.project(commons).coerce(heading.project(commons))
15
+ end
16
+
17
+ def to_tuple
18
+ to_relation.tuple_extract
19
+ end
20
+
21
+ private
22
+
23
+ def each(&bl)
24
+ return to_enum unless block_given?
25
+ if form_data?
26
+ yield(Support.symbolize_keys(self.POST))
27
+ else
28
+ Alf::Reader.by_mime_type(media_type, body_io).each(&bl)
29
+ end
30
+ end
31
+
32
+ def body_io
33
+ case body
34
+ when IO, StringIO then body
35
+ else
36
+ body.rewind if body.respond_to?(:rewind)
37
+ StringIO.new(body.read)
38
+ end
39
+ end
40
+
41
+ end # class Request
42
+ end # module Rest
43
+ end # module Alf
@@ -0,0 +1,21 @@
1
+ module Alf
2
+ module Rest
3
+ class Response < Rack::Response
4
+
5
+ def initialize(env = {})
6
+ accept = env['HTTP_ACCEPT'] || 'application/json'
7
+ if @renderer = Alf::Renderer.from_http_accept(accept)
8
+ super()
9
+ self['Content-Type'] = @renderer.mime_type
10
+ else
11
+ raise Rack::Accept::Context::AcceptError, accept
12
+ end
13
+ end
14
+
15
+ def body=(payload)
16
+ super(@renderer.new(payload))
17
+ end
18
+
19
+ end # class Response
20
+ end # module Rest
21
+ end # module Alf
@@ -0,0 +1,16 @@
1
+ require 'alf-rest'
2
+ module Alf
3
+ module Rest
4
+ module Test
5
+
6
+ def self.config
7
+ @config ||= Config.new.tap{|c|
8
+ yield(c) if block_given?
9
+ }
10
+ end
11
+
12
+ end # module Test
13
+ end # module Rest
14
+ end # module Alf
15
+ require_relative 'test/ext'
16
+ require_relative 'test/client'
@@ -0,0 +1,83 @@
1
+ module Alf
2
+ module Rest
3
+ module Test
4
+ class Client
5
+ include ::Rack::Test::Methods
6
+ include Payload::Client
7
+
8
+ def initialize(config)
9
+ @config = config
10
+ @database = config.database
11
+ @db_conn = config.database.connection
12
+ @global_headers = { "Content-Type" => "application/json" }
13
+ @global_parameters = { }
14
+ reset
15
+ end
16
+ attr_reader :database, :db_conn
17
+ attr_accessor :global_parameters
18
+ attr_accessor :global_headers
19
+ attr_accessor :body
20
+ attr_accessor :parameters
21
+
22
+ def reset
23
+ self.body = nil
24
+ self.parameters = {}
25
+ global_headers.each{|k,v| header(k,v) }
26
+ end
27
+
28
+ def disconnect
29
+ db_conn.close if db_conn
30
+ end
31
+
32
+ def with_database
33
+ yield(database)
34
+ end
35
+
36
+ def with_db_conn(&bl)
37
+ yield(db_conn)
38
+ end
39
+
40
+ def with_relvar(*args, &bl)
41
+ with_db_conn do |db_conn|
42
+ yield(db_conn.relvar(*args))
43
+ end
44
+ end
45
+
46
+ def headers
47
+ current_session.headers
48
+ end
49
+
50
+ def global_header(k, v)
51
+ global_headers[k] = v
52
+ end
53
+
54
+ def parameter(k, v)
55
+ parameters[k] = v
56
+ end
57
+
58
+ [:get, :patch, :put, :post, :delete].each do |m|
59
+ define_method(m) do |url, &bl|
60
+ # build the url
61
+ url ||= ""
62
+ url += (url =~ /\?/ ? "&" : "?")
63
+ url += hash2uri(global_parameters.merge(parameters))
64
+ args = [url]
65
+
66
+ # encode and set the body
67
+ args << to_payload(body)
68
+
69
+ # make the call
70
+ super(*args, &bl).tap{ reset }
71
+ end
72
+ end
73
+
74
+ private
75
+
76
+ def hash2uri(h)
77
+ URI.escape(h.map{|k,v| "#{k}=#{v}"}.join('&'))
78
+ end
79
+
80
+ end # class Client
81
+ end # module Test
82
+ end # module Rest
83
+ end # module Alf
@@ -0,0 +1,7 @@
1
+ module Rack
2
+ module Test
3
+ class Session
4
+ attr_reader :headers
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,286 @@
1
+ def client
2
+ @client
3
+ end
4
+
5
+ Before do
6
+ @client ||= Alf::Rest::Test::Client.new(Alf::Rest::Test.config)
7
+ end
8
+
9
+ After do
10
+ client.disconnect
11
+ end
12
+
13
+ Given /^the (.*?) relvar is empty$/ do |relvar|
14
+ client.with_relvar(relvar) do |rv|
15
+ rv.delete
16
+ end
17
+ end
18
+
19
+ Given /^the (.*?) relvar has the following value:$/ do |relvar,table|
20
+ client.with_relvar(relvar) do |rv|
21
+ rv.affect Relation(rv.heading.coerce(table.hashes))
22
+ end
23
+ end
24
+
25
+ Given /^the following (.*?) relation is mapped under (.*):$/ do |prototype, url, table|
26
+ client.with_relvar(prototype) do |rv|
27
+ rv.affect Relation(rv.heading.coerce(table.hashes))
28
+ end
29
+ app.rest_get(url) do
30
+ relvar(prototype)
31
+ end
32
+ app.get("#{url}/:id") do
33
+ agent.relvar = prototype
34
+ agent.mode = :tuple
35
+ agent.primary_key_equal(params[:id])
36
+ agent.get
37
+ end
38
+ app.delete(url) do
39
+ agent.relvar = prototype
40
+ agent.mode = :relation
41
+ agent.delete
42
+ end
43
+ app.delete("#{url}/:id") do
44
+ agent.relvar = prototype
45
+ agent.mode = :tuple
46
+ agent.primary_key_equal(params[:id])
47
+ agent.delete
48
+ end
49
+ app.post(url) do
50
+ agent.locator = url
51
+ agent.relvar = prototype
52
+ agent.mode = :relation
53
+ agent.body = payload rescue halt(400)
54
+ agent.post
55
+ end
56
+ app.patch("#{url}/:id") do
57
+ agent.locator = url
58
+ agent.relvar = prototype
59
+ agent.mode = :tuple
60
+ agent.primary_key_equal(params[:id])
61
+ agent.body = payload rescue halt(400)
62
+ agent.patch
63
+ end
64
+ app.put("#{url}/:id") do
65
+ agent.locator = url
66
+ agent.relvar = prototype
67
+ agent.mode = :tuple
68
+ agent.primary_key_equal(params[:id])
69
+ agent.body = payload rescue halt(400)
70
+ agent.patch
71
+ end
72
+ end
73
+
74
+ Given /^the "(.*?)" header is "(.*?)"$/ do |k,v|
75
+ client.header(k,v)
76
+ end
77
+
78
+ Given /^the "(.*?)" header is not set$/ do |k|
79
+ client.header(k,nil)
80
+ end
81
+
82
+ Given /^the "(.*?)" parameter is "(.*?)"$/ do |k,v|
83
+ client.parameter(k.to_sym,v)
84
+ end
85
+
86
+ Given /^the body of the next request is the following tuple:$/ do |table|
87
+ client.body = table.hashes.first
88
+ end
89
+
90
+ Given /^the body of the next request is the following (.*?) tuple:$/ do |prototype,table|
91
+ client.with_relvar(prototype) do |rv|
92
+ client.body = rv.heading.coerce(table.hashes.first)
93
+ end
94
+ end
95
+
96
+ Given /^the body of the next request is the following (.*?) tuples:$/ do |prototype,table|
97
+ client.with_relvar(prototype) do |rv|
98
+ client.body = rv.heading.coerce(table.hashes)
99
+ end
100
+ end
101
+
102
+ Given /^the body has the following (.*?) attribute (.*?):$/ do |attrname,prototype,table|
103
+ client.body ||= {}
104
+ client.with_relvar(prototype) do |rv|
105
+ client.body[attrname.to_sym] = rv.heading.coerce(table.hashes)
106
+ end
107
+ end
108
+
109
+ Given /^the body has the following (.*?) rva (.*?):$/ do |attrname,prototype,table|
110
+ client.body ||= {}
111
+ client.with_relvar(prototype) do |rv|
112
+ client.body[attrname.to_sym] = rv.heading.coerce(table.hashes)
113
+ end
114
+ end
115
+
116
+ Given /^the body has the following (.*?) tva (.*?):$/ do |attrname,prototype,table|
117
+ client.body ||= {}
118
+ client.with_relvar(prototype) do |rv|
119
+ client.body[attrname.to_sym] = Relation(rv.heading.coerce(table.hashes)).tuple_extract
120
+ end
121
+ end
122
+
123
+ Given /^the body has a nil for (.*?)$/ do |attrname|
124
+ client.body ||= {}
125
+ client.body[attrname.to_sym] = nil
126
+ end
127
+
128
+ Given /^I make a (.*?) (on|to) (.*)$/ do |verb, _, url|
129
+ client.send(verb.downcase.to_sym, url)
130
+ end
131
+
132
+ Then /^the status should be (\d+)$/ do |status|
133
+ client.last_response.status.should eq(Integer(status))
134
+ end
135
+
136
+ Then /^the status should not be (\d+)$/ do |status|
137
+ client.last_response.status.should_not eq(Integer(status))
138
+ end
139
+
140
+ Then /^the content type should be (.*)$/ do |ct|
141
+ client.last_response.content_type.should =~ Regexp.new(Regexp.escape(ct))
142
+ end
143
+
144
+ Then /^the "(.+?)" response header should be set$/ do |header|
145
+ client.last_response.headers[header].should_not be_nil
146
+ end
147
+
148
+ Then /^the "(.+?)" response header should not be set$/ do |header|
149
+ client.last_response.headers[header].should be_nil
150
+ end
151
+
152
+ Then /^the "(.+?)" response header should equal "(.*?)"$/ do |header,value|
153
+ client.last_response.headers[header].should eq(value)
154
+ end
155
+
156
+ Given /^I follow the specified Location$/ do
157
+ client.get(client.last_response.location)
158
+ end
159
+
160
+ Then /^the body should be empty$/ do
161
+ client.body.should be_nil
162
+ end
163
+
164
+ Then /^the body should be a JSON array$/ do
165
+ client.payload.should be_a(Array)
166
+ end
167
+
168
+ Then /^the body should be an empty JSON array$/ do
169
+ client.payload.should eq([])
170
+ end
171
+
172
+ Then /^the body should be a JSON object$/ do
173
+ client.payload.should be_a(Hash)
174
+ end
175
+
176
+ Then /^the body should equal "(.*?)"$/ do |expected|
177
+ client.last_response.body.should eq(expected)
178
+ end
179
+
180
+ Then /^the body contains "(.*?)"$/ do |expected|
181
+ client.last_response.body.should match(Regexp.compile(Regexp.escape(expected)))
182
+ end
183
+
184
+ Then /^a decoded tuple should equal:$/ do |expected|
185
+ expected = Tuple(expected.hashes.first)
186
+ @decoded = Tuple(client.payload)
187
+ @decoded.project(expected.keys).should eq(expected)
188
+ end
189
+
190
+ Then /^a decoded (.*?) tuple should equal:$/ do |prototype,expected|
191
+ client.with_relvar(prototype) do |rv|
192
+ expected = Relation(rv.heading.coerce(expected.hashes.first))
193
+ @decoded = Relation(rv.heading.coerce(client.payload))
194
+ @decoded.project(expected.to_attr_list).should eq(expected)
195
+ end
196
+ end
197
+
198
+ Then /^a decoded (.*?) relation should equal:$/ do |prototype,expected|
199
+ client.with_relvar(prototype) do |rv|
200
+ expected = Relation(rv.heading.coerce(expected.hashes))
201
+ @decoded = Relation(rv.heading.coerce(client.payload))
202
+ @decoded.project(expected.to_attr_list).should eq(expected)
203
+ end
204
+ end
205
+
206
+ Then /^a decoded relation should be (.*?)$/ do |expected|
207
+ client.with_relvar(expected) do |rv|
208
+ @decoded = Relation(rv.heading.coerce(client.payload))
209
+ @decoded.should eq(rv.value)
210
+ end
211
+ end
212
+
213
+ Then /^a decoded (.*?) relation should be empty$/ do |prototype|
214
+ client.with_relvar(prototype) do |rv|
215
+ @decoded = Relation(rv.heading.coerce(client.payload))
216
+ @decoded.should be_empty
217
+ end
218
+ end
219
+
220
+ Then /^the size of a decoded relation should be (\d+)$/ do |size|
221
+ @decoded = Relation(client.payload)
222
+ @decoded.size.should eq(Integer(size))
223
+ end
224
+
225
+ Then /its (.*?) attribute should be nil/ do |attrname|
226
+ @decoded.tuple_extract[attrname.to_sym].should be_nil
227
+ end
228
+
229
+ Then /^its (.*?) tva should equal:$/ do |tva,expected|
230
+ decoded = Relation(@decoded.tuple_extract[tva.to_sym])
231
+ expected = Relation(decoded.heading.coerce(expected.hashes))
232
+ decoded.project(expected.to_attr_list).tuple_extract.should eq(expected.tuple_extract)
233
+ end
234
+
235
+ Then /^its (.*?) rva should equal:$/ do |rva,expected|
236
+ decoded = Relation(@decoded.tuple_extract[rva.to_sym])
237
+ expected = Relation(decoded.heading.coerce(expected.hashes))
238
+ decoded.project(expected.to_attr_list).should eq(expected)
239
+ end
240
+
241
+ Then /^its (.*?) rva should be empty$/ do |rva|
242
+ tuple = @decoded.tuple_extract
243
+ tuple[rva.to_sym].should be_empty
244
+ end
245
+
246
+ Then /^its (.*?) rva should have size (\d+)$/ do |rva,size|
247
+ tuple = @decoded.tuple_extract
248
+ tuple[rva.to_sym].size.should eq(Integer(size))
249
+ end
250
+
251
+ Then /^it should be a '(.*?)' response$/ do |kind|
252
+ statuses = {
253
+ 'created' => 201,
254
+ 'updated' => 200,
255
+ 'deleted' => 200,
256
+ 'skipped' => 200,
257
+ 'tuple' => 200,
258
+ 'relation' => 200,
259
+ 'client-error' => 400,
260
+ 'unauthorized' => 401,
261
+ 'forbidden' => 403,
262
+ 'not-found' => 404,
263
+ 'conflict' => 409
264
+ }
265
+ lr = client.last_response
266
+ lr.status.should eq(statuses[kind])
267
+ client.last_response.headers["Content-Type"].should eq("application/json")
268
+ body = JSON.parse(lr.body)
269
+ case kind
270
+ when 'tuple'
271
+ client.payload.should be_a(Hash)
272
+ @decoded = Relation(client.payload)
273
+ when 'relation'
274
+ client.payload.should be_a(Array)
275
+ @decoded = Relation(client.payload)
276
+ when 'created', 'updated', 'deleted', 'skipped'
277
+ body.should eq('status' => 'success', 'message' => kind)
278
+ @decoded = Relation(client.payload)
279
+ when 'not-found', 'client-error', 'forbidden', 'unauthorized', 'conflict'
280
+ body['status'].should eq(kind)
281
+ body['message'].should_not be_nil
282
+ client.last_response.location.should be_nil
283
+ else
284
+ raise "Unexpected response kind `#{kind}`"
285
+ end
286
+ end