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
data/CHANGELOG.md
ADDED
data/Gemfile
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
source 'http://rubygems.org'
|
2
|
+
|
3
|
+
group :runtime do
|
4
|
+
gem "rack", "~> 1.5"
|
5
|
+
gem "rack-accept", "~> 0.4.5"
|
6
|
+
gem "ruby_cop", "~> 1.0"
|
7
|
+
|
8
|
+
gem "alf-core", path: "../alf-core"
|
9
|
+
end
|
10
|
+
|
11
|
+
group :development do
|
12
|
+
gem "rack-test", "~> 0.6.2"
|
13
|
+
gem "rake", "~> 10.1"
|
14
|
+
gem "path", "~> 1.3"
|
15
|
+
gem "rspec", "~> 2.14"
|
16
|
+
gem "sinatra", "~> 1.4"
|
17
|
+
gem "sqlite3", "~> 1.3", :platforms => ['mri', 'rbx']
|
18
|
+
gem "jdbc-sqlite3", "~> 3.7", :platforms => ['jruby']
|
19
|
+
gem "sequel", "~> 4.2"
|
20
|
+
|
21
|
+
gem "alf-sql", path: "../alf-sql"
|
22
|
+
gem "alf-sequel", path: "../alf-sequel"
|
23
|
+
end
|
data/Gemfile.lock
ADDED
@@ -0,0 +1,76 @@
|
|
1
|
+
PATH
|
2
|
+
remote: ../alf-core
|
3
|
+
specs:
|
4
|
+
alf-core (0.15.0)
|
5
|
+
domain (~> 1.0)
|
6
|
+
myrrha (~> 3.0)
|
7
|
+
path (~> 1.3)
|
8
|
+
sexpr (~> 0.6.0)
|
9
|
+
|
10
|
+
PATH
|
11
|
+
remote: ../alf-sequel
|
12
|
+
specs:
|
13
|
+
alf-sequel (0.15.0)
|
14
|
+
alf-core (~> 0.15.0)
|
15
|
+
alf-sql (~> 0.15.0)
|
16
|
+
sequel (~> 4.2)
|
17
|
+
|
18
|
+
PATH
|
19
|
+
remote: ../alf-sql
|
20
|
+
specs:
|
21
|
+
alf-sql (0.15.0)
|
22
|
+
alf-core (~> 0.15.0)
|
23
|
+
sexpr (~> 0.6.0)
|
24
|
+
|
25
|
+
GEM
|
26
|
+
remote: http://rubygems.org/
|
27
|
+
specs:
|
28
|
+
diff-lcs (1.2.4)
|
29
|
+
domain (1.0.0)
|
30
|
+
myrrha (3.0.0)
|
31
|
+
domain (~> 1.0)
|
32
|
+
path (1.3.3)
|
33
|
+
rack (1.5.2)
|
34
|
+
rack-accept (0.4.5)
|
35
|
+
rack (>= 0.4)
|
36
|
+
rack-protection (1.5.1)
|
37
|
+
rack
|
38
|
+
rack-test (0.6.2)
|
39
|
+
rack (>= 1.0)
|
40
|
+
rake (10.1.0)
|
41
|
+
rspec (2.14.1)
|
42
|
+
rspec-core (~> 2.14.0)
|
43
|
+
rspec-expectations (~> 2.14.0)
|
44
|
+
rspec-mocks (~> 2.14.0)
|
45
|
+
rspec-core (2.14.7)
|
46
|
+
rspec-expectations (2.14.3)
|
47
|
+
diff-lcs (>= 1.1.3, < 2.0)
|
48
|
+
rspec-mocks (2.14.4)
|
49
|
+
ruby_cop (1.0.5)
|
50
|
+
sequel (4.3.0)
|
51
|
+
sexpr (0.6.0)
|
52
|
+
sinatra (1.4.4)
|
53
|
+
rack (~> 1.4)
|
54
|
+
rack-protection (~> 1.4)
|
55
|
+
tilt (~> 1.3, >= 1.3.4)
|
56
|
+
sqlite3 (1.3.8)
|
57
|
+
tilt (1.4.1)
|
58
|
+
|
59
|
+
PLATFORMS
|
60
|
+
ruby
|
61
|
+
|
62
|
+
DEPENDENCIES
|
63
|
+
alf-core!
|
64
|
+
alf-sequel!
|
65
|
+
alf-sql!
|
66
|
+
jdbc-sqlite3 (~> 3.7)
|
67
|
+
path (~> 1.3)
|
68
|
+
rack (~> 1.5)
|
69
|
+
rack-accept (~> 0.4.5)
|
70
|
+
rack-test (~> 0.6.2)
|
71
|
+
rake (~> 10.1)
|
72
|
+
rspec (~> 2.14)
|
73
|
+
ruby_cop (~> 1.0)
|
74
|
+
sequel (~> 4.2)
|
75
|
+
sinatra (~> 1.4)
|
76
|
+
sqlite3 (~> 1.3)
|
data/LICENCE.md
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
# The MIT Licence
|
2
|
+
|
3
|
+
Copyright (c) 2013 - Bernard Lambeau
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/Manifest.txt
ADDED
data/README.md
ADDED
@@ -0,0 +1,58 @@
|
|
1
|
+
# Alf::Rack
|
2
|
+
|
3
|
+
[](http://travis-ci.org/alf-tool/alf-rack)
|
4
|
+
[](https://gemnasium.com/alf-tool/alf-rack)
|
5
|
+
[](https://codeclimate.com/github/alf-tool/alf-rack)
|
6
|
+
|
7
|
+
A collection of Rack middlewares for using Alf in web applications.
|
8
|
+
|
9
|
+
## Links
|
10
|
+
|
11
|
+
* http://github.com/blambeau/alf
|
12
|
+
* http://github.com/blambeau/alf-rack
|
13
|
+
|
14
|
+
## Example: a RESTful-like interface
|
15
|
+
|
16
|
+
**See the examples folder for more advanced examples.**
|
17
|
+
|
18
|
+
```
|
19
|
+
require 'sinatra'
|
20
|
+
require 'alf-rack'
|
21
|
+
|
22
|
+
# include some helpers (see later)
|
23
|
+
include Alf::Rack::Helpers
|
24
|
+
|
25
|
+
# This middleware will open a database connection on every request.
|
26
|
+
# That connection and query methods are available through the helpers.
|
27
|
+
use Alf::Rack::Connect do |cfg|
|
28
|
+
cfg.database = ::Sequel.connect("postgres://...")
|
29
|
+
end
|
30
|
+
|
31
|
+
# Let send all suppliers, automatically use HTTP_ACCEPT to encode in
|
32
|
+
# requested format (csv, json, etc.)
|
33
|
+
get '/suppliers' do |id|
|
34
|
+
Alf::Rack::Response.new{|r|
|
35
|
+
r.body = relvar{ suppliers }
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
# Similar for a single supplier tuple
|
40
|
+
get '/suppliers/:id' do |id|
|
41
|
+
# Find the supplier
|
42
|
+
Alf::Rack::Response.new{|r|
|
43
|
+
r.body = tuple_extract{ restrict(suppliers, sid: id) }
|
44
|
+
end
|
45
|
+
end
|
46
|
+
```
|
47
|
+
|
48
|
+
## Example: arbitrary queries
|
49
|
+
|
50
|
+
```
|
51
|
+
# As before
|
52
|
+
use Alf::Rack::Connect do |cfg|
|
53
|
+
cfg.database = ::Sequel.connect("postgres://...")
|
54
|
+
end
|
55
|
+
|
56
|
+
# Answers arbitrary queries on POST /
|
57
|
+
run Alf::Rack::Query.new
|
58
|
+
```
|
data/Rakefile
ADDED
data/lib/alf-rack.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require_relative "alf/rack"
|
data/lib/alf/rack.rb
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
require_relative 'rack/version'
|
2
|
+
require_relative 'rack/loader'
|
3
|
+
module Alf
|
4
|
+
module Rack
|
5
|
+
end
|
6
|
+
end
|
7
|
+
require_relative 'rack/errors'
|
8
|
+
require_relative 'rack/config'
|
9
|
+
require_relative 'rack/connect'
|
10
|
+
require_relative 'rack/helpers'
|
11
|
+
require_relative 'rack/response'
|
@@ -0,0 +1,62 @@
|
|
1
|
+
module Alf
|
2
|
+
module Rack
|
3
|
+
class Config < Support::Config
|
4
|
+
|
5
|
+
# The database instance to use for obtaining connections
|
6
|
+
option :database, Database, nil
|
7
|
+
|
8
|
+
# The connection options to use
|
9
|
+
option :connection_options, Hash, {}
|
10
|
+
|
11
|
+
# Enclose all requests in a single database transaction?
|
12
|
+
option :transactional, Boolean, false
|
13
|
+
|
14
|
+
# Sets the database, coercing it if required
|
15
|
+
def database=(db)
|
16
|
+
@database = db.is_a?(Database) ? db : Alf.database(db)
|
17
|
+
end
|
18
|
+
|
19
|
+
# Returns the default viewpoint to use
|
20
|
+
def viewpoint
|
21
|
+
connection_options[:viewpoint]
|
22
|
+
end
|
23
|
+
|
24
|
+
# Sets the default viewpoint on connection options
|
25
|
+
def viewpoint=(vp)
|
26
|
+
connection_options[:viewpoint] = vp
|
27
|
+
end
|
28
|
+
|
29
|
+
### At runtime (requires having dup the config first)
|
30
|
+
|
31
|
+
# The current database connection
|
32
|
+
attr_reader :connection
|
33
|
+
|
34
|
+
# Connects to the database, starts a transaction if required, then
|
35
|
+
# yields the block with the connection object.
|
36
|
+
#
|
37
|
+
# The connection is kept under an instance variable and can be later
|
38
|
+
# obtained through the `connection` accessor. Do NOT use this method
|
39
|
+
# without having called `dup` on the config object as it relies on
|
40
|
+
# shared mutable state.
|
41
|
+
def connect(&bl)
|
42
|
+
return yield unless database
|
43
|
+
database.connect(connection_options) do |conn|
|
44
|
+
@connection = conn
|
45
|
+
if transactional?
|
46
|
+
conn.in_transaction{ yield(conn) }
|
47
|
+
else
|
48
|
+
yield(conn)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
# Reconnect with new options. This method is provided if you want to
|
54
|
+
# use another viewpoint than the original one in the middle of the
|
55
|
+
# request treatment.
|
56
|
+
def reconnect(opts)
|
57
|
+
connection.reconnect(opts)
|
58
|
+
end
|
59
|
+
|
60
|
+
end # class Config
|
61
|
+
end # module Rack
|
62
|
+
end # module Alf
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module Alf
|
2
|
+
module Rack
|
3
|
+
# Connect to a database and make the connection available in the Rack
|
4
|
+
# environment.
|
5
|
+
#
|
6
|
+
# Example:
|
7
|
+
#
|
8
|
+
# ```
|
9
|
+
# require 'sinatra'
|
10
|
+
#
|
11
|
+
# use Alf::Rack::Connect do |cfg| # see Alf::Rack::Config
|
12
|
+
# cfg.database = ... # (required) a Alf::Database or Alf::Adapter
|
13
|
+
# end
|
14
|
+
#
|
15
|
+
# get '/' do
|
16
|
+
# # the configuration object (a dup of what has been seen above)
|
17
|
+
# config = env[Alf::Rack::Connect::CONFIG_KEY]
|
18
|
+
#
|
19
|
+
# # the config object is connected
|
20
|
+
# connection = config.connection
|
21
|
+
# # => Alf::Database::Connection
|
22
|
+
#
|
23
|
+
# # ...
|
24
|
+
# end
|
25
|
+
# ```
|
26
|
+
class Connect
|
27
|
+
|
28
|
+
CONFIG_KEY = "ALF_RACK_CONFIG".freeze
|
29
|
+
|
30
|
+
def initialize(app, config = Config.new)
|
31
|
+
@app = app
|
32
|
+
@config = config
|
33
|
+
yield(config) if block_given?
|
34
|
+
end
|
35
|
+
|
36
|
+
def call(env)
|
37
|
+
env[CONFIG_KEY] = cfg = @config.dup
|
38
|
+
cfg.connect do
|
39
|
+
@app.call(env)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
end # class Connect
|
44
|
+
end # module Rack
|
45
|
+
end # module Alf
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module Alf
|
2
|
+
module Rack
|
3
|
+
|
4
|
+
# Superclass of all Alf::Rack errors
|
5
|
+
class Error < StandardError; end
|
6
|
+
|
7
|
+
# Raised when Alf is unable to convert a tuple or relation into the
|
8
|
+
# requested mime type (HTTP_ACCEPT).
|
9
|
+
class AcceptError < Error; end
|
10
|
+
|
11
|
+
# Raised by the Query middleware when a query seems invalid.
|
12
|
+
class QueryError < StandardError; end
|
13
|
+
|
14
|
+
end # module Rack
|
15
|
+
end # module Alf
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module Alf
|
2
|
+
module Rack
|
3
|
+
module Helpers
|
4
|
+
|
5
|
+
# Returns Alf configuration previously installed by the Connect
|
6
|
+
# middleware
|
7
|
+
def alf_config
|
8
|
+
env[Alf::Rack::Connect::CONFIG_KEY]
|
9
|
+
end
|
10
|
+
|
11
|
+
# Returns Alf's connection previously installed by the Connect
|
12
|
+
# middleware
|
13
|
+
def alf_connection
|
14
|
+
alf_config.connection
|
15
|
+
end
|
16
|
+
|
17
|
+
# Executes a query on the connection and returns the result.
|
18
|
+
def query(*args, &bl)
|
19
|
+
alf_connection.query(*args, &bl)
|
20
|
+
end
|
21
|
+
|
22
|
+
# Requests a relvar on the connection and returns it.
|
23
|
+
def relvar(*args, &bl)
|
24
|
+
alf_connection.relvar(*args, &bl)
|
25
|
+
end
|
26
|
+
|
27
|
+
# Requests a tuple on the connection and returns it.
|
28
|
+
def tuple_extract(*args, &bl)
|
29
|
+
alf_connection.tuple_extract(*args, &bl)
|
30
|
+
end
|
31
|
+
|
32
|
+
end # module Helpers
|
33
|
+
end # module Rack
|
34
|
+
end # module Alf
|
@@ -0,0 +1,148 @@
|
|
1
|
+
require "ruby_cop"
|
2
|
+
module Alf
|
3
|
+
module Rack
|
4
|
+
# This Rack application allows client to query your database by simply
|
5
|
+
# sending Alf queries as body of POST requests. It automatically run those
|
6
|
+
# queries on the current connection and encodes the result according to
|
7
|
+
# the HTTP_ACCEPT header.
|
8
|
+
#
|
9
|
+
# IMPORTANT: Alf has no true parser for now. In order to mitigate the risk
|
10
|
+
# of exposing serious attack vectors, you MUST take care of installing the
|
11
|
+
# safer parser on your database, as illustrated below. This seriously makes
|
12
|
+
# attacks harder, unfortunately without any guarantee...
|
13
|
+
#
|
14
|
+
# By default, this class catches all errors (e.g. syntax, type-checking
|
15
|
+
# security, runtime query execution) and return a 400 response with the
|
16
|
+
# error message. A side-effect is that all tuples are loaded in memory
|
17
|
+
# before returning the response, to ensure that any error is discovered
|
18
|
+
# immediately. When setting `catch_all` to false, this class sets a relvar
|
19
|
+
# instance as response body and let all errors percolate up the Rack stack.
|
20
|
+
# This means that errors may occur later, during actual query execution.
|
21
|
+
#
|
22
|
+
# Example:
|
23
|
+
#
|
24
|
+
# ```
|
25
|
+
# # in a config.ru or something
|
26
|
+
#
|
27
|
+
# # Create a database with a safer parser than usual
|
28
|
+
# require 'alf/lang/parser/safer'
|
29
|
+
# DB = Alf::Database.new(...){|opts|
|
30
|
+
# opts.parser = Alf::Lang::Parser::Safer
|
31
|
+
# }
|
32
|
+
#
|
33
|
+
# Connect the database on every request
|
34
|
+
# use Alf::Rack::Connect{|cfg|
|
35
|
+
# cfg.database = DB
|
36
|
+
# }
|
37
|
+
#
|
38
|
+
# # let the query engine run under '/'
|
39
|
+
# run Alf::Rack::Query.new{|q|
|
40
|
+
# q.type_check = false # to bypass expressions type-checking
|
41
|
+
# q.catch_all = false # to let errors percolate
|
42
|
+
# }
|
43
|
+
# ```
|
44
|
+
class Query
|
45
|
+
include Alf::Rack::Helpers
|
46
|
+
|
47
|
+
# Rack response when not found
|
48
|
+
NOT_FOUND = [404, {}, []]
|
49
|
+
|
50
|
+
# Recognized URLs
|
51
|
+
RECOGNIZED_URLS_RX = /^(\/(data|metadata|logical|physical)?)?$/
|
52
|
+
|
53
|
+
# Apply type checking (defaults to true)?
|
54
|
+
attr_accessor :type_check
|
55
|
+
alias :type_check? :type_check
|
56
|
+
|
57
|
+
# Catch all errors or let them percolate up the stack (default to true)?
|
58
|
+
attr_accessor :catch_all
|
59
|
+
alias :catch_all? :catch_all
|
60
|
+
|
61
|
+
# Creates an application instance
|
62
|
+
def initialize
|
63
|
+
@type_check = true
|
64
|
+
@catch_all = true
|
65
|
+
yield(self) if block_given?
|
66
|
+
end
|
67
|
+
|
68
|
+
# Call on a duplicated instance
|
69
|
+
def call(env)
|
70
|
+
return NOT_FOUND unless env['REQUEST_METHOD'] == 'POST'
|
71
|
+
return NOT_FOUND unless env['PATH_INFO'] =~ RECOGNIZED_URLS_RX
|
72
|
+
dup._call(env)
|
73
|
+
end
|
74
|
+
|
75
|
+
# Set the environment, execute the query and encode the response.
|
76
|
+
def _call(env)
|
77
|
+
@env = env
|
78
|
+
Alf::Rack::Response.new(env){|r|
|
79
|
+
safe(r){ execute }
|
80
|
+
}.finish
|
81
|
+
end
|
82
|
+
attr_reader :env
|
83
|
+
|
84
|
+
private
|
85
|
+
|
86
|
+
# Executes the block in a begin/rescue implementing the catch_all
|
87
|
+
# strategy
|
88
|
+
def safe(response)
|
89
|
+
result = yield
|
90
|
+
response.body = result
|
91
|
+
rescue => ex
|
92
|
+
raise unless catch_all?
|
93
|
+
response.status = 400
|
94
|
+
response.body = { "error" => "#{ex.class}: #{ex.message}" }
|
95
|
+
end
|
96
|
+
|
97
|
+
# Executes the request
|
98
|
+
def execute
|
99
|
+
case env['PATH_INFO']
|
100
|
+
when '', '/', '/data' then data
|
101
|
+
when '/metadata' then metadata
|
102
|
+
when '/logical' then logical_plan
|
103
|
+
when '/physical' then physical_plan
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def data
|
108
|
+
relvar(query).value
|
109
|
+
end
|
110
|
+
|
111
|
+
def metadata
|
112
|
+
q = query
|
113
|
+
keys = q.keys.to_a.map{|k| k.to_a }
|
114
|
+
heading = Relation(q.heading.to_hash.each_pair.map{|k,v|
|
115
|
+
{attribute: k, type: v.to_s}
|
116
|
+
})
|
117
|
+
{heading: heading, keys: keys}
|
118
|
+
end
|
119
|
+
|
120
|
+
def logical_plan
|
121
|
+
q = query
|
122
|
+
{
|
123
|
+
origin: q.to_ascii_tree,
|
124
|
+
optimized: relvar(q).expr.to_ascii_tree
|
125
|
+
}
|
126
|
+
end
|
127
|
+
|
128
|
+
def physical_plan
|
129
|
+
q = query
|
130
|
+
{
|
131
|
+
plan: relvar(q).to_cog.to_ascii_tree
|
132
|
+
}
|
133
|
+
end
|
134
|
+
|
135
|
+
def query
|
136
|
+
query = env['rack.input'].read
|
137
|
+
query = alf_connection.parse(query)
|
138
|
+
if query.is_a?(Algebra::Operand)
|
139
|
+
query.type_check if type_check?
|
140
|
+
query
|
141
|
+
else
|
142
|
+
raise QueryError, "Not a relational expression"
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
end # class Query
|
147
|
+
end # module Rack
|
148
|
+
end # module Alf
|