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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1118aa168a7f6135326ca4479e6c3ce85c366ef0e9c280ec2ac96f242a25c717
4
- data.tar.gz: 054200f0d28e69a9b725c9ecc0a34f78e09f8a610e3b66f11d64cd7455b41217
3
+ metadata.gz: 884ab5737629463437ef0991555e1ea30faa8db0cd6d8c79da712942aa69011b
4
+ data.tar.gz: d8562fdf11b2a204e9ced685b434d6959bafe2dca605dd3795355aeed1ed5238
5
5
  SHA512:
6
- metadata.gz: df2518dc919e0fd84eac03c97b77f27fc125bfcea83f17dd786d0539c4907e093f2ede6f3c0c636c3033e4f515aa7c4a06c982f782c3a149286b2eed0deec7f3
7
- data.tar.gz: b736b5e447a1445dda04245f6d1a86f481d818e71f16f67b850705ad1efcf937ab4db154cb0abfdcbd2bded999a21164fc5ff6f8a29c2aff2ead40a2d1af29ba
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
@@ -6,8 +6,13 @@ AllCops:
6
6
  NewCops: enable
7
7
  TargetRubyVersion: 2.6
8
8
 
9
+ Metrics/BlockLength:
10
+ Max: 30
11
+ IgnoredMethods:
12
+ - describe
13
+
9
14
  Metrics/MethodLength:
10
15
  Max: 20
11
16
 
12
- Style/Documentation:
13
- Enabled: false
17
+ RSpec/ExampleLength:
18
+ Max: 10
data/CHANGELOG.md CHANGED
@@ -1,3 +1,12 @@
1
- #### February 9th, 2022
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.
@@ -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
- ## Execute Ruby code through YAML
3
+ #### Execute Ruby code through YAML
4
4
 
5
- **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)
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
- [![Ruby Gem CI](https://github.com/mattruggio/nocode/actions/workflows/rubygem.yml/badge.svg)](https://github.com/mattruggio/nocode/actions/workflows/rubygem.yml)
9
+ ## Installation
10
10
 
11
- 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.
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
@@ -11,4 +11,4 @@ if path.to_s.empty?
11
11
  exit
12
12
  end
13
13
 
14
- puts '<Implementation goes here>'
14
+ Nocode.execute(Pathname.new(path))
@@ -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
@@ -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,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nocode
4
+ module Steps
5
+ # Remove a register completely.
6
+ class Delete < Step
7
+ option :register
8
+
9
+ def perform
10
+ registers.delete(register_option)
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,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nocode
4
+ module Steps
5
+ # Simply output the passed in message into the outputted log.
6
+ class Log < Step
7
+ option :message
8
+
9
+ def perform
10
+ io.puts(message_option.to_s)
11
+ end
12
+ end
13
+ end
14
+ 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,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nocode
4
+ module Steps
5
+ # Sleep for an arbitrary number of seconds.
6
+ class Sleep < Step
7
+ option :seconds
8
+
9
+ def perform
10
+ Kernel.sleep(seconds_option.to_f)
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nocode
4
+ module Util
5
+ # Hand
6
+ module Arrayable
7
+ def array(value)
8
+ if value.is_a?(Hash)
9
+ [value]
10
+ else
11
+ Array(value)
12
+ end
13
+ end
14
+ end
15
+ end
16
+ 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
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'util/arrayable'
4
+ require_relative 'util/class_loader'
5
+ require_relative 'util/class_registry'
6
+ require_relative 'util/dictionary'
7
+ require_relative 'util/object_template'
8
+ require_relative 'util/optionable'
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Nocode
4
- VERSION = '0.0.0'
4
+ VERSION = '0.0.4'
5
5
  end
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
@@ -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.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-09 00:00:00.000000000 Z
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')