fastrb 0.1.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 +7 -0
- data/bin/rubyapi +2 -0
- data/lib/rubyapi/cli.rb +162 -0
- data/lib/rubyapi/config.rb +35 -0
- data/lib/rubyapi/context.rb +181 -0
- data/lib/rubyapi/di.rb +51 -0
- data/lib/rubyapi/error_handler.rb +20 -0
- data/lib/rubyapi/job.rb +93 -0
- data/lib/rubyapi/middleware/logging.rb +67 -0
- data/lib/rubyapi/openapi.rb +50 -0
- data/lib/rubyapi/param_converters.rb +58 -0
- data/lib/rubyapi/plugin.rb +37 -0
- data/lib/rubyapi/plugins/auth.rb +20 -0
- data/lib/rubyapi/plugins/cache.rb +41 -0
- data/lib/rubyapi/plugins/cors.rb +35 -0
- data/lib/rubyapi/plugins/jwt.rb +42 -0
- data/lib/rubyapi/router.rb +99 -0
- data/lib/rubyapi/schema.rb +46 -0
- data/lib/rubyapi/serializer.rb +25 -0
- data/lib/rubyapi/session.rb +62 -0
- data/lib/rubyapi/sse.rb +30 -0
- data/lib/rubyapi/streaming.rb +58 -0
- data/lib/rubyapi/version.rb +3 -0
- data/lib/rubyapi/websocket.rb +64 -0
- data/lib/rubyapi.rb +306 -0
- metadata +148 -0
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
require "securerandom"
|
|
2
|
+
require "time"
|
|
3
|
+
require "date"
|
|
4
|
+
require "bigdecimal"
|
|
5
|
+
|
|
6
|
+
module RubyAPI
|
|
7
|
+
Boolean = Class.new unless defined?(Boolean)
|
|
8
|
+
UUID = Class.new unless defined?(UUID)
|
|
9
|
+
Decimal = BigDecimal unless defined?(Decimal)
|
|
10
|
+
|
|
11
|
+
class ConversionError < StandardError
|
|
12
|
+
def initialize(param_name, type_name)
|
|
13
|
+
super("#{param_name} must be #{type_name}")
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
module ParamConverters
|
|
18
|
+
CONVERTERS = {
|
|
19
|
+
String => ->(v) { v.to_s },
|
|
20
|
+
Integer => ->(v) { Integer(v) },
|
|
21
|
+
Float => ->(v) { Float(v) },
|
|
22
|
+
Boolean => ->(v) {
|
|
23
|
+
case v.to_s.downcase
|
|
24
|
+
when "true", "1", "yes" then true
|
|
25
|
+
when "false", "0", "no", "nil" then false
|
|
26
|
+
else raise ArgumentError
|
|
27
|
+
end
|
|
28
|
+
},
|
|
29
|
+
Date => ->(v) { Date.parse(v) },
|
|
30
|
+
Time => ->(v) { Time.parse(v) },
|
|
31
|
+
DateTime => ->(v) { DateTime.parse(v) },
|
|
32
|
+
Array => ->(v) {
|
|
33
|
+
return v if v.is_a?(Array)
|
|
34
|
+
JSON.parse(v)
|
|
35
|
+
},
|
|
36
|
+
Hash => ->(v) {
|
|
37
|
+
return v if v.is_a?(Hash)
|
|
38
|
+
JSON.parse(v)
|
|
39
|
+
},
|
|
40
|
+
UUID => ->(v) {
|
|
41
|
+
uuid = v.to_s
|
|
42
|
+
raise ArgumentError unless uuid.match?(/\A[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\z/i)
|
|
43
|
+
uuid
|
|
44
|
+
},
|
|
45
|
+
Decimal => ->(v) { BigDecimal(v.to_s) },
|
|
46
|
+
Symbol => ->(v) { v.to_sym }
|
|
47
|
+
}.freeze
|
|
48
|
+
|
|
49
|
+
def self.convert(value, type)
|
|
50
|
+
converter = CONVERTERS[type]
|
|
51
|
+
return value.to_s unless converter
|
|
52
|
+
converter.call(value)
|
|
53
|
+
rescue ArgumentError, TypeError, JSON::ParserError
|
|
54
|
+
type_name = type.respond_to?(:name) ? type.name : type.to_s
|
|
55
|
+
raise ConversionError.new(value.to_s, type_name)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
module RubyAPI
|
|
2
|
+
class Plugin
|
|
3
|
+
def self.inherited(subclass)
|
|
4
|
+
RubyAPI::PluginRegistry.register(subclass)
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
def self.on_load(app); end
|
|
8
|
+
def self.register_routes(app); end
|
|
9
|
+
def self.register_cli(commands); end
|
|
10
|
+
def self.plugin_name
|
|
11
|
+
name.split("::").last
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def self.options
|
|
15
|
+
@options ||= {}
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def self.option(key, value = nil)
|
|
19
|
+
@options ||= {}
|
|
20
|
+
@options[key] = value
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
class PluginRegistry
|
|
25
|
+
def self.plugins
|
|
26
|
+
@plugins ||= []
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def self.register(klass)
|
|
30
|
+
plugins << klass unless plugins.include?(klass)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def self.find(name)
|
|
34
|
+
plugins.find { |p| p.plugin_name == name.to_s || p == name }
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
module RubyAPI
|
|
2
|
+
module Plugins
|
|
3
|
+
class Auth < Plugin
|
|
4
|
+
option :unauthorized_status, 401
|
|
5
|
+
option :unauthorized_body, { error: "Unauthorized" }
|
|
6
|
+
|
|
7
|
+
def self.on_load(app)
|
|
8
|
+
app.before do |ctx|
|
|
9
|
+
payload = ctx.get(:jwt_payload)
|
|
10
|
+
unless payload
|
|
11
|
+
status = options[:unauthorized_status]
|
|
12
|
+
body = options[:unauthorized_body]
|
|
13
|
+
ctx.response_status = status
|
|
14
|
+
ctx.response_body = body
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
module RubyAPI
|
|
2
|
+
module Plugins
|
|
3
|
+
class Cache < Plugin
|
|
4
|
+
option :store, {}
|
|
5
|
+
option :default_ttl, 3600
|
|
6
|
+
|
|
7
|
+
def self.store
|
|
8
|
+
options[:store]
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def self.get(key)
|
|
12
|
+
entry = store[key]
|
|
13
|
+
return nil unless entry
|
|
14
|
+
return nil if Time.now > entry[:expires_at]
|
|
15
|
+
entry[:value]
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def self.set(key, value, ttl: nil)
|
|
19
|
+
ttl ||= options[:default_ttl]
|
|
20
|
+
store[key] = {
|
|
21
|
+
value: value,
|
|
22
|
+
expires_at: Time.now + ttl
|
|
23
|
+
}
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def self.delete(key)
|
|
27
|
+
store.delete(key)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def self.clear
|
|
31
|
+
store.clear
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def self.on_load(app)
|
|
35
|
+
app.before do |ctx|
|
|
36
|
+
ctx.set(:cache, self)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
module RubyAPI
|
|
2
|
+
module Plugins
|
|
3
|
+
class CORS < Plugin
|
|
4
|
+
option :origins, ["*"]
|
|
5
|
+
option :methods, ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"]
|
|
6
|
+
option :headers, ["Content-Type", "Authorization"]
|
|
7
|
+
option :allow_credentials, false
|
|
8
|
+
option :max_age, 86400
|
|
9
|
+
|
|
10
|
+
def self.on_load(app)
|
|
11
|
+
allowed = options[:origins]
|
|
12
|
+
methods = options[:methods].join(", ")
|
|
13
|
+
headers = options[:headers].join(", ")
|
|
14
|
+
credentials = options[:allow_credentials]
|
|
15
|
+
max_age = options[:max_age]
|
|
16
|
+
|
|
17
|
+
app.before do |ctx|
|
|
18
|
+
origin = ctx.request.get_header("HTTP_ORIGIN")
|
|
19
|
+
next unless allowed.include?("*") || allowed.include?(origin)
|
|
20
|
+
|
|
21
|
+
ctx.response_headers["access-control-allow-origin"] = origin || "*"
|
|
22
|
+
ctx.response_headers["access-control-allow-methods"] = methods
|
|
23
|
+
ctx.response_headers["access-control-allow-headers"] = headers
|
|
24
|
+
ctx.response_headers["access-control-allow-credentials"] = credentials.to_s if credentials
|
|
25
|
+
ctx.response_headers["access-control-max-age"] = max_age.to_s
|
|
26
|
+
|
|
27
|
+
if ctx.request.request_method == "OPTIONS"
|
|
28
|
+
ctx.response_status = 204
|
|
29
|
+
ctx.response_body = ""
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
require "base64"
|
|
3
|
+
require "openssl"
|
|
4
|
+
require "jwt"
|
|
5
|
+
|
|
6
|
+
module RubyAPI
|
|
7
|
+
module Plugins
|
|
8
|
+
class JWT < Plugin
|
|
9
|
+
option :secret, "default_secret"
|
|
10
|
+
option :algorithm, "HS256"
|
|
11
|
+
option :header, "Authorization"
|
|
12
|
+
option :prefix, "Bearer "
|
|
13
|
+
|
|
14
|
+
def self.decode(token)
|
|
15
|
+
secret = options[:secret]
|
|
16
|
+
algorithm = options[:algorithm]
|
|
17
|
+
payload, _header = ::JWT.decode(token, secret, true, algorithm: algorithm)
|
|
18
|
+
payload
|
|
19
|
+
rescue StandardError
|
|
20
|
+
nil
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def self.encode(payload)
|
|
24
|
+
secret = options[:secret]
|
|
25
|
+
algorithm = options[:algorithm]
|
|
26
|
+
::JWT.encode(payload, secret, algorithm)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def self.on_load(app)
|
|
30
|
+
app.before do |ctx|
|
|
31
|
+
header_value = ctx.request.get_header("HTTP_AUTHORIZATION")
|
|
32
|
+
next unless header_value
|
|
33
|
+
|
|
34
|
+
prefix = options[:prefix]
|
|
35
|
+
token = header_value.sub(/\A#{Regexp.escape(prefix)}/, "")
|
|
36
|
+
payload = decode(token)
|
|
37
|
+
ctx.set(:jwt_payload, payload) if payload
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
module RubyAPI
|
|
2
|
+
class Router
|
|
3
|
+
attr_reader :routes, :current_prefix
|
|
4
|
+
|
|
5
|
+
def initialize
|
|
6
|
+
@routes = []
|
|
7
|
+
@trie = TrieNode.new
|
|
8
|
+
@current_prefix = ""
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def add_route(method, path, **opts, &block)
|
|
12
|
+
full_path = @current_prefix + path
|
|
13
|
+
pattern = compile_pattern(full_path)
|
|
14
|
+
param_names = extract_param_names(full_path)
|
|
15
|
+
|
|
16
|
+
route = {
|
|
17
|
+
method: method,
|
|
18
|
+
path: full_path,
|
|
19
|
+
pattern: pattern,
|
|
20
|
+
param_names: param_names,
|
|
21
|
+
block: block,
|
|
22
|
+
params: opts[:params] || {},
|
|
23
|
+
body: opts[:body],
|
|
24
|
+
injected_deps: opts[:inject] || [],
|
|
25
|
+
dependency_checks: opts[:depends] || []
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
@routes << route
|
|
29
|
+
@trie.insert(full_path, route)
|
|
30
|
+
route
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def push_prefix(prefix)
|
|
34
|
+
@current_prefix = @current_prefix + prefix
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def pop_prefix(prefix)
|
|
38
|
+
@current_prefix = @current_prefix[0...-prefix.length]
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def find(method, path)
|
|
42
|
+
route = @trie.search(path)
|
|
43
|
+
return nil unless route
|
|
44
|
+
return nil unless route[:method].to_s == method.to_s
|
|
45
|
+
route
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
def compile_pattern(path)
|
|
51
|
+
segments = path.split("/")
|
|
52
|
+
pattern_parts = segments.map do |seg|
|
|
53
|
+
if seg.start_with?(":")
|
|
54
|
+
"([^/]+)"
|
|
55
|
+
elsif seg == "*"
|
|
56
|
+
"(.+)"
|
|
57
|
+
else
|
|
58
|
+
Regexp.escape(seg)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
/\A#{pattern_parts.join("/")}\z/
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def extract_param_names(path)
|
|
65
|
+
path.split("/").select { |s| s.start_with?(":") }.map { |s| s[1..].to_sym }
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
class TrieNode
|
|
70
|
+
attr_accessor :children, :route
|
|
71
|
+
|
|
72
|
+
def initialize
|
|
73
|
+
@children = {}
|
|
74
|
+
@route = nil
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def insert(path, route)
|
|
78
|
+
segments = path.split("/").reject(&:empty?)
|
|
79
|
+
node = self
|
|
80
|
+
segments.each do |seg|
|
|
81
|
+
child_key = seg.start_with?(":") ? :__param__ : seg
|
|
82
|
+
node.children[child_key] ||= TrieNode.new
|
|
83
|
+
node = node.children[child_key]
|
|
84
|
+
end
|
|
85
|
+
node.route = route
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def search(path)
|
|
89
|
+
segments = path.split("/").reject(&:empty?)
|
|
90
|
+
node = self
|
|
91
|
+
segments.each do |seg|
|
|
92
|
+
child = node.children[seg] || node.children[:__param__]
|
|
93
|
+
return nil unless child
|
|
94
|
+
node = child
|
|
95
|
+
end
|
|
96
|
+
node.route
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
module RubyAPI
|
|
2
|
+
class Schema
|
|
3
|
+
class ValidationError < StandardError
|
|
4
|
+
attr_reader :errors
|
|
5
|
+
|
|
6
|
+
def initialize(errors)
|
|
7
|
+
@errors = errors
|
|
8
|
+
super("Validation failed: #{errors.map { |k, v| "#{k}: #{v}" }.join(', ')}")
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def self.field(name, type: String)
|
|
13
|
+
@fields ||= {}
|
|
14
|
+
@fields[name] = type
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def self.fields
|
|
18
|
+
@fields || {}
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def self.inherited(subclass)
|
|
22
|
+
subclass.instance_variable_set(:@fields, (@fields || {}).dup)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def self.validate(data)
|
|
26
|
+
errors = {}
|
|
27
|
+
result = {}
|
|
28
|
+
|
|
29
|
+
fields.each do |name, type|
|
|
30
|
+
value = data.is_a?(Hash) ? (data[name.to_s] || data[name]) : nil
|
|
31
|
+
if value.nil?
|
|
32
|
+
errors[name] = "is required"
|
|
33
|
+
next
|
|
34
|
+
end
|
|
35
|
+
begin
|
|
36
|
+
result[name] = ParamConverters.convert(value, type)
|
|
37
|
+
rescue ConversionError => e
|
|
38
|
+
errors[name] = e.message
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
raise ValidationError.new(errors) if errors.any?
|
|
43
|
+
result
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
module RubyAPI
|
|
2
|
+
module Serializer
|
|
3
|
+
def self.included(base)
|
|
4
|
+
base.extend(ClassMethods)
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
module ClassMethods
|
|
8
|
+
def field(name, type: String)
|
|
9
|
+
@fields ||= {}
|
|
10
|
+
@fields[name] = type
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def fields
|
|
14
|
+
@fields || {}
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def serialize(object)
|
|
19
|
+
self.class.fields.each_with_object({}) do |(name, type), hash|
|
|
20
|
+
value = object.send(name)
|
|
21
|
+
hash[name] = value
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
require "rack"
|
|
2
|
+
require "json"
|
|
3
|
+
require "base64"
|
|
4
|
+
require "openssl"
|
|
5
|
+
|
|
6
|
+
module RubyAPI
|
|
7
|
+
class Session
|
|
8
|
+
SESSION_COOKIE = "_rubyapi_session".freeze
|
|
9
|
+
|
|
10
|
+
def initialize(request, secret_key: nil)
|
|
11
|
+
@request = request
|
|
12
|
+
@secret_key = secret_key || "rubyapi_default_secret"
|
|
13
|
+
@data = load_session
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def [](key)
|
|
17
|
+
@data[key.to_s]
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def []=(key, value)
|
|
21
|
+
@data[key.to_s] = value
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def to_hash
|
|
25
|
+
@data.dup
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def serialize
|
|
29
|
+
json = JSON.generate(@data)
|
|
30
|
+
encode(json)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def load_session
|
|
36
|
+
cookie_value = @request.cookies[SESSION_COOKIE]
|
|
37
|
+
return {} unless cookie_value
|
|
38
|
+
json = decode(cookie_value)
|
|
39
|
+
JSON.parse(json)
|
|
40
|
+
rescue StandardError
|
|
41
|
+
{}
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def encode(data)
|
|
45
|
+
encoded = Base64.strict_encode64(data)
|
|
46
|
+
signature = sign(encoded)
|
|
47
|
+
"#{encoded}.#{signature}"
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def decode(data)
|
|
51
|
+
encoded, signature = data.to_s.split(".", 2)
|
|
52
|
+
return nil unless encoded && signature
|
|
53
|
+
expected = sign(encoded)
|
|
54
|
+
return nil unless Rack::Utils.secure_compare(signature, expected)
|
|
55
|
+
Base64.strict_decode64(encoded)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def sign(data)
|
|
59
|
+
OpenSSL::HMAC.hexdigest("SHA256", @secret_key, data)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
data/lib/rubyapi/sse.rb
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
module RubyAPI
|
|
2
|
+
class SSE
|
|
3
|
+
attr_reader :io
|
|
4
|
+
|
|
5
|
+
def initialize(io)
|
|
6
|
+
@io = io
|
|
7
|
+
@closed = false
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def send(event:, data:, id: nil)
|
|
11
|
+
return if @closed
|
|
12
|
+
@io.write("event: #{event}\n") if event
|
|
13
|
+
@io.write("id: #{id}\n") if id
|
|
14
|
+
data.to_s.each_line do |line|
|
|
15
|
+
@io.write("data: #{line}")
|
|
16
|
+
end
|
|
17
|
+
@io.write("\n")
|
|
18
|
+
@io.flush
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def close
|
|
22
|
+
@closed = true
|
|
23
|
+
@io.close if @io.respond_to?(:close) && !@io.closed?
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def closed?
|
|
27
|
+
@closed
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
require "securerandom"
|
|
2
|
+
|
|
3
|
+
module RubyAPI
|
|
4
|
+
class StreamingBody
|
|
5
|
+
include Enumerable
|
|
6
|
+
|
|
7
|
+
def initialize
|
|
8
|
+
@buffer = Queue.new
|
|
9
|
+
@closed = false
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def write(data)
|
|
13
|
+
@buffer << data.to_s
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def close
|
|
17
|
+
@closed = true
|
|
18
|
+
@buffer << nil
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def each
|
|
22
|
+
loop do
|
|
23
|
+
data = @buffer.pop
|
|
24
|
+
break if data.nil?
|
|
25
|
+
yield data
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def closed?
|
|
30
|
+
@closed
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
class SSEStream
|
|
35
|
+
attr_reader :body
|
|
36
|
+
|
|
37
|
+
def initialize
|
|
38
|
+
@body = StreamingBody.new
|
|
39
|
+
@sse = SSE.new(@body)
|
|
40
|
+
@closed = false
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def send_event(event:, data:, id: nil)
|
|
44
|
+
return if @closed
|
|
45
|
+
@sse.send(event: event, data: data, id: id)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def close
|
|
49
|
+
@closed = true
|
|
50
|
+
@sse.close
|
|
51
|
+
@body.close
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def closed?
|
|
55
|
+
@closed
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
module RubyAPI
|
|
2
|
+
class WebSocket
|
|
3
|
+
attr_reader :env, :path
|
|
4
|
+
|
|
5
|
+
def initialize(env, &handler)
|
|
6
|
+
@env = env
|
|
7
|
+
@handler = handler
|
|
8
|
+
@messages = Queue.new
|
|
9
|
+
@closed = false
|
|
10
|
+
@on_message = nil
|
|
11
|
+
@on_close = nil
|
|
12
|
+
@on_open = nil
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def on_open(&block)
|
|
16
|
+
@on_open = block
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def on_message(&block)
|
|
20
|
+
@on_message = block
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def on_close(&block)
|
|
24
|
+
@on_close = block
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def send(data)
|
|
28
|
+
@messages << { type: :send, data: data }
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def close(code = 1000, reason = "")
|
|
32
|
+
@messages << { type: :close, code: code, reason: reason }
|
|
33
|
+
@closed = true
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def closed?
|
|
37
|
+
@closed
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def receive
|
|
41
|
+
msg = @messages.pop
|
|
42
|
+
msg
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def hijack?
|
|
46
|
+
true
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def process!
|
|
50
|
+
@on_open&.call(self)
|
|
51
|
+
@handler.call(self) if @handler
|
|
52
|
+
rescue => e
|
|
53
|
+
@on_close&.call(e) if @on_close
|
|
54
|
+
ensure
|
|
55
|
+
@on_close&.call(nil) unless @closed
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def self.upgrade(env)
|
|
59
|
+
io = env["rack.hijack"]&.call
|
|
60
|
+
return nil unless io
|
|
61
|
+
io
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|