wabur 0.6.2 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
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