oivoodoo/mongoid-report

View on GitHub
lib/mongoid/report.rb

Summary

Maintainability
A
2 hrs
Test Coverage
require 'active_support/concern'
require 'active_support/core_ext/class/attribute'

require_relative 'report/config'
require_relative 'report/queries_builder'
require_relative 'report/attach_proxy'
require_relative 'report/collection'
require_relative 'report/batches'
require_relative 'report/merger'
require_relative 'report/collections'
require_relative 'report/output'
require_relative 'report/input'
require_relative 'report/scope'
require_relative 'report/scope_collection'
require_relative 'report/report_proxy'

module Mongoid
  module Report
    extend ActiveSupport::Concern

    included do
      extend ClassMethods

      class_attribute :settings

      # TODO: rewrite this module adding clone method for the all settings
      # defined for the mongoid-report. for now it's creating the duplicates.
      # check out the mongoid library for the best example.
      self.settings = {}

      def self.inherited(subclass)
        subclass.settings = {}
      end

      # Variable for copying internal class settings to the instance because of
      # possible modifications in case of using maches with lambda
      # expressions.
      attr_reader :report_module_settings

      def initialize_report_module
        # Lets store settings under created instance.
        @report_module_settings = self.settings.inject({}) do |hash_module, (report_module, module_settings)|
          hash_module.merge!(
            report_module =>
              {
                fields:     module_settings[:fields],
                group_by:   module_settings[:group_by],
                batches:    module_settings[:batches],
                columns:    module_settings[:columns],
                mapping:    module_settings[:mapping],
                queries:    (module_settings[:queries] || []).dup,
                reports:    (module_settings[:reports] || {}).inject({}) do |hash_report, (report_name, report_settings)|
                  hash_report.merge!(
                    report_name => {
                      collection:  report_settings[:collection],
                      fields:      report_settings[:fields],
                      group_by:    report_settings[:group_by],
                      batches:     report_settings[:batches],
                      columns:     report_settings[:columns],
                      mapping:     report_settings[:mapping],
                      queries:     (report_settings[:queries] || []).dup,
                    })
                end
              })
        end

        @report_module_settings.each do |report_module, module_configuration|
          # Lets do not run queries builder in case of missing queries or group
          # by parameters
          unless module_configuration[:queries].empty? && module_configuration[:group_by].empty?
            builder = QueriesBuilder.new(module_configuration)

            # Prepare group queries depends on the configuration in the included
            # class.
            queries = builder.do

            # Now we have access to compiled queries to run it in aggregation
            # framework.
            module_configuration[:queries] = module_configuration[:queries] + queries
          end

          # For now we are filtering by $match queries only.
          matches = module_configuration[:queries].select do |query|
            query['$match'].present?
          end

          module_configuration[:reports].each do |report_name, report_configuration|
            # Lets merge report and module settings together.
            report_configuration[:fields]    = report_configuration[:fields] | module_configuration[:fields]
            report_configuration[:group_by]  = report_configuration[:group_by] | module_configuration[:group_by]
            report_configuration[:columns]   = report_configuration[:columns].merge(module_configuration[:columns])
            report_configuration[:mapping]   = report_configuration[:mapping].merge(module_configuration[:mapping])

            builder = QueriesBuilder.new(report_configuration)

            # Prepare group queries depends on the configuration in the included
            # class.
            queries = builder.do

            # Now we have access to compiled queries to run it in aggregation
            # framework.
            report_configuration[:queries] = report_configuration[:queries] + matches + queries
          end
        end
      end
      alias :initialize :initialize_report_module

      def queries(report_module, report_name)
        report_module_settings[report_module][:reports][report_name][:queries]
      end

      def mapping(report_module, report_name)
        report_module_settings[report_module][:reports][report_name][:mapping]
      end

      def batches(report_module, report_name)
        report_module_settings[report_module][:reports][report_name][:batches]
      end

      def groups(report_module, report_name)
        report_module_settings[report_module][:reports][report_name][:group_by]
      end

      def fields(report_module, report_name)
        report_module_settings[report_module][:reports][report_name][:fields]
      end

      def columns(report_module, report_name)
        report_module_settings[report_module][:reports][report_name][:columns]
      end

      # Method for preparing of aggregation scope where you can apply query,
      # yield and other grouping methods.
      def aggregate_for(report_module, report_name)
        Scope.new(self, report_module, report_name)
      end

      def aggregate
        ScopeCollection.new(self)
      end
    end

    module ClassMethods
      def report(name, &block)
        proxy = ReportProxy.new(self, name)
        proxy.instance_eval(&block)
      end

      def attach_to(*fields, &block)
        options = fields.extract_options!
        collection = fields[0]

        options.merge!(report_name: options[:as]) if options[:as]

        define_report_method(options.merge(collection: collection)) do
          proxy = AttachProxy.new(self, collection, options)
          proxy.instance_eval(&block)
        end
      end

      def batches(*fields)
        define_report_method(*fields) do |_, report_module, report_name, batches|
          self.set_settings(report_module, report_name, :batches, batches.stringify_keys!)
        end
      end

      def match(*fields)
        define_report_method(*fields) do |_, report_module, report_name, options|
          queries = self.get_settings(report_module, report_name, :queries)

          options.each do |key, value|
            queries
              .concat([{
                '$match' => { key => value }
              }])
          end
        end
      end

      def query(*fields)
        define_report_method(*fields) do |_, report_module, report_name, options|
          queries = self.get_settings(report_module, report_name, :queries)

          options.each do |key, value|
            queries.concat([{ key => value }])
          end
        end
      end

      def group_by(*fields)
        define_report_method(*fields) do |groups, report_module, report_name, _|
          self.set_settings(report_module, report_name, :group_by, groups.map(&:to_s))
        end
      end

      def column(*fields)
        define_report_method(*fields) do |columns, report_module, report_name, _|
          columns.each do |field|
            self.get_settings(report_module, report_name, :fields) << field.to_s
          end
        end
      end

      def columns(*fields)
        define_report_method(*fields) do |_, report_module, report_name, columns|
          self.set_settings(report_module, report_name, :columns, columns.stringify_keys!)
        end
      end

      def mapping(*fields)
        define_report_method(*fields) do |_, report_module, report_name, mapping|
          mapping.stringify_keys!

          mapping.each do |key, value|
            mapping[key] = value.to_s
          end

          self.set_settings(report_module, report_name, :mapping, mapping)
        end
      end

      def get_settings(report_module, report_name, field)
        unless report_name
          self.settings[report_module][field]
        else
          self.settings[report_module][:reports][report_name][field]
        end
      end

      def set_settings(report_module, report_name, field, value)
        unless report_name
          self.settings[report_module][field] = value
        else
          self.settings[report_module][:reports][report_name][field] = value
        end
      end

      private

      def define_report_method(*fields)
        options = fields.extract_options!

        # We should always specify model to attach fields, groups
        collection = options.fetch(:collection)
        options.delete(:collection)

        # In case if user passed mongoid model we should get name of collection
        # instead of using mongoid models. on deep_dup operations it will work
        # find with strings.
        collection = Collections.name(collection)

        report_module = options.delete(:report_module)
        report_module ||= self.name

        report_name = options.delete(:report_name)
        report_name ||= Collections.name(collection)

        # We should always have for option
        initialize_settings_by(report_module, report_name, collection)

        # Because of modifying fields(usign exract options method of
        # ActiveSupport) lets pass fields to the next block with collection.
        yield fields, report_module, report_name, options || {}
      end

      def initialize_settings_by(report_module, report_name, collection)
        # Global settings for the report block
        settings[report_module] ||= settings.fetch(report_module) do
          {
            reports:   {},
            fields:    [],
            group_by:  [],
            batches:   {},
            columns:   {},
            mapping:   {},
            # needs to be cloned
            queries:   [],
          }
        end

        return unless report_name

        settings[report_module][:reports][report_name] ||=
          settings[report_module][:reports].fetch(report_name) do
            {
              collection: collection,
              fields:     [],
              group_by:   [],
              batches:    {},
              columns:    {},
              mapping:    {},
              # needs to be cloned
              queries:    [],
            }
          end
      end
    end

  end
end