# frozen_string_literal: true module ActiveMocker class Base include DoNothingActiveRecordMethods include TemplateMethods extend Queries extend AliasAttribute def self.inherited(subclass) ActiveMocker::LoadedMocks.send(:add, 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.delete(:id) || attributes.delete("id")) record.save record.touch(:created_at, :created_on) if ActiveMocker::LoadedMocks.features[:timestamps] record.assign_attributes(attributes, &block) record._create_caller_locations = caller_locations record end end alias 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 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 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, fail_hard=false) ActiveMocker::LoadedMocks.find(klass).tap do |found_class| raise MockNotLoaded, "The ActiveMocker version of #{klass} is not required." if fail_hard && !found_class found_class end end # @param [Array] collection, an array of mock instances # @return [ScopeRelation] for the given mock so that it will include any scoped methods def __new_relation__(collection) ScopeRelation.new(collection) end private :classes, :build_type, :__new_relation__ public # @deprecated def clear_mock delete_all end def _find_associations_by_class(klass_name) associations_by_class[klass_name.to_s] end private def created_with(version) raise UpdateMocksError.new(name, version, ActiveMocker::VERSION) if version != ActiveMocker::VERSION end # @deprecated def call_mock_method(method:, caller:, arguments: []) is_implemented(method, "::", caller) end # @deprecated def is_implemented(method, type, call_stack) raise NotImplementedError, "#{type}#{method} for Class: #{name}. To continue stub the method.", call_stack end end # @deprecated def call_mock_method(method:, caller:, arguments: []) self.class.send(:is_implemented, method, '#', caller) end private :call_mock_method def classes(klass, fail_hard = false) self.class.send(:classes, klass, fail_hard) end private :classes attr_reader :associations, :types, :attributes # @private attr_accessor :_create_caller_locations # New objects can be instantiated as either empty (pass no construction parameter) or pre-set with # attributes. # # ==== 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) save end # Allows you to set all the attributes by passing in a hash of attributes with # keys matching the attribute names (which again matches the column names). # # cat = Cat.new(name: "Gorby", status: "yawning") # cat.attributes # => { "name" => "Gorby", "status" => "yawning", "created_at" => nil, "updated_at" => nil} # cat.assign_attributes(status: "sleeping") # cat.attributes # => { "name" => "Gorby", "status" => "sleeping", "created_at" => nil, "updated_at" => nil } # # Aliased to attributes=. def assign_attributes(new_attributes) 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) self.class.send(:insert, self) unless self.class.exists?(self) touch if ActiveMocker::LoadedMocks.features[:timestamps] true end alias save! save def touch(*names) raise ActiveMocker::Error, "cannot touch on a new record object" unless persisted? attributes = [:updated_at, :update_on] attributes.concat(names) current_time = Time.now.utc attributes.each do |column| column = column.to_s write_attribute(column, current_time) if self.class.attribute_names.include?(column) end true end def records self.class.send(:records) end private :records def delete records.delete(self) end alias 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.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 a hash of the given methods with their names as keys and returned values as values. def slice(*methods) Hash[methods.map! { |method| [method, public_send(method)] }].with_indifferent_access 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(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 def name self.class.name end private :name 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_association, :write_association end include PropertiesGetterAndSetter class ScopeRelation < Association end module Scopes end end end