LearnRuby.com

Ruby Training and Ruby on Rails Training exclusively

Parsing JSON

This is a solution to Ruby Quiz #155. The goal of the quiz is to parse JSON into the equivalent Ruby values.

This solution uses the Treetop parsing expression grammar that allows one to specify a grammar and some associated functionality.

The solution consists of a small class to bridge to act as a bridge to the Treetop parser and the Treetop grammar file.

Here is the small Ruby class:


# A solution to RubyQuiz #155.

# Takes a JSON string and parses it into an equivalent Ruby value.

# See http://www.rubyquiz.com/quiz155.html for details.

# The latest version of this solution can also be found at
# http://learnruby.com/examples/ruby-quiz-155.shtml .


require 'rubygems'
require 'treetop'


Treetop.load 'json'


class JSONParser
  def initialize
    @parser = JSONHelperParser.new
  end

  def parse(input)
    result = @parser.parse(input)
    raise "could not parse" if result.nil?
    result.resolve
  end
end

And here is the treetop grammar file for JSON:

# Treetop grammar for JSON.

grammar JSONHelper
  rule value
    spaces v:(simple_literal / string / number / array / object) spaces  {
      def resolve
        v.resolve
      end
    }
  end

  rule spaces
    [ \t\n\r]*
  end

  # SIMPLE LITERALS

  rule simple_literal
    ("true" / "false" / "null")  {
      def resolve
        case text_value
          when "true"  : true
          when "false" : false
          when "null"  : nil
        end
      end
    }
  end

  # NUMBERS

  rule number
    integer fractional:fractional? exponent:exponent? {
      def resolve
        if fractional.text_value.empty? && exponent.text_value.empty?
          integer.text_value.to_i
        else
          text_value.to_f
        end
      end
    }
  end

  rule integer
    "-"? [1-9] digits
    /
    # single digit
    "-"? [0-9]
  end

  rule fractional
    "." digits
  end

  rule exponent
    [eE] [-+]? digits
  end

  rule digits
    [0-9]*
  end

  # STRINGS

  rule string
    "\"\""  {
      def resolve
        ""
      end
    }
    /
    "\"" characters:character* "\""  {
      def resolve
        characters.elements.map { |c| c.resolve }.join
      end
    }
  end

  rule character
    # regular characters
    (!"\"" !"\\" .)+  {
      def resolve
        text_value
      end
    }
    /
    # escaped: \\, \", and \/
    "\\" char:("\"" / "\\" / "/")  {
      def resolve
        char.text_value
      end
    }
    /
    # escaped: \b, \f, \n, \r, and \t
    "\\" char:[bfnrt]  {
      def resolve
        case char.text_value
          when 'b' : "\b"
          when 'f' : "\f"
          when 'n' : "\n"
          when 'r' : "\r"
          when 't' : "\t"
        end
      end
    }
    /
    # Unicode characters \uXXXX (where each X is a hex digit)
    "\\u" digits:(hex_digit hex_digit hex_digit hex_digit)  {
      def resolve
        str = [digits.text_value.hex].pack("U")
      end
    }
  end

  rule hex_digit
    [0-9a-fA-F]
  end

  # ARRAYS

  rule array
    "[" spaces "]"  {
      def resolve
        Array.new
      end
    }
    /
    "[" spaces value_list spaces "]"  {
      def resolve
        value_list.resolve
      end
    }
  end

  rule value_list
    value !(spaces ",")  {
      def resolve
        [ value.resolve ]
      end
    }
    /
    value spaces "," spaces value_list  {
      def resolve
        value_list.resolve.unshift(value.resolve)
      end
    }
  end

  # OBJECTS

  rule object
    "{" spaces "}"  {
      def resolve
        Hash.new
      end
    }
    /
    "{" spaces pair_list spaces "}"  {
      def resolve
        pair_list.resolve
      end
    }
  end

  rule pair_list
    pair !(spaces ",")  {
      def resolve
        { pair.resolve[0] => pair.resolve[1] }
      end
    }
    /
    pair spaces "," spaces pair_list  {
      def resolve
        hash = pair_list.resolve
        hash[pair.resolve[0]] = pair.resolve[1]
        hash
      end
    }
  end

  rule pair
    string spaces ":" spaces value  {
      def resolve
        [ string.resolve, value.resolve ]
      end
    }
  end
end