vop 0.3.4 → 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 -8
  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 +16 -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 -85
  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 +26 -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 +72 -42
  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,41 +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
- $logger.debug "missing params : #{missing_mandatory_params.map(&:name)}"
73
-
74
+ def maybe_execute
74
75
  if missing_mandatory_params.size > 0
75
76
  @missing_params = missing_mandatory_params
76
77
  @prompt = "#{@command.short_name}.#{@missing_params.first.name} ? "
77
78
  else
78
79
  begin
79
- request = Request.new(@op, @command.short_name, @arguments, @context)
80
+ request = @op.prepare_request(@command.short_name, @arguments, @context, @command.short_name)
80
81
  request.shell = self
81
82
  response = @op.execute_request(request)
82
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
83
90
  @context.merge! response.context
84
91
 
85
92
  display_type = @formatter.analyze(request, response)
@@ -93,65 +100,92 @@ module Vop
93
100
  end
94
101
 
95
102
  def accept_param(line)
96
- current_param = @missing_params.shift
97
- $logger.debug "value for param #{current_param.name} : #{line}"
98
- @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
99
107
 
100
- maybe_execute
108
+ maybe_execute
109
+ end
101
110
  end
102
111
 
103
- def parse_command_line(args)
104
- result = {}
105
- unless args.empty?
106
- args.each do |token|
107
- if token.include? "="
108
- (key, value) = token.split("=")
109
- result[key] = value
110
- else
111
- default_param = @command.default_param
112
- if default_param
113
- result[default_param.name] = args
114
- end
115
- 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?"
116
130
  end
117
131
  end
118
- result
119
132
  end
120
133
 
121
- def parse_and_execute(command_line)
134
+ def parse(command_line)
122
135
  (command, *args) = command_line.split
123
136
 
137
+ arguments = {}
124
138
  if command
125
139
  $logger.debug "command : #{command}, args : #{args}"
126
- if command.start_with?('$')
127
- if command.start_with?('$vop')
128
- puts "executing #{command}"
129
- puts eval command
130
- else
131
- puts "unknown $-command #{command} - try '$vop' maybe?"
132
- end
133
- elsif command.start_with?('@')
134
- if command.start_with?('@op')
135
- puts "executing #{command}"
136
- puts eval command
137
- else
138
- 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
139
167
  end
168
+ [command, cmd, arguments]
140
169
  else
141
- if command.end_with?("?")
142
- help_command = command[0..-2]
143
- command = "help"
144
- args << "name=#{help_command}"
145
- end
170
+ [command, nil, arguments]
171
+ end
172
+ end
173
+ end
174
+
175
+ def parse_and_execute(command_line)
176
+ command, cmd, arguments = parse(command_line)
146
177
 
178
+ if command
179
+ $logger.debug "command : #{command}, args : #{arguments}"
180
+ if is_special?(command)
181
+ handle_special(command_line)
182
+ else
147
183
  if command == "exit"
148
184
  @input.exit
149
185
  else
150
- known_commands = @op.commands.keys
151
- if known_commands.include? command
152
- @command = @op.commands[command]
153
- @arguments = parse_command_line(args)
154
-
186
+ if cmd
187
+ @command = cmd
188
+ @arguments = arguments
155
189
  maybe_execute
156
190
  else
157
191
  puts "unknown command '#{command}'"
@@ -159,47 +193,62 @@ module Vop
159
193
  end
160
194
  end
161
195
  end
162
-
163
196
  end
164
197
 
165
- def tab_completion(s)
166
- 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]
167
203
 
168
- if @command
169
- current_param = @missing_params.first
170
- if current_param && current_param.options.has_key?(:lookup)
171
- begin
172
- lookup_block = current_param.options[:lookup]
173
-
174
- # the lookup block might want the previously collected params as input
175
- lookups = if lookup_block.arity > 0
176
- params_for_lookup = mix_arguments_and_context
177
- lookup_block.call(params_for_lookup)
178
- else
179
- lookup_block.call()
180
- end
181
- rescue => detail
182
- $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()
183
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
184
230
  end
185
231
  else
186
- lookups = @op.commands.keys.sort
232
+ lookups = @op.commands.keys.sort.grep /^#{Regexp.escape(s)}/
187
233
  end
188
234
 
189
- lookups.grep /^#{Regexp.escape(s)}/
190
235
  end
191
236
 
192
- def do_it(command_line = nil)
193
- #Readline.completion_append_character = ""
194
- #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
195
244
 
196
- if command_line
245
+ def do_it(command_line = nil)
246
+ if command_line && command_line != ""
197
247
  parse_and_execute(command_line)
198
248
  else
199
249
  while line = @input.read(@prompt)
200
- #while line = Readline.readline(@prompt, true)
201
250
  if @command
202
- # if a command has already been selected, we ask for missing params
251
+ # if a command has already been selected, ask for missing params
203
252
  accept_param(line)
204
253
  else
205
254
  # otherwise input is treated as regular command line (command + args)
@@ -211,7 +260,7 @@ module Vop
211
260
 
212
261
  def self.run(op = nil, command_line = nil)
213
262
  if op.nil?
214
- op = Vop.new
263
+ op = Vop.new(origin: "shell:#{Process.pid}@#{`hostname`.strip}")
215
264
  end
216
265
  self.new(op).do_it(command_line)
217
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)
@@ -85,6 +79,21 @@ module Vop
85
79
  @command.block = block
86
80
  end
87
81
 
82
+ def invalidate(&block)
83
+ @command.invalidation_block = block
84
+ end
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
+
88
97
  end
89
98
 
90
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.4"
2
+ VERSION = "0.3.6"
3
3
  end