DannyBen/active_cabinet

View on GitHub
lib/active_cabinet/metaclass.rb

Summary

Maintainability
A
35 mins
Test Coverage
require 'hash_cabinet'
require 'forwardable'
require 'active_cabinet/config'

# {ActiveCabinet} lets you create a +HashCabinet+ collection model by
# subclassing {ActiveCabinet}:
#
#   class Song < ActiveCabinet
#   end
#
# Now, you can perform CRUD operations on this collection, which will be
# persisted to disk:
#
#   # Create
#   Song.create id: 1, title: 'Moonchild', artist: 'Iron Maiden'
#
#   # Read
#   moonchild = Song[1] # or Song.find 1
#
#   # Update
#   moonchild.title = "22 Acacia Avenue"
#   moonchild.save
#   # or
#   moonchild.update! title: "22 Acacia Avenue"
#
#   # Delete
#   Song.delete 1
#
class ActiveCabinet
  class << self
    extend Forwardable
    def_delegators :cabinet, :count, :delete, :empty?, :keys, :size

    # @!group Creating Records

    # Creates and saves a new record instance.
    #
    # @param [String] id the record id.
    # @param [Hash] attributes the attributes to create.
    def []=(id, attributes)
      create attributes.merge(id: id)
    end

    # Creates and saves a new record instance.
    #
    # @param [Hash] attributes the attributes to create.
    # @return [Object] the record.
    def create(attributes)
      record = new attributes
      record.save || record
    end

    # @!group Reading Records

    # Returns all records.
    #
    # @return [Array] array of all records.
    def all
      cabinet.values.map do |attributes|
        new(attributes)
      end
    end

    # Returns all records, as an Array of Hashes.
    #
    # @return [Array] array of all records.
    def all_attributes
      cabinet.values
    end

    # Returns an array of records for which the block returns true.
    # When +query+ is provided, it should be a Hash with a single key and
    # value. The result will be records that have a matching attribute.
    #
    # @example Search using a Hash query
    #   Song.where artist: "Iron Maiden"
    #
    # @example Search using a block
    #   Song.where { |record| record[:artist] == "Iron Maiden" }
    #
    # @yieldparam [Object] record all record instances.
    # @return [Array<Object>] record all record instances.
    def where(query = nil, &block)
      if query
        key, value = query.first
        all.select { |record| record[key] == value }
      else
        all.select(&block)
      end
    end

    # Returns the record matching the +id+.
    # When providing a Hash with a single key-value pair, it will return the
    # first matching object from the respective {where} query.
    #
    # @example Retrieve a record by ID
    #   Song.find 1
    #   Song[1]
    #
    # @example Retrieve a different attributes
    #   Song.find artist: "Iron Maiden"
    #   Song[artist: "Iron Maiden"]
    #
    # @return [Object, nil] the object if found, or +nil+.
    def find(id)
      if id.is_a? Hash
        where(id).first
      else
        attributes = cabinet[id]
        attributes ? new(attributes) : nil
      end
    end
    alias [] find

    # Yields each record to the given block.
    #
    # @yieldparam [Object] record all record instances.
    def each
      cabinet.each_value do |attributes|
        yield new(attributes)
      end
    end

    # Returns the first record.
    #
    # @return [Object] the record.
    def first
      find keys.first
    end

    # Returns the last record.
    #
    # @return [Object] the record.
    def last
      find keys.last
    end

    # Returns a random racord.
    #
    # @return [Object] the record.
    def random
      find keys.sample
    end

    # @!group Deleting Records

    # Deletes a record matching the +id+.
    #
    # @param [String] id the record ID.
    # @return [Boolean] +true+ on success, +false+ otherwise.
    def delete(id)
      !!cabinet.delete(id)
    end

    # Deletes a record for which the block returns true.
    #
    # @example Delete records using a block
    #   Song.delete_if { |record| record[:artist] == "Iron Maiden" }
    def delete_if
      cabinet.delete_if { |key, _value| yield self[key] }
    end

    # Deletes all records.
    def drop
      cabinet.clear
    end

    # @!group Attribute Management

    # Returns an array containing the keys of all allowed attributes as
    # defined by {required_attributes}, {optional_attributes} and
    # {default_attributes}.
    #
    # @return [Array<Symbol>] array of required attribute keys.
    def allowed_attributes
      (optional_attributes || []) + required_attributes + default_attributes.keys
    end

    # Sets the required record attribute names.
    #
    # @param [Array<Symbol>] *attributes one or more attribute names.
    # @return [Array<Symbol>] the array of required attributes.
    def required_attributes(*args)
      args = args.first if args.first.is_a? Array
      if args.any?
        @required_attributes = args
        @required_attributes.push :id unless @required_attributes.include? :id
        @required_attributes
      else
        @required_attributes ||= [:id]
      end
    end

    # Sets the optional record attribute names.
    #
    # @param [Array<Symbol>] *attributes one or more attribute names.
    # @return [Array<Symbol>] the array of optional attributes.
    def optional_attributes(*args)
      args = args.first if args.first.is_a? Array
      if args.first == false
        @optional_attributes = false
      elsif args.any?
        @optional_attributes = *args
      else
        @optional_attributes.nil? ? [] : @optional_attributes
      end
    end

    # Sets the default record attribute values.
    #
    # @param [Hash<Symbol, Object>] **attributes one or more attribute names and values.
    # @return [Hash<Symbol, Object>] the hash of the default attributes.
    def default_attributes(args = nil)
      if args
        @default_attributes = args
      else
        @default_attributes ||= {}
      end
    end

    # @!group Utilities

    # Returns all records as a hash, with record IDs as the keys.
    def to_h
      cabinet.to_h.transform_values { |attributes| new(attributes) }
    end

    # Returns the +HashCabinet+ instance.
    #
    # @return [HashCabinet] the +HashCabinet+ object.
    def cabinet
      @cabinet ||= HashCabinet.new "#{Config.dir}/#{cabinet_name}"
    end

    # Returns or sets the cabinet name.
    # Defaults to the name of the class, lowercase.
    #
    # @param [String] name the name of the cabinet file.
    # @return [String] name the name of the cabinet file.
    def cabinet_name(new_name = nil)
      if new_name
        @cabinet = nil
        @cabinet_name = new_name
      else
        @cabinet_name ||= to_s.downcase.gsub('::', '_')
      end
    end
  end
end