thronglets 0.0.1 → 0.0.2
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 +4 -4
- data/lib/thronglets/activity.rb +29 -0
- data/lib/thronglets/cli.rb +41 -8
- data/lib/thronglets/concerns/abstract_class.rb +15 -0
- data/lib/thronglets/concerns/input.rb +39 -0
- data/lib/thronglets/concerns/output.rb +37 -0
- data/lib/thronglets/listener.rb +34 -0
- data/lib/thronglets/loader.rb +22 -0
- data/lib/thronglets/registry.rb +37 -0
- data/lib/thronglets/thor_ext/start.rb +67 -0
- data/lib/thronglets/version.rb +1 -1
- data/lib/thronglets/worker.rb +35 -0
- data/lib/thronglets/workflow.rb +29 -0
- data/lib/thronglets.rb +13 -1
- metadata +68 -3
- data/lib/thronglets/thor_ext.rb +0 -71
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2069a8ba6a74b72c831a2db5a2f309ed6dda12c85bc91ec2f09172c509d7fcb1
|
4
|
+
data.tar.gz: 6bbe85fde0072e98312c600acbb4be3c902b8734c329a09e1edc271c6f95e96b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 0ba8543c6ad46a7bf85a476ddc4a749bdee16fee65a136f41d36af633185d6c132024cd223c548f27b8cf3fd5cf6827b503c96196f887e9837e995f053d0bc02
|
7
|
+
data.tar.gz: 344d171f7c884bd8d1ad5294072393f4cac2342e81aa4e9ea400a3aab5d14b5d282a4e4cf44e30d61e1ce0de6042dd892149f64e7bd46385a04424c891cc3b88
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Thronglets::Activity < Temporal::Activity
|
4
|
+
include Thronglets::Concerns::AbstractClass
|
5
|
+
include Thronglets::Concerns::Input
|
6
|
+
include Thronglets::Concerns::Output
|
7
|
+
|
8
|
+
attr_reader :params
|
9
|
+
|
10
|
+
def call
|
11
|
+
raise "NotImplemented"
|
12
|
+
end
|
13
|
+
|
14
|
+
def execute(args)
|
15
|
+
@params = validate_input!(args.as_json)
|
16
|
+
|
17
|
+
data = call.as_json
|
18
|
+
|
19
|
+
if output_schema
|
20
|
+
validate_output!(data)
|
21
|
+
else
|
22
|
+
data
|
23
|
+
end
|
24
|
+
rescue InputValidationError, OutputValidationError => e
|
25
|
+
{
|
26
|
+
errors: e.errors,
|
27
|
+
}.as_json
|
28
|
+
end
|
29
|
+
end
|
data/lib/thronglets/cli.rb
CHANGED
@@ -1,16 +1,49 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require "thor"
|
4
|
+
require "irb"
|
5
|
+
require_relative "worker"
|
6
|
+
require_relative "listener"
|
4
7
|
|
5
|
-
|
6
|
-
|
7
|
-
extend ThorExt::Start
|
8
|
+
class Thronglets::CLI < Thor
|
9
|
+
extend Thronglets::ThorExt::Start
|
8
10
|
|
9
|
-
|
11
|
+
map %w[-v --version] => "version"
|
12
|
+
map %w[-w --worker] => "worker"
|
13
|
+
map %w[-l --listen] => "listen"
|
14
|
+
map %w[-c --console] => "console"
|
10
15
|
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
16
|
+
desc "version", "Display thronglets version", hide: true
|
17
|
+
def version
|
18
|
+
say "thronglets/#{Thronglets::VERSION} #{RUBY_DESCRIPTION}"
|
19
|
+
end
|
20
|
+
|
21
|
+
desc "worker", "Start worker"
|
22
|
+
def worker
|
23
|
+
say "Starting worker"
|
24
|
+
require File.join(Dir.pwd, "config", "temporal/env.rb")
|
25
|
+
|
26
|
+
app = Thronglets::Worker.new
|
27
|
+
app.run
|
28
|
+
end
|
29
|
+
|
30
|
+
desc "listen", "Start worker in listen mode"
|
31
|
+
def listen
|
32
|
+
say "Starting worker in listen mode"
|
33
|
+
require File.join(Dir.pwd, "config", "temporal/env.rb")
|
34
|
+
|
35
|
+
app = Thronglets::Listener.new
|
36
|
+
app.run
|
37
|
+
end
|
38
|
+
|
39
|
+
desc "console", "Start console"
|
40
|
+
def console
|
41
|
+
say "Starting console"
|
42
|
+
require File.join(Dir.pwd, "config", "temporal/env.rb")
|
43
|
+
loader = Thronglets::Loader.new
|
44
|
+
loader.load
|
45
|
+
|
46
|
+
ARGV.clear # otherwise all script parameters get passed to IRB
|
47
|
+
IRB.start
|
15
48
|
end
|
16
49
|
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Thronglets::Concerns::AbstractClass
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
class_methods do
|
7
|
+
def abstract_class?
|
8
|
+
defined?(@abstract_class) && @abstract_class == true
|
9
|
+
end
|
10
|
+
|
11
|
+
def abstract_class=(value)
|
12
|
+
@abstract_class = value
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Thronglets::Concerns::Input
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
attr_reader :input_errors, :input_result
|
7
|
+
|
8
|
+
delegate :input_schema, to: :class
|
9
|
+
|
10
|
+
class_methods do
|
11
|
+
def input(&)
|
12
|
+
@input = Dry::Schema.Params(&)
|
13
|
+
end
|
14
|
+
|
15
|
+
def input_schema
|
16
|
+
@input
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
class InputValidationError < StandardError
|
21
|
+
attr_reader :errors
|
22
|
+
|
23
|
+
def initialize(errors)
|
24
|
+
@errors = errors
|
25
|
+
super("Parameters are not valid")
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def validate_input!(args)
|
30
|
+
return args unless input_schema
|
31
|
+
|
32
|
+
@input_result = input_schema.call(args)
|
33
|
+
input_result_data = input_result.to_h.as_json
|
34
|
+
@input_errors = input_result.errors.to_h
|
35
|
+
return input_result_data if input_errors.blank?
|
36
|
+
|
37
|
+
raise InputValidationError, input_errors
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Thronglets::Concerns::Output
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
attr_reader :output_errors, :output_result
|
7
|
+
|
8
|
+
delegate :output_schema, to: :class
|
9
|
+
|
10
|
+
class_methods do
|
11
|
+
def output(&)
|
12
|
+
@output = Dry::Schema.Params(&)
|
13
|
+
end
|
14
|
+
|
15
|
+
def output_schema
|
16
|
+
@output
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
class OutputValidationError < StandardError
|
21
|
+
attr_reader :errors
|
22
|
+
|
23
|
+
def initialize(errors)
|
24
|
+
@errors = errors
|
25
|
+
super("Results are not valid")
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def validate_output!(data)
|
30
|
+
@output_result = output_schema.call(data)
|
31
|
+
output_result_data = output_result.to_h.as_json
|
32
|
+
@output_errors = output_result.errors.to_h
|
33
|
+
return output_result_data if output_errors.blank?
|
34
|
+
|
35
|
+
raise OutputValidationError, output_errors
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "listen"
|
4
|
+
require "childprocess"
|
5
|
+
|
6
|
+
class Thronglets::Listener
|
7
|
+
attr_reader :process
|
8
|
+
|
9
|
+
def initialize
|
10
|
+
@process = nil
|
11
|
+
end
|
12
|
+
|
13
|
+
def run
|
14
|
+
spawn_process
|
15
|
+
|
16
|
+
listener = Listen.to("app") do |_modified, _added, _removed|
|
17
|
+
process.stop
|
18
|
+
ensure
|
19
|
+
spawn_process
|
20
|
+
end
|
21
|
+
listener.start
|
22
|
+
sleep
|
23
|
+
rescue Interrupt
|
24
|
+
process.stop # tries increasingly harsher methods to kill the process.
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def spawn_process
|
30
|
+
@process = ChildProcess.build("thronglets", "-w")
|
31
|
+
process.io.inherit!
|
32
|
+
process.start
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Thronglets::Loader
|
4
|
+
attr_reader :loader
|
5
|
+
|
6
|
+
def initialize
|
7
|
+
@loader = Zeitwerk::Loader.new
|
8
|
+
[
|
9
|
+
"app/activities",
|
10
|
+
"app/actors",
|
11
|
+
"app/workflows",
|
12
|
+
"app/models",
|
13
|
+
"app/models/concerns",
|
14
|
+
].each do |path|
|
15
|
+
loader.push_dir(path) if Dir.exist?(path)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def load
|
20
|
+
loader.setup
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Thronglets::Registry
|
4
|
+
attr_reader :worker
|
5
|
+
|
6
|
+
def initialize(worker)
|
7
|
+
@worker = worker
|
8
|
+
end
|
9
|
+
|
10
|
+
def load!
|
11
|
+
list_classes_in_dir("app/activities").each do |activity|
|
12
|
+
if can_register_class?(activity)
|
13
|
+
puts format("Registered: %s", activity)
|
14
|
+
worker.register_activity(activity)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
list_classes_in_dir("app/workflows").each do |workflow|
|
18
|
+
if can_register_class?(workflow)
|
19
|
+
puts format("Registered: %s", workflow)
|
20
|
+
worker.register_workflow(workflow)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def list_classes_in_dir(path)
|
28
|
+
Dir.glob("#{path}/**/*.{rb}").map do |file|
|
29
|
+
name = file.delete_prefix("#{path}/").delete_suffix(".rb")
|
30
|
+
name.camelize.constantize
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def can_register_class?(klass)
|
35
|
+
!klass.abstract_class?
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Configures Thor to behave more like a typical CLI, with better help and error handling.
|
4
|
+
#
|
5
|
+
# - Passing -h or --help to a command will show help for that command.
|
6
|
+
# - Unrecognized options will be treated as errors (instead of being silently ignored).
|
7
|
+
# - Error messages will be printed in red to stderr, without stack trace.
|
8
|
+
# - Full stack traces can be enabled by setting the VERBOSE environment variable.
|
9
|
+
# - Errors will cause Thor to exit with a non-zero status.
|
10
|
+
#
|
11
|
+
# To take advantage of this behavior, your CLI should subclass Thor and extend this module.
|
12
|
+
#
|
13
|
+
# class CLI < Thor
|
14
|
+
# extend ThorExt::Start
|
15
|
+
# end
|
16
|
+
#
|
17
|
+
# Start your CLI with:
|
18
|
+
#
|
19
|
+
# CLI.start
|
20
|
+
#
|
21
|
+
# In tests, prevent Kernel.exit from being called when an error occurs, like this:
|
22
|
+
#
|
23
|
+
# CLI.start(args, exit_on_failure: false)
|
24
|
+
#
|
25
|
+
module Thronglets::ThorExt::Start
|
26
|
+
def self.extended(base)
|
27
|
+
super
|
28
|
+
base.check_unknown_options!
|
29
|
+
end
|
30
|
+
|
31
|
+
def start(given_args = ARGV, config = {})
|
32
|
+
config[:shell] ||= Thor::Base.shell.new
|
33
|
+
handle_help_switches(given_args) do |args|
|
34
|
+
dispatch(nil, args, nil, config)
|
35
|
+
end
|
36
|
+
rescue StandardError => e
|
37
|
+
handle_exception_on_start(e, config)
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
def handle_help_switches(given_args)
|
43
|
+
yield(given_args.dup)
|
44
|
+
rescue Thor::UnknownArgumentError => e
|
45
|
+
retry_with_args = []
|
46
|
+
|
47
|
+
if given_args.first == "help"
|
48
|
+
retry_with_args = [ "help" ] if given_args.length > 1
|
49
|
+
elsif e.unknown.intersect?(%w[-h --help])
|
50
|
+
retry_with_args = [ "help", (given_args - e.unknown).first ]
|
51
|
+
end
|
52
|
+
raise unless retry_with_args.any?
|
53
|
+
|
54
|
+
yield(retry_with_args)
|
55
|
+
end
|
56
|
+
|
57
|
+
def handle_exception_on_start(error, config)
|
58
|
+
return if error.is_a?(Errno::EPIPE)
|
59
|
+
raise if ENV["VERBOSE"] || !config.fetch(:exit_on_failure, true)
|
60
|
+
|
61
|
+
message = error.message.to_s
|
62
|
+
message.prepend("[#{error.class}] ") if message.empty? || !error.is_a?(Thor::Error)
|
63
|
+
|
64
|
+
config[:shell]&.say_error(message, :red)
|
65
|
+
exit(false)
|
66
|
+
end
|
67
|
+
end
|
data/lib/thronglets/version.rb
CHANGED
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "zeitwerk"
|
4
|
+
require "temporal"
|
5
|
+
require "temporal/worker"
|
6
|
+
require_relative "registry"
|
7
|
+
|
8
|
+
class Thronglets::Worker
|
9
|
+
attr_reader :loader
|
10
|
+
|
11
|
+
def initialize
|
12
|
+
@loader = Thronglets::Loader.new
|
13
|
+
loader.load
|
14
|
+
end
|
15
|
+
|
16
|
+
def run
|
17
|
+
registry.load!
|
18
|
+
worker.start # runs forever
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def worker
|
24
|
+
@worker ||= Temporal::Worker.new(
|
25
|
+
# how many threads poll for activities
|
26
|
+
activity_thread_pool_size: 20,
|
27
|
+
# how many threads poll for workflows
|
28
|
+
workflow_thread_pool_size: 10,
|
29
|
+
)
|
30
|
+
end
|
31
|
+
|
32
|
+
def registry
|
33
|
+
@registry ||= Thronglets::Registry.new(worker)
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Thronglets::Workflow < Temporal::Workflow
|
4
|
+
include Thronglets::Concerns::AbstractClass
|
5
|
+
include Thronglets::Concerns::Input
|
6
|
+
include Thronglets::Concerns::Output
|
7
|
+
|
8
|
+
attr_reader :params
|
9
|
+
|
10
|
+
def call
|
11
|
+
raise "NotImplemented"
|
12
|
+
end
|
13
|
+
|
14
|
+
def execute(args)
|
15
|
+
@params = validate_input!(args.as_json)
|
16
|
+
|
17
|
+
data = call.as_json
|
18
|
+
|
19
|
+
if output_schema
|
20
|
+
validate_output!(data)
|
21
|
+
else
|
22
|
+
data
|
23
|
+
end
|
24
|
+
rescue InputValidationError, OutputValidationError => e
|
25
|
+
{
|
26
|
+
errors: e.errors,
|
27
|
+
}.as_json
|
28
|
+
end
|
29
|
+
end
|
data/lib/thronglets.rb
CHANGED
@@ -1,7 +1,19 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module Thronglets
|
4
|
+
module ThorExt
|
5
|
+
autoload :Start, "thronglets/thor_ext/start"
|
6
|
+
end
|
7
|
+
|
8
|
+
module Concerns
|
9
|
+
autoload :AbstractClass, "thronglets/concerns/abstract_class"
|
10
|
+
autoload :Output, "thronglets/concerns/output"
|
11
|
+
autoload :Input, "thronglets/concerns/input"
|
12
|
+
end
|
13
|
+
|
4
14
|
autoload :CLI, "thronglets/cli"
|
5
15
|
autoload :VERSION, "thronglets/version"
|
6
|
-
autoload :
|
16
|
+
autoload :Workflow, "thronglets/workflow"
|
17
|
+
autoload :Activity, "thronglets/activity"
|
18
|
+
autoload :Loader, "thronglets/loader"
|
7
19
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: thronglets
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Artem Mashchenko
|
@@ -9,6 +9,48 @@ bindir: exe
|
|
9
9
|
cert_chain: []
|
10
10
|
date: 1980-01-02 00:00:00.000000000 Z
|
11
11
|
dependencies:
|
12
|
+
- !ruby/object:Gem::Dependency
|
13
|
+
name: activesupport
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
15
|
+
requirements:
|
16
|
+
- - ">="
|
17
|
+
- !ruby/object:Gem::Version
|
18
|
+
version: '0'
|
19
|
+
type: :runtime
|
20
|
+
prerelease: false
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
22
|
+
requirements:
|
23
|
+
- - ">="
|
24
|
+
- !ruby/object:Gem::Version
|
25
|
+
version: '0'
|
26
|
+
- !ruby/object:Gem::Dependency
|
27
|
+
name: childprocess
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
29
|
+
requirements:
|
30
|
+
- - ">="
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '0'
|
33
|
+
type: :runtime
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - ">="
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: '0'
|
40
|
+
- !ruby/object:Gem::Dependency
|
41
|
+
name: listen
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
43
|
+
requirements:
|
44
|
+
- - ">="
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: '0'
|
47
|
+
type: :runtime
|
48
|
+
prerelease: false
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - ">="
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0'
|
12
54
|
- !ruby/object:Gem::Dependency
|
13
55
|
name: thor
|
14
56
|
requirement: !ruby/object:Gem::Requirement
|
@@ -23,6 +65,20 @@ dependencies:
|
|
23
65
|
- - "~>"
|
24
66
|
- !ruby/object:Gem::Version
|
25
67
|
version: '1.2'
|
68
|
+
- !ruby/object:Gem::Dependency
|
69
|
+
name: zeitwerk
|
70
|
+
requirement: !ruby/object:Gem::Requirement
|
71
|
+
requirements:
|
72
|
+
- - ">="
|
73
|
+
- !ruby/object:Gem::Version
|
74
|
+
version: '0'
|
75
|
+
type: :runtime
|
76
|
+
prerelease: false
|
77
|
+
version_requirements: !ruby/object:Gem::Requirement
|
78
|
+
requirements:
|
79
|
+
- - ">="
|
80
|
+
- !ruby/object:Gem::Version
|
81
|
+
version: '0'
|
26
82
|
email:
|
27
83
|
- artem.maschenko@gmail.com
|
28
84
|
executables:
|
@@ -34,9 +90,18 @@ files:
|
|
34
90
|
- README.md
|
35
91
|
- exe/thronglets
|
36
92
|
- lib/thronglets.rb
|
93
|
+
- lib/thronglets/activity.rb
|
37
94
|
- lib/thronglets/cli.rb
|
38
|
-
- lib/thronglets/
|
95
|
+
- lib/thronglets/concerns/abstract_class.rb
|
96
|
+
- lib/thronglets/concerns/input.rb
|
97
|
+
- lib/thronglets/concerns/output.rb
|
98
|
+
- lib/thronglets/listener.rb
|
99
|
+
- lib/thronglets/loader.rb
|
100
|
+
- lib/thronglets/registry.rb
|
101
|
+
- lib/thronglets/thor_ext/start.rb
|
39
102
|
- lib/thronglets/version.rb
|
103
|
+
- lib/thronglets/worker.rb
|
104
|
+
- lib/thronglets/workflow.rb
|
40
105
|
homepage: https://github.com/kkdoo/thronglets
|
41
106
|
licenses:
|
42
107
|
- MIT
|
@@ -53,7 +118,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
53
118
|
requirements:
|
54
119
|
- - ">="
|
55
120
|
- !ruby/object:Gem::Version
|
56
|
-
version: '3.
|
121
|
+
version: '3.2'
|
57
122
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
58
123
|
requirements:
|
59
124
|
- - ">="
|
data/lib/thronglets/thor_ext.rb
DELETED
@@ -1,71 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Thronglets
|
4
|
-
module ThorExt
|
5
|
-
# Configures Thor to behave more like a typical CLI, with better help and error handling.
|
6
|
-
#
|
7
|
-
# - Passing -h or --help to a command will show help for that command.
|
8
|
-
# - Unrecognized options will be treated as errors (instead of being silently ignored).
|
9
|
-
# - Error messages will be printed in red to stderr, without stack trace.
|
10
|
-
# - Full stack traces can be enabled by setting the VERBOSE environment variable.
|
11
|
-
# - Errors will cause Thor to exit with a non-zero status.
|
12
|
-
#
|
13
|
-
# To take advantage of this behavior, your CLI should subclass Thor and extend this module.
|
14
|
-
#
|
15
|
-
# class CLI < Thor
|
16
|
-
# extend ThorExt::Start
|
17
|
-
# end
|
18
|
-
#
|
19
|
-
# Start your CLI with:
|
20
|
-
#
|
21
|
-
# CLI.start
|
22
|
-
#
|
23
|
-
# In tests, prevent Kernel.exit from being called when an error occurs, like this:
|
24
|
-
#
|
25
|
-
# CLI.start(args, exit_on_failure: false)
|
26
|
-
#
|
27
|
-
module Start
|
28
|
-
def self.extended(base)
|
29
|
-
super
|
30
|
-
base.check_unknown_options!
|
31
|
-
end
|
32
|
-
|
33
|
-
def start(given_args=ARGV, config={})
|
34
|
-
config[:shell] ||= Thor::Base.shell.new
|
35
|
-
handle_help_switches(given_args) do |args|
|
36
|
-
dispatch(nil, args, nil, config)
|
37
|
-
end
|
38
|
-
rescue StandardError => e
|
39
|
-
handle_exception_on_start(e, config)
|
40
|
-
end
|
41
|
-
|
42
|
-
private
|
43
|
-
|
44
|
-
def handle_help_switches(given_args)
|
45
|
-
yield(given_args.dup)
|
46
|
-
rescue Thor::UnknownArgumentError => e
|
47
|
-
retry_with_args = []
|
48
|
-
|
49
|
-
if given_args.first == "help"
|
50
|
-
retry_with_args = ["help"] if given_args.length > 1
|
51
|
-
elsif e.unknown.intersect?(%w[-h --help])
|
52
|
-
retry_with_args = ["help", (given_args - e.unknown).first]
|
53
|
-
end
|
54
|
-
raise unless retry_with_args.any?
|
55
|
-
|
56
|
-
yield(retry_with_args)
|
57
|
-
end
|
58
|
-
|
59
|
-
def handle_exception_on_start(error, config)
|
60
|
-
return if error.is_a?(Errno::EPIPE)
|
61
|
-
raise if ENV["VERBOSE"] || !config.fetch(:exit_on_failure, true)
|
62
|
-
|
63
|
-
message = error.message.to_s
|
64
|
-
message.prepend("[#{error.class}] ") if message.empty? || !error.is_a?(Thor::Error)
|
65
|
-
|
66
|
-
config[:shell]&.say_error(message, :red)
|
67
|
-
exit(false)
|
68
|
-
end
|
69
|
-
end
|
70
|
-
end
|
71
|
-
end
|