nocode 0.0.1 → 0.0.5

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 (41) 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 +14 -1
  5. data/CODE_OF_CONDUCT.md +73 -0
  6. data/README.md +121 -3
  7. data/lib/nocode/context.rb +3 -1
  8. data/lib/nocode/executor.rb +15 -9
  9. data/lib/nocode/step.rb +10 -2
  10. data/lib/nocode/step_registry.rb +6 -0
  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 +18 -0
  19. data/lib/nocode/steps/deserialize/json.rb +18 -0
  20. data/lib/nocode/steps/deserialize/yaml.rb +22 -0
  21. data/lib/nocode/steps/io/read.rb +1 -0
  22. data/lib/nocode/steps/io/write.rb +1 -0
  23. data/lib/nocode/steps/log.rb +1 -0
  24. data/lib/nocode/steps/serialize/csv.rb +37 -0
  25. data/lib/nocode/steps/serialize/json.rb +19 -0
  26. data/lib/nocode/steps/serialize/yaml.rb +19 -0
  27. data/lib/nocode/steps/set.rb +1 -0
  28. data/lib/nocode/steps/sleep.rb +3 -0
  29. data/lib/nocode/util/arrayable.rb +1 -0
  30. data/lib/nocode/util/class_loader.rb +1 -0
  31. data/lib/nocode/util/class_registry.rb +3 -0
  32. data/lib/nocode/util/dictionary.rb +1 -0
  33. data/lib/nocode/util/object_template.rb +44 -0
  34. data/lib/nocode/util/optionable.rb +14 -0
  35. data/lib/nocode/util/string_template.rb +6 -1
  36. data/lib/nocode/util.rb +1 -1
  37. data/lib/nocode/version.rb +1 -1
  38. data/lib/nocode.rb +4 -0
  39. data/nocode.gemspec +1 -0
  40. metadata +29 -3
  41. data/lib/nocode/options_template.rb +0 -34
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e3f1a62969f19767ac717993632a0d51785969482b172c6e33cbfc94cfd13ce3
4
- data.tar.gz: 76e62efa73b9d7489cea1bb7825df6ed490439de097f3b3abb404a8f74f98d63
3
+ metadata.gz: 8726547b06744c6a7870a8634901b344e41a65bb1ede23c7e68e99d2218d482f
4
+ data.tar.gz: 3462f703d7639662c0da4f96ec9913749698c7f7a08a58387671db909a88785f
5
5
  SHA512:
6
- metadata.gz: f64bfa9cc952d58d9a5d37c5ac1045fae4e5b63d1f9bac5f5ac2522420790dc0003b878e52c009bc275960acf39732ccfd65a9d3c68d223954bd4ff56ba30eb7
7
- data.tar.gz: 317d4ef406a064b2160247475a30911eaed26ac8d009a435c3ec9abe95ee91d64a88729a7146d3e19f7ab719e405b69b42dcaddf2fa928e0ef9d9fa014574b47
6
+ metadata.gz: f7bcfd37d152798a753801944a6a4787e3ad83a08fcfca0b40d9a0957cd7c5142a298adbb64215ed2e161c9e7f842b89ab460f6374fb8ec552a3482aa0b712eb
7
+ data.tar.gz: 6e024f679e2973888d5bb67c20988c8e93b296a6b6c4dbc66f7cb1f55d8638db48842834fd00c2a6d24050d9aa61950666107ff285a469e3f63835f74e7a5e7d
@@ -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,16 @@
1
- #### February 9th, 2022
1
+ #### 0.0.5 - February 13th, 2022
2
+
3
+ * Added initial Dataset steps.
4
+
5
+ #### 0.0.4 - February 13th, 2022
6
+
7
+ * Increase class documentation
8
+ * Add YAML serialization/deserialization
9
+
10
+ #### 0.0.3 - February 12th, 2022
11
+
12
+ * Add initial implementation.
13
+
14
+ #### 0.0.0 - February 9th, 2022
2
15
 
3
16
  * Establish initial repository and gem infrastructure.
@@ -0,0 +1,73 @@
1
+ # Contributor Covenant Code of Conduct
2
+
3
+ ## Our Pledge
4
+
5
+ In the interest of fostering an open and welcoming environment, we as
6
+ contributors and maintainers pledge to making participation in our project and
7
+ our community a harassment-free experience for everyone, regardless of age, body
8
+ size, disability, ethnicity, gender identity and expression, level of experience,
9
+ nationality, personal appearance, race, religion, or sexual identity and
10
+ orientation.
11
+
12
+ ## Our Standards
13
+
14
+ Examples of behavior that contributes to creating a positive environment
15
+ include:
16
+
17
+ * Using welcoming and inclusive language
18
+ * Being respectful of differing viewpoints and experiences
19
+ * Gracefully accepting constructive criticism
20
+ * Focusing on what is best for the community
21
+ * Showing empathy towards other community members
22
+
23
+ Examples of unacceptable behavior by participants include:
24
+
25
+ * The use of sexualized language or imagery and unwelcome sexual attention or
26
+ advances
27
+ * Trolling, insulting/derogatory comments, and personal or political attacks
28
+ * Public or private harassment
29
+ * Publishing others' private information, such as a physical or electronic
30
+ address, without explicit permission
31
+ * Other conduct which could reasonably be considered inappropriate in a
32
+ professional setting
33
+
34
+ ## Our Responsibilities
35
+
36
+ Project maintainers are responsible for clarifying the standards of acceptable
37
+ behavior and are expected to take appropriate and fair corrective action in
38
+ response to any instances of unacceptable behavior.
39
+
40
+ Project maintainers have the right and responsibility to remove, edit, or
41
+ reject comments, commits, code, wiki edits, issues, and other contributions
42
+ that are not aligned to this Code of Conduct, or to ban temporarily or
43
+ permanently any contributor for other behaviors that they deem inappropriate,
44
+ threatening, offensive, or harmful.
45
+
46
+ ## Scope
47
+
48
+ This Code of Conduct applies both within project spaces and in public spaces
49
+ when an individual is representing the project or its community. Examples of
50
+ representing a project or community include using an official project e-mail
51
+ address, posting via an official social media account, or acting as an appointed
52
+ representative at an online or offline event. Representation of a project may be
53
+ further defined and clarified by project maintainers.
54
+
55
+ ## Enforcement
56
+
57
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be
58
+ reported. All complaints will be reviewed and investigated and will result in a response that
59
+ is deemed necessary and appropriate to the circumstances. The project team is
60
+ obligated to maintain confidentiality with regard to the reporter of an incident.
61
+ Further details of specific enforcement policies may be posted separately.
62
+
63
+ Project maintainers who do not follow or enforce the Code of Conduct in good
64
+ faith may face temporary or permanent repercussions as determined by other
65
+ members of the project's leadership.
66
+
67
+ ## Attribution
68
+
69
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
70
+ available at [http://contributor-covenant.org/version/1/4][version]
71
+
72
+ [homepage]: http://contributor-covenant.org
73
+ [version]: http://contributor-covenant.org/version/1/4/
data/README.md CHANGED
@@ -2,8 +2,126 @@
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.
8
+
9
+ ## Installation
10
+
11
+ To install through Rubygems:
12
+
13
+ ````
14
+ gem install nocode
15
+ ````
16
+
17
+ You can also add this to your Gemfile using:
18
+
19
+ ````
20
+ bundle add nocode
21
+ ````
22
+
23
+ ## Examples
24
+
25
+ Create a file called `nocode-csv-to-json.yaml`:
26
+
27
+ ### CSV-to-JSON File Converter
28
+
29
+ ````yaml
30
+ parameters:
31
+ input_filename: input.csv
32
+ output_filename: output.json
33
+
34
+ steps:
35
+ - type: io/read
36
+ name: READ FILE
37
+ options:
38
+ path:
39
+ - files
40
+ - << parameters.input_filename >>
41
+ - type: deserialize/csv
42
+ - type: serialize/json
43
+ - type: io/write
44
+ options:
45
+ path:
46
+ - files
47
+ - << parameters.output_filename >>
48
+ ````
49
+
50
+ Create csv file at: `files/input.csv`
51
+
52
+ Execute in Ruby:
53
+
54
+ ````ruby
55
+ path = Pathname.new('nocode-csv-to-json.yaml')
56
+
57
+ Nocode.execute(path)
58
+ ````
59
+
60
+ Or use bundler:
61
+
62
+ ````zsh
63
+ bundle exec nocode `nocode-csv-to-json.yaml
64
+ ````
65
+
66
+ A file should have been created at: `files/output.json`.
67
+
68
+ Notes:
69
+
70
+ * Path can be an array or a string. If its an array then the environment's path separator will be used.
71
+ * The `name` job key is optional. If present then it will print out on the log.
72
+ * Parameter values can be interpolated with keys and values using `<< parameters.key >>` syntax
73
+
74
+
75
+
76
+ ## Contributing
77
+
78
+ ### Development Environment Configuration
79
+
80
+ Basic steps to take to get this repository compiling:
81
+
82
+ 1. Install [Ruby](https://www.ruby-lang.org/en/documentation/installation/) (check nocode.gemspec for versions supported)
83
+ 2. Install bundler (gem install bundler)
84
+ 3. Clone the repository (git clone git@github.com:mattruggio/nocode.git)
85
+ 4. Navigate to the root folder (cd nocode)
86
+ 5. Install dependencies (bundle)
87
+
88
+ ### Running Tests
89
+
90
+ To execute the test suite run:
91
+
92
+ ````bash
93
+ bundle exec rspec spec --format documentation
94
+ ````
95
+
96
+ Alternatively, you can have Guard watch for changes:
97
+
98
+ ````bash
99
+ bundle exec guard
100
+ ````
101
+
102
+ Also, do not forget to run Rubocop:
103
+
104
+ ````bash
105
+ bundle exec rubocop
106
+ ````
107
+
108
+ ### Publishing
109
+
110
+ Note: ensure you have proper authorization before trying to publish new versions.
111
+
112
+ After code changes have successfully gone through the Pull Request review process then the following steps should be followed for publishing new versions:
113
+
114
+ 1. Merge Pull Request into main
115
+ 2. Update `version.rb` using [semantic versioning](https://semver.org/)
116
+ 3. Install dependencies: `bundle`
117
+ 4. Update `CHANGELOG.md` with release notes
118
+ 5. Commit & push main to remote and ensure CI builds main successfully
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).
120
+
121
+ ## Code of Conduct
122
+
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).
124
+
125
+ ## License
126
+
127
+ This project is MIT Licensed.
@@ -1,6 +1,8 @@
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
7
  attr_reader :io, :parameters, :registers
6
8
 
@@ -17,7 +19,7 @@ module Nocode
17
19
  end
18
20
 
19
21
  def parameter(key)
20
- parameters(key)
22
+ parameters[key]
21
23
  end
22
24
 
23
25
  def to_h
@@ -1,11 +1,17 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative 'context'
4
- require_relative 'options_template'
5
4
  require_relative 'step_registry'
6
5
 
7
6
  module Nocode
7
+ # Manages the lifecycle and executes a job.
8
8
  class Executor
9
+ NAME_KEY = 'name'
10
+ OPTIONS_KEY = 'options'
11
+ PARAMETERS_KEY = 'parameters'
12
+ STEPS_KEY = 'steps'
13
+ TYPE_KEY = 'type'
14
+
9
15
  attr_reader :yaml, :io
10
16
 
11
17
  def initialize(yaml, io: $stdout)
@@ -17,8 +23,8 @@ module Nocode
17
23
  end
18
24
 
19
25
  def execute
20
- steps = yaml['steps'] || []
21
- parameters = yaml['parameters'] || {}
26
+ steps = yaml[STEPS_KEY] || []
27
+ parameters = yaml[PARAMETERS_KEY] || {}
22
28
  context = Context.new(io: io, parameters: parameters)
23
29
 
24
30
  log_title
@@ -38,14 +44,14 @@ module Nocode
38
44
  private
39
45
 
40
46
  def make_step(step, context)
41
- type = step['type'].to_s
42
- name = step['name'].to_s
43
- options = step['options'] || {}
44
- compiled_options = OptionsTemplate.new(options).evaluate(context.to_h)
45
- step_class = StepRegistry.constant!(type)
47
+ step = Util::ObjectTemplate.new(step).evaluate(context.to_h)
48
+ type = step[TYPE_KEY].to_s
49
+ name = step[NAME_KEY].to_s
50
+ options = step[OPTIONS_KEY] || {}
51
+ step_class = StepRegistry.constant!(type)
46
52
 
47
53
  step_class.new(
48
- options: Util::Dictionary.new(compiled_options),
54
+ options: Util::Dictionary.new(options),
49
55
  context: context,
50
56
  name: name,
51
57
  type: type
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,7 +2,11 @@
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
 
@@ -22,9 +26,11 @@ module Nocode
22
26
  def load!
23
27
  files_loaded = Util::ClassLoader.new(DIR).load!
24
28
 
29
+ # Class the parent to load up the registry with the files we found.
25
30
  load(files_loaded, 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
 
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nocode
4
+ module Steps
5
+ module Deserialize
6
+ # take a specified register and parse it as a CSV to produce an array of hashes.
7
+ class Csv < Step
8
+ option :register
9
+
10
+ def perform
11
+ input = registers[register_option].to_s
12
+
13
+ registers[register_option] = CSV.new(input, headers: true).map(&:to_h)
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nocode
4
+ module Steps
5
+ module Deserialize
6
+ # take a specified register and parse it as JSON to produce Ruby object(s).
7
+ class Json < Step
8
+ option :register
9
+
10
+ def perform
11
+ input = registers[register_option]
12
+
13
+ registers[register_option] = JSON.parse(input)
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -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
@@ -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
 
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nocode
4
+ module Steps
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.
8
+ class Csv < Step
9
+ option :register
10
+
11
+ def perform
12
+ input = registers[register_option]
13
+
14
+ registers[register_option] = CSV.generate do |csv|
15
+ array(input).each_with_index do |object, index|
16
+ csv << object.keys if index.zero? && object.respond_to?(:keys)
17
+
18
+ add_object(object, csv)
19
+ end
20
+ end
21
+ end
22
+
23
+ private
24
+
25
+ def add_object(object, csv)
26
+ object ||= {}
27
+
28
+ if object.is_a?(Array)
29
+ csv << object
30
+ elsif object.respond_to?(:values)
31
+ csv << object.values
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -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 JSON. The serialized JSON
7
+ # will override the register specified.
8
+ class Json < Step
9
+ option :register
10
+
11
+ def perform
12
+ input = registers[register_option]
13
+
14
+ registers[register_option] = input.to_json
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -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
 
@@ -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
 
@@ -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)
@@ -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.1'
4
+ VERSION = '0.0.5'
5
5
  end
data/lib/nocode.rb CHANGED
@@ -1,7 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'benchmark'
4
+ require 'csv'
4
5
  require 'fileutils'
6
+ require 'json'
5
7
  require 'singleton'
6
8
  require 'time'
7
9
  require 'yaml'
@@ -12,7 +14,9 @@ require 'nocode/util'
12
14
  # Core
13
15
  require 'nocode/executor'
14
16
 
17
+ # Establish main top-level namespace
15
18
  module Nocode
19
+ # Default consumer entrypoint into the library.
16
20
  class << self
17
21
  def execute(yaml, io: $stdout)
18
22
  Executor.new(yaml, io: io).execute
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.1
4
+ version: 0.0.5
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
@@ -151,6 +165,7 @@ files:
151
165
  - ".rubocop.yml"
152
166
  - ".tool-versions"
153
167
  - CHANGELOG.md
168
+ - CODE_OF_CONDUCT.md
154
169
  - Gemfile
155
170
  - Guardfile
156
171
  - LICENSE
@@ -161,14 +176,24 @@ files:
161
176
  - lib/nocode.rb
162
177
  - lib/nocode/context.rb
163
178
  - lib/nocode/executor.rb
164
- - lib/nocode/options_template.rb
165
179
  - lib/nocode/step.rb
166
180
  - lib/nocode/step_registry.rb
167
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
168
187
  - lib/nocode/steps/delete.rb
188
+ - lib/nocode/steps/deserialize/csv.rb
189
+ - lib/nocode/steps/deserialize/json.rb
190
+ - lib/nocode/steps/deserialize/yaml.rb
169
191
  - lib/nocode/steps/io/read.rb
170
192
  - lib/nocode/steps/io/write.rb
171
193
  - lib/nocode/steps/log.rb
194
+ - lib/nocode/steps/serialize/csv.rb
195
+ - lib/nocode/steps/serialize/json.rb
196
+ - lib/nocode/steps/serialize/yaml.rb
172
197
  - lib/nocode/steps/set.rb
173
198
  - lib/nocode/steps/sleep.rb
174
199
  - lib/nocode/util.rb
@@ -176,6 +201,7 @@ files:
176
201
  - lib/nocode/util/class_loader.rb
177
202
  - lib/nocode/util/class_registry.rb
178
203
  - lib/nocode/util/dictionary.rb
204
+ - lib/nocode/util/object_template.rb
179
205
  - lib/nocode/util/optionable.rb
180
206
  - lib/nocode/util/string_template.rb
181
207
  - lib/nocode/version.rb
@@ -1,34 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Nocode
4
- class OptionsTemplate
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