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