X-Git-Url: http://nitlanguage.org diff --git a/lib/json/static.nit b/lib/json/static.nit index 3743da3..f5eef1d 100644 --- a/lib/json/static.nit +++ b/lib/json/static.nit @@ -16,19 +16,192 @@ # See the License for the specific language governing permissions and # limitations under the License. -# Static interface to get Nit objects from a Json string. +# Static interface to read Nit objects from JSON strings # -# `String::json_to_nit_object` returns an equivalent Nit object from -# the Json source. This object can then be type checked by the usual -# languages features (`isa` and `as`). +# `Text::parse_json` returns a simple Nit object from the JSON source. +# This object can then be type checked as usual with `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 + super Serializable +end + +redef class Text + super Jsonable + + # Removes JSON-escaping if necessary in a JSON string + # + # 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('\\') + + # 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(byte_length) + 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.to_u32.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 + + # 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 + parser.tokens.add_all(tokens) + var root_node = parser.parse + if root_node isa NStart then + return root_node.n_0.to_nit_object + 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 Int + super Jsonable +end + +redef class Float + super Jsonable +end + +redef class Bool + super Jsonable +end + +# A map that can be translated into a JSON object. +interface JsonMapRead[K: String, V: nullable Jsonable] + super MapRead[K, V] + super Jsonable +end + +# A JSON Object. +class JsonObject + super JsonMapRead[String, nullable Jsonable] + super HashMap[String, nullable Jsonable] +end + +# A sequence that can be translated into a JSON array. +class JsonSequenceRead[E: nullable Jsonable] + super Jsonable + super SequenceRead[E] +end + +# A JSON array. +class JsonArray + super JsonSequenceRead[nullable Jsonable] + super Array[nullable Jsonable] +end + +redef class JsonParseError + super Jsonable +end + +redef class Position + super Jsonable +end + +################################################################################ +# Redef parser + redef class Nvalue - fun to_nit_object: nullable Object is abstract + # The represented value. + private fun to_nit_object: nullable Jsonable is abstract end redef class Nvalue_number @@ -57,47 +230,13 @@ 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 - redef fun to_nit_object - do - var obj = new HashMap[String, nullable Object] + redef fun to_nit_object do + var obj = new JsonObject var members = n_members if members != null then var pairs = members.pairs @@ -108,7 +247,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 @@ -125,14 +265,17 @@ redef class Nmembers_head end redef class Npair - fun name: String do return n_string.to_nit_string - fun value: nullable Object 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 redef fun to_nit_object do - var arr = new Array[nullable Object] + var arr = new JsonArray var elements = n_elements if elements != null then var items = elements.items @@ -143,7 +286,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 @@ -158,25 +302,3 @@ end redef class Nelements_head redef fun items do return [n_value] end - -redef class Text - fun json_to_nit_object: nullable Object - do - var lexer = new Lexer_json(to_s) - var parser = new Parser_json - var tokens = lexer.lex - parser.tokens.add_all(tokens) - 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 abort - end -end