midori.rb 0.4.3

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 (49) hide show
  1. checksums.yaml +7 -0
  2. data/.editorconfig +9 -0
  3. data/LICENSE +21 -0
  4. data/ext/midori/extconf.rb +4 -0
  5. data/ext/midori/websocket.c +32 -0
  6. data/lib/midori/api.rb +426 -0
  7. data/lib/midori/api_engine.rb +109 -0
  8. data/lib/midori/clean_room.rb +24 -0
  9. data/lib/midori/configure.rb +12 -0
  10. data/lib/midori/connection.rb +59 -0
  11. data/lib/midori/const.rb +118 -0
  12. data/lib/midori/core_ext/configurable.rb +33 -0
  13. data/lib/midori/core_ext/define_class.rb +29 -0
  14. data/lib/midori/core_ext/proc.rb +13 -0
  15. data/lib/midori/core_ext/string.rb +29 -0
  16. data/lib/midori/env.rb +18 -0
  17. data/lib/midori/eventsource.rb +20 -0
  18. data/lib/midori/exception.rb +22 -0
  19. data/lib/midori/logger.rb +15 -0
  20. data/lib/midori/middleware.rb +31 -0
  21. data/lib/midori/request.rb +115 -0
  22. data/lib/midori/response.rb +34 -0
  23. data/lib/midori/route.rb +20 -0
  24. data/lib/midori/runner.rb +63 -0
  25. data/lib/midori/sandbox.rb +46 -0
  26. data/lib/midori/server.rb +106 -0
  27. data/lib/midori/version.rb +5 -0
  28. data/lib/midori/websocket.rb +105 -0
  29. data/lib/midori.rb +37 -0
  30. data/midori.sublime-project +16 -0
  31. data/tutorial/README.md +11 -0
  32. data/tutorial/SUMMARY.md +28 -0
  33. data/tutorial/advanced/custom_extensions.md +0 -0
  34. data/tutorial/advanced/deploying_for_production.md +0 -0
  35. data/tutorial/advanced/error_handling.md +0 -0
  36. data/tutorial/advanced/rerl.md +0 -0
  37. data/tutorial/advanced/scaling_project.md +0 -0
  38. data/tutorial/advanced/unit_testing.md +0 -0
  39. data/tutorial/essentials/extensions.md +31 -0
  40. data/tutorial/essentials/getting_started.md +27 -0
  41. data/tutorial/essentials/installation.md +93 -0
  42. data/tutorial/essentials/middlewares.md +88 -0
  43. data/tutorial/essentials/request_handling.md +74 -0
  44. data/tutorial/essentials/routing.md +136 -0
  45. data/tutorial/essentials/runner.md +59 -0
  46. data/tutorial/meta/comparison_with_other_frameworks.md +0 -0
  47. data/tutorial/meta/join_the_midori_community.md +0 -0
  48. data/tutorial/meta/next_steps.md +0 -0
  49. metadata +155 -0
@@ -0,0 +1,59 @@
1
+ ##
2
+ # States of a connection
3
+ class Midori::Connection
4
+ include Midori::Server
5
+
6
+ # @!attribute data
7
+ # @return [String] string buffer of data to send
8
+ attr_accessor :data
9
+
10
+ # Init Connection with socket
11
+ # @param [IO] socket raw socket
12
+ def initialize(socket)
13
+ @registered = false
14
+ @socket = socket
15
+ @monitor = nil
16
+ @close_flag = false
17
+ @data = ''
18
+ listen(socket)
19
+ end
20
+
21
+ # Register events of connection
22
+ # @param [IO] socket raw socket
23
+ def listen(socket)
24
+ EventLoop.register(socket, :rw) do |monitor|
25
+ @monitor = monitor
26
+ if monitor.readable?
27
+ receive_data(monitor)
28
+ end
29
+ if monitor.writable?
30
+ if !@data == ''
31
+ # :nocov:
32
+ # Leave for corner cases
33
+ monitor.io.write_nonblock(@data)
34
+ @data = ''
35
+ # :nocov:
36
+ elsif @close_flag
37
+ close_connection
38
+ end
39
+ end
40
+ end
41
+ end
42
+
43
+ # Send message to client
44
+ # @param [String] data data to send
45
+ def send_data(data)
46
+ @monitor.writable? ? @socket.write_nonblock(data) : @data << data
47
+ end
48
+
49
+ # Close the connection
50
+ def close_connection
51
+ EventLoop.deregister @socket
52
+ @socket.close
53
+ end
54
+
55
+ # Close the connection after writing
56
+ def close_connection_after_writing
57
+ @close_flag = true
58
+ end
59
+ end
@@ -0,0 +1,118 @@
1
+ ##
2
+ # Module for store Midori Const
3
+ module Midori::Const
4
+ # Hash table for converting numbers to HTTP/1.1 status code line
5
+ STATUS_CODE = {
6
+ 100 => '100 Continue',
7
+ 101 => '101 Switching Protocols',
8
+ 102 => '102 Processing', # RFC 2518
9
+ 200 => '200 OK',
10
+ 201 => '201 Created',
11
+ 202 => '202 Accepted',
12
+ 203 => '203 Non-Authoritative Information',
13
+ 204 => '204 No Content',
14
+ 205 => '205 Reset Content',
15
+ 206 => '206 Partial Content', # RFC 7233
16
+ 207 => '207 Multi-Status', # RFC 4918
17
+ 208 => '208 Already Reported', # RFC 5842
18
+ 226 => '226 IM Used', # RFC 3229
19
+ 300 => '300 Multiple Choices',
20
+ 301 => '301 Moved Permanently',
21
+ 302 => '302 Found',
22
+ 303 => '303 See Other',
23
+ 304 => '304 Not Modified', # RFC 7232
24
+ 305 => '305 Use Proxy',
25
+ 307 => '307 Temporary Redirect',
26
+ 308 => '308 Permanent Redirect', # RFC 7538
27
+ 400 => '400 Bad Request',
28
+ 401 => '401 Unauthorized', # RFC 7235
29
+ 402 => '402 Payment Required',
30
+ 403 => '403 Forbidden',
31
+ 404 => '404 Not Found',
32
+ 405 => '405 Method Not Allowed',
33
+ 406 => '406 Not Acceptable',
34
+ 407 => '407 Proxy Authentication Required', # RFC 7235
35
+ 408 => '408 Request Time-out',
36
+ 409 => '409 Conflict',
37
+ 410 => '410 Gone',
38
+ 411 => '411 Length Required',
39
+ 412 => '412 Precondition Failed', # RFC 7232
40
+ 413 => '413 Payload Too Large', # RFC 7231
41
+ 414 => '414 URI Too Long', # RFC 7231
42
+ 415 => '415 Unsupported Media Type',
43
+ 416 => '416 Range Not Satisfiable',
44
+ 417 => '417 Expectation Failed', # RFC 2324
45
+ 418 => '418 I\'m a teaport', # RFC 2324
46
+ 421 => '421 Misdirected Request', # RFC 7540
47
+ 422 => '422 Unprocessable Entity', # RFC 4918
48
+ 423 => '423 Locked', # RFC 4918
49
+ 424 => '424 Failed Dependency', # RFC 4918
50
+ 426 => '426 Upgrade Required',
51
+ 428 => '428 Precondition Required', # RFC 6585
52
+ 429 => '429 Too Many Requests', # RFC 6585
53
+ 431 => '431 Request Header Fields Too Large', # RFC 6585
54
+ 451 => '451 Unavailable For Legal Reasons',
55
+ 500 => '500 Internal Server Error',
56
+ 501 => '501 Not Implemented',
57
+ 502 => '502 Bad Gateway',
58
+ 503 => '503 Service Unavailable',
59
+ 504 => '504 Gateway Time-out',
60
+ 505 => '505 HTTP Version not supported',
61
+ 506 => '506 Variant Also Negotiates', # RFC 2295
62
+ 507 => '507 Insufficient Storage', # RFC 4918
63
+ 508 => '508 Loop Detected', # RFC 5842
64
+ 510 => '510 Not Extended', # RFC 2774
65
+ 511 => '511 Network Authentication Required', # RFC 6585
66
+ }
67
+ STATUS_CODE.default = '500 Internal Server Error'
68
+ STATUS_CODE.freeze
69
+
70
+ # Default header for Basic HTTP response
71
+ DEFAULT_HEADER = {
72
+ 'Server' => "Midori/#{Midori::VERSION}"
73
+ }
74
+
75
+ # Default header for EventSource response
76
+ EVENTSOURCE_HEADER = {
77
+ 'Content-Type' => 'text-event-stream',
78
+ 'Cache-Control' => 'no-cache',
79
+ 'Connection' => 'keep-alive'
80
+ }
81
+
82
+ # Default header for WebSocket response
83
+ WEBSOCKET_HEADER = {
84
+ 'Upgrade' => 'websocket',
85
+ 'Connection' => 'Upgrade'
86
+ }
87
+
88
+ # Array of Route Methods
89
+ ROUTE_METHODS = %i(
90
+ DELETE
91
+ GET
92
+ HEAD
93
+ POST
94
+ PUT
95
+ CONNECT
96
+ OPTIONS
97
+ TRACE
98
+ COPY
99
+ LOCK
100
+ MKCOL
101
+ MOVE
102
+ PROPFIND
103
+ PROPPATCH
104
+ UNLOCK
105
+ REPORT
106
+ MKACTIVITY
107
+ CHECKOUT
108
+ MERGE
109
+ M-SEARCH
110
+ NOTIFY
111
+ SUBSCRIBE
112
+ UNSUBSCRIBE
113
+ PATCH
114
+ PURGE
115
+ WEBSOCKET
116
+ EVENTSOURCE
117
+ )
118
+ end
@@ -0,0 +1,33 @@
1
+ ##
2
+ # Modified from Sinatra
3
+ # Provide flexible configuration for Midori Server
4
+ module Configurable
5
+ # Sets an option to the given value.
6
+ # @param [Symbol] option the name of config
7
+ # @param [Object] value value to the name
8
+ # @param [Boolean] read_only Generate option= method or not
9
+ # @return [nil] nil
10
+ def set(option, value = (not_set = true), read_only = false)
11
+ raise ArgumentError if not_set
12
+
13
+ setter = proc { |val| set option, val }
14
+ getter = proc { value }
15
+
16
+ define_singleton("#{option}=", setter) unless read_only
17
+ define_singleton(option, getter)
18
+ define_singleton("#{option}?", "!!#{option}") unless method_defined? "#{option}?"
19
+
20
+ self
21
+ end
22
+
23
+ # Dynamically defines a method on settings.
24
+ # @param [String] name method name
25
+ # @param [Proc] content method content
26
+ # @return [nil] nil
27
+ private def define_singleton(name, content = Proc.new)
28
+ singleton_class.class_eval do
29
+ undef_method(name) if method_defined? name
30
+ String === content ? class_eval("def #{name}() #{content}; end") : define_method(name, &content)
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,29 @@
1
+ ##
2
+ # Meta-programming Kernel for Syntactic Sugars
3
+ module Kernel
4
+ # This method is implemented to dynamically generate class with given name and template.
5
+ # Referenced from {Ruby China}[https://ruby-china.org/topics/17382]
6
+ # @param [String] name name of class
7
+ # @param [Class] ancestor class to be inherited
8
+ # @yield inner block to be inserted into class
9
+ # @return [Class] the class defined
10
+ def define_class(name, ancestor = Object)
11
+ Object.const_set(name, Class.new(ancestor))
12
+ Object.const_get(name).class_eval(&Proc.new) if block_given?
13
+ Object.const_get(name)
14
+ end
15
+
16
+ # Define a batch of error handler with given name
17
+ # @param [Array<Symbol>] args names to be defined
18
+ # @return [nil] nil
19
+ # @example
20
+ # define_error(:foo_error, :bar_error)
21
+ # => nil, FooError < StandardError and BarError < StandardError would be defined
22
+ def define_error(*args)
23
+ args.each do |arg|
24
+ class_name = arg.to_s.split('_').collect(&:capitalize).join
25
+ define_class(class_name, StandardError)
26
+ end
27
+ nil
28
+ end
29
+ end
@@ -0,0 +1,13 @@
1
+ ##
2
+ # Meta-programming Proc for Syntactic Sugars
3
+ class Proc
4
+ # Convert [Proc] to [Lambda]
5
+ # @param [Object] instance the context
6
+ # @return [Lambda] Lambda converted
7
+ # @note Converting [Proc] to [Lambda] may have incorrect behaviours on corner cases.
8
+ # @note See {Ruby Language Issues}[https://bugs.ruby-lang.org/issues/7314] for more details.
9
+ def to_lambda(instance = Object.new)
10
+ instance.define_singleton_method(:_, &self)
11
+ instance.method(:_).to_proc
12
+ end
13
+ end
@@ -0,0 +1,29 @@
1
+ ##
2
+ # Meta-programming String for Syntactic Sugars
3
+ class String
4
+ # @param [Integer] color_code ANSI color code
5
+ # @return [String] colored string
6
+ def colorize(color_code)
7
+ "\e[#{color_code}m#{self}\e[0m"
8
+ end
9
+
10
+ # color the string with red color
11
+ def red
12
+ colorize(31)
13
+ end
14
+
15
+ # color the string with green color
16
+ def green
17
+ colorize(32)
18
+ end
19
+
20
+ # color the string with yellow color
21
+ def yellow
22
+ colorize(33)
23
+ end
24
+
25
+ # color the string with blue color
26
+ def blue
27
+ colorize(34)
28
+ end
29
+ end
data/lib/midori/env.rb ADDED
@@ -0,0 +1,18 @@
1
+ module Midori
2
+ # @return [String] midori environment
3
+ def self.env
4
+ ENV['MIDORI_ENV'] || 'development'
5
+ end
6
+ end
7
+
8
+ class String
9
+ # @return [TrueClass | FalseClass] if string is equal to production
10
+ def production?
11
+ self == 'production'
12
+ end
13
+
14
+ #@return [TrueClass | FalseClass] if string is equal to development
15
+ def development?
16
+ self == 'development'
17
+ end
18
+ end
@@ -0,0 +1,20 @@
1
+ ##
2
+ # This class provides methods for EventSource connection instance.
3
+ # @attr [Midori::Connection] connection the connection instance of EventMachine
4
+ class Midori::EventSource
5
+ attr_accessor :connection
6
+
7
+ # Init a EventSource instance with a connection
8
+ # @param [Midori::Connection] connection the connection instance of EventMachine
9
+ def initialize(connection)
10
+ @connection = connection
11
+ end
12
+
13
+ # Send data and close the connection
14
+ # @param [String] data data to be sent
15
+ def send(data)
16
+ raise Midori::Exception::EventSourceTypeError unless data.is_a? String
17
+ @connection.send_data(data.split("\n").map {|str| "data: #{str}\n"}.join + "\n")
18
+ @connection.close_connection_after_writing
19
+ end
20
+ end
@@ -0,0 +1,22 @@
1
+ ##
2
+ # This module store errors to be handled inside Midori
3
+ module Midori::Exception
4
+ # No route matched
5
+ class NotFound < StandardError; end
6
+ # Internal Error
7
+ class InternalError < StandardError; end
8
+ # Midori doesn't support continuous frame of WebSockets yet
9
+ class ContinuousFrame < StandardError; end
10
+ # WebSocket OpCode not defined in RFC standards
11
+ class OpCodeError < StandardError; end
12
+ # Websocket request not masked
13
+ class NotMasked < StandardError; end
14
+ # Websocket frame has ended
15
+ class FrameEnd < StandardError; end
16
+ # Websocket Ping Pong size too large
17
+ class PingPongSizeTooLarge < StandardError; end
18
+ # Not sending String in EventSource
19
+ class EventSourceTypeError < StandardError; end
20
+ # Insert a not middleware class to middleware list
21
+ class MiddlewareError < StandardError; end
22
+ end
@@ -0,0 +1,15 @@
1
+ module Midori
2
+ class << self
3
+ # Return current logger midori is using
4
+ # @return [Logger] the current logger midori is using
5
+ def logger
6
+ @logger ||= ::Logger.new(STDOUT)
7
+ end
8
+
9
+ # Return midori's logger
10
+ # @param [Logger] logger set midori logger
11
+ def logger=(logger)
12
+ @logger = logger
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,31 @@
1
+ ##
2
+ # Ancestor of all middlewares
3
+ class Midori::Middleware
4
+ # Init a middleware
5
+ def initialize
6
+ end
7
+
8
+ # run before processing a request
9
+ # @param [Midori::Request] request raw request
10
+ # @return [Midori::Request] request to be further processed
11
+ def before(request)
12
+ request
13
+ end
14
+
15
+ # run after processing a request
16
+ # @param [Midori::Request] _request raw request
17
+ # @param [Midori::Response] response raw response
18
+ # @return [Midori::Response] response to be further processed
19
+ def after(_request, response)
20
+ response
21
+ end
22
+
23
+ # Dynamically generate a method to use inside router
24
+ # @param [Symbol] name name of the method
25
+ # @yield the block to run
26
+ def self.helper(name, &block)
27
+ Midori::CleanRoom.class_exec do
28
+ define_method(name, &block)
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,115 @@
1
+ ##
2
+ # Request class for midori
3
+ # @attr [String] ip client ip address
4
+ # @attr [Integer] port client port
5
+ # @attr [String] protocol protocol version of HTTP request
6
+ # @attr [Symbol] method HTTP method
7
+ # @attr [String] path request path
8
+ # @attr [Hash] query_param parameter parsed from query string
9
+ # @attr [String | nil] query_string request query string
10
+ # @attr [Hash] header request header
11
+ # @attr [String] body request body
12
+ # @attr [Hash] cookie cookie hash coming from request
13
+ # @attr [Boolean] parsed whether the request header parsed
14
+ # @attr [Boolean] body_parsed whether the request body parsed
15
+ # @attr [Hash] params params in the url
16
+ class Midori::Request
17
+ attr_accessor :ip, :port,
18
+ :protocol, :method, :path, :query_params, :query_string,
19
+ :header, :body, :parsed, :body_parsed, :params, :cookie
20
+
21
+ # Init Request
22
+ def initialize
23
+ @header = {}
24
+ @parsed = false
25
+ @body_parsed = false
26
+ @is_websocket = false
27
+ @is_eventsource = false
28
+ @parser = Http::Parser.new
29
+ @params = {}
30
+ @query_params = Hash.new(Array.new)
31
+ @cookie = {}
32
+ @body = ''
33
+ @parser.on_headers_complete = proc do
34
+ @protocol = @parser.http_version
35
+ @method = @parser.http_method
36
+ @path = @parser.request_url
37
+ @header = @parser.headers
38
+
39
+ @query_string = @path.match(/\?(.*?)$/)
40
+ unless @query_string.nil?
41
+ @query_string = @query_string[1]
42
+ @query_params = CGI::parse(@query_string)
43
+ end
44
+
45
+ @cookie = CGI::Cookie.parse(@header['Cookie']) unless @header['Cookie'].nil?
46
+ @path.gsub!(/\?(.*?)$/, '')
47
+ @method = @method.to_sym
48
+ @parsed = true
49
+ :stop
50
+ end
51
+ end
52
+
53
+ # Init an request with String data
54
+ # @param [String] data
55
+ # @return [nil] nil
56
+ def parse(data)
57
+ # Call parser if header not parsed
58
+ if @parsed
59
+ @body += data
60
+ else
61
+ offset = @parser << data
62
+ @body += data[offset..-1] if @parsed
63
+ end
64
+
65
+ # Set body parsed if body reaches content length
66
+ if @parsed && (@header['Content-Length'].to_i || 0) <= @body.bytesize
67
+ @body_parsed = true
68
+ pre_proceed
69
+ end
70
+ nil
71
+ end
72
+
73
+ # Preproceed the request after parsed
74
+ # @return [nil] nil
75
+ def pre_proceed
76
+ # Deal with WebSocket
77
+ if @header['Upgrade'] == 'websocket' && @header['Connection'] == 'Upgrade'
78
+ @method = :WEBSOCKET
79
+ @is_websocket = true
80
+ end
81
+
82
+ # Deal with EventSource
83
+ if @header['Accept'] == 'text/event-stream'
84
+ @method = :EVENTSOURCE
85
+ @is_eventsource = true
86
+ end
87
+
88
+ @method = @method.to_sym
89
+ nil
90
+ end
91
+
92
+ # Syntactic sugar for whether a request header is parsed
93
+ # @return [Boolean] parsed or not
94
+ def parsed?
95
+ @parsed
96
+ end
97
+
98
+ # Syntactic sugar for whether a request body is parsed
99
+ # @return [Boolean] parsed or not
100
+ def body_parsed?
101
+ @body_parsed
102
+ end
103
+
104
+ # Syntactic sugar for whether a request is a websocket request
105
+ # @return [Boolean] websocket or not
106
+ def websocket?
107
+ @is_websocket
108
+ end
109
+
110
+ # Syntactic sugar for whether a request is an eventsource request
111
+ # @return [Boolean] eventsource or not
112
+ def eventsource?
113
+ @is_eventsource
114
+ end
115
+ end
@@ -0,0 +1,34 @@
1
+ ##
2
+ # Class for midori response
3
+ # @attr [String] status HTTP response status
4
+ # @attr [Hash] header HTTP response header
5
+ # @attr [String] body HTTP response body
6
+ class Midori::Response
7
+ attr_accessor :status, :header, :body
8
+
9
+ # @param [Hash] options HTTP response
10
+ # @option options [Integer] code HTTP response code
11
+ # @option options [Hash] header HTTP response header
12
+ # @option options [String] body HTTP response body
13
+ # Init a Response
14
+ def initialize(options = {})
15
+ code = options[:status] || 200
16
+ @status = Midori::Const::STATUS_CODE[code]
17
+ @header = options[:header] || Midori::Const::DEFAULT_HEADER.clone
18
+ @body = options[:body] || ''
19
+ end
20
+
21
+ # Generate header string from hash
22
+ # @return [String] generated string
23
+ def generate_header
24
+ @header.map do |key, value|
25
+ "#{key}: #{value}\r\n"
26
+ end.join
27
+ end
28
+
29
+ # Convert response to raw string
30
+ # @return [String] generated string
31
+ def to_s
32
+ "HTTP/1.1 #{@status}\r\n#{generate_header}\r\n#{@body}"
33
+ end
34
+ end
@@ -0,0 +1,20 @@
1
+ ##
2
+ # Class for Midori route
3
+ # @attr [String] method HTTP method
4
+ # @attr [Regexp] path regex to match
5
+ # @attr [Proc] function what to do after matched
6
+ # @attr [Array<Class>] middlewares middlewares used in the route
7
+ class Midori::Route
8
+ attr_accessor :method, :path, :function, :middlewares
9
+
10
+ # Define a route
11
+ # @param [String] method HTTP method
12
+ # @param [Regexp] path regex to match
13
+ # @param [Proc] function what to do after matched
14
+ def initialize(method, path, function)
15
+ @method = method
16
+ @path = path
17
+ @function = function
18
+ @middlewares = []
19
+ end
20
+ end
@@ -0,0 +1,63 @@
1
+ ##
2
+ # Abstract runner class to control instance of Midori Server
3
+ # @attr [String] bind the address to bind
4
+ # @attr [Integer] port the port to bind
5
+ # @attr [Logger] logger midori logger
6
+ class Midori::Runner
7
+ attr_reader :bind, :port, :logger
8
+
9
+ # Define status of a runner
10
+ # @param [Class] api inherited from [Midori::API]
11
+ # @param [Class] configure inherited from [Midori::Configure]
12
+ def initialize(api, configure = Midori::Configure)
13
+ @logger = configure.logger
14
+ Midori.logger = configure.logger
15
+ @bind = configure.bind
16
+ @port = configure.port
17
+ @api = (api.is_a?Midori::APIEngine) ? api : Midori::APIEngine.new(api, configure.route_type)
18
+ @before = configure.before
19
+ end
20
+
21
+ # Get Midori server whether running
22
+ # @return [Boolean] [true] running
23
+ # @return [Boolean] [false] not running
24
+ def running?
25
+ !!@server
26
+ end
27
+
28
+ # Start the Midori server
29
+ # @note This is an async method, but no callback
30
+ def start
31
+ return false if running? || EventLoop.running?
32
+ @logger.info "Midori #{Midori::VERSION} is now running on #{bind}:#{port}".blue
33
+ @server = TCPServer.new(@bind, @port)
34
+ EventLoop.register(@server, :r) do |monitor|
35
+ socket = monitor.io.accept_nonblock
36
+ connection = Midori::Connection.new(socket)
37
+ connection.server_initialize(@api, @logger)
38
+ end
39
+ async_fiber(Fiber.new do
40
+ @before.call
41
+ end)
42
+ EventLoop.start
43
+ nil
44
+ end
45
+
46
+ # Stop the Midori server
47
+ # @note This is an async method, but no callback
48
+ # @return [Boolean] [true] stop successfully
49
+ # @return [Boolean] [false] nothing to stop
50
+ def stop
51
+ if running?
52
+ @logger.info 'Stopping Midori'.blue
53
+ EventLoop.deregister @server
54
+ @server.close
55
+ @server = nil
56
+ EventLoop.stop
57
+ true
58
+ else
59
+ @logger.error 'Midori Server has NOT been started'.red
60
+ false
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,46 @@
1
+ ##
2
+ # Sandbox for global error capture
3
+ class Midori::Sandbox
4
+ class << self
5
+ def class_initialize
6
+ @handlers = Hash.new
7
+ @handlers[Midori::Exception::InternalError] = proc {|e| Midori::Response.new(status: 500, body: "#{e.inspect} #{e.backtrace}")}
8
+ @handlers[Midori::Exception::NotFound] = proc {|_e| Midori::Response.new(status: 404, body: '404 Not Found')}
9
+ end
10
+
11
+ # Add a rule to Sandbox
12
+ # @param [Class] class_name the class to capture
13
+ # @param [Proc] block what to do when captured
14
+ # @return [nil] nil
15
+ def add_rule(class_name, block)
16
+ @handlers[class_name] = block
17
+ nil
18
+ end
19
+
20
+ # Detect what to run with given error
21
+ # @param [StandardError] error the error captured
22
+ # @return [nil] nil
23
+ def capture(error)
24
+ if @handlers[error.class].nil?
25
+ @handlers[Midori::Exception::InternalError].call(error)
26
+ else
27
+ @handlers[error.class].call(error)
28
+ end
29
+ end
30
+
31
+ # Run sandbox inside given clean room
32
+ # @param [Midori::CleanRoom] clean_room Clean room to run
33
+ # @param [Proc] function the block to run
34
+ # @return [nil] nil
35
+ def run(clean_room, function, *args)
36
+ begin
37
+ function.to_lambda(clean_room).call(*args)
38
+ rescue StandardError => e
39
+ capture(e)
40
+ end
41
+ end
42
+ end
43
+
44
+ private_class_method :class_initialize
45
+ class_initialize
46
+ end