rivulet-rb 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/rivulet +4 -0
- data/lib/rivulet/application.rb +90 -0
- data/lib/rivulet/cli/console.rb +15 -0
- data/lib/rivulet/cli/db/migrate.rb +17 -0
- data/lib/rivulet/cli/generate/handler/operation.rb +75 -0
- data/lib/rivulet/cli/generate/handler/step.rb +91 -0
- data/lib/rivulet/cli/generate/handler.rb +135 -0
- data/lib/rivulet/cli/generate/migration.rb +31 -0
- data/lib/rivulet/cli/generate/operation.rb +111 -0
- data/lib/rivulet/cli/generate/resource.rb +25 -0
- data/lib/rivulet/cli/generate/service/operation.rb +118 -0
- data/lib/rivulet/cli/generate/service/projection.rb +89 -0
- data/lib/rivulet/cli/generate/service/step.rb +91 -0
- data/lib/rivulet/cli/generate/service.rb +143 -0
- data/lib/rivulet/cli/new.rb +191 -0
- data/lib/rivulet/cli/routes.rb +15 -0
- data/lib/rivulet/cli.rb +43 -0
- data/lib/rivulet/container.rb +28 -0
- data/lib/rivulet/operation.rb +21 -0
- data/lib/rivulet/operations/dispatch_request.rb +21 -0
- data/lib/rivulet/operations/migrate.rb +23 -0
- data/lib/rivulet/operations/print_routes.rb +17 -0
- data/lib/rivulet/operations/run_console.rb +25 -0
- data/lib/rivulet/operations/startup.rb +23 -0
- data/lib/rivulet/projection.rb +6 -0
- data/lib/rivulet/request.rb +18 -0
- data/lib/rivulet/response.rb +12 -0
- data/lib/rivulet/routing/mapper.rb +78 -0
- data/lib/rivulet/routing/route.rb +14 -0
- data/lib/rivulet/step.rb +22 -0
- data/lib/rivulet/steps/build_config.rb +26 -0
- data/lib/rivulet/steps/build_context.rb +76 -0
- data/lib/rivulet/steps/compile_response.rb +113 -0
- data/lib/rivulet/steps/dispatch.rb +17 -0
- data/lib/rivulet/steps/load_app.rb +20 -0
- data/lib/rivulet/steps/load_db.rb +24 -0
- data/lib/rivulet/steps/load_routes.rb +28 -0
- data/lib/rivulet/steps/load_settings.rb +42 -0
- data/lib/rivulet/steps/print_routes.rb +123 -0
- data/lib/rivulet/steps/run_console.rb +26 -0
- data/lib/rivulet/steps/run_migrations.rb +50 -0
- data/lib/rivulet/steps/validate_response.rb +42 -0
- data/lib/rivulet/telemetry/node.rb +8 -0
- data/lib/rivulet/telemetry/sequel_extension.rb +19 -0
- data/lib/rivulet/telemetry/timing_wrapper.rb +12 -0
- data/lib/rivulet/telemetry.rb +62 -0
- data/lib/rivulet/version.rb +3 -0
- data/lib/rivulet.rb +66 -0
- metadata +342 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
module Rivulet
|
|
2
|
+
class Container
|
|
3
|
+
extend Dry::Core::Container::Mixin
|
|
4
|
+
|
|
5
|
+
namespace('operations') do
|
|
6
|
+
register('startup') { Rivulet::Operations::Startup.new }
|
|
7
|
+
register('dispatch_request') { Rivulet::Operations::DispatchRequest.new }
|
|
8
|
+
register('migrate') { Rivulet::Operations::Migrate.new }
|
|
9
|
+
register('run_console') { Rivulet::Operations::RunConsole.new }
|
|
10
|
+
register('print_routes') { Rivulet::Operations::PrintRoutes.new }
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
namespace('steps') do
|
|
14
|
+
register('build_config') { Rivulet::Steps::BuildConfig.new }
|
|
15
|
+
register('build_context') { Rivulet::Steps::BuildContext.new }
|
|
16
|
+
register('validate_response') { Rivulet::Steps::ValidateResponse.new }
|
|
17
|
+
register('compile_response') { Rivulet::Steps::CompileResponse.new }
|
|
18
|
+
register('dispatch') { Rivulet::Steps::Dispatch.new }
|
|
19
|
+
register('load_app') { Rivulet::Steps::LoadApp.new }
|
|
20
|
+
register('load_settings') { Rivulet::Steps::LoadSettings.new }
|
|
21
|
+
register('load_db') { Rivulet::Steps::LoadDb.new }
|
|
22
|
+
register('load_routes') { Rivulet::Steps::LoadRoutes.new }
|
|
23
|
+
register('run_migrations') { Rivulet::Steps::RunMigrations.new }
|
|
24
|
+
register('run_console') { Rivulet::Steps::RunConsole.new }
|
|
25
|
+
register('print_routes') { Rivulet::Steps::PrintRoutes.new }
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
module Rivulet
|
|
2
|
+
class Operation < Dry::Operation
|
|
3
|
+
def self.container_class_path
|
|
4
|
+
self
|
|
5
|
+
.name
|
|
6
|
+
.split('::')
|
|
7
|
+
.then { |path| path[0...path.index('Operations')] }
|
|
8
|
+
.push('Container')
|
|
9
|
+
.inject(Object) { |mod, name| mod.const_get(name) }
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def self.inherited(subclass)
|
|
13
|
+
super
|
|
14
|
+
|
|
15
|
+
subclass.prepend(Rivulet::Telemetry::TimingWrapper)
|
|
16
|
+
subclass.const_set(
|
|
17
|
+
:Import, Dry::AutoInject(subclass.container_class_path)
|
|
18
|
+
)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
module Rivulet
|
|
2
|
+
module Operations
|
|
3
|
+
class DispatchRequest < Rivulet::Operation
|
|
4
|
+
include Import[
|
|
5
|
+
build_context: 'steps.build_context',
|
|
6
|
+
dispatch: 'steps.dispatch',
|
|
7
|
+
validate_response: 'steps.validate_response',
|
|
8
|
+
compile_response: 'steps.compile_response'
|
|
9
|
+
]
|
|
10
|
+
|
|
11
|
+
def call(input = {})
|
|
12
|
+
result = step build_context.(input)
|
|
13
|
+
result = step dispatch.(result)
|
|
14
|
+
result = step validate_response.(result)
|
|
15
|
+
result = step compile_response.(result)
|
|
16
|
+
|
|
17
|
+
result
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
module Rivulet
|
|
2
|
+
module Operations
|
|
3
|
+
class Migrate < Rivulet::Operation
|
|
4
|
+
include Import[
|
|
5
|
+
build_config: 'steps.build_config',
|
|
6
|
+
load_settings: 'steps.load_settings',
|
|
7
|
+
load_app: 'steps.load_app',
|
|
8
|
+
load_db: 'steps.load_db',
|
|
9
|
+
run_migrations: 'steps.run_migrations'
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
def call(input = {})
|
|
13
|
+
result = step build_config.(input)
|
|
14
|
+
result = step load_settings.(result)
|
|
15
|
+
result = step load_db.(result)
|
|
16
|
+
result = step load_app.(result)
|
|
17
|
+
result = step run_migrations.(result)
|
|
18
|
+
|
|
19
|
+
result
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
module Rivulet
|
|
2
|
+
module Operations
|
|
3
|
+
class PrintRoutes < Rivulet::Operation
|
|
4
|
+
include Import[
|
|
5
|
+
load_routes: 'steps.load_routes',
|
|
6
|
+
print_routes: 'steps.print_routes'
|
|
7
|
+
]
|
|
8
|
+
|
|
9
|
+
def call(input = {})
|
|
10
|
+
result = step load_routes.(input)
|
|
11
|
+
result = step print_routes.(result)
|
|
12
|
+
|
|
13
|
+
result
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
module Rivulet
|
|
2
|
+
module Operations
|
|
3
|
+
class RunConsole < Rivulet::Operation
|
|
4
|
+
include Import[
|
|
5
|
+
build_config: 'steps.build_config',
|
|
6
|
+
load_settings: 'steps.load_settings',
|
|
7
|
+
load_app: 'steps.load_app',
|
|
8
|
+
load_db: 'steps.load_db',
|
|
9
|
+
load_routes: 'steps.load_routes',
|
|
10
|
+
run_console: 'steps.run_console'
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
def call(input = {})
|
|
14
|
+
result = step build_config.(input)
|
|
15
|
+
result = step load_settings.(result)
|
|
16
|
+
result = step load_db.(result)
|
|
17
|
+
result = step load_app.(result)
|
|
18
|
+
result = step load_routes.(result)
|
|
19
|
+
result = step run_console.(result)
|
|
20
|
+
|
|
21
|
+
result
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
module Rivulet
|
|
2
|
+
module Operations
|
|
3
|
+
class Startup < Rivulet::Operation
|
|
4
|
+
include Import[
|
|
5
|
+
build_config: 'steps.build_config',
|
|
6
|
+
load_settings: 'steps.load_settings',
|
|
7
|
+
load_app: 'steps.load_app',
|
|
8
|
+
load_db: 'steps.load_db',
|
|
9
|
+
load_routes: 'steps.load_routes'
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
def call(input = {})
|
|
13
|
+
result = step build_config.(input)
|
|
14
|
+
result = step load_settings.(result)
|
|
15
|
+
result = step load_db.(result)
|
|
16
|
+
result = step load_app.(result)
|
|
17
|
+
result = step load_routes.(result)
|
|
18
|
+
|
|
19
|
+
result
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
module Rivulet
|
|
2
|
+
class Request
|
|
3
|
+
attr_reader :http_method, :path, :content_type, :body, :session, :env
|
|
4
|
+
|
|
5
|
+
def initialize(env)
|
|
6
|
+
@http_method = env['REQUEST_METHOD'].downcase.to_sym
|
|
7
|
+
@path = env['PATH_INFO']
|
|
8
|
+
@content_type = env['CONTENT_TYPE']
|
|
9
|
+
@body = env['rack.input']
|
|
10
|
+
@session = env['rack.session']
|
|
11
|
+
@env = env
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def cookies
|
|
15
|
+
Rack::Utils.parse_cookies(env)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
module Rivulet
|
|
2
|
+
module Routing
|
|
3
|
+
class Mapper
|
|
4
|
+
include Enumerable
|
|
5
|
+
|
|
6
|
+
def each(&block)
|
|
7
|
+
@routes.each(&block)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def initialize(routes, prefix: '', scopes: [])
|
|
11
|
+
@routes = routes
|
|
12
|
+
@prefix = prefix
|
|
13
|
+
@scopes = scopes
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
%i[get post put patch delete].each do |method|
|
|
17
|
+
define_method(method) do |path, to:|
|
|
18
|
+
handler_name = case to
|
|
19
|
+
when String then to
|
|
20
|
+
when Hash then "#{to[:to]}#{'#' + to[:action] if to[:action]}"
|
|
21
|
+
else nil
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
route_path = join(@prefix, path.to_s)
|
|
25
|
+
path_regex, param_names = compile_path(route_path)
|
|
26
|
+
|
|
27
|
+
@routes << Route.new(
|
|
28
|
+
http_method: method,
|
|
29
|
+
path: route_path,
|
|
30
|
+
callable: build_callable(to),
|
|
31
|
+
scopes: @scopes,
|
|
32
|
+
handler_name: handler_name,
|
|
33
|
+
path_regex: path_regex,
|
|
34
|
+
param_names: param_names
|
|
35
|
+
)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def draw(&block)
|
|
40
|
+
instance_eval(&block)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def scope(name, &block)
|
|
44
|
+
Mapper.new(
|
|
45
|
+
@routes,
|
|
46
|
+
prefix: join(@prefix, name.to_s),
|
|
47
|
+
scopes: @scopes + [name]
|
|
48
|
+
).instance_eval(&block)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def build_callable(to)
|
|
52
|
+
case to
|
|
53
|
+
when String
|
|
54
|
+
handler, action = to.split('#')
|
|
55
|
+
when Hash
|
|
56
|
+
handler, action = to.values_at(:to, :action)
|
|
57
|
+
else
|
|
58
|
+
raise 'Cannot parse route handler'
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
->(input) { ::Handlers[handler].send(action, input) }
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
private
|
|
65
|
+
|
|
66
|
+
def join(*parts)
|
|
67
|
+
'/' + parts.flat_map { |p| p.to_s.split('/') }.reject(&:empty?).join('/')
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def compile_path(path)
|
|
71
|
+
param_names = path.scan(/:([^\/]+)/).flatten.map(&:to_sym)
|
|
72
|
+
regex = /\A#{Regexp.escape(path).gsub(/:[^\/]+/, '([^/]+)')}\z/
|
|
73
|
+
|
|
74
|
+
[regex, param_names]
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
data/lib/rivulet/step.rb
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
module Rivulet
|
|
2
|
+
class Step
|
|
3
|
+
include Dry::Monads[:result]
|
|
4
|
+
|
|
5
|
+
def self.container_class_path
|
|
6
|
+
self
|
|
7
|
+
.name
|
|
8
|
+
.split('::')
|
|
9
|
+
.then { |path| path[0...path.index('Steps')] }
|
|
10
|
+
.push('Container')
|
|
11
|
+
.inject(Object) { |mod, name| mod.const_get(name) }
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def self.inherited(subclass)
|
|
15
|
+
super
|
|
16
|
+
subclass.prepend(Rivulet::Telemetry::TimingWrapper)
|
|
17
|
+
subclass.const_set(
|
|
18
|
+
:Import, Dry::AutoInject(subclass.container_class_path)
|
|
19
|
+
)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
module Rivulet
|
|
2
|
+
module Steps
|
|
3
|
+
class BuildConfig < Rivulet::Step
|
|
4
|
+
def call(input)
|
|
5
|
+
Rivulet::Application.setting :database do
|
|
6
|
+
setting :dsn
|
|
7
|
+
setting :pool
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
Rivulet::Application.setting :logger, reader: true do
|
|
11
|
+
setting :engine
|
|
12
|
+
setting :name
|
|
13
|
+
setting :level
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
Rivulet::Application.setting :sendfile do
|
|
17
|
+
setting :enabled, default: false
|
|
18
|
+
setting :variation, default: 'x-accel-redirect'
|
|
19
|
+
setting :mappings, default: []
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
Success(input)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
module Rivulet
|
|
2
|
+
module Steps
|
|
3
|
+
class BuildContext < Rivulet::Step
|
|
4
|
+
def call(input)
|
|
5
|
+
input => { env:, resource: }
|
|
6
|
+
|
|
7
|
+
request = Rivulet::Request.new(env)
|
|
8
|
+
routes = resource.routes
|
|
9
|
+
|
|
10
|
+
route, path_match = find_route(routes, request)
|
|
11
|
+
return Failure[:route_not_found] unless route
|
|
12
|
+
|
|
13
|
+
input.merge!(
|
|
14
|
+
route: route,
|
|
15
|
+
params: build_params(route, request, path_match),
|
|
16
|
+
context: {
|
|
17
|
+
headers: extract_headers(request),
|
|
18
|
+
cookies: request.cookies,
|
|
19
|
+
session: request.session
|
|
20
|
+
}
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
resource.logger.info(
|
|
24
|
+
"Request #{request.http_method.upcase} #{request.path} #{input[:params]}"
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
Success(input)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def build_params(route, request, path_match)
|
|
33
|
+
path_params = extract_params(route, path_match)
|
|
34
|
+
body_params = parse_body(request)
|
|
35
|
+
body_params.merge(path_params)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def parse_body(request)
|
|
39
|
+
return {} unless request.content_type&.include?('application/json')
|
|
40
|
+
raw = request.body.read
|
|
41
|
+
return {} if raw.nil? || raw.empty?
|
|
42
|
+
Oj.load(raw, symbolize_names: true)
|
|
43
|
+
rescue Oj::ParseError
|
|
44
|
+
{}
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def find_route(routes, request)
|
|
48
|
+
request_method = request.http_method
|
|
49
|
+
path_info = request.path
|
|
50
|
+
|
|
51
|
+
routes.each do |route|
|
|
52
|
+
next unless route.http_method == request_method
|
|
53
|
+
|
|
54
|
+
path_match = route.path_regex.match(path_info)
|
|
55
|
+
return [route, path_match] if path_match
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
[nil, nil]
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def extract_headers(request)
|
|
62
|
+
request.env.each_with_object({}) do |(key, value), headers|
|
|
63
|
+
if key.start_with?('HTTP_')
|
|
64
|
+
headers[key[5..].split('_').map(&:capitalize).join('-')] = value
|
|
65
|
+
elsif (key == 'CONTENT_TYPE' || key == 'CONTENT_LENGTH') && !value.to_s.empty?
|
|
66
|
+
headers[key.split('_').map(&:capitalize).join('-')] = value
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def extract_params(route, path_match)
|
|
72
|
+
route.param_names.zip(path_match.captures).to_h
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
module Rivulet
|
|
2
|
+
module Steps
|
|
3
|
+
class CompileResponse < Rivulet::Step
|
|
4
|
+
def call(input)
|
|
5
|
+
input => { resource:, response: }
|
|
6
|
+
|
|
7
|
+
status = response.status
|
|
8
|
+
format = response.format
|
|
9
|
+
|
|
10
|
+
body, headers =
|
|
11
|
+
case format
|
|
12
|
+
when :json
|
|
13
|
+
payload = Oj.dump(response.body, mode: :json)
|
|
14
|
+
|
|
15
|
+
[
|
|
16
|
+
Protocol::HTTP::Body::Buffered.wrap(payload),
|
|
17
|
+
{
|
|
18
|
+
'Content-Type' => 'application/json',
|
|
19
|
+
'Content-Length' => payload.bytesize.to_s
|
|
20
|
+
}
|
|
21
|
+
]
|
|
22
|
+
when :text
|
|
23
|
+
payload = response.body.to_s
|
|
24
|
+
|
|
25
|
+
[
|
|
26
|
+
Protocol::HTTP::Body::Buffered.wrap(payload),
|
|
27
|
+
{
|
|
28
|
+
'Content-Type' => 'text/plain; charset=utf-8',
|
|
29
|
+
'Content-Length' => payload.bytesize.to_s
|
|
30
|
+
}
|
|
31
|
+
]
|
|
32
|
+
when :file
|
|
33
|
+
result = build_file_body(response.body, resource)
|
|
34
|
+
return result if result in Failure
|
|
35
|
+
result
|
|
36
|
+
when :stream
|
|
37
|
+
[Protocol::HTTP::Body::Stream.new(response.body), {}]
|
|
38
|
+
when :as_is
|
|
39
|
+
[response.body, {}]
|
|
40
|
+
else
|
|
41
|
+
[[], {}]
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
headers.merge!(response.headers)
|
|
45
|
+
|
|
46
|
+
input[:response] = [status, headers, body]
|
|
47
|
+
|
|
48
|
+
Success(input)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
def build_file_body(body, app)
|
|
54
|
+
path = body.is_a?(Hash) ? body[:path] : body
|
|
55
|
+
|
|
56
|
+
if app.config.sendfile.enabled
|
|
57
|
+
build_sendfile_body(body, path, app.config.sendfile)
|
|
58
|
+
else
|
|
59
|
+
build_streaming_file_body(body, path)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def build_streaming_file_body(body, path)
|
|
64
|
+
file_body =
|
|
65
|
+
begin
|
|
66
|
+
Protocol::HTTP::Body::File.open(path)
|
|
67
|
+
rescue Errno::ENOENT, Errno::EACCES
|
|
68
|
+
return Failure[:file_not_found, "Cannot read file: #{path}"]
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
[file_body, file_headers(body, file_body.length)]
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def build_sendfile_body(body, path, sendfile_config)
|
|
75
|
+
uri = map_sendfile_path(path, sendfile_config.mappings)
|
|
76
|
+
headers = file_headers(body, 0)
|
|
77
|
+
headers[sendfile_config.variation.downcase] = uri
|
|
78
|
+
[[], headers]
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def map_sendfile_path(path, mappings)
|
|
82
|
+
return path if mappings.empty?
|
|
83
|
+
|
|
84
|
+
mappings.each do |internal, external|
|
|
85
|
+
mapped = path.sub(/\A#{Regexp.escape(internal)}/i, external)
|
|
86
|
+
return mapped unless mapped == path
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
path
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def file_headers(body, length)
|
|
93
|
+
if body.is_a?(Hash)
|
|
94
|
+
path = body[:path]
|
|
95
|
+
filename = body.fetch(:filename) { File.basename(path) }
|
|
96
|
+
disposition = body.fetch(:disposition, 'inline')
|
|
97
|
+
mime_type = body.fetch(:mime_type) { Rack::Mime.mime_type(File.extname(filename)) }
|
|
98
|
+
|
|
99
|
+
{
|
|
100
|
+
'Content-Type' => mime_type,
|
|
101
|
+
'Content-Length' => length.to_s,
|
|
102
|
+
'Content-Disposition' => "#{disposition}; filename=\"#{filename}\""
|
|
103
|
+
}
|
|
104
|
+
else
|
|
105
|
+
{
|
|
106
|
+
'Content-Type' => Rack::Mime.mime_type(File.extname(body)),
|
|
107
|
+
'Content-Length' => length.to_s
|
|
108
|
+
}
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
module Rivulet
|
|
2
|
+
module Steps
|
|
3
|
+
class Dispatch < Rivulet::Step
|
|
4
|
+
def call(input)
|
|
5
|
+
input => { route:, params:, context: }
|
|
6
|
+
|
|
7
|
+
result = route.callable.call(params: params, context: context)
|
|
8
|
+
|
|
9
|
+
# We don't care if it's success or failure.
|
|
10
|
+
# The result will be a response anyway.
|
|
11
|
+
input[:response] = result.value_or(result.failure)
|
|
12
|
+
|
|
13
|
+
Success(input)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
require 'zeitwerk'
|
|
2
|
+
|
|
3
|
+
module Rivulet
|
|
4
|
+
module Steps
|
|
5
|
+
class LoadApp < Rivulet::Step
|
|
6
|
+
def call(input)
|
|
7
|
+
app_dir = File.expand_path('app')
|
|
8
|
+
return Success(input) unless Dir.exist?(app_dir)
|
|
9
|
+
|
|
10
|
+
loader = Zeitwerk::Loader.new
|
|
11
|
+
loader.push_dir(app_dir)
|
|
12
|
+
loader.push_dir("#{app_dir}/models")
|
|
13
|
+
loader.setup
|
|
14
|
+
loader.eager_load
|
|
15
|
+
|
|
16
|
+
Success(input)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
require 'sequel'
|
|
2
|
+
require_relative '../telemetry/sequel_extension'
|
|
3
|
+
|
|
4
|
+
module Rivulet
|
|
5
|
+
module Steps
|
|
6
|
+
class LoadDb < Rivulet::Step
|
|
7
|
+
def call(input)
|
|
8
|
+
app = input[:resource]
|
|
9
|
+
db_config = app.config.database
|
|
10
|
+
return Failure("Rivulet.config.database.dsn is required") if db_config&.dsn.to_s.empty?
|
|
11
|
+
|
|
12
|
+
pool = db_config.pool&.to_h || {}
|
|
13
|
+
db = Sequel.connect(db_config.dsn, **pool, logger: app.logger, sql_log_level: :debug)
|
|
14
|
+
db.extension(:rivulet_telemetry)
|
|
15
|
+
app.db = db
|
|
16
|
+
|
|
17
|
+
Sequel::Model.db = db
|
|
18
|
+
Sequel::Model.require_valid_table = false
|
|
19
|
+
|
|
20
|
+
Success(input)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
module Rivulet
|
|
2
|
+
module Steps
|
|
3
|
+
class LoadRoutes < Rivulet::Step
|
|
4
|
+
def call(input)
|
|
5
|
+
routes_file = File.expand_path('config/routes.rb')
|
|
6
|
+
return Failure("Routes file not found: #{routes_file}") unless File.exist?(routes_file)
|
|
7
|
+
|
|
8
|
+
load(routes_file)
|
|
9
|
+
|
|
10
|
+
duplicates = duplicate_routes(input[:resource].routes)
|
|
11
|
+
return Failure(duplicate_message(duplicates)) unless duplicates.empty?
|
|
12
|
+
|
|
13
|
+
Success(input)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
def duplicate_routes(routes)
|
|
19
|
+
routes.group_by { |r| [r.http_method, r.path] }.select { |_, v| v.size > 1 }
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def duplicate_message(duplicates)
|
|
23
|
+
list = duplicates.keys.map { |m, p| "#{m.to_s.upcase} #{p}" }.join(', ')
|
|
24
|
+
"Duplicate routes: #{list}"
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
module Rivulet
|
|
2
|
+
module Steps
|
|
3
|
+
class LoadSettings < Rivulet::Step
|
|
4
|
+
def call(input)
|
|
5
|
+
app = input[:resource]
|
|
6
|
+
app_file = File.expand_path('config/application.rb')
|
|
7
|
+
return Failure(:settings_file_not_found) unless File.exist?(app_file)
|
|
8
|
+
|
|
9
|
+
load app_file
|
|
10
|
+
|
|
11
|
+
app.config.logger = app.config.logger.engine || default_logger(app)
|
|
12
|
+
app.config.finalize!
|
|
13
|
+
|
|
14
|
+
Success(input)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
private
|
|
18
|
+
|
|
19
|
+
def default_logger(app)
|
|
20
|
+
Dry.Logger(app.config.logger.name, level: app.config.logger.level) do |setup|
|
|
21
|
+
setup.add_backend(
|
|
22
|
+
stream: $stdout,
|
|
23
|
+
log_if: :debug?,
|
|
24
|
+
template: '<gray>[%<severity>s]</gray> %<time>s %<message>s'
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
setup.add_backend(
|
|
28
|
+
stream: $stdout,
|
|
29
|
+
log_if: :info?,
|
|
30
|
+
template: '<blue>[%<severity>s]</blue> %<time>s %<message>s'
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
setup.add_backend(
|
|
34
|
+
stream: $stdout,
|
|
35
|
+
log_if: :error?,
|
|
36
|
+
template: '<red>[%<severity>s]</red> %<time>s %<message>s'
|
|
37
|
+
)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|