lib/puma/json_serialization.rb

Summary

Maintainability
A
2 hrs
Test Coverage
# frozen_string_literal: true
require 'stringio'

module Puma

  # Puma deliberately avoids the use of the json gem and instead performs JSON
  # serialization without any external dependencies. In a puma cluster, loading
  # any gem into the puma master process means that operators cannot use a
  # phased restart to upgrade their application if the new version of that
  # application uses a different version of that gem. The json gem in
  # particular is additionally problematic because it leverages native
  # extensions. If the puma master process relies on a gem with native
  # extensions and operators remove gems from disk related to old releases,
  # subsequent phased restarts can fail.
  #
  # The implementation of JSON serialization in this module is not designed to
  # be particularly full-featured or fast. It just has to handle the few places
  # where Puma relies on JSON serialization internally.

  module JSONSerialization
    QUOTE = /"/
    BACKSLASH = /\\/
    CONTROL_CHAR_TO_ESCAPE = /[\x00-\x1F]/ # As required by ECMA-404
    CHAR_TO_ESCAPE = Regexp.union QUOTE, BACKSLASH, CONTROL_CHAR_TO_ESCAPE

    class SerializationError < StandardError; end

    class << self
      def generate(value)
        StringIO.open do |io|
          serialize_value io, value
          io.string
        end
      end

      private

      def serialize_value(output, value)
        case value
        when Hash
          output << '{'
          value.each_with_index do |(k, v), index|
            output << ',' if index != 0
            serialize_object_key output, k
            output << ':'
            serialize_value output, v
          end
          output << '}'
        when Array
          output << '['
          value.each_with_index do |member, index|
            output << ',' if index != 0
            serialize_value output, member
          end
          output << ']'
        when Integer, Float
          output << value.to_s
        when String
          serialize_string output, value
        when true
          output << 'true'
        when false
          output << 'false'
        when nil
          output << 'null'
        else
          raise SerializationError, "Unexpected value of type #{value.class}"
        end
      end

      def serialize_string(output, value)
        output << '"'
        output << value.gsub(CHAR_TO_ESCAPE) do |character|
          case character
          when BACKSLASH
            '\\\\'
          when QUOTE
            '\\"'
          when CONTROL_CHAR_TO_ESCAPE
            '\u%.4X' % character.ord
          end
        end
        output << '"'
      end

      def serialize_object_key(output, value)
        case value
        when Symbol, String
          serialize_string output, value.to_s
        else
          raise SerializationError, "Could not serialize object of type #{value.class} as object key"
        end
      end
    end
  end
end