alf-rack 0.15.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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,16 @@
1
+ module Alf
2
+ module Rack
3
+ module Version
4
+
5
+ MAJOR = 0
6
+ MINOR = 15
7
+ TINY = 0
8
+
9
+ def self.to_s
10
+ [ MAJOR, MINOR, TINY ].join('.')
11
+ end
12
+
13
+ end
14
+ VERSION = Version.to_s
15
+ end
16
+ end
@@ -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