Source file helpers.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
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
open Core
open Type

module Context_stack : sig
  (* A [Context_stack] is a nonempty accumulator that supports traversal in insertion
     order. Its semantics fit the way raising through a series of exception handlers
     constructs a stack by slipping things under the bottom. *)

  type 'a t

  val singleton : 'a -> 'a t
  val add_caller : 'a t -> 'a -> 'a t
  val pop_caller : 'a t -> 'a * 'a t option

  (* [iter] and [to_list] traverse in callee-before-caller order. *)

  val iter : 'a t -> f:('a -> unit) -> unit

  (* The farthest-up caller comes out last and has [depth = 0]. *)
  val to_list : 'a t -> f:(depth:int -> 'a -> 'b) -> 'b list
end = struct
  type 'a t = 'a Fdeque.t

  let singleton = Fdeque.singleton
  let add_caller = Fdeque.enqueue_back

  let pop_caller t =
    Fdeque.dequeue_back_exn t
    |> Tuple2.map_snd ~f:(fun rest -> Option.some_if (not (Fdeque.is_empty rest)) rest)
  ;;

  let iter = Fdeque.iter

  let to_list t ~f =
    let length = Fdeque.length t - 1 in
    Fdeque.to_list t |> List.mapi ~f:(fun i a -> f ~depth:(length - i) a)
  ;;
end

module Conv_failure = struct
  module Context = struct
    type t =
      { json : Json.t
      ; location : string option
      }

    let to_exn_sexp { json; location } ~depth =
      let tag = [%string "json context [%{depth#Int}]"] in
      let tag =
        match location with
        | None -> tag
        | Some location -> [%string "%{tag}, at %{location}"]
      in
      [%sexp (tag : string), (json : Json.t)]
    ;;

    let to_string_hum_fmt fmt { json; location } =
      Option.iter location ~f:(Format.fprintf fmt "at %s ");
      (* <0 4> means we're indenting by 4 spaces, and <v 0> means we have a vertical box
         and each new line is not indented. This is what we want because the [Jsonaf]
         returns the correct spacing already. *)
      Format.fprintf fmt "in json:@;@;<0 4>@[<v 0>";
      (* Even though we have [Jsonaf.pp], it just writes out raw newlines, which is never
         what you want with [Format]. We break down the string here and use @; to delimit
         lines. *)
      List.iter
        (String.split_lines (Jsonaf.to_string_hum json))
        ~f:(Format.fprintf fmt "%s@;");
      Format.fprintf fmt "@]@;@;"
    ;;
  end

  type t =
    { exn : Exn.t
    ; context_stack : Context.t Context_stack.t
    }

  let context_sexp context_stack =
    Context_stack.to_list context_stack ~f:(fun ~depth context ->
      Context.to_exn_sexp context ~depth)
  ;;

  (* This [sexp_of_t] exists only to give a slightly nicer pretty-printing sexp for the
     exception [Of_json_conv_failed] below. See tests for formatting examples. *)
  let sexp_of_t { exn; context_stack } =
    [%sexp
      "Of_json failed to convert"
      :: (exn : Exn.t)
      :: (context_sexp context_stack : Sexp.t list)]
  ;;

  let to_string_hum_fmt fmt { exn; context_stack } =
    Format.fprintf fmt "@[<v>";
    Exn.pp fmt exn;
    Format.fprintf fmt "@;";
    Context_stack.iter context_stack ~f:(Context.to_string_hum_fmt fmt);
    Format.fprintf fmt "@]"
  ;;

  let to_string_hum t =
    let buffer = Buffer.create 1024 (* arbitrary *) in
    let fmt = Format.formatter_of_buffer buffer in
    to_string_hum_fmt fmt t;
    Format.pp_print_flush fmt ();
    Buffer.contents buffer
  ;;

  let extract_exn { exn; _ } = exn
end

exception Of_json_conv_failed of Conv_failure.t [@@deriving sexp]

let reraise exn ~context =
  match exn with
  | Of_json_conv_failed { exn = inner_exn; context_stack } ->
    let context_stack = Context_stack.add_caller context_stack context in
    raise (Of_json_conv_failed { exn = inner_exn; context_stack })
  | _ ->
    raise (Of_json_conv_failed { exn; context_stack = Context_stack.singleton context })
;;

let annotate ?location t json =
  try run t json with
  | exn -> reraise exn ~context:{ json; location }
;;

(* Equivalent to [map], but [reraise]s exceptions only from the application of [f] with
   context. *)
let annotated_map ?location t ~f json =
  let result = run t json in
  try f result with
  | exn -> reraise exn ~context:{ json; location }
;;

let lookup key = Json.member key

let lookup_exn key json =
  match lookup key json with
  | Some value -> value
  | None -> raise_s [%message "Key not in object" (key : string)]
;;

let using key t =
  (* We annotate the key access and run separately so that we only provide a location
     indicator if it exists *)
  annotate (lookup_exn key) |> annotated_map ~f:(run t) ~location:[%string "key [%{key}]"]
;;

let using_opt key t =
  annotate
    (fun json -> Option.map (lookup key json) ~f:(run t))
    ~location:[%string {|key [%{key}]|}]
;;

let map_object ~f =
  annotate ?location:None
  @@ fun json ->
  List.map (Json.keys json) ~f:(fun key -> run (f key) (lookup_exn key json))
;;

let safe t json =
  try Some (run t json) with
  | _ex -> None
;;

module Alternative_error = struct
  module Context = Conv_failure.Context

  type branch =
    { exn : Exn.t
    ; context_stack : Context.t Context_stack.t option
    }

  type t =
    { branches : branch Appendable_list.t
    ; context : Context.t
    }

  let of_conv_failure (err : Conv_failure.t) =
    let local, rest = Context_stack.pop_caller err.context_stack in
    { branches = Appendable_list.singleton { exn = err.exn; context_stack = rest }
    ; context = local
    }
  ;;

  let combine t1 t2 =
    { t1 with branches = Appendable_list.append t1.branches t2.branches }
  ;;

  let sexp_of_t { branches; context } : Sexp.t =
    let branches =
      Sequence.mapi (Appendable_list.to_sequence branches) ~f:(fun i branch ->
        let tag = [%string "branch [%{i#Int}]"] in
        let context =
          match branch.context_stack with
          | None -> []
          | Some stack -> Conv_failure.context_sexp stack
        in
        [%sexp (tag : string) :: (branch.exn : Exn.t) :: (context : Sexp.t list)])
    in
    List
      (List.concat
         [ [ [%sexp "expected one non-failure"] ]
         ; Sequence.to_list branches
         ; [ [%sexp "branch context", (context.json : Json.t)] ]
         ])
  ;;
end

exception Alternative_error of Alternative_error.t [@@deriving sexp]

let combined_exns exn1 exn2 ~context =
  let conv_exn = function
    | Alternative_error e -> e
    | Of_json_conv_failed e -> Alternative_error.of_conv_failure e
    | exn ->
      let context_stack = Context_stack.singleton context in
      Alternative_error.of_conv_failure { exn; context_stack }
  in
  Alternative_error (Alternative_error.combine (conv_exn exn1) (conv_exn exn2))
;;

let ( <|> ) a b json =
  try run a json with
  | left_exn ->
    (try run b json with
     | right_exn ->
       raise (combined_exns left_exn right_exn ~context:{ json; location = None }))
;;

let choice ts =
  if List.is_empty ts
  then failwith "Expected at least one [of_json] to choose from"
  else List.reduce_exn ts ~f:( <|> )
;;

let ( @. ) = using
let ( @? ) = using_opt
let ( @> ) t f = annotated_map t ~f
let ( >>> ) a b = Fn.compose b a
let json = Fn.id
let int = annotate Json.as_int
let float = annotate Json.as_float
let number = annotate Json.as_number
let string = annotate Json.as_string
let bool = annotate Json.as_bool
let list t = annotate (Json.as_list @> List.map ~f:(run t))
let option t = annotate (Json.as_option @> Option.map ~f:(run t))
let ( @?? ) key t = using_opt key (option t) @> Option.join
let as_sexp of_sexp = string @> (Sexp.of_string >>> of_sexp)

(** These are for sloppy APIs which sometimes double quotes values. *)
let number_string = annotate (Json.as_number <|> Json.as_string)

let float_string = number_string @> Float.of_string
let int_string = number_string @> Int.of_string

module Array_as_tuple = struct
  module T = struct
    type 'a t = Json.t list -> 'a * Json.t list

    let run t jsons = t jsons

    let bind t ~f jsons =
      let x, jsons = run t jsons in
      run (f x) jsons
    ;;

    let return a jsons = a, jsons

    let map t ~f jsons =
      let x, jsons = run t jsons in
      f x, jsons
    ;;

    let apply af ax = bind af ~f:(fun f -> map ax ~f)
    let map = `Custom map
  end

  include T
  include Applicative.Make (T)

  (* This module is supposed to be used in an inline open, so we have to do some
     functor/signature dance to get the correct [Let_syntax] module out. *)
  module M = Monad.Make (T)
  include (M : Monad.S_without_syntax with type 'a t := 'a t)
  module Let_syntax = M.Let_syntax.Let_syntax

  let run_exhaustively (t : 'a t) jsons : 'a =
    let a, jsons = t jsons in
    match jsons with
    | [] -> a
    | elems ->
      raise_s [%message "array_as_tuple has unparsed elements" (elems : Json.t list)]
  ;;

  let shift of_json : _ t = function
    | [] -> raise_s [%message "ran out of elements while parsing tuple"]
    | hd :: tl -> Type.run of_json hd, tl
  ;;

  let drop_rest : unit t = fun _jsons -> (), []
end

let tuple m = list json @> Array_as_tuple.run_exhaustively m