wabur 0.6.2 → 0.7.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/README.md +106 -15
- data/bin/wabur +6 -0
- data/export/assets/js/ui.js +18 -0
- data/lib/wab.rb +7 -1
- data/lib/wab/client.rb +145 -0
- data/lib/wab/controller.rb +1 -1
- data/lib/wab/errors.rb +6 -0
- data/lib/wab/impl.rb +1 -1
- data/lib/wab/impl/agoo.rb +18 -0
- data/lib/wab/impl/agoo/export_proxy.rb +55 -0
- data/lib/wab/impl/agoo/handler.rb +51 -0
- data/lib/wab/impl/agoo/sender.rb +50 -0
- data/lib/wab/impl/agoo/server.rb +59 -0
- data/lib/wab/impl/agoo/tql_handler.rb +35 -0
- data/lib/wab/impl/model.rb +8 -1
- data/lib/wab/impl/rack_error.rb +27 -0
- data/lib/wab/impl/rack_handler.rb +69 -0
- data/lib/wab/impl/shell.rb +56 -51
- data/lib/wab/impl/sinatra.rb +18 -0
- data/lib/wab/impl/sinatra/export_proxy.rb +57 -0
- data/lib/wab/impl/sinatra/handler.rb +50 -0
- data/lib/wab/impl/sinatra/sender.rb +53 -0
- data/lib/wab/impl/sinatra/server.rb +66 -0
- data/lib/wab/impl/sinatra/tql_handler.rb +35 -0
- data/lib/wab/impl/templates/wabur.conf.template +1 -1
- data/lib/wab/impl/webrick.rb +18 -0
- data/lib/wab/impl/webrick/export_proxy.rb +41 -0
- data/lib/wab/impl/webrick/handler.rb +116 -0
- data/lib/wab/impl/webrick/sender.rb +34 -0
- data/lib/wab/impl/webrick/server.rb +39 -0
- data/lib/wab/impl/webrick/tql_handler.rb +58 -0
- data/lib/wab/racker.rb +25 -0
- data/lib/wab/version.rb +1 -1
- data/pages/Architecture.md +15 -6
- data/test/test_client.rb +282 -0
- data/test/test_impl.rb +2 -0
- data/test/test_runner.rb +267 -91
- metadata +27 -5
- data/lib/wab/impl/export_proxy.rb +0 -39
- data/lib/wab/impl/handler.rb +0 -98
@@ -0,0 +1,51 @@
|
|
1
|
+
|
2
|
+
module WAB
|
3
|
+
module Impl
|
4
|
+
module Agoo
|
5
|
+
|
6
|
+
# Handler for requests that fall under the path assigned to the
|
7
|
+
# Controller. This is used only with the WAB::Impl::Shell.
|
8
|
+
class Handler
|
9
|
+
include Sender
|
10
|
+
|
11
|
+
def initialize(shell, controller)
|
12
|
+
@shell = shell
|
13
|
+
@controller = controller
|
14
|
+
end
|
15
|
+
|
16
|
+
def on_request(req, res)
|
17
|
+
path = (req.script_name + req.path_info).split('/')[1..-1]
|
18
|
+
query = parse_query(req.query_string)
|
19
|
+
body = req.body
|
20
|
+
unless body.nil?
|
21
|
+
if body.empty?
|
22
|
+
body = nil
|
23
|
+
else
|
24
|
+
body = Data.new(Oj.strict_load(body, symbol_keys: true))
|
25
|
+
body.detect
|
26
|
+
end
|
27
|
+
end
|
28
|
+
case req.request_method
|
29
|
+
when 'GET'
|
30
|
+
@shell.log_call(@controller, 'read', path, query)
|
31
|
+
send_result(@controller.read(path, query), res, path, query)
|
32
|
+
when 'PUT'
|
33
|
+
@shell.log_call(@controller, 'create', path, query, body)
|
34
|
+
send_result(@controller.create(path, query, body), res, path, query)
|
35
|
+
when 'POST'
|
36
|
+
@shell.log_call(@controller, 'update', path, query, body)
|
37
|
+
send_result(@controller.update(path, query, body), res, path, query)
|
38
|
+
when 'DELETE'
|
39
|
+
@shell.log_call(@controller, 'delete', path, query)
|
40
|
+
send_result(@controller.delete(path, query), res, path, query)
|
41
|
+
else
|
42
|
+
raise StandardError.new("#{method} is not a supported method") if op.nil?
|
43
|
+
end
|
44
|
+
rescue StandardError => e
|
45
|
+
send_error(e, res)
|
46
|
+
end
|
47
|
+
|
48
|
+
end # Handler
|
49
|
+
end # Agoo
|
50
|
+
end # Impl
|
51
|
+
end # WAB
|
@@ -0,0 +1,50 @@
|
|
1
|
+
|
2
|
+
module WAB
|
3
|
+
module Impl
|
4
|
+
module Agoo
|
5
|
+
|
6
|
+
# The Sender module adds support for sending results and errors.
|
7
|
+
module Sender
|
8
|
+
|
9
|
+
# Sends the results from a controller request.
|
10
|
+
def send_result(result, res, path, query)
|
11
|
+
result = @shell.data(result) unless result.is_a?(WAB::Data)
|
12
|
+
response_body = result.json(@shell.indent)
|
13
|
+
res.code = 200
|
14
|
+
res['Content-Type'] = 'application/json'
|
15
|
+
@shell.logger.debug("reply to #{path.join('/')}#{query}: #{response_body}") if @shell.logger.debug?
|
16
|
+
res.body = response_body
|
17
|
+
end
|
18
|
+
|
19
|
+
# Sends an error from a rescued call.
|
20
|
+
def send_error(e, res)
|
21
|
+
res.code = 500
|
22
|
+
res['Content-Type'] = 'application/json'
|
23
|
+
body = { code: -1, error: "#{e.class}: #{e.message}" }
|
24
|
+
body[:backtrace] = e.backtrace
|
25
|
+
res.body = @shell.data(body).json(@shell.indent)
|
26
|
+
@shell.logger.warn(Impl.format_error(e))
|
27
|
+
end
|
28
|
+
|
29
|
+
# Parses a query string into a Hash.
|
30
|
+
def parse_query(query_string)
|
31
|
+
query = {}
|
32
|
+
if !query_string.nil? && !query_string.empty?
|
33
|
+
query_string.split('&').each { |opt|
|
34
|
+
k, v = opt.split('=')
|
35
|
+
# TBD convert %xx to char
|
36
|
+
query[k] = v
|
37
|
+
}
|
38
|
+
end
|
39
|
+
# Detect numbers (others later)
|
40
|
+
query.each_pair { |k,v|
|
41
|
+
i = Utils.attempt_key_to_int(v)
|
42
|
+
query[k] = i unless i.nil?
|
43
|
+
# TBD how about float
|
44
|
+
}
|
45
|
+
end
|
46
|
+
|
47
|
+
end # Sender
|
48
|
+
end # Agoo
|
49
|
+
end # Impl
|
50
|
+
end # WAB
|
@@ -0,0 +1,59 @@
|
|
1
|
+
|
2
|
+
require 'agoo'
|
3
|
+
|
4
|
+
module WAB
|
5
|
+
module Impl
|
6
|
+
module Agoo
|
7
|
+
|
8
|
+
# The Server module provides a server start method.
|
9
|
+
module Server
|
10
|
+
|
11
|
+
# Start the server and set the mount points.
|
12
|
+
def self.start(shell)
|
13
|
+
options = {
|
14
|
+
pedantic: false,
|
15
|
+
log_dir: '',
|
16
|
+
thread_count: 0,
|
17
|
+
log_console: true,
|
18
|
+
log_classic: true,
|
19
|
+
log_colorize: true,
|
20
|
+
log_states: {
|
21
|
+
INFO: shell.logger.info?,
|
22
|
+
DEBUG: shell.logger.debug?,
|
23
|
+
connect: shell.logger.info?,
|
24
|
+
request: shell.logger.info?,
|
25
|
+
response: shell.logger.info?,
|
26
|
+
eval: shell.logger.info?,
|
27
|
+
}
|
28
|
+
}
|
29
|
+
server = ::Agoo::Server.new(shell.http_port, shell.http_dir, options)
|
30
|
+
|
31
|
+
shell.mounts.each { |hh|
|
32
|
+
if hh.has_key?(:type)
|
33
|
+
handler = WAB::Impl::Agoo::Handler.new(shell, shell.create_controller(hh[:handler]))
|
34
|
+
server.handle(nil, "#{shell.pre_path}/#{hh[:type]}", handler)
|
35
|
+
server.handle(nil, "#{shell.pre_path}/#{hh[:type]}/*", handler)
|
36
|
+
elsif hh.has_key?(:path)
|
37
|
+
path = hh[:path]
|
38
|
+
if path.empty?
|
39
|
+
path = '/**'
|
40
|
+
elsif '*' != path[-1]
|
41
|
+
path << '/' unless '/' == path[-1]
|
42
|
+
path << '**'
|
43
|
+
end
|
44
|
+
server.handle(:POST, path, shell.create_controller(hh[:handler]))
|
45
|
+
else
|
46
|
+
raise WAB::Error.new("Invalid handle configuration. Missing path or type.")
|
47
|
+
end
|
48
|
+
}
|
49
|
+
server.handle(:POST, shell.tql_path, WAB::Impl::Agoo::TqlHandler.new(shell)) unless (shell.tql_path.nil? || shell.tql_path.empty?)
|
50
|
+
server.handle_not_found(WAB::Impl::Agoo::ExportProxy.new(shell)) if shell.export_proxy
|
51
|
+
|
52
|
+
trap 'INT' do server.shutdown end
|
53
|
+
server.start
|
54
|
+
end
|
55
|
+
|
56
|
+
end # Server
|
57
|
+
end # Agoo
|
58
|
+
end # Impl
|
59
|
+
end # WAB
|
@@ -0,0 +1,35 @@
|
|
1
|
+
|
2
|
+
module WAB
|
3
|
+
module Impl
|
4
|
+
module Agoo
|
5
|
+
|
6
|
+
# Handler for requests that fall under the path assigned to the
|
7
|
+
# Controller. This is used only with the WAB::Impl::Shell.
|
8
|
+
class TqlHandler
|
9
|
+
include Sender
|
10
|
+
|
11
|
+
def initialize(shell)
|
12
|
+
@shell = shell
|
13
|
+
end
|
14
|
+
|
15
|
+
def on_request(req, res)
|
16
|
+
path = (req.script_name + req.path_info).split('/')[1..-1]
|
17
|
+
query = parse_query(req.query_string)
|
18
|
+
tql = Oj.load(req.body, mode: :wab)
|
19
|
+
log_request_with_body('TQL', path, query, tql) if @shell.logger.info?
|
20
|
+
send_result(@shell.query(tql), res, path, query)
|
21
|
+
rescue StandardError => e
|
22
|
+
send_error(e, res)
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def log_request_with_body(caller, path, query, body)
|
28
|
+
body = Data.new(body) unless body.is_a?(WAB::Data)
|
29
|
+
@shell.logger.info("#{caller} #{path.join('/')}#{query}\n#{body.json(@shell.indent)}")
|
30
|
+
end
|
31
|
+
|
32
|
+
end # TqlHandler
|
33
|
+
end # Agoo
|
34
|
+
end # Impl
|
35
|
+
end # WAB
|
data/lib/wab/impl/model.rb
CHANGED
@@ -114,11 +114,18 @@ module WAB
|
|
114
114
|
result
|
115
115
|
end
|
116
116
|
|
117
|
-
def update(obj, _rid, where,
|
117
|
+
def update(obj, _rid, where, filter)
|
118
118
|
updated = []
|
119
119
|
@lock.synchronize {
|
120
120
|
if where.is_a?(Expr)
|
121
121
|
# TBD must be able to update portions of an object
|
122
|
+
@map.each_pair { |ref, v|
|
123
|
+
if where.eval(v) && (filter.nil? || filter.eval(v))
|
124
|
+
@map[ref] = Data.new(obj, true)
|
125
|
+
updated << ref
|
126
|
+
write_to_file(ref, obj)
|
127
|
+
end
|
128
|
+
}
|
122
129
|
else
|
123
130
|
# A reference.
|
124
131
|
@map[where] = Data.new(obj, true)
|
@@ -0,0 +1,27 @@
|
|
1
|
+
|
2
|
+
module WAB
|
3
|
+
module Impl
|
4
|
+
|
5
|
+
# The RackError class is a logger that is used in the Rack env in a
|
6
|
+
# request. It uses the shell logger to log errors.
|
7
|
+
class RackError
|
8
|
+
|
9
|
+
# Create a new instance.
|
10
|
+
def initialize(shell)
|
11
|
+
@shell = shell
|
12
|
+
end
|
13
|
+
|
14
|
+
def puts(message)
|
15
|
+
@shell.logger.error(message).to_s
|
16
|
+
end
|
17
|
+
|
18
|
+
def write(message)
|
19
|
+
@shell.logger.error(message)
|
20
|
+
end
|
21
|
+
|
22
|
+
def flush
|
23
|
+
end
|
24
|
+
|
25
|
+
end # RackError
|
26
|
+
end # Impl
|
27
|
+
end # WAB
|
@@ -0,0 +1,69 @@
|
|
1
|
+
|
2
|
+
require 'webrick'
|
3
|
+
|
4
|
+
module WAB
|
5
|
+
module Impl
|
6
|
+
|
7
|
+
# Handler for requests that fall under the path assigned to the rack
|
8
|
+
# Controller. This is used only with the WAB::Impl::Shell.
|
9
|
+
class RackHandler < WEBrick::HTTPServlet::AbstractServlet
|
10
|
+
|
11
|
+
def initialize(server, shell, handler)
|
12
|
+
super(server)
|
13
|
+
@shell = shell
|
14
|
+
case handler
|
15
|
+
when String
|
16
|
+
handler = Object.const_get(handler).new(self)
|
17
|
+
when Class
|
18
|
+
handler = handler.new(self)
|
19
|
+
end
|
20
|
+
handler.shell = self
|
21
|
+
@handler = handler
|
22
|
+
end
|
23
|
+
|
24
|
+
def service(req, res)
|
25
|
+
env = {
|
26
|
+
'REQUEST_METHOD' => req.request_method,
|
27
|
+
'SCRIPT_NAME' => req.script_name,
|
28
|
+
'PATH_INFO' => req.path_info,
|
29
|
+
'QUERY_STRING' => req.query_string,
|
30
|
+
'SERVER_NAME' => req.server_name,
|
31
|
+
'SERVER_PORT' => req.port,
|
32
|
+
'rack.version' => '1.2',
|
33
|
+
'rack.url_scheme' => req.ssl? ? 'https' : 'http',
|
34
|
+
'rack.errors' => '', ## TBD
|
35
|
+
'rack.multithread' => false,
|
36
|
+
'rack.multiprocess' => false,
|
37
|
+
'rack.run_once' => false,
|
38
|
+
}
|
39
|
+
req.each { |k| env['HTTP_' + k] = req[k] }
|
40
|
+
unless req.body.nil?
|
41
|
+
env['rack.input'] = StringIO.new(req.body)
|
42
|
+
end
|
43
|
+
rres = @handler.call(env)
|
44
|
+
res.status = rres[0]
|
45
|
+
rres[1].each { |a| res[a[0]] = a[1] }
|
46
|
+
unless rres[2].empty?
|
47
|
+
res.body = ''
|
48
|
+
rres[2].each { |s| res.body << s }
|
49
|
+
end
|
50
|
+
@shell.logger.debug("reply to #{path.join('/')}#{query}: #{res.body}") if @shell.logger.debug?
|
51
|
+
rescue StandardError => e
|
52
|
+
send_error(e, res)
|
53
|
+
end
|
54
|
+
|
55
|
+
private
|
56
|
+
|
57
|
+
# Sends an error from a rescued call.
|
58
|
+
def send_error(e, res)
|
59
|
+
res.status = 500
|
60
|
+
res['Content-Type'] = 'application/json'
|
61
|
+
body = { code: -1, error: "#{e.class}: #{e.message}" }
|
62
|
+
body[:backtrace] = e.backtrace
|
63
|
+
res.body = @shell.data(body).json(@shell.indent)
|
64
|
+
@shell.logger.warn(Impl.format_error(e))
|
65
|
+
end
|
66
|
+
|
67
|
+
end # RackHandler
|
68
|
+
end # Impl
|
69
|
+
end # WAB
|
data/lib/wab/impl/shell.rb
CHANGED
@@ -1,5 +1,4 @@
|
|
1
1
|
|
2
|
-
require 'wab/impl/handler'
|
3
2
|
require 'wab/impl/model'
|
4
3
|
|
5
4
|
module WAB
|
@@ -13,6 +12,12 @@ module WAB
|
|
13
12
|
# Returns the path where a data type is located. The default is 'kind'.
|
14
13
|
attr_reader :type_key
|
15
14
|
attr_reader :path_pos
|
15
|
+
attr_reader :pre_path
|
16
|
+
attr_reader :mounts
|
17
|
+
attr_reader :http_dir
|
18
|
+
attr_reader :http_port
|
19
|
+
attr_reader :tql_path
|
20
|
+
attr_reader :export_proxy
|
16
21
|
|
17
22
|
attr_accessor :indent
|
18
23
|
|
@@ -26,6 +31,7 @@ module WAB
|
|
26
31
|
@indent = config[:indent].to_i || 0
|
27
32
|
@pre_path = config[:path_prefix] || '/v1'
|
28
33
|
@path_pos = @pre_path.split('/').length - 1
|
34
|
+
@tql_path = config[:tql_path] || '/tql'
|
29
35
|
base = config[:base] || '.'
|
30
36
|
@model = Model.new((config['store.dir'] || File.join(base, 'data')).gsub('$BASE', base), indent)
|
31
37
|
@type_key = config[:type_key] || 'kind'
|
@@ -36,7 +42,8 @@ module WAB
|
|
36
42
|
@export_proxy = config[:export_proxy]
|
37
43
|
@export_proxy = true if @export_proxy.nil? # The default is true if not present.
|
38
44
|
@controllers = {}
|
39
|
-
|
45
|
+
@mounts = config[:handler] || []
|
46
|
+
@server = config['http.server'].to_s.downcase
|
40
47
|
requires = config[:require]
|
41
48
|
case requires
|
42
49
|
when Array
|
@@ -44,26 +51,22 @@ module WAB
|
|
44
51
|
when String
|
45
52
|
requires.split(',').each { |r| require r.strip }
|
46
53
|
end
|
47
|
-
|
48
|
-
if config[:handler].is_a?(Array)
|
49
|
-
config[:handler].each { |hh| register_controller(hh[:type], hh[:handler]) }
|
50
|
-
end
|
51
54
|
end
|
52
55
|
|
53
56
|
# Start listening. This should be called after registering Controllers
|
54
57
|
# with the Shell.
|
55
|
-
def start
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
58
|
+
def start
|
59
|
+
case @server
|
60
|
+
when 'agoo'
|
61
|
+
require 'wab/impl/agoo'
|
62
|
+
WAB::Impl::Agoo::Server::start(self)
|
63
|
+
when 'sinatra'
|
64
|
+
require 'wab/impl/sinatra'
|
65
|
+
WAB::Impl::Sinatra::Server::start(self)
|
66
|
+
else
|
67
|
+
require 'wab/impl/webrick'
|
68
|
+
WAB::Impl::WEBrick::Server::start(self)
|
69
|
+
end
|
67
70
|
end
|
68
71
|
|
69
72
|
# Register a controller for a named type.
|
@@ -76,37 +79,7 @@ module WAB
|
|
76
79
|
# identified +type+. This can be a Controller, a Controller
|
77
80
|
# class, or a Controller class name.
|
78
81
|
def register_controller(type, controller)
|
79
|
-
|
80
|
-
when String
|
81
|
-
controller = Object.const_get(controller).new(self)
|
82
|
-
when Class
|
83
|
-
controller = controller.new(self)
|
84
|
-
end
|
85
|
-
controller.shell = self
|
86
|
-
@controllers[type] = controller
|
87
|
-
end
|
88
|
-
|
89
|
-
# Returns the controller associated with the type key found in the
|
90
|
-
# data. If a controller has not be registered under that key the default
|
91
|
-
# controller is returned if there is one.
|
92
|
-
#
|
93
|
-
# data:: data to extract the type from for lookup in the controllers
|
94
|
-
def controller(data)
|
95
|
-
path = data.get(:path)
|
96
|
-
path = path.native if path.is_a?(WAB::Data)
|
97
|
-
return path_controller(path) unless path.nil? || (path.length <= @path_pos)
|
98
|
-
|
99
|
-
content = data.get(:content)
|
100
|
-
return @controllers[content.get(@type_key)] || default_controller unless content.nil?
|
101
|
-
|
102
|
-
default_controller
|
103
|
-
end
|
104
|
-
|
105
|
-
# Returns the controller according to the type in the path.
|
106
|
-
#
|
107
|
-
# path: path Array such as from a URL
|
108
|
-
def path_controller(path)
|
109
|
-
@controllers[path[@path_pos]] || default_controller
|
82
|
+
@mount << { type: type, handler: controller }
|
110
83
|
end
|
111
84
|
|
112
85
|
# Create and return a new data instance with the provided initial value.
|
@@ -124,10 +97,42 @@ module WAB
|
|
124
97
|
Data.new(value, repair)
|
125
98
|
end
|
126
99
|
|
100
|
+
# Helper function that creates a controller instance given either a
|
101
|
+
# class name, a class, or an already created object.
|
102
|
+
def create_controller(controller)
|
103
|
+
case controller
|
104
|
+
when String
|
105
|
+
controller = Object.const_get(controller).new(self)
|
106
|
+
when Class
|
107
|
+
controller = controller.new(self)
|
108
|
+
end
|
109
|
+
controller.shell = self
|
110
|
+
controller
|
111
|
+
end
|
112
|
+
|
113
|
+
def log_call(controller, op, path, query, body=nil)
|
114
|
+
if @logger.debug?
|
115
|
+
if body.nil?
|
116
|
+
@logger.debug("#{controller.class}.#{op}(#{path_to_s(path)}#{query})")
|
117
|
+
else
|
118
|
+
body = body.json(@indent) unless body.is_a?(String)
|
119
|
+
@logger.debug("#{controller.class}.#{op}(#{path_to_s(path)}#{query})\n#{body}")
|
120
|
+
end
|
121
|
+
elsif @logger.info?
|
122
|
+
@logger.info("#{controller.class}.#{op}(#{path_to_s(path)}#{query})") if @logger.info?
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
127
126
|
private
|
128
127
|
|
129
|
-
def
|
130
|
-
|
128
|
+
def path_to_s(path)
|
129
|
+
if path.is_a?(String)
|
130
|
+
path
|
131
|
+
elsif path.is_a?(Array)
|
132
|
+
path.join('/')
|
133
|
+
else
|
134
|
+
path.to_s
|
135
|
+
end
|
131
136
|
end
|
132
137
|
|
133
138
|
end # Shell
|