exel 1.2.1 → 1.3.0

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 (60) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +5 -4
  3. data/.rubocop_todo.yml +1 -13
  4. data/Gemfile +1 -0
  5. data/Guardfile +1 -0
  6. data/README.md +95 -30
  7. data/Rakefile +1 -0
  8. data/exel.gemspec +2 -2
  9. data/lib/exel.rb +6 -1
  10. data/lib/exel/ast_node.rb +4 -9
  11. data/lib/exel/context.rb +1 -0
  12. data/lib/exel/deferred_context_value.rb +1 -0
  13. data/lib/exel/error/job_termination.rb +9 -0
  14. data/lib/exel/events.rb +1 -0
  15. data/lib/exel/instruction.rb +3 -1
  16. data/lib/exel/instruction_node.rb +1 -0
  17. data/lib/exel/job.rb +1 -0
  18. data/lib/exel/listen_instruction.rb +1 -0
  19. data/lib/exel/logging.rb +22 -1
  20. data/lib/exel/logging/logger_wrapper.rb +28 -0
  21. data/lib/exel/logging_helper.rb +35 -0
  22. data/lib/exel/middleware/chain.rb +66 -0
  23. data/lib/exel/middleware/logging.rb +30 -0
  24. data/lib/exel/null_instruction.rb +1 -0
  25. data/lib/exel/processor_helper.rb +7 -0
  26. data/lib/exel/processors/async_processor.rb +2 -8
  27. data/lib/exel/processors/run_processor.rb +2 -7
  28. data/lib/exel/processors/split_processor.rb +5 -8
  29. data/lib/exel/providers/local_file_provider.rb +1 -0
  30. data/lib/exel/providers/threaded_async_provider.rb +1 -0
  31. data/lib/exel/sequence_node.rb +1 -0
  32. data/lib/exel/value.rb +1 -0
  33. data/lib/exel/version.rb +2 -1
  34. data/spec/exel/ast_node_spec.rb +24 -3
  35. data/spec/exel/context_spec.rb +1 -0
  36. data/spec/exel/deferred_context_value_spec.rb +1 -0
  37. data/spec/exel/events_spec.rb +1 -0
  38. data/spec/exel/instruction_node_spec.rb +1 -0
  39. data/spec/exel/instruction_spec.rb +6 -0
  40. data/spec/exel/job_spec.rb +1 -0
  41. data/spec/exel/listen_instruction_spec.rb +1 -0
  42. data/spec/exel/logging/logger_wrapper_spec.rb +95 -0
  43. data/spec/exel/logging_helper_spec.rb +25 -0
  44. data/spec/exel/logging_spec.rb +36 -3
  45. data/spec/exel/middleware/chain_spec.rb +67 -0
  46. data/spec/exel/middleware/logging_spec.rb +33 -0
  47. data/spec/exel/middleware_spec.rb +69 -0
  48. data/spec/exel/null_instruction_spec.rb +1 -0
  49. data/spec/exel/processors/async_processor_spec.rb +1 -0
  50. data/spec/exel/processors/run_processor_spec.rb +1 -0
  51. data/spec/exel/processors/split_processor_spec.rb +2 -7
  52. data/spec/exel/providers/local_file_provider_spec.rb +1 -0
  53. data/spec/exel/providers/threaded_async_provider_spec.rb +1 -0
  54. data/spec/exel/sequence_node_spec.rb +1 -0
  55. data/spec/exel/value_spec.rb +1 -0
  56. data/spec/exel_spec.rb +1 -0
  57. data/spec/integration/integration_spec.rb +1 -0
  58. data/spec/spec_helper.rb +1 -0
  59. data/spec/support/integration_test_classes.rb +1 -0
  60. metadata +18 -18
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+ module EXEL
3
+ # Logging related helper methods for processors
4
+ module LoggingHelper
5
+ # @return [Logger] Returns the EXEL logger
6
+ def logger
7
+ EXEL.logger
8
+ end
9
+
10
+ # Logs a message with DEBUG severity
11
+ def log_debug(message)
12
+ logger.debug(message)
13
+ end
14
+
15
+ # Logs a message with INFO severity
16
+ def log_info(message)
17
+ logger.info(message)
18
+ end
19
+
20
+ # Logs a message with WARN severity
21
+ def log_warn(message)
22
+ logger.warn(message)
23
+ end
24
+
25
+ # Logs a message with ERROR severity
26
+ def log_error(message)
27
+ logger.error(message)
28
+ end
29
+
30
+ # Logs a message with FATAL severity
31
+ def log_fatal(message)
32
+ logger.fatal(message)
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+ module EXEL
3
+ # Middleware is code configured to run around each processor execution. Custom middleware can be added as follows:
4
+ #
5
+ # EXEL.configure do |config|
6
+ # config.middleware.add(MyMiddleware)
7
+ # config.middleware.add(AnotherMiddleware, 'constructor arg')
8
+ # end
9
+ #
10
+ # Middleware can be any class that implements a +call+ method that includes a call to +yield+:
11
+ #
12
+ # class MyMiddleware
13
+ # def call(processor, context, args)
14
+ # puts 'before process'
15
+ # yield
16
+ # puts 'after process'
17
+ # end
18
+ # end
19
+ #
20
+ # The +call+ method will be passed the class of the processor that will be executed, the current context, and any args
21
+ # that were passed to the processor in the job definition.
22
+ module Middleware
23
+ # Chain of middleware to be invoked in sequence around each processor execution.
24
+ class Chain
25
+ attr_reader :entries
26
+
27
+ Entry = Struct.new(:klass, :args)
28
+
29
+ def initialize
30
+ @entries = []
31
+ end
32
+
33
+ # Adds a middleware class to the chain. If it is already in the chain it will be removed and added to the end.
34
+ # Any additional arguments will be passed to +new+ when the middleware is created.
35
+ def add(klass, *args)
36
+ remove(klass)
37
+ @entries << Entry.new(klass, args)
38
+ end
39
+
40
+ # Removes a middleware class from the chain.
41
+ def remove(klass)
42
+ @entries.delete_if { |entry| entry.klass == klass }
43
+ end
44
+
45
+ # Returns true if the given class is in the chain.
46
+ def include?(klass)
47
+ @entries.any? { |entry| entry.klass == klass }
48
+ end
49
+
50
+ # Calls each middleware in the chain.
51
+ def invoke(*args)
52
+ chain = @entries.map { |entry| entry.klass.new(*entry.args) }
53
+
54
+ traverse_chain = lambda do
55
+ if chain.empty?
56
+ yield
57
+ else
58
+ chain.shift.call(*args, &traverse_chain)
59
+ end
60
+ end
61
+
62
+ traverse_chain.call
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+ module EXEL
3
+ module Middleware
4
+ # Middleware to add a prefix to all messages logged during processor execution. The prefix is specified by the
5
+ # +:log_prefix+ key in the context. Also logs start, finish, and failure of the processor execution.
6
+ class Logging
7
+ def call(processor_class, context, _args, &block)
8
+ EXEL::Logging.with_prefix("#{context[:log_prefix]}[#{processor_class}] ") { log_process(&block) }
9
+ end
10
+
11
+ private
12
+
13
+ def log_process
14
+ start_time = Time.now
15
+ EXEL.logger.info 'Starting'
16
+
17
+ yield
18
+
19
+ EXEL.logger.info "Finished in #{duration(start_time)} seconds"
20
+ rescue Exception # rubocop:disable Lint/RescueException
21
+ EXEL.logger.info "Failed in #{duration(start_time)} seconds"
22
+ raise
23
+ end
24
+
25
+ def duration(start_time)
26
+ (Time.now - start_time).round(3)
27
+ end
28
+ end
29
+ end
30
+ end
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module EXEL
2
3
  # An {Instruction} that does nothing when executed
3
4
  class NullInstruction
@@ -1,6 +1,13 @@
1
+ # frozen_string_literal: true
1
2
  module EXEL
2
3
  # Helper methods useful to processors
4
+ # @deprecated Most functionality replaced by {EXEL::Middleware::Logging} middleware.
3
5
  module ProcessorHelper
6
+ def self.included(other)
7
+ warn "DEPRECATION WARNING: [#{other}] EXEL::ProcessorHelper will be removed. For process logging, please use "\
8
+ 'EXEL::Middleware::Logging instead'
9
+ end
10
+
4
11
  def tag(*tags)
5
12
  tags.map { |t| "[#{t}]" }.join('')
6
13
  end
@@ -1,24 +1,18 @@
1
- require_relative '../processor_helper'
1
+ # frozen_string_literal: true
2
2
 
3
3
  module EXEL
4
4
  module Processors
5
5
  # Implements the +async+ instruction by using the configured async provider to run a block asynchronously.
6
6
  class AsyncProcessor
7
- include EXEL::ProcessorHelper
8
7
  attr_reader :provider
9
8
 
10
9
  def initialize(context)
11
10
  @context = context
12
11
  @provider = EXEL.async_provider.new(context)
13
-
14
- log_prefix_with '[AsyncProcessor]'
15
12
  end
16
13
 
17
14
  def process(block)
18
- log_process do
19
- @provider.do_async(block)
20
- log_info 'call to async completed'
21
- end
15
+ @provider.do_async(block)
22
16
  end
23
17
  end
24
18
  end
@@ -1,11 +1,8 @@
1
- require_relative '../processor_helper'
2
-
1
+ # frozen_string_literal: true
3
2
  module EXEL
4
3
  module Processors
5
4
  # Implements the +run+ instruction.
6
5
  class RunProcessor
7
- include EXEL::ProcessorHelper
8
-
9
6
  # Requires +context[:job]+ to contain the name of the job to be run.
10
7
  def initialize(context)
11
8
  @context = context
@@ -13,9 +10,7 @@ module EXEL
13
10
 
14
11
  # Runs the specified job with the current context
15
12
  def process(_block = nil)
16
- log_process "running job #{@context[:job]}" do
17
- EXEL::Job.run(@context[:job], @context)
18
- end
13
+ EXEL::Job.run(@context[:job], @context)
19
14
  end
20
15
  end
21
16
  end
@@ -1,6 +1,7 @@
1
+ # frozen_string_literal: true
1
2
  require 'csv'
2
3
  require 'tempfile'
3
- require_relative '../processor_helper'
4
+ require_relative '../logging_helper'
4
5
 
5
6
  module EXEL
6
7
  module Processors
@@ -12,7 +13,7 @@ module EXEL
12
13
  # be deleted when splitting is complete
13
14
  # * +:chunk_size+ Set to specify the number of lines that each chunk should contain
14
15
  class SplitProcessor
15
- include EXEL::ProcessorHelper
16
+ include EXEL::LoggingHelper
16
17
 
17
18
  attr_accessor :file_name, :block
18
19
 
@@ -26,15 +27,11 @@ module EXEL
26
27
  @context = context
27
28
  @file = context[:resource]
28
29
  @context[:delete_resource] = true if @context[:delete_resource].nil?
29
-
30
- log_prefix_with '[SplitProcessor]'
31
30
  end
32
31
 
33
32
  def process(callback)
34
- log_process do
35
- process_file(callback)
36
- finish(callback)
37
- end
33
+ process_file(callback)
34
+ finish(callback)
38
35
  end
39
36
 
40
37
  def process_line(line, callback)
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module EXEL
2
3
  module Providers
3
4
  # The default remote provider. Doesn't actually upload and download files to and from remote storage, but rather
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module EXEL
2
3
  module Providers
3
4
  # The default remote provider. Provides async execution by running the given EXEL block in a new Thread
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require_relative './ast_node'
2
3
 
3
4
  module EXEL
data/lib/exel/value.rb CHANGED
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module EXEL
2
3
  # Contains methods to handle remote and local values. Used for {Context} serialization
3
4
  module Value
data/lib/exel/version.rb CHANGED
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module EXEL
2
- VERSION = '1.2.1'.freeze
3
+ VERSION = '1.3.0'
3
4
  end
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module EXEL
2
3
  describe ASTNode do
3
4
  let(:context) { instance_double(EXEL::Context) }
@@ -9,13 +10,33 @@ module EXEL
9
10
  TestNode = Class.new(ASTNode)
10
11
 
11
12
  describe '#start' do
12
- context 'when an JobTermination error bubbles up' do
13
- it 'ensures the process fails silently' do
14
- node = TestNode.new(instruction)
13
+ context 'when a JobTermination error bubbles up' do
14
+ let(:node) { TestNode.new(instruction) }
15
+
16
+ before do
15
17
  allow(node).to receive(:run).and_raise(EXEL::Error::JobTermination, 'Error')
18
+ end
19
+
20
+ it 'ensures the process fails silently' do
16
21
  expect(EXEL.logger).to receive(:error).with('JobTerminationError: Error')
17
22
  expect { node.start(context) }.not_to raise_error
18
23
  end
24
+
25
+ it 'logs the error by default' do
26
+ expect(EXEL.logger).to receive(:error).with('JobTerminationError: Error')
27
+ node.start(context)
28
+ end
29
+
30
+ context 'given a log instruction' do
31
+ before do
32
+ allow(node).to receive(:run).and_raise(EXEL::Error::JobTermination.new('Error', :warn))
33
+ end
34
+
35
+ it 'logs the error with the given cmd' do
36
+ expect(EXEL.logger).to receive(:warn).with('JobTerminationError: Error')
37
+ node.start(context)
38
+ end
39
+ end
19
40
  end
20
41
  end
21
42
 
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module EXEL
2
3
  describe Context do
3
4
  subject(:context) { EXEL::Context.new(key1: '1', key2: 2) }
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module EXEL
2
3
  describe DeferredContextValue do
3
4
  subject(:deferred_value) { DeferredContextValue.new }
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module EXEL
2
3
  describe Events do
3
4
  class EventTest
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module EXEL
2
3
  describe InstructionNode do
3
4
  let(:context) { {} }
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module EXEL
2
3
  describe Instruction do
3
4
  subject(:instruction) { EXEL::Instruction.new(processor_class, args) }
@@ -14,6 +15,11 @@ module EXEL
14
15
  instruction.execute(context)
15
16
  end
16
17
 
18
+ it 'invokes the middleware chain' do
19
+ expect(EXEL.middleware).to receive(:invoke).with(processor_class, context, args)
20
+ instruction.execute(context)
21
+ end
22
+
17
23
  it 'does not pass a copy of the context' do
18
24
  allow(processor_class).to receive(:new) do |context_arg|
19
25
  expect(context_arg).to be(context)
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module EXEL
2
3
  describe Job do
3
4
  describe '.define' do
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module EXEL
2
3
  describe ListenInstruction do
3
4
  subject(:instruction) { EXEL::ListenInstruction.new(:event, listener) }
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+ module EXEL
3
+ module Logging
4
+ describe LoggerWrapper do
5
+ subject(:wrapper) { LoggerWrapper.new(logger) }
6
+ let(:logger) { instance_double(Logger) }
7
+
8
+ it { is_expected.to be_a(SimpleDelegator) }
9
+
10
+ LOG_LEVELS = %i(debug info warn error fatal unknown).freeze
11
+
12
+ context 'without a Logging prefix' do
13
+ LOG_LEVELS.each do |level|
14
+ describe "##{level}" do
15
+ context 'when passed a message string' do
16
+ it 'passes the message to its wrapped logger' do
17
+ expect(logger).to receive(level).with('message')
18
+ wrapper.send(level, 'message')
19
+ end
20
+ end
21
+
22
+ context 'when passed a message block' do
23
+ it 'passes the block to its wrapped logger' do
24
+ block = proc {}
25
+ expect(logger).to receive(level).with(nil, &block)
26
+
27
+ wrapper.send(level, &block)
28
+ end
29
+
30
+ context 'and a progname' do
31
+ it 'passes the block and progname to its wrapped logger' do
32
+ block = proc {}
33
+ expect(logger).to receive(level).with('test', &block)
34
+
35
+ wrapper.send(level, 'test', &block)
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
41
+
42
+ describe '#add' do
43
+ it 'passes the message to its wrapped logger' do
44
+ expect(logger).to receive(:add).with(Logger::FATAL, 'message', 'progname')
45
+ wrapper.add(Logger::FATAL, 'message', 'progname')
46
+ end
47
+ end
48
+ end
49
+
50
+ context 'with a Logging prefix' do
51
+ before { allow(Logging).to receive(:prefix).and_return('prefix: ') }
52
+
53
+ LOG_LEVELS.each do |level|
54
+ describe "##{level}" do
55
+ context 'when passed a message string' do
56
+ it 'passes the prefixed message to its wrapped logger' do
57
+ expect(logger).to receive(level).with('prefix: message')
58
+ wrapper.send(level, 'message')
59
+ end
60
+ end
61
+
62
+ context 'when passed a message block' do
63
+ it 'passes the prefixed block to its wrapped logger' do
64
+ expect(logger).to receive(level) do |progname, &block|
65
+ expect(progname).to be_nil
66
+ expect(block.call).to eq('prefix: message')
67
+ end
68
+
69
+ wrapper.send(level) { 'message' }
70
+ end
71
+
72
+ context 'and a progname' do
73
+ it 'passes the prefixed block and progname to its wrapped logger' do
74
+ expect(logger).to receive(level) do |progname, &block|
75
+ expect(progname).to eq('test')
76
+ expect(block.call).to eq('prefix: message')
77
+ end
78
+
79
+ wrapper.send(level, 'test') { 'message' }
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
85
+
86
+ describe '#add' do
87
+ it 'passes the prefixed message to its wrapped logger' do
88
+ expect(logger).to receive(:add).with(Logger::FATAL, 'prefix: message', 'progname')
89
+ wrapper.add(Logger::FATAL, 'message', 'progname')
90
+ end
91
+ end
92
+ end
93
+ end
94
+ end
95
+ end