nocode 0.0.3 → 0.0.7

Sign up to get free protection for your applications and to get access to all the features.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/rubygem.yml +2 -0
  3. data/.rubocop.yml +0 -3
  4. data/CHANGELOG.md +19 -0
  5. data/README.md +4 -6
  6. data/exe/nocode +2 -0
  7. data/lib/nocode/context.rb +18 -3
  8. data/lib/nocode/job_executor.rb +55 -0
  9. data/lib/nocode/step.rb +10 -2
  10. data/lib/nocode/step_registry.rb +9 -3
  11. data/lib/nocode/steps/copy.rb +2 -1
  12. data/lib/nocode/steps/dataset/append.rb +21 -0
  13. data/lib/nocode/steps/dataset/coalesce.rb +22 -0
  14. data/lib/nocode/steps/dataset/insert.rb +29 -0
  15. data/lib/nocode/steps/dataset/prepend.rb +21 -0
  16. data/lib/nocode/steps/dataset/range.rb +32 -0
  17. data/lib/nocode/steps/delete.rb +1 -0
  18. data/lib/nocode/steps/deserialize/csv.rb +1 -0
  19. data/lib/nocode/steps/deserialize/json.rb +1 -0
  20. data/lib/nocode/steps/deserialize/yaml.rb +22 -0
  21. data/lib/nocode/steps/each.rb +31 -0
  22. data/lib/nocode/steps/io/read.rb +1 -0
  23. data/lib/nocode/steps/io/write.rb +1 -0
  24. data/lib/nocode/steps/log.rb +1 -0
  25. data/lib/nocode/steps/serialize/csv.rb +2 -6
  26. data/lib/nocode/steps/serialize/json.rb +2 -0
  27. data/lib/nocode/steps/serialize/yaml.rb +19 -0
  28. data/lib/nocode/steps/set.rb +1 -0
  29. data/lib/nocode/steps/sleep.rb +3 -0
  30. data/lib/nocode/steps_executor.rb +68 -0
  31. data/lib/nocode/util/arrayable.rb +1 -0
  32. data/lib/nocode/util/class_loader.rb +1 -0
  33. data/lib/nocode/util/class_registry.rb +5 -2
  34. data/lib/nocode/util/dictionary.rb +1 -0
  35. data/lib/nocode/util/object_template.rb +44 -0
  36. data/lib/nocode/util/optionable.rb +22 -0
  37. data/lib/nocode/util/string_template.rb +6 -1
  38. data/lib/nocode/util.rb +1 -1
  39. data/lib/nocode/version.rb +1 -1
  40. data/lib/nocode.rb +4 -2
  41. data/nocode.gemspec +1 -0
  42. metadata +27 -4
  43. data/lib/nocode/executor.rb +0 -84
  44. data/lib/nocode/object_template.rb +0 -34
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4609bf6b3f082509087ab07dfbe8b96f55df01788baac74c0d7c4500f1a48f35
4
- data.tar.gz: 1acd73ef5df505964078a840362f271a8196b7f099e5f18e9f5d7f4692152e5c
3
+ metadata.gz: 73aa9b620a2823a96b00c4b38f5a8e0fc8f5f040b5100e255e5f3df546263544
4
+ data.tar.gz: d5dbacadf0759e59ddd279c7446f679052ff89b4f2d3ea8d49378173368bd28f
5
5
  SHA512:
6
- metadata.gz: ce4af8b6a125a1cd178d0436e32918ed246053ac8f214b7adc98f4051ab34916c60794bb915f978aa0101adca7f4d9ccd332b04f0f27efaba8dc9fb17035e310
7
- data.tar.gz: 4e2e951062a9085a6bba5d146c5672f0862e6dc92eeedcc5d08cc5e7fdcdad86c08395240034a13441b6770c3bf0d31b1383446bf9f27ed4f18ceb0cd4cda9ac
6
+ metadata.gz: 5fe2d5363d14bb6d035072629e280d6e87717a3a16e3d28d69e520ea6a8aa21d1ad330f554c94c6d6c5113c8acbb665fc3d4b9f8bde74d7652e8b14d3c1b42c8
7
+ data.tar.gz: c8f2c04683439abecfc3de0c63a909cc1b52686f2741f1936f03ea152bef263a41c4613d26e2ae46c732c6f879a6cfe40e62eed6d8922cb7b862a87329624b45
@@ -25,3 +25,5 @@ jobs:
25
25
  bundler-cache: true # runs 'bundle install' and caches installed gems automatically
26
26
  - name: Run tests
27
27
  run: bundle exec rake
28
+ - name: Security audit dependencies
29
+ run: bundle exec bundler-audit check --update
data/.rubocop.yml CHANGED
@@ -14,8 +14,5 @@ Metrics/BlockLength:
14
14
  Metrics/MethodLength:
15
15
  Max: 20
16
16
 
17
- Style/Documentation:
18
- Enabled: false
19
-
20
17
  RSpec/ExampleLength:
21
18
  Max: 10
data/CHANGELOG.md CHANGED
@@ -1,3 +1,22 @@
1
+ #### 0.0.7 - February 13th, 2022
2
+
3
+ * Move shared logging logic to context (until a first class log writer emerges).
4
+ * Provide type_prefix option for class_registry
5
+
6
+ #### 0.0.6 - February 13th, 2022
7
+
8
+ * Expose registers to main YAML configuration.
9
+ * Add Each step to serve as an example of an iterator.
10
+
11
+ #### 0.0.5 - February 13th, 2022
12
+
13
+ * Added initial Dataset steps.
14
+
15
+ #### 0.0.4 - February 13th, 2022
16
+
17
+ * Increase class documentation
18
+ * Add YAML serialization/deserialization
19
+
1
20
  #### 0.0.3 - February 12th, 2022
2
21
 
3
22
  * Add initial implementation.
data/README.md CHANGED
@@ -2,9 +2,7 @@
2
2
 
3
3
  #### Execute Ruby code through YAML
4
4
 
5
- [![Ruby Gem CI](https://github.com/mattruggio/nocode/actions/workflows/rubygem.yml/badge.svg)](https://github.com/mattruggio/nocode/actions/workflows/rubygem.yml)
6
-
7
- **Warning**: This library is currently experimental.
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)
8
6
 
9
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.
10
8
 
@@ -113,16 +111,16 @@ Note: ensure you have proper authorization before trying to publish new versions
113
111
 
114
112
  After code changes have successfully gone through the Pull Request review process then the following steps should be followed for publishing new versions:
115
113
 
116
- 1. Merge Pull Request into master
114
+ 1. Merge Pull Request into main
117
115
  2. Update `version.rb` using [semantic versioning](https://semver.org/)
118
116
  3. Install dependencies: `bundle`
119
117
  4. Update `CHANGELOG.md` with release notes
120
- 5. Commit & push master to remote and ensure CI builds master successfully
118
+ 5. Commit & push main to remote and ensure CI builds main successfully
121
119
  6. Run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
122
120
 
123
121
  ## Code of Conduct
124
122
 
125
- Everyone interacting in this codebase, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/mattruggio/nocode/blob/master/CODE_OF_CONDUCT.md).
123
+ Everyone interacting in this codebase, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/mattruggio/nocode/blob/main/CODE_OF_CONDUCT.md).
126
124
 
127
125
  ## License
128
126
 
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?
@@ -1,8 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Nocode
4
+ # Describes the environment for each running step. An instance is initialized when a job
5
+ # kicks off and then is passed from step to step.
4
6
  class Context
5
- attr_reader :io, :parameters, :registers
7
+ PARAMETERS_KEY = 'parameters'
8
+ REGISTERS_KEY = 'registers'
9
+
10
+ attr_reader :io,
11
+ :parameters,
12
+ :registers
6
13
 
7
14
  def initialize(io: $stdout, parameters: {}, registers: {})
8
15
  @io = io || $stdout
@@ -22,9 +29,17 @@ module Nocode
22
29
 
23
30
  def to_h
24
31
  {
25
- 'registers' => registers,
26
- 'parameters' => parameters
32
+ REGISTERS_KEY => registers,
33
+ PARAMETERS_KEY => parameters
27
34
  }
28
35
  end
36
+
37
+ def log_line
38
+ log('-' * 50)
39
+ end
40
+
41
+ def log(msg)
42
+ io.puts(msg)
43
+ end
29
44
  end
30
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
data/lib/nocode/step.rb CHANGED
@@ -1,14 +1,22 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Nocode
4
+ # Defines a running step. Steps should be sub-classes of this class as well as to implement
5
+ # #perform.
4
6
  class Step
5
7
  extend Forwardable
6
8
  include Util::Arrayable
7
9
  include Util::Optionable
8
10
 
9
- attr_reader :name, :context, :options, :type
11
+ attr_reader :context,
12
+ :name,
13
+ :options,
14
+ :type
10
15
 
11
- def_delegators :context, :parameters, :registers, :io
16
+ def_delegators :context,
17
+ :io,
18
+ :parameters,
19
+ :registers
12
20
 
13
21
  def initialize(
14
22
  context: Context.new,
@@ -2,12 +2,16 @@
2
2
 
3
3
  require_relative 'step'
4
4
 
5
+ # This will execute the StepRegisty's load! method upon script evaluation.
5
6
  module Nocode
7
+ # Provides a global place to register all valid steps by their types. By default the
8
+ # steps directory will be autoloaded and their paths will be used as their types. For example:
9
+ # for the class: steps/io/write, it would register as "io/write" type.
6
10
  class StepRegistry < Util::ClassRegistry
7
11
  include Singleton
8
12
 
9
- PREFIX = 'Nocode::Steps::'
10
- DIR = File.join(__dir__, 'steps')
13
+ CLASS_PREFIX = 'Nocode::Steps::'
14
+ DIR = File.join(__dir__, 'steps')
11
15
 
12
16
  class << self
13
17
  extend Forwardable
@@ -22,9 +26,11 @@ module Nocode
22
26
  def load!
23
27
  files_loaded = Util::ClassLoader.new(DIR).load!
24
28
 
25
- load(files_loaded, PREFIX)
29
+ # Class the parent to load up the registry with the files we found.
30
+ load(files_loaded, class_prefix: CLASS_PREFIX)
26
31
  end
27
32
  end
28
33
 
34
+ # Call upon class evaluation to autoload all classes.
29
35
  StepRegistry.load!
30
36
  end
@@ -2,11 +2,12 @@
2
2
 
3
3
  module Nocode
4
4
  module Steps
5
+ # Shallow-copy one register to another.
5
6
  class Copy < Step
6
7
  option :from_register, :to_register
7
8
 
8
9
  def perform
9
- registers[to_register_option] = registers[from_register_option]
10
+ registers[to_register_option] = registers[from_register_option].dup
10
11
  end
11
12
  end
12
13
  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 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
@@ -2,6 +2,7 @@
2
2
 
3
3
  module Nocode
4
4
  module Steps
5
+ # Remove a register completely.
5
6
  class Delete < Step
6
7
  option :register
7
8
 
@@ -3,6 +3,7 @@
3
3
  module Nocode
4
4
  module Steps
5
5
  module Deserialize
6
+ # take a specified register and parse it as a CSV to produce an array of hashes.
6
7
  class Csv < Step
7
8
  option :register
8
9
 
@@ -3,6 +3,7 @@
3
3
  module Nocode
4
4
  module Steps
5
5
  module Deserialize
6
+ # take a specified register and parse it as JSON to produce Ruby object(s).
6
7
  class Json < Step
7
8
  option :register
8
9
 
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nocode
4
+ module Steps
5
+ module Deserialize
6
+ # Take a specified register and parse it as YAML to produce Ruby object(s).
7
+ #
8
+ # NOTE: This will throw an error if unsafe YAML types are used. The only allowed types are
9
+ # array, hash, strings, numbers, booleans, nil. See:
10
+ # https://ruby-doc.org/stdlib-2.6.1/libdoc/psych/rdoc/Psych.html#method-c-safe_load
11
+ class Yaml < Step
12
+ option :register
13
+
14
+ def perform
15
+ input = registers[register_option]
16
+
17
+ registers[register_option] = YAML.safe_load(input)
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,31 @@
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
+ # rubocop:disable Metrics/AbcSize
16
+ def perform
17
+ entries = array(registers[register_option])
18
+
19
+ entries.each_with_index do |entry, index|
20
+ steps = array(steps_option)
21
+
22
+ registers["#{element_register_prefix_option}_element"] = entry
23
+ registers["#{element_register_prefix_option}_index"] = index
24
+
25
+ StepsExecutor.new(context: context, steps: steps).execute
26
+ end
27
+ end
28
+ # rubocop:enable Metrics/AbcSize
29
+ end
30
+ end
31
+ end
@@ -3,6 +3,7 @@
3
3
  module Nocode
4
4
  module Steps
5
5
  module Io
6
+ # Read a file from disk and place its contents in a register.
6
7
  class Read < Step
7
8
  option :path,
8
9
  :register
@@ -3,6 +3,7 @@
3
3
  module Nocode
4
4
  module Steps
5
5
  module Io
6
+ # Write the contents of a register to disk.
6
7
  class Write < Step
7
8
  option :path,
8
9
  :register
@@ -2,6 +2,7 @@
2
2
 
3
3
  module Nocode
4
4
  module Steps
5
+ # Simply output the passed in message into the outputted log.
5
6
  class Log < Step
6
7
  option :message
7
8
 
@@ -3,6 +3,8 @@
3
3
  module Nocode
4
4
  module Steps
5
5
  module Serialize
6
+ # Take the contents of a register and create a CSV out of its contents. The CSV contents
7
+ # will override the register specified.
6
8
  class Csv < Step
7
9
  option :register
8
10
 
@@ -25,14 +27,8 @@ module Nocode
25
27
 
26
28
  if object.is_a?(Array)
27
29
  csv << object
28
-
29
- true
30
30
  elsif object.respond_to?(:values)
31
31
  csv << object.values
32
-
33
- true
34
- else
35
- false
36
32
  end
37
33
  end
38
34
  end
@@ -3,6 +3,8 @@
3
3
  module Nocode
4
4
  module Steps
5
5
  module Serialize
6
+ # Take the contents of a register and serialize it as JSON. The serialized JSON
7
+ # will override the register specified.
6
8
  class Json < Step
7
9
  option :register
8
10
 
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nocode
4
+ module Steps
5
+ module Serialize
6
+ # Take the contents of a register and serialize it as YAML. The serialized YAML
7
+ # will override the register specified.
8
+ class Yaml < Step
9
+ option :register
10
+
11
+ def perform
12
+ input = registers[register_option]
13
+
14
+ registers[register_option] = input.to_yaml
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -2,6 +2,7 @@
2
2
 
3
3
  module Nocode
4
4
  module Steps
5
+ # Set a register's value to the value option specified.
5
6
  class Set < Step
6
7
  option :register, :value
7
8
 
@@ -2,6 +2,9 @@
2
2
 
3
3
  module Nocode
4
4
  module Steps
5
+ # Sleep for an arbitrary number of seconds.
6
+ #
7
+ # Mechanic: https://apidock.com/ruby/Kernel/sleep
5
8
  class Sleep < Step
6
9
  option :seconds
7
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
@@ -2,6 +2,7 @@
2
2
 
3
3
  module Nocode
4
4
  module Util
5
+ # Hand
5
6
  module Arrayable
6
7
  def array(value)
7
8
  if value.is_a?(Hash)
@@ -2,6 +2,7 @@
2
2
 
3
3
  module Nocode
4
4
  module Util
5
+ # Loads a directory full of Ruby classes and returns their relative paths.
5
6
  class ClassLoader
6
7
  EXTENSION = '.rb'
7
8
 
@@ -2,6 +2,9 @@
2
2
 
3
3
  module Nocode
4
4
  module Util
5
+ # Create a type -> class constant interface. Classes can be registered as types. Types
6
+ # are snake-cased while class names are stored as pascal-cased. Then constant! can be called
7
+ # to retrieve the class constant by type.
5
8
  class ClassRegistry
6
9
  extend Forwardable
7
10
 
@@ -17,13 +20,13 @@ module Nocode
17
20
  freeze
18
21
  end
19
22
 
20
- def load(types, prefix = '')
23
+ def load(types, class_prefix: '', type_prefix: '')
21
24
  types.each do |type|
22
25
  pascal_cased = type.split(File::SEPARATOR).map do |part|
23
26
  part.split('_').collect(&:capitalize).join
24
27
  end.join('::')
25
28
 
26
- register(type, "#{prefix}#{pascal_cased}")
29
+ register("#{type_prefix}#{type}", "#{class_prefix}#{pascal_cased}")
27
30
  end
28
31
 
29
32
  self
@@ -2,6 +2,7 @@
2
2
 
3
3
  module Nocode
4
4
  module Util
5
+ # A hash-like object which ensures all keys are strings.
5
6
  class Dictionary
6
7
  extend Forwardable
7
8
 
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'string_template'
4
+
5
+ module Nocode
6
+ module Util
7
+ # Built on top of StringTemplate but instead of only working for a string, this will
8
+ # recursively evaluate all strings within an object. Heuristics:
9
+ # - Strings evaluate using StringTemplate
10
+ # - Hashes will have their keys and values traversed
11
+ # - Arrays will have their entries traversed
12
+ # - All other types will simply return themselves
13
+ class ObjectTemplate
14
+ attr_reader :object
15
+
16
+ def initialize(object)
17
+ @object = object
18
+
19
+ freeze
20
+ end
21
+
22
+ def evaluate(values = {})
23
+ recursive_evaluate(object, values)
24
+ end
25
+
26
+ private
27
+
28
+ def recursive_evaluate(expression, values)
29
+ case expression
30
+ when Array
31
+ expression.map { |o| recursive_evaluate(o, values) }
32
+ when Hash
33
+ expression.to_h do |k, v|
34
+ [recursive_evaluate(k, values), recursive_evaluate(v, values)]
35
+ end
36
+ when String
37
+ Util::StringTemplate.new(expression).evaluate(values)
38
+ else
39
+ expression
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -2,6 +2,20 @@
2
2
 
3
3
  module Nocode
4
4
  module Util
5
+ # Add on a DSL for classes. The DSL allows for a new class-level keyword called 'option'
6
+ # which can be used to describe what metadata values are important. Then instances
7
+ # can reference those option's values using magic _option methods. For example:
8
+ #
9
+ # class Animal
10
+ # include Optionable
11
+ # option :type
12
+ # attr_writer :options
13
+ # end
14
+ #
15
+ # animal = Animal.new
16
+ # animal.options = { 'type' => 'dog' }
17
+ #
18
+ # animal.type_option # -> should return 'dog'
5
19
  module Optionable
6
20
  def self.included(klass)
7
21
  klass.extend(ClassMethods)
@@ -9,6 +23,14 @@ module Nocode
9
23
 
10
24
  # Class-level DSL Methods
11
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
+
12
34
  def option(*values)
13
35
  values.each { |v| options << v.to_s }
14
36
  end
@@ -2,6 +2,11 @@
2
2
 
3
3
  module Nocode
4
4
  module Util
5
+ # Takes in an expression and interpolates in any parameters using << >> notation.
6
+ # For example:
7
+ # input = { 'person' => 'hops' }
8
+ # Nocode::Util::StringTemplate.new("Hello, << person.name >>!").evaluate(input)
9
+ # Should produce: "Hello, hops!"
5
10
  class StringTemplate
6
11
  LEFT_TOKEN = '<<'
7
12
  RIGHT_TOKEN = '>>'
@@ -11,7 +16,7 @@ module Nocode
11
16
  attr_reader :expression
12
17
 
13
18
  def initialize(expression)
14
- @expression = expression
19
+ @expression = expression.to_s
15
20
 
16
21
  freeze
17
22
  end
data/lib/nocode/util.rb CHANGED
@@ -4,5 +4,5 @@ require_relative 'util/arrayable'
4
4
  require_relative 'util/class_loader'
5
5
  require_relative 'util/class_registry'
6
6
  require_relative 'util/dictionary'
7
+ require_relative 'util/object_template'
7
8
  require_relative 'util/optionable'
8
- require_relative 'util/string_template'
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Nocode
4
- VERSION = '0.0.3'
4
+ VERSION = '0.0.7'
5
5
  end
data/lib/nocode.rb CHANGED
@@ -12,12 +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
+ # Establish main top-level namespace
17
18
  module Nocode
19
+ # Default consumer entrypoint into the library.
18
20
  class << self
19
21
  def execute(yaml, io: $stdout)
20
- Executor.new(yaml, io: io).execute
22
+ JobExecutor.new(yaml, io: io).execute
21
23
  end
22
24
  end
23
25
  end
data/nocode.gemspec CHANGED
@@ -29,6 +29,7 @@ Gem::Specification.new do |s|
29
29
 
30
30
  s.required_ruby_version = '>= 2.6'
31
31
 
32
+ s.add_development_dependency('bundler-audit')
32
33
  s.add_development_dependency('guard-rspec')
33
34
  s.add_development_dependency('pry')
34
35
  s.add_development_dependency('rake')
metadata CHANGED
@@ -1,15 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: nocode
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.3
4
+ version: 0.0.7
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-14 00:00:00.000000000 Z
12
12
  dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler-audit
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
13
27
  - !ruby/object:Gem::Dependency
14
28
  name: guard-rspec
15
29
  requirement: !ruby/object:Gem::Requirement
@@ -161,26 +175,35 @@ files:
161
175
  - exe/nocode
162
176
  - lib/nocode.rb
163
177
  - lib/nocode/context.rb
164
- - lib/nocode/executor.rb
165
- - lib/nocode/object_template.rb
178
+ - lib/nocode/job_executor.rb
166
179
  - lib/nocode/step.rb
167
180
  - lib/nocode/step_registry.rb
168
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
169
187
  - lib/nocode/steps/delete.rb
170
188
  - lib/nocode/steps/deserialize/csv.rb
171
189
  - lib/nocode/steps/deserialize/json.rb
190
+ - lib/nocode/steps/deserialize/yaml.rb
191
+ - lib/nocode/steps/each.rb
172
192
  - lib/nocode/steps/io/read.rb
173
193
  - lib/nocode/steps/io/write.rb
174
194
  - lib/nocode/steps/log.rb
175
195
  - lib/nocode/steps/serialize/csv.rb
176
196
  - lib/nocode/steps/serialize/json.rb
197
+ - lib/nocode/steps/serialize/yaml.rb
177
198
  - lib/nocode/steps/set.rb
178
199
  - lib/nocode/steps/sleep.rb
200
+ - lib/nocode/steps_executor.rb
179
201
  - lib/nocode/util.rb
180
202
  - lib/nocode/util/arrayable.rb
181
203
  - lib/nocode/util/class_loader.rb
182
204
  - lib/nocode/util/class_registry.rb
183
205
  - lib/nocode/util/dictionary.rb
206
+ - lib/nocode/util/object_template.rb
184
207
  - lib/nocode/util/optionable.rb
185
208
  - lib/nocode/util/string_template.rb
186
209
  - lib/nocode/version.rb
@@ -1,84 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative 'context'
4
- require_relative 'object_template'
5
- require_relative 'step_registry'
6
-
7
- module Nocode
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 = 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
@@ -1,34 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Nocode
4
- class ObjectTemplate
5
- attr_reader :object
6
-
7
- def initialize(object)
8
- @object = object
9
-
10
- freeze
11
- end
12
-
13
- def evaluate(values = {})
14
- recursive_evaluate(object, values)
15
- end
16
-
17
- private
18
-
19
- def recursive_evaluate(expression, values)
20
- case expression
21
- when Array
22
- expression.map { |o| recursive_evaluate(o, values) }
23
- when Hash
24
- expression.to_h do |k, v|
25
- [recursive_evaluate(k, values), recursive_evaluate(v, values)]
26
- end
27
- when String
28
- Util::StringTemplate.new(expression).evaluate(values)
29
- else
30
- expression
31
- end
32
- end
33
- end
34
- end