alf-rack 0.15.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG.md +5 -0
- data/Gemfile +23 -0
- data/Gemfile.lock +76 -0
- data/LICENCE.md +22 -0
- data/Manifest.txt +12 -0
- data/README.md +58 -0
- data/Rakefile +11 -0
- data/lib/alf-rack.rb +1 -0
- data/lib/alf/rack.rb +11 -0
- data/lib/alf/rack/config.rb +62 -0
- data/lib/alf/rack/connect.rb +45 -0
- data/lib/alf/rack/errors.rb +15 -0
- data/lib/alf/rack/helpers.rb +34 -0
- data/lib/alf/rack/loader.rb +3 -0
- data/lib/alf/rack/query.rb +148 -0
- data/lib/alf/rack/response.rb +74 -0
- data/lib/alf/rack/version.rb +16 -0
- data/spec/config/test_database.rb +28 -0
- data/spec/config/test_viewpoint.rb +18 -0
- data/spec/connect/test_principle.rb +54 -0
- data/spec/query/test_logical_behavior.rb +41 -0
- data/spec/query/test_metadata_behavior.rb +45 -0
- data/spec/query/test_root_behavior.rb +98 -0
- data/spec/response/class/test_renderer.rb +52 -0
- data/spec/response/class/test_renderer_bang.rb +26 -0
- data/spec/response/class/test_supported_media_types.rb +18 -0
- data/spec/response/test_initialize.rb +30 -0
- data/spec/response/test_principle.rb +66 -0
- data/spec/sap.db +0 -0
- data/spec/spec_helper.rb +22 -0
- data/spec/test_rack.rb +10 -0
- data/tasks/examples.rake +7 -0
- data/tasks/gem.rake +8 -0
- data/tasks/test.rake +7 -0
- metadata +280 -0
@@ -0,0 +1,74 @@
|
|
1
|
+
module Alf
|
2
|
+
module Rack
|
3
|
+
# Specialization of `::Rack::Response` that automatically handles the
|
4
|
+
# encoding of tuples and relations according to HTTP_ACCEPT and available
|
5
|
+
# renderers.
|
6
|
+
#
|
7
|
+
# Example:
|
8
|
+
#
|
9
|
+
# ```
|
10
|
+
# require 'sinatra'
|
11
|
+
#
|
12
|
+
# use Alf::Rack::Connect{|cfg| ... }
|
13
|
+
#
|
14
|
+
# get '/users' do
|
15
|
+
# # get the connection (see Alf::Rack::Connect)
|
16
|
+
# conn = ...
|
17
|
+
#
|
18
|
+
# # The body relation/relvar will automatically be encoded to
|
19
|
+
# # whatever format the user want among the available ones.
|
20
|
+
# # The Content-Type response header is set accordingly.
|
21
|
+
# Alf::Rack::Response.new(env){|r|
|
22
|
+
# r.body = conn.query{ users }
|
23
|
+
# }.finish
|
24
|
+
# end
|
25
|
+
# ```
|
26
|
+
#
|
27
|
+
class Response < ::Rack::Response
|
28
|
+
|
29
|
+
# Prepares a Response instance for a given Rack environment. Raises an
|
30
|
+
# AcceptError if no renderer can be found for the `HTTP_ACCEPT` header.
|
31
|
+
def initialize(env = {})
|
32
|
+
@renderer = Response.renderer!(env)
|
33
|
+
super([], 200, 'Content-Type' => @renderer.mime_type)
|
34
|
+
end
|
35
|
+
|
36
|
+
# Sets the body of the response to `payload`. The latter can be any
|
37
|
+
# object that Alf is able to render through the IO renderers (relations,
|
38
|
+
# relvars, tuples, etc.).
|
39
|
+
def body=(payload)
|
40
|
+
super(@renderer.new(payload))
|
41
|
+
end
|
42
|
+
|
43
|
+
class << self
|
44
|
+
|
45
|
+
# Returns the best renderer to use given HTTP_ACCEPT header and
|
46
|
+
# available Alf renderers.
|
47
|
+
def renderer(env)
|
48
|
+
media_type = ::Rack::Accept::MediaType.new(accept(env))
|
49
|
+
if best = media_type.best_of(supported_media_types)
|
50
|
+
Renderer.each.find{|(name,_,r)| r.mime_type == best }.last
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
# Returns the renderer to use for `env`. Raises an AcceptError if no
|
55
|
+
# renderer can be found.
|
56
|
+
def renderer!(env)
|
57
|
+
renderer(env) || raise(AcceptError, "Unsupported content type `#{accept(env)}`")
|
58
|
+
end
|
59
|
+
|
60
|
+
# Returns the HTTP_ACCEPT header of `env`. Defaults to 'application/json'
|
61
|
+
def accept(env)
|
62
|
+
env['HTTP_ACCEPT'] || 'application/json'
|
63
|
+
end
|
64
|
+
|
65
|
+
# Returns media types supported by the Renderer class.
|
66
|
+
def supported_media_types
|
67
|
+
Renderer.each.map{|(_,_,r)| r.mime_type}.compact.sort
|
68
|
+
end
|
69
|
+
|
70
|
+
end # class << self
|
71
|
+
|
72
|
+
end # class Response
|
73
|
+
end # module Rack
|
74
|
+
end # module Alf
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
module Alf
|
3
|
+
module Rack
|
4
|
+
describe Config, "database=" do
|
5
|
+
|
6
|
+
let(:config){ Config.new }
|
7
|
+
|
8
|
+
subject{
|
9
|
+
config.database = db
|
10
|
+
config.database
|
11
|
+
}
|
12
|
+
|
13
|
+
context 'with a Alf::Database' do
|
14
|
+
let(:db){ Alf.database(Path.dir) }
|
15
|
+
|
16
|
+
it{ should be_a(Alf::Database) }
|
17
|
+
it{ should be(db) }
|
18
|
+
end
|
19
|
+
|
20
|
+
context 'with an adapter' do
|
21
|
+
let(:db){ Path.dir }
|
22
|
+
|
23
|
+
it{ should be_a(Alf::Database) }
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
module Alf
|
3
|
+
module Rack
|
4
|
+
describe Config, "viewpoint=" do
|
5
|
+
|
6
|
+
let(:config){ Config.new }
|
7
|
+
|
8
|
+
subject{ config.viewpoint = Alf::Viewpoint::NATIVE }
|
9
|
+
|
10
|
+
it 'sets it on the connection options' do
|
11
|
+
subject
|
12
|
+
config.connection_options[:viewpoint].should be(Alf::Viewpoint::NATIVE)
|
13
|
+
config.viewpoint.should be(Alf::Viewpoint::NATIVE)
|
14
|
+
end
|
15
|
+
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
module Alf
|
3
|
+
module Rack
|
4
|
+
describe Connect, 'its Rack behavior' do
|
5
|
+
include ::Rack::Test::Methods
|
6
|
+
|
7
|
+
def mock_app(&bl)
|
8
|
+
Class.new(Sinatra::Base) do
|
9
|
+
set :environment, :test
|
10
|
+
|
11
|
+
disable :logging
|
12
|
+
disable :dump_errors
|
13
|
+
enable :raise_errors
|
14
|
+
disable :show_exceptions
|
15
|
+
|
16
|
+
class_config = nil
|
17
|
+
use Alf::Rack::Connect do |cfg|
|
18
|
+
cfg.database = Path.dir
|
19
|
+
class_config = cfg
|
20
|
+
bl.call(cfg) if bl
|
21
|
+
end
|
22
|
+
|
23
|
+
get '/check-config' do
|
24
|
+
check = env[Alf::Rack::Connect::CONFIG_KEY].is_a?(Config)
|
25
|
+
check &= env[Alf::Rack::Connect::CONFIG_KEY] != class_config
|
26
|
+
check.to_s
|
27
|
+
end
|
28
|
+
|
29
|
+
get '/generate-error' do
|
30
|
+
raise "blah"
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
context 'on a default app' do
|
36
|
+
let(:app){ mock_app }
|
37
|
+
|
38
|
+
it 'sets a duplicata of the configuration' do
|
39
|
+
get '/check-config'
|
40
|
+
last_response.body.should eq("true")
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
context 'when an error occurs' do
|
45
|
+
let(:app){ mock_app }
|
46
|
+
|
47
|
+
it 'raises the Error outside the app' do
|
48
|
+
lambda{ get '/generate-error' }.should raise_error(/blah/)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'alf/rack/query'
|
3
|
+
module Alf
|
4
|
+
module Rack
|
5
|
+
describe Query, 'POST /logical' do
|
6
|
+
include ::Rack::Test::Methods
|
7
|
+
|
8
|
+
def mock_app(&bl)
|
9
|
+
sap = self.sap
|
10
|
+
::Rack::Builder.new do
|
11
|
+
use Alf::Rack::Connect do |cfg|
|
12
|
+
cfg.database = sap
|
13
|
+
end
|
14
|
+
run Alf::Rack::Query.new
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
let(:app){ mock_app }
|
19
|
+
|
20
|
+
subject{ post("/logical", body, {"HTTP_ACCEPT" => "text/plain"}) }
|
21
|
+
|
22
|
+
before{ subject }
|
23
|
+
|
24
|
+
context 'when the body contains a valid query' do
|
25
|
+
let(:body){
|
26
|
+
"suppliers"
|
27
|
+
}
|
28
|
+
|
29
|
+
it 'succeeds' do
|
30
|
+
last_response.status.should eq(200)
|
31
|
+
end
|
32
|
+
|
33
|
+
it 'returns the expected plans' do
|
34
|
+
last_response.body.should =~ /origin/
|
35
|
+
last_response.body.should =~ /optimized/
|
36
|
+
last_response.body.should =~ /suppliers/
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'alf/rack/query'
|
3
|
+
module Alf
|
4
|
+
module Rack
|
5
|
+
describe Query, 'POST /metadata' do
|
6
|
+
include ::Rack::Test::Methods
|
7
|
+
|
8
|
+
def mock_app(&bl)
|
9
|
+
sap = self.sap
|
10
|
+
::Rack::Builder.new do
|
11
|
+
use Alf::Rack::Connect do |cfg|
|
12
|
+
cfg.database = sap
|
13
|
+
end
|
14
|
+
run Alf::Rack::Query.new
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
let(:app){ mock_app }
|
19
|
+
|
20
|
+
subject{ post("/metadata", body, {}) }
|
21
|
+
|
22
|
+
before{ subject }
|
23
|
+
|
24
|
+
context 'when the body contains a valid query' do
|
25
|
+
let(:body){
|
26
|
+
"suppliers"
|
27
|
+
}
|
28
|
+
|
29
|
+
it 'succeeds' do
|
30
|
+
last_response.status.should eq(200)
|
31
|
+
end
|
32
|
+
|
33
|
+
it 'correctly sets the content type' do
|
34
|
+
last_response.content_type.should eq('application/json')
|
35
|
+
end
|
36
|
+
|
37
|
+
it 'returns the expected answer' do
|
38
|
+
body = ::JSON.parse(last_response.body)
|
39
|
+
body["heading"].should be_a(Array)
|
40
|
+
body["keys"].should be_a(Array)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'alf/rack/query'
|
3
|
+
require 'alf/lang/parser/safer'
|
4
|
+
module Alf
|
5
|
+
module Rack
|
6
|
+
describe Query, 'POST /' do
|
7
|
+
include ::Rack::Test::Methods
|
8
|
+
|
9
|
+
def mock_app(&bl)
|
10
|
+
sap = self.sap
|
11
|
+
::Rack::Builder.new do
|
12
|
+
use Alf::Rack::Connect do |cfg|
|
13
|
+
cfg.database = Alf::Database.new(sap, parser: Alf::Lang::Parser::Safer)
|
14
|
+
end
|
15
|
+
run Alf::Rack::Query.new
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
let(:app){ mock_app }
|
20
|
+
|
21
|
+
subject{ post(url, body, {}) }
|
22
|
+
|
23
|
+
before{ subject }
|
24
|
+
|
25
|
+
let(:url){ '/' }
|
26
|
+
|
27
|
+
context 'when the body contains a valid query' do
|
28
|
+
let(:body){
|
29
|
+
"restrict(suppliers, city: 'London')"
|
30
|
+
}
|
31
|
+
|
32
|
+
it 'succeeds' do
|
33
|
+
last_response.status.should eq(200)
|
34
|
+
end
|
35
|
+
|
36
|
+
it 'correctly sets the content type' do
|
37
|
+
last_response.content_type.should eq('application/json')
|
38
|
+
end
|
39
|
+
|
40
|
+
it 'returns the expected suppliers' do
|
41
|
+
body = ::JSON.parse(last_response.body)
|
42
|
+
body.should be_a(Array)
|
43
|
+
body.size.should eq(2)
|
44
|
+
body.map{|t| t["city"]}.uniq.should eq(["London"])
|
45
|
+
body.map{|t| t["sid"]}.should eq(["S1", "S4"])
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
context 'when the url is empty' do
|
50
|
+
let(:url){ '' }
|
51
|
+
let(:body){
|
52
|
+
"restrict(suppliers, city: 'London')"
|
53
|
+
}
|
54
|
+
|
55
|
+
it 'succeeds' do
|
56
|
+
last_response.status.should eq(200)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
shared_examples_for 'an invalid client request' do
|
61
|
+
it 'leads to a 400 status' do
|
62
|
+
last_response.status.should eq(400)
|
63
|
+
end
|
64
|
+
|
65
|
+
it 'has the correct resuting content type' do
|
66
|
+
last_response.content_type.should eq('application/json')
|
67
|
+
end
|
68
|
+
|
69
|
+
it 'leads to an error message' do
|
70
|
+
body = ::JSON.parse(last_response.body)
|
71
|
+
body.should be_a(Hash)
|
72
|
+
body.keys.should eq(["error"])
|
73
|
+
body["error"].should =~ expected_message
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
context 'when the body contains an attack attempt' do
|
78
|
+
let(:body){
|
79
|
+
"`ls -lA`"
|
80
|
+
}
|
81
|
+
let(:expected_message){
|
82
|
+
/Forbidden/
|
83
|
+
}
|
84
|
+
|
85
|
+
it_should_behave_like "an invalid client request"
|
86
|
+
end
|
87
|
+
|
88
|
+
context 'when not a POST request' do
|
89
|
+
subject{ get("/") }
|
90
|
+
|
91
|
+
it 'fails with a 404' do
|
92
|
+
last_response.status.should eq(404)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
module Alf
|
3
|
+
module Rack
|
4
|
+
describe Response, '.renderer' do
|
5
|
+
|
6
|
+
subject{ Response.renderer({"HTTP_ACCEPT" => accept}) }
|
7
|
+
|
8
|
+
context 'application/json' do
|
9
|
+
let(:accept){ "application/json" }
|
10
|
+
|
11
|
+
it{ should be(Renderer::JSON) }
|
12
|
+
end
|
13
|
+
|
14
|
+
context 'text/plain' do
|
15
|
+
let(:accept){ "text/plain" }
|
16
|
+
|
17
|
+
it{ should be(Renderer::Text) }
|
18
|
+
end
|
19
|
+
|
20
|
+
context 'text/csv' do
|
21
|
+
let(:accept){ "text/csv" }
|
22
|
+
|
23
|
+
it{ should be(Renderer::CSV) }
|
24
|
+
end
|
25
|
+
|
26
|
+
context 'text/yaml' do
|
27
|
+
let(:accept){ "text/yaml" }
|
28
|
+
|
29
|
+
it{ should be(Renderer::YAML) }
|
30
|
+
end
|
31
|
+
|
32
|
+
context '*/*' do
|
33
|
+
let(:accept){ "*/*" }
|
34
|
+
|
35
|
+
it{ should be(Renderer::JSON) }
|
36
|
+
end
|
37
|
+
|
38
|
+
context 'text/unknown' do
|
39
|
+
let(:accept){ "text/unknown" }
|
40
|
+
|
41
|
+
it{ should be(nil) }
|
42
|
+
end
|
43
|
+
|
44
|
+
context 'a complex one' do
|
45
|
+
let(:accept){ "text/unknown, text/*;q=0.8, */*;q=0.5" }
|
46
|
+
|
47
|
+
it{ should be(Renderer::CSV) }
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
module Alf
|
3
|
+
module Rack
|
4
|
+
describe Response, '.renderer!' do
|
5
|
+
|
6
|
+
subject{ Response.renderer!({"HTTP_ACCEPT" => accept}) }
|
7
|
+
|
8
|
+
context 'on supported' do
|
9
|
+
let(:accept){ "application/json" }
|
10
|
+
|
11
|
+
it{ should be(Renderer::JSON) }
|
12
|
+
end
|
13
|
+
|
14
|
+
context 'on unsupported' do
|
15
|
+
let(:accept){ "text/unknown" }
|
16
|
+
|
17
|
+
it 'raises a AcceptError' do
|
18
|
+
lambda{
|
19
|
+
subject
|
20
|
+
}.should raise_error(AcceptError, /Unsupported content type `text\/unknown`/)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|