Class: LiteralParser

Inherits:
Object
  • Object
show all
Includes:
Expressions
Defined in:
lib/literal_parser.rb,
lib/literal_parser/version.rb

Overview

Note:

Limitations

  • LiteralParser does not support ruby 1.9’s ‘value` syntax.

  • LiteralParser does not currently support all of rubys escape sequences in strings and symbols, e.g. “C-…” type sequences don’t work.

  • Trailing commas in Array and Hash are not supported.

Note:

BigDecimals

You can instruct LiteralParser to parse “12.5” as a bigdecimal and use “12.5e” to have it parsed as float (short for “12.5e0”, equivalent to “1.25e1”)

Note:

Date & Time

LiteralParser supports a subset of ISO-8601 for Date and Time which are not actual valid ruby literals. The form YYYY-MM-DD (e.g. 2012-05-20) is translated to a Date object, and YYYY-MM-DD“T”HH:MM:SS (e.g. 2012-05-20T18:29:52) is translated to a Time object.

LiteralParser

Parse Strings containing ruby literals.

LiteralParser recognizes constants and the following literals:

nil                   # nil
true                  # true
false                 # false
-123                  # Fixnum/Bignum (decimal)
0b1011                # Fixnum/Bignum (binary)
0755                  # Fixnum/Bignum (octal)
0xff                  # Fixnum/Bignum (hexadecimal)
120.30                # Float (optional: BigDecimal)
1e0                   # Float
"foo"                 # String, no interpolation, but \t etc. work
'foo'                 # String, only \\ and \' are escaped
/foo/                 # Regexp
:foo                  # Symbol
:"foo"                # Symbol
2012-05-20            # Date
2012-05-20T18:29:52   # DateTime
[Any, Literals, Here] # Array
{Any => Literals}     # Hash

Examples:

LiteralParser.parse("nil") # => nil
LiteralParser.parse(":foo") # => :foo
LiteralParser.parse("123") # => 123
LiteralParser.parse("1.5") # => 1.5
LiteralParser.parse("1.5", use_big_decimal: true) # => #<BigDecimal:…,'0.15E1',18(18)>
LiteralParser.parse("[1, 2, 3]") # => [1, 2, 3]
LiteralParser.parse("{:a => 1, :b => 2}") # => {:a => 1, :b => 2}

Defined Under Namespace

Modules: Expressions Classes: SyntaxError

Constant Summary collapse

Version =

The version of the LiteralParser library

Gem::Version.new("1.0.1")

Constants included from Expressions

Expressions::DStringEscapes, Expressions::RArrayBegin, Expressions::RArrayEnd, Expressions::RArraySeparator, Expressions::RArrayVoid, Expressions::RBigDecimal, Expressions::RBinaryInteger, Expressions::RConstant, Expressions::RDString, Expressions::RDate, Expressions::RDateTime, Expressions::RFalse, Expressions::RFloat, Expressions::RHashArrow, Expressions::RHashBegin, Expressions::RHashEnd, Expressions::RHashKeySymbol, Expressions::RHashSeparator, Expressions::RHashVoid, Expressions::RHexInteger, Expressions::RInteger, Expressions::RNil, Expressions::ROctalInteger, Expressions::RRegexp, Expressions::RSString, Expressions::RSymbol, Expressions::RTime, Expressions::RTimeZone, Expressions::RTrue

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(string, options = nil) ⇒ LiteralParser

Parse a String, returning the object which it contains.

Parameters:

  • string (String)

    The string which should be parsed

  • options (nil, Hash) (defaults to: nil)

    An options-hash

Options Hash (options):

  • :use_big_decimal (Boolean)

    Whether to use BigDecimal instead of Float for objects like “1.23”. Defaults to false.

  • :constant_base (Boolean)

    Determines from what constant other constants are searched. Defaults to Object (nil is treated as Object too, Object is the toplevel-namespace).



202
203
204
205
206
207
208
# File 'lib/literal_parser.rb', line 202

def initialize(string, options=nil)
  options = options ? options.dup : {}
  @constant_base    = options[:constant_base] # nil means toplevel
  @use_big_decimal  = options.delete(:use_big_decimal) { false }
  @string           = string
  @scanner          = StringScanner.new(string)
end

Instance Attribute Details

#constant_baseModule? (readonly)

Returns Where to lookup constants. Nil is toplevel (equivalent to Object).

Returns:

  • (Module, nil)

    Where to lookup constants. Nil is toplevel (equivalent to Object).



181
182
183
# File 'lib/literal_parser.rb', line 181

def constant_base
  @constant_base
end

#use_big_decimalBoolean (readonly)

Returns True if “1.25” should be parsed into a big-decimal, false if it should be parsed as Float.

Returns:

  • (Boolean)

    True if “1.25” should be parsed into a big-decimal, false if it should be parsed as Float.



186
187
188
# File 'lib/literal_parser.rb', line 186

def use_big_decimal
  @use_big_decimal
end

Class Method Details

.parse(string, options = nil) ⇒ Object

Parse a String, returning the object which it contains.

Examples:

LiteralParser.parse(":foo") # => :foo

Parameters:

  • string (String)

    The string which should be parsed

  • options (nil, Hash) (defaults to: nil)

    An options-hash

Options Hash (options):

  • :use_big_decimal (Boolean)

    Whether to use BigDecimal instead of Float for objects like “1.23”. Defaults to false.

  • :constant_base (Boolean)

    Determines from what constant other constants are searched. Defaults to Object (nil is treated as Object too, Object is the toplevel-namespace).

Returns:

  • (Object)

    The object in the string.

Raises:



171
172
173
174
175
176
177
# File 'lib/literal_parser.rb', line 171

def self.parse(string, options=nil)
  parser  = new(string, options)
  value   = parser.scan_value
  raise SyntaxError, "Unexpected superfluous data: #{parser.rest.inspect}" unless parser.end_of_string?

  value
end

Instance Method Details

#end_of_string?Boolean

Returns Whether the scanner reached the end of the string.

Returns:

  • (Boolean)

    Whether the scanner reached the end of the string.



224
225
226
# File 'lib/literal_parser.rb', line 224

def end_of_string?
  @scanner.eos?
end

#positionInteger

Returns The position of the scanner in the string.

Returns:

  • (Integer)

    The position of the scanner in the string



211
212
213
# File 'lib/literal_parser.rb', line 211

def position
  @scanner.pos
end

#position=(value) ⇒ Object

Moves the scanners position to the given character-index.

Parameters:

  • value (Integer)

    The new position of the scanner



219
220
221
# File 'lib/literal_parser.rb', line 219

def position=(value)
  @scanner.pos = value
end

#restString

Returns The currently unprocessed rest of the string.

Returns:

  • (String)

    The currently unprocessed rest of the string.



229
230
231
# File 'lib/literal_parser.rb', line 229

def rest
  @scanner.rest
end

#scan_valueObject

Scans the string for a single value and advances the parsers position

Returns:

  • (Object)

    the scanned value

Raises:

  • (LiteralParser::SyntaxError)

    When no valid ruby object could be scanned at the given position, a LiteralParser::SyntaxError is raised.



240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
# File 'lib/literal_parser.rb', line 240

def scan_value
  case
    when @scanner.scan(RArrayBegin)    then
      value = []
      @scanner.scan(RArrayVoid)
      if @scanner.scan(RArrayEnd)
        value
      else
        value << scan_value
        while @scanner.scan(RArraySeparator)
          value << scan_value
        end
        raise SyntaxError, "Expected ]" unless @scanner.scan(RArrayVoid) && @scanner.scan(RArrayEnd)

        value
      end
    when @scanner.scan(RHashBegin)    then
      value = {}
      @scanner.scan(RHashVoid)
      if @scanner.scan(RHashEnd)
        value
      else
        if @scanner.scan(RHashKeySymbol)
          key = @scanner[1].to_sym
          @scanner.scan(RHashVoid)
        else
          key = scan_value
          raise SyntaxError, "Expected =>" unless @scanner.scan(RHashArrow)
        end
        val = scan_value
        value[key] = val
        while @scanner.scan(RHashSeparator)
          if @scanner.scan(RHashKeySymbol)
            key = @scanner[1].to_sym
            @scanner.scan(RHashVoid)
          else
            key = scan_value
            raise SyntaxError, "Expected =>" unless @scanner.scan(RHashArrow)
          end
          val = scan_value
          value[key] = val
        end
        raise SyntaxError, "Expected }" unless @scanner.scan(RHashVoid) && @scanner.scan(RHashEnd)

        value
      end
    when @scanner.scan(RConstant)      then eval("#{@constant_base}::#{@scanner[0]}") # yes, I know it's evil, but it's sane due to the regex, also it's less annoying than deep_const_get
    when @scanner.scan(RNil)           then nil
    when @scanner.scan(RTrue)          then true
    when @scanner.scan(RFalse)         then false
    when @scanner.scan(RDateTime)      then
      Time.mktime(@scanner[1], @scanner[2], @scanner[3], @scanner[4], @scanner[5], @scanner[6])
    when @scanner.scan(RDate)          then
      date = @scanner[1].to_i, @scanner[2].to_i, @scanner[3].to_i
      Date.civil(*date)
    when @scanner.scan(RTime)          then
      now = Time.now
      Time.mktime(now.year, now.month, now.day, @scanner[1].to_i, @scanner[2].to_i, @scanner[3].to_i)
    when @scanner.scan(RFloat)         then Float(@scanner.matched.delete('^0-9.e-'))
    when @scanner.scan(RBigDecimal)    then
      data = @scanner.matched.delete('^0-9.-')
      @use_big_decimal ? BigDecimal(data) : Float(data)
    when @scanner.scan(ROctalInteger)  then Integer(@scanner.matched.delete('^0-9-'))
    when @scanner.scan(RHexInteger)    then Integer(@scanner.matched.delete('^xX0-9A-Fa-f-'))
    when @scanner.scan(RBinaryInteger) then Integer(@scanner.matched.delete('^bB01-'))
    when @scanner.scan(RInteger)       then @scanner.matched.delete('^0-9-').to_i
    when @scanner.scan(RRegexp)        then
      source = @scanner[1]
      flags  = 0
      lang   = nil
      if @scanner[2] then
        flags |= Regexp::IGNORECASE if @scanner[2].include?('i')
        flags |= Regexp::EXTENDED if @scanner[2].include?('m')
        flags |= Regexp::MULTILINE if @scanner[2].include?('x')
        lang   = @scanner[2].delete('^nNeEsSuU')[-1,1]
      end
      Regexp.new(source, flags, lang)
    when @scanner.scan(RSymbol)        then
      case @scanner.matched[1,1]
        when '"'
          @scanner.matched[2..-2].gsub(/\\(?:[0-3]?\d\d?|x[A-Fa-f\d]{2}|.)/) { |m|
            DStringEscapes[m]
          }.to_sym
        when "'"
          @scanner.matched[2..-2].gsub(/\\'/, "'").gsub(/\\\\/, "\\").to_sym
        else
          @scanner.matched[1..-1].to_sym
      end
    when @scanner.scan(RSString)       then
      @scanner.matched[1..-2].gsub(/\\'/, "'").gsub(/\\\\/, "\\")
    when @scanner.scan(RDString)       then
      @scanner.matched[1..-2].gsub(/\\(?:[0-3]?\d\d?|x[A-Fa-f\d]{2}|.)/) { |m| DStringEscapes[m] }
    else raise SyntaxError, "Unrecognized pattern: #{@scanner.rest.inspect}"
  end
end