moonmaster9000/frill

View on GitHub
lib/frill/frill.rb

Summary

Maintainability
A
0 mins
Test Coverage
module Frill
  class CyclicDependency < RuntimeError; end

  def self.included base
    self.dependency_graph.add base
    base.extend ClassMethods
  end

  def self.decorators
    @decorators ||= dependency_graph.to_a
    @decorators.dup
  end

  def self.reset!
    @decorators = nil
    @dependency_graph = nil
  end

  def self.decorate object, context, options={}
    frills = decorators

    if subset = options[:with]
      frills.select! {|d| subset.include? d}
    end

    frills.each do |f|
      object.extend f if f.frill? object, context
    end

    object
  end

  def self.dependency_graph
    @dependency_graph ||= DependencyGraph.new
  end

  module ClassMethods
    def before decorator
      Frill.dependency_graph.move_before self, decorator
    end

    def after decorator
      Frill.dependency_graph.move_before decorator, self
    end
  end

  class DependencyGraph
    def initialize
      @nodes = {}
    end

    def add label
      nodes[label] ||= Node.new label
    end

    def move_before label1, label2
      node1 = add label1
      node2 = add label2

      node1.move_before node2

      CycleDetecter.detect! nodes
    end

    def [](label)
      nodes[label]
    end

    def empty?
      nodes.empty?
    end

    def include? label
      nodes[label]
    end

    def index label
      to_a.index label
    end

    def to_a
      array = []

      nodes.values.each do |node|
        array += construct_array(node) unless array.include? node.label
      end

      array
    end

    private
    attr_reader :nodes

    def construct_array node
      array = []
      current_node = node.first

      while current_node
        array << current_node.label
        current_node = current_node.next
      end

      array
    end

    class CycleDetecter
      def self.detect! nodes
        new(nodes).detect!
      end

      def initialize nodes
        @nodes = nodes
        @visited = {}
      end

      def detect!
        nodes.values.each do |node|
          fan_out node unless visited[node.label]
        end
      end

      private
      attr_reader :nodes, :visited

      def fan_out node
        visited[node.label] = true

        fan :next, node
        fan :previous, node
      end

      def fan direction, start_node
        current_node = start_node.send direction

        while current_node
          raise Frill::CyclicDependency if visited[current_node.label]
          visited[current_node.label] = true
          current_node = current_node.send direction
        end
      end
    end

    class Node
      attr_accessor :next, :previous
      attr_reader :label

      def initialize(label)
        @label  = label
        @next = nil
        @previous  = nil
      end

      def move_before node
        next_node = node.first
        previous_node = self.last

        previous_node.next = next_node
        next_node.previous = previous_node
      end

      def first
        first_node = self
        first_node = first_node.previous while first_node.previous
        first_node
      end

      def last
        last_node = self
        last_node = last_node.next while last_node.next
        last_node
      end
    end
  end
end