lib/protocol/http/headers.rb in protocol-http-0.16.2 vs lib/protocol/http/headers.rb in protocol-http-0.16.3

- old
+ new

@@ -23,56 +23,107 @@ require_relative 'header/split' require_relative 'header/multiple' require_relative 'header/cookie' require_relative 'header/connection' require_relative 'header/cache_control' +require_relative 'header/etag' require_relative 'header/etags' require_relative 'header/vary' module Protocol module HTTP # Headers are an array of key-value pairs. Some header keys represent multiple values. class Headers Split = Header::Split Multiple = Header::Multiple + TRAILERS = 'trailers' - def self.[] hash - self.new(hash.to_a) - end - - def initialize(fields = nil, indexed = nil) - if fields - @fields = fields.dup + # Construct an instance from a headers Array or Hash. No-op if already an instance of `Headers`. + # @return [Headers] an instance of headers. + def self.[] headers + if headers.is_a?(self) + headers else - @fields = [] + self.new(headers.to_a) end + end + + def initialize(fields = [], indexed = nil) + @fields = fields + @indexed = indexed - if indexed - @indexed = indexed.dup - else - @indexed = nil - end + # Marks where trailers start in the @fields array. + @tail = nil + @deferred = [] end - def dup - self.class.new(@fields, @indexed) + def initialize_dup(other) + super + + @fields = @fields.dup + @indexed = @indexed.dup + @deferred = @deferred.dup end def clear @fields.clear @indexed = nil + @tail = nil + @deferred.clear end # An array of `[key, value]` pairs. attr :fields + # Mark the subsequent headers as trailers. + def trailers! + @tail ||= @fields.size + end + + # @return the trailers if there are any. + def trailers? + @tail != nil + end + + def flatten! + unless @deferred.empty? + @tail ||= @fields.size + + @deferred.each do |key, value| + self.add(key, value.call) + end + end + end + + # Enumerate all trailers, including evaluating all deferred headers. + def trailers(&block) + return nil unless self.include?(TRAILERS) + + return to_enum(:trailers) unless block_given? + + flatten! + + if @tail + @fields.drop(@tail).each(&block) + end + end + def freeze return if frozen? + # Ensure all deferred headers are evaluated: + self.flatten! + # Ensure @indexed is generated: self.to_h + # Remove all trailers: + self.delete(TRAILERS) + + # No longer has stateful trailers: + @tail = nil + @fields.freeze @indexed.freeze super end @@ -87,10 +138,14 @@ def include? key self[key] != nil end + def keys + self.to_h.keys + end + def extract(keys) deleted, @fields = @fields.partition do |field| keys.include?(field.first.downcase) end @@ -101,18 +156,21 @@ end return deleted end - # This is deprecated. - alias slice! extract - # Add the specified header key value pair. # @param key [String] the header key. # @param value [String] the header value to assign. - def add(key, value) - self[key] = value + # @yield dynamically generate the value when used as a trailer. + def add(key, value = nil, &block) + if block_given? + @deferred << [key, block] + self[TRAILERS] = key + else + self[key] = value + end end # Set the specified header key to the specified value, replacing any existing header keys with the same name. # @param key [String] the header key to replace. # @param value [String] the header value to assign. @@ -143,11 +201,11 @@ end @fields << [key, value] end - MERGE_POLICY = { + POLICY = { # Headers which may only be specified once. 'content-type' => false, 'content-disposition' => false, 'content-length' => false, 'user-agent' => false, @@ -159,19 +217,21 @@ 'if-unmodified-since' => false, 'from' => false, 'location' => false, 'max-forwards' => false, + # Custom headers: 'connection' => Header::Connection, 'cache-control' => Header::CacheControl, 'vary' => Header::Vary, # Headers specifically for proxies: 'via' => Split, 'x-forwarded-for' => Split, # Cache validations: + 'etag' => Header::ETag, 'if-match' => Header::ETags, 'if-none-match' => Header::ETags, # Headers which may be specified multiple times, but which can't be concatenated: 'www-authenticate' => Multiple, @@ -192,11 +252,11 @@ return nil end if @indexed return @indexed.delete(key) - elsif policy = MERGE_POLICY[key] + elsif policy = POLICY[key] (key, value), *tail = deleted merged = policy.new(value) tail.each{|k,v| merged << v} @@ -206,10 +266,10 @@ return value end end protected def merge_into(hash, key, value) - if policy = MERGE_POLICY[key] + if policy = POLICY[key] if current_value = hash[key] current_value << value else hash[key] = policy.new(value) end