src/derails/action_dispatch/middleware/flash.rb
# frozen_string_literal: true

# :markup: markdown

require "active_support/core_ext/hash/keys"

module ActionDispatch
  # # Action Dispatch Flash
  #
  # The flash provides a way to pass temporary primitive-types (String, Array,
  # Hash) between actions. Anything you place in the flash will be exposed to the
  # very next action and then cleared out. This is a great way of doing notices
  # and alerts, such as a create action that sets `flash[:notice] = "Post
  # successfully created"` before redirecting to a display action that can then
  # expose the flash to its template. Actually, that exposure is automatically
  # done.
  #
  #     class PostsController < ActionController::Base
  #       def create
  #         # save post
  #         flash[:notice] = "Post successfully created"
  #         redirect_to @post
  #       end
  #
  #       def show
  #         # doesn't need to assign the flash notice to the template, that's done automatically
  #       end
  #     end
  #
  # Then in `show.html.erb`:
  #
  #     <% if flash[:notice] %>
  #       <div class="notice"><%= flash[:notice] %></div>
  #     <% end %>
  #
  # Since the `notice` and `alert` keys are a common idiom, convenience accessors
  # are available:
  #
  #     flash.alert = "You must be logged in"
  #     flash.notice = "Post successfully created"
  #
  # This example places a string in the flash. And of course, you can put as many
  # as you like at a time too. If you want to pass non-primitive types, you will
  # have to handle that in your application. Example: To show messages with links,
  # you will have to use sanitize helper.
  #
  # Just remember: They'll be gone by the time the next action has been performed.
  #
  # See docs on the FlashHash class for more details about the flash.
  class Flash
    KEY = "action_dispatch.request.flash_hash"

    module RequestMethods
      # Access the contents of the flash. Returns a ActionDispatch::Flash::FlashHash.
      #
      # See ActionDispatch::Flash for example usage.
      def flash
        flash = flash_hash
        return flash if flash
        self.flash = Flash::FlashHash.from_session_value(session["flash"])
      end

      def flash=(flash)
        set_header Flash::KEY, flash
      end

      def flash_hash # :nodoc:
        get_header Flash::KEY
      end

      def commit_flash # :nodoc:
        return unless session.enabled?

        if flash_hash && (flash_hash.present? || session.key?("flash"))
          session["flash"] = flash_hash.to_session_value
          self.flash = flash_hash.dup
        end

        if session.loaded? && session.key?("flash") && session["flash"].nil?
          session.delete("flash")
        end
      end

      def reset_session # :nodoc:
        super
        self.flash = nil
      end
    end

    class FlashNow # :nodoc:
      attr_accessor :flash

      def initialize(flash)
        @flash = flash
      end

      def []=(k, v)
        k = k.to_s
        @flash[k] = v
        @flash.discard(k)
        v
      end

      def [](k)
        @flash[k.to_s]
      end

      # Convenience accessor for `flash.now[:alert]=`.
      def alert=(message)
        self[:alert] = message
      end

      # Convenience accessor for `flash.now[:notice]=`.
      def notice=(message)
        self[:notice] = message
      end
    end

    class FlashHash
      include Enumerable

      def self.from_session_value(value) # :nodoc:
        case value
        when FlashHash # Rails 3.1, 3.2
          flashes = value.instance_variable_get(:@flashes)
          if discard = value.instance_variable_get(:@used)
            flashes.except!(*discard)
          end
          new(flashes, flashes.keys)
        when Hash # Rails 4.0
          flashes = value["flashes"]
          if discard = value["discard"]
            flashes.except!(*discard)
          end
          new(flashes, flashes.keys)
        else
          new
        end
      end

      # Builds a hash containing the flashes to keep for the next request. If there
      # are none to keep, returns `nil`.
      def to_session_value # :nodoc:
        flashes_to_keep = @flashes.except(*@discard)
        return nil if flashes_to_keep.empty?
        { "discard" => [], "flashes" => flashes_to_keep }
      end

      def initialize(flashes = {}, discard = []) # :nodoc:
        @discard = Set.new(stringify_array(discard))
        @flashes = flashes.stringify_keys
        @now     = nil
      end

      def initialize_copy(other)
        if other.now_is_loaded?
          @now = other.now.dup
          @now.flash = self
        end
        super
      end

      def []=(k, v)
        k = k.to_s
        @discard.delete k
        @flashes[k] = v
      end

      def [](k)
        @flashes[k.to_s]
      end

      def update(h) # :nodoc:
        @discard.subtract stringify_array(h.keys)
        @flashes.update h.stringify_keys
        self
      end

      def keys
        @flashes.keys
      end

      def key?(name)
        @flashes.key? name.to_s
      end

      # Immediately deletes the single flash entry. Use this method when you want
      # remove the message within the current action. See also #discard.
      def delete(key)
        key = key.to_s
        @discard.delete key
        @flashes.delete key
        self
      end

      def to_hash
        @flashes.dup
      end

      def empty?
        @flashes.empty?
      end

      def clear
        @discard.clear
        @flashes.clear
      end

      def each(&block)
        @flashes.each(&block)
      end

      alias :merge! :update

      def replace(h) # :nodoc:
        @discard.clear
        @flashes.replace h.stringify_keys
        self
      end

      # Sets a flash that will not be available to the next action, only to the
      # current.
      #
      #     flash.now[:message] = "Hello current action"
      #
      # This method enables you to use the flash as a central messaging system in your
      # app. When you need to pass an object to the next action, you use the standard
      # flash assign (`[]=`). When you need to pass an object to the current action,
      # you use `now`, and your object will vanish when the current action is done.
      #
      # Entries set via `now` are accessed the same way as standard entries:
      # `flash['my-key']`.
      #
      # Also, brings two convenience accessors:
      #
      #     flash.now.alert = "Beware now!"
      #     # Equivalent to flash.now[:alert] = "Beware now!"
      #
      #     flash.now.notice = "Good luck now!"
      #     # Equivalent to flash.now[:notice] = "Good luck now!"
      def now
        @now ||= FlashNow.new(self)
      end

      # Keeps either the entire current flash or a specific flash entry available for
      # the next action:
      #
      #     flash.keep            # keeps the entire flash
      #     flash.keep(:notice)   # keeps only the "notice" entry, the rest of the flash is discarded
      def keep(k = nil)
        k = k.to_s if k
        @discard.subtract Array(k || keys)
        k ? self[k] : self
      end

      # Marks the entire flash or a single flash entry to be discarded by the end of
      # the current action:
      #
      #     flash.discard              # discard the entire flash at the end of the current action
      #     flash.discard(:warning)    # discard only the "warning" entry at the end of the current action
      #
      # Use this method when you want to display the message in the current action but
      # not in the next one. See also #delete.
      def discard(k = nil)
        k = k.to_s if k
        @discard.merge Array(k || keys)
        k ? self[k] : self
      end

      # Mark for removal entries that were kept, and delete unkept ones.
      #
      # This method is called automatically by filters, so you generally don't need to
      # care about it.
      def sweep # :nodoc:
        @discard.each { |k| @flashes.delete k }
        @discard.replace @flashes.keys
      end

      # Convenience accessor for `flash[:alert]`.
      def alert
        self[:alert]
      end

      # Convenience accessor for `flash[:alert]=`.
      def alert=(message)
        self[:alert] = message
      end

      # Convenience accessor for `flash[:notice]`.
      def notice
        self[:notice]
      end

      # Convenience accessor for `flash[:notice]=`.
      def notice=(message)
        self[:notice] = message
      end

      protected
        def now_is_loaded?
          @now
        end

      private
        def stringify_array(array) # :doc:
          array.map do |item|
            item.kind_of?(Symbol) ? item.to_s : item
          end
        end
    end

    def self.new(app) app; end
  end

  class Request
    prepend Flash::RequestMethods
  end
end
