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
@@ -15,9 +15,12 @@ module Vop
15
15
 
16
16
  def execute(request)
17
17
  next_link = self.next
18
- response = nil
19
- response = next_link.execute(request) if next_link
20
- response
18
+
19
+ if next_link
20
+ next_link.execute(request)
21
+ else
22
+ nil
23
+ end
21
24
  end
22
25
 
23
26
  end
@@ -13,7 +13,7 @@ module Vop
13
13
  attr_accessor :invalidation_block
14
14
 
15
15
  attr_accessor :show_options
16
- attr_accessor :dont_register
16
+ attr_accessor :dont_register, :dont_log
17
17
  attr_accessor :read_only
18
18
  attr_accessor :allows_extra
19
19
 
@@ -29,6 +29,8 @@ module Vop
29
29
  @show_options = {}
30
30
 
31
31
  @dont_register = false
32
+ @dont_log = false
33
+
32
34
  @read_only = false
33
35
  @allows_extra = false
34
36
  end
@@ -55,23 +57,30 @@ module Vop
55
57
  end
56
58
  end
57
59
 
58
- def mandatory_params
60
+ def mandatory_params(values = {})
59
61
  params_with { |x| x.options[:mandatory] == true }
60
62
  end
61
63
 
64
+ def missing_mandatory_params(values = {})
65
+ mandatory_params.select do |param|
66
+ ! values.keys.include?(param.name.to_sym) &&
67
+ ! values.keys.include?(param.name.to_s)
68
+ end
69
+ end
70
+
62
71
  # The default param is the one used when a command is called with a single "scalar" param only, like
63
72
  # @op.foo("zaphod")
64
73
  # If a parameter is marked as default, it will be assigned the value "zaphod" in this case.
65
74
  # If there is only a single param, it is the default param by default
66
75
  # Also, if there is only one mandatory param, it is considered to be the default param
67
- def default_param
76
+ def default_param(values = {})
68
77
  if params.size == 1
69
78
  params.first
70
79
  else
71
80
  result = params_with { |x| x.options[:default_param] == true }.first
72
81
  if result.nil?
73
- mandatory = mandatory_params
74
- if mandatory_params.size == 1
82
+ mandatory = missing_mandatory_params(values)
83
+ if mandatory.size == 1
75
84
  result = mandatory.first
76
85
  end
77
86
  end
@@ -26,6 +26,21 @@ module Vop
26
26
  @options = defaults.merge(options)
27
27
  end
28
28
 
29
+ def lookup(current_values = {})
30
+ begin
31
+ return [] unless lookup_block = options[:lookup]
32
+
33
+ # the lookup block might want the previously collected params as input
34
+ lookups = if lookup_block.arity > 0
35
+ lookup_block.call(current_values)
36
+ else
37
+ lookup_block.call()
38
+ end
39
+ rescue => detail
40
+ $logger.error "problem loading lookup values for #{name} : #{detail.message}"
41
+ end
42
+ end
43
+
29
44
  # some params do not want to prefilled from the context
30
45
  def wants_context
31
46
  !(
@@ -34,6 +49,13 @@ module Vop
34
49
  )
35
50
  end
36
51
 
52
+ def to_json(options)
53
+ {
54
+ name: @name,
55
+ options: @options
56
+ }.to_json(options)
57
+ end
58
+
37
59
  end
38
60
 
39
61
  end
@@ -3,17 +3,17 @@ module Vop
3
3
  class Entities < Array
4
4
 
5
5
  def [](key)
6
- # if key.is_a? Numeric
7
- # super(key)
8
- # else
9
- $logger.debug "accessing entity with key '#{key}'"
10
- found = select { |x| x.id == key }.first
11
- if found
12
- found
6
+ $logger.debug "accessing entity with key '#{key}'"
7
+ found = select { |x| x.id == key }.first
8
+ if found
9
+ found
10
+ else
11
+ if key.to_i.to_s == key.to_s
12
+ super(key.to_i)
13
13
  else
14
14
  raise "no element with key '#{key}'"
15
15
  end
16
- # end
16
+ end
17
17
  end
18
18
 
19
19
  end
@@ -4,10 +4,11 @@ module Vop
4
4
 
5
5
  attr_reader :type, :data, :key
6
6
 
7
- def initialize(op, type, key, data)
7
+ def initialize(op, definition, data)
8
8
  @op = op
9
- @type = type
10
- @key = key
9
+ @type = definition.short_name
10
+ @key = definition.key
11
+ @definition = definition
11
12
  @data = data
12
13
 
13
14
  unless @data.has_key? @key
@@ -15,6 +16,7 @@ module Vop
15
16
  end
16
17
 
17
18
  make_methods_for_commands
19
+ make_methods_for_data
18
20
  make_method_for_id
19
21
  end
20
22
 
@@ -32,40 +34,79 @@ module Vop
32
34
  end
33
35
  end
34
36
 
37
+ def ancestor_names
38
+ # TODO : would be nice if this operation was transitive
39
+ @op.list_contribution_targets(source_command: @definition.name)
40
+ end
41
+
35
42
  # all commands that have a parameter with the same name as the entity
36
43
  # are considered eligible for this entity (TODO that's too broad, isn't it?)
37
44
  def entity_commands
38
- result = @op.commands.values.select do |command|
39
- command.params.select do |param|
40
- param.name == @type
41
- end.count > 0
42
- end
43
- @command_count = result.count
44
- result
45
+ namified = ancestor_names.map { |x| x.carefully_singularize }
46
+ similar_names = [ @type.to_s ] + namified
47
+ # TODO [performance] this is probably expensive, move into definition time?
48
+ @op.commands.values.flat_map do |command|
49
+ similar_names.map do |similar_name|
50
+ # TODO we might also want to check if param.entity?
51
+ next unless command.params.any? { |param| param.name == similar_name }
52
+ [command.short_name, similar_name]
53
+ end.compact
54
+ end.to_h
45
55
  end
46
56
 
47
57
  def make_methods_for_commands
48
- entity_commands.each do |command|
49
- # TODO this is very similar to code in Vop.<<
50
- self.class.send(:define_method, command.short_name) do |*args, &block|
51
- $logger.debug "[#{@type}:#{id}] #{command.short_name} (#{args.pretty_inspect}, block? #{block_given?})"
58
+ entity_commands.each do |command_name, similar_name|
59
+ # TODO this used to be very similar to code in Vop.<<
60
+ define_singleton_method command_name.to_sym do |*args, &block|
61
+ $logger.debug "[#{@type}:#{id}] #{command_name} (#{args.pretty_inspect}, block? #{block_given?})"
52
62
  ruby_args = args.length > 0 ? args[0] : {}
53
63
  # TODO we might want to do this only if there's a block param defined
54
64
  # TODO this does not work if *args comes with a scalar default param
55
65
  if block
56
66
  ruby_args["block"] = block
57
67
  end
58
- @op.execute(command.short_name, ruby_args, { @type.to_s => id })
68
+ extra = { similar_name => id }
69
+ if @definition.on
70
+ if @data[@definition.on.to_s]
71
+ extra[@definition.on.to_s] = @data[@definition.on.to_s]
72
+ else
73
+ $logger.warn "entity #{id} does not seem to have data with key #{@definition.on}, though that's required through the 'on' keyword"
74
+ end
75
+ end
76
+ @op.execute(command_name, ruby_args, extra)
77
+ end
78
+ end
79
+ end
80
+
81
+ def make_methods_for_data
82
+ @data.each do |k,v|
83
+ define_singleton_method k.to_sym do |*args|
84
+ v
59
85
  end
60
86
  end
61
87
  end
62
88
 
63
89
  def make_method_for_id
64
- self.class.send(:define_method, @key) do |*args|
90
+ define_singleton_method @key.to_sym do |*args|
65
91
  id
66
92
  end
67
93
  end
68
94
 
95
+ def to_json(options = nil)
96
+ {
97
+ entity: type,
98
+ key: key,
99
+ data: data
100
+ }.to_json(options)
101
+ end
102
+
103
+ def self.from_json(op, json_data)
104
+ parsed = JSON.parse(json_data)
105
+ entity_name = parsed["entity"]
106
+ definition = op.entities[entity_name]
107
+ new(op, definition, parsed["data"])
108
+ end
109
+
69
110
  def to_s
70
111
  "Vop::Entity (#{@type})"
71
112
  end
@@ -3,11 +3,16 @@ module Vop
3
3
  class EntityDefinition
4
4
 
5
5
  attr_reader :plugin, :name
6
+ attr_accessor :description
7
+
6
8
  attr_accessor :key
7
9
  attr_accessor :block
8
10
  attr_accessor :on
9
11
  attr_accessor :show_options
10
12
 
13
+ attr_accessor :invalidation_block
14
+ attr_accessor :read_only
15
+
11
16
  def initialize(plugin, name)
12
17
  @plugin = plugin
13
18
  @name = name
@@ -15,9 +20,11 @@ module Vop
15
20
  @data = {}
16
21
 
17
22
  @block = lambda { |params| $logger.warn "entity #{name} does not have a run block" }
23
+ @invalidation_block = nil
18
24
 
19
25
  @on = nil
20
26
  @show_options = {}
27
+ @read_only = true
21
28
  end
22
29
 
23
30
  def short_name
@@ -28,6 +35,21 @@ module Vop
28
35
  plugin.sources[:entities][name]
29
36
  end
30
37
 
38
+ def list_command_name
39
+ short_name.carefully_pluralize
40
+ end
41
+
42
+ # this would be necessary if invalidation commands were generated for entities
43
+ # def params
44
+ # result = []
45
+ #
46
+ # if @on
47
+ # result << CommandParam.new(@on.to_s, mandatory: true)
48
+ # end
49
+ #
50
+ # result
51
+ # end
52
+
31
53
  end
32
54
 
33
55
  end
@@ -1,4 +1,5 @@
1
1
  require "json"
2
+
2
3
  require_relative "../parts/entity_loader"
3
4
  require_relative "../parts/command_loader"
4
5
  require_relative "../parts/filter_loader"
@@ -9,6 +10,7 @@ module Vop
9
10
  class Plugin < ThingWithParams
10
11
 
11
12
  attr_reader :op
13
+
12
14
  attr_reader :name
13
15
  attr_accessor :description
14
16
  attr_reader :options
@@ -17,7 +19,9 @@ module Vop
17
19
  attr_reader :sources
18
20
  attr_reader :state
19
21
  attr_reader :config
22
+
20
23
  attr_accessor :dependencies
24
+ attr_accessor :external_dependencies
21
25
 
22
26
  def initialize(op, plugin_name, plugin_path, options = {})
23
27
  super()
@@ -39,28 +43,44 @@ module Vop
39
43
  @config = {}
40
44
 
41
45
  @dependencies = []
46
+ @external_dependencies = Hash.new { |h, k| h[k] = [] }
42
47
 
43
48
  @hooks = {}
44
49
  end
45
50
 
51
+ def auto_load?
52
+ @options[:auto_load]
53
+ end
54
+
46
55
  def to_s
47
56
  "Vop::Plugin #{name}"
48
57
  end
49
58
 
50
- def init
59
+ def inspect
60
+ {
61
+ name: name,
62
+ path: @path
63
+ }.to_json()
64
+ end
65
+
66
+ def load
51
67
  $logger.debug "plugin init : #{@name}"
52
68
 
53
69
  @sources = Hash.new { |h, k| h[k] = {} }
54
70
 
55
71
  @config = {}
56
72
 
57
- # call_hook :preload ?
58
73
  load_helpers
59
74
  load_default_config
60
75
  load_config
61
76
 
77
+ load_gem_dependencies unless ENV["VOP_IGNORE_PLUGINS"]
78
+ end
79
+
80
+ def init
62
81
  # TODO proceed only if auto_load
63
82
  call_hook :init
83
+
64
84
  load_entities
65
85
  load_commands
66
86
  load_filters
@@ -105,6 +125,18 @@ module Vop
105
125
  end
106
126
  end
107
127
 
128
+ def load_gem_dependencies
129
+ gems = @external_dependencies[:gem]
130
+ return unless gems.any?
131
+ $logger.debug "loading gem dependencies : #{gems}"
132
+
133
+ gems.each do |g|
134
+ gem, options = g
135
+ gem_name = options[:require] || gem
136
+ require gem_name
137
+ end
138
+ end
139
+
108
140
  def load_code_from_dir(type_name)
109
141
  dir = plugin_dir(type_name)
110
142
 
@@ -186,15 +218,25 @@ module Vop
186
218
  @hooks[name.to_sym] = block
187
219
  end
188
220
 
221
+ def has_hook?(name)
222
+ ! @hooks[name.to_sym].nil?
223
+ end
224
+
189
225
  def call_hook(name, *args)
190
226
  result = nil
191
227
  if @hooks.has_key? name
192
- result = @hooks[name].call(self, *args)
228
+ begin
229
+ result = @hooks[name].call(self, *args)
230
+ rescue => e
231
+ $logger.error "hit a problem while running hook #{name} on #{self.name}"
232
+ raise e
233
+ end
193
234
  end
194
235
  result
195
236
  end
196
237
 
197
- def template_path(name)
238
+ def template_path(name_or_sym)
239
+ name = name_or_sym.to_s
198
240
  name += ".erb" unless name.end_with? ".erb"
199
241
  File.join(plugin_dir(:templates), name)
200
242
  end
@@ -8,8 +8,10 @@ module Vop
8
8
 
9
9
  attr_reader :command_name, :param_values, :extra
10
10
  attr_accessor :shell
11
+ attr_accessor :origin
12
+ attr_accessor :dont_log
11
13
 
12
- def initialize(op, command_name, param_values = {}, extra = {})
14
+ def initialize(op, command_name, param_values = {}, extra = {}, origin = nil)
13
15
  @op = op
14
16
  @command_name = command_name
15
17
  raise "unknown command '#{command_name}'" if command.nil?
@@ -19,6 +21,10 @@ module Vop
19
21
 
20
22
  @current_filter = nil
21
23
  @filter_chain = @op.filter_chain.clone
24
+ # TODO not sure if this is really a hash out in the wild
25
+ @origin = origin || {}
26
+
27
+ @dont_log = false
22
28
  end
23
29
 
24
30
  def command
@@ -40,13 +46,14 @@ module Vop
40
46
 
41
47
  def self.from_json(op, json)
42
48
  hash = JSON.parse(json)
43
- self.new(op, hash["command"], hash["params"], hash["extra"])
49
+ self.new(op, hash["command"], hash["params"], hash["extra"], hash["origin"])
44
50
  end
45
51
 
46
52
  def to_json
47
53
  {
48
54
  command: @command_name,
49
55
  params: @param_values,
56
+ origin: @origin,
50
57
  extra: @extra
51
58
  }.to_json
52
59
  end
@@ -56,9 +63,6 @@ module Vop
56
63
  end
57
64
 
58
65
  def execute
59
- result = nil
60
- context = nil
61
-
62
66
  # build a chain out of all filters + the command itself
63
67
  filter_chain = @op.filter_chain.clone.map {
64
68
  |filter_name| @op.filters[filter_name.split(".").first]
@@ -18,8 +18,6 @@ module Vop
18
18
  root_plugin = Plugin.new(@op, "__root__", nil)
19
19
  root_plugin.dependencies = @plugins.keys
20
20
 
21
- $logger.debug "root dummy : #{root_plugin}"
22
-
23
21
  resolve(root_plugin, resolved, unresolved)
24
22
  resolved.delete_if { |x| x == root_plugin.name }
25
23
 
@@ -27,11 +25,9 @@ module Vop
27
25
  end
28
26
 
29
27
  def resolve(plugin, resolved, unresolved, level = 0)
30
- $logger.debug "#{' ' * level}checking dependencies for #{plugin.name}"
31
28
  unresolved << plugin.name
32
29
 
33
30
  plugin.dependencies.each do |dep|
34
- $logger.debug "#{' ' * level}resolving #{dep}"
35
31
  already_loaded = @op.plugins.map(&:name).include? dep
36
32
  unless already_loaded
37
33
  unless resolved.include? dep
@@ -13,8 +13,6 @@ module Vop
13
13
 
14
14
  @plugin.inject_helpers(self)
15
15
 
16
- #@command = @entity
17
- #extend CommandSyntax
18
16
  extend EntitySyntax
19
17
  end
20
18
 
@@ -26,7 +24,6 @@ module Vop
26
24
  end
27
25
 
28
26
  def read_sources(named_sources)
29
- # reads a hash of <name> => <source string>
30
27
  named_sources.each do |name, source|
31
28
 
32
29
  prepare(name)
@@ -17,7 +17,7 @@ module Vop
17
17
  else
18
18
  # if there is a default param, it can be passed to execute as "scalar"
19
19
  # param, but it will be converted into a "normal" named param
20
- dp = request.command.default_param
20
+ dp = request.command.default_param(request.extra)
21
21
  if dp
22
22
  result = {
23
23
  dp.name => ruby_args
@@ -118,12 +118,36 @@ module Vop
118
118
  entity = entity_list.select { |x| x.short_name == name.to_s }.first
119
119
 
120
120
  unless entity.nil?
121
- #$logger.debug "auto-inflating entity #{name.to_s} (#{param})"
122
-
123
121
  list_command_name = entity.short_name.carefully_pluralize
124
- the_list = @op.execute(list_command_name, {})
125
- #$logger.debug "inflated entity list : #{the_list.size} entities"
126
- param = the_list.select { |x| x[entity.key] == param }.first
122
+ list_command_params = {}
123
+ if entity.on
124
+ on_name = prepared[entity.on] || prepared[entity.on.to_s]
125
+ if on_name.nil?
126
+ raise "missing parameter #{entity.on} for stacked entity #{name}"
127
+ else
128
+ list_command_params[entity.on] = on_name
129
+ end
130
+ end
131
+ the_list = @op.execute(list_command_name, list_command_params)
132
+
133
+ # param might be :multi, autobox if necessary
134
+ autobox = ! command_param.options[:multi] && ! param.is_a?(Array)
135
+ inflatables = autobox ? [ param ] : param
136
+ inflated = []
137
+ inflatables.each do |inflatable|
138
+ i = the_list.select { |x| x[entity.key] == inflatable }.first
139
+ if i.nil?
140
+ $logger.warn "problem auto-inflating entity with key #{inflatable}"
141
+ else
142
+ $logger.debug "auto-inflated #{name.to_s} entity : #{i}"
143
+ inflated << i
144
+ end
145
+ end
146
+ if autobox
147
+ param = inflated.first
148
+ else
149
+ param = inflated
150
+ end
127
151
  end
128
152
  end
129
153
 
@@ -135,13 +159,14 @@ module Vop
135
159
  end
136
160
 
137
161
  def execute(request)
138
- blacklist = %w|list_contributors collect_contributions machines rails_machines|
162
+ # TODO: use command.dont_log instead?
163
+ blacklist = %w|list_contributors list_contribution_targets collect_contributions machines rails_machines|
139
164
  unless blacklist.include? request.command.short_name
140
165
  $logger.debug "+++ #{request.command.short_name} (#{request.param_values}) +++"
141
166
  end
142
167
  command = request.command
143
168
 
144
- context = {}
169
+ context = request.extra
145
170
  block_param_names = request.command.block.parameters.map { |x| x.last }
146
171
  payload = prepare_payload(request, context, block_param_names)
147
172
  result = command.execute(payload)
@@ -19,24 +19,14 @@ module Vop
19
19
  reset
20
20
 
21
21
  $logger.debug "scanning #{paths} for plugins..."
22
+
22
23
  paths = [ paths ] unless paths.is_a? Array
24
+ paths.each do |path|
25
+ next unless File.exists? path
23
26
 
24
- if paths.size > 0
25
- paths.each do |path|
26
- begin
27
- next unless File.exists? path
28
- rescue => e
29
- if e.message =~ /Fixnum/
30
- $logger.warn "unexpected Fixnum path : #{path}"
31
- end
32
- raise e
33
- end
34
-
35
-
36
- @plugins += Dir.glob("#{path}/**/*.plugin").map { |x| Pathname.new(File.dirname(x)).realpath.to_s }
37
- @templates += Dir.glob("#{path}/**/plugin.vop").map { |x| Pathname.new(x).realpath.to_s }
38
- end
39
- end
27
+ @plugins += Dir.glob("#{path}/**/*.plugin").map { |x| Pathname.new(File.dirname(x)).realpath.to_s }
28
+ @templates += Dir.glob("#{path}/**/plugin.vop").map { |x| Pathname.new(x).realpath.to_s }
29
+ end unless paths.size.zero?
40
30
 
41
31
  self
42
32
  end
@@ -0,0 +1,12 @@
1
+ require_relative "vop"
2
+
3
+ module Vop
4
+
5
+ def self.gem_dependencies
6
+ @cached_gem_dependencies ||= begin
7
+ vop = ::Vop::Vop.new(no_init: true)
8
+ vop.plugins.flat_map { |p| p.external_dependencies[:gem].map(&:first) }.uniq
9
+ end
10
+ end
11
+
12
+ end