module ActiveMocker
module Mock
class Base
include DoNothingActiveRecordMethods
include MockAbilities
include TemplateMethods
extend Queries
def self.inherited(subclass)
return ActiveMocker::LoadedMocks.send(:add, subclass) if subclass.superclass == Base
ActiveMocker::LoadedMocks.send(:add_subclass, subclass)
end
class << self
# Creates an object (or multiple objects) and saves it to memory.
#
# The +attributes+ parameter can be either a Hash or an Array of Hashes. These Hashes describe the
# attributes on the objects that are to be created.
#
# ==== Examples
# # Create a single new object
# User.create(first_name: 'Jamie')
#
# # Create an Array of new objects
# User.create([{ first_name: 'Jamie' }, { first_name: 'Jeremy' }])
#
# # Create a single object and pass it into a block to set other attributes.
# User.create(first_name: 'Jamie') do |u|
# u.is_admin = false
# end
#
# # Creating an Array of new objects using a block, where the block is executed for each object:
# User.create([{ first_name: 'Jamie' }, { first_name: 'Jeremy' }]) do |u|
# u.is_admin = false
# end
def create(attributes = {}, &block)
if attributes.is_a?(Array)
attributes.collect { |attr| create(attr, &block) }
else
record = new(id: attributes[:id] || attributes['id'])
record.save
record.assign_attributes(attributes, &block)
record._create_caller_locations = caller_locations
record
end
end
alias_method :create!, :create
def records
@records ||= Records.new
end
private :records
delegate :insert, :exists?, :to_a, :to => :records
delegate :first, :last, :to => :all
# Delete an object (or multiple objects) that has the given id.
#
# This essentially finds the object (or multiple objects) with the given id and then calls delete on it.
#
# ==== Parameters
#
# * +id+ - Can be either an Integer or an Array of Integers.
#
# ==== Examples
#
# # Destroy a single object
# TodoMock.delete(1)
#
# # Destroy multiple objects
# todos = [1,2,3]
# TodoMock.delete(todos)
def delete(id)
if id.is_a?(Array)
id.map { |one_id| delete(one_id) }
else
find(id).delete
end
end
alias_method :destroy, :delete
# Deletes the records matching +conditions+.
#
# Post.where(person_id: 5).where(category: ['Something', 'Else']).delete_all
def delete_all(conditions=nil)
return records.reset if conditions.nil?
super
end
alias_method :destroy_all, :delete_all
# @api private
def from_limit?
false
end
def abstract_class?
true
end
def build_type(type)
@@built_types ||= {}
@@built_types[type] ||= Virtus::Attribute.build(type)
end
def classes(klass)
ActiveMocker::LoadedMocks.find(klass)
end
def new_relation(collection)
ScopeRelation.new(collection)
end
private :classes, :build_type, :new_relation
public
def clear_mock
clear_mocked_methods
delete_all
end
def _find_associations_by_class(klass_name)
associations_by_class[klass_name.to_s]
end
def created_with(version)
raise UpdateMocksError.new(self.name, version, ActiveMocker::VERSION) if version != ActiveMocker::VERSION
end
private :created_with
end
def classes(klass)
self.class.send(:classes, klass)
end
private :classes
attr_reader :associations, :types, :attributes
attr_accessor :_create_caller_locations
# New objects can be instantiated as either empty (pass no construction parameter) or pre-set with
# attributes but not yet saved (pass a hash with key names matching the associated table column names).
# In both instances, valid attribute keys are determined by the column names of the associated table --
# hence you can't have attributes that aren't part of the table columns.
#
# ==== Example:
# # Instantiates a single new object
# UserMock.new(first_name: 'Jamie')
def initialize(attributes = {}, &block)
if self.class.abstract_class?
raise NotImplementedError, "#{self.class.name} is an abstract class and cannot be instantiated."
end
setup_instance_variables
assign_attributes(attributes, &block)
end
def setup_instance_variables
@types = self.class.send(:types)
@attributes = self.class.send(:attributes).dup
@associations = self.class.send(:associations).dup
end
private :setup_instance_variables
def update(attributes={})
assign_attributes(attributes)
end
# @api private
def assign_attributes(new_attributes, &block)
yield self if block_given?
unless new_attributes.respond_to?(:stringify_keys)
raise ArgumentError, "When assigning attributes, you must pass a hash as an argument."
end
return nil if new_attributes.blank?
attributes = new_attributes.stringify_keys
attributes.each do |k, v|
_assign_attribute(k, v)
end
end
alias attributes= assign_attributes
# @api private
def _assign_attribute(k, v)
public_send("#{k}=", v)
rescue NoMethodError
if respond_to?("#{k}=")
raise
else
raise UnknownAttributeError.new(self, k)
end
end
def save(*args)
unless self.class.exists?(self)
self.class.send(:insert, self)
end
true
end
alias save! save
def records
self.class.send(:records)
end
private :records
def delete
records.delete(self)
end
alias_method :destroy, :delete
delegate :[], :[]=, to: :attributes
# Returns true if this object hasn't been saved yet; otherwise, returns false.
def new_record?
records.new_record?(self)
end
# Indicates if the model is persisted. Default is +false+.
#
# person = Person.new(id: 1, name: 'bob')
# person.persisted? # => false
def persisted?
records.persisted?(id)
end
# Returns +true+ if the given attribute is in the attributes hash, otherwise +false+.
#
# person = Person.new
# person.has_attribute?(:name) # => true
# person.has_attribute?('age') # => true
# person.has_attribute?(:nothing) # => false
def has_attribute?(attr_name)
@attributes.has_key?(attr_name.to_s)
end
# Returns +true+ if the specified +attribute+ has been set and is neither +nil+ nor empty? (the latter only applies
# to objects that respond to empty?, most notably Strings). Otherwise, +false+.
# Note that it always returns +true+ with boolean attributes.
#
# person = Task.new(title: '', is_done: false)
# person.attribute_present?(:title) # => false
# person.attribute_present?(:is_done) # => true
# person.name = 'Francesco'
# person.is_done = true
# person.attribute_present?(:title) # => true
# person.attribute_present?(:is_done) # => true
def attribute_present?(attribute)
value = read_attribute(attribute)
!value.nil? && !(value.respond_to?(:empty?) && value.empty?)
end
# Returns an array of names for the attributes available on this object.
#
# person = Person.new
# person.attribute_names
# # => ["id", "created_at", "updated_at", "name", "age"]
def attribute_names
self.class.attribute_names
end
def inspect
ObjectInspect.new(self.class.name, attributes).to_s
end
# Will not allow attributes to be changed
#
# Will freeze attributes forever. Querying for the record again will not unfreeze it because records exist in memory
# and are not initialized upon a query. This behaviour differs from ActiveRecord, beware of any side effect this may
# have when using this method.
def freeze
@attributes.freeze; self
end
module PropertiesGetterAndSetter
# Returns the value of the attribute identified by attr_name after
# it has been typecast (for example, "2004-12-12" in a date column is cast
# to a date object, like Date.new(2004, 12, 12))
def read_attribute(attr)
@attributes[attr]
end
# Updates the attribute identified by attr_name with the
# specified +value+. Empty strings for fixnum and float columns are
# turned into +nil+.
def write_attribute(attr, value)
@attributes[attr] = types[attr].coerce(value)
end
# @api private
def read_association(attr, assign_if_value_nil=nil)
@associations[attr.to_sym] ||= assign_if_value_nil.try(:call)
end
# @api private
def write_association(attr, value)
@associations[attr.to_sym] = value
end
protected :read_attribute, :write_attribute, :read_association, :write_association
end
include PropertiesGetterAndSetter
class ScopeRelation < ::ActiveMocker::Mock::Association
end
module Scopes
end
end
end
end