summaryrefslogtreecommitdiff
path: root/src/json.sml
blob: 81d7b8b4bc8f889c4d1e877c116ae6bf66fdeb38 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
(*******************************************************************************
*  Standard ML JSON parser
*  Copyright (C) 2010  Gian Perrone
*
*  This program is free software: you can redistribute it and/or modify
*  it under the terms of the GNU General Public License as published by
*  the Free Software Foundation, either version 3 of the License, or
*  (at your option) any later version.
*
*  This program is distributed in the hope that it will be useful,
*  but WITHOUT ANY WARRANTY; without even the implied warranty of
*  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
*  GNU General Public License for more details.
*
*  You should have received a copy of the GNU General Public License
*  along with this program.  If not, see <http://www.gnu.org/licenses/>.
******************************************************************************)

signature JSON_CALLBACKS = 
sig
    type json_data

    val json_object   : json_data list -> json_data
    val json_pair     : string * json_data -> json_data
    val json_array    : json_data list -> json_data
    val json_value    : json_data -> json_data
    val json_string   : string -> json_data
    val json_int      : int -> json_data
    val json_real     : real -> json_data
    val json_bool     : bool -> json_data
    val json_null     : unit -> json_data

    val error_handle  : string * int * string -> json_data
end

functor JSONParser (Callbacks : JSON_CALLBACKS) =
struct
   type json_data = Callbacks.json_data

   exception JSONParseError of string * int

   val inputData = ref ""
   val inputPosition = ref 0

   fun isDigit () = Char.isDigit (String.sub (!inputData,0))

   fun ws () = while (String.isPrefix " " (!inputData) orelse
                      String.isPrefix "\n" (!inputData) orelse
                      String.isPrefix "\t" (!inputData) orelse
                      String.isPrefix "\r" (!inputData))
               do (inputData := String.extract (!inputData, 1, NONE))

   fun peek () = String.sub (!inputData,0)
   fun take () = 
      String.sub (!inputData,0) before 
         inputData := String.extract (!inputData, 1, NONE)

   fun matches s = (ws(); String.isPrefix s (!inputData))
   fun consume s =
      if matches s then 
         (inputData := String.extract (!inputData, size s, NONE);
          inputPosition := !inputPosition + size s)
                   else 
         raise JSONParseError ("Expected '"^s^"'", !inputPosition)

   fun parseObject () =
      if not (matches "{") then 
         raise JSONParseError ("Expected '{'", !inputPosition)
      else 
         (consume "{"; ws ();
          if matches "}" then Callbacks.json_object [] before consume "}"
          else 
            (Callbacks.json_object (parseMembers ()) 
               before (ws (); consume "}")))

   and parseMembers () =
      parsePair () :: 
         (if matches "," then (consume ","; parseMembers ()) else [])

   and parsePair () =
      Callbacks.json_pair (parseString (),
                           (ws(); consume ":"; ws(); parseValue ()))

   and parseArray () =
      if not (matches "[") then 
         raise JSONParseError ("Expected '['", !inputPosition)
      else 
        (consume "[";
         if matches "]" then
            Callbacks.json_array [] before consume "]" 
         else
            Callbacks.json_array (parseElements ()) before (ws (); consume "]"))

   and parseElements () =
      parseValue () ::
         (if matches "," then (consume ","; parseElements ()) else [])

   and parseValue () =
      Callbacks.json_value (
         if matches "\"" then Callbacks.json_string (parseString ()) else
         if matches "-" orelse isDigit () then parseNumber () else
         if matches "true" then Callbacks.json_bool true before consume "true" else
         if matches "false" then Callbacks.json_bool false before consume "false" else
         if matches "null" then Callbacks.json_null () before consume "null" else
         if matches "[" then parseArray () else
         if matches "{" then parseObject () else
         raise JSONParseError ("Expected value", !inputPosition))

   and parseString () =
        (ws () ;
         consume ("\"") ;
         parseChars () before consume "\"")

   and parseChars () = 
   let
       val escapedchars = ["n", "r", "b", "f", "t"]
      fun pickChars s =
          if peek () = #"\"" (* " = end of string *)
          then s
          else
              if peek () = #"\\" andalso (String.sub (!inputData, 1)) = #"\""
              then (consume "\\\""; pickChars (s ^ "\""))
              else
                  if peek () = #"\\" andalso String.sub (!inputData, 1) = #"\\" andalso String.sub (!inputData, 2) = #"n"
                  then (consume "\\\\n"; pickChars (s ^ "\\n"))
                  else
                      if peek () = #"\\" andalso (String.sub (!inputData, 1)) = #"n"
                      then (consume "\\n"; pickChars (s ^ "\n"))
                      else
                          if peek () = #"\\" andalso String.sub (!inputData, 1) = #"\\" andalso String.sub (!inputData, 2) = #"r"
                          then (consume "\\\\r"; pickChars (s ^ "\\r"))
                          else
                              if peek () = #"\\" andalso (String.sub (!inputData, 1)) = #"r"
                              then (consume "\\r"; pickChars (s ^ "\r"))
                              else pickChars (s ^ String.str (take ()))
   in
      pickChars ""
   end

   and parseNumber () =
   let
      val i = parseInt ()
   in
      if peek () = #"e" orelse peek () = #"E" then 
         Callbacks.json_int (valOf (Int.fromString (i^parseExp())))
      else if peek () = #"." then
         let
            val f = parseFrac()

            val f' = if peek() = #"e" orelse peek() = #"E" then
                        i ^ f ^ parseExp ()
                     else i ^ f
         in
            Callbacks.json_real (valOf (Real.fromString f'))
         end
      else Callbacks.json_int (valOf (Int.fromString i))
   end

   and parseInt () =
   let
      val f =
          if peek () = #"-"
          then (take (); "~")
          else String.str (take ())
   in
      f ^ parseDigits ()
   end

   and parseDigits () = 
   let
      val r = ref ""
   in
      (while Char.isDigit (peek ()) do
         r := !r ^ String.str (take ());
       !r)
   end

   and parseFrac () =
      (consume "." ;
         "." ^ parseDigits ())

   and parseExp () =
   let
      val _ = 
         if peek () = #"e" orelse
            peek () = #"E" then take ()
         else 
            raise JSONParseError ("Invalid number", !inputPosition)

      val f = if peek () = #"-" then (take (); "~")
               else if peek () = #"+" then (take (); "")
               else ""
   in
      "e" ^ f ^ parseDigits ()
   end

   fun parse s = 
      (inputData := s ;
       inputPosition := 0 ;
       parseObject ()) handle JSONParseError (m,p) => 
         Callbacks.error_handle (m,p,!inputData)
end

structure JsonIntermAst = 
struct 
datatype ast =
         Array of ast list
         | Null
         | Float of real
         | String of string
         | Bool of bool
         | Int of int
         | Pair of (string * ast)
         | Obj of ast list
end

structure Json :> JSON = struct 
datatype json =
         Array of json list
         | Null
         | Float of real
         | String of string
         | Bool of bool
         | Int of int
         | Obj of (string * json) list

fun fromInterm (interm: JsonIntermAst.ast): json =
    case interm of
        JsonIntermAst.Array l => Array (List.map fromInterm l)
      | JsonIntermAst.Null => Null
      | JsonIntermAst.Float r => Float r
      | JsonIntermAst.String s => String s
      | JsonIntermAst.Bool b => Bool b
      | JsonIntermAst.Int i => Int i
      | JsonIntermAst.Pair (k,v) =>
        raise Fail ("JSON Parsing error. Pair of JSON found where it shouldn't. Key = " ^ k)
      | JsonIntermAst.Obj l =>
        Obj
          (List.foldl
            (fn (a, acc) =>
                case a of
                    JsonIntermAst.Pair (k, v) => (k, fromInterm v) :: acc
                  | JsonIntermAst.Array _ => raise Fail ("JSON Parsing error. Found Array in object instead of key-value pair")
                  | JsonIntermAst.Null =>  raise Fail ("JSON Parsing error. Found Null in object instead of key-value pair")
                  | JsonIntermAst.Float _ =>  raise Fail ("JSON Parsing error. Found Float in object instead of key-value pair")
                  | JsonIntermAst.String _ =>  raise Fail ("JSON Parsing error. Found String in object instead of key-value pair")
                  | JsonIntermAst.Bool _ =>  raise Fail ("JSON Parsing error. Found Bool in object instead of key-value pair")
                  | JsonIntermAst.Int _ => raise Fail ("JSON Parsing error. Found Int in object instead of key-value pair")
                  | JsonIntermAst.Obj _ => raise Fail ("JSON Parsing error. Found Obj in object instead of key-value pair")
            ) [] l)

structure StandardJsonParserCallbacks =
struct 
    type json_data = JsonIntermAst.ast
    fun json_object l = JsonIntermAst.Obj l
    fun json_pair (k,v) = JsonIntermAst.Pair (k,v)
    fun json_array l = JsonIntermAst.Array l
    fun json_value x = x
    fun json_string s = JsonIntermAst.String s
    fun json_int i = JsonIntermAst.Int i
    fun json_real r = JsonIntermAst.Float r
    fun json_bool b = JsonIntermAst.Bool b
    fun json_null () = JsonIntermAst.Null
    fun error_handle (msg,pos,data) =
        raise Fail ("Error: " ^ msg ^ " near " ^ Int.toString pos ^ " data: " ^
                    data)
end

structure MyJsonParser = JSONParser (StandardJsonParserCallbacks)

fun parse (str: string): json =
    fromInterm (MyJsonParser.parse str)
fun print (ast: json): string = 
    case ast of
        Array l => "["
                   ^ List.foldl (fn (a, acc) => acc ^ (if acc = "" then "" else ", ") ^ print a) "" l
                   ^ "]"
      | Null => "null"
      | Float r => Real.toString r
      | String s =>
        "\"" ^
        String.translate
        (fn c => if c = #"\"" then "\\\"" else Char.toString c)
        s ^
        "\""
      | Bool b => if b then "true" else "false"
      | Int i => if i >= 0
                 then (Int.toString i)
                 else "-" ^ (Int.toString (Int.abs i)) (* default printing uses ~ instead of - *)
      | Obj l => "{"
                 ^ List.foldl (fn ((k, v), acc) => acc ^ (if acc = "" then "" else ", ") ^ "\"" ^ k ^ "\": " ^ print v ) "" l
                ^ "}"
end