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.
Files changed (41) hide show
  1. checksums.yaml +5 -5
  2. data/README.md +106 -15
  3. data/bin/wabur +6 -0
  4. data/export/assets/js/ui.js +18 -0
  5. data/lib/wab.rb +7 -1
  6. data/lib/wab/client.rb +145 -0
  7. data/lib/wab/controller.rb +1 -1
  8. data/lib/wab/errors.rb +6 -0
  9. data/lib/wab/impl.rb +1 -1
  10. data/lib/wab/impl/agoo.rb +18 -0
  11. data/lib/wab/impl/agoo/export_proxy.rb +55 -0
  12. data/lib/wab/impl/agoo/handler.rb +51 -0
  13. data/lib/wab/impl/agoo/sender.rb +50 -0
  14. data/lib/wab/impl/agoo/server.rb +59 -0
  15. data/lib/wab/impl/agoo/tql_handler.rb +35 -0
  16. data/lib/wab/impl/model.rb +8 -1
  17. data/lib/wab/impl/rack_error.rb +27 -0
  18. data/lib/wab/impl/rack_handler.rb +69 -0
  19. data/lib/wab/impl/shell.rb +56 -51
  20. data/lib/wab/impl/sinatra.rb +18 -0
  21. data/lib/wab/impl/sinatra/export_proxy.rb +57 -0
  22. data/lib/wab/impl/sinatra/handler.rb +50 -0
  23. data/lib/wab/impl/sinatra/sender.rb +53 -0
  24. data/lib/wab/impl/sinatra/server.rb +66 -0
  25. data/lib/wab/impl/sinatra/tql_handler.rb +35 -0
  26. data/lib/wab/impl/templates/wabur.conf.template +1 -1
  27. data/lib/wab/impl/webrick.rb +18 -0
  28. data/lib/wab/impl/webrick/export_proxy.rb +41 -0
  29. data/lib/wab/impl/webrick/handler.rb +116 -0
  30. data/lib/wab/impl/webrick/sender.rb +34 -0
  31. data/lib/wab/impl/webrick/server.rb +39 -0
  32. data/lib/wab/impl/webrick/tql_handler.rb +58 -0
  33. data/lib/wab/racker.rb +25 -0
  34. data/lib/wab/version.rb +1 -1
  35. data/pages/Architecture.md +15 -6
  36. data/test/test_client.rb +282 -0
  37. data/test/test_impl.rb +2 -0
  38. data/test/test_runner.rb +267 -91
  39. metadata +27 -5
  40. data/lib/wab/impl/export_proxy.rb +0 -39
  41. 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
@@ -114,11 +114,18 @@ module WAB
114
114
  result
115
115
  end
116
116
 
117
- def update(obj, _rid, where, _filter)
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
@@ -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
- mime_types = WEBrick::HTTPUtils::DefaultMimeTypes
57
- mime_types['es6'] = 'application/javascript'
58
- server = WEBrick::HTTPServer.new(Port: @http_port,
59
- DocumentRoot: @http_dir,
60
- MimeTypes: mime_types)
61
- server.logger.level = 5 - @logger.level unless @logger.nil?
62
- server.mount(@pre_path, Handler, self)
63
- server.mount('/', ExportProxy, @http_dir) if @export_proxy
64
-
65
- trap 'INT' do server.shutdown end
66
- server.start
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
- case controller
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 default_controller
130
- @default_controller ||= @controllers[nil]
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