# Static interface to get Nit objects from a Json string.
#
-# `String::json_to_nit_object` returns an equivalent Nit object from
+# `Text::parse_json` returns an equivalent Nit object from
# the Json source. This object can then be type checked by the usual
# languages features (`isa` and `as`).
module static
-import standard
+import error
private import json_parser
private import json_lexer
# Something that can be translated to JSON.
interface Jsonable
# Encode `self` in JSON.
+ #
+ # This is a recursive method which can be refined by any subclasses.
+ # To write any `Serializable` object to JSON, see `serialize_to_json`.
+ #
+ # SEE: `append_json`
fun to_json: String is abstract
+
+ # Use `append_json` to implement `to_json`.
+ #
+ # Therefore, one that redefine `append_json` may use the following
+ # redefinition to link `to_json` and `append_json`:
+ #
+ # ~~~nitish
+ # redef fun to_json do return to_json_by_append
+ # ~~~
+ #
+ # Note: This is not the default implementation of `to_json` in order to
+ # avoid cyclic references between `append_json` and `to_json` when none are
+ # implemented.
+ protected fun to_json_by_append: String do
+ var buffer = new FlatBuffer
+ append_json(buffer)
+ return buffer.to_s
+ end
+
+ # Append the JSON representation of `self` to the specified buffer.
+ #
+ # SEE: `to_json`
+ fun append_json(buffer: Buffer) do buffer.append(to_json)
+
+ # Pretty print JSON string.
+ #
+ # ~~~
+ # var obj = new JsonObject
+ # obj["foo"] = 1
+ # obj["bar"] = true
+ # var arr = new JsonArray
+ # arr.add 2
+ # arr.add false
+ # arr.add "baz"
+ # obj["baz"] = arr
+ # var res = obj.to_pretty_json
+ # var exp = """{
+ # \t"foo": 1,
+ # \t"bar": true,
+ # \t"baz": [2, false, "baz"]
+ # }\n"""
+ # assert res == exp
+ # ~~~
+ fun to_pretty_json: String do
+ var res = new FlatBuffer
+ pretty_json_visit(res, 0)
+ res.add '\n'
+ return res.to_s
+ end
+
+ private fun pretty_json_visit(buffer: FlatBuffer, indent: Int) is abstract
end
redef class Text
super Jsonable
- # Encode `self` in JSON.
+ # Removes JSON-escaping if necessary in a JSON string
#
- # assert "\t\"http://example.com\"\r\n\0\\".to_json ==
- # "\"\\t\\\"http:\\/\\/example.com\\\"\\r\\n\\u0000\\\\\""
- redef fun to_json do
- var buffer = new FlatBuffer
+ # assert "\\\"string\\uD83D\\uDE02\\\"".unescape_json == "\"stringš\""
+ fun unescape_json: Text do
+ if not json_need_escape then return self
+ return self.json_to_nit_string
+ end
+
+ # Does `self` need treatment from JSON to Nit ?
+ #
+ # i.e. is there at least one `\` character in it ?
+ #
+ # assert not "string".json_need_escape
+ # assert "\\\"string\\\"".json_need_escape
+ protected fun json_need_escape: Bool do return has('\\')
+
+ redef fun append_json(buffer) do
buffer.add '\"'
- for i in [0..self.length[ do
+ for i in [0 .. self.length[ do
var char = self[i]
if char == '\\' then
buffer.append "\\\\"
else if char == '\"' then
buffer.append "\\\""
- else if char == '\/' then
- buffer.append "\\/"
- else if char < 16.ascii then
+ else if char < ' ' then
if char == '\n' then
buffer.append "\\n"
else if char == '\r' then
buffer.append "\\r"
else if char == '\t' then
buffer.append "\\t"
- else if char == 0x0C.ascii then
- buffer.append "\\f"
- else if char == 0x08.ascii then
- buffer.append "\\b"
else
- buffer.append "\\u000{char.ascii.to_hex}"
+ buffer.append char.escape_to_utf16
end
- else if char < ' ' then
- buffer.append "\\u00{char.ascii.to_hex}"
else
buffer.add char
end
end
buffer.add '\"'
- return buffer.write_to_string
end
- fun json_to_nit_object: nullable Jsonable do
+ # Escapes `self` from a JSON string to a Nit string
+ #
+ # assert "\\\"string\\\"".json_to_nit_string == "\"string\""
+ # assert "\\nEscape\\t\\n".json_to_nit_string == "\nEscape\t\n"
+ # assert "\\u0041zu\\uD800\\uDFD3".json_to_nit_string == "Azuš"
+ protected fun json_to_nit_string: String do
+ var res = new FlatBuffer.with_capacity(bytelen)
+ var i = 0
+ var ln = self.length
+ while i < ln do
+ var char = self[i]
+ if char == '\\' then
+ i += 1
+ char = self[i]
+ if char == 'b' then
+ char = 0x08.code_point
+ else if char == 'f' then
+ char = 0x0C.code_point
+ else if char == 'n' then
+ char = '\n'
+ else if char == 'r' then
+ char = '\r'
+ else if char == 't' then
+ char = '\t'
+ else if char == 'u' then
+ var u16_esc = from_utf16_digit(i + 1)
+ char = u16_esc.code_point
+ if char.is_surrogate and i + 10 < ln then
+ if self[i + 5] == '\\' and self[i + 6] == 'u' then
+ u16_esc <<= 16
+ u16_esc += from_utf16_digit(i + 7)
+ char = u16_esc.from_utf16_surr.code_point
+ i += 6
+ else
+ char = 0xFFFD.code_point
+ end
+ end
+ i += 4
+ end
+ # `"`, `/` or `\` => Keep `char` as-is.
+ end
+ res.add char
+ i += 1
+ end
+ return res.to_s
+ end
+
+
+ # Encode `self` in JSON.
+ #
+ # ~~~
+ # assert "\t\"http://example.com\"\r\n\0\\".to_json ==
+ # "\"\\t\\\"http://example.com\\\"\\r\\n\\u0000\\\\\""
+ # ~~~
+ redef fun to_json do
+ var b = new FlatBuffer.with_capacity(bytelen)
+ append_json(b)
+ return b.to_s
+ end
+
+ # Parse `self` as JSON.
+ #
+ # If `self` is not a valid JSON document or contains an unsupported escape
+ # sequence, return a `JSONParseError`.
+ #
+ # Example with `JsonObject`:
+ #
+ # var obj = "\{\"foo\": \{\"bar\": true, \"goo\": [1, 2, 3]\}\}".parse_json
+ # assert obj isa JsonObject
+ # assert obj["foo"] isa JsonObject
+ # assert obj["foo"].as(JsonObject)["bar"] == true
+ #
+ # Example with `JsonArray`:
+ #
+ # var arr = "[1, 2, 3]".parse_json
+ # assert arr isa JsonArray
+ # assert arr.length == 3
+ # assert arr.first == 1
+ # assert arr.last == 3
+ #
+ # Example with `String`:
+ #
+ # var str = "\"foo, bar, baz\"".parse_json
+ # assert str isa String
+ # assert str == "foo, bar, baz"
+ #
+ # Example of a syntaxic error:
+ #
+ # var bad = "\{foo: \"bar\"\}".parse_json
+ # assert bad isa JsonParseError
+ # assert bad.position.col_start == 2
+ fun parse_json: nullable Jsonable do
var lexer = new Lexer_json(to_s)
var parser = new Parser_json
var tokens = lexer.lex
var root_node = parser.parse
if root_node isa NStart then
return root_node.n_0.to_nit_object
- else if root_node isa NLexerError then
- var pos = root_node.position
- print "Json lexer error: {root_node.message} at {pos or else "<unknown>"} for {root_node}"
- return null
- else if root_node isa NParserError then
- var pos = root_node.position
- print "Json parsing error: {root_node.message} at {pos or else "<unknown>"} for {root_node}"
- return null
+ else if root_node isa NError then
+ return new JsonParseError(root_node.message, root_node.position)
else abort
end
end
+redef class FlatText
+ redef fun json_need_escape do
+ var its = items
+ for i in [first_byte .. last_byte] do
+ if its[i] == 0x5Cu8 then return true
+ end
+ return false
+ end
+end
+
redef class Buffer
# Append the JSON representation of `jsonable` to `self`.
super MapRead[K, V]
super Jsonable
+ redef fun append_json(buffer) do
+ buffer.append "\{"
+ var it = iterator
+ if it.is_ok then
+ append_json_entry(it, buffer)
+ while it.is_ok do
+ buffer.append ","
+ append_json_entry(it, buffer)
+ end
+ end
+ it.finish
+ buffer.append "\}"
+ end
+
# Encode `self` in JSON.
#
# var obj = new JsonObject
# obj = new JsonObject
# obj["baz"] = null
# assert obj.to_json == "\{\"baz\":null\}"
- redef fun to_json do
- var buffer = new FlatBuffer
- buffer.append "\{"
- var it = iterator
- if it.is_ok then
- append_json_entry(it, buffer)
- while it.is_ok do
+ redef fun to_json do return to_json_by_append
+
+ redef fun pretty_json_visit(buffer, indent) do
+ buffer.append "\{\n"
+ indent += 1
+ var i = 0
+ for k, v in self do
+ buffer.append "\t" * indent
+ buffer.append "\"{k}\": "
+ if v isa JsonObject or v isa JsonArray then
+ v.pretty_json_visit(buffer, indent)
+ else
+ buffer.append v.to_json
+ end
+ if i < length - 1 then
buffer.append ","
- append_json_entry(it, buffer)
end
+ buffer.append "\n"
+ i += 1
end
- it.finish
+ indent -= 1
+ buffer.append "\t" * indent
buffer.append "\}"
- return buffer.write_to_string
end
private fun append_json_entry(iterator: MapIterator[String, nullable Jsonable],
super Jsonable
super SequenceRead[E]
- # Encode `self` in JSON.
- #
- # var arr = new JsonArray.with_items("foo", null)
- # assert arr.to_json == "[\"foo\",null]"
- # arr.pop
- # assert arr.to_json =="[\"foo\"]"
- # arr.pop
- # assert arr.to_json =="[]"
- redef fun to_json do
- var buffer = new FlatBuffer
+ redef fun append_json(buffer) do
buffer.append "["
var it = iterator
if it.is_ok then
end
it.finish
buffer.append "]"
- return buffer.write_to_string
+ end
+
+ # Encode `self` in JSON.
+ #
+ # var arr = new JsonArray.with_items("foo", null)
+ # assert arr.to_json == "[\"foo\",null]"
+ # arr.pop
+ # assert arr.to_json =="[\"foo\"]"
+ # arr.pop
+ # assert arr.to_json =="[]"
+ redef fun to_json do return to_json_by_append
+
+ redef fun pretty_json_visit(buffer, indent) do
+ buffer.append "\["
+ var i = 0
+ for v in self do
+ if v isa JsonObject or v isa JsonArray then
+ v.pretty_json_visit(buffer, indent)
+ else
+ buffer.append v.to_json
+ end
+ if i < length - 1 then buffer.append ", "
+ i += 1
+ end
+ buffer.append "\]"
end
private fun append_json_entry(iterator: Iterator[nullable Jsonable],
super Array[nullable Jsonable]
end
+redef class JsonParseError
+ super Jsonable
+
+ # Get the JSON representation of `self`.
+ #
+ # ~~~
+ # var err = new JsonParseError("foo", new Position(1, 2, 3, 4, 5, 6))
+ # assert err.to_json == "\{\"error\":\"JsonParseError\"," +
+ # "\"position\":\{" +
+ # "\"pos_start\":1,\"pos_end\":2," +
+ # "\"line_start\":3,\"line_end\":4," +
+ # "\"col_start\":5,\"col_end\":6" +
+ # "\},\"message\":\"foo\"\}"
+ # ~~~
+ redef fun to_json do
+ return "\{\"error\":\"JsonParseError\"," +
+ "\"position\":{position.to_json}," +
+ "\"message\":{message.to_json}\}"
+ end
+
+ redef fun pretty_json_visit(buf, indents) do
+ buf.clear
+ buf.append(to_json)
+ end
+end
+
+redef class Position
+ super Jsonable
+
+ # Get the JSON representation of `self`.
+ #
+ # ~~~
+ # var pos = new Position(1, 2, 3, 4, 5, 6)
+ # assert pos.to_json == "\{" +
+ # "\"pos_start\":1,\"pos_end\":2," +
+ # "\"line_start\":3,\"line_end\":4," +
+ # "\"col_start\":5,\"col_end\":6" +
+ # "\}"
+ # ~~~
+ redef fun to_json do
+ return "\{\"pos_start\":{pos_start},\"pos_end\":{pos_end}," +
+ "\"line_start\":{line_start},\"line_end\":{line_end}," +
+ "\"col_start\":{col_start},\"col_end\":{col_end}\}"
+ end
+end
+
################################################################################
# Redef parser
redef class Nvalue
- fun to_nit_object: nullable Jsonable is abstract
+ # The represented value.
+ private fun to_nit_object: nullable Jsonable is abstract
end
redef class Nvalue_number
end
redef class Nstring
- fun to_nit_string: String do
- var res = new FlatBuffer
- var i = 1
- while i < text.length - 1 do
- var char = text[i]
- if char == '\\' then
- i += 1
- char = text[i]
- if char == 'b' then
- char = 0x08.ascii
- else if char == 'f' then
- char = 0x0C.ascii
- else if char == 'n' then
- char = '\n'
- else if char == 'r' then
- char = '\r'
- else if char == 't' then
- char = '\t'
- else if char == 'u' then
- var code = text.substring(i + 1, 4).to_hex
- # TODO UTF-16 escaping is not supported yet.
- if code >= 128 then
- char = '?'
- else
- char = code.ascii
- end
- i += 4
- end
- # `"`, `/` or `\` => Keep `char` as-is.
- end
- res.add char
- i += 1
- end
- return res.write_to_string
- end
+ # The represented string.
+ private fun to_nit_string: String do return text.substring(1, text.length - 2).unescape_json.to_s
end
redef class Nvalue_object
end
redef class Nmembers
- fun pairs: Array[Npair] is abstract
+ #Ā All the key-value pairs.
+ private fun pairs: Array[Npair] is abstract
end
redef class Nmembers_tail
end
redef class Npair
- fun name: String do return n_string.to_nit_string
- fun value: nullable Jsonable do return n_value.to_nit_object
+ # The represented key.
+ private fun name: String do return n_string.to_nit_string
+
+ # The represented value.
+ private fun value: nullable Jsonable do return n_value.to_nit_object
end
redef class Nvalue_array
end
redef class Nelements
- fun items: Array[Nvalue] is abstract
+ # All the items.
+ private fun items: Array[Nvalue] is abstract
end
redef class Nelements_tail