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.
- data/CHANGELOG.md +5 -0
- data/Gemfile +21 -0
- data/Gemfile.lock +71 -0
- data/LICENCE.md +22 -0
- data/Manifest.txt +12 -0
- data/README.md +11 -0
- data/Rakefile +11 -0
- data/lib/alf-rest.rb +1 -0
- data/lib/alf/rest.rb +30 -0
- data/lib/alf/rest/alf-ext/renderer.rb +16 -0
- data/lib/alf/rest/alf-ext/unit_of_work.rb +3 -0
- data/lib/alf/rest/alf-ext/unit_of_work/delete.rb +21 -0
- data/lib/alf/rest/alf-ext/unit_of_work/insert.rb +22 -0
- data/lib/alf/rest/alf-ext/unit_of_work/update.rb +22 -0
- data/lib/alf/rest/config.rb +55 -0
- data/lib/alf/rest/errors.rb +5 -0
- data/lib/alf/rest/helpers.rb +68 -0
- data/lib/alf/rest/loader.rb +5 -0
- data/lib/alf/rest/middleware.rb +19 -0
- data/lib/alf/rest/payload.rb +12 -0
- data/lib/alf/rest/payload/client.rb +22 -0
- data/lib/alf/rest/request.rb +43 -0
- data/lib/alf/rest/response.rb +21 -0
- data/lib/alf/rest/test.rb +16 -0
- data/lib/alf/rest/test/client.rb +83 -0
- data/lib/alf/rest/test/ext.rb +7 -0
- data/lib/alf/rest/test/steps.rb +286 -0
- data/lib/alf/rest/version.rb +16 -0
- data/lib/sinatra/alf-rest.rb +73 -0
- data/spec/fixtures/sap.db +0 -0
- data/spec/integration/sinatra/rest_get/test_accept.rb +98 -0
- data/spec/integration/spec_helper.rb +27 -0
- data/spec/test_rest.rb +10 -0
- data/spec/unit/config/test_database.rb +28 -0
- data/spec/unit/config/test_viewpoint.rb +18 -0
- data/spec/unit/ext/renderer/test_from_http_accept.rb +50 -0
- data/spec/unit/ext/renderer/test_supported_media_types.rb +10 -0
- data/spec/unit/middleware/test_behavior.rb +55 -0
- data/spec/unit/request/test_to_relation.rb +56 -0
- data/spec/unit/spec_helper.rb +35 -0
- data/spec/unit/test_rest.rb +10 -0
- data/tasks/gem.rake +8 -0
- data/tasks/test.rake +17 -0
- metadata +251 -0
@@ -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,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
|