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.
Files changed (50) hide show
  1. checksums.yaml +7 -0
  2. data/bin/rivulet +4 -0
  3. data/lib/rivulet/application.rb +90 -0
  4. data/lib/rivulet/cli/console.rb +15 -0
  5. data/lib/rivulet/cli/db/migrate.rb +17 -0
  6. data/lib/rivulet/cli/generate/handler/operation.rb +75 -0
  7. data/lib/rivulet/cli/generate/handler/step.rb +91 -0
  8. data/lib/rivulet/cli/generate/handler.rb +135 -0
  9. data/lib/rivulet/cli/generate/migration.rb +31 -0
  10. data/lib/rivulet/cli/generate/operation.rb +111 -0
  11. data/lib/rivulet/cli/generate/resource.rb +25 -0
  12. data/lib/rivulet/cli/generate/service/operation.rb +118 -0
  13. data/lib/rivulet/cli/generate/service/projection.rb +89 -0
  14. data/lib/rivulet/cli/generate/service/step.rb +91 -0
  15. data/lib/rivulet/cli/generate/service.rb +143 -0
  16. data/lib/rivulet/cli/new.rb +191 -0
  17. data/lib/rivulet/cli/routes.rb +15 -0
  18. data/lib/rivulet/cli.rb +43 -0
  19. data/lib/rivulet/container.rb +28 -0
  20. data/lib/rivulet/operation.rb +21 -0
  21. data/lib/rivulet/operations/dispatch_request.rb +21 -0
  22. data/lib/rivulet/operations/migrate.rb +23 -0
  23. data/lib/rivulet/operations/print_routes.rb +17 -0
  24. data/lib/rivulet/operations/run_console.rb +25 -0
  25. data/lib/rivulet/operations/startup.rb +23 -0
  26. data/lib/rivulet/projection.rb +6 -0
  27. data/lib/rivulet/request.rb +18 -0
  28. data/lib/rivulet/response.rb +12 -0
  29. data/lib/rivulet/routing/mapper.rb +78 -0
  30. data/lib/rivulet/routing/route.rb +14 -0
  31. data/lib/rivulet/step.rb +22 -0
  32. data/lib/rivulet/steps/build_config.rb +26 -0
  33. data/lib/rivulet/steps/build_context.rb +76 -0
  34. data/lib/rivulet/steps/compile_response.rb +113 -0
  35. data/lib/rivulet/steps/dispatch.rb +17 -0
  36. data/lib/rivulet/steps/load_app.rb +20 -0
  37. data/lib/rivulet/steps/load_db.rb +24 -0
  38. data/lib/rivulet/steps/load_routes.rb +28 -0
  39. data/lib/rivulet/steps/load_settings.rb +42 -0
  40. data/lib/rivulet/steps/print_routes.rb +123 -0
  41. data/lib/rivulet/steps/run_console.rb +26 -0
  42. data/lib/rivulet/steps/run_migrations.rb +50 -0
  43. data/lib/rivulet/steps/validate_response.rb +42 -0
  44. data/lib/rivulet/telemetry/node.rb +8 -0
  45. data/lib/rivulet/telemetry/sequel_extension.rb +19 -0
  46. data/lib/rivulet/telemetry/timing_wrapper.rb +12 -0
  47. data/lib/rivulet/telemetry.rb +62 -0
  48. data/lib/rivulet/version.rb +3 -0
  49. data/lib/rivulet.rb +66 -0
  50. metadata +342 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 5ae318924632d21dfc60983838454a230011b5c6a762446c52b927870074980c
4
+ data.tar.gz: eb65be1dcabda72c0ce3d590e6952d8929f1b945f23344de7fcc2403188a94eb
5
+ SHA512:
6
+ metadata.gz: 0c6433c7c4814dd1d763d1d16d01ce903a9b84b308451766e1f6f64625c8195235526faf95c78fa43a2f1bfe9d4f4c9d9dfdbf69454c6ca0a92d6ec3f8a3c05a
7
+ data.tar.gz: fdb3fe00dd3280bce1844cd9ef71ae24c3c30f398d4aaea45cfe755442c6c2a4413c7d183f04938c5770f1b9c5910a61ff549377b304b63851c66c40f12e6b11
data/bin/rivulet ADDED
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env ruby
2
+ require_relative '../lib/rivulet/cli'
3
+
4
+ Dry::CLI.new(Rivulet::CLI::Commands).call
@@ -0,0 +1,90 @@
1
+ module Rivulet
2
+ class Application
3
+ include Dry::Configurable
4
+ include Dry::Monads[:result]
5
+ include Dry::AutoInject(Rivulet::Container)[
6
+ dispatch_request: 'operations.dispatch_request',
7
+ startup_operation: 'operations.startup',
8
+ migrate_operation: 'operations.migrate',
9
+ run_console_operation: 'operations.run_console',
10
+ print_routes_operation: 'operations.print_routes'
11
+ ]
12
+
13
+ attr_accessor :db
14
+
15
+ def call(env)
16
+ result = with_telemetry do
17
+ dispatch_request.(resource: self, env: env)
18
+ end
19
+
20
+ return result.success[:response] if result.success?
21
+
22
+ case result
23
+ in Failure[:route_not_found]
24
+ [404, { 'Content-Type' => 'text/plain' }, ['Not Found']]
25
+ in Failure[:wrong_response_type, message]
26
+ logger.error(message)
27
+ [500, {}, []]
28
+ in Failure[:conflicting_response, message]
29
+ logger.error(message)
30
+ [500, {}, []]
31
+ in Failure[:file_not_found, message]
32
+ logger.error(message)
33
+ [500, {}, []]
34
+ end
35
+ end
36
+
37
+ def routes
38
+ @routes ||= Routing::Mapper.new([])
39
+ end
40
+
41
+ def startup
42
+ result = startup_operation.(resource: self)
43
+ if result.failure?
44
+ warn "Startup failed: #{result.failure}"
45
+ exit 1
46
+ end
47
+ self
48
+ end
49
+
50
+ def migrate!
51
+ result = migrate_operation.(resource: self)
52
+ if result.failure?
53
+ warn "Migration failed: #{result.failure}"
54
+ exit 1
55
+ end
56
+ end
57
+
58
+ def run_console
59
+ result = run_console_operation.(resource: self)
60
+ if result.failure?
61
+ warn "Cannot start console: #{result.failure}"
62
+ exit 1
63
+ end
64
+ end
65
+
66
+ def print_routes
67
+ result = print_routes_operation.(resource: self)
68
+ if result.failure?
69
+ warn "Cannot print routes: #{result.failure}"
70
+ exit 1
71
+ end
72
+ end
73
+
74
+ private
75
+
76
+ def with_telemetry
77
+ t = Telemetry.new
78
+ Fiber[:rivulet_telemetry] = t
79
+
80
+ result = yield
81
+
82
+ logger.info(
83
+ "Completed total_ms=#{t.total_ms} db_ms=#{t.db_ms} flow:\n#{t.print_flow}"
84
+ )
85
+ result
86
+ ensure
87
+ Fiber[:rivulet_telemetry] = nil
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,15 @@
1
+ require 'rivulet'
2
+
3
+ module Rivulet
4
+ module CLI
5
+ module Commands
6
+ class Console < Dry::CLI::Command
7
+ desc 'Run console within rivulet environmet'
8
+
9
+ def call(**)
10
+ Rivulet.app.run_console
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,17 @@
1
+ require 'rivulet'
2
+
3
+ module Rivulet
4
+ module CLI
5
+ module Commands
6
+ module DB
7
+ class Migrate < Dry::CLI::Command
8
+ desc "Run pending database migrations"
9
+
10
+ def call(**)
11
+ Rivulet.app.migrate!
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,75 @@
1
+ require 'fileutils'
2
+
3
+ module Rivulet
4
+ module CLI
5
+ module Commands
6
+ module Generate
7
+ class Handler
8
+ class Operation < Dry::CLI::Command
9
+ desc "Generate an operation"
10
+ argument :name, required: true, desc: "Operation name in handler.operation format (e.g. users.create)"
11
+
12
+ def call(name:, **)
13
+ handler_name, operation_name = name.split('.')
14
+ handler_dir = underscore(handler_name)
15
+ operation_dir = underscore(operation_name)
16
+ handler_module = camelize(handler_dir)
17
+ operation_module = camelize(operation_dir)
18
+ base = "app/handlers/#{handler_dir}"
19
+
20
+ write "#{base}/operations/#{operation_dir}.rb", operation_template(handler_module, operation_module)
21
+ register_operation(base, handler_dir, handler_module, operation_dir, operation_module)
22
+ end
23
+
24
+ private
25
+
26
+ def underscore(str)
27
+ str
28
+ .gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
29
+ .gsub(/([a-z\d])([A-Z])/, '\1_\2')
30
+ .downcase
31
+ end
32
+
33
+ def camelize(str)
34
+ str.split('_').map(&:capitalize).join
35
+ end
36
+
37
+ def write(path, content)
38
+ File.write(path, content)
39
+ puts " create #{path}"
40
+ end
41
+
42
+ def operation_template(handler_module, operation_module)
43
+ <<~RUBY
44
+ module Handlers
45
+ module #{handler_module}
46
+ module Operations
47
+ class #{operation_module} < Rivulet::Operation
48
+ def call(input = {})
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
54
+ RUBY
55
+ end
56
+
57
+ def register_operation(base, handler_dir, handler_module, operation_dir, operation_module)
58
+ container_file = "#{base}/container.rb"
59
+ op_registration = " register('#{operation_dir}') { Handlers::#{handler_module}::Operations::#{operation_module}.new }\n"
60
+
61
+ if File.exist?(container_file)
62
+ content = File.read(container_file)
63
+ content.sub!(/( namespace\('operations'\) do\n)/, "\\1#{op_registration}")
64
+ File.write(container_file, content)
65
+ puts " update #{container_file}"
66
+ else
67
+ write container_file, container_template(handler_module, operation_dir, operation_module)
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,91 @@
1
+ require 'fileutils'
2
+
3
+ module Rivulet
4
+ module CLI
5
+ module Commands
6
+ module Generate
7
+ class Handler
8
+ class Step < Dry::CLI::Command
9
+ desc "Generate a step"
10
+ argument :name, required: true, desc: "Step name in handler.step format (e.g. users.create)"
11
+
12
+ def call(name:, **)
13
+ handler_name, step_name = name.split('.')
14
+ handler_dir = underscore(handler_name)
15
+ step_dir = underscore(step_name)
16
+ handler_module = camelize(handler_dir)
17
+ step_class = camelize(step_dir)
18
+ base = "app/handlers/#{handler_dir}"
19
+
20
+ write "#{base}/steps/#{step_dir}.rb", step_template(handler_module, step_class)
21
+ register_step(base, handler_module, step_dir, step_class)
22
+ end
23
+
24
+ private
25
+
26
+ def underscore(str)
27
+ str
28
+ .gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
29
+ .gsub(/([a-z\d])([A-Z])/, '\1_\2')
30
+ .downcase
31
+ end
32
+
33
+ def camelize(str)
34
+ str.split('_').map(&:capitalize).join
35
+ end
36
+
37
+ def write(path, content)
38
+ File.write(path, content)
39
+ puts " create #{path}"
40
+ end
41
+
42
+ def step_template(handler_module, step_class)
43
+ <<~RUBY
44
+ module Handlers
45
+ module #{handler_module}
46
+ module Steps
47
+ class #{step_class} < Rivulet::Step
48
+ def call(input)
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
54
+ RUBY
55
+ end
56
+
57
+ def register_step(base, handler_module, step_dir, step_module)
58
+ container_file = "#{base}/container.rb"
59
+ step_registration = " register('#{step_dir}') { Handlers::#{handler_module}::Steps::#{step_module}.new }\n"
60
+
61
+ if File.exist?(container_file)
62
+ content = File.read(container_file)
63
+ content.sub!(/( namespace\('steps'\) do\n)/, "\\1#{step_registration}")
64
+ File.write(container_file, content)
65
+ puts " update #{container_file}"
66
+ else
67
+ write container_file, container_template(handler_module, step_dir, step_module)
68
+ end
69
+ end
70
+
71
+ def container_template(handler_module, step_name, step_class)
72
+ <<~RUBY
73
+ module Handlers
74
+ module #{handler_module}
75
+ class Container
76
+ extend Dry::Core::Container::Mixin
77
+
78
+ namespace('steps') do
79
+ register('#{step_name}') { Handlers::#{service_module}::Steps::#{step_class}.new }
80
+ end
81
+ end
82
+ end
83
+ end
84
+ RUBY
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,135 @@
1
+ require 'fileutils'
2
+
3
+ module Rivulet
4
+ module CLI
5
+ module Commands
6
+ module Generate
7
+ class Handler < Dry::CLI::Command
8
+ desc "Generate a handler"
9
+ argument :name, required: true, desc: "Handler name (e.g. Users or users)"
10
+
11
+ option :create, type: :boolean, aliases: ['-c'], desc: 'Add "create" operation'
12
+ option :read, type: :boolean, aliases: ['-r'], desc: 'Add "show" operation'
13
+ option :update, type: :boolean, aliases: ['-u'], desc: 'Add "update" operation'
14
+ option :delete, type: :boolean, aliases: ['-d'], desc: 'Add "delete" operation'
15
+ option :list, type: :boolean, aliases: ['-l'], desc: 'Add "index" operation'
16
+
17
+ SUBDIRS = %w[operations steps].freeze
18
+
19
+ def call(name:, **options)
20
+ dir_name = underscore(name)
21
+ module_name = camelize(dir_name)
22
+ base = "app/handlers/#{dir_name}"
23
+
24
+ SUBDIRS.each { |d| create_dir "#{base}/#{d}" }
25
+ write "#{base}/handler.rb", handler_template(module_name)
26
+ create_container(base, module_name) unless File.exist?("#{base}/container.rb")
27
+ register_handler(dir_name, module_name)
28
+
29
+ if options[:create]
30
+ Operation.new.call(name: "#{name}.create")
31
+ end
32
+
33
+ if options[:read]
34
+ Operation.new.call(name: "#{name}.show")
35
+ end
36
+
37
+ if options[:update]
38
+ Operation.new.call(name: "#{name}.update")
39
+ end
40
+
41
+ if options[:delete]
42
+ Operation.new.call(name: "#{name}.delete")
43
+ end
44
+
45
+ if options[:list]
46
+ Operation.new.call(name: "#{name}.index")
47
+ end
48
+ end
49
+
50
+ private
51
+
52
+ def underscore(str)
53
+ str
54
+ .gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
55
+ .gsub(/([a-z\d])([A-Z])/, '\1_\2')
56
+ .downcase
57
+ end
58
+
59
+ def camelize(str)
60
+ str.split('_').map(&:capitalize).join
61
+ end
62
+
63
+ def create_container(base, module_name)
64
+ write "#{base}/container.rb", container_template(module_name)
65
+ end
66
+
67
+ def create_dir(path)
68
+ FileUtils.mkdir_p(path)
69
+ puts " create #{path}/"
70
+ end
71
+
72
+ def write(path, content)
73
+ File.write(path, content)
74
+ puts " create #{path}"
75
+ end
76
+
77
+ def handler_template(module_name)
78
+ <<~RUBY
79
+ module Handlers
80
+ module #{module_name}
81
+ class Handler
82
+ NAMESPACE = 'operations'
83
+
84
+ def method_missing(name, input = {}, options = {}, &block)
85
+ key = [NAMESPACE, name].join('.')
86
+ super unless Container.key?(key)
87
+
88
+ Container[key].call(input, **options, &block)
89
+ end
90
+
91
+ def respond_to_missing?(name, include_private = false)
92
+ key = [NAMESPACE, name].join('.')
93
+ Container.key?(key) || super
94
+ end
95
+ end
96
+ end
97
+ end
98
+ RUBY
99
+ end
100
+
101
+ def container_template(module_name)
102
+ <<~RUBY
103
+ module Handlers
104
+ module #{module_name}
105
+ class Container
106
+ extend Dry::Core::Container::Mixin
107
+ import Handlers::Shared::Namespace
108
+
109
+ namespace('operations') do
110
+ end
111
+
112
+ namespace('steps') do
113
+ end
114
+ end
115
+ end
116
+ end
117
+ RUBY
118
+ end
119
+
120
+ def register_handler(dir_name, module_name)
121
+ handlers_file = 'app/handlers.rb'
122
+ return unless File.exist?(handlers_file)
123
+
124
+ content = File.read(handlers_file)
125
+ registration = " register('#{dir_name}') { Handlers::#{module_name}::Handler.new }\n"
126
+
127
+ updated = content.sub(/^end\s*\z/, "#{registration}end\n")
128
+ File.write(handlers_file, updated)
129
+ puts " update #{handlers_file}"
130
+ end
131
+ end
132
+ end
133
+ end
134
+ end
135
+ end
@@ -0,0 +1,31 @@
1
+ require 'fileutils'
2
+
3
+ module Rivulet
4
+ module CLI
5
+ module Commands
6
+ module Generate
7
+ class Migration < Dry::CLI::Command
8
+ desc "Generate a migration"
9
+ argument :name, required: true, desc: "Migration name (e.g. create_users)"
10
+
11
+ def call(name:, **)
12
+ FileUtils.mkdir_p('db/migrations')
13
+ filename = "#{Time.now.strftime('%Y%m%d%H%M%S')}_#{underscore(name)}.sql"
14
+ path = File.join('db/migrations', filename)
15
+ File.write(path, "")
16
+ puts " create #{path}"
17
+ end
18
+
19
+ private
20
+
21
+ def underscore(str)
22
+ str
23
+ .gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
24
+ .gsub(/([a-z\d])([A-Z])/, '\1_\2')
25
+ .downcase
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,111 @@
1
+ require 'fileutils'
2
+
3
+ module Rivulet
4
+ module CLI
5
+ module Commands
6
+ module Generate
7
+ class Operation < Dry::CLI::Command
8
+ desc "Generate an operation"
9
+ argument :name, required: true, desc: "Operation name in service.operation format (e.g. users.create)"
10
+
11
+ def call(name:, **)
12
+ service_name, operation_name = name.split('.')
13
+ service_dir = underscore(service_name)
14
+ operation_dir = underscore(operation_name)
15
+ service_module = camelize(service_dir)
16
+ operation_module = camelize(operation_dir)
17
+ base = "app/services/#{service_dir}"
18
+
19
+ write "#{base}/operations/#{operation_dir}.rb", operation_template(service_module, operation_module)
20
+ write "#{base}/contracts/#{operation_dir}.rb", contract_template(service_module, operation_module)
21
+ register_operation(base, service_dir, service_module, operation_dir, operation_module)
22
+ end
23
+
24
+ private
25
+
26
+ def underscore(str)
27
+ str
28
+ .gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
29
+ .gsub(/([a-z\d])([A-Z])/, '\1_\2')
30
+ .downcase
31
+ end
32
+
33
+ def camelize(str)
34
+ str.split('_').map(&:capitalize).join
35
+ end
36
+
37
+ def write(path, content)
38
+ File.write(path, content)
39
+ puts " create #{path}"
40
+ end
41
+
42
+ def operation_template(service_module, operation_module)
43
+ <<~RUBY
44
+ module Services
45
+ module #{service_module}
46
+ module Operations
47
+ class #{operation_module} < Rivulet::Operation
48
+ def call(input = {})
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
54
+ RUBY
55
+ end
56
+
57
+ def contract_template(service_module, operation_module)
58
+ <<~RUBY
59
+ module Services
60
+ module #{service_module}
61
+ module Contracts
62
+ class #{operation_module}
63
+ params do
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
69
+ RUBY
70
+ end
71
+
72
+ def register_operation(base, service_dir, service_module, operation_dir, operation_module)
73
+ container_file = "#{base}/container.rb"
74
+ op_registration = " register('#{operation_dir}') { Services::#{service_module}::Operations::#{operation_module}.new }\n"
75
+ contract_registration = " register('#{operation_dir}') { Services::#{service_module}::Contracts::#{operation_module}.new }\n"
76
+
77
+ if File.exist?(container_file)
78
+ content = File.read(container_file)
79
+ content.sub!(/( namespace\('operations'\) do\n)/, "\\1#{op_registration}")
80
+ content.sub!(/( namespace\('contracts'\) do\n)/, "\\1#{contract_registration}")
81
+ File.write(container_file, content)
82
+ puts " update #{container_file}"
83
+ else
84
+ write container_file, container_template(service_module, operation_dir, operation_module)
85
+ end
86
+ end
87
+
88
+ def container_template(service_module, operation_dir, operation_module)
89
+ <<~RUBY
90
+ module Services
91
+ module #{service_module}
92
+ class Container
93
+ extend Dry::Core::Container::Mixin
94
+
95
+ namespace('operations') do
96
+ register('#{operation_dir}') { Services::#{service_module}::Operations::#{operation_module}.new }
97
+ end
98
+
99
+ namespace('contracts') do
100
+ register('#{operation_dir}') { Services::#{service_module}::Contracts::#{operation_module}.new }
101
+ end
102
+ end
103
+ end
104
+ end
105
+ RUBY
106
+ end
107
+ end
108
+ end
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,25 @@
1
+ require 'fileutils'
2
+
3
+ module Rivulet
4
+ module CLI
5
+ module Commands
6
+ module Generate
7
+ class Resource < Dry::CLI::Command
8
+ desc "Generate a resource (handler + service)"
9
+ argument :name, required: true, desc: "Resource name (e.g. Users or users)"
10
+
11
+ option :create, type: :boolean, aliases: ['-c'], desc: 'Add operations to create resource'
12
+ option :read, type: :boolean, aliases: ['-r'], desc: 'Add operations to read resource'
13
+ option :update, type: :boolean, aliases: ['-u'], desc: 'Add operations to update resource'
14
+ option :delete, type: :boolean, aliases: ['-d'], desc: 'Add operations to delete resource'
15
+ option :list, type: :boolean, aliases: ['-l'], desc: 'Add operations to list resource'
16
+
17
+ def call(name:, **options)
18
+ Handler.new.call(name: name, **options)
19
+ Service.new.call(name: name, **options)
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end