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.
@@ -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