lib/ruby_report/report.rb



# frozen_string_literal: true

module RubyReport
  class Report
    class << self
      attr_reader :columns_set, :decorators, :formatters

      def columns(*keys, decorators: ::RubyReport::Decorator, formatters: ::RubyReport::Formatter)
        @columns_set = Set.new(keys)
        @decorators = decorators
        @formatters = formatters
      end
    end

    def initialize(
      data:, scope: nil, columns: nil,
      header_builder: ::RubyReport::Builder::DefaultHeaderBuilder,
      row_builder: ::RubyReport::Builder::DefaultRowBuilder,
      row_resolver: ->(row) { row }
    )
      @data = data
      @scope = scope
      @columns =
        if columns.nil?
          self.class.columns_set || raise("Columns not defined")
        else
          Set.new(columns)
        end
      @header_builder = header_builder
      @row_builder = row_builder
      @row_resolver = row_resolver
      @reports = [self]
    end

    def add_report(report)
      reports << report
    end

    def prepend_report(report)
      reports.unshift(report)
    end

    def header
      @header ||= reports.flat_map do |report|
        if report == self
          report.build_header
        else
          report.header
        end
      end
    end

    def rows
      @rows ||= each_row.to_a
    end

    def each_row
      return enum_for(:each_row) unless block_given?

      method =
        if data.respond_to?(:find_each)
          :find_each
        else
          :each
        end

      data.public_send(method) do |row|
        yield collect_row(row)
      end
    end

    def to_with(generator, **opts)
      report = generator.new
      report.add_report(self, **opts)
      report.generate
    end

    [:hash, :csv, :xlsx].each do |type|
      define_method("to_#{type}") do |**opts|
        require "ruby_report/generator/#{type}"

        to_with(
          Object.const_get("::RubyReport::Generator::#{type.to_s.capitalize}"),
          **opts
        )
      end
    end

    alias_method :to_h, :to_hash

    def build_header
      columns.map do |key|
        header_builder.call(key, self)
      end
    end

    def build_row(row)
      current_row = row_resolver.call(row)

      return [] unless current_row

      Array(decorators).each { |decorator| current_row = decorator.new(current_row, scope) }
      Array(formatters).each { |formatter| current_row = formatter.new(current_row, scope) }

      columns.map do |key|
        row_builder.call(current_row, key, self)
      end
    end

    def collect_row(row)
      result_row = []

      reports.each do |report|
        result_row +=
          if report == self
            report.build_row(row)
          else
            report.collect_row(row)
          end
      end

      result_row
    end

    private

    attr_reader :data, :scope, :columns, :header_builder, :row_builder, :row_resolver, :reports

    def decorators
      self.class.decorators
    end

    def formatters
      self.class.formatters
    end
  end
end