|
| 1 | +# frozen_string_literal: true |
| 2 | + |
| 3 | +module Hooks |
| 4 | + module Utils |
| 5 | + # Utility class for normalizing HTTP headers |
| 6 | + # |
| 7 | + # Provides a robust method to consistently format HTTP headers |
| 8 | + # across the application, handling various edge cases and formats. |
| 9 | + class Normalize |
| 10 | + # Normalize a hash of HTTP headers |
| 11 | + # |
| 12 | + # @param headers [Hash, #each] Headers hash or hash-like object |
| 13 | + # @return [Hash] Normalized headers hash with downcased keys and trimmed values |
| 14 | + # |
| 15 | + # @example Hash of headers normalization |
| 16 | + # headers = { "Content-Type" => " application/json ", "X-GitHub-Event" => "push" } |
| 17 | + # normalized = Normalize.headers(headers) |
| 18 | + # # => { "content-type" => "application/json", "x-github-event" => "push" } |
| 19 | + # |
| 20 | + # @example Handle various input types |
| 21 | + # Normalize.headers(nil) # => nil |
| 22 | + # Normalize.headers({}) # => {} |
| 23 | + # Normalize.headers({ "KEY" => ["a", "b"] }) # => { "key" => "a" } |
| 24 | + # Normalize.headers({ "Key" => 123 }) # => { "key" => "123" } |
| 25 | + def self.headers(headers) |
| 26 | + # Handle nil input |
| 27 | + return nil if headers.nil? |
| 28 | + |
| 29 | + # Fast path for non-enumerable inputs (numbers, etc.) |
| 30 | + return {} unless headers.respond_to?(:each) |
| 31 | + |
| 32 | + normalized = {} |
| 33 | + |
| 34 | + headers.each do |key, value| |
| 35 | + # Skip nil keys or values entirely |
| 36 | + next if key.nil? || value.nil? |
| 37 | + |
| 38 | + # Convert key to string, downcase, and strip in one operation |
| 39 | + normalized_key = key.to_s.downcase.strip |
| 40 | + next if normalized_key.empty? |
| 41 | + |
| 42 | + # Handle different value types efficiently |
| 43 | + normalized_value = case value |
| 44 | + when String |
| 45 | + value.strip |
| 46 | + when Array |
| 47 | + # Take first non-empty element for multi-value headers |
| 48 | + first_valid = value.find { |v| v && !v.to_s.strip.empty? } |
| 49 | + first_valid ? first_valid.to_s.strip : nil |
| 50 | + else |
| 51 | + value.to_s.strip |
| 52 | + end |
| 53 | + |
| 54 | + # Only add if we have a non-empty value |
| 55 | + normalized[normalized_key] = normalized_value if normalized_value && !normalized_value.empty? |
| 56 | + end |
| 57 | + |
| 58 | + normalized |
| 59 | + end |
| 60 | + |
| 61 | + # Normalize a single HTTP header name |
| 62 | + # |
| 63 | + # @param header [String] Header name to normalize |
| 64 | + # @return [String, nil] Normalized header name (downcased and trimmed), or nil if input is nil |
| 65 | + # |
| 66 | + # @example Single header normalization |
| 67 | + # Normalize.header(" Content-Type ") # => "content-type" |
| 68 | + # Normalize.header("X-GitHub-Event") # => "x-github-event" |
| 69 | + # Normalize.header("") # => "" |
| 70 | + # Normalize.header(nil) # => nil |
| 71 | + # |
| 72 | + # @raise [ArgumentError] If input is not a String or nil |
| 73 | + def self.header(header) |
| 74 | + return nil if header.nil? |
| 75 | + if header.is_a?(String) |
| 76 | + header.downcase.strip |
| 77 | + else |
| 78 | + raise ArgumentError, "Expected a String for header normalization" |
| 79 | + end |
| 80 | + end |
| 81 | + end |
| 82 | + end |
| 83 | +end |
0 commit comments