# frozen_string_literal: true module AceOfBase # A query builder for querying our data. class Query require 'ace_of_base/query/operator' require 'ace_of_base/query/or' require 'ace_of_base/query/and' require 'ace_of_base/query/eq' include ValidField INVALID_AGGREGATION_MESSAGE = 'Operation %s is not a valid aggregation' VALID_AGGREGATIONS = %i[min max sum count collect].freeze def initialize(storage) @fields = [] @order_bys = [] @filters = [] @aggregations = {} @group_bys = [] @storage = storage end def select(*fields) ensure_valid_fields!(fields) @fields = fields self end def aggregate(aggregations) ensure_valid_fields!(aggregations.keys) ensure_valid_aggregations!(aggregations.values) @aggregations = aggregations self end def group_by(*fields) ensure_valid_fields!(fields) @group_bys = fields self end def order_by(*fields) ensure_valid_fields!(fields) @order_bys = fields self end def filter(*filters) @filters = filters self end def execute collection = @storage collection = apply_filters(collection) collection = apply_aggregations(collection) collection = apply_sorts(collection) collection = take_fields(collection) collection.to_a end private attr_reader :fields, :order_bys, :filters, :storage, :group_bys, :aggregations def ensure_valid_aggregations!(aggregations) aggregations.each do |op| raise ArgumentError, format(INVALID_AGGREGATION_MESSAGE, op) unless VALID_AGGREGATIONS.include?(op.to_sym) end end def apply_filters(collection) return collection if filters.empty? collection.select do |record| filters.all? do |filter| filter.match?(record) end end end def apply_sorts(collection) return collection if order_bys.empty? collection.sort do |lhs, rhs| order_bys .lazy .map { |attr| lhs.public_send(attr) <=> rhs.public_send(attr) } .find(-> { 0 }, &:nonzero?) end end def apply_aggregations(results) return results if group_bys.empty? results .group_by { |record| group_bys.map { |attr| record.public_send(attr) } } .values .map { |records| AggregatedRecord.new(records, aggregations) } end def take_fields(collection) collection.map do |record| selected_fields.map { |field| record.public_send(field) } end end def selected_fields return Record::ATTRIBUTES if fields.empty? fields end end end