module Sequel::Plugins::BitFields

Public Class Methods

bit_fields_for_models() click to toggle source
# File lib/sequel/plugins/bit_fields.rb, line 7
def self.bit_fields_for_models
  @@bit_fields_for_models
end
configure(model, bit_field_column, bit_fields, options = {}) click to toggle source
# File lib/sequel/plugins/bit_fields.rb, line 11
def self.configure(model, bit_field_column, bit_fields, options = {})
  model.class_eval do
    options[:scope] = bit_field_column        if options[:scope] == true
    options[:scope] = options[:scope].to_sym  if options[:scope].is_a?(String)

    bit_fields = bit_fields.dup.map do |bit_field|
      if bit_field.is_a?(Hash)
        raise "Unnamed field: #{bit_field.inspect}" unless bit_field[:name]
        bit_field
      else
        { :name => bit_field }
      end
    end

    if options[:scope].is_a?(Symbol)
      bit_fields = bit_fields.map do |bit_field|
        bit_field.merge(:name => "#{options[:scope]}_#{bit_field[:name]}".to_sym)
      end
    end

    bit_fields = bit_fields.map do |bit_field|
      { :description => "Description for '#{bit_field[:name]}' not available." }.merge(bit_field)
    end

    # at this point, all bit_fields do have the following format:
    # { description => 'something', :name => :something }

    @@bit_fields_for_models[model.to_s] ||= {}
    @@bit_fields_for_models[model.to_s][bit_field_column] = bit_fields

    unless respond_to?(:bit_fields)
      define_singleton_method(:bit_fields) do |*args|
        if column_name = [*args].first
          @@bit_fields_for_models[model.to_s][column_name]
        else
          @@bit_fields_for_models[model.to_s]
        end
      end
    end

    unless respond_to?(:bit_field_indexes_for)
      define_singleton_method(:bit_field_indexes_for) do |*args|
        if column_name = [*args].first
          hash = {}

          @@bit_fields_for_models[model.to_s][column_name].each_with_index do |attribute, i|
            hash[attribute[:name].to_sym] = 2**i
          end

          hash
        else
          raise 'No bit field name was passed!'
        end
      end
    end
  end

  model.instance_eval do
    # inject convenience method for multiple flag or bit_field assignment
    # overrides default assignment behavior for column_name=
    # example:
    #   model.roles = [:author, :contributor, :reader]
    #   model.roles = 6 # [:contributor, :reader]
    flag_assign = bit_field_column.to_s + '='
    unless respond_to?(flag_assign)
      define_method(flag_assign) do |args|
        args = [*args]

        if args.empty?
          self[bit_field_column] = 0
        #argument is an integer
        elsif args.first.is_a?(Fixnum)
          self[bit_field_column] = args.first
        else
          #set each bit_field presented in args to true, others to false
          bit_fields.each do |bit_field|
            self.send("#{bit_field[:name]}=", args.include?(bit_field[:name]))
          end
        end
      end
    end

    unless respond_to?(:bit_field_values_for)
      # inject the method bit_field_values_for which
      # returns a hash with all the values of the bit_fields
      define_method("bit_field_values_for") do |*args|
        if column_name = [*args].first
          hash  = {}
          value = [*args][1]

          @@bit_fields_for_models[model.to_s][column_name].each do |attribute|
            hash[attribute[:name].to_sym] = self.send("#{attribute[:name]}?".to_sym)
          end

          unless value.nil?
            hash.dup.each do |key, _value|
              hash.delete(key) unless _value == value
            end
          end

          hash
        else
          raise 'No bit field name was passed!'
        end
      end
    end

    unless respond_to?(:bit_changed?)
      define_method(:bit_changed?) do |*args|
        unless defined? self.initial_value()
          raise "Dirty plugin was not activated. Add 'plugin :dirty' to your model declaration!"
        end

        if args.size == 1
          bitfield_name = args.first

          # Search for column name and bit
          column_name = nil
          bit         = nil
          @@bit_fields_for_models[model.to_s].each do |_column_name, bitfields|
            bitfields.each_with_index do |bitfield, index|
              if bitfield[:name] == bitfield_name
                column_name = _column_name
                bit         = 1 << index
                break
              end
            end
            break if column_name
          end
          raise "Bit field with name `#{bitfield_name}' unknown" unless column_name

          # Check if bit has changed
          return initial_value(column_name) & bit != self[column_name] & bit
        else
          raise 'No bit field name was passed!'
        end
      end
    end
  end

  bit_fields.each_with_index do |bit_field, i|
    bit_field_name = bit_field[:name]
    index          = 2**i

    model.class.instance_eval do
      # inject the sql generator methods
      #
      # example:
      #   MyModel.where(MyModel.finished_sql(true)).all
      # and would return
      #   "status_bits & 1 = 1"
      #
      define_method("#{bit_field_name}_sql") do |*args|
        value   = [*args].first
        value   = true if value.nil?
        options = { :table => self.table_name.to_s }.merge([*args][1] || {})

        "#{db.literal(Sequel.qualify(options[:table], bit_field_column))} & #{index} #{'!' unless value}= #{index}"
      end
    end

    model.instance_eval do
      # inject the setter methods
      #
      # example:
      #   model = MyModel.create()
      #   model.finished = false
      #
      define_method("#{bit_field_name}=") do |value|
        value   = [true, 1, '1', 'true'].include?(value)
        current = self.send("#{bit_field_name}?".to_sym)

        # init the bit_field with 0 if not yet set
        self[bit_field_column] = 0 if self[bit_field_column].nil?

        self[bit_field_column] = if value && !current
          # value is true and current value is false
          self[bit_field_column] | index
        elsif !value && current
          # value is false and current value is true
          self[bit_field_column] ^ index
        else
          # don't do anything
          self[bit_field_column]
        end
      end

      # inject the getter methods
      #
      # example:
      #   model = MyModel.create()
      #   model.finished? # == false
      #
      define_method("#{bit_field_name}?") do
        (self[bit_field_column] || 0) & index == index
      end

      # inject the dataset methods
      # example:
      #   MyModel.finished(true)
      #   MyModel.where(:foo => "bar").finished
      # and would return a dataset with
      #   "status_bits & 1 = 1"
      #
      dataset_module do
        define_method("#{bit_field_name}") do |*args|
          filter model.send("#{bit_field_name}_sql", *args)
        end
      end
    end
  end
end