Source file jsonoo.ml

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
open Js_of_ocaml

type t = < > Js.t

exception Decode_error of string

let decode_error message = raise (Decode_error message)

let _JSON = Js.Unsafe.global##._JSON

let try_parse_opt s =
  try Some (_JSON##parse (Js.string s)) with
  | _ -> None

let try_parse_exn s =
  try _JSON##parse (Js.string s) with
  | _ -> decode_error ("Failed to parse JSON string \"" ^ s ^ "\"")

let stringify ?spaces (json : t) =
  let js_string =
    match spaces with
    | None   -> _JSON##stringify json
    | Some n ->
      let spaces = Js.number_of_float (float_of_int n) in
      _JSON##stringify json Js.undefined spaces
  in
  Js.to_string js_string

module Decode = struct
  type 'a decoder = t -> 'a

  let expected typ (json : t) =
    decode_error ("Expected " ^ typ ^ ", got " ^ stringify json)

  let expected_length length array =
    decode_error
      ( "Expected array of length "
      ^ string_of_int length
      ^ ", got array of length "
      ^ string_of_int (Array.length array) )

  let typeof value = Js.to_string (Js.typeof value)

  let is_array value =
    let array_constr = Js.Unsafe.global##._Array in
    Js.instanceof value array_constr

  let id (json : t) = json

  let null (json : t) =
    if Js.Opt.test (Js.Opt.return json) then
      expected "null" json
    else
      Js.null

  let bool (json : t) =
    if typeof json = "boolean" then
      Js.to_bool (Js.Unsafe.coerce json)
    else
      expected "boolean" json

  let float (json : t) =
    if typeof json = "number" then
      Js.float_of_number (Js.Unsafe.coerce json)
    else
      expected "number" json

  let int (json : t) =
    let f = float json in
    if Float.is_finite f && Float.floor f = f then
      int_of_float f
    else
      expected "integer" json

  let string (json : t) =
    if typeof json = "string" then
      Js.to_string (Js.Unsafe.coerce json)
    else
      expected "string" json

  let char (json : t) =
    let s = string json in
    if String.length s = 1 then
      s.[0]
    else
      expected "single-character string" json

  let nullable decode (json : t) =
    if Js.Opt.test (Js.Opt.return json) then
      Some (decode json)
    else
      None

  let array decode (json : t) =
    if is_array json then
      let array = Js.to_array (Js.Unsafe.coerce json) in
      let convert i (json : t) =
        try decode json with
        | Decode_error message ->
          decode_error (message ^ "\n\tin array at index " ^ string_of_int i)
      in
      Array.mapi convert array
    else
      expected "array" json

  let list decode (json : t) = Array.to_list (array decode json)

  let tuple_element decode array i =
    try decode array.(i) with
    | Decode_error message ->
      decode_error (message ^ "\n\tin array at index " ^ string_of_int i)

  let pair decode_a decode_b (json : t) =
    let array = array id json in
    if Array.length array = 2 then
      let a = tuple_element decode_a array 0 in
      let b = tuple_element decode_b array 1 in
      (a, b)
    else
      expected_length 2 array

  let tuple2 = pair

  let tuple3 decode_a decode_b decode_c (json : t) =
    let array = array id json in
    if Array.length array = 3 then
      let a = tuple_element decode_a array 0 in
      let b = tuple_element decode_b array 1 in
      let c = tuple_element decode_c array 2 in
      (a, b, c)
    else
      expected_length 3 array

  let tuple4 decode_a decode_b decode_c decode_d (json : t) =
    let array = array id json in
    if Array.length array = 4 then
      let a = tuple_element decode_a array 0 in
      let b = tuple_element decode_b array 1 in
      let c = tuple_element decode_c array 2 in
      let d = tuple_element decode_d array 3 in
      (a, b, c, d)
    else
      expected_length 4 array

  let object_field decode js_object key =
    try decode (Js.Unsafe.get js_object key) with
    | Decode_error message ->
      decode_error
        (message ^ "\n\tin object at field '" ^ Js.to_string key ^ "'")

  let dict decode (json : t) =
    if
      typeof json = "object"
      && (not (is_array json))
      && Js.Opt.(test (return json))
    then (
      let keys = Js.to_array (Js.object_keys json) in
      let table = Hashtbl.create (Array.length keys) in
      let set key =
        let value = object_field decode json key in
        Hashtbl.add table (Js.to_string key) value
      in
      Array.iter set keys;
      table
    ) else
      expected "object" json

  let field key decode (json : t) =
    if
      typeof json = "object"
      && (not (is_array json))
      && Js.Opt.(test (return json))
    then
      let js_key = Js.string key in
      if Js.Optdef.(test @@ return (Js.Unsafe.get json js_key)) then
        object_field decode json js_key
      else
        decode_error ("Expected field '" ^ key ^ "'")
    else
      expected "object" json

  let rec at key_path decoder =
    match key_path with
    | [ key ]       -> field key decoder
    | first :: rest -> field first (at rest decoder)
    | []            ->
      invalid_arg "Expected key_path to contain at least one element"

  let try_optional decode (json : t) =
    try Some (decode json) with
    | Decode_error _ -> None

  let try_default value decode (json : t) =
    try decode json with
    | Decode_error _ -> value

  let any decoders (json : t) =
    let rec inner errors = function
      | []             ->
        let rev_errors = List.rev errors in
        decode_error
          ( "Value was not able to be decoded with the given decoders. Errors: "
          ^ String.concat "\n" rev_errors )
      | decode :: rest -> (
        try decode json with
        | Decode_error e -> inner (e :: errors) rest )
    in
    inner [] decoders

  let either a b = any [ a; b ]

  let map f decode (json : t) = f (decode json)

  let bind b a (json : t) = b (a json) json
end

module Encode = struct
  type 'a encoder = 'a -> t

  let id (json : t) = json

  let null : t = Obj.magic Js.null

  let bool b : t = Js.Unsafe.coerce (Js.bool b)

  let float f : t = Js.Unsafe.coerce (Js.number_of_float f)

  let int i : t = Js.Unsafe.coerce (Js.number_of_float (float_of_int i))

  let string s : t = Js.Unsafe.coerce (Js.string s)

  let char c : t = string (String.make 1 c)

  let nullable encode = function
    | None   -> null
    | Some v -> encode v

  let array encode a : t =
    let encoded : t array = Array.map encode a in
    Js.Unsafe.coerce (Js.array encoded)

  let list encode l : t = array encode (Array.of_list l)

  let pair encode_a encode_b (a, b) : t =
    let encoded : t array = [| encode_a a; encode_b b |] in
    Js.Unsafe.coerce (Js.array encoded)

  let tuple2 = pair

  let tuple3 encode_a encode_b encode_c (a, b, c) : t =
    let encoded : t array = [| encode_a a; encode_b b; encode_c c |] in
    Js.Unsafe.coerce (Js.array encoded)

  let tuple4 encode_a encode_b encode_c encode_d (a, b, c, d) : t =
    let encoded : t array =
      [| encode_a a; encode_b b; encode_c c; encode_d d |]
    in
    Js.Unsafe.coerce (Js.array encoded)

  let dict encode table : t =
    let encode_pair (k, v) = (k, Js.Unsafe.coerce (encode v)) in
    table
    |> Hashtbl.to_seq
    |> Array.of_seq
    |> Array.map encode_pair
    |> Js.Unsafe.obj

  let object_ (props : (string * t) list) : t =
    let coerce (k, v) = (k, Js.Unsafe.coerce v) in
    Js.Unsafe.obj (Array.map coerce @@ Array.of_list props)
end

let t_of_js : Ojs.t -> t = Obj.magic

let t_to_js : t -> Ojs.t = Obj.magic