README: document nit_env.sh
[nit.git] / lib / json / static.nit
index f70f7d5..966f2c9 100644 (file)
@@ -1,6 +1,8 @@
 # This file is part of NIT ( http://www.nitlanguage.org ).
 #
 # Copyright 2014 Alexis Laferrière <alexis.laf@xymus.net>
+# Copyright 2014 Alexandre Terrasa <alexandre@moz-concept.com>
+# Copyright 2014 Jean-Christophe Beaupré <jcbrinfo@users.noreply.github.com>
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
 
 # 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 error
 private import json_parser
 private import json_lexer
 
+# Something that can be translated to JSON.
+interface Jsonable
+       # Encode `self` in 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 RopeBuffer
+               append_json(buffer)
+               return buffer.write_to_string
+       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.write_to_string
+       end
+
+       private fun pretty_json_visit(buffer: FlatBuffer, indent: Int) is abstract
+end
+
+redef class Text
+       super Jsonable
+
+       redef fun append_json(buffer) do
+               buffer.add '\"'
+               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.code_point 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.code_point then
+                                       buffer.append "\\f"
+                               else if char == 0x08.code_point then
+                                       buffer.append "\\b"
+                               else
+                                       buffer.append "\\u000{char.code_point.to_hex}"
+                               end
+                       else if char < ' ' then
+                               buffer.append "\\u00{char.code_point.to_hex}"
+                       else
+                               buffer.add char
+                       end
+               end
+               buffer.add '\"'
+       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 return to_json_by_append
+
+       # 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 Buffer
+
+       # Append the JSON representation of `jsonable` to `self`.
+       #
+       # Append `"null"` for `null`.
+       private fun append_json_of(jsonable: nullable Jsonable) do
+               if jsonable isa Jsonable then
+                       append jsonable.to_json
+               else
+                       append "null"
+               end
+       end
+end
+
+redef class Int
+       super Jsonable
+
+       # Encode `self` in JSON.
+       #
+       #     assert 0.to_json == "0"
+       #     assert (-42).to_json == "-42"
+       redef fun to_json do return self.to_s
+end
+
+redef class Float
+       super Jsonable
+
+       # Encode `self` in JSON.
+       #
+       # Note: Because this method use `to_s`, it may lose precision.
+       #
+       # ~~~
+       # # Will not work as expected.
+       # # assert (-0.0).to_json == "-0.0"
+       #
+       # assert (.5).to_json == "0.5"
+       # assert (0.0).to_json == "0.0"
+       # ~~~
+       redef fun to_json do return self.to_s
+end
+
+redef class Bool
+       super Jsonable
+
+       # Encode `self` in JSON.
+       #
+       #     assert true.to_json == "true"
+       #     assert false.to_json == "false"
+       redef fun to_json do return self.to_s
+end
+
+# A map that can be translated into a JSON object.
+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
+       #     obj["foo"] = "bar"
+       #     assert obj.to_json == "\{\"foo\":\"bar\"\}"
+       #     obj = new JsonObject
+       #     obj["baz"] = null
+       #     assert obj.to_json == "\{\"baz\":null\}"
+       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 ","
+                       end
+                       buffer.append "\n"
+                       i += 1
+               end
+               indent -= 1
+               buffer.append "\t" * indent
+               buffer.append "\}"
+       end
+
+       private fun append_json_entry(iterator: MapIterator[String, nullable Jsonable],
+                       buffer: Buffer) do
+               buffer.append iterator.key.to_json
+               buffer.append ":"
+               buffer.append_json_of(iterator.item)
+               iterator.next
+       end
+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]
+
+       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 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],
+                       buffer: Buffer) do
+               buffer.append_json_of(iterator.item)
+               iterator.next
+       end
+end
+
+# A JSON array.
+class JsonArray
+       super JsonSequenceRead[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
+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 Object is abstract
+       # The represented value.
+       private fun to_nit_object: nullable Jsonable is abstract
 end
 
 redef class Nvalue_number
@@ -54,17 +425,47 @@ redef class Nvalue_null
 end
 
 redef class Nstring
-       # FIXME support \n, etc.
-       fun to_nit_string: String do return text.substring(1, text.length-2).
-               replace("\\\\", "\\").replace("\\\"", "\"").replace("\\b", "\b").
-               replace("\\/", "/").replace("\\n", "\n").replace("\\r", "\r").
-               replace("\\t", "\t")
+       # The represented string.
+       private 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.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 code = text.substring(i + 1, 4).to_hex
+                                       # TODO UTF-16 escaping is not supported yet.
+                                       if code >= 128 then
+                                               char = '?'
+                                       else
+                                               char = code.code_point
+                                       end
+                                       i += 4
+                               end
+                               # `"`, `/` or `\` => Keep `char` as-is.
+                       end
+                       res.add char
+                       i += 1
+               end
+               return res.write_to_string
+       end
 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
@@ -75,7 +476,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
@@ -92,14 +494,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
@@ -110,7 +515,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
@@ -125,25 +531,3 @@ end
 redef class Nelements_head
        redef fun items do return [n_value]
 end
-
-redef class String
-       fun json_to_nit_object: nullable Object
-       do
-               var lexer = new Lexer_json(self)
-               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 "<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 abort
-       end
-end