module RailsBetterFilters::ClassMethods

Public Instance Methods

better_filter(name, callback = nil, &block) click to toggle source

Register a new bfilter, or overwrite (!) an already register one. Set a callback function (i.e. a symbol naming an instance method of the resp. controller), callable or block. If neither callback nor block are given, the name of the filter is used as the name of the instance method.

# File lib/rails-better-filters/rails_better_filters.rb, line 38
def better_filter(name, callback = nil, &block)
  @bfilters ||= {}

  if callback
    @bfilters[name.to_sym] = callback
  elsif block_given?
    @bfilters[name.to_sym] = block
  else
    @bfilters[name.to_sym] = name
  end
end
better_filter_chain() click to toggle source

Get the chain of bfilters for this class. Uses caching for performance of subsequent calls. Returns a list of hashes of :name => [:only_actions], e.g.

{:bfilter1 => [:action1, :action2], :bfilter2 => [], …}
# File lib/rails-better-filters/rails_better_filters.rb, line 64
def better_filter_chain
  if !@final_bfilter_chain
    # Make sure everything is in place.
    @bfilters     ||= {}
    @bfilter_opts ||= {}

    # Get advice from our forefathers.
    igo = inherited_bfilter_opts
    ig = inherited_bfilters

    # Do the math.
    @final_bfilter_opts = finalize_bfilter_opts(ig, igo)
    @final_bfilter_chain = finalize_bfilters(ig, @final_bfilter_opts)
  end

  @final_bfilter_chain
end
better_filter_chain_each() { |name, data, data| ... } click to toggle source
# File lib/rails-better-filters/rails_better_filters.rb, line 82
def better_filter_chain_each
  better_filter_chain.each do |bfilter|
    name = bfilter.keys.first
    data = bfilter.values.first
    yield name, data[:callback], data[:only]
  end
end
better_filter_opts(name, opts = {}) click to toggle source

Set some options for a bfilter. Option hashes get merged in the end, conflicting options will be resolved based on the value of the :importance entry (the higher the better).

# File lib/rails-better-filters/rails_better_filters.rb, line 54
def better_filter_opts(name, opts = {})
  @bfilter_opts ||= {}
  @bfilter_opts[name.to_sym] ||= []
  @bfilter_opts[name.to_sym] << sanitize_opts(opts)
end

Private Instance Methods

finalize_bfilter_opts(_bfilters, _bfilter_opts) click to toggle source

Consolidates an array of bfilter_opts hashes into a single hash, ensures bfilter_opts hashes for every known bfilter and discards hashes for unknown bfilters.

# File lib/rails-better-filters/rails_better_filters.rb, line 122
def finalize_bfilter_opts(_bfilters, _bfilter_opts)
  result = {}
  _bfilters.keys.each do |name|
    if !_bfilter_opts[name] || _bfilter_opts[name].empty?
      # Default options if no option hash given.
      result[name] = sanitize_opts({})
    elsif _bfilter_opts[name].size == 1
      # Use the first option hash if only one was given.
      result[name] = _bfilter_opts[name].first
    else
      # Flatten multiple option hashs weighted by their :importance.
      result[name] = flatten_opts_list(_bfilter_opts[name])
    end 
    # Drop the now useless :importance field.
    result[name].delete(:importance)
  end
  result
end
finalize_bfilters(_bfilters, _bfilter_opts) click to toggle source

Finds a topological weighted ordering for the bfilters known in this class and given the various :before, :after, :blocks, and :priority constraints. Returns a list of hashes of bfilter names pointing to their callbacks and :only constraints.

# File lib/rails-better-filters/rails_better_filters.rb, line 146
def finalize_bfilters(_bfilters, _bfilter_opts)
  # Step 1: Drop unknown :before, :after, :block constraints
  #         and convert all :after constraints into :before.
  _bfilter_opts.each do |name, opts|
    [:before, :after, :blocks].each do |s|
      opts[s].select! { |g| _bfilters.keys.include?(g) }
    end

    while (g = opts[:after].shift)
      if !_bfilter_opts[g][:before].include?(name)
        _bfilter_opts[g][:before] << name
      end
    end
  end

  # Step 1: Make sure they're all unique now.
  _bfilter_opts.each do |_, opts|
    [:before, :blocks].each do |s|
      opts[s].uniq!
    end
  end

  # Step 2: Blocking! This is actually pretty tricky functionality.
  #         For transitive :block constraints (e.g., bfilter :a
  #         blocks bfilter :b blocks bfilter :c), we want to block the
  #         bfilters one after the other, i.e. in this case first block
  #         bfilter :b (as required by :a), but then *keep* bfilter :c, as
  #         it is now not blocked by :b anymore. The reasoning behind this
  #         is that :a most likely came last and knows about both :b and :c,
  #         so if :a wanted to block them both it would be sufficient to
  #         specify them both in the :block field. Need to find a real-world
  #         example for this.
  #         Anyway, the algorithm right now goes like this:
  #         Topologically sort the bfilters based on the :blocks DAG and their
  #         priority, execute all the blocks of the very first bfilter (that
  #         specifies blocks), repeat until no :blocks remain.

  # To speed things up a little, we first remove all nodes that do
  # not have any outgoing or incoming :blocks constraints.
  candidates = _bfilters.keys.select do |name| 
    !_bfilter_opts[name][:blocks].empty? ||
    _bfilter_opts.any? { |_, opts| opts[:blocks].include?(name) }
  end
  candidates_opts = _bfilter_opts.select { |name, _| candidates.include?(name) }

  victims = []
  loop do
    nodes = Hash[candidates.map { |name| [name, candidates_opts[:priority]] }]
    edges = Hash[candidates_opts.map { |name, opts| [name, opts[:blocks]] }]
    break if edges.empty?

    wts = WeightedTopologicalSort.sort(nodes, edges)
    executor = wts.shift

    # Do the blocks!
    victims.concat(candidates_opts[executor][:blocks])

    # Filter the candidates.
    candidates.delete(executor)
    candidates.reject! { |name| victims.include?(name) }
    candidates_opts.delete(executor)
    candidates_opts.reject! { |name, _| victims.include?(name) }
  end

  # Now the final shoot-the-bfilter-in-the-head.
  _bfilters.reject! { |name, _| victims.include?(name) }
  _bfilter_opts.reject! { |name, _| victims.include?(name) }

  # Step 3: Sorting! Compared to blocking, this is rather easy.
  #         Topological sort based on the :before values and the :priority.
  nodes = Hash[_bfilter_opts.map { |name, opts| [name, opts[:priority]] }]
  edges = Hash[_bfilter_opts.map { |name, opts| [name, opts[:before]] }]
  edges.select! { |_, to| !to.empty? }
  wts = WeightedTopologicalSort.sort(nodes, edges)

  # Step 4: Data shuffle.
  wts.map { |name| { name => {:callback => _bfilters[name], :only => _bfilter_opts[name][:only]} } }
end
flatten_opts_list(opts_list) click to toggle source

Merges a list of option hashes ordered by their :importance, so that for conflicting keys the value of the most important hash is chosen.

# File lib/rails-better-filters/rails_better_filters.rb, line 243
def flatten_opts_list(opts_list)
  opts_list.sort_by { |opts| opts[:importance] }.inject({}) { |res, opts| res.merge(opts) }
end
inherited_bfilter_opts() click to toggle source

Finds all bfilter_opts hashes in ancestor classes and the current class, storing them in an array.

# File lib/rails-better-filters/rails_better_filters.rb, line 107
def inherited_bfilter_opts
  ancestors.inject({}) do |igo, ancestor|
    if ancestor.instance_variable_defined?(:@bfilter_opts)
      ancestor.instance_variable_get(:@bfilter_opts).each do |name, opts|
        igo[name] ||= []
        igo[name].concat(opts)
      end
    end
    igo
  end
end
inherited_bfilters() click to toggle source

Finds all the bfilters referenced in all base classes of the current class. For conflicting bfilters, the most recent one (i.e. youngest in inheritance hierarchy) is chosen.

# File lib/rails-better-filters/rails_better_filters.rb, line 95
def inherited_bfilters
  ancestors.reverse.inject({}) do |ig, ancestor|
    if ancestor.instance_variable_defined?(:@bfilters)
      ig.merge(ancestor.instance_variable_get(:@bfilters))
    else
      ig
    end
  end
end
sanitize_opts(opts) click to toggle source

Default values for option hashes.

# File lib/rails-better-filters/rails_better_filters.rb, line 226
def sanitize_opts(opts)
  opts = opts.dup
  opts[:importance] ||= 0
  opts[:priority] ||= 0
  [:before, :after, :blocks, :only].each do |k|
    if opts[k]
      opts[k] = [opts[k]] if !opts[k].is_a?(Array)
      opts[k].map! { |s| s.to_sym }
    else
      opts[k] = []
    end
  end
  opts
end