etna 0.1.14 → 0.1.20
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/bin/etna +18 -0
- data/etna.completion +1001 -0
- data/etna_app.completion +133 -0
- data/ext/completions/extconf.rb +20 -0
- data/lib/commands.rb +395 -0
- data/lib/etna.rb +7 -0
- data/lib/etna/application.rb +46 -22
- data/lib/etna/client.rb +82 -48
- data/lib/etna/clients.rb +4 -0
- data/lib/etna/clients/enum.rb +9 -0
- data/lib/etna/clients/janus.rb +2 -0
- data/lib/etna/clients/janus/client.rb +73 -0
- data/lib/etna/clients/janus/models.rb +78 -0
- data/lib/etna/clients/magma.rb +4 -0
- data/lib/etna/clients/magma/client.rb +80 -0
- data/lib/etna/clients/magma/formatting.rb +1 -0
- data/lib/etna/clients/magma/formatting/models_csv.rb +354 -0
- data/lib/etna/clients/magma/models.rb +630 -0
- data/lib/etna/clients/magma/workflows.rb +10 -0
- data/lib/etna/clients/magma/workflows/add_project_models_workflow.rb +67 -0
- data/lib/etna/clients/magma/workflows/attribute_actions_from_json_workflow.rb +62 -0
- data/lib/etna/clients/magma/workflows/create_project_workflow.rb +123 -0
- data/lib/etna/clients/magma/workflows/crud_workflow.rb +85 -0
- data/lib/etna/clients/magma/workflows/ensure_containing_record_workflow.rb +44 -0
- data/lib/etna/clients/magma/workflows/file_attributes_blank_workflow.rb +68 -0
- data/lib/etna/clients/magma/workflows/file_linking_workflow.rb +115 -0
- data/lib/etna/clients/magma/workflows/json_converters.rb +81 -0
- data/lib/etna/clients/magma/workflows/json_validators.rb +452 -0
- data/lib/etna/clients/magma/workflows/model_synchronization_workflow.rb +306 -0
- data/lib/etna/clients/magma/workflows/record_synchronization_workflow.rb +63 -0
- data/lib/etna/clients/magma/workflows/update_attributes_from_csv_workflow.rb +246 -0
- data/lib/etna/clients/metis.rb +3 -0
- data/lib/etna/clients/metis/client.rb +239 -0
- data/lib/etna/clients/metis/models.rb +313 -0
- data/lib/etna/clients/metis/workflows.rb +2 -0
- data/lib/etna/clients/metis/workflows/metis_download_workflow.rb +37 -0
- data/lib/etna/clients/metis/workflows/metis_upload_workflow.rb +137 -0
- data/lib/etna/clients/polyphemus.rb +3 -0
- data/lib/etna/clients/polyphemus/client.rb +33 -0
- data/lib/etna/clients/polyphemus/models.rb +68 -0
- data/lib/etna/clients/polyphemus/workflows.rb +1 -0
- data/lib/etna/clients/polyphemus/workflows/set_configuration_workflow.rb +47 -0
- data/lib/etna/command.rb +243 -5
- data/lib/etna/controller.rb +4 -0
- data/lib/etna/csvs.rb +159 -0
- data/lib/etna/directed_graph.rb +56 -0
- data/lib/etna/environment_scoped.rb +19 -0
- data/lib/etna/errors.rb +6 -0
- data/lib/etna/generate_autocompletion_script.rb +131 -0
- data/lib/etna/json_serializable_struct.rb +37 -0
- data/lib/etna/logger.rb +24 -2
- data/lib/etna/multipart_serializable_nested_hash.rb +50 -0
- data/lib/etna/route.rb +1 -1
- data/lib/etna/server.rb +3 -0
- data/lib/etna/spec/vcr.rb +99 -0
- data/lib/etna/templates/attribute_actions_template.json +43 -0
- data/lib/etna/test_auth.rb +3 -1
- data/lib/etna/user.rb +11 -1
- data/lib/helpers.rb +90 -0
- metadata +70 -5
@@ -0,0 +1,37 @@
|
|
1
|
+
require 'ostruct'
|
2
|
+
require 'digest'
|
3
|
+
require 'fileutils'
|
4
|
+
require 'tempfile'
|
5
|
+
|
6
|
+
module Etna
|
7
|
+
module Clients
|
8
|
+
class Metis
|
9
|
+
class MetisDownloadWorkflow < Struct.new(:metis_client, :project_name, :bucket_name, :max_attempts, keyword_init: true)
|
10
|
+
|
11
|
+
def initialize(args)
|
12
|
+
super({max_attempts: 3}.update(args))
|
13
|
+
end
|
14
|
+
|
15
|
+
# TODO: Might be possible to use range headers to select and resume downloads on failure in the future.
|
16
|
+
def do_download(dest_file, metis_file, &block)
|
17
|
+
size = metis_file.size
|
18
|
+
completed = 0.0
|
19
|
+
start = Time.now
|
20
|
+
|
21
|
+
::File.open(dest_file, "w") do |io|
|
22
|
+
metis_client.download_file(metis_file) do |chunk|
|
23
|
+
io.write chunk
|
24
|
+
completed += chunk.size
|
25
|
+
|
26
|
+
block.call([
|
27
|
+
:progress,
|
28
|
+
size == 0 ? 1 : completed / size,
|
29
|
+
(completed / (Time.now - start)).round(2),
|
30
|
+
]) unless block.nil?
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,137 @@
|
|
1
|
+
require 'ostruct'
|
2
|
+
require 'digest'
|
3
|
+
require 'fileutils'
|
4
|
+
require 'tempfile'
|
5
|
+
require 'securerandom'
|
6
|
+
|
7
|
+
module Etna
|
8
|
+
module Clients
|
9
|
+
class Metis
|
10
|
+
class MetisUploadWorkflow < Struct.new(:metis_client, :metis_uid, :project_name, :bucket_name, :max_attempts, keyword_init: true)
|
11
|
+
|
12
|
+
def initialize(args)
|
13
|
+
super({max_attempts: 3, metis_uid: SecureRandom.hex}.update(args))
|
14
|
+
end
|
15
|
+
|
16
|
+
def do_upload(source_file, dest_path, &block)
|
17
|
+
upload = Upload.new(source_file: source_file)
|
18
|
+
|
19
|
+
dir = ::File.dirname(dest_path)
|
20
|
+
metis_client.create_folder(CreateFolderRequest.new(
|
21
|
+
project_name: project_name,
|
22
|
+
bucket_name: bucket_name,
|
23
|
+
folder_path: dir,
|
24
|
+
))
|
25
|
+
|
26
|
+
authorize_response = metis_client.authorize_upload(AuthorizeUploadRequest.new(
|
27
|
+
project_name: project_name,
|
28
|
+
bucket_name: bucket_name,
|
29
|
+
file_path: dest_path,
|
30
|
+
))
|
31
|
+
|
32
|
+
upload_parts(upload, authorize_response.upload_path, &block)
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def upload_parts(upload, upload_path, attempt_number = 1, reset = false, &block)
|
38
|
+
if attempt_number > max_attempts
|
39
|
+
raise "Upload failed after (#{attempt_number}) attempts."
|
40
|
+
end
|
41
|
+
|
42
|
+
start = Time.now
|
43
|
+
unsent_zero_byte_file = upload.current_byte_position == 0
|
44
|
+
|
45
|
+
upload.resume_from!(metis_client.upload_start(UploadStartRequest.new(
|
46
|
+
upload_path: upload_path,
|
47
|
+
file_size: upload.file_size,
|
48
|
+
next_blob_size: upload.next_blob_size,
|
49
|
+
next_blob_hash: upload.next_blob_hash,
|
50
|
+
metis_uid: metis_uid,
|
51
|
+
reset: reset,
|
52
|
+
)))
|
53
|
+
|
54
|
+
until upload.complete? && !unsent_zero_byte_file
|
55
|
+
begin
|
56
|
+
blob_bytes = upload.next_blob_bytes
|
57
|
+
byte_position = upload.current_byte_position
|
58
|
+
|
59
|
+
upload.advance_position!
|
60
|
+
metis_client.upload_blob(UploadBlobRequest.new(
|
61
|
+
upload_path: upload_path,
|
62
|
+
next_blob_size: upload.next_blob_size,
|
63
|
+
next_blob_hash: upload.next_blob_hash,
|
64
|
+
blob_data: StringIO.new(blob_bytes),
|
65
|
+
metis_uid: metis_uid,
|
66
|
+
current_byte_position: byte_position,
|
67
|
+
))
|
68
|
+
|
69
|
+
unsent_zero_byte_file = false
|
70
|
+
rescue Etna::Error => e
|
71
|
+
m = yield [:error, e] unless block.nil?
|
72
|
+
if m == false
|
73
|
+
raise e
|
74
|
+
end
|
75
|
+
|
76
|
+
if e.status == 422
|
77
|
+
return upload_parts(upload, upload_path, attempt_number + 1, true, &block)
|
78
|
+
elsif e.status >= 500
|
79
|
+
return upload_parts(upload, upload_path, attempt_number + 1, &block)
|
80
|
+
end
|
81
|
+
|
82
|
+
raise e
|
83
|
+
end
|
84
|
+
|
85
|
+
yield [
|
86
|
+
:progress,
|
87
|
+
upload.file_size == 0 ? 1.0 : upload.current_byte_position.to_f / upload.file_size,
|
88
|
+
(upload.current_byte_position / (Time.now - start).to_f).round(2),
|
89
|
+
] unless block.nil?
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
class Upload < Struct.new(:source_file, :next_blob_size, :current_byte_position, keyword_init: true)
|
94
|
+
INITIAL_BLOB_SIZE = 2 ** 10
|
95
|
+
MAX_BLOB_SIZE = 2 ** 22
|
96
|
+
ZERO_HASH = 'd41d8cd98f00b204e9800998ecf8427e'
|
97
|
+
|
98
|
+
def initialize(**args)
|
99
|
+
super
|
100
|
+
self.next_blob_size = [file_size, INITIAL_BLOB_SIZE].min
|
101
|
+
self.current_byte_position = 0
|
102
|
+
end
|
103
|
+
|
104
|
+
def file_size
|
105
|
+
::File.size(source_file)
|
106
|
+
end
|
107
|
+
|
108
|
+
def advance_position!
|
109
|
+
self.current_byte_position = self.current_byte_position + self.next_blob_size
|
110
|
+
self.next_blob_size = [
|
111
|
+
MAX_BLOB_SIZE,
|
112
|
+
# in fact we should stop when we hit the end of the file
|
113
|
+
file_size - current_byte_position
|
114
|
+
].min
|
115
|
+
end
|
116
|
+
|
117
|
+
def complete?
|
118
|
+
current_byte_position >= file_size
|
119
|
+
end
|
120
|
+
|
121
|
+
def next_blob_hash
|
122
|
+
Digest::MD5.hexdigest(next_blob_bytes)
|
123
|
+
end
|
124
|
+
|
125
|
+
def next_blob_bytes
|
126
|
+
IO.binread(source_file, next_blob_size, current_byte_position)
|
127
|
+
end
|
128
|
+
|
129
|
+
def resume_from!(upload_response)
|
130
|
+
self.current_byte_position = upload_response.current_byte_position
|
131
|
+
self.next_blob_size = upload_response.next_blob_size
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'net/http/persistent'
|
2
|
+
require 'net/http/post/multipart'
|
3
|
+
require 'singleton'
|
4
|
+
require_relative '../../client'
|
5
|
+
require_relative './models'
|
6
|
+
|
7
|
+
module Etna
|
8
|
+
module Clients
|
9
|
+
class Polyphemus
|
10
|
+
def initialize(host:, token:, ignore_ssl: false, persistent: true)
|
11
|
+
raise 'Polyphemus client configuration is missing host.' unless host
|
12
|
+
raise 'Polyphemus client configuration is missing token.' unless token
|
13
|
+
@etna_client = ::Etna::Client.new(
|
14
|
+
host,
|
15
|
+
token,
|
16
|
+
routes_available: false,
|
17
|
+
persistent: persistent,
|
18
|
+
ignore_ssl: ignore_ssl)
|
19
|
+
end
|
20
|
+
|
21
|
+
def configuration(configuration_request = ConfigurationRequest.new)
|
22
|
+
json = nil
|
23
|
+
@etna_client.get(
|
24
|
+
"/configuration",
|
25
|
+
configuration_request) do |res|
|
26
|
+
json = JSON.parse(res.body, symbolize_names: true)
|
27
|
+
end
|
28
|
+
|
29
|
+
ConfigurationResponse.new(json)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
require 'ostruct'
|
2
|
+
require_relative '../../json_serializable_struct'
|
3
|
+
|
4
|
+
# TODO: In the near future, I'd like to transition to specifying apis via SWAGGER and generating model stubs from the
|
5
|
+
# common definitions. For nowe I've written them out by hand here.
|
6
|
+
module Etna
|
7
|
+
module Clients
|
8
|
+
class Polyphemus
|
9
|
+
class ConfigurationRequest
|
10
|
+
def map
|
11
|
+
[]
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
class ConfigurationResponse
|
16
|
+
attr_reader :raw
|
17
|
+
|
18
|
+
def initialize(raw = '')
|
19
|
+
@raw = raw
|
20
|
+
end
|
21
|
+
|
22
|
+
def environment
|
23
|
+
@raw.keys.first
|
24
|
+
end
|
25
|
+
|
26
|
+
def environments
|
27
|
+
@raw.keys
|
28
|
+
end
|
29
|
+
|
30
|
+
def environment_configuration(env = environment)
|
31
|
+
EnvironmentConfiguration.new(@raw[env])
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
class EnvironmentConfiguration
|
36
|
+
attr_reader :raw
|
37
|
+
|
38
|
+
def initialize(raw = {})
|
39
|
+
@raw = raw
|
40
|
+
end
|
41
|
+
|
42
|
+
def magma
|
43
|
+
@raw['magma']
|
44
|
+
end
|
45
|
+
|
46
|
+
def metis
|
47
|
+
@raw['magma']
|
48
|
+
end
|
49
|
+
|
50
|
+
def janus
|
51
|
+
@raw['magma']
|
52
|
+
end
|
53
|
+
|
54
|
+
def timur
|
55
|
+
@raw['magma']
|
56
|
+
end
|
57
|
+
|
58
|
+
def polyphemus
|
59
|
+
@raw['magma']
|
60
|
+
end
|
61
|
+
|
62
|
+
def auth_redirect
|
63
|
+
@raw['auth_redirect']
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
require_relative './workflows/set_configuration_workflow'
|
@@ -0,0 +1,47 @@
|
|
1
|
+
# Given an environment (i.e. test, development, staging),
|
2
|
+
# and a polyphemus host, fetches the configuration
|
3
|
+
# hash and sets it up in a local config file,
|
4
|
+
# ~/.etna.json by default.
|
5
|
+
|
6
|
+
# Adds a model to a project, from a JSON file.
|
7
|
+
# This workflow:
|
8
|
+
# 1) Validates the model JSON file.
|
9
|
+
# 2) Validates that the model parent and any link attributes exist in Magma.
|
10
|
+
# 3) Adds the model to the Magma project.
|
11
|
+
# 4) Adds any attributes to Magma.
|
12
|
+
|
13
|
+
require 'json'
|
14
|
+
require 'yaml'
|
15
|
+
require 'ostruct'
|
16
|
+
|
17
|
+
module Etna
|
18
|
+
module Clients
|
19
|
+
class Polyphemus
|
20
|
+
class SetConfigurationWorkflow < Struct.new(:polyphemus_client, :config_file, keyword_init: true)
|
21
|
+
def fetch_configuration
|
22
|
+
polyphemus_client.configuration
|
23
|
+
end
|
24
|
+
|
25
|
+
def update_configuration_file(**additional_config)
|
26
|
+
if File.exist?(config_file)
|
27
|
+
etna_config = YAML.load_file(config_file) || {}
|
28
|
+
else
|
29
|
+
etna_config = {}
|
30
|
+
end
|
31
|
+
|
32
|
+
config = polyphemus_client.configuration
|
33
|
+
env = config.environment.to_sym
|
34
|
+
|
35
|
+
# Ensure that env is the last key in the result, which becomes the new 'default' config.
|
36
|
+
etna_config.delete(env) if etna_config.include?(env)
|
37
|
+
env_config = config.environment_configuration.raw.dup
|
38
|
+
env_config.update(additional_config)
|
39
|
+
etna_config.update({ env => env_config })
|
40
|
+
|
41
|
+
File.open(config_file, 'w') { |f| YAML.dump(etna_config, f) }
|
42
|
+
config
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
data/lib/etna/command.rb
CHANGED
@@ -1,26 +1,264 @@
|
|
1
|
+
require 'rollbar'
|
2
|
+
|
3
|
+
# Commands resolution works by starting from a class that includes CommandExecutor, and searches for
|
4
|
+
# INNER (belong to their scope) classes that include :find_command (generally subclasses of Etna::Command or
|
5
|
+
# classes that include CommandExecutor).
|
6
|
+
# A command completes the find_command chain by returning itself, while CommandExecutors consume one of the arguments
|
7
|
+
# to dispatch to a subcommand.
|
8
|
+
# eg of subcommands
|
9
|
+
#
|
10
|
+
# ./bin/my_app my_command help
|
11
|
+
# ./bin/my_app my_command subcommand
|
12
|
+
# class MyApp < Etna::Application
|
13
|
+
# class MyCommand
|
14
|
+
# include Etna::CommandExecutor
|
15
|
+
#
|
16
|
+
# class Subcommand < Etna::Command
|
17
|
+
# def execute
|
18
|
+
# end
|
19
|
+
# end
|
20
|
+
# end
|
21
|
+
# end
|
22
|
+
#
|
1
23
|
module Etna
|
2
|
-
|
3
|
-
|
24
|
+
# Provides the usage DSL and method that includes information about a CommandExecutor or a Command.
|
25
|
+
# Commands or CommandExecutors can call 'usage' in their definition to provide a specific description
|
26
|
+
# to be given about that command. By default, the command name + desc method will be shown.
|
27
|
+
module CommandOrExecutor
|
28
|
+
module Dsl
|
4
29
|
def usage(desc)
|
5
30
|
define_method :usage do
|
6
|
-
" #{"%-
|
31
|
+
" #{"%-45s" % command_name}#{desc}"
|
7
32
|
end
|
8
33
|
end
|
34
|
+
|
35
|
+
def boolean_flags
|
36
|
+
@boolean_flags ||= []
|
37
|
+
end
|
38
|
+
|
39
|
+
def string_flags
|
40
|
+
@string_flags ||= []
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def flag_as_parameter(flag)
|
45
|
+
flag.gsub('--', '').gsub('-', '_')
|
9
46
|
end
|
10
47
|
|
11
|
-
def
|
12
|
-
|
48
|
+
def parse_flags(*args)
|
49
|
+
new_args = []
|
50
|
+
flags = {}
|
51
|
+
found_non_flag = false
|
52
|
+
|
53
|
+
until args.empty?
|
54
|
+
next_arg = args.shift
|
55
|
+
|
56
|
+
unless next_arg.start_with? '--'
|
57
|
+
new_args << next_arg
|
58
|
+
found_non_flag = true
|
59
|
+
next
|
60
|
+
end
|
61
|
+
|
62
|
+
arg_name = flag_as_parameter(next_arg).to_sym
|
63
|
+
|
64
|
+
if self.class.boolean_flags.include?(next_arg)
|
65
|
+
flags[arg_name] = true
|
66
|
+
elsif self.class.string_flags.include?(next_arg)
|
67
|
+
if args.empty?
|
68
|
+
raise "flag #{next_arg} requires an argument"
|
69
|
+
else
|
70
|
+
flags[arg_name] = args.shift
|
71
|
+
end
|
72
|
+
elsif !found_non_flag
|
73
|
+
raise "#{program_name} does not recognize flag #{next_arg}"
|
74
|
+
else
|
75
|
+
new_args << next_arg
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
[flags, new_args]
|
80
|
+
end
|
81
|
+
|
82
|
+
def completions_for(parameter)
|
83
|
+
if parameter == 'env' || parameter == 'environment' || parameter =~ /_env/
|
84
|
+
['production', 'staging', 'development']
|
85
|
+
else
|
86
|
+
["__#{parameter}__"]
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def command_name
|
91
|
+
self.class.name.snake_case.split(/::/).last
|
92
|
+
end
|
93
|
+
|
94
|
+
def usage
|
95
|
+
" #{"%-45s" % command_name}#{desc}"
|
96
|
+
end
|
97
|
+
|
98
|
+
def flag_argspec
|
99
|
+
@argspec ||= []
|
100
|
+
end
|
101
|
+
|
102
|
+
# Program name is used to compose the usage display. The top level executor will just assume the running
|
103
|
+
# process's bin name, while other executors will use the 'command name' of the Command / Exectuor
|
104
|
+
# (class name derived) and whatever program_name exists for its parent scope.
|
105
|
+
def program_name
|
106
|
+
if parent.nil? || !parent.respond_to?(:program_name)
|
107
|
+
$PROGRAM_NAME
|
108
|
+
else
|
109
|
+
parent.program_name + " " + command_name
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
# By default, the description of a command maps the execute parameters into CLI descriptions,
|
114
|
+
# where as CommandExecutors will display their subcommands.
|
115
|
+
def desc
|
116
|
+
if respond_to?(:execute)
|
117
|
+
method(:execute).parameters.map do |type, name|
|
118
|
+
name = "..." if name.nil?
|
119
|
+
|
120
|
+
case type
|
121
|
+
when :req
|
122
|
+
"<#{name}>"
|
123
|
+
when :opt
|
124
|
+
"[<#{name}>]"
|
125
|
+
when :rest
|
126
|
+
"<#{name}>..."
|
127
|
+
when :keyrest
|
128
|
+
"[flags...]"
|
129
|
+
when :key
|
130
|
+
flag = "--#{name.to_s.gsub('_', '-')}"
|
131
|
+
if self.class.boolean_flags.include?(flag)
|
132
|
+
"[#{flag}]"
|
133
|
+
else
|
134
|
+
"[#{flag} <#{name}>]"
|
135
|
+
end
|
136
|
+
else
|
137
|
+
raise "Invalid command execute argument specification #{type}, unsure how to format description."
|
138
|
+
end
|
139
|
+
end.join(' ')
|
140
|
+
elsif respond_to?(:subcommands)
|
141
|
+
'<command> <args>...'
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
def self.included(cls)
|
146
|
+
cls.extend(Dsl)
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
# Include this module into class that dispatches to child CommandExecutors | Commands that must exist as
|
151
|
+
# inner classes. Note that non-root CommandExecutors must accept and set their @parent just like commands.
|
152
|
+
module CommandExecutor
|
153
|
+
attr_reader :parent
|
154
|
+
|
155
|
+
def initialize(parent = nil)
|
156
|
+
super()
|
157
|
+
@parent = parent
|
158
|
+
end
|
159
|
+
|
160
|
+
def self.included(cls)
|
161
|
+
cls.include(CommandOrExecutor)
|
162
|
+
cls.const_set(:Help, Class.new(Etna::Command) do
|
163
|
+
usage 'List this help'
|
164
|
+
|
165
|
+
def execute
|
166
|
+
self.parent.help
|
167
|
+
end
|
168
|
+
end) unless cls.const_defined?(:Help)
|
169
|
+
end
|
170
|
+
|
171
|
+
def help
|
172
|
+
puts "usage: #{program_name} #{desc}"
|
173
|
+
subcommands.each do |name, cmd|
|
174
|
+
puts cmd.usage
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
def find_command(*args, **kwds)
|
179
|
+
flags, args = parse_flags(*args)
|
180
|
+
dispatch_to_subcommand(*args, **(kwds.update(flags)))
|
181
|
+
end
|
182
|
+
|
183
|
+
def dispatch_to_subcommand(cmd = 'help', *args, **kwds)
|
184
|
+
unless subcommands.include?(cmd)
|
185
|
+
cmd = 'help'
|
186
|
+
args = []
|
187
|
+
end
|
188
|
+
|
189
|
+
subcommands[cmd].find_command(*args, **kwds)
|
190
|
+
end
|
191
|
+
|
192
|
+
def subcommands
|
193
|
+
@subcommands ||= self.class.constants.sort.reduce({}) do |acc, n|
|
194
|
+
acc.tap do
|
195
|
+
c = self.class.const_get(n)
|
196
|
+
next unless c.instance_methods.include?(:find_command)
|
197
|
+
v = c.new(self)
|
198
|
+
acc[v.command_name] = v
|
199
|
+
end
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
def all_subcommands
|
204
|
+
subcommands.values + (subcommands.values.map { |s| s.respond_to?(:all_subcommands) ? s.all_subcommands : [] }.flatten)
|
205
|
+
end
|
206
|
+
end
|
207
|
+
|
208
|
+
class Command
|
209
|
+
include CommandOrExecutor
|
210
|
+
|
211
|
+
attr_reader :parent
|
212
|
+
|
213
|
+
def initialize(parent = nil)
|
214
|
+
@parent = parent
|
215
|
+
end
|
216
|
+
|
217
|
+
def self.parent_scope
|
218
|
+
parts = self.name.split('::')
|
219
|
+
parts.pop
|
220
|
+
Kernel.const_get(parts.join('::'))
|
221
|
+
end
|
222
|
+
|
223
|
+
def find_command(*args, **kwds)
|
224
|
+
flags, args = parse_flags(*args)
|
225
|
+
[self, args, kwds.update(flags)]
|
226
|
+
end
|
227
|
+
|
228
|
+
def fill_in_missing_params(args)
|
229
|
+
req_params = method(:execute).parameters.select { |type, name| type == :req }
|
230
|
+
args + (req_params[(args.length)..(req_params.length)] || []).map do |type, name|
|
231
|
+
puts "#{name}?"
|
232
|
+
STDIN.gets.chomp
|
233
|
+
end
|
234
|
+
end
|
235
|
+
|
236
|
+
def completions
|
237
|
+
method(:execute).parameters.map do |type, name|
|
238
|
+
name = "..." if name.nil?
|
239
|
+
if type == :req || type == :opt
|
240
|
+
[completions_for(name)]
|
241
|
+
else
|
242
|
+
[]
|
243
|
+
end
|
244
|
+
end.inject([], &:+)
|
13
245
|
end
|
14
246
|
|
15
247
|
# To be overridden during inheritance.
|
16
248
|
def execute
|
17
249
|
raise 'Command is not implemented'
|
250
|
+
rescue => e
|
251
|
+
Rollbar.error(e)
|
252
|
+
raise
|
18
253
|
end
|
19
254
|
|
20
255
|
# To be overridden during inheritance, to e.g. connect to a database.
|
21
256
|
# Should be called with super by inheriting method.
|
22
257
|
def setup(config)
|
23
258
|
Etna::Application.find(self.class).configure(config)
|
259
|
+
rescue => e
|
260
|
+
Rollbar.error(e)
|
261
|
+
raise
|
24
262
|
end
|
25
263
|
end
|
26
264
|
end
|