module ActiveMocker
module Mock
module Queries
class Find
def initialize(record)
@record = record
end
def is_of(conditions={})
conditions.all? do |col, match|
next match.any? { |m| @record.send(col) == m } if match.is_a? Enumerable
@record.send(col) == match
end
end
end
class WhereNotChain
def initialize(collection, parent_class)
@collection = collection
@parent_class = parent_class
end
def not(conditions={})
@parent_class.call(@collection.reject do |record|
Find.new(record).is_of(conditions)
end)
end
end
# Deletes the records matching +conditions+ by instantiating each
# record and calling its +delete+ method.
#
# ==== Parameters
#
# * +conditions+ - A string, array, or hash that specifies which records
# to destroy. If omitted, all records are destroyed.
#
# ==== Examples
#
# PersonMock.destroy_all(status: "inactive")
# PersonMock.where(age: 0..18).destroy_all
#
# If a limit scope is supplied, +delete_all+ raises an ActiveMocker error:
#
# Post.limit(100).delete_all
# # => ActiveMocker::Mock::Error: delete_all doesn't support limit scope
def delete_all(conditions=nil)
raise ActiveMocker::Mock::Error.new("delete_all doesn't support limit scope") if from_limit?
if conditions.nil?
to_a.map(&:delete)
return to_a.clear
end
where(conditions).map { |r| r.delete }.count
end
alias_method :destroy_all, :delete_all
# Returns a new relation, which is the result of filtering the current relation
# according to the conditions in the arguments.
#
# === hash
#
# #where will accept a hash condition, in which the keys are fields and the values
# are values to be searched for.
#
# Fields can be symbols or strings. Values can be single values, arrays, or ranges.
#
# User.where({ name: "Joe", email: "joe@example.com" })
#
# User.where({ name: ["Alice", "Bob"]})
#
# User.where({ created_at: (Time.now.midnight - 1.day)..Time.now.midnight })
#
# In the case of a belongs_to relationship, an association key can be used
# to specify the model if an ActiveRecord object is used as the value.
#
# author = Author.find(1)
#
# # The following queries will be equivalent:
# Post.where(author: author)
# Post.where(author_id: author)
#
# This also works with polymorphic belongs_to relationships:
#
# treasure = Treasure.create(name: 'gold coins')
# treasure.price_estimates << PriceEstimate.create(price: 125)
#
# # The following queries will be equivalent:
# PriceEstimate.where(estimate_of: treasure)
# PriceEstimate.where(estimate_of_type: 'Treasure', estimate_of_id: treasure)
#
# === no argument
#
# If no argument is passed, #where returns a new instance of WhereChain, that
# can be chained with #not to return a new relation that negates the where clause.
#
# User.where.not(name: "Jon")
#
# See WhereChain for more details on #not.
def where(conditions=nil)
return WhereNotChain.new(all, method(:new_relation)) if conditions.nil?
new_relation(to_a.select do |record|
Find.new(record).is_of(conditions)
end)
end
# Find by id - This can either be a specific id (1), a list of ids (1, 5, 6), or an array of ids ([5, 6, 10]).
# If no record can be found for all of the listed ids, then RecordNotFound will be raised. If the primary key
# is an integer, find by id coerces its arguments using +to_i+.
#
# Person.find(1) # returns the object for ID = 1
# Person.find(1, 2, 6) # returns an array for objects with IDs in (1, 2, 6)
# Person.find([7, 17]) # returns an array for objects with IDs in (7, 17)
# Person.find([1]) # returns an array for the object with ID = 1
#
# ActiveRecord::RecordNotFound will be raised if one or more ids are not found.
def find(ids)
results = [*ids].map do |id|
find_by!(id: id)
end
return new_relation(results) if ids.class == Array
results.first
end
# Updates all records with details given if they match a set of conditions supplied, limits and order can
# also be supplied.
#
# ==== Parameters
#
# * +updates+ - A string, array, or hash.
#
# ==== Examples
#
# # Update all customers with the given attributes
# Customer.update_all wants_email: true
#
# # Update all books with 'Rails' in their title
# BookMock.where(title: 'Rails').update_all(author: 'David')
#
# # Update all books that match conditions, but limit it to 5 ordered by date
# BookMock.where(title: 'Rails').order(:created_at).limit(5).update_all(author: 'David')
def update_all(conditions)
all.each { |i| i.update(conditions) }
end
# Updates an object (or multiple objects) and saves it.
#
# ==== Parameters
#
# * +id+ - This should be the id or an array of ids to be updated.
# * +attributes+ - This should be a hash of attributes or an array of hashes.
#
# ==== Examples
#
# # Updates one record
# Person.update(15, user_name: 'Samuel', group: 'expert')
#
# # Updates multiple records
# people = { 1 => { "first_name" => "David" }, 2 => { "first_name" => "Jeremy" } }
# Person.update(people.keys, people.values)
def update(id, attributes)
if id.is_a?(Array)
id.map.with_index { |one_id, idx| update(one_id, attributes[idx]) }
else
object = find(id)
object.update(attributes)
object
end
end
# Finds the first record matching the specified conditions. There
# is no implied ordering so if order matters, you should specify it
# yourself.
#
# If no record is found, returns nil.
#
# Post.find_by name: 'Spartacus', rating: 4
def find_by(conditions = {})
send(:where, conditions).first
end
# Like find_by, except that if no record is found, raises
# an ActiveRecord::RecordNotFound error.
def find_by!(conditions={})
result = find_by(conditions)
raise RecordNotFound if result.nil?
result
end
# Finds the first record with the given attributes, or creates a record
# with the attributes if one is not found:
#
# # Find the first user named "Penélope" or create a new one.
# UserMock.find_or_create_by(first_name: 'Penélope')
# # => #
#
# # Find the first user named "Penélope" or create a new one.
# # We already have one so the existing record will be returned.
# UserMock.find_or_create_by(first_name: 'Penélope')
# # => #
#
# This method accepts a block, which is passed down to +create+. The last example
# above can be alternatively written this way:
#
# # Find the first user named "Scarlett" or create a new one with a
# # different last name.
# User.find_or_create_by(first_name: 'Scarlett') do |user|
# user.last_name = 'Johansson'
# end
# # => #
#
def find_or_create_by(attributes, &block)
find_by(attributes) || create(attributes, &block)
end
alias_method :find_or_create_by!, :find_or_create_by
# Like find_or_create_by, but calls new instead of create.
def find_or_initialize_by(attributes, &block)
find_by(attributes) || new(attributes, &block)
end
# Count the records.
#
# PersonMock.count
# # => the total count of all people
#
# PersonMock.count(:age)
# # => returns the total count of all people whose age is present in database
def count(column_name = nil)
return all.size if column_name.nil?
where.not(column_name => nil).size
end
# Specifies a limit for the number of records to retrieve.
#
# User.limit(10)
def limit(num)
relation = new_relation(all.take(num))
relation.send(:set_from_limit)
relation
end
# Calculates the sum of values on a given column. The value is returned
# with the same data type of the column, 0 if there's no row.
#
# Person.sum(:age) # => 4562
def sum(key)
values = values_by_key(key)
values.inject(0) do |sum, n|
sum + (n || 0)
end
end
# Calculates the average value on a given column. Returns +nil+ if there's
# no row.
#
# PersonMock.average(:age) # => 35.8
def average(key)
values = values_by_key(key)
total = values.inject { |sum, n| sum + n }
BigDecimal.new(total) / BigDecimal.new(values.count)
end
# Calculates the minimum value on a given column. The value is returned
# with the same data type of the column, or +nil+ if there's no row.
#
# Person.minimum(:age) # => 7
def minimum(key)
values_by_key(key).min_by { |i| i }
end
# Calculates the maximum value on a given column. The value is returned
# with the same data type of the column, or +nil+ if there's no row.
#
# Person.maximum(:age) # => 93
def maximum(key)
values_by_key(key).max_by { |i| i }
end
# Allows to specify an order attribute:
#
# User.order('name')
#
# User.order(:name)
def order(key)
new_relation(all.sort_by { |item| item.send(key) })
end
# Reverse the existing order clause on the relation.
#
# User.order('name').reverse_order
def reverse_order
new_relation(to_a.reverse)
end
def all
new_relation(to_a || [])
end
private
def values_by_key(key)
all.map { |obj| obj.send(key) }
end
def new_relation(collection)
duped = self.dup
duped.collection = collection
duped
end
end
end
end