lib/lookbook/stores/panel_store.rb



module Lookbook
  class PanelStore
    CONFIG_FILE = "config/panels.yml"

    DEFAULTS = {
      label: lambda { |data| data.name.titleize },
      hotkey: nil,
      disabled: false,
      show: true,
      copy: nil,
      locals: {}
    }

    attr_reader :store
    delegate :to_h, to: :store

    def initialize(config = nil)
      @store = {}
      load_config(config)
    end

    def add_panel(name, group_name, *args)
      if get_panel(name)
        raise ConfigError.new("panel with name '#{name}' already exists", scope: "panels.config")
      else
        panel = build_config(name, group_name, *args)
        insert_at_position(group_name, panel.position, panel)
      end
    end

    def update_panel(name, opts = {})
      panel = get_panel(name)
      if panel.present?
        panel.merge!(opts.except(:name, :position))
        if opts.key?(:position)
          remove_panel(name)
          insert_at_position(panel.group, opts[:position], panel)
        end
      else
        not_found!(name)
      end
    end

    def remove_panel(name)
      store.each do |group_name, panels|
        return true unless panels.reject! { |p| p.name == name.to_sym }.nil?
      end
      not_found!(name)
    end

    def load_config(config)
      config.to_h.each do |group_name, panels|
        panels.each do |opts|
          add_panel(opts[:name], group_name, opts.except(:name))
        end
      end
    end

    def get_panel(name, group_name = nil)
      panels(group_name).find { |p| p.name == name.to_sym }
    end

    def count_panels(group_name = nil)
      panels(group_name).count
    end

    def in_group(name)
      store[name.to_sym] ||= []
    end

    def panels(group_name = nil)
      store.to_h.reduce([]) do |result, (name, group_panels)|
        result.push(*group_panels) if group_name.nil? || name == group_name.to_sym
      end
    end

    def self.resolve_config(opts, data)
      if opts[:name].present?
        data = data.is_a?(Store) ? data : Store.new(data)
        data.name = opts[:name].to_s
        resolved = opts.transform_values do |value|
          value.respond_to?(:call) ? value.call(data) : value
        end
        Store.new(resolved)
      else
        raise ConfigError.new(":name key is required when resolving config", scope: "panels.config")
      end
    end

    def self.init_from_config
      new(default_config)
    end

    def self.default_config
      config = ConfigLoader.call(CONFIG_FILE)
      config.each do |group, panels|
        panels.map! do |opts|
          opts.transform_values! do |value|
            (value.is_a?(String) && value.start_with?("->")) ? eval(value) : value # standard:disable Security/Eval
          end
        end
      end
    end

    protected

    def insert_at_position(group_name, position, opts)
      group_panels = in_group(group_name)
      index = insert_index(position, group_panels.count)
      group_panels.insert(index, opts.except!(:position))
    end

    def insert_index(position, items_count)
      index = (position == 0) ? 1 : (position || 0).to_int
      last_position = items_count + 1
      index = last_position if index > last_position
      index - 1
    end

    def build_config(name, group_name, *args)
      opts = if args.many? && args.last.is_a?(Hash)
        args.last.merge({partial: args.first})
      elsif args.any?
        args.first.is_a?(String) ? {partial: args.first} : args.first
      else
        {}
      end
      if opts[:partial].present?
        opts[:name] = name.to_sym
        opts[:group] = group_name.to_sym
        Store.new(DEFAULTS.merge(opts))
      else
        raise ConfigError.new("panels must define a partial path", scope: "panels.config")
      end
    end

    def not_found!(name)
      raise ConfigError.new("could not find panel named '#{name}'", scope: "panels.config")
    end
  end
end