//
// Copyright (c) 2008, Brian Frank and Andy Frank
// Licensed under the Academic Free License version 3.0
//
// History:
// 18 Sep 08 Kevin McIntire Creation
// 24 Mar 10 Brian Frank json::JsonWriter to util::JsonOutStream
//
**
** JsonInStream reads objects from Javascript Object Notation (JSON).
**
** See [pod doc]`pod-doc#json` for details.
**
@Js
class JsonInStream : InStream
{
**
** Construct by wrapping given input stream.
**
new make(InStream in) : super(in) {}
**
** Read a JSON object from this stream and return one
** of the follow types:
** - null
** - Bool
** - Int
** - Float
** - Str
** - Str:Obj?
** - Obj?[]
**
** See [Str.in]`sys::Str.in` to read from an in-memory string.
**
Obj? readJson()
{
pos = 0
consume
skipWhitespace
return parseVal
}
private Str:Obj? parseObj()
{
pairs := Str:Obj?[:] { ordered = true }
skipWhitespace
expect(JsonToken.objectStart)
while (true)
{
skipWhitespace
if (maybe(JsonToken.objectEnd)) return pairs
// FIXIT would like pair to be a 2-tuple
// OR a map with atom/symbol keys!
// FIXIT what about empty object?
parsePair(pairs)
if (!maybe(JsonToken.comma)) break
}
expect(JsonToken.objectEnd)
return pairs
}
private Void parsePair(Str:Obj? obj)
{
skipWhitespace
key := parseStr
skipWhitespace
expect(JsonToken.colon)
skipWhitespace
val := parseVal
skipWhitespace
obj[key] = val
}
private Obj? parseVal()
{
if (this.cur == JsonToken.quote) return parseStr
else if (this.cur.isDigit || this.cur == '-') return parseNum
else if (this.cur == JsonToken.objectStart) return parseObj
else if (this.cur == JsonToken.arrayStart) return parseArray
else if (this.cur == 't')
{
"true".size.times |->| { consume }
return true
}
else if (this.cur == 'f')
{
"false".size.times |->| { consume }
return false
}
else if (this.cur == 'n')
{
"null".size.times |->| { consume }
return null
}
if (cur < 0) throw err("Unexpected end of stream")
throw err("Unexpected token " + this.cur)
}
private Obj parseNum()
{
integral := StrBuf()
fractional := StrBuf()
exponent := StrBuf()
if (maybe('-'))
integral.add("-")
while (this.cur.isDigit)
{
integral.addChar(this.cur)
consume
}
if (this.cur == '.')
{
decimal := true
consume
while (this.cur.isDigit)
{
fractional.addChar(this.cur)
consume
}
}
if (this.cur == 'e' || this.cur == 'E')
{
exponent.addChar(this.cur)
consume
if (this.cur == '+') consume
else if (this.cur == '-')
{
exponent.addChar(this.cur)
consume
}
while (this.cur.isDigit)
{
exponent.addChar(this.cur)
consume
}
}
Num? num := null
if (fractional.size > 0)
num = Float.fromStr(integral.toStr+"."+fractional.toStr+exponent.toStr)
else if (exponent.size > 0)
num = Float.fromStr(integral.toStr+exponent.toStr)
else num = Int.fromStr(integral.toStr)
return num
}
private Str parseStr()
{
s := StrBuf()
expect(JsonToken.quote)
while( cur != JsonToken.quote )
{
if (cur < 0) throw err("Unexpected end of str literal")
if (cur == '\\')
{
s.addChar(escape)
}
else
{
s.addChar(cur)
consume
}
}
expect(JsonToken.quote)
return s.toStr
}
private Int escape()
{
// consume slash
expect('\\')
// check basics
switch (cur)
{
case 'b': consume; return '\b'
case 'f': consume; return '\f'
case 'n': consume; return '\n'
case 'r': consume; return '\r'
case 't': consume; return '\t'
case '"': consume; return '"'
case '\\': consume; return '\\'
case '/': consume; return '/'
}
// check for uxxxx
if (cur == 'u')
{
consume
n3 := cur.fromDigit(16); consume
n2 := cur.fromDigit(16); consume
n1 := cur.fromDigit(16); consume
n0 := cur.fromDigit(16); consume
if (n3 == null || n2 == null || n1 == null || n0 == null) throw err("Invalid hex value for \\uxxxx")
return n3.shiftl(12).or(n2.shiftl(8)).or(n1.shiftl(4)).or(n0)
}
throw err("Invalid escape sequence")
}
private List parseArray()
{
array := [,]
expect(JsonToken.arrayStart)
skipWhitespace
if (maybe(JsonToken.arrayEnd)) return array
while (true)
{
skipWhitespace
val := parseVal
array.add(val)
skipWhitespace
if (!maybe(JsonToken.comma)) break
}
skipWhitespace
expect(JsonToken.arrayEnd)
return array
}
private Void skipWhitespace()
{
while (this.cur.isSpace)
consume
}
private Void expect(Int tt)
{
if (this.cur < 0) throw err("Unexpected end of stream, expected ${tt.toChar}")
if (this.cur != tt) throw err("Expected ${tt.toChar}, got ${cur.toChar} at ${pos}")
consume
}
private Bool maybe(Int tt)
{
if (this.cur != tt) return false
consume
return true
}
private Void consume()
{
this.cur = readChar ?: -1
pos++
}
private Err err(Str msg) { ParseErr(msg) }
private Int cur := '?'
private Int pos := 0
}
**
** JsonToken represents the tokens in JSON.
**
@Js
internal class JsonToken
{
internal static const Int objectStart := '{'
internal static const Int objectEnd := '}'
internal static const Int colon := ':'
internal static const Int arrayStart := '['
internal static const Int arrayEnd := ']'
internal static const Int comma := ','
internal static const Int quote := '"'
internal static const Int grave := '`'
}