vop 0.3.5 → 0.3.6

Sign up to get free protection for your applications and to get access to all the features.
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