vop 0.3.5 → 0.3.6

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 (53) hide show
  1. checksums.yaml +5 -5
  2. data/.github/workflows/ci.yml +39 -0
  3. data/.ruby-version +1 -0
  4. data/Gemfile.lock +34 -47
  5. data/README.md +118 -16
  6. data/bin/sidekiq.sh +10 -0
  7. data/exe/vop +2 -2
  8. data/lib/core/cache/cache.plugin +0 -0
  9. data/lib/core/cache/commands/invalidate_cache.rb +9 -0
  10. data/lib/core/{structure → meta}/commands/list_commands.rb +2 -1
  11. data/lib/core/meta/commands/list_filters.rb +3 -0
  12. data/lib/core/meta/commands/list_plugins.rb +3 -0
  13. data/lib/core/meta/commands/new_plugin.rb +3 -7
  14. data/lib/core/shell/commands/detail.rb +21 -0
  15. data/lib/core/shell/commands/edit.rb +5 -2
  16. data/lib/core/shell/commands/help.rb +1 -1
  17. data/lib/core/shell/commands/source.rb +10 -4
  18. data/lib/core/structure/commands/collect_contributions.rb +8 -2
  19. data/lib/core/structure/commands/generate_entity_commands.rb +19 -10
  20. data/lib/core/structure/commands/generate_invalidation_commands.rb +19 -9
  21. data/lib/core/structure/commands/list_contribution_targets.rb +9 -0
  22. data/lib/core/structure/commands/list_contributors.rb +1 -1
  23. data/lib/core/structure/structure.plugin +1 -1
  24. data/lib/vop/objects/chain.rb +6 -3
  25. data/lib/vop/objects/command.rb +14 -5
  26. data/lib/vop/objects/command_param.rb +22 -0
  27. data/lib/vop/objects/entities.rb +8 -8
  28. data/lib/vop/objects/entity.rb +57 -16
  29. data/lib/vop/objects/entity_definition.rb +22 -0
  30. data/lib/vop/objects/plugin.rb +46 -4
  31. data/lib/vop/objects/request.rb +9 -5
  32. data/lib/vop/parts/dependency_resolver.rb +0 -4
  33. data/lib/vop/parts/entity_loader.rb +0 -3
  34. data/lib/vop/parts/executor.rb +33 -8
  35. data/lib/vop/parts/plugin_finder.rb +6 -16
  36. data/lib/vop/search_path.rb +12 -0
  37. data/lib/vop/shell/shell.rb +134 -87
  38. data/lib/vop/shell/shell_formatter.rb +32 -17
  39. data/lib/vop/shell/shell_input_readline.rb +3 -0
  40. data/lib/vop/shell/shell_input_testable.rb +9 -3
  41. data/lib/vop/syntax/command_syntax.rb +22 -17
  42. data/lib/vop/syntax/entity_syntax.rb +21 -6
  43. data/lib/vop/syntax/plugin_syntax.rb +6 -0
  44. data/lib/vop/util/pluralizer.rb +9 -1
  45. data/lib/vop/version.rb +1 -1
  46. data/lib/vop/vop.rb +70 -44
  47. data/lib/vop.rb +11 -1
  48. data/vop.gemspec +8 -6
  49. metadata +103 -28
  50. data/lib/core/meta/commands/search_gems_for_plugins.rb +0 -38
  51. data/lib/core/meta/commands/search_path.rb +0 -6
  52. data/lib/core/structure/commands/list_plugins.rb +0 -3
  53. data/lib/vop/util/worker.rb +0 -24
@@ -1,4 +1,3 @@
1
- require "readline"
2
1
  require_relative "shell_formatter"
3
2
  require_relative "shell_input"
4
3
  require_relative "shell_input_readline"
@@ -8,6 +7,7 @@ module Vop
8
7
  class Shell
9
8
 
10
9
  attr_reader :context
10
+ attr_reader :last_response
11
11
 
12
12
  def initialize(op, input = nil)
13
13
  @op = op
@@ -15,7 +15,7 @@ module Vop
15
15
 
16
16
  @formatter = ShellFormatter.new
17
17
 
18
- # TODO for testing
18
+ # override for testing
19
19
  if input.nil?
20
20
  input = ShellInputReadline.new(method(:tab_completion))
21
21
  end
@@ -45,43 +45,48 @@ module Vop
45
45
  puts
46
46
  print @prompt
47
47
  else
48
- puts "\nbye"
48
+ puts "\n"
49
49
  exit
50
50
  end
51
51
  end
52
52
 
53
- def mix_arguments_and_context
54
- result = @arguments
53
+ def mix_arguments_and_context(command = nil, arguments = nil)
54
+ result = arguments || @arguments
55
55
  @context.each do |k,v|
56
- param = @command.param(k)
57
- if param && param.wants_context
58
- result[k] = @context[k]
56
+ cmd = command || @command
57
+ if cmd
58
+ param = (cmd).param(k)
59
+ if param && param.wants_context
60
+ result[k] = @context[k]
61
+ end
59
62
  end
60
63
  end
61
64
  result
62
65
  end
63
66
 
64
- def maybe_execute
65
- mandatory = @command.mandatory_params
66
-
67
- missing_mandatory_params = @command.mandatory_params.delete_if do |param|
68
- @arguments.keys.include?(param.name) ||
67
+ def missing_mandatory_params(command = @command, arguments = @arguments)
68
+ command.mandatory_params.delete_if do |param|
69
+ arguments.keys.include?(param.name) ||
69
70
  (@context.keys.include?(param.name) && param.wants_context)
70
71
  end
72
+ end
71
73
 
72
- if missing_mandatory_params.size > 0
73
- $logger.debug "missing params : #{missing_mandatory_params.map(&:name)}"
74
- end
75
-
74
+ def maybe_execute
76
75
  if missing_mandatory_params.size > 0
77
76
  @missing_params = missing_mandatory_params
78
77
  @prompt = "#{@command.short_name}.#{@missing_params.first.name} ? "
79
78
  else
80
79
  begin
81
- request = Request.new(@op, @command.short_name, @arguments, @context)
80
+ request = @op.prepare_request(@command.short_name, @arguments, @context, @command.short_name)
82
81
  request.shell = self
83
82
  response = @op.execute_request(request)
84
83
 
84
+ # log the last response for the "detail" command
85
+ unless @command.short_name == "detail"
86
+ @last_response = response
87
+ end
88
+
89
+ # mix context changes from the response into the local context
85
90
  @context.merge! response.context
86
91
 
87
92
  display_type = @formatter.analyze(request, response)
@@ -95,65 +100,92 @@ module Vop
95
100
  end
96
101
 
97
102
  def accept_param(line)
98
- current_param = @missing_params.shift
99
- $logger.debug "value for param #{current_param.name} : #{line}"
100
- @arguments[current_param.name] = line
103
+ unless line.nil?
104
+ current_param = @missing_params.shift
105
+ $logger.debug "value for param #{current_param.name} : #{line}"
106
+ @arguments[current_param.name] = line
101
107
 
102
- maybe_execute
108
+ maybe_execute
109
+ end
103
110
  end
104
111
 
105
- def parse_command_line(args)
106
- result = {}
107
- unless args.empty?
108
- args.each do |token|
109
- if token.include? "="
110
- (key, value) = token.split("=")
111
- result[key] = value
112
- else
113
- default_param = @command.default_param
114
- if default_param
115
- result[default_param.name] = args
116
- end
117
- end
112
+ def is_special?(command)
113
+ command.start_with?('$') || command.start_with?('@') || command.end_with?('?')
114
+ end
115
+
116
+ def handle_special(command)
117
+ if command.start_with?('$')
118
+ if command.start_with?('$vop')
119
+ puts "executing #{command}"
120
+ puts eval command
121
+ else
122
+ puts "unknown $-command #{command} - try '$vop' maybe?"
123
+ end
124
+ elsif command.start_with?('@')
125
+ if command.start_with?('@op')
126
+ puts "executing #{command}"
127
+ puts eval command
128
+ else
129
+ puts "unknown @-command #{command} - try '@op' maybe?"
118
130
  end
119
131
  end
120
- result
121
132
  end
122
133
 
123
- def parse_and_execute(command_line)
134
+ def parse(command_line)
124
135
  (command, *args) = command_line.split
125
136
 
137
+ arguments = {}
126
138
  if command
127
139
  $logger.debug "command : #{command}, args : #{args}"
128
- if command.start_with?('$')
129
- if command.start_with?('$vop')
130
- puts "executing #{command}"
131
- puts eval command
132
- else
133
- puts "unknown $-command #{command} - try '$vop' maybe?"
134
- end
135
- elsif command.start_with?('@')
136
- if command.start_with?('@op')
137
- puts "executing #{command}"
138
- puts eval command
139
- else
140
- puts "unknown @-command #{command} - try '@op' maybe?"
140
+
141
+ if command.end_with?("??")
142
+ target_command = command[0..-3]
143
+ command = "source"
144
+ arguments["name"] = target_command
145
+ elsif command.end_with?("?")
146
+ target_command = command[0..-2]
147
+ command = "help"
148
+ arguments["name"] = target_command
149
+ end
150
+
151
+ known_commands = @op.commands.keys
152
+ if known_commands.include? command
153
+ cmd = @op.commands[command]
154
+
155
+ unless args.empty? || is_special?(command)
156
+ args.each do |token|
157
+ if token.include? "="
158
+ (key, value) = token.split("=")
159
+ arguments[key] = value
160
+ else
161
+ default_param = cmd.default_param(mix_arguments_and_context)
162
+ if default_param
163
+ arguments[default_param.name] = args
164
+ end
165
+ end
166
+ end
141
167
  end
168
+ [command, cmd, arguments]
142
169
  else
143
- if command.end_with?("?")
144
- help_command = command[0..-2]
145
- command = "help"
146
- args << "name=#{help_command}"
147
- end
170
+ [command, nil, arguments]
171
+ end
172
+ end
173
+ end
148
174
 
175
+ def parse_and_execute(command_line)
176
+ command, cmd, arguments = parse(command_line)
177
+
178
+ if command
179
+ $logger.debug "command : #{command}, args : #{arguments}"
180
+ if is_special?(command)
181
+ handle_special(command_line)
182
+ else
149
183
  if command == "exit"
150
184
  @input.exit
151
185
  else
152
- known_commands = @op.commands.keys
153
- if known_commands.include? command
154
- @command = @op.commands[command]
155
- @arguments = parse_command_line(args)
156
-
186
+ if cmd
187
+ @command = cmd
188
+ @arguments = arguments
157
189
  maybe_execute
158
190
  else
159
191
  puts "unknown command '#{command}'"
@@ -161,47 +193,62 @@ module Vop
161
193
  end
162
194
  end
163
195
  end
164
-
165
196
  end
166
197
 
167
- def tab_completion(s)
168
- lookups = []
198
+ def complete_for_command(s)
199
+ current_param = @missing_params.first
200
+ if current_param && current_param.options.has_key?(:lookup)
201
+ begin
202
+ lookup_block = current_param.options[:lookup]
169
203
 
170
- if @command
171
- current_param = @missing_params.first
172
- if current_param && current_param.options.has_key?(:lookup)
173
- begin
174
- lookup_block = current_param.options[:lookup]
175
-
176
- # the lookup block might want the previously collected params as input
177
- lookups = if lookup_block.arity > 0
178
- params_for_lookup = mix_arguments_and_context
179
- lookup_block.call(params_for_lookup)
180
- else
181
- lookup_block.call()
182
- end
183
- rescue => detail
184
- $logger.error "problem loading lookup values for #{current_param.name} : #{detail.message}"
204
+ # the lookup block might want the previously collected params as input
205
+ lookups = if lookup_block&.arity > 0
206
+ params_for_lookup = mix_arguments_and_context
207
+ lookup_block.call(params_for_lookup)
208
+ else
209
+ lookup_block.call()
185
210
  end
211
+ lookups.grep /^#{Regexp.escape(s)}/
212
+ rescue => detail
213
+ $logger.error "problem loading lookup values for #{current_param.name} : #{detail.message}"
214
+ end
215
+ end
216
+ end
217
+
218
+ def complete_command_line(s)
219
+ potential_command, potential_cmd, potential_args = parse(s)
220
+ #$logger.debug "? >>#{potential_cmd}<< (#{potential_args}) [#{Readline.line_buffer}]"
221
+ if potential_cmd
222
+ default_param = potential_cmd.default_param
223
+ if default_param
224
+ lookups = default_param.lookup(mix_arguments_and_context)
225
+ lookups
226
+ .grep(/^#{Regexp.escape(s.split.last)}/)
227
+ .map do |lookup|
228
+ "#{potential_command} #{lookup}"
229
+ end
186
230
  end
187
231
  else
188
- lookups = @op.commands.keys.sort
232
+ lookups = @op.commands.keys.sort.grep /^#{Regexp.escape(s)}/
189
233
  end
190
234
 
191
- lookups.grep /^#{Regexp.escape(s)}/
192
235
  end
193
236
 
194
- def do_it(command_line = nil)
195
- #Readline.completion_append_character = ""
196
- #Readline.completion_proc = method(:tab_completion)
237
+ def tab_completion(s)
238
+ if @command
239
+ complete_for_command(s)
240
+ else
241
+ complete_command_line(s)
242
+ end
243
+ end
197
244
 
198
- if command_line
245
+ def do_it(command_line = nil)
246
+ if command_line && command_line != ""
199
247
  parse_and_execute(command_line)
200
248
  else
201
249
  while line = @input.read(@prompt)
202
- #while line = Readline.readline(@prompt, true)
203
250
  if @command
204
- # if a command has already been selected, we ask for missing params
251
+ # if a command has already been selected, ask for missing params
205
252
  accept_param(line)
206
253
  else
207
254
  # otherwise input is treated as regular command line (command + args)
@@ -213,7 +260,7 @@ module Vop
213
260
 
214
261
  def self.run(op = nil, command_line = nil)
215
262
  if op.nil?
216
- op = Vop.new
263
+ op = Vop.new(origin: "shell:#{Process.pid}@#{`hostname`.strip}")
217
264
  end
218
265
  self.new(op).do_it(command_line)
219
266
  end
@@ -35,10 +35,11 @@ module Vop
35
35
  command = request.command
36
36
  show_options = command.show_options
37
37
 
38
- result = case display_type
38
+ case display_type
39
39
  when :table
40
40
  columns_to_display =
41
41
  if show_options[:columns]
42
+ # TODO validate all columns exist?
42
43
  show_options[:columns]
43
44
  else
44
45
  # TODO this is not optimal - what if the second row has more keys than the first?
@@ -54,8 +55,9 @@ module Vop
54
55
  data.each do |row|
55
56
  values = [ ]
56
57
  columns_to_display.each do |key|
57
- values << (row[key.to_s] || row[key.to_sym])
58
- end
58
+ potential_value = row[key.to_s] || row[key.to_sym]
59
+ values << potential_value
60
+ end unless row.nil?
59
61
  rearranged << values
60
62
  end
61
63
 
@@ -79,18 +81,33 @@ module Vop
79
81
  "#{k} : #{v}"
80
82
  end.join("\n")
81
83
  when :entity_list
82
- data.sort_by { |e| e.id }.map do |entity|
83
- attributes = entity.data.sort_by do |x|
84
-
85
- end.map do |key, value|
86
- if key == entity.key
87
- nil
88
- else
89
- "#{key} : #{value}"
90
- end
91
- end.compact.join("\n ")
92
- "[#{entity.type}] #{entity.id}\n #{attributes}"
93
- end.join("\n")
84
+ sorted = data.sort_by { |e| e.id }
85
+
86
+ columns = if show_options[:columns]
87
+ show_options[:columns]
88
+ else
89
+ blacklisted_keys = %w|name params plugin_name|
90
+ all_keys = sorted.map { |x| x.data.keys }.flatten.uniq
91
+ all_keys.delete_if { |x| blacklisted_keys.include? x }
92
+ end
93
+
94
+ headers = [ sorted.first.key ] + columns
95
+
96
+ rows = sorted.map do |entity|
97
+ row = [ entity.id ]
98
+
99
+ columns.each do |column|
100
+ value = entity.data[column] || ""
101
+ row << (value.respond_to?(to_s) ? value.to_s[0..49] : value)
102
+ end
103
+
104
+ row
105
+ end
106
+
107
+ Terminal::Table.new(
108
+ rows: rows,
109
+ headings: headers
110
+ )
94
111
  when :entity
95
112
  entity = data
96
113
  "[#{entity.type}] #{entity.id}"
@@ -101,8 +118,6 @@ module Vop
101
118
  else
102
119
  raise "unknown display type #{display_type}"
103
120
  end
104
-
105
- result
106
121
  end
107
122
 
108
123
  end
@@ -1,3 +1,5 @@
1
+ require "readline"
2
+
1
3
  module Vop
2
4
 
3
5
  class ShellInputReadline
@@ -5,6 +7,7 @@ module Vop
5
7
  def initialize(completion_method)
6
8
  Readline.completion_append_character = ""
7
9
  Readline.completion_proc = completion_method
10
+ Readline.completer_word_break_characters = "\t\n\"\\'\`@$><=;|&{(" # default, but without space
8
11
  end
9
12
 
10
13
  def read(prompt)
@@ -3,17 +3,23 @@ module Vop
3
3
  class TestableShellInput
4
4
 
5
5
  attr_accessor :answers
6
- attr_reader :exit
6
+ attr_reader :exit, :prompt
7
+ attr_accessor :fail_if_out_of_answers
7
8
 
8
9
  def initialize
9
10
  @answers = []
10
11
  @exit = false
12
+ @prompt = nil
13
+ @fail_if_out_of_answers = true
11
14
  end
12
15
 
13
16
  def read(prompt)
17
+ @prompt = prompt
14
18
  answer = @answers.shift
15
- if answer.nil?
16
- raise "no more pre-defined answers (asked for '#{prompt}')"
19
+ if answer.nil? && @fail_if_out_of_answers
20
+ unless @exit
21
+ raise "no more pre-defined answers (asked for '#{prompt}')"
22
+ end
17
23
  end
18
24
  answer
19
25
  end
@@ -2,24 +2,17 @@ module Vop
2
2
 
3
3
  module CommandSyntax
4
4
 
5
- def run(&block)
6
- @command.block = block
7
- end
8
-
9
5
  def description(s)
10
6
  @command.description = s
11
7
  end
12
8
 
13
- def resolve_options_string(options)
14
- if options.is_a? String
15
- options = {
16
- description: options
17
- }
18
- end
19
- options
9
+ def run(&block)
10
+ @command.block = block
20
11
  end
21
12
 
22
- def param(name, options = {})
13
+ def param(name, options = {}, more_options = {})
14
+ options = resolve_options_string(options).merge(more_options)
15
+
23
16
  if name.is_a? Symbol
24
17
  key = "name" # default for select_machine
25
18
  entity = @op.entities.values.select { |x| x.short_name == name.to_s }.first
@@ -38,18 +31,15 @@ module Vop
38
31
  name = name.to_s
39
32
  end
40
33
 
41
- options = resolve_options_string(options)
42
-
43
34
  @command.add_param(name, options)
44
35
  end
45
36
 
46
- def param!(name, options = {})
47
- options = resolve_options_string(options)
37
+ def param!(name, options = {}, more_options = {})
38
+ options = resolve_options_string(options).merge(more_options)
48
39
  options.merge! mandatory: true
49
40
  param(name, options)
50
41
  end
51
42
 
52
- # TODO does not really work yet
53
43
  def block_param(name = "block", options = {})
54
44
  options.merge! block: true
55
45
  param(name, options)
@@ -69,6 +59,10 @@ module Vop
69
59
  @command.allows_extra = true
70
60
  end
71
61
 
62
+ def dont_log
63
+ @command.dont_log = true
64
+ end
65
+
72
66
  def show(options = {})
73
67
  @command.show_options[:columns] = options.delete(:columns)
74
68
  @command.show_options[:display_type] = options.delete(:display_type)
@@ -89,6 +83,17 @@ module Vop
89
83
  @command.invalidation_block = block
90
84
  end
91
85
 
86
+ private
87
+
88
+ def resolve_options_string(options)
89
+ if options.is_a? String
90
+ options = {
91
+ description: options
92
+ }
93
+ end
94
+ options
95
+ end
96
+
92
97
  end
93
98
 
94
99
  end
@@ -2,18 +2,20 @@ module Vop
2
2
 
3
3
  module EntitySyntax
4
4
 
5
+ def description(s)
6
+ @entity.description = s
7
+ end
8
+
5
9
  def key(key)
6
10
  @entity.key = key
7
11
  end
8
12
 
9
13
  def entity(options = { key: "name" }, &block)
10
- if block
11
- run(&block)
12
- end
14
+ run(&block)
13
15
  end
14
16
 
15
17
  def run(&block)
16
- @entity.block = block
18
+ @entity.block = block if block
17
19
  end
18
20
 
19
21
  def on(other_entity)
@@ -26,8 +28,21 @@ module Vop
26
28
 
27
29
  raise "unknown keyword #{options.keys.first}" if options.keys.length > 0
28
30
 
29
- @entity.show_options[:columns] = column_options
30
- @entity.show_options[:display_type] = display_type
31
+ @entity.show_options[:columns] = column_options if column_options
32
+ @entity.show_options[:display_type] = display_type if display_type
33
+ end
34
+
35
+ def invalidate(&block)
36
+ @entity.invalidation_block = block
37
+ end
38
+
39
+ def contribute(options, &block)
40
+ raise "missing option 'to'" unless options.has_key?(:to)
41
+ @op.register_contributor(
42
+ command_name: options[:to],
43
+ contributor: @entity.name.to_s.carefully_pluralize
44
+ )
45
+ run(&block)
31
46
  end
32
47
 
33
48
  end
@@ -42,6 +42,12 @@ module Vop
42
42
  end
43
43
  end
44
44
 
45
+ # TODO: support version requirements
46
+ def depends_on_gem(gem, **options)
47
+ $logger.debug "plugin #{@plugin.name} depends on gem #{gem}"
48
+ @plugin.external_dependencies[:gem] << [gem, options]
49
+ end
50
+
45
51
  def on(hook_sym, &block)
46
52
  @plugin.hook(hook_sym, &block)
47
53
  end
@@ -2,7 +2,7 @@ begin
2
2
  require "active_support/inflector"
3
3
  rescue Exception => e
4
4
  message = "active_support inflector cannot be loaded - pluralization results may deviate : #{e.message}"
5
- puts message
5
+ #puts message
6
6
  end
7
7
 
8
8
  module Vop
@@ -19,6 +19,14 @@ module Vop
19
19
  end
20
20
  end
21
21
 
22
+ def carefully_singularize
23
+ begin
24
+ self.singularize
25
+ rescue
26
+ "#{self[0..-2]}"
27
+ end
28
+ end
29
+
22
30
  end
23
31
 
24
32
  end
data/lib/vop/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Vop
2
- VERSION = "0.3.5"
2
+ VERSION = "0.3.6"
3
3
  end