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,123 @@
|
|
|
1
|
+
require "io/console"
|
|
2
|
+
|
|
3
|
+
module Rivulet
|
|
4
|
+
module Steps
|
|
5
|
+
class PrintRoutes < Rivulet::Step
|
|
6
|
+
HEADERS = ['HTTP Verb', 'Path', 'Handler#action']
|
|
7
|
+
GAP = 2
|
|
8
|
+
|
|
9
|
+
def call(input)
|
|
10
|
+
routes = input[:resource].routes
|
|
11
|
+
|
|
12
|
+
widths = calculate_widths(routes)
|
|
13
|
+
|
|
14
|
+
print_row(HEADERS, widths)
|
|
15
|
+
puts "" # Spacer after header
|
|
16
|
+
|
|
17
|
+
routes.each do |route|
|
|
18
|
+
print_route(route, widths)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
Success(input)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def terminal_width
|
|
27
|
+
IO.console.winsize[1]
|
|
28
|
+
rescue
|
|
29
|
+
80
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def calculate_widths(routes)
|
|
33
|
+
cols = HEADERS.size
|
|
34
|
+
route_methods = [:http_method, :path, :callable]
|
|
35
|
+
|
|
36
|
+
widths = Array.new(cols, 0)
|
|
37
|
+
|
|
38
|
+
cols.times do |i|
|
|
39
|
+
widths[i] = [
|
|
40
|
+
HEADERS[i].length,
|
|
41
|
+
*routes.map { |r| r.send(route_methods[i]).to_s.length }
|
|
42
|
+
].max
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
total_width_needed = widths.sum + GAP * (cols - 1)
|
|
46
|
+
|
|
47
|
+
return widths if total_width_needed <= terminal_width
|
|
48
|
+
|
|
49
|
+
# If too wide, shrink the last column to fit, prioritizing wrapping there.
|
|
50
|
+
fixed_width = widths[0..-2].sum + GAP * (cols - 2)
|
|
51
|
+
available_for_last = terminal_width - fixed_width - GAP
|
|
52
|
+
|
|
53
|
+
widths[-1] = [available_for_last, 20].max
|
|
54
|
+
|
|
55
|
+
# Final fallback to ensure total doesn't exceed terminal width
|
|
56
|
+
while (widths.sum + GAP * (cols - 1)) > terminal_width && widths[-1] > 5
|
|
57
|
+
widths[-
|
|
58
|
+
1] -= 1
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
widths
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def print_route(route, widths)
|
|
65
|
+
handler_display = route.handler_name || route.callable.inspect
|
|
66
|
+
row_data = [route.http_method, route.path, handler_display]
|
|
67
|
+
print_row(row_data, widths)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def print_row(cells, widths)
|
|
71
|
+
wrapped_cells = cells.each_with_index.map do |cell, i|
|
|
72
|
+
wrap(cell.to_s, widths[i])
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
num_lines = wrapped_cells.map(&:size).max
|
|
76
|
+
|
|
77
|
+
num_lines.times do |line_idx|
|
|
78
|
+
line_parts = wrapped_cells.each_with_index.map do |lines, col_idx|
|
|
79
|
+
text = lines[line_idx] || ""
|
|
80
|
+
text.ljust(widths[col_idx])
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
puts line_parts.join(" " * GAP)
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def wrap(text, width)
|
|
88
|
+
return [] if text.empty?
|
|
89
|
+
|
|
90
|
+
tokens = text.split(/(\s+)/)
|
|
91
|
+
lines = []
|
|
92
|
+
current_line = ""
|
|
93
|
+
|
|
94
|
+
tokens.each do |token|
|
|
95
|
+
if (current_line + token).length <= width
|
|
96
|
+
current_line += token
|
|
97
|
+
else
|
|
98
|
+
if token.strip.empty?
|
|
99
|
+
lines << current_line.rstrip
|
|
100
|
+
current_line = ""
|
|
101
|
+
elsif token.length > width
|
|
102
|
+
parts = token.scan(/.{1,#{width}}/)
|
|
103
|
+
parts.each_with_index do |part, idx|
|
|
104
|
+
if idx == parts.size - 1
|
|
105
|
+
current_line += part
|
|
106
|
+
else
|
|
107
|
+
lines << (current_line + part).rstrip
|
|
108
|
+
current_line = ""
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
else
|
|
112
|
+
lines << current_line.rstrip
|
|
113
|
+
current_line = token
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
lines << current_line.rstrip unless current_line.empty?
|
|
119
|
+
lines.reject(&:empty?)
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
require 'irb'
|
|
2
|
+
require 'irb/completion'
|
|
3
|
+
|
|
4
|
+
module Rivulet
|
|
5
|
+
module Steps
|
|
6
|
+
class RunConsole < Rivulet::Step
|
|
7
|
+
def call(input)
|
|
8
|
+
app = input[:resource]
|
|
9
|
+
|
|
10
|
+
IRB.setup(nil)
|
|
11
|
+
|
|
12
|
+
IRB.conf[:USE_AUTOCOMPLETE] = false
|
|
13
|
+
IRB.conf[:AP_NAME] = 'rivulet'
|
|
14
|
+
|
|
15
|
+
# workspace = IRB::WorkSpace.new(binding)
|
|
16
|
+
# irb = IRB::Irb.new(workspace)
|
|
17
|
+
|
|
18
|
+
# IRB.conf[:MAIN_CONTEXT] = irb.context
|
|
19
|
+
|
|
20
|
+
IRB::Irb.new.run(IRB.conf)
|
|
21
|
+
|
|
22
|
+
Success(input)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
module Rivulet
|
|
2
|
+
module Steps
|
|
3
|
+
class RunMigrations < Rivulet::Step
|
|
4
|
+
TABLE = :schema_migrations
|
|
5
|
+
|
|
6
|
+
def call(input)
|
|
7
|
+
db = input[:resource].db
|
|
8
|
+
return Failure("Database not configured") unless db
|
|
9
|
+
|
|
10
|
+
ensure_table(db)
|
|
11
|
+
migrations = pending(db)
|
|
12
|
+
|
|
13
|
+
if migrations.empty?
|
|
14
|
+
puts " up to date"
|
|
15
|
+
else
|
|
16
|
+
migrations.each do |path|
|
|
17
|
+
version = File.basename(path, '.sql')
|
|
18
|
+
db.transaction do
|
|
19
|
+
db.run(File.read(path))
|
|
20
|
+
db[TABLE].insert(version: version)
|
|
21
|
+
end
|
|
22
|
+
puts " applied #{File.basename(path)}"
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
Success(input)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def ensure_table(db)
|
|
32
|
+
db.create_table?(TABLE) do
|
|
33
|
+
String :version, null: false
|
|
34
|
+
primary_key [:version]
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def pending(db)
|
|
39
|
+
applied = db[TABLE].select_map(:version).to_set
|
|
40
|
+
all_files.reject { |f| applied.include?(File.basename(f, '.sql')) }
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def all_files
|
|
44
|
+
dir = File.expand_path('db/migrations')
|
|
45
|
+
return [] unless Dir.exist?(dir)
|
|
46
|
+
Dir[File.join(dir, '*.sql')].sort
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
module Rivulet
|
|
2
|
+
module Steps
|
|
3
|
+
class ValidateResponse < Rivulet::Step
|
|
4
|
+
NO_BODY_STATUSES = [204, 304].freeze
|
|
5
|
+
FORMAT_VALUES = %i[json text file stream as_is].freeze
|
|
6
|
+
|
|
7
|
+
def call(input)
|
|
8
|
+
input => { response:, route: }
|
|
9
|
+
|
|
10
|
+
unless response.is_a? Rivulet::Response
|
|
11
|
+
return Failure[:wrong_response_type, "Invalid response type for #{route.path}"]
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
if NO_BODY_STATUSES.include?(response.status) && !response.body.nil?
|
|
15
|
+
return Failure[:conflicting_response, "Status #{response.status} has body #{response.body}"]
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
unless FORMAT_VALUES.include?(response.format)
|
|
19
|
+
return Failure[:wrong_response_format, "Unsupported response format #{response.format.inspect}"]
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
if response.format == :stream && !io_like?(response.body)
|
|
23
|
+
return Failure[:wrong_response_type, 'Response body is not supported for stream format']
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
if response.format == :file
|
|
27
|
+
body = response.body
|
|
28
|
+
return Failure[:wrong_response_type, "File body requires :path key"] if body.is_a?(Hash) && !body.key?(:path)
|
|
29
|
+
return Failure[:wrong_response_type, "Response body is not supported for file format"] unless body.is_a?(String) || body.is_a?(Hash)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
Success(input)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def io_like?(obj)
|
|
38
|
+
%i[gets each read rewind].all? { obj.respond_to?(_1) }
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
module Rivulet
|
|
2
|
+
class Telemetry
|
|
3
|
+
module SequelExtension
|
|
4
|
+
def log_connection_yield(*args, &block)
|
|
5
|
+
start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
6
|
+
super
|
|
7
|
+
ensure
|
|
8
|
+
elapsed = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - start) * 1000
|
|
9
|
+
Fiber[:rivulet_telemetry]&.record_db(elapsed)
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
if defined?(Sequel)
|
|
16
|
+
Sequel::Database.register_extension(:rivulet_telemetry) do |db|
|
|
17
|
+
db.extend(Rivulet::Telemetry::SequelExtension)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
module Rivulet
|
|
2
|
+
class Telemetry
|
|
3
|
+
attr_reader :db_ms
|
|
4
|
+
|
|
5
|
+
def initialize
|
|
6
|
+
@started_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
7
|
+
@root = nil
|
|
8
|
+
@stack = []
|
|
9
|
+
@db_ms = 0.0
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def start_recording(activity)
|
|
13
|
+
node = Rivulet::Telemetry::Node.new(
|
|
14
|
+
name: activity.class.name,
|
|
15
|
+
started_at: Process.clock_gettime(Process::CLOCK_MONOTONIC),
|
|
16
|
+
children: []
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
if @stack.empty?
|
|
20
|
+
@root = node
|
|
21
|
+
else
|
|
22
|
+
@stack.last[:children] << node
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
@stack << node
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def stop_recording(activity)
|
|
29
|
+
node = @stack.pop
|
|
30
|
+
node.ended_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
31
|
+
node.duration_ms =
|
|
32
|
+
((node.ended_at - node.started_at) * 1000.0).round(3)
|
|
33
|
+
node.self_ms =
|
|
34
|
+
(node.duration_ms - node.children.sum(&:duration_ms)).round(3)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def record_db(ms)
|
|
38
|
+
@db_ms = (@db_ms + ms).round(3)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def total_ms
|
|
42
|
+
((Process.clock_gettime(Process::CLOCK_MONOTONIC) - @started_at) * 1000).round(3)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def flow
|
|
46
|
+
@root
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def print_flow(node = @root, offset = 0)
|
|
50
|
+
entry = " " * 2 * offset
|
|
51
|
+
entry += "#{node.name} (#{node.self_ms})"
|
|
52
|
+
entry += " =>" if node.children.any?
|
|
53
|
+
entry += "\n"
|
|
54
|
+
|
|
55
|
+
node.children.each do |cnode|
|
|
56
|
+
entry += print_flow(cnode, offset + 1)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
entry
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
data/lib/rivulet.rb
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
require 'forwardable'
|
|
2
|
+
require 'dry/monads'
|
|
3
|
+
require 'dry/operation'
|
|
4
|
+
require 'dry/configurable'
|
|
5
|
+
require 'dry/auto_inject'
|
|
6
|
+
require 'dry/core/container'
|
|
7
|
+
require 'dry/logger'
|
|
8
|
+
require 'dry/validation'
|
|
9
|
+
require 'dry/transformer'
|
|
10
|
+
require 'oj'
|
|
11
|
+
require 'protocol/http'
|
|
12
|
+
require 'protocol/http/body/file'
|
|
13
|
+
require 'protocol/http/body/stream'
|
|
14
|
+
require 'rack'
|
|
15
|
+
require 'rack/utils'
|
|
16
|
+
require 'sequel'
|
|
17
|
+
|
|
18
|
+
require_relative 'rivulet/version'
|
|
19
|
+
require_relative 'rivulet/telemetry'
|
|
20
|
+
require_relative 'rivulet/telemetry/node'
|
|
21
|
+
require_relative 'rivulet/telemetry/sequel_extension'
|
|
22
|
+
require_relative 'rivulet/telemetry/timing_wrapper'
|
|
23
|
+
require_relative 'rivulet/step'
|
|
24
|
+
require_relative 'rivulet/container'
|
|
25
|
+
require_relative 'rivulet/operation'
|
|
26
|
+
require_relative 'rivulet/request'
|
|
27
|
+
require_relative 'rivulet/response'
|
|
28
|
+
require_relative 'rivulet/projection'
|
|
29
|
+
require_relative 'rivulet/routing/route'
|
|
30
|
+
require_relative 'rivulet/routing/mapper'
|
|
31
|
+
require_relative 'rivulet/steps/build_config'
|
|
32
|
+
require_relative 'rivulet/steps/build_context'
|
|
33
|
+
require_relative 'rivulet/steps/validate_response'
|
|
34
|
+
require_relative 'rivulet/steps/compile_response'
|
|
35
|
+
require_relative 'rivulet/steps/dispatch'
|
|
36
|
+
require_relative 'rivulet/steps/load_app'
|
|
37
|
+
require_relative 'rivulet/steps/load_db'
|
|
38
|
+
require_relative 'rivulet/steps/load_routes'
|
|
39
|
+
require_relative 'rivulet/steps/load_settings'
|
|
40
|
+
require_relative 'rivulet/steps/print_routes'
|
|
41
|
+
require_relative 'rivulet/steps/run_migrations'
|
|
42
|
+
require_relative 'rivulet/steps/run_console'
|
|
43
|
+
require_relative 'rivulet/operations/startup'
|
|
44
|
+
require_relative 'rivulet/operations/dispatch_request'
|
|
45
|
+
require_relative 'rivulet/operations/migrate'
|
|
46
|
+
require_relative 'rivulet/operations/print_routes'
|
|
47
|
+
require_relative 'rivulet/operations/run_console'
|
|
48
|
+
require_relative 'rivulet/application'
|
|
49
|
+
|
|
50
|
+
module Rivulet
|
|
51
|
+
extend SingleForwardable
|
|
52
|
+
|
|
53
|
+
def_delegators :app, :config, :configure, :routes, :logger
|
|
54
|
+
|
|
55
|
+
def self.app
|
|
56
|
+
return @app if @app
|
|
57
|
+
|
|
58
|
+
@app = Application.new
|
|
59
|
+
@app
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def self.plugin(as: 'rivulet')
|
|
63
|
+
Dry::Core::Container::Namespace.new(as) do
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|