# Example usageā¦ # # class ProductCounter # include Counter::Definition # # This specifies the association we're counting # count :products # sum :price # optional # as "my_counter" # end class Counter::Definition include Singleton # Attributes set by Counters#counter integration: attr_accessor :association_name # Set the model we're attached to (set by Counters#counter) attr_accessor :model # Set the thing we're counting (set by Counters#counter) attr_accessor :countable_model # Set the inverse association (i.e., from the products to the user) attr_accessor :inverse_association # When using sum, set the column we're summing attr_accessor :column_to_count # Test if we should count items using conditions attr_writer :conditions attr_writer :conditional # Set the name of the counter (used as the method name) attr_accessor :method_name attr_accessor :name # An array of all global counters attr_writer :global_counters # An array of Proc to run when the counter changes attr_writer :counter_hooks # The counters this calculated counter depends on attr_writer :dependent_counters # The block to call to calculate the counter attr_accessor :calculated_from # Is this a counter which sums a column? def sum? column_to_count.present? end # Is this a global counter? i.e., not attached to a model def global? model.nil? end # Is this counter conditional? def conditional? @conditional end # Is this counter calculated from other counters? def calculated? !@calculated_from.nil? end # Is this a manual counter? # Manual counters are not automatically updated from an association # or calculated from other counters def manual? association_name.nil? && !calculated? end # for global counter instances to find their definition def self.find_definition name Counter::Definition.instance.global_counters.find { |c| c.name == name } end # Access the counter value for global counters def self.counter raise "Unable to find counter instances via #{name}#counter. Use must use #{instance.model}#find_counter or #{instance.model}##{instance.counter_name}" unless instance.global? Counter::Value.find_counter self end # What we record in Counter::Value#name def record_name return name if global? return "#{model.name.underscore}-#{association_name}" if association_name.present? return "#{model.name.underscore}-#{name}" end def conditions @conditions ||= {} @conditions end def global_counters @global_counters ||= [] @global_counters end def counter_hooks @counter_hooks ||= [] @counter_hooks end def dependent_counters @dependent_counters ||= [] @dependent_counters end # Set the association we're counting def self.count association_name, as: "#{association_name}_counter" instance.association_name = association_name instance.name = as.to_s # How the counter can be accessed e.g. counter.products_counter instance.method_name = as.to_s end def self.global Counter::Definition.instance.global_counters << instance end def self.calculated_from *dependent_counters, &block instance.dependent_counters = dependent_counters instance.calculated_from = block dependent_counters.each do |dependent_counter| # Install after_change hooks on the dependent counters dependent_counter.after_change :update_calculated_counters dependent_counter.define_method :update_calculated_counters do |counter, _old_value, _new_value| # Fetch all the counters which depend on this one calculated_counters = counter.parent.class.counter_configs.select { |c| c.dependent_counters.include?(counter.definition.class) } calculated_counters = calculated_counters.map { |c| counter.parent.counters.find_or_create_counter!(c) } # calculate the new values calculated_counters.each(&:calculate!) end end end # Set the name of the counter def self.as name instance.name = name.to_s instance.method_name = name.to_s end # Get the name of the association we're counting def self.association_name instance.association_name end # Set the column we're summing. Leave blank to count the number of items def self.sum column_name instance.column_to_count = column_name end # Define a conditional filter def self.on action, &block instance.conditional = true conditions = Counter::Conditions.new conditions.instance_eval(&block) instance.conditions[action] ||= [] instance.conditions[action] << conditions end def self.after_change block instance.counter_hooks << block end end