action_command 0.1.3 → 0.1.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -103,7 +103,7 @@
103
103
  </div>
104
104
 
105
105
  <div id="footer">
106
- Generated on Fri Mar 4 10:20:14 2016 by
106
+ Generated on Mon Mar 7 14:27:17 2016 by
107
107
  <a href="http://yardoc.org" title="Yay! A Ruby Documentation Tool" target="_parent">yard</a>
108
108
  0.8.7.6 (ruby-2.2.3).
109
109
  </div>
@@ -34,6 +34,12 @@ module ActionCommand
34
34
  def rake_context?
35
35
  return root_context == ActionCommand::CONTEXT_RAKE
36
36
  end
37
+
38
+ # @return true if this command was executed using ActionCommand.execute_api
39
+ def api_context?
40
+ return root_context == ActionCommand::CONTEXT_API
41
+ end
42
+
37
43
 
38
44
  # @return true if this command is a child of another command
39
45
  def child_context?
@@ -0,0 +1,27 @@
1
+ module ActionCommand
2
+ # Root class for action commands that can be executed by this library.
3
+ # Override execute_internal to implement one, call one of the variants
4
+ # of ActionCommand.execute_... to execute one.
5
+ class ExecutableTransaction < Executable
6
+
7
+ # starts a transaction only if we are not already within one.
8
+ def execute(result)
9
+ if ActiveRecord::Base.connection.open_transactions >= 1
10
+ super(result)
11
+ else
12
+ result.info('start_transaction')
13
+ ActiveRecord::Base.transaction do
14
+ super(result)
15
+ if result.ok?
16
+ result.info('end_transaction')
17
+ else
18
+ result.info('rollback_transaction')
19
+ raise ActiveRecord::Rollback, 'rollback transaction'
20
+ end
21
+ end
22
+ end
23
+ end
24
+
25
+ end
26
+
27
+ end
@@ -126,6 +126,11 @@ module ActionCommand
126
126
  insert_io(@input, sym, desc, opts)
127
127
  end
128
128
 
129
+ # Defines output for a command
130
+ # @param sym [Symbol] symbol identifying the parameter
131
+ # @param desc [String] description for use by internal developers, or on a rake task with
132
+ # rake your_task_name[help]
133
+ # @param opts Optional arguments.
129
134
  def output(sym, desc, opts = {})
130
135
  insert_io(@output, sym, desc, opts)
131
136
  end
@@ -0,0 +1,105 @@
1
+
2
+ # Unnecessary comment for rubocop
3
+ module ActionCommand
4
+
5
+ # A single entry in the action command log
6
+ class LogMessage
7
+
8
+ attr_accessor :sequence, :cmd, :kind, :msg, :key
9
+
10
+ # Create a new log message
11
+ def populate(line, msg)
12
+ @line = line
13
+ @sequence = msg['sequence']
14
+ @depth = msg['depth']
15
+ @cmd = msg['cmd']
16
+ @kind = msg['kind']
17
+ @msg = msg['msg']
18
+ @key = msg['key']
19
+ end
20
+
21
+ # @return the number of parents the current command has
22
+ def depth
23
+ return @depth
24
+ end
25
+
26
+ # @return true if this command is the root command
27
+ def root?
28
+ return @depth == 0
29
+ end
30
+
31
+ # @return the line that was used to create this message.
32
+ def line
33
+ return @line
34
+ end
35
+
36
+ def key?(key)
37
+ return @key == key
38
+ end
39
+
40
+ # @return true if the kinds equal (tolerant of string/symbol mismatch)
41
+ def kind?(kind)
42
+ kind = kind.to_s
43
+ return @kind == kind
44
+ end
45
+
46
+ # @return true if the cmds equal (tolerant of being passed a class)
47
+ def command?(cmd)
48
+ cmd = cmd.name if cmd.is_a? Class
49
+ return @cmd == cmd
50
+ end
51
+
52
+ # @ return true if msgs equal
53
+ def match_message?(msg)
54
+ return @msg == msg unless msg.is_a? Hash
55
+ msg.each do |k, v|
56
+ k = k.to_s if k.is_a? Symbol
57
+ return false unless @msg.key?(k)
58
+ return false unless @msg[k] == v
59
+ end
60
+ return true
61
+ end
62
+ end
63
+
64
+ # reads from a stream containing log statements, and returns
65
+ # LogMessage entries for them.
66
+ class LogParser
67
+
68
+ # Create a new log parser for an IO subclass
69
+ def initialize(stream, sequence = nil)
70
+ @stream = stream
71
+ @sequence = sequence
72
+ end
73
+
74
+ # Check if we have reached the end of the stream.
75
+ def eof?
76
+ return @stream.eof?
77
+ end
78
+
79
+ # Populates a message from the next line in the
80
+ def next(msg)
81
+ # be tolerant of the fact that there might be other
82
+ # stuff in the log file.
83
+ next_line do |input, line|
84
+ if input.key?('sequence')
85
+ msg.populate(line, input) unless @sequence && @sequence != input['sequence']
86
+ return true
87
+ end
88
+ end
89
+ return false
90
+ end
91
+
92
+ private
93
+
94
+ def next_line
95
+ until @stream.eof?
96
+ line = @stream.readline
97
+ line.scan(/--\s+:\s+({.*})/) do |item|
98
+ input = JSON.parse(item[0])
99
+ yield input, line
100
+ end
101
+ end
102
+ end
103
+ end
104
+
105
+ end
@@ -0,0 +1,114 @@
1
+
2
+ module ActionCommand
3
+
4
+ # Action that parses a log and pretty prints the log,
5
+ # optionally filtering on a particular command sequence.
6
+ class PrettyPrintLogAction < Executable
7
+ attr_accessor :source, :dest, :sequence
8
+
9
+ # specifies the input/output for this command
10
+ def self.describe_io
11
+ return ActionCommand.describe_io(self, 'Command that does some logging') do |io|
12
+ io.input(:source, 'Stream to read')
13
+ io.input(:sequence, 'sequence to filter on', OPTIONAL)
14
+ io.input(:dest, 'Optional output stream, defaults to STDOUT', OPTIONAL)
15
+ end
16
+ end
17
+
18
+ # return the destination stream, default to STDOUT
19
+ def dest
20
+ @dest = STDOUT unless @dest
21
+ return @dest
22
+ end
23
+
24
+ protected
25
+
26
+
27
+ # Say hello to the specified person.
28
+ def execute_internal(_result)
29
+ item = LogMessage.new
30
+ parser = LogParser.new(@source, @sequence)
31
+ sequences = {}
32
+ # keep track of sequences, and when you complete one, then print out the
33
+ # entire thing at once.
34
+ while parser.next(item)
35
+ if item.kind?(ActionCommand::LOG_KIND_COMMAND_OUTPUT) && item.root?
36
+ process_output(sequences, item)
37
+ else
38
+ process_other(sequences, item)
39
+ end
40
+ item = LogMessage.new
41
+ end
42
+
43
+ # print out any incomplete sequences
44
+ print_sequences(sequences)
45
+ end
46
+
47
+ def process_other(sequences, item)
48
+ sequences[item.sequence] = [] unless sequences.key?(item.sequence)
49
+ sequences[item.sequence] << item
50
+ end
51
+
52
+ def process_output(sequences, item)
53
+ seq = sequences[item.sequence]
54
+ sequences.delete(item.sequence)
55
+ seq << item
56
+ print_sequence(seq)
57
+ end
58
+
59
+ def print_sequences(sequences)
60
+ sequences.each do |_k, v|
61
+ print_sequence(v)
62
+ end
63
+ end
64
+
65
+ def print_sequence(sequence)
66
+ sequence.each_with_index do |item, _i|
67
+ print_sequence_item(item)
68
+ end
69
+ end
70
+
71
+ def print_sequence_item(item)
72
+ # indent
73
+ if item.kind?(ActionCommand::LOG_KIND_COMMAND_INPUT)
74
+ print_cmd_input(item)
75
+ elsif item.kind?(ActionCommand::LOG_KIND_COMMAND_OUTPUT)
76
+ print_cmd_output(item)
77
+ else
78
+ print_msg(item.depth + 1, item)
79
+ end
80
+ end
81
+
82
+ def print_cmd_output(item)
83
+ println(item.depth, 'output:')
84
+ print_msg(item.depth + 1, item)
85
+ end
86
+
87
+ def print_cmd_input(item)
88
+ result = item.cmd
89
+ result << " (#{item.sequence})" if item.root?
90
+ println(item.depth, result)
91
+ println(item.depth + 1, 'input:')
92
+ print_msg(item.depth + 2, item)
93
+ end
94
+
95
+ def print_msg(depth, item)
96
+ if item.msg.is_a? String
97
+ println(depth, item.msg)
98
+ return
99
+ end
100
+
101
+ item.msg.each do |k, v|
102
+ println(depth, "#{k}: #{v}")
103
+ end
104
+ end
105
+
106
+ def println(depth, line)
107
+ padding = ''.rjust(depth * 2)
108
+ dest.puts("#{padding}#{line}")
109
+ end
110
+
111
+
112
+ end
113
+
114
+ end
@@ -1,29 +1,79 @@
1
1
 
2
2
  module ActionCommand
3
+
4
+
3
5
  # The result of one or more commands being executed.
4
- class Result
6
+ class Result # rubocop:disable Metrics/ClassLength
5
7
  # By default, a command is ok?
6
- def initialize(logger)
7
- @ok = true
8
+ def initialize
9
+ @result_code = RESULT_CODE_OK
8
10
  @values = [{}]
11
+ @logger = nil
12
+ end
13
+
14
+ # set the logger for this result
15
+ def logger=(logger)
16
+ return unless logger
17
+ @sequence = SecureRandom.hex
18
+ @stack = []
9
19
  @logger = logger
10
20
  end
21
+
22
+ # @return true if logging is enabled.
23
+ def logging?
24
+ return !@logger.nil?
25
+ end
26
+
27
+ # display an debugging message to the logger, if there is one.
28
+ # @yield return a message or hash
29
+ def debug(msg = nil)
30
+ if @logger
31
+ json = build_log(msg || yield, ActionCommand::LOG_KIND_DEBUG)
32
+ @logger.info(json)
33
+ end
34
+ end
35
+
36
+ # display an informational message to the logger, if there is one.
37
+ # @yield return a message or hash
38
+ def info(msg = nil)
39
+ if @logger
40
+ json = build_log(msg || yield, ActionCommand::LOG_KIND_INFO)
41
+ @logger.info(json)
42
+ end
43
+ end
44
+
45
+ # display an error message to the logger, if there is one.
46
+ def error(msg)
47
+ @logger.error(build_log(msg, ActionCommand::LOG_KIND_ERROR)) if @logger
48
+ end
11
49
 
12
50
  # Call this if your command implementation fails. Sets
13
51
  # ok? to false on the result.
14
52
  # @param msg [String] message describing the failure.
15
53
  def failed(msg)
16
- @ok = false
54
+ @result_code = RESULT_CODE_FAILED
55
+ error(msg)
56
+ end
57
+
58
+ # Call this if your command implementation fails. Sets
59
+ # ok? to false on the result.
60
+ # @param msg [String] message describing the failure.
61
+ # @param result_code [Integer]
62
+ def failed_with_code(msg, result_code)
63
+ @result_code = result_code
17
64
  error(msg)
18
65
  end
19
66
 
20
67
  # @return [Boolean] true, up until failed has been called at least once.
21
68
  def ok?
22
- return @ok
69
+ return @result_code == RESULT_CODE_OK
23
70
  end
24
71
 
72
+ # @return [Integer] the current result code
73
+ attr_reader :result_code
74
+
25
75
  # adds results under the subkey until pop is called
26
- def push(key)
76
+ def push(key, cmd)
27
77
  return unless key
28
78
  old_cur = current
29
79
  if old_cur.key?(key)
@@ -32,12 +82,14 @@ module ActionCommand
32
82
  @values << {}
33
83
  old_cur[key] = @values.last
34
84
  end
85
+ @stack << { key: key, cmd: cmd } if @logger
35
86
  end
36
87
 
37
88
  # removes the current set of results from the stack.
38
89
  def pop(key)
39
90
  return unless key
40
91
  @values.pop
92
+ @stack.pop if @logger
41
93
  end
42
94
 
43
95
  # returns the current hash of values we are operating on.
@@ -50,6 +102,11 @@ module ActionCommand
50
102
  current[key] = val
51
103
  end
52
104
 
105
+ # return the unique sequence id for the commands under this result
106
+ def sequence
107
+ return @sequence
108
+ end
109
+
53
110
  # determine if a key exists in the result.
54
111
  def key?(key)
55
112
  return current.key?(key)
@@ -59,18 +116,51 @@ module ActionCommand
59
116
  def [](key)
60
117
  return current[key]
61
118
  end
119
+
120
+ # Used internally to log the input parameters to a command
121
+ def log_input(params)
122
+ return unless @logger
123
+ output = params.reject { |k, _v| internal_key?(k) }
124
+ log_info_hash(output, ActionCommand::LOG_KIND_COMMAND_INPUT)
125
+ end
62
126
 
63
- # display an informational message to the logger, if there is one.
64
- def info(msg)
65
- @logger.info(msg) if @logger
127
+ # Used internally to log the output parameters for a command.
128
+ def log_output
129
+ return unless @logger
130
+ # only log the first level parameters, subcommands will log
131
+ # their own output.
132
+ output = current.reject { |k, v| v.is_a?(Hash) || internal_key?(k) }
133
+ log_info_hash(output, ActionCommand::LOG_KIND_COMMAND_OUTPUT)
66
134
  end
67
-
68
- protected
69
-
70
- # display an error message to the logger, if there is one.
71
- def error(msg)
72
- @logger.error(msg) if @logger
135
+
136
+ # Used internally to establish the class of the root command
137
+ def root_command(cls)
138
+ @stack << { key: nil, cmd: cls } if @logger
139
+ end
140
+
141
+
142
+ private
143
+
144
+ def internal_key?(k)
145
+ return k == :logger || k == :test || k == :parent
146
+ end
147
+
148
+ def log_info_hash(params, kind)
149
+ return unless @logger
150
+ @logger.info(build_log(params, kind))
151
+ end
152
+
153
+ def build_log(msg, kind)
154
+ cur = @stack.last
155
+ out = {
156
+ sequence: @sequence,
157
+ cmd: cur[:cmd].name,
158
+ depth: @stack.length - 1
159
+ }
160
+ out[:key] = cur[:key] if cur[:key]
161
+ out[:kind] = kind
162
+ out[:msg] = msg if msg
163
+ return JSON.generate(out)
73
164
  end
74
-
75
165
  end
76
166
  end
@@ -15,11 +15,6 @@ module ActionCommand
15
15
  return cls.find(item) if item.is_a? Integer
16
16
  return yield(item)
17
17
  end
18
-
19
-
20
-
21
-
22
-
23
18
  end
24
19
 
25
20
  end
@@ -1,4 +1,4 @@
1
1
  module ActionCommand
2
2
  # Version of this Gem
3
- VERSION = '0.1.3'.freeze
3
+ VERSION = '0.1.4'.freeze
4
4
  end
@@ -3,6 +3,8 @@ require 'action_command/result'
3
3
  require 'action_command/input_output'
4
4
  require 'action_command/executable'
5
5
  require 'action_command/utils'
6
+ require 'action_command/log_parser'
7
+ require 'action_command/pretty_print_log_action'
6
8
 
7
9
  # To use action command, create subclasses of ActionCommand::Executable
8
10
  # and run them using the ActionCommand.execute_... variants.
@@ -18,6 +20,32 @@ module ActionCommand
18
20
  # Used as root parent of command if we are executing it from rails (a controller, etc)
19
21
  CONTEXT_RAILS = :rails
20
22
 
23
+ # Used as a root element when the command is executed from an API context
24
+ CONTEXT_API = :api
25
+
26
+ # Used if a result has had no failures
27
+ RESULT_CODE_OK = 0
28
+
29
+ # Used as a generic result code for failure, if you do not provide
30
+ # a more specific one through {ActionCommand::Result#failed_with_code}
31
+ RESULT_CODE_FAILED = 1
32
+
33
+ # log entry for the input to a commmand
34
+ LOG_KIND_COMMAND_INPUT = :command_input
35
+
36
+ # log entry for the output from a command
37
+ LOG_KIND_COMMAND_OUTPUT = :command_output
38
+
39
+ # info message from within a command
40
+ LOG_KIND_INFO = :info
41
+
42
+ # debug message from within a command
43
+ LOG_KIND_DEBUG = :debug
44
+
45
+ # error message from within a command
46
+ LOG_KIND_ERROR = :error
47
+
48
+
21
49
  # Used to create an optional parameter in describe_io
22
50
  OPTIONAL = { optional: true }.freeze
23
51
 
@@ -32,7 +60,7 @@ module ActionCommand
32
60
 
33
61
  # @return a new, valid, empty result.
34
62
  def self.create_result
35
- return ActionCommand::Result.new(@@logger)
63
+ return ActionCommand::Result.new
36
64
  end
37
65
 
38
66
  # Execute a command at the root level of a testing context
@@ -83,6 +111,15 @@ module ActionCommand
83
111
  return ActionCommand.create_and_execute(cls, params, CONTEXT_RAILS, result)
84
112
  end
85
113
 
114
+ # Execute a command at the root level of an api context
115
+ # @param cls [ActionCommand::Executable] The class of an Executable subclass
116
+ # @param params [Hash] parameters used by the command.
117
+ # @return [ActionCommand::Result]
118
+ def self.execute_api(cls, params = {})
119
+ result = create_result
120
+ return ActionCommand.create_and_execute(cls, params, CONTEXT_API, result)
121
+ end
122
+
86
123
  # Execute a child command, placing its results under the specified subkey
87
124
  # @param parent [ActionCommand::Executable] An instance of the parent command
88
125
  # @param cls [ActionCommand::Executable] The class of an Executable subclass
@@ -92,7 +129,7 @@ module ActionCommand
92
129
  # @param params [Hash] parameters used by the command.
93
130
  # @return [ActionCommand::Result]
94
131
  def self.execute_child(parent, cls, result, result_key, params = {})
95
- result.push(result_key)
132
+ result.push(result_key, cls)
96
133
  ActionCommand.create_and_execute(cls, params, parent, result)
97
134
  result.pop(result_key)
98
135
  return result
@@ -115,14 +152,24 @@ module ActionCommand
115
152
 
116
153
  # Used internally, not for general purpose use.
117
154
  def self.create_and_execute(cls, params, parent, result)
155
+ check_params(cls, params)
156
+ params[:parent] = parent
157
+ result.logger = params[:logger]
158
+ result.logger = @@logger unless params[:logger]
159
+ result.root_command(cls) if parent.is_a? Symbol
160
+ action = cls.new(params)
161
+
162
+ result.log_input(params)
163
+ action.execute(result)
164
+ result.log_output
165
+ return result
166
+ end
167
+
168
+ def self.check_params(cls, params)
118
169
  raise ArgumentError, 'Expected params to be a Hash' unless params.is_a? Hash
119
170
 
120
171
  unless cls.is_a?(Class) && cls.ancestors.include?(ActionCommand::Executable)
121
172
  raise ArgumentError, 'Expected an ActionCommand::Executable as class'
122
173
  end
123
-
124
- params[:parent] = parent
125
- action = cls.new(params)
126
- return action.execute(result)
127
174
  end
128
175
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: action_command
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.3
4
+ version: 0.1.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Chris Jones
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2016-03-04 00:00:00.000000000 Z
11
+ date: 2016-03-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -24,6 +24,20 @@ dependencies:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
26
  version: '1.11'
27
+ - !ruby/object:Gem::Dependency
28
+ name: byebug
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
27
41
  - !ruby/object:Gem::Dependency
28
42
  name: rake
29
43
  requirement: !ruby/object:Gem::Requirement
@@ -192,6 +206,34 @@ dependencies:
192
206
  - - ">="
193
207
  - !ruby/object:Gem::Version
194
208
  version: '0'
209
+ - !ruby/object:Gem::Dependency
210
+ name: activerecord
211
+ requirement: !ruby/object:Gem::Requirement
212
+ requirements:
213
+ - - "~>"
214
+ - !ruby/object:Gem::Version
215
+ version: 4.0.0
216
+ type: :development
217
+ prerelease: false
218
+ version_requirements: !ruby/object:Gem::Requirement
219
+ requirements:
220
+ - - "~>"
221
+ - !ruby/object:Gem::Version
222
+ version: 4.0.0
223
+ - !ruby/object:Gem::Dependency
224
+ name: sqlite3
225
+ requirement: !ruby/object:Gem::Requirement
226
+ requirements:
227
+ - - ">="
228
+ - !ruby/object:Gem::Version
229
+ version: '0'
230
+ type: :development
231
+ prerelease: false
232
+ version_requirements: !ruby/object:Gem::Requirement
233
+ requirements:
234
+ - - ">="
235
+ - !ruby/object:Gem::Version
236
+ version: '0'
195
237
  description: Simple implementation of command pattern focused on reuse in multiple
196
238
  contexts
197
239
  email:
@@ -200,6 +242,7 @@ executables: []
200
242
  extensions: []
201
243
  extra_rdoc_files: []
202
244
  files:
245
+ - ".byebug_history"
203
246
  - ".codeclimate.yml"
204
247
  - ".gitignore"
205
248
  - ".rspec"
@@ -236,7 +279,10 @@ files:
236
279
  - doc/top-level-namespace.html
237
280
  - lib/action_command.rb
238
281
  - lib/action_command/executable.rb
282
+ - lib/action_command/executable_transaction.rb
239
283
  - lib/action_command/input_output.rb
284
+ - lib/action_command/log_parser.rb
285
+ - lib/action_command/pretty_print_log_action.rb
240
286
  - lib/action_command/result.rb
241
287
  - lib/action_command/utils.rb
242
288
  - lib/action_command/version.rb