class JSON::Pure::Parser - JSON implemention for Ruby

# class JSON::Pure::Parser

This class implements the JSON parser that is used to parse a JSON string into a Ruby data structure.

### Constants

ARRAY_CLOSE
ARRAY_OPEN
COLLECTION_DELIMITER
EMPTY_8BIT_STRING
FALSE
FLOAT
IGNORE
INFINITY
INTEGER
MINUS_INFINITY
NAN
NULL
OBJECT_CLOSE
OBJECT_OPEN
PAIR_DELIMITER
STRING
TRUE
UNESCAPE_MAP

  Unescape characters in strings.

UNPARSED

### Public Class Methods

new(source, opts = {}) click to toggle source

Creates a new JSON::Pure::Parser instance for the string source.

It will be configured by the opts hash. opts can have the following keys:

- max_nesting: The maximum depth of nesting allowed in the parsed data structures. Disable depth checking with :max_nesting => false|nil|0, it defaults to 100.

- allow_nan: If set to true, allow NaN, Infinity and -Infinity in defiance of RFC 7159 to be parsed by the Parser. This option defaults to false.

- symbolize_names: If set to true, returns symbols for the names (keys) in a JSON object. Otherwise strings are returned, which is also the default. It's not possible to use this option in conjunction with the create_additions option.

- create_additions: If set to true, the Parser creates additions when if a matching class and create_id was found. This option defaults to false.

- object_class: Defaults to Hash

- array_class: Defaults to Array

Calls superclass method

    # File lib/json/pure/parser.rb, line 73
    def initialize(source, opts = {})
      opts ||= {}
      source = convert_encoding source
      super source
      if !opts.key?(:max_nesting) # defaults to 100
        @max_nesting = 100
      elsif opts[:max_nesting]
        @max_nesting = opts[:max_nesting]
      else
        @max_nesting = 0
      end
      @allow_nan = !!opts[:allow_nan]
      @symbolize_names = !!opts[:symbolize_names]
      if opts.key?(:create_additions)
        @create_additions = !!opts[:create_additions]
      else
        @create_additions = false
      end
      @symbolize_names && @create_additions and raise ArgumentError,
        'options :symbolize_names and :create_additions cannot be used '           'in conjunction'
      @create_id = @create_additions ? JSON.create_id : nil
      @object_class = opts[:object_class] || Hash
      @array_class  = opts[:array_class] || Array
      @match_string = opts[:match_string]
    end

### Public Instance Methods

convert_encoding(source) click to toggle source

    # File lib/json/pure/parser.rb, line 127
    def convert_encoding(source)
      if source.respond_to?(:to_str)
        source = source.to_str
      else
        raise TypeError,
          "#{source.inspect} is not like a string"
      end
      if source.encoding != ::Encoding::ASCII_8BIT
        source = source.encode(::Encoding::UTF_8)
        source.force_encoding(::Encoding::ASCII_8BIT)
      end
      source
    end

parse() click to toggle source

Parses the current JSON string source and returns the complete data structure as a result.

    # File lib/json/pure/parser.rb, line 109
        def parse
          reset
          obj = nil
          while !eos? && skip(IGNORE) do end
          if eos?
            raise ParserError, "source is not valid JSON!"
          else
            obj = parse_value
            UNPARSED.equal?(obj) and raise ParserError,
              "source is not valid JSON!"
          end
          while !eos? && skip(IGNORE) do end
          eos? or raise ParserError, "source is not valid JSON!"
          obj
        end

        private

        def convert_encoding(source)
          if source.respond_to?(:to_str)
            source = source.to_str
          else
            raise TypeError,
              "#{source.inspect} is not like a string"
          end
          if source.encoding != ::Encoding::ASCII_8BIT
            source = source.encode(::Encoding::UTF_8)
            source.force_encoding(::Encoding::ASCII_8BIT)
          end
          source
        end

        # Unescape characters in strings.
        UNESCAPE_MAP = Hash.new { |h, k| h[k] = k.chr }
        UNESCAPE_MAP.update({
          ?"  => '"',
          ?\\ => '\',
          ?/  => '/',
          ?b  => "\b",
          ?f  => "\f",
          ?n  => "\n",
          ?r  => "\r",
          ?t  => "\t",
          ?u  => nil,
        })

        EMPTY_8BIT_STRING = ''
        if ::String.method_defined?(:encode)
          EMPTY_8BIT_STRING.force_encoding Encoding::ASCII_8BIT
        end

        def parse_string
          if scan(STRING)
            return '' if self[1].empty?
            string = self[1].gsub(%r((?:\[\bfnrt"/]|(?:\u(?:[A-Fa-f\d]{4}))+|\[\x20-\xff]))n) do |c|
              if u = UNESCAPE_MAP[$&[1]]
                u
              else # \uXXXX
                bytes = EMPTY_8BIT_STRING.dup
                i = 0
                while c[6 * i] == ?\\ && c[6 * i + 1] == ?u
                  bytes << c[6 * i + 2, 2].to_i(16) << c[6 * i + 4, 2].to_i(16)
                  i += 1
                end
                JSON.iconv('utf-8', 'utf-16be', bytes)
              end
            end
            if string.respond_to?(:force_encoding)
              string.force_encoding(::Encoding::UTF_8)
            end
            if @create_additions and @match_string
              for (regexp, klass) in @match_string
                klass.json_creatable? or next
                string =~ regexp and return klass.json_create(string)
              end
            end
            string
          else
            UNPARSED
          end
        rescue => e
          raise ParserError, "Caught #{e.class} at '#{peek(20)}': #{e}"
        end

        def parse_value
          case
          when scan(FLOAT)
            Float(self[1])
          when scan(INTEGER)
            Integer(self[1])
          when scan(TRUE)
            true
          when scan(FALSE)
            false
          when scan(NULL)
            nil
          when !UNPARSED.equal?(string = parse_string)
            string
          when scan(ARRAY_OPEN)
            @current_nesting += 1
            ary = parse_array
            @current_nesting -= 1
            ary
          when scan(OBJECT_OPEN)
            @current_nesting += 1
            obj = parse_object
            @current_nesting -= 1
            obj
          when @allow_nan && scan(NAN)
            NaN
          when @allow_nan && scan(INFINITY)
            Infinity
          when @allow_nan && scan(MINUS_INFINITY)
            MinusInfinity
          else
            UNPARSED
          end
        end

        def parse_array
          raise NestingError, "nesting of #@current_nesting is too deep" if
            @max_nesting.nonzero? && @current_nesting > @max_nesting
          result = @array_class.new
          delim = false
          until eos?
            case
            when !UNPARSED.equal?(value = parse_value)
              delim = false
              result << value
              skip(IGNORE)
              if scan(COLLECTION_DELIMITER)
                delim = true
              elsif match?(ARRAY_CLOSE)
                ;
              else
                raise ParserError, "expected ',' or ']' in array at '#{peek(20)}'!"
              end
            when scan(ARRAY_CLOSE)
              if delim
                raise ParserError, "expected next element in array at '#{peek(20)}'!"
              end
              break
            when skip(IGNORE)
              ;
            else
              raise ParserError, "unexpected token in array at '#{peek(20)}'!"
            end
          end
          result
        end

        def parse_object
          raise NestingError, "nesting of #@current_nesting is too deep" if
            @max_nesting.nonzero? && @current_nesting > @max_nesting
          result = @object_class.new
          delim = false
          until eos?
            case
            when !UNPARSED.equal?(string = parse_string)
              skip(IGNORE)
              unless scan(PAIR_DELIMITER)
                raise ParserError, "expected ':' in object at '#{peek(20)}'!"
              end
              skip(IGNORE)
              unless UNPARSED.equal?(value = parse_value)
                result[@symbolize_names ? string.to_sym : string] = value
                delim = false
                skip(IGNORE)
                if scan(COLLECTION_DELIMITER)
                  delim = true
                elsif match?(OBJECT_CLOSE)
                  ;
                else
                  raise ParserError, "expected ',' or '}' in object at '#{peek(20)}'!"
                end
              else
                raise ParserError, "expected value in object at '#{peek(20)}'!"
              end
            when scan(OBJECT_CLOSE)
              if delim
                raise ParserError, "expected next name, value pair in object at '#{peek(20)}'!"
              end
              if @create_additions and klassname = result[@create_id]
                klass = JSON.deep_const_get klassname
                break unless klass and klass.json_creatable?
                result = klass.json_create(result)
              end
              break
            when skip(IGNORE)
              ;
            else
              raise ParserError, "unexpected token in object at '#{peek(20)}'!"
            end
          end
          result
        end
      end
    end

parse_array() click to toggle source

    # File lib/json/pure/parser.rb, line 228
    def parse_array
      raise NestingError, "nesting of #@current_nesting is too deep" if
        @max_nesting.nonzero? && @current_nesting > @max_nesting
      result = @array_class.new
      delim = false
      until eos?
        case
        when !UNPARSED.equal?(value = parse_value)
          delim = false
          result << value
          skip(IGNORE)
          if scan(COLLECTION_DELIMITER)
            delim = true
          elsif match?(ARRAY_CLOSE)
            ;
          else
            raise ParserError, "expected ',' or ']' in array at '#{peek(20)}'!"
          end
        when scan(ARRAY_CLOSE)
          if delim
            raise ParserError, "expected next element in array at '#{peek(20)}'!"
          end
          break
        when skip(IGNORE)
          ;
        else
          raise ParserError, "unexpected token in array at '#{peek(20)}'!"
        end
      end
      result
    end

parse_object() click to toggle source

    # File lib/json/pure/parser.rb, line 260
    def parse_object
      raise NestingError, "nesting of #@current_nesting is too deep" if
        @max_nesting.nonzero? && @current_nesting > @max_nesting
      result = @object_class.new
      delim = false
      until eos?
        case
        when !UNPARSED.equal?(string = parse_string)
          skip(IGNORE)
          unless scan(PAIR_DELIMITER)
            raise ParserError, "expected ':' in object at '#{peek(20)}'!"
          end
          skip(IGNORE)
          unless UNPARSED.equal?(value = parse_value)
            result[@symbolize_names ? string.to_sym : string] = value
            delim = false
            skip(IGNORE)
            if scan(COLLECTION_DELIMITER)
              delim = true
            elsif match?(OBJECT_CLOSE)
              ;
            else
              raise ParserError, "expected ',' or '}' in object at '#{peek(20)}'!"
            end
          else
            raise ParserError, "expected value in object at '#{peek(20)}'!"
          end
        when scan(OBJECT_CLOSE)
          if delim
            raise ParserError, "expected next name, value pair in object at '#{peek(20)}'!"
          end
          if @create_additions and klassname = result[@create_id]
            klass = JSON.deep_const_get klassname
            break unless klass and klass.json_creatable?
            result = klass.json_create(result)
          end
          break
        when skip(IGNORE)
          ;
        else
          raise ParserError, "unexpected token in object at '#{peek(20)}'!"
        end
      end
      result
    end

parse_string() click to toggle source

    # File lib/json/pure/parser.rb, line 160
    def parse_string
      if scan(STRING)
        return '' if self[1].empty?
        string = self[1].gsub(%r((?:\[\bfnrt"/]|(?:\u(?:[A-Fa-f\d]{4}))+|\[\x20-\xff]))n) do |c|
          if u = UNESCAPE_MAP[$&[1]]
            u
          else # \uXXXX
            bytes = EMPTY_8BIT_STRING.dup
            i = 0
            while c[6 * i] == ?\\ && c[6 * i + 1] == ?u
              bytes << c[6 * i + 2, 2].to_i(16) << c[6 * i + 4, 2].to_i(16)
              i += 1
            end
            JSON.iconv('utf-8', 'utf-16be', bytes)
          end
        end
        if string.respond_to?(:force_encoding)
          string.force_encoding(::Encoding::UTF_8)
        end
        if @create_additions and @match_string
          for (regexp, klass) in @match_string
            klass.json_creatable? or next
            string =~ regexp and return klass.json_create(string)
          end
        end
        string
      else
        UNPARSED
      end
    rescue => e
      raise ParserError, "Caught #{e.class} at '#{peek(20)}': #{e}"
    end

parse_value() click to toggle source

    # File lib/json/pure/parser.rb, line 193
    def parse_value
      case
      when scan(FLOAT)
        Float(self[1])
      when scan(INTEGER)
        Integer(self[1])
      when scan(TRUE)
        true
      when scan(FALSE)
        false
      when scan(NULL)
        nil
      when !UNPARSED.equal?(string = parse_string)
        string
      when scan(ARRAY_OPEN)
        @current_nesting += 1
        ary = parse_array
        @current_nesting -= 1
        ary
      when scan(OBJECT_OPEN)
        @current_nesting += 1
        obj = parse_object
        @current_nesting -= 1
        obj
      when @allow_nan && scan(NAN)
        NaN
      when @allow_nan && scan(INFINITY)
        Infinity
      when @allow_nan && scan(MINUS_INFINITY)
        MinusInfinity
      else
        UNPARSED
      end
    end

reset() click to toggle source
Calls superclass method

    # File lib/json/pure/parser.rb, line 102
    def reset
      super
      @current_nesting = 0
    end