nocode 0.0.4 → 0.0.8

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 884ab5737629463437ef0991555e1ea30faa8db0cd6d8c79da712942aa69011b
4
- data.tar.gz: d8562fdf11b2a204e9ced685b434d6959bafe2dca605dd3795355aeed1ed5238
3
+ metadata.gz: f42cd4574b36e4098fbb2c3de7263d8530ee2c935ec1d86da30950c51f45998e
4
+ data.tar.gz: a1259cd1075a946a056db27b2a3da5c7acebee2002c0f1e01b2f6a5fef203fa9
5
5
  SHA512:
6
- metadata.gz: 677aa00aba3a998a4cc616002420c9366ac945fabdfc336d948b5158c24eae6c9bfc1b8a7ee658718c5c55af4148c4f83656fddf8b45995002471ef7224870da
7
- data.tar.gz: e4de53676eaebb1d9bd3738dbbbb6df777ce0fdf1674e9aa370176c2eca9d42d4672d6edd682a313b943f24d149566491d1d7322d9c30ed0cc352db7730e0f3f
6
+ metadata.gz: 7be2f97365a7d2494e956362074d47c07cf39a741af34299b2bd30e70c2a74f8bf7566a24de816b4af692deede762c4e03ba9b2e1f8ddd81cf161a263e6e6f1b
7
+ data.tar.gz: '0099f90f5b0a569f2fe95b93d16aa5e737f957830bcfcb84d598f362c21edec9c759e113f0d01049a5ecfa05bb17d5e70ec49436b5c558a66f0e194d8c32ff28'
data/CHANGELOG.md CHANGED
@@ -1,7 +1,28 @@
1
+
2
+ #### 0.0.8 - February 14th, 2022
3
+
4
+ * Add `map` step to iterate and collect a dataset result.
5
+ * Add `record/map` step to iterate over a hash.
6
+ * Add `io/list` to populate a register with the contents of a directory.
7
+ * Add `io/delete` to delete a file specified in the path option.
8
+ #### 0.0.7 - February 13th, 2022
9
+
10
+ * Move shared logging logic to context (until a first class log writer emerges).
11
+ * Provide type_prefix option for class_registry
12
+
13
+ #### 0.0.6 - February 13th, 2022
14
+
15
+ * Expose registers to main YAML configuration.
16
+ * Add `each` step to serve as an example of an iterator.
17
+
18
+ #### 0.0.5 - February 13th, 2022
19
+
20
+ * Added initial `dataset` steps.
21
+
1
22
  #### 0.0.4 - February 13th, 2022
2
23
 
3
24
  * Increase class documentation
4
- * Add YAML serialization/deserializationß
25
+ * Add YAML serialization/deserialization
5
26
 
6
27
  #### 0.0.3 - February 12th, 2022
7
28
 
data/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  #### Execute Ruby code through YAML
4
4
 
5
- [![Gem Version](https://badge.fury.io/rb/nocode.svg)](https://badge.fury.io/rb/nocode) [![Ruby Gem CI](https://github.com/mattruggio/nocode/actions/workflows/rubygem.yml/badge.svg)](https://github.com/mattruggio/nocode/actions/workflows/rubygem.yml)
5
+ [![Gem Version](https://badge.fury.io/rb/nocode.svg)](https://badge.fury.io/rb/nocode) [![Ruby Gem CI](https://github.com/mattruggio/nocode/actions/workflows/rubygem.yml/badge.svg)](https://github.com/mattruggio/nocode/actions/workflows/rubygem.yml) [![Maintainability](https://api.codeclimate.com/v1/badges/66479dae44129c87dc88/maintainability)](https://codeclimate.com/github/mattruggio/nocode/maintainability) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
6
6
 
7
7
  This is a proof of concept showing how a YAML interface could be draped over arbitrary Ruby code. The YAML contains a series of steps with each step mapping to a specific Ruby class. The Ruby classes just have one responsibility: to implement #perform.
8
8
 
data/exe/nocode CHANGED
@@ -4,6 +4,8 @@
4
4
  require 'bundler/setup'
5
5
  require 'nocode'
6
6
 
7
+ require 'pry'
8
+
7
9
  path = ARGV[0]
8
10
 
9
11
  if path.to_s.empty?
@@ -4,7 +4,12 @@ module Nocode
4
4
  # Describes the environment for each running step. An instance is initialized when a job
5
5
  # kicks off and then is passed from step to step.
6
6
  class Context
7
- attr_reader :io, :parameters, :registers
7
+ PARAMETERS_KEY = 'parameters'
8
+ REGISTERS_KEY = 'registers'
9
+
10
+ attr_reader :io,
11
+ :parameters,
12
+ :registers
8
13
 
9
14
  def initialize(io: $stdout, parameters: {}, registers: {})
10
15
  @io = io || $stdout
@@ -24,9 +29,17 @@ module Nocode
24
29
 
25
30
  def to_h
26
31
  {
27
- 'registers' => registers,
28
- 'parameters' => parameters
32
+ REGISTERS_KEY => registers,
33
+ PARAMETERS_KEY => parameters
29
34
  }
30
35
  end
36
+
37
+ def log_line
38
+ log('-' * 50)
39
+ end
40
+
41
+ def log(msg)
42
+ io.puts(msg)
43
+ end
31
44
  end
32
45
  end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'context'
4
+ require_relative 'steps_executor'
5
+
6
+ module Nocode
7
+ # Manages the lifecycle and executes a job.
8
+ class JobExecutor
9
+ PARAMETERS_KEY = 'parameters'
10
+ REGISTERS_KEY = 'registers'
11
+ STEPS_KEY = 'steps'
12
+
13
+ attr_reader :yaml, :io
14
+
15
+ def initialize(yaml, io: $stdout)
16
+ @yaml = yaml.respond_to?(:read) ? yaml.read : yaml
17
+ @yaml = YAML.safe_load(@yaml) || {}
18
+ @io = io
19
+
20
+ freeze
21
+ end
22
+
23
+ def execute
24
+ steps = yaml[STEPS_KEY] || []
25
+ parameters = yaml[PARAMETERS_KEY] || {}
26
+ registers = yaml[REGISTERS_KEY] || {}
27
+
28
+ context = Context.new(
29
+ io: io,
30
+ parameters: parameters,
31
+ registers: registers
32
+ )
33
+
34
+ log_title(context)
35
+
36
+ StepsExecutor.new(context: context, steps: steps).execute
37
+
38
+ context.log("Ended: #{DateTime.now}")
39
+ context.log_line
40
+
41
+ context
42
+ end
43
+
44
+ private
45
+
46
+ def log_title(context)
47
+ context.log_line
48
+
49
+ context.log('Nocode Execution')
50
+ context.log("Started: #{DateTime.now}")
51
+
52
+ context.log_line
53
+ end
54
+ end
55
+ end
@@ -10,8 +10,8 @@ module Nocode
10
10
  class StepRegistry < Util::ClassRegistry
11
11
  include Singleton
12
12
 
13
- PREFIX = 'Nocode::Steps::'
14
- DIR = File.join(__dir__, 'steps')
13
+ CLASS_PREFIX = 'Nocode::Steps::'
14
+ DIR = File.join(__dir__, 'steps')
15
15
 
16
16
  class << self
17
17
  extend Forwardable
@@ -27,7 +27,7 @@ module Nocode
27
27
  files_loaded = Util::ClassLoader.new(DIR).load!
28
28
 
29
29
  # Class the parent to load up the registry with the files we found.
30
- load(files_loaded, PREFIX)
30
+ load(files_loaded, class_prefix: CLASS_PREFIX)
31
31
  end
32
32
  end
33
33
 
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nocode
4
+ module Steps
5
+ module Dataset
6
+ # Add entries specified in the options to the end of the specified register's
7
+ # existing entries.
8
+ class Append < Step
9
+ option :entries, :register
10
+
11
+ def perform
12
+ registers[register_option] = array(registers[register_option])
13
+
14
+ array(entries_option).each do |entry|
15
+ registers[register_option].append(entry)
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nocode
4
+ module Steps
5
+ module Dataset
6
+ # Combine all specified from_registers into one dataset and place in the specified
7
+ # to_register. If anything currently exists in the to_register then it will be coerced
8
+ # to an array and prepended to the beginning.
9
+ class Coalesce < Step
10
+ option :from_registers, :to_register
11
+
12
+ def perform
13
+ registers[to_register_option] = array(registers[to_register_option])
14
+
15
+ array(from_registers_option).each do |from_register|
16
+ registers[to_register_option] += array(registers[from_register])
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nocode
4
+ module Steps
5
+ module Dataset
6
+ # Insert the entries to the at_index of the specified register.
7
+ # If at_index is nil then they will appended to the end.
8
+ class Insert < Step
9
+ option :at_index, :entries, :register
10
+
11
+ def perform
12
+ registers[register_option] = array(registers[register_option])
13
+
14
+ registers[register_option].insert(at_index, *entries)
15
+ end
16
+
17
+ private
18
+
19
+ def entries
20
+ array(entries_option)
21
+ end
22
+
23
+ def at_index
24
+ at_index_option.nil? ? -1 : at_index_option.to_i
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nocode
4
+ module Steps
5
+ module Dataset
6
+ # Add entries specified in the options to the beginning of the specified register's
7
+ # existing entries.
8
+ class Prepend < Step
9
+ option :entries, :register
10
+
11
+ def perform
12
+ registers[register_option] = array(registers[register_option])
13
+
14
+ array(entries_option).reverse_each do |entry|
15
+ registers[register_option].prepend(entry)
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nocode
4
+ module Steps
5
+ module Dataset
6
+ # Slice a dataset and keep on the entries between start_index and end_index, inclusively.
7
+ # If start_index is not provided then it defaults to 0.
8
+ # If end_index is not provided then it defaults to the end of the dataset.
9
+ class Range < Step
10
+ option :end_index,
11
+ :register,
12
+ :start_index
13
+
14
+ def perform
15
+ registers[register_option] = array(registers[register_option])
16
+
17
+ registers[register_option] = registers[register_option][start_index..end_index]
18
+ end
19
+
20
+ private
21
+
22
+ def start_index
23
+ start_index_option.nil? ? 0 : start_index_option.to_i
24
+ end
25
+
26
+ def end_index
27
+ end_index_option.nil? ? -1 : end_index_option.to_i
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nocode
4
+ module Steps
5
+ # Iterate over a register. Each iteration will store the current element and index in
6
+ # special registers called: _element and _index. You can prefix these registers by setting
7
+ # the element_register_prefix option.
8
+ class Each < Step
9
+ option :element_register_prefix,
10
+ :register,
11
+ :steps
12
+
13
+ skip_options_evaluation!
14
+
15
+ def perform
16
+ entries.each_with_index do |entry, index|
17
+ registers[element_key] = entry
18
+ registers[index_key] = index
19
+
20
+ execute_steps
21
+ end
22
+ end
23
+
24
+ private
25
+
26
+ def execute_steps
27
+ StepsExecutor.new(context: context, steps: steps).execute
28
+ end
29
+
30
+ def entries
31
+ array(registers[register_option])
32
+ end
33
+
34
+ def steps
35
+ array(steps_option)
36
+ end
37
+
38
+ def element_key
39
+ "#{element_register_prefix_option}_element"
40
+ end
41
+
42
+ def index_key
43
+ "#{element_register_prefix_option}_index"
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nocode
4
+ module Steps
5
+ module Io
6
+ # Delete the specified path. Does nothing if the file does not exist.
7
+ class Delete < Step
8
+ option :path
9
+
10
+ def perform
11
+ return if path.to_s.empty?
12
+
13
+ FileUtils.rm_f(path) if File.exist?(path)
14
+ end
15
+
16
+ private
17
+
18
+ def path
19
+ File.join(*array(path_option))
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nocode
4
+ module Steps
5
+ module Io
6
+ # List all files in the path option. Wildcards can be used.
7
+ #
8
+ # Mechanic: https://ruby-doc.org/core-2.5.0/Dir.html#method-c-glob
9
+ class List < Step
10
+ option :path,
11
+ :register
12
+
13
+ def perform
14
+ registers[register_option] = Dir[path].reject { |p| File.directory?(p) }
15
+ end
16
+
17
+ private
18
+
19
+ def path
20
+ File.join(*array(path_option))
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nocode
4
+ module Steps
5
+ # Iterate over a register. Each iteration will store the current element and index in
6
+ # special registers called: _element and _index. You can prefix these registers by setting
7
+ # the element_register_prefix option.
8
+ #
9
+ # The main difference between this and 'each' is that this will collect the iterator
10
+ # element register and set the register to this new collection.
11
+ class Map < Step
12
+ option :element_register_prefix,
13
+ :register,
14
+ :steps
15
+
16
+ skip_options_evaluation!
17
+
18
+ def perform
19
+ registers[register_option] = entries.map.with_index do |entry, index|
20
+ registers[element_key] = entry
21
+ registers[index_key] = index
22
+
23
+ execute_steps
24
+
25
+ registers[element_key]
26
+ end
27
+ end
28
+
29
+ private
30
+
31
+ def execute_steps
32
+ StepsExecutor.new(context: context, steps: steps).execute
33
+ end
34
+
35
+ def entries
36
+ array(registers[register_option])
37
+ end
38
+
39
+ def steps
40
+ array(steps_option)
41
+ end
42
+
43
+ def element_key
44
+ "#{element_register_prefix_option}_element"
45
+ end
46
+
47
+ def index_key
48
+ "#{element_register_prefix_option}_index"
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nocode
4
+ module Steps
5
+ module Record
6
+ # Create a new hash from an existing hash mapping each key as configured by the
7
+ # key_mappings option. The key_mappings option should be in the form of:
8
+ # new_key => old_key
9
+ class Map < Step
10
+ option :key_mappings, :register
11
+
12
+ def perform
13
+ input = registers[register_option] || {}
14
+ output = {}
15
+
16
+ (key_mappings_option || {}).each do |to, from|
17
+ output[to.to_s] = input[from.to_s]
18
+ end
19
+
20
+ registers[register_option] = output
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -27,14 +27,8 @@ module Nocode
27
27
 
28
28
  if object.is_a?(Array)
29
29
  csv << object
30
-
31
- true
32
30
  elsif object.respond_to?(:values)
33
31
  csv << object.values
34
-
35
- true
36
- else
37
- false
38
32
  end
39
33
  end
40
34
  end
@@ -3,6 +3,8 @@
3
3
  module Nocode
4
4
  module Steps
5
5
  # Sleep for an arbitrary number of seconds.
6
+ #
7
+ # Mechanic: https://apidock.com/ruby/Kernel/sleep
6
8
  class Sleep < Step
7
9
  option :seconds
8
10
 
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'step_registry'
4
+
5
+ module Nocode
6
+ # Class that knows how to execute a series of steps given a context.
7
+ class StepsExecutor
8
+ extend Forwardable
9
+
10
+ NAME_KEY = 'name'
11
+ OPTIONS_KEY = 'options'
12
+ TYPE_KEY = 'type'
13
+
14
+ attr_reader :context, :steps
15
+
16
+ def_delegators :context, :log_line, :log
17
+
18
+ def initialize(context:, steps:)
19
+ @context = context
20
+ @steps = steps
21
+
22
+ freeze
23
+ end
24
+
25
+ def execute
26
+ steps.each do |step|
27
+ step_instance = make_step(step)
28
+
29
+ execute_step(step_instance)
30
+ end
31
+ end
32
+
33
+ private
34
+
35
+ def make_step(step)
36
+ evaluated_step = Util::ObjectTemplate.new(step).evaluate(context.to_h)
37
+ type = evaluated_step[TYPE_KEY].to_s
38
+ name = evaluated_step[NAME_KEY].to_s
39
+ step_class = StepRegistry.constant!(type)
40
+
41
+ options =
42
+ if step_class.skip_options_evaluation?
43
+ step[OPTIONS_KEY]
44
+ else
45
+ evaluated_step[OPTIONS_KEY]
46
+ end
47
+
48
+ step_class.new(
49
+ options: Util::Dictionary.new(options),
50
+ context: context,
51
+ name: name,
52
+ type: type
53
+ )
54
+ end
55
+
56
+ def execute_step(step)
57
+ log(step.name) unless step.name.empty?
58
+ log("Step: #{step.type}")
59
+ log("Class: #{step.class}")
60
+
61
+ time_in_seconds = Benchmark.measure { step.perform }.real
62
+
63
+ log("Completed in #{time_in_seconds.round(3)} second(s)")
64
+
65
+ log_line
66
+ end
67
+ end
68
+ end
@@ -20,13 +20,13 @@ module Nocode
20
20
  freeze
21
21
  end
22
22
 
23
- def load(types, prefix = '')
23
+ def load(types, class_prefix: '', type_prefix: '')
24
24
  types.each do |type|
25
25
  pascal_cased = type.split(File::SEPARATOR).map do |part|
26
26
  part.split('_').collect(&:capitalize).join
27
27
  end.join('::')
28
28
 
29
- register(type, "#{prefix}#{pascal_cased}")
29
+ register("#{type_prefix}#{type}", "#{class_prefix}#{pascal_cased}")
30
30
  end
31
31
 
32
32
  self
@@ -23,6 +23,14 @@ module Nocode
23
23
 
24
24
  # Class-level DSL Methods
25
25
  module ClassMethods
26
+ def skip_options_evaluation?
27
+ @skip_options_evaluation || false
28
+ end
29
+
30
+ def skip_options_evaluation!
31
+ @skip_options_evaluation = true
32
+ end
33
+
26
34
  def option(*values)
27
35
  values.each { |v| options << v.to_s }
28
36
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Nocode
4
- VERSION = '0.0.4'
4
+ VERSION = '0.0.8'
5
5
  end
data/lib/nocode.rb CHANGED
@@ -12,14 +12,14 @@ require 'yaml'
12
12
  require 'nocode/util'
13
13
 
14
14
  # Core
15
- require 'nocode/executor'
15
+ require 'nocode/job_executor'
16
16
 
17
17
  # Establish main top-level namespace
18
18
  module Nocode
19
19
  # Default consumer entrypoint into the library.
20
20
  class << self
21
21
  def execute(yaml, io: $stdout)
22
- Executor.new(yaml, io: io).execute
22
+ JobExecutor.new(yaml, io: io).execute
23
23
  end
24
24
  end
25
25
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: nocode
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.4
4
+ version: 0.0.8
5
5
  platform: ruby
6
6
  authors:
7
7
  - Matthew Ruggio
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2022-02-13 00:00:00.000000000 Z
11
+ date: 2022-02-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler-audit
@@ -175,22 +175,33 @@ files:
175
175
  - exe/nocode
176
176
  - lib/nocode.rb
177
177
  - lib/nocode/context.rb
178
- - lib/nocode/executor.rb
178
+ - lib/nocode/job_executor.rb
179
179
  - lib/nocode/step.rb
180
180
  - lib/nocode/step_registry.rb
181
181
  - lib/nocode/steps/copy.rb
182
+ - lib/nocode/steps/dataset/append.rb
183
+ - lib/nocode/steps/dataset/coalesce.rb
184
+ - lib/nocode/steps/dataset/insert.rb
185
+ - lib/nocode/steps/dataset/prepend.rb
186
+ - lib/nocode/steps/dataset/range.rb
182
187
  - lib/nocode/steps/delete.rb
183
188
  - lib/nocode/steps/deserialize/csv.rb
184
189
  - lib/nocode/steps/deserialize/json.rb
185
190
  - lib/nocode/steps/deserialize/yaml.rb
191
+ - lib/nocode/steps/each.rb
192
+ - lib/nocode/steps/io/delete.rb
193
+ - lib/nocode/steps/io/list.rb
186
194
  - lib/nocode/steps/io/read.rb
187
195
  - lib/nocode/steps/io/write.rb
188
196
  - lib/nocode/steps/log.rb
197
+ - lib/nocode/steps/map.rb
198
+ - lib/nocode/steps/record/map.rb
189
199
  - lib/nocode/steps/serialize/csv.rb
190
200
  - lib/nocode/steps/serialize/json.rb
191
201
  - lib/nocode/steps/serialize/yaml.rb
192
202
  - lib/nocode/steps/set.rb
193
203
  - lib/nocode/steps/sleep.rb
204
+ - lib/nocode/steps_executor.rb
194
205
  - lib/nocode/util.rb
195
206
  - lib/nocode/util/arrayable.rb
196
207
  - lib/nocode/util/class_loader.rb
@@ -1,84 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative 'context'
4
- require_relative 'step_registry'
5
-
6
- module Nocode
7
- # Manages the lifecycle and executes a job.
8
- class Executor
9
- attr_reader :yaml, :io
10
-
11
- def initialize(yaml, io: $stdout)
12
- @yaml = yaml.respond_to?(:read) ? yaml.read : yaml
13
- @yaml = YAML.safe_load(@yaml) || {}
14
- @io = io
15
-
16
- freeze
17
- end
18
-
19
- def execute
20
- steps = yaml['steps'] || []
21
- parameters = yaml['parameters'] || {}
22
- context = Context.new(io: io, parameters: parameters)
23
-
24
- log_title
25
-
26
- steps.each do |step|
27
- step_instance = make_step(step, context)
28
-
29
- execute_step(step_instance)
30
- end
31
-
32
- log("Ended: #{DateTime.now}")
33
- log_line
34
-
35
- context
36
- end
37
-
38
- private
39
-
40
- def make_step(step, context)
41
- step = Util::ObjectTemplate.new(step).evaluate(context.to_h)
42
- type = step['type'].to_s
43
- name = step['name'].to_s
44
- options = step['options'] || {}
45
- step_class = StepRegistry.constant!(type)
46
-
47
- step_class.new(
48
- options: Util::Dictionary.new(options),
49
- context: context,
50
- name: name,
51
- type: type
52
- )
53
- end
54
-
55
- def execute_step(step)
56
- log(step.name) unless step.name.empty?
57
- log("Step: #{step.type}")
58
- log("Class: #{step.class}")
59
-
60
- time_in_seconds = Benchmark.measure { step.perform }.real
61
-
62
- log("Completed in #{time_in_seconds.round(3)} second(s)")
63
-
64
- log_line
65
- end
66
-
67
- def log_title
68
- log_line
69
-
70
- log('Nocode Execution')
71
- log("Started: #{DateTime.now}")
72
-
73
- log_line
74
- end
75
-
76
- def log_line
77
- log('-' * 50)
78
- end
79
-
80
- def log(msg)
81
- io.puts(msg)
82
- end
83
- end
84
- end