class Overlay < ApplicationRecord belongs_to :user, optional: true has_many :template_overlays, dependent: :destroy has_many :server_templates, through: :template_overlays validates :name, :overlay_type, :slug, presence: true validates :overlay_type, inclusion: { in: %w[system custom] } validates :name, uniqueness: { scope: :user_id, message: "must be unique per user" } validate :slug_is_single_directory before_validation :normalize_slug scope :system_overlays, -> { where(overlay_type: "system").where(user_id: nil) } scope :custom_overlays, -> { where(overlay_type: "custom") } scope :for_user, ->(user) { where("user_id IS NULL OR user_id = ?", user.id) } private def normalize_slug return if slug.blank? && name.blank? # Use provided slug or derive from name source = slug.presence || name self.slug = source.to_s.strip .downcase .gsub(%r{[^\w\-.]}, "_") # Replace non-word chars (except - and .) with underscore .gsub(%r{_+}, "_") # Collapse multiple underscores .sub(%r{^_+}, "") # Strip leading underscores .sub(%r{_+$}, "") # Strip trailing underscores end def slug_is_single_directory return if slug.blank? if slug.match?(%r{[\\/]}) errors.add(:slug, "must be a single directory name (no slashes)") end end end