package ocaml-protoc-plugin

  1. Overview
  2. Docs
Legend:
Page
Library
Module
Module type
Parameter
Class
Class type
Source

Source file serialize_json.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
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
open! StdLabels
open Spec

(** Serialize to json as per https://protobuf.dev/programming-guides/proto3/#json-options *)
let value_error type_name json =
  Result.raise (`Wrong_field_type (type_name, Json.to_string json))

type field = string * Json.t

let int32_value v = `Int (Int32.to_int v)
let int32_int_value v = `Int v
let int64_value v = `String (Int64.to_string v)
let int64_int_value v = `String (string_of_int v)
let bool_value v = `Bool v
let enum_value ~f v = `Int (f v)
let enum_name ~f v = `String (f v)
let string_value v = `String v
let bytes_value v = `String (Base64.encode_string ~pad:true (Bytes.unsafe_to_string v))
let list_value v = `List v
let float_value v =
  match Float.is_integer v with
  | true -> `Int (Float.to_int v)
  | false -> `Float v

let key_to_string = function
  | `String s -> s
  | `Bool b -> string_of_bool b
  | `Int v -> string_of_int v
  | json -> Result.raise (`Wrong_field_type ("map key", (Json.to_string json)))

let key ~json_names (_, name, json_name) =
  match json_names with
  | true -> json_name
  | false -> name

let get_key ~f ~default key = function
  | `Assoc l ->
    List.assoc_opt key l
    |> Option.map f
    |> Option.value ~default
  | json -> value_error "Expected Assoc" json

let to_camel_case s =
  let open StdLabels in
  String.split_on_char ~sep:'_' s
  |> List.map ~f:String.lowercase_ascii
  |> List.map ~f:String.capitalize_ascii
  |> String.concat ~sep:""
  |> String.uncapitalize_ascii

let%expect_test "json name to proto name" =
  let test s = Printf.printf "%10s -> %10s\n" s (to_camel_case s) in
  test "camel_case";
  test "Camel_case";
  test "Camel_Case";
  test "Camel_Case";
  test "camel_cASe";
  test "CAMel_case";
  ();
  [%expect {|
    camel_case ->  camelCase
    Camel_case ->  camelCase
    Camel_Case ->  camelCase
    Camel_Case ->  camelCase
    camel_cASe ->  camelCase
    CAMel_case ->  camelCase |}]

let duration_to_json json =
  let seconds = get_key "seconds" ~f:Deserialize_json.to_int64 ~default:0L json in
  let nanos = get_key "nanos" ~f:Deserialize_json.to_int32 ~default:0l json in
  let seconds = match seconds < 0L || nanos < 0l with
    | true -> Int64.mul (-1L) (Int64.abs seconds)
    | false -> (Int64.abs seconds)
  in
  let duration =
    match nanos with
    | 0l -> Printf.sprintf "%Lds" seconds
    | _ -> Printf.sprintf "%Ld.%09lds" seconds (Int32.abs nanos)
  in
  `String duration

let%expect_test "duration_to_json" =
  let test seconds nanos =
    let json = `Assoc ["seconds", `Int seconds; "nanos", `Int nanos] in
    Printf.printf "%d.%d -> %s\n" seconds nanos (Json.to_string (duration_to_json json))
  in
  test 100 0;
  test (1000) (123456);
  test (-1000) (-123456);
  ();
  [%expect {|
    100.0 -> "100s"
    1000.123456 -> "1000.000123456s"
    -1000.-123456 -> "-1000.000123456s" |}]

let timestamp_to_json json =
  let open Stdlib in
  let open StdLabels in
  let seconds = get_key "seconds" ~f:Deserialize_json.to_int64 ~default:0L json in
  let nanos = get_key "nanos" ~f:Deserialize_json.to_int64 ~default:0L json in
  let s1 = Ptime.Span.of_float_s (Int64.to_float seconds) |> Option.get in
  let s2 = Ptime.Span.of_float_s (Int64.to_float nanos /. 1_000_000_000.0) |> Option.get in
  let t =
    Ptime.Span.add s1 s2
    |> Ptime.of_span
    |> Option.get
   in
  t
  |> Ptime.to_rfc3339 ~frac_s:9
  |> String.split_on_char ~sep:'-'
  |> List.rev
  |> List.tl
  |> List.rev
  |> String.concat ~sep:"-"
  |> fun s -> `String (s^"Z")

let%expect_test "timestamp_to_json" =
  let test seconds nanos =
    let json = `Assoc ["seconds", `String seconds; "nanos", `Int nanos] in
    Printf.printf "%s.%d -> %s\n" seconds nanos (Json.to_string (timestamp_to_json json))
  in
  test "1709931283" 0;
  test "1709931283" (1_000_000_002/2);
  test "1709931283" 1_000_000_000;
  test "0" 1;
  ();
  [%expect {|
    1709931283.0 -> "2024-03-08T20:54:43.000000000Z"
    1709931283.500000001 -> "2024-03-08T20:54:43.500000001Z"
    1709931283.1000000000 -> "2024-03-08T20:54:44.000000000Z"
    0.1 -> "1970-01-01T00:00:00.000000001Z" |}]

let wrapper_to_json json = get_key ~f:(fun id -> id) ~default:`Null "value" json

let map_enum_json: (module Enum) -> Json.t -> Json.t = fun (module Enum) ->
  let name =
    Enum.name ()
    |> String.split_on_char ~sep:'.'
    |> List.tl
    |> String.concat ~sep:"."
  in
  match name with
  | "google.protobuf.NullValue" -> begin
      function
      | `Int 0 -> `Null
      | `String s when s = Enum.to_string (Enum.from_int_exn 0) -> `Null
      | json -> value_error name json
    end
  | _ -> fun json -> json


(* Convert already emitted json based on json mappings *)
let map_message_json: name:string -> (Json.t -> Json.t) option = fun ~name ->
  match name with
  | ".google.protobuf.Empty"  ->
    Some (fun json -> json)
  (* Duration - google/protobuf/timestamp.proto *)
  | ".google.protobuf.Duration" ->
    Some (duration_to_json)
  (* Timestamp - google/protobuf/timestamp.proto *)
  | ".google.protobuf.Timestamp" ->
    Some (timestamp_to_json)
  (* Wrapper types - google/protobuf/wrappers.proto *)
  | ".google.protobuf.DoubleValue"
  | ".google.protobuf.FloatValue"
  | ".google.protobuf.Int64Value"
  | ".google.protobuf.UInt64Value"
  | ".google.protobuf.Int32Value"
  | ".google.protobuf.UInt32Value"
  | ".google.protobuf.BoolValue"
  | ".google.protobuf.StringValue"
  | ".google.protobuf.BytesValue" ->
    Some (wrapper_to_json)
  | ".google.protobuf.Value" ->
    let map = function
      | `Assoc [_, json] -> json
      | json -> value_error name json
    in
    Some map
  | ".google.protobuf.Struct" ->
    let map = function
      | `Assoc ["fields", json ] -> json
      | json -> value_error name json
    in
    Some map
  | ".google.protobuf.ListValue" ->
    let map = function
      | `Assoc ["values", json ] -> json
      | json -> value_error name json
    in
    Some map
  | ".google.protobuf.FieldMask" ->
    let open StdLabels in
    let map = function
      | `Assoc ["paths", `List masks] ->
        List.map ~f:(function
          | `String mask -> (to_camel_case mask)
          | json -> value_error name json
        ) masks
        |> String.concat ~sep:","
        |> fun mask -> `String mask
      | json -> value_error name json
    in
    Some map
  | _ -> None

let rec json_of_spec: type a b. Json_options.t -> (a, b) spec -> a -> Json.t =
  fun options -> function
  | Double -> float_value
  | Float -> float_value
  | Bool -> bool_value
  | String -> string_value
  | Bytes -> bytes_value

  | Int32 -> int32_value
  | UInt32 -> int32_value
  | SInt32 -> int32_value
  | Fixed32 -> int32_value
  | SFixed32 -> int32_value

  | Int32_int -> int32_int_value
  | UInt32_int -> int32_int_value
  | SInt32_int -> int32_int_value
  | Fixed32_int -> int32_int_value
  | SFixed32_int -> int32_int_value

  | Int64 -> int64_value
  | UInt64 -> int64_value
  | SInt64 -> int64_value
  | Fixed64 -> int64_value
  | SFixed64 -> int64_value

  | Int64_int -> int64_int_value
  | UInt64_int -> int64_int_value
  | SInt64_int -> int64_int_value
  | Fixed64_int -> int64_int_value
  | SFixed64_int -> int64_int_value

  | Enum (module Enum) -> begin
      let map_enum_json = map_enum_json (module Enum) in
      let f = match options.enum_names with
        | true -> enum_name ~f:Enum.to_string
        | false -> enum_value ~f:Enum.to_int
      in
      fun v ->
        f v |> map_enum_json
  end
  | Message (module Message) ->
    fun v -> Message.to_json options v

and write: type a b. Json_options.t -> (a, b) compound -> a -> field list = fun options ->
  function
  | Basic (index, spec, default) -> begin
      let key = key ~json_names:options.json_names index in
      let to_json = json_of_spec options spec in
      function
      | v when options.omit_default_values && v = default -> []
      | v -> [key, to_json v]
    end
  | Basic_opt (index, spec) -> begin
      let key = key ~json_names:options.json_names index in
      let to_json = json_of_spec options spec in
      function
      | Some v ->
        [key, to_json v]
      | None -> []
    end
  | Basic_req (index, spec) ->
    let key = key ~json_names:options.json_names index in
    let to_json = json_of_spec options spec in
    fun v -> [key, to_json v]
  | Repeated (index, spec, _packed) ->
    let key = key ~json_names:options.json_names index in
    let to_json = json_of_spec options spec in
    fun v ->
      let value = List.map ~f:to_json v |> list_value in
      [key, value]
  | Map (index, (key_spec, value_compound)) ->
    let key = key ~json_names:options.json_names index in
    let json_of_key = json_of_spec { options with omit_default_values=false } key_spec in
    let json_of_value = match value_compound with
      | Basic (_, spec, _) -> json_of_spec options spec
      | Basic_opt (_, spec) ->
        let json_of_value = json_of_spec options spec in (* 3 *) (* This is a message. It will be evaluated recursively,  Indefinatly. *)
        let json_of_value = function
          | None -> `Null
          | Some v -> json_of_value v
        in
        json_of_value
    in
    let json_of_entry (key, value) =
      let key = json_of_key key |> key_to_string in
      let value = json_of_value value in
      (key, value)
    in
    begin
      function
      | [] when not options.omit_default_values -> []
      | vs -> [key, `Assoc (List.map ~f:json_of_entry vs )]
    end
  (* Something is recursive. a -> b -> a -> b -> a -> b. How do we break the chain? *)
  | Oneof (oneofs, index_f) -> begin
      let array =
        List.map ~f:(fun (Oneof_elem (index, spec, (_, destr))) -> (* 1 *)
          let to_json = json_of_spec options spec in (* 5 *)
          let key = key ~json_names:options.json_names index in
          fun v -> [key, to_json (destr v)]
        ) oneofs
        |> Array.of_list
      in
      function
      | `not_set -> []
      | v ->
        let f = array.(index_f v) in
        f v
    end

let serialize: type a. message_name:string -> Json_options.t -> (a, Json.t) compound_list -> field list -> a = fun ~message_name options ->
  let omit_default_values, map_result = match map_message_json ~name:message_name with
    | Some mapping -> false, fun json -> `Assoc (List.rev json) |> mapping
    | None -> options.omit_default_values, fun json -> `Assoc (List.rev json)
  in
  let options = { options with omit_default_values } in
  let rec inner: type a. (a, Json.t) compound_list -> field list -> a =
    function
    | Nil -> map_result
    | Nil_ext _extension_ranges -> fun json _extensions -> map_result json
    | Cons (compound, rest) ->
      let cont = inner rest in
      let write = write options compound in (* 2 *) (* 4 *)
      fun acc v ->
        let v = write v in
        cont (List.rev_append v acc)
  in
  inner

let serialize: type a. message_name:string -> (a, Json.t) compound_list -> Json_options.t -> a =
  fun ~message_name spec ->
  let arr = Array.init (Json_options.max_int + 1) ~f:(fun i -> Lazy.from_fun (fun () -> serialize ~message_name (Json_options.of_int i) spec [])) in
  fun options ->
    Lazy.force arr.(Json_options.to_int options)