lib/gladys/script.rb



# frozen_string_literal: true

module Gladys
  class Script
    attr_reader :path

    def self.all
      Dir.glob(File.join(__dir__, "..", "..", "scripts", "*.rb")).map do |path|
        load(path.split("/").last.split(".").first)
      end
    end

    def self.load(path, option_set: nil)
      code = if File.exist?(path)
               File.read(path)
             elsif File.exist?(File.join(__dir__, "..", "..", "scripts", "#{path}.rb"))
               File.read(File.join(__dir__, "..", "..", "scripts", "#{path}.rb"))
             else
               raise Errors::ScriptNotFound,
                     "Script #{File.join(__dir__, '..', '..', 'scripts', "#{path}.rb")} not found."
             end

      new(path, code, option_set: option_set).tap(&:validate_provided_options!)
    end

    def initialize(path, code, option_set: nil)
      @path = path
      @option_set = option_set
      @blocks = {
        cleanup: proc { |*| },
        prepare: proc { |*| },
        benchmark: proc { |*| },
        create_tables: proc { |*| },
        create_indexes: proc { |*| },
        helpers: proc { |*| },
        before: Concurrent::Hash.new,
        after: Concurrent::Hash.new,
        inputs: Concurrent::Hash.new,
        description: proc { |*| "TODO" }
      }
      @definitions = {
        preload_scale: 200_000,
        insert_batch_size: 5_000,
        database_size: nil
      }
      @option_sets = {}

      eval_code(code)
    end

    def describe
      @blocks[:description].call
    end

    def option_sets
      @option_sets.values
    end

    def validate_provided_options!
      return unless @option_set && !@option_sets.key?(@option_set.to_sym)

      raise Errors::InvalidOptionSet, "--option-set #{@option_set} is not valid."
    end

    def script_name
      @path.split("/").last.split(".").first
    end

    def name
      name = @path.split("/").last.split(".").first
      name = @display_name if @display_name
      name
    end

    def running_version
      @version || "unknown"
    end

    def helpers_block
      @blocks[:helpers]
    end

    def action_block(action)
      @blocks[action]
    end

    def before_block(action)
      @blocks[:before][action] || proc { |*| }
    end

    def after_block(action)
      @blocks[:after][action] || proc { |*| }
    end

    def defined_inputs
      @blocks[:inputs].keys
    end

    def preload_inputs?
      defined_inputs.any? { |input| preload_input?(input) }
    end

    def preload_input?(name)
      (@blocks[:inputs][name] || {}).fetch(:preload, false)
    end

    def inputs_block(name)
      (@blocks[:inputs][name] || {}).fetch(:block, proc { |*| })
    end

    # Check for the provided option set, or default to "default" (if it exists)
    # and run the block to get the options.
    def defined_options
      @defined_options ||= begin
        block = @option_sets[@option_set.to_sym] if @option_set
        block ||= @option_sets[:default]
        block&.call

        Struct.new(*@definitions.keys).new(*@definitions.values)
      end
    end

    def option(name, value: nil)
      @definitions[name] = value || yield
    end

    def option_set(name, description = "", &)
      @option_sets[name] = OptionSet.new(name, description, &)
    end

    def define_input(name, preload: false, &block)
      @blocks[:inputs][name] = {
        preload: preload,
        block: block
      }
    end

    def cleanup(&block)
      @blocks[:cleanup] = block
    end

    def prepare(&block)
      @blocks[:prepare] = block
    end

    def benchmark(&block)
      @blocks[:benchmark] = block
    end

    def create_tables(&block)
      @blocks[:create_tables] = block
    end

    def create_indexes(&block)
      @blocks[:create_indexes] = block
    end

    def helpers(&block)
      @blocks[:helpers] = block
    end

    def before(action, &block)
      @blocks[:before][action] = block
    end

    def after(action, &block)
      @blocks[:after][action] = block
    end

    def display_name(name)
      @display_name = name
    end

    def version(version)
      @version = version
    end

    def description(&block)
      @blocks[:description] = block
    end

    private

    # Eval in as much isolation as possible.
    # TODO: Improve this somewhat.
    def eval_code(code)
      eval(code, binding, @path)
    end
  end
end