X-Git-Url: http://nitlanguage.org diff --git a/lib/json/static.nit b/lib/json/static.nit index 4ebf1c0..e333b85 100644 --- a/lib/json/static.nit +++ b/lib/json/static.nit @@ -18,64 +18,213 @@ # 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 @@ -83,18 +232,22 @@ redef class Text 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 ""} 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 ""} 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`. @@ -151,6 +304,20 @@ interface JsonMapRead[K: String, V: nullable Jsonable] 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 @@ -159,20 +326,29 @@ interface JsonMapRead[K: String, V: nullable Jsonable] # 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], @@ -195,16 +371,7 @@ class JsonSequenceRead[E: 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 @@ -216,7 +383,31 @@ class JsonSequenceRead[E: nullable Jsonable] 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], @@ -232,11 +423,58 @@ class JsonArray 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 @@ -265,41 +503,8 @@ redef class Nvalue_null 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 @@ -315,7 +520,8 @@ 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 @@ -332,8 +538,11 @@ redef class Nmembers_head 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 @@ -350,7 +559,8 @@ 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