module Reflekt

Access variables via one object to avoid polluting the caller's scope.

@pattern Singleton

@note Variables not accessed via Accessor:

- @reflekt_counts on the instance
- @@reflekt_skipped_methods on the instance's singleton class

A shadow action.

@hierachy

1. Action <- YOU ARE HERE
2. Reflection
3. Meta

Track the actions in a shadow call stack.

@pattern Stack

A clone of the instance that a reflection calls methods on, as well as any other instances that those methods may lead to.

@note

Not currently in use due to bug where "send" needs to be called directly
on object, not indirectly through clone which results in "undefined method".

@hierachy

1. Action
2. Reflection
3. Clone <- YOU ARE HERE

Metadata for input and output.

@pattern Abstract class @see lib/meta for each meta.

@hierachy

1. Action
2. Reflection
3. Meta <- YOU ARE HERE

A pattern that metadata follows.

@pattern Abstract class

@hierachy

1. RuleSetAggregator
2. RuleSet
3. Rule <- YOU ARE HERE

@see lib/rules for rules.

Public Class Methods

configure() { |config| ... } click to toggle source

Configure Config singleton.

# File lib/reflekt.rb, line 172
def self.configure
  reflekt_setup_class()

  yield(@@reflekt.config)
end
count(klass, method) click to toggle source
# File lib/reflekt.rb, line 159
def self.count(klass, method)
  count = @@reflekt.counts.dig(klass.object_id, method) || 0
  count
end
get_methods(klass) click to toggle source

Get child and parent instance methods.

TODO: Include methods from all ancestors. TODO: Include core methods like β€œArray.include?”.

# File lib/reflekt.rb, line 62
def self.get_methods(klass)
  child_instance_methods = klass.class.instance_methods(false)
  parent_instance_methods = klass.class.superclass.instance_methods(false)

  return child_instance_methods + parent_instance_methods
end
increase_count(klass, method) click to toggle source
# File lib/reflekt.rb, line 164
def self.increase_count(klass, method)
  caller_id = klass.object_id
  @@reflekt.counts[caller_id][method] = @@reflekt.counts[caller_id][method] + 1
end
new(*args) click to toggle source

Setup Reflekt per class. Override methods on class instantiation.

@scope self [Object] Refers to the class that Reflekt is prepended to.

Calls superclass method
# File lib/reflekt.rb, line 37
def initialize(*args)
  if @@reflekt.config.enabled
    @reflekt_initialized = false

    πŸ”₯"Initialize", :info, :setup, self.class

    # Override methods.
    Reflekt.get_methods(self).each do |method|
      Reflekt.setup_count(self, method)
      Reflekt.override_method(self, method)
    end

    @reflekt_initialized = true
  end

  # Continue initialization.
  super
end
override_method(klass, method) click to toggle source

Override a method.

@param klass [Dynamic] The class to override. @param method [Method] The method to override.

Calls superclass method
# File lib/reflekt.rb, line 75
def self.override_method(klass, method)
  klass.define_singleton_method(method) do |*args|

    # When method called in flow.
    if @reflekt_initialized

      ##
      # Reflect-Execute loop.
      #
      # Reflect each method before finally executing it.
      #
      # @loop
      #   1. The first method call creates an action
      #   2. The action creates reflections and calls the method again
      #   3. Subsequent method calls execute these reflections
      #   4. Each reflection executes on cloned data
      #   5. The original method call completes execution
      #
      # @see https://reflekt.dev/docs/reflect-execute-loop
      ##

      unless @@reflekt.error

        action = @@reflekt.stack.peek()

        # New action when old action done reflecting.
        if action.nil? || action.has_finished_loop?
          πŸ”₯"^ Create action for #{method}()", :info, :action, klass.class
          action = Action.new(klass, method, @@reflekt.config, @@reflekt.db, @@reflekt.stack, @@reflekt.aggregator)
          @@reflekt.stack.push(action)
        end

        ##
        # REFLECT
        ##

        unless action.is_reflecting? && klass.class.reflekt_skipped?(method) || Reflekt.count(klass, method) >= @@reflekt.config.reflect_limit
          unless action.is_actioned?
            action.is_actioned = true
            action.is_reflecting = true

            action.reflect(*args)
            if action.control.status == :error
              @@reflekt.error = action.control.message
            end

            # Render results.
            @@reflekt.renderer.render()

            action.is_reflecting = false
          end
        else
          πŸ”₯"> Skip reflection of #{method}()", :skip, :reflect, klass.class
        end

        ##
        # EXECUTE
        ##

        unless action.is_reflecting? && klass.class.reflekt_skipped?(method)
          πŸ”₯"> Execute #{method}()", :info, :execute, klass.class
          super *args
        end

      # Finish execution if control encounters unrecoverable error.
      else
        πŸ”₯"Reflection error, finishing original execution...", :error, :reflect, klass.class
        super *args
      end

    # When method called in constructor.
    else
      p "Reflection unsupported in constructor for #{method}()", :info, :setup, klass.class
      super *args
    end
  end
end
setup_count(klass, method) click to toggle source
# File lib/reflekt.rb, line 153
def self.setup_count(klass, method)
  caller_id = klass.object_id
  @@reflekt.counts[caller_id] = {} unless @@reflekt.counts.has_key? caller_id
  @@reflekt.counts[caller_id][method] = 0 unless @@reflekt.counts[caller_id].has_key? method
end

Private Class Methods

prepended(base) click to toggle source
# File lib/reflekt.rb, line 180
def self.prepended(base)
  # Prepend class methods to the instance's singleton class.
  base.singleton_class.prepend(SingletonClassMethods)

  reflekt_setup_class()
end
reflekt_setup_class() click to toggle source

Setup class.

@paths

- package_path [String] Absolute path to the library itself.
- project_path [String] Absolute path to the project root.
- output_path [String] Absolute path to the reflections directory.
# File lib/reflekt.rb, line 195
def self.reflekt_setup_class()

  # Only setup once.
  return if defined? @@reflekt

  @@reflekt = Accessor.new()
  @@reflekt.config = Config.new()
  @@reflekt.stack = ActionStack.new()

  # Setup paths.
  @@reflekt.package_path = File.dirname(File.realpath(__FILE__))
  @@reflekt.project_path = @@reflekt.config.project_path
  @@reflekt.output_path = File.join(@@reflekt.project_path, @@reflekt.config.output_directory)
  unless Dir.exist? @@reflekt.output_path
    Dir.mkdir(@@reflekt.output_path)
  end

  # Setup database.
  @@reflekt.db = Rowdb.new(@@reflekt.output_path + '/db.js')
  @@reflekt.db.defaults({ :reflekt => { :api_version => 1 }})
  # TODO: Fix Rowdb.get(path) not returning values at path after Rowdb.push()
  db = @@reflekt.db.value()

  # Train aggregated rule sets.
  @@reflekt.aggregator = RuleSetAggregator.new(@@reflekt.config.meta_map)
  @@reflekt.aggregator.train(db[:controls])

  # Setup renderer.
  @@reflekt.renderer = Renderer.new(@@reflekt.package_path, @@reflekt.output_path)

  LitCLI.configure do |config|
    config.statuses = {
      :info => { icon: "β„Ή", color: :blue, styles: [:upcase] },
      :pass => { icon: "βœ”", color: :green, styles: [:upcase] },
      :save => { icon: "βœ”", color: :green, styles: [:upcase] },
      :skip => { icon: "β¨―", color: :yellow, styles: [:upcase] },
      :warn => { icon: "⚠", color: :yellow, styles: [:upcase] },
      :fail => { icon: "β¨―", color: :red, styles: [:upcase] },
      :error => { icon: "!", color: :red, styles: [:upcase] },
      :debug => { icon: "?", color: :purple, styles: [:upcase] },
    }
    config.types = {
      :setup => { styles: [:dim, :bold, :capitalize] },
      :event => { color: :yellow, styles: [:bold, :capitalize] },
      :reflect => { color: :yellow, styles: [:bold, :capitalize] },
      :result => { color: :yellow, styles: [:bold, :capitalize] },
      :action => { color: :red, styles: [:bold, :capitalize] },
      :control => { color: :blue, styles: [:bold, :capitalize] },
      :experiment => { color: :green, styles: [:bold, :capitalize] },
      :execute => { color: :purple, styles: [:bold, :capitalize] },
      :meta => { color: :blue, styles: [:bold, :capitalize] },
    }
  end

  return true
end