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 ADDED
@@ -0,0 +1,5 @@
1
+ # 0.15.0 / FIX ME
2
+
3
+ * Enhancements
4
+
5
+ * Birthday!
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
@@ -0,0 +1,12 @@
1
+ rack.gemspec
2
+ rack.noespec
3
+ CHANGELOG.md
4
+ Gemfile
5
+ Gemfile.lock
6
+ lib/**/*
7
+ LICENCE.md
8
+ Manifest.txt
9
+ Rakefile
10
+ README.md
11
+ spec/**/*
12
+ tasks/**/*
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
@@ -0,0 +1,11 @@
1
+ # We run tests by default
2
+ task :default => :test
3
+
4
+ #
5
+ # Install all tasks found in tasks folder
6
+ #
7
+ # See .rake files there for complete documentation.
8
+ #
9
+ Dir["tasks/*.rake"].each do |taskfile|
10
+ load taskfile
11
+ end
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,3 @@
1
+ require "rack"
2
+ require "rack/accept"
3
+ require "alf-core"
@@ -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