alf-rest 0.14.0

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