nocode 0.0.0 → 0.0.1

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: e3f1a62969f19767ac717993632a0d51785969482b172c6e33cbfc94cfd13ce3
4
+ data.tar.gz: 76e62efa73b9d7489cea1bb7825df6ed490439de097f3b3abb404a8f74f98d63
5
5
  SHA512:
6
- metadata.gz: df2518dc919e0fd84eac03c97b77f27fc125bfcea83f17dd786d0539c4907e093f2ede6f3c0c636c3033e4f515aa7c4a06c982f782c3a149286b2eed0deec7f3
7
- data.tar.gz: b736b5e447a1445dda04245f6d1a86f481d818e71f16f67b850705ad1efcf937ab4db154cb0abfdcbd2bded999a21164fc5ff6f8a29c2aff2ead40a2d1af29ba
6
+ metadata.gz: f64bfa9cc952d58d9a5d37c5ac1045fae4e5b63d1f9bac5f5ac2522420790dc0003b878e52c009bc275960acf39732ccfd65a9d3c68d223954bd4ff56ba30eb7
7
+ data.tar.gz: 317d4ef406a064b2160247475a30911eaed26ac8d009a435c3ec9abe95ee91d64a88729a7146d3e19f7ab719e405b69b42dcaddf2fa928e0ef9d9fa014574b47
@@ -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:
data/.rubocop.yml CHANGED
@@ -6,8 +6,16 @@ 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
17
  Style/Documentation:
13
18
  Enabled: false
19
+
20
+ RSpec/ExampleLength:
21
+ Max: 10
data/README.md CHANGED
@@ -1,11 +1,9 @@
1
1
  # Nocode
2
2
 
3
- ## Execute Ruby code through YAML
4
-
5
- **Warning**: This library is currently experimental.
6
-
7
- ---
3
+ #### Execute Ruby code through YAML
8
4
 
9
5
  [![Ruby Gem CI](https://github.com/mattruggio/nocode/actions/workflows/rubygem.yml/badge.svg)](https://github.com/mattruggio/nocode/actions/workflows/rubygem.yml)
10
6
 
7
+ **Warning**: This library is currently experimental.
8
+
11
9
  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.
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,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nocode
4
+ class Context
5
+ attr_reader :io, :parameters, :registers
6
+
7
+ def initialize(io: $stdout, parameters: {}, registers: {})
8
+ @io = io || $stdout
9
+ @parameters = Util::Dictionary.ensure(parameters)
10
+ @registers = Util::Dictionary.ensure(registers)
11
+
12
+ freeze
13
+ end
14
+
15
+ def register(key)
16
+ registers[key]
17
+ end
18
+
19
+ def parameter(key)
20
+ parameters(key)
21
+ end
22
+
23
+ def to_h
24
+ {
25
+ 'registers' => registers,
26
+ 'parameters' => parameters
27
+ }
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'context'
4
+ require_relative 'options_template'
5
+ require_relative 'step_registry'
6
+
7
+ module Nocode
8
+ class Executor
9
+ attr_reader :yaml, :io
10
+
11
+ def initialize(yaml, io: $stdout)
12
+ @yaml = yaml.respond_to?(:read) ? yaml.read : yaml
13
+ @yaml = YAML.safe_load(@yaml) || {}
14
+ @io = io
15
+
16
+ freeze
17
+ end
18
+
19
+ def execute
20
+ steps = yaml['steps'] || []
21
+ parameters = yaml['parameters'] || {}
22
+ context = Context.new(io: io, parameters: parameters)
23
+
24
+ log_title
25
+
26
+ steps.each do |step|
27
+ step_instance = make_step(step, context)
28
+
29
+ execute_step(step_instance)
30
+ end
31
+
32
+ log("Ended: #{DateTime.now}")
33
+ log_line
34
+
35
+ context
36
+ end
37
+
38
+ private
39
+
40
+ def make_step(step, context)
41
+ 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)
46
+
47
+ step_class.new(
48
+ options: Util::Dictionary.new(compiled_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,34 @@
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
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nocode
4
+ class Step
5
+ extend Forwardable
6
+ include Util::Arrayable
7
+ include Util::Optionable
8
+
9
+ attr_reader :name, :context, :options, :type
10
+
11
+ def_delegators :context, :parameters, :registers, :io
12
+
13
+ def initialize(
14
+ context: Context.new,
15
+ name: '',
16
+ options: {},
17
+ type: ''
18
+ )
19
+ @context = context
20
+ @options = options
21
+ @name = name
22
+ @type = type
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'step'
4
+
5
+ module Nocode
6
+ class StepRegistry < Util::ClassRegistry
7
+ include Singleton
8
+
9
+ PREFIX = 'Nocode::Steps::'
10
+ DIR = File.join(__dir__, 'steps')
11
+
12
+ class << self
13
+ extend Forwardable
14
+
15
+ def_delegators :instance,
16
+ :register,
17
+ :constant!,
18
+ :add,
19
+ :load!
20
+ end
21
+
22
+ def load!
23
+ files_loaded = Util::ClassLoader.new(DIR).load!
24
+
25
+ load(files_loaded, PREFIX)
26
+ end
27
+ end
28
+
29
+ StepRegistry.load!
30
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nocode
4
+ module Steps
5
+ class Copy < Step
6
+ option :from_register, :to_register
7
+
8
+ def perform
9
+ registers[to_register_option] = registers[from_register_option]
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nocode
4
+ module Steps
5
+ class Delete < Step
6
+ option :register
7
+
8
+ def perform
9
+ registers.delete(register_option)
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nocode
4
+ module Steps
5
+ module Io
6
+ class Read < Step
7
+ option :path,
8
+ :register
9
+
10
+ def perform
11
+ data = File.read(path)
12
+
13
+ registers[register_option] = data
14
+ end
15
+
16
+ private
17
+
18
+ def path
19
+ File.join(*array(path_option))
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nocode
4
+ module Steps
5
+ module Io
6
+ class Write < Step
7
+ option :path,
8
+ :register
9
+
10
+ def perform
11
+ data = registers[register_option]
12
+
13
+ FileUtils.mkdir_p(File.dirname(path))
14
+
15
+ File.write(path, data)
16
+ end
17
+
18
+ private
19
+
20
+ def path
21
+ File.join(*array(path_option))
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nocode
4
+ module Steps
5
+ class Log < Step
6
+ option :message
7
+
8
+ def perform
9
+ io.puts(message_option.to_s)
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nocode
4
+ module Steps
5
+ class Set < Step
6
+ option :register, :value
7
+
8
+ def perform
9
+ registers[register_option] = value_option
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nocode
4
+ module Steps
5
+ class Sleep < Step
6
+ option :seconds
7
+
8
+ def perform
9
+ Kernel.sleep(seconds_option.to_f)
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nocode
4
+ module Util
5
+ module Arrayable
6
+ def array(value)
7
+ if value.is_a?(Hash)
8
+ [value]
9
+ else
10
+ Array(value)
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nocode
4
+ module Util
5
+ class ClassLoader
6
+ EXTENSION = '.rb'
7
+
8
+ attr_reader :dir
9
+
10
+ def initialize(dir)
11
+ @dir = dir
12
+
13
+ freeze
14
+ end
15
+
16
+ def load!
17
+ Dir[File.join(dir, '**', "*#{EXTENSION}")].sort.map do |step_path|
18
+ require step_path
19
+
20
+ step_path
21
+ .delete_prefix(dir)
22
+ .delete_prefix(File::SEPARATOR)
23
+ .delete_suffix(EXTENSION)
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nocode
4
+ module Util
5
+ class ClassRegistry
6
+ extend Forwardable
7
+
8
+ class NotRegisteredError < StandardError; end
9
+
10
+ attr_reader :types_to_classes
11
+
12
+ def_delegators :types_to_classes, :to_s
13
+
14
+ def initialize(types_to_classes = {})
15
+ @types_to_classes = Dictionary.new(types_to_classes)
16
+
17
+ freeze
18
+ end
19
+
20
+ def load(types, prefix = '')
21
+ types.each do |type|
22
+ pascal_cased = type.split(File::SEPARATOR).map do |part|
23
+ part.split('_').collect(&:capitalize).join
24
+ end.join('::')
25
+
26
+ register(type, "#{prefix}#{pascal_cased}")
27
+ end
28
+
29
+ self
30
+ end
31
+
32
+ def register(type, class_name)
33
+ tap { types_to_classes[type] = class_name }
34
+ end
35
+
36
+ def unregister(type)
37
+ tap { types_to_classes.delete(type) }
38
+ end
39
+
40
+ def constant!(type)
41
+ name = types_to_classes[type]
42
+
43
+ raise NotRegisteredError, "Constant not registered for: #{type}" if name.to_s.empty?
44
+
45
+ if Object.const_defined?(name, false)
46
+ Object.const_get(name, false)
47
+ else
48
+ Object.const_missing(name)
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nocode
4
+ module Util
5
+ class Dictionary
6
+ extend Forwardable
7
+
8
+ NEWLINE_CHAR = "\n"
9
+
10
+ class << self
11
+ def ensure(value)
12
+ if value.is_a?(self)
13
+ value
14
+ else
15
+ new(value)
16
+ end
17
+ end
18
+ end
19
+
20
+ attr_reader :values
21
+
22
+ def_delegators :values, :empty?
23
+
24
+ def initialize(values = {})
25
+ @values = {}
26
+
27
+ (values || {}).each { |k, v| assign(k, v) }
28
+
29
+ freeze
30
+ end
31
+
32
+ def delete(key)
33
+ tap { values.delete(keyify(key)) }
34
+ end
35
+
36
+ def []=(key, value)
37
+ tap { values[keyify(key)] = value }
38
+ end
39
+
40
+ def [](key)
41
+ values[keyify(key)]
42
+ end
43
+
44
+ def dig(*keys)
45
+ top_level = keyify(keys.first)
46
+ keys = [top_level] + keys[1..]
47
+
48
+ values.dig(*keys)
49
+ end
50
+
51
+ def key?(key)
52
+ values.key?(keyify(key))
53
+ end
54
+
55
+ def to_s
56
+ values.map { |k, v| "#{k}: #{v}" }.join(NEWLINE_CHAR)
57
+ end
58
+
59
+ private
60
+
61
+ def assign(key, value)
62
+ values[keyify(key)] = value
63
+ end
64
+
65
+ def keyify(value)
66
+ value.to_s
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nocode
4
+ module Util
5
+ module Optionable
6
+ def self.included(klass)
7
+ klass.extend(ClassMethods)
8
+ end
9
+
10
+ # Class-level DSL Methods
11
+ module ClassMethods
12
+ def option(*values)
13
+ values.each { |v| options << v.to_s }
14
+ end
15
+
16
+ def options
17
+ @options ||= []
18
+ end
19
+ end
20
+
21
+ OPTION_PREFIX = '_option'
22
+
23
+ def options
24
+ @options || {}
25
+ end
26
+
27
+ def method_missing(name, *args, &block)
28
+ key = option_key(name)
29
+
30
+ if name.to_s.end_with?(OPTION_PREFIX) && self.class.options.include?(key)
31
+ options[key]
32
+ else
33
+ super
34
+ end
35
+ end
36
+
37
+ def respond_to_missing?(name, include_private = false)
38
+ key = option_key(name)
39
+
40
+ (name.to_s.end_with?(OPTION_PREFIX) && self.class.options.include?(key)) || super
41
+ end
42
+
43
+ private
44
+
45
+ def option_key(name)
46
+ name.to_s.gsub(OPTION_PREFIX, '')
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nocode
4
+ module Util
5
+ class StringTemplate
6
+ LEFT_TOKEN = '<<'
7
+ RIGHT_TOKEN = '>>'
8
+ SEPARATOR = '.'
9
+ REG_EXPR = /#{Regexp.quote(LEFT_TOKEN)}(.*?)#{Regexp.quote(RIGHT_TOKEN)}/.freeze
10
+
11
+ attr_reader :expression
12
+
13
+ def initialize(expression)
14
+ @expression = expression
15
+
16
+ freeze
17
+ end
18
+
19
+ def evaluate(values = {})
20
+ resolved = tokens_to_values(tokens, values)
21
+
22
+ tokens.inject(expression) do |memo, token|
23
+ memo.gsub("#{LEFT_TOKEN}#{token}#{RIGHT_TOKEN}", resolved[token].to_s)
24
+ end
25
+ end
26
+
27
+ private
28
+
29
+ def tokens
30
+ expression.to_s.scan(REG_EXPR).flatten
31
+ end
32
+
33
+ def tokens_to_values(tokens, values)
34
+ tokens.each_with_object({}) do |token, memo|
35
+ cleansed = token.strip
36
+ parts = cleansed.split(SEPARATOR)
37
+ value = values.dig(*parts)
38
+
39
+ memo[token] = value
40
+ end
41
+ end
42
+ end
43
+ end
44
+ 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/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.0'
4
+ VERSION = '0.0.1'
5
5
  end
data/lib/nocode.rb CHANGED
@@ -1,4 +1,21 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'benchmark'
4
+ require 'fileutils'
5
+ require 'singleton'
6
+ require 'time'
7
+ require 'yaml'
8
+
9
+ # Util
10
+ require 'nocode/util'
11
+
12
+ # Core
13
+ require 'nocode/executor'
14
+
3
15
  module Nocode
16
+ class << self
17
+ def execute(yaml, io: $stdout)
18
+ Executor.new(yaml, io: io).execute
19
+ end
20
+ end
4
21
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: nocode
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.0
4
+ version: 0.0.1
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
13
  - !ruby/object:Gem::Dependency
14
14
  name: guard-rspec
@@ -157,10 +157,27 @@ files:
157
157
  - README.md
158
158
  - Rakefile
159
159
  - bin/console
160
- - bin/rspec
161
- - bin/rubocop
162
160
  - exe/nocode
163
161
  - lib/nocode.rb
162
+ - lib/nocode/context.rb
163
+ - lib/nocode/executor.rb
164
+ - lib/nocode/options_template.rb
165
+ - lib/nocode/step.rb
166
+ - lib/nocode/step_registry.rb
167
+ - lib/nocode/steps/copy.rb
168
+ - lib/nocode/steps/delete.rb
169
+ - lib/nocode/steps/io/read.rb
170
+ - lib/nocode/steps/io/write.rb
171
+ - lib/nocode/steps/log.rb
172
+ - lib/nocode/steps/set.rb
173
+ - lib/nocode/steps/sleep.rb
174
+ - lib/nocode/util.rb
175
+ - lib/nocode/util/arrayable.rb
176
+ - lib/nocode/util/class_loader.rb
177
+ - lib/nocode/util/class_registry.rb
178
+ - lib/nocode/util/dictionary.rb
179
+ - lib/nocode/util/optionable.rb
180
+ - lib/nocode/util/string_template.rb
164
181
  - lib/nocode/version.rb
165
182
  - nocode.gemspec
166
183
  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')