lib/http/cache/headers.rb



require "delegate"

require "http/errors"
require "http/headers"

module HTTP
  class Cache
    # Convenience methods around cache control headers.
    class Headers < ::SimpleDelegator
      def initialize(headers)
        if headers.is_a? HTTP::Headers
          super headers
        else
          super HTTP::Headers.coerce headers
        end
      end

      # @return [Boolean] does this message force revalidation
      def forces_revalidation?
        must_revalidate? || max_age == 0
      end

      # @return [Boolean] does the cache control include 'must-revalidate'
      def must_revalidate?
        matches?(/\bmust-revalidate\b/i)
      end

      # @return [Boolean] does the cache control include 'no-cache'
      def no_cache?
        matches?(/\bno-cache\b/i)
      end

      # @return [Boolean] does the cache control include 'no-stor'
      def no_store?
        matches?(/\bno-store\b/i)
      end

      # @return [Boolean] does the cache control include 'public'
      def public?
        matches?(/\bpublic\b/i)
      end

      # @return [Boolean] does the cache control include 'private'
      def private?
        matches?(/\bprivate\b/i)
      end

      # @return [Numeric] the max number of seconds this message is
      # considered fresh.
      def max_age
        explicit_max_age || seconds_til_expires || Float::INFINITY
      end

      # @return [Boolean] is the vary header set to '*'
      def vary_star?
        get("Vary").any? { |v| "*" == v.strip }
      end

      private

      # @return [Boolean] true when cache-control header matches the pattern
      def matches?(pattern)
        get("Cache-Control").any? { |v| v =~ pattern }
      end

      # @return [Numeric] number of seconds until the time in the
      # expires header is reached.
      #
      # ---
      # Some servers send a "Expire: -1" header which must be treated as expired
      def seconds_til_expires
        get("Expires").
          map { |e| http_date_to_ttl(e) }.
          max
      end

      def http_date_to_ttl(t_str)
        ttl = to_time_or_epoch(t_str) - Time.now

        ttl < 0 ? 0 : ttl
      end

      # @return [Time] parses t_str at a time; if that fails returns epoch time
      def to_time_or_epoch(t_str)
        Time.httpdate(t_str)
      rescue ArgumentError
        Time.at(0)
      end

      # @return [Numeric] the value of the max-age component of cache control
      def explicit_max_age
        get("Cache-Control").
          map { |v| (/max-age=(\d+)/i).match(v) }.
          compact.
          map { |m| m[1].to_i }.
          max
      end
    end
  end
end