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
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
|
+
[![Build Status](https://secure.travis-ci.org/alf-tool/alf-rack.png)](http://travis-ci.org/alf-tool/alf-rack)
|
4
|
+
[![Dependency Status](https://gemnasium.com/alf-tool/alf-rack.png)](https://gemnasium.com/alf-tool/alf-rack)
|
5
|
+
[![Code Climate](https://codeclimate.com/github/alf-tool/alf-sql.png)](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
|