nocode 0.0.0 → 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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')