alf-rack 0.15.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 +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
|