wabur 0.6.2 → 0.7.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.
- 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
|