# frozen_string_literal: true require_relative "menuable/version" require "active_support/all" module Menuable extend ActiveSupport::Concern class_methods do def resource(resource_name, **options, &block) resources(resource_name, single: true, **options, &block) end def resources(resource_name, single: false, **options, &block) namespace = name.to_s.deconstantize.constantize menu = Class.new(MenuDefinition) menu.options = options menu.single = single menu.resource_name = resource_name menu.model_name = ActiveModel::Name.new(nil, nil, "#{namespace}/#{resource_name.to_s.classify}") menu.instance_eval(&block) if block class_eval do class_attribute :menu, default: menu end end end class MenuDefinition class_attribute :options class_attribute :resource_name class_attribute :model_name class_attribute :single NOTHING = ->(_) { true } def self.single? single end def self.loyalty(&value) return (@loyalty || NOTHING) if value.nil? @loyalty = value end def self.actions(&value) return @actions if value.nil? @actions = value end end class MenuContext attr_reader :context def initialize(menus:, context:) @menus = menus @context = context end def each # rubocop:todo Metrics/MethodLength return enum_for(:each) unless block_given? @menus.each do |config| case config in divider: yield config in items: yield menu({ **config, items: items.filter_map { |item| menu(item) } }) else menu(config).try { yield _1 } end end end private def menu(config) return unless approved?(config) return { **config, active: active?(config) } if config[:controller].nil? { **config, active: active?(config), path: path(config) } end def path(config) path = context.url_for(config[:controller].menu) if config[:controller] path || config[:path] || "/" end def active?(menu) case menu in items: items.any? do |item| path = path(item) path = path[0..-2] if path.end_with?("/") context.request.path.start_with?(path) end else path = path(menu) path = path[0..-2] if path.end_with?("/") context.request.path.start_with?(path) end end def approved?(config) if config[:loyalty] context.public_send(:"#{config[:loyalty]}?") elsif config[:controller] config[:controller].menu.loyalty.call(context.current_user) else true end end end class Menu def initialize(namespace, path) # rubocop:todo Metrics/MethodLength, Metrics/AbcSize, Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity extract_namespace = lambda do |name| if name namespaces = name.split("/") namespaces.pop namespaces else [] end end @controllers = {} @menus = YAML.load_file(path).map do |config| config.deep_symbolize_keys! namespaces = extract_namespace.call(config[:name]) controller = "#{namespace}/#{config[:name]&.pluralize}_controller".classify.safe_constantize @controllers[namespaces] ||= [] @controllers[namespaces] << controller if controller config[:items]&.each do |item| item[:controller] = "#{namespace}/#{item[:name]&.pluralize}_controller".classify.safe_constantize item_namespaces = extract_namespace.call(item[:name]) @controllers[item_namespaces] ||= [] @controllers[item_namespaces] << item[:controller] if item[:controller] end { **config, controller:, namespaces: } end end def all @menus end def call(context) MenuContext.new(menus: @menus, context:) end def first(current_user:) @controllers.values.flatten.each do |controller| break controller.menu if controller.menu.loyalty.call(current_user) end end def routes(routing) # rubocop:todo Metrics/MethodLength, Metrics/CyclomaticComplexity, Metrics/AbcSize, Metrics/PerceivedComplexity controller_mappings = @controllers routing.instance_eval do # rubocop:todo Metrics/BlockLength controller_mappings.each do |namespaces, controllers| # rubocop:todo Metrics/BlockLength define = controllers.map do |controller| if controller.menu.single? lambda do |router| router.resource controller.menu.resource_name do router.instance_eval(&controller.menu.actions) if controller.menu.actions end end else lambda do |router| router.resources controller.menu.resource_name do router.instance_eval(&controller.menu.actions) if controller.menu.actions end end end end case namespaces.length when 3 namespace namespaces[0] do namespace namespaces[1] do namespace namespaces[2] { define.each { _1.call(self) } } end end when 2 namespace namespaces[0] do namespace(namespaces[1]) { define.each { _1.call(self) } } end when 1 namespace(namespaces[0]) { define.each { _1.call(self) } } when 0 define.each { _1.call(self) } end end end end end end