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
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
val iter : 'a t -> f:('a -> unit) -> unit
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 ");
Format.fprintf fmt "in json:@;@;<0 4>@[<v 0>";
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)
;;
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 in
let fmt = Format.formatter_of_buffer buffer in
to_string_hum_fmt fmt t;
Format.pp_print_flush fmt ();
Buffer.contents buffer
;;
let { 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 }
;;
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 =
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)
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