nocode 0.0.0 → 0.0.4
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.
- checksums.yaml +4 -4
- data/.github/workflows/rubygem.yml +2 -1
- data/.rubocop.yml +7 -2
- data/CHANGELOG.md +10 -1
- data/CODE_OF_CONDUCT.md +73 -0
- data/README.md +121 -5
- data/exe/nocode +1 -1
- data/lib/nocode/context.rb +32 -0
- data/lib/nocode/executor.rb +84 -0
- data/lib/nocode/step.rb +33 -0
- data/lib/nocode/step_registry.rb +36 -0
- data/lib/nocode/steps/copy.rb +14 -0
- data/lib/nocode/steps/delete.rb +14 -0
- data/lib/nocode/steps/deserialize/csv.rb +18 -0
- data/lib/nocode/steps/deserialize/json.rb +18 -0
- data/lib/nocode/steps/deserialize/yaml.rb +22 -0
- data/lib/nocode/steps/io/read.rb +25 -0
- data/lib/nocode/steps/io/write.rb +27 -0
- data/lib/nocode/steps/log.rb +14 -0
- data/lib/nocode/steps/serialize/csv.rb +43 -0
- data/lib/nocode/steps/serialize/json.rb +19 -0
- data/lib/nocode/steps/serialize/yaml.rb +19 -0
- data/lib/nocode/steps/set.rb +14 -0
- data/lib/nocode/steps/sleep.rb +14 -0
- data/lib/nocode/util/arrayable.rb +16 -0
- data/lib/nocode/util/class_loader.rb +29 -0
- data/lib/nocode/util/class_registry.rb +56 -0
- data/lib/nocode/util/dictionary.rb +71 -0
- data/lib/nocode/util/object_template.rb +44 -0
- data/lib/nocode/util/optionable.rb +64 -0
- data/lib/nocode/util/string_template.rb +49 -0
- data/lib/nocode/util.rb +8 -0
- data/lib/nocode/version.rb +1 -1
- data/lib/nocode.rb +21 -0
- data/nocode.gemspec +1 -0
- metadata +42 -4
- data/bin/rspec +0 -29
- data/bin/rubocop +0 -29
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 884ab5737629463437ef0991555e1ea30faa8db0cd6d8c79da712942aa69011b
|
4
|
+
data.tar.gz: d8562fdf11b2a204e9ced685b434d6959bafe2dca605dd3795355aeed1ed5238
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 677aa00aba3a998a4cc616002420c9366ac945fabdfc336d948b5158c24eae6c9bfc1b8a7ee658718c5c55af4148c4f83656fddf8b45995002471ef7224870da
|
7
|
+
data.tar.gz: e4de53676eaebb1d9bd3738dbbbb6df777ce0fdf1674e9aa370176c2eca9d42d4672d6edd682a313b943f24d149566491d1d7322d9c30ed0cc352db7730e0f3f
|
@@ -8,7 +8,6 @@ on:
|
|
8
8
|
|
9
9
|
jobs:
|
10
10
|
test:
|
11
|
-
|
12
11
|
runs-on: ubuntu-latest
|
13
12
|
strategy:
|
14
13
|
matrix:
|
@@ -26,3 +25,5 @@ jobs:
|
|
26
25
|
bundler-cache: true # runs 'bundle install' and caches installed gems automatically
|
27
26
|
- name: Run tests
|
28
27
|
run: bundle exec rake
|
28
|
+
- name: Security audit dependencies
|
29
|
+
run: bundle exec bundler-audit check --update
|
data/.rubocop.yml
CHANGED
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,12 @@
|
|
1
|
-
#### February
|
1
|
+
#### 0.0.4 - February 13th, 2022
|
2
|
+
|
3
|
+
* Increase class documentation
|
4
|
+
* Add YAML serialization/deserializationß
|
5
|
+
|
6
|
+
#### 0.0.3 - February 12th, 2022
|
7
|
+
|
8
|
+
* Add initial implementation.
|
9
|
+
|
10
|
+
#### 0.0.0 - February 9th, 2022
|
2
11
|
|
3
12
|
* Establish initial repository and gem infrastructure.
|
data/CODE_OF_CONDUCT.md
ADDED
@@ -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
@@ -1,11 +1,127 @@
|
|
1
1
|
# Nocode
|
2
2
|
|
3
|
-
|
3
|
+
#### Execute Ruby code through YAML
|
4
4
|
|
5
|
-
|
5
|
+
[](https://badge.fury.io/rb/nocode) [](https://github.com/mattruggio/nocode/actions/workflows/rubygem.yml)
|
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
|
|
9
|
-
|
9
|
+
## Installation
|
10
10
|
|
11
|
-
|
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.
|
data/exe/nocode
CHANGED
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
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.
|
6
|
+
class Context
|
7
|
+
attr_reader :io, :parameters, :registers
|
8
|
+
|
9
|
+
def initialize(io: $stdout, parameters: {}, registers: {})
|
10
|
+
@io = io || $stdout
|
11
|
+
@parameters = Util::Dictionary.ensure(parameters)
|
12
|
+
@registers = Util::Dictionary.ensure(registers)
|
13
|
+
|
14
|
+
freeze
|
15
|
+
end
|
16
|
+
|
17
|
+
def register(key)
|
18
|
+
registers[key]
|
19
|
+
end
|
20
|
+
|
21
|
+
def parameter(key)
|
22
|
+
parameters[key]
|
23
|
+
end
|
24
|
+
|
25
|
+
def to_h
|
26
|
+
{
|
27
|
+
'registers' => registers,
|
28
|
+
'parameters' => parameters
|
29
|
+
}
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,84 @@
|
|
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
|
data/lib/nocode/step.rb
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Nocode
|
4
|
+
# Defines a running step. Steps should be sub-classes of this class as well as to implement
|
5
|
+
# #perform.
|
6
|
+
class Step
|
7
|
+
extend Forwardable
|
8
|
+
include Util::Arrayable
|
9
|
+
include Util::Optionable
|
10
|
+
|
11
|
+
attr_reader :context,
|
12
|
+
:name,
|
13
|
+
:options,
|
14
|
+
:type
|
15
|
+
|
16
|
+
def_delegators :context,
|
17
|
+
:io,
|
18
|
+
:parameters,
|
19
|
+
:registers
|
20
|
+
|
21
|
+
def initialize(
|
22
|
+
context: Context.new,
|
23
|
+
name: '',
|
24
|
+
options: {},
|
25
|
+
type: ''
|
26
|
+
)
|
27
|
+
@context = context
|
28
|
+
@options = options
|
29
|
+
@name = name
|
30
|
+
@type = type
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'step'
|
4
|
+
|
5
|
+
# This will execute the StepRegisty's load! method upon script evaluation.
|
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.
|
10
|
+
class StepRegistry < Util::ClassRegistry
|
11
|
+
include Singleton
|
12
|
+
|
13
|
+
PREFIX = 'Nocode::Steps::'
|
14
|
+
DIR = File.join(__dir__, 'steps')
|
15
|
+
|
16
|
+
class << self
|
17
|
+
extend Forwardable
|
18
|
+
|
19
|
+
def_delegators :instance,
|
20
|
+
:register,
|
21
|
+
:constant!,
|
22
|
+
:add,
|
23
|
+
:load!
|
24
|
+
end
|
25
|
+
|
26
|
+
def load!
|
27
|
+
files_loaded = Util::ClassLoader.new(DIR).load!
|
28
|
+
|
29
|
+
# Class the parent to load up the registry with the files we found.
|
30
|
+
load(files_loaded, PREFIX)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
# Call upon class evaluation to autoload all classes.
|
35
|
+
StepRegistry.load!
|
36
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Nocode
|
4
|
+
module Steps
|
5
|
+
# Shallow-copy one register to another.
|
6
|
+
class Copy < Step
|
7
|
+
option :from_register, :to_register
|
8
|
+
|
9
|
+
def perform
|
10
|
+
registers[to_register_option] = registers[from_register_option].dup
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
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 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
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Nocode
|
4
|
+
module Steps
|
5
|
+
module Io
|
6
|
+
# Read a file from disk and place its contents in a register.
|
7
|
+
class Read < Step
|
8
|
+
option :path,
|
9
|
+
:register
|
10
|
+
|
11
|
+
def perform
|
12
|
+
data = File.read(path)
|
13
|
+
|
14
|
+
registers[register_option] = data
|
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,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Nocode
|
4
|
+
module Steps
|
5
|
+
module Io
|
6
|
+
# Write the contents of a register to disk.
|
7
|
+
class Write < Step
|
8
|
+
option :path,
|
9
|
+
:register
|
10
|
+
|
11
|
+
def perform
|
12
|
+
data = registers[register_option]
|
13
|
+
|
14
|
+
FileUtils.mkdir_p(File.dirname(path))
|
15
|
+
|
16
|
+
File.write(path, data)
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def path
|
22
|
+
File.join(*array(path_option))
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,43 @@
|
|
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
|
+
|
31
|
+
true
|
32
|
+
elsif object.respond_to?(:values)
|
33
|
+
csv << object.values
|
34
|
+
|
35
|
+
true
|
36
|
+
else
|
37
|
+
false
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
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
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Nocode
|
4
|
+
module Steps
|
5
|
+
# Set a register's value to the value option specified.
|
6
|
+
class Set < Step
|
7
|
+
option :register, :value
|
8
|
+
|
9
|
+
def perform
|
10
|
+
registers[register_option] = value_option
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Nocode
|
4
|
+
module Util
|
5
|
+
# Loads a directory full of Ruby classes and returns their relative paths.
|
6
|
+
class ClassLoader
|
7
|
+
EXTENSION = '.rb'
|
8
|
+
|
9
|
+
attr_reader :dir
|
10
|
+
|
11
|
+
def initialize(dir)
|
12
|
+
@dir = dir
|
13
|
+
|
14
|
+
freeze
|
15
|
+
end
|
16
|
+
|
17
|
+
def load!
|
18
|
+
Dir[File.join(dir, '**', "*#{EXTENSION}")].sort.map do |step_path|
|
19
|
+
require step_path
|
20
|
+
|
21
|
+
step_path
|
22
|
+
.delete_prefix(dir)
|
23
|
+
.delete_prefix(File::SEPARATOR)
|
24
|
+
.delete_suffix(EXTENSION)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Nocode
|
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.
|
8
|
+
class ClassRegistry
|
9
|
+
extend Forwardable
|
10
|
+
|
11
|
+
class NotRegisteredError < StandardError; end
|
12
|
+
|
13
|
+
attr_reader :types_to_classes
|
14
|
+
|
15
|
+
def_delegators :types_to_classes, :to_s
|
16
|
+
|
17
|
+
def initialize(types_to_classes = {})
|
18
|
+
@types_to_classes = Dictionary.new(types_to_classes)
|
19
|
+
|
20
|
+
freeze
|
21
|
+
end
|
22
|
+
|
23
|
+
def load(types, prefix = '')
|
24
|
+
types.each do |type|
|
25
|
+
pascal_cased = type.split(File::SEPARATOR).map do |part|
|
26
|
+
part.split('_').collect(&:capitalize).join
|
27
|
+
end.join('::')
|
28
|
+
|
29
|
+
register(type, "#{prefix}#{pascal_cased}")
|
30
|
+
end
|
31
|
+
|
32
|
+
self
|
33
|
+
end
|
34
|
+
|
35
|
+
def register(type, class_name)
|
36
|
+
tap { types_to_classes[type] = class_name }
|
37
|
+
end
|
38
|
+
|
39
|
+
def unregister(type)
|
40
|
+
tap { types_to_classes.delete(type) }
|
41
|
+
end
|
42
|
+
|
43
|
+
def constant!(type)
|
44
|
+
name = types_to_classes[type]
|
45
|
+
|
46
|
+
raise NotRegisteredError, "Constant not registered for: #{type}" if name.to_s.empty?
|
47
|
+
|
48
|
+
if Object.const_defined?(name, false)
|
49
|
+
Object.const_get(name, false)
|
50
|
+
else
|
51
|
+
Object.const_missing(name)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Nocode
|
4
|
+
module Util
|
5
|
+
# A hash-like object which ensures all keys are strings.
|
6
|
+
class Dictionary
|
7
|
+
extend Forwardable
|
8
|
+
|
9
|
+
NEWLINE_CHAR = "\n"
|
10
|
+
|
11
|
+
class << self
|
12
|
+
def ensure(value)
|
13
|
+
if value.is_a?(self)
|
14
|
+
value
|
15
|
+
else
|
16
|
+
new(value)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
attr_reader :values
|
22
|
+
|
23
|
+
def_delegators :values, :empty?
|
24
|
+
|
25
|
+
def initialize(values = {})
|
26
|
+
@values = {}
|
27
|
+
|
28
|
+
(values || {}).each { |k, v| assign(k, v) }
|
29
|
+
|
30
|
+
freeze
|
31
|
+
end
|
32
|
+
|
33
|
+
def delete(key)
|
34
|
+
tap { values.delete(keyify(key)) }
|
35
|
+
end
|
36
|
+
|
37
|
+
def []=(key, value)
|
38
|
+
tap { values[keyify(key)] = value }
|
39
|
+
end
|
40
|
+
|
41
|
+
def [](key)
|
42
|
+
values[keyify(key)]
|
43
|
+
end
|
44
|
+
|
45
|
+
def dig(*keys)
|
46
|
+
top_level = keyify(keys.first)
|
47
|
+
keys = [top_level] + keys[1..]
|
48
|
+
|
49
|
+
values.dig(*keys)
|
50
|
+
end
|
51
|
+
|
52
|
+
def key?(key)
|
53
|
+
values.key?(keyify(key))
|
54
|
+
end
|
55
|
+
|
56
|
+
def to_s
|
57
|
+
values.map { |k, v| "#{k}: #{v}" }.join(NEWLINE_CHAR)
|
58
|
+
end
|
59
|
+
|
60
|
+
private
|
61
|
+
|
62
|
+
def assign(key, value)
|
63
|
+
values[keyify(key)] = value
|
64
|
+
end
|
65
|
+
|
66
|
+
def keyify(value)
|
67
|
+
value.to_s
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -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
|
@@ -0,0 +1,64 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Nocode
|
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'
|
19
|
+
module Optionable
|
20
|
+
def self.included(klass)
|
21
|
+
klass.extend(ClassMethods)
|
22
|
+
end
|
23
|
+
|
24
|
+
# Class-level DSL Methods
|
25
|
+
module ClassMethods
|
26
|
+
def option(*values)
|
27
|
+
values.each { |v| options << v.to_s }
|
28
|
+
end
|
29
|
+
|
30
|
+
def options
|
31
|
+
@options ||= []
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
OPTION_PREFIX = '_option'
|
36
|
+
|
37
|
+
def options
|
38
|
+
@options || {}
|
39
|
+
end
|
40
|
+
|
41
|
+
def method_missing(name, *args, &block)
|
42
|
+
key = option_key(name)
|
43
|
+
|
44
|
+
if name.to_s.end_with?(OPTION_PREFIX) && self.class.options.include?(key)
|
45
|
+
options[key]
|
46
|
+
else
|
47
|
+
super
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def respond_to_missing?(name, include_private = false)
|
52
|
+
key = option_key(name)
|
53
|
+
|
54
|
+
(name.to_s.end_with?(OPTION_PREFIX) && self.class.options.include?(key)) || super
|
55
|
+
end
|
56
|
+
|
57
|
+
private
|
58
|
+
|
59
|
+
def option_key(name)
|
60
|
+
name.to_s.gsub(OPTION_PREFIX, '')
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Nocode
|
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!"
|
10
|
+
class StringTemplate
|
11
|
+
LEFT_TOKEN = '<<'
|
12
|
+
RIGHT_TOKEN = '>>'
|
13
|
+
SEPARATOR = '.'
|
14
|
+
REG_EXPR = /#{Regexp.quote(LEFT_TOKEN)}(.*?)#{Regexp.quote(RIGHT_TOKEN)}/.freeze
|
15
|
+
|
16
|
+
attr_reader :expression
|
17
|
+
|
18
|
+
def initialize(expression)
|
19
|
+
@expression = expression.to_s
|
20
|
+
|
21
|
+
freeze
|
22
|
+
end
|
23
|
+
|
24
|
+
def evaluate(values = {})
|
25
|
+
resolved = tokens_to_values(tokens, values)
|
26
|
+
|
27
|
+
tokens.inject(expression) do |memo, token|
|
28
|
+
memo.gsub("#{LEFT_TOKEN}#{token}#{RIGHT_TOKEN}", resolved[token].to_s)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def tokens
|
35
|
+
expression.to_s.scan(REG_EXPR).flatten
|
36
|
+
end
|
37
|
+
|
38
|
+
def tokens_to_values(tokens, values)
|
39
|
+
tokens.each_with_object({}) do |token, memo|
|
40
|
+
cleansed = token.strip
|
41
|
+
parts = cleansed.split(SEPARATOR)
|
42
|
+
value = values.dig(*parts)
|
43
|
+
|
44
|
+
memo[token] = value
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
data/lib/nocode/util.rb
ADDED
data/lib/nocode/version.rb
CHANGED
data/lib/nocode.rb
CHANGED
@@ -1,4 +1,25 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require 'benchmark'
|
4
|
+
require 'csv'
|
5
|
+
require 'fileutils'
|
6
|
+
require 'json'
|
7
|
+
require 'singleton'
|
8
|
+
require 'time'
|
9
|
+
require 'yaml'
|
10
|
+
|
11
|
+
# Util
|
12
|
+
require 'nocode/util'
|
13
|
+
|
14
|
+
# Core
|
15
|
+
require 'nocode/executor'
|
16
|
+
|
17
|
+
# Establish main top-level namespace
|
3
18
|
module Nocode
|
19
|
+
# Default consumer entrypoint into the library.
|
20
|
+
class << self
|
21
|
+
def execute(yaml, io: $stdout)
|
22
|
+
Executor.new(yaml, io: io).execute
|
23
|
+
end
|
24
|
+
end
|
4
25
|
end
|
data/nocode.gemspec
CHANGED
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.
|
4
|
+
version: 0.0.4
|
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-
|
11
|
+
date: 2022-02-13 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,16 +165,40 @@ 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
|
157
172
|
- README.md
|
158
173
|
- Rakefile
|
159
174
|
- bin/console
|
160
|
-
- bin/rspec
|
161
|
-
- bin/rubocop
|
162
175
|
- exe/nocode
|
163
176
|
- lib/nocode.rb
|
177
|
+
- lib/nocode/context.rb
|
178
|
+
- lib/nocode/executor.rb
|
179
|
+
- lib/nocode/step.rb
|
180
|
+
- lib/nocode/step_registry.rb
|
181
|
+
- lib/nocode/steps/copy.rb
|
182
|
+
- lib/nocode/steps/delete.rb
|
183
|
+
- lib/nocode/steps/deserialize/csv.rb
|
184
|
+
- lib/nocode/steps/deserialize/json.rb
|
185
|
+
- lib/nocode/steps/deserialize/yaml.rb
|
186
|
+
- lib/nocode/steps/io/read.rb
|
187
|
+
- lib/nocode/steps/io/write.rb
|
188
|
+
- lib/nocode/steps/log.rb
|
189
|
+
- lib/nocode/steps/serialize/csv.rb
|
190
|
+
- lib/nocode/steps/serialize/json.rb
|
191
|
+
- lib/nocode/steps/serialize/yaml.rb
|
192
|
+
- lib/nocode/steps/set.rb
|
193
|
+
- lib/nocode/steps/sleep.rb
|
194
|
+
- lib/nocode/util.rb
|
195
|
+
- lib/nocode/util/arrayable.rb
|
196
|
+
- lib/nocode/util/class_loader.rb
|
197
|
+
- lib/nocode/util/class_registry.rb
|
198
|
+
- lib/nocode/util/dictionary.rb
|
199
|
+
- lib/nocode/util/object_template.rb
|
200
|
+
- lib/nocode/util/optionable.rb
|
201
|
+
- lib/nocode/util/string_template.rb
|
164
202
|
- lib/nocode/version.rb
|
165
203
|
- nocode.gemspec
|
166
204
|
homepage: https://github.com/mattruggio/nocode
|
data/bin/rspec
DELETED
@@ -1,29 +0,0 @@
|
|
1
|
-
#!/usr/bin/env ruby
|
2
|
-
# frozen_string_literal: true
|
3
|
-
|
4
|
-
#
|
5
|
-
# This file was generated by Bundler.
|
6
|
-
#
|
7
|
-
# The application 'rspec' is installed as part of a gem, and
|
8
|
-
# this file is here to facilitate running it.
|
9
|
-
#
|
10
|
-
|
11
|
-
require 'pathname'
|
12
|
-
ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile',
|
13
|
-
Pathname.new(__FILE__).realpath)
|
14
|
-
|
15
|
-
bundle_binstub = File.expand_path('bundle', __dir__)
|
16
|
-
|
17
|
-
if File.file?(bundle_binstub)
|
18
|
-
if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/
|
19
|
-
load(bundle_binstub)
|
20
|
-
else
|
21
|
-
abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
|
22
|
-
Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
|
23
|
-
end
|
24
|
-
end
|
25
|
-
|
26
|
-
require 'rubygems'
|
27
|
-
require 'bundler/setup'
|
28
|
-
|
29
|
-
load Gem.bin_path('rspec-core', 'rspec')
|
data/bin/rubocop
DELETED
@@ -1,29 +0,0 @@
|
|
1
|
-
#!/usr/bin/env ruby
|
2
|
-
# frozen_string_literal: true
|
3
|
-
|
4
|
-
#
|
5
|
-
# This file was generated by Bundler.
|
6
|
-
#
|
7
|
-
# The application 'rubocop' is installed as part of a gem, and
|
8
|
-
# this file is here to facilitate running it.
|
9
|
-
#
|
10
|
-
|
11
|
-
require 'pathname'
|
12
|
-
ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile',
|
13
|
-
Pathname.new(__FILE__).realpath)
|
14
|
-
|
15
|
-
bundle_binstub = File.expand_path('bundle', __dir__)
|
16
|
-
|
17
|
-
if File.file?(bundle_binstub)
|
18
|
-
if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/
|
19
|
-
load(bundle_binstub)
|
20
|
-
else
|
21
|
-
abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
|
22
|
-
Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
|
23
|
-
end
|
24
|
-
end
|
25
|
-
|
26
|
-
require 'rubygems'
|
27
|
-
require 'bundler/setup'
|
28
|
-
|
29
|
-
load Gem.bin_path('rubocop', 'rubocop')
|