···55 {|(library
66 (name %s)
77 (libraries hermes yojson lwt)
88- (preprocess (pps ppx_deriving_yojson)))|}
99-1010-let util_file =
1111- Printf.sprintf
1212- {|let query_string_list_of_yojson = function
1313- | `List l ->
1414- Ok (List.filter_map (function `String s -> Some s | _ -> None) l)
1515- | `String s ->
1616- Ok [s]
1717- | `Null ->
1818- Ok []
1919- | _ ->
2020- Error "expected string or string list"
2121-2222-let query_string_list_to_yojson l = `List (List.map (fun s -> `String s) l)
2323-2424-let query_int_list_of_yojson = function
2525- | `List l ->
2626- Ok (List.filter_map (function `Int i -> Some i | _ -> None) l)
2727- | `Int i ->
2828- Ok [i]
2929- | `Null ->
3030- Ok []
3131- | _ ->
3232- Error "expected int or int list"
3333-3434-let query_int_list_to_yojson l = `List (List.map (fun i -> `Int i) l)
3535-3636-let query_string_list_option_of_yojson = function
3737- | `List l ->
3838- Ok (Some (List.filter_map (function `String s -> Some s | _ -> None) l))
3939- | `String s ->
4040- Ok (Some [s])
4141- | `Null ->
4242- Ok None
4343- | _ ->
4444- Error "expected string or string list"
4545-4646-let query_string_list_option_to_yojson = function
4747- | Some l ->
4848- `List (List.map (fun s -> `String s) l)
4949- | None ->
5050- `Null
5151-5252-let query_int_list_option_of_yojson = function
5353- | `List l ->
5454- Ok (Some (List.filter_map (function `Int i -> Some i | _ -> None) l))
5555- | `Int i ->
5656- Ok (Some [i])
5757- | `Null ->
5858- Ok None
5959- | _ ->
6060- Error "expected int or int list"
6161-6262-let query_int_list_option_to_yojson = function
6363- | Some l ->
6464- `List (List.map (fun i -> `Int i) l)
6565- | None ->
6666- `Null|}
88+ (preprocess (pps hermes_ppx ppx_deriving_yojson)))|}
6796810(* recursively find all json files in a path (file or directory) *)
6911let find_json_files path =
···199141 Out_channel.output_string oc (dune_file (String.lowercase_ascii module_name)) ;
200142 close_out oc ;
201143 Printf.printf "Generated dune file\n" ;
202202- (* generate util file *)
203203- let util_path = Filename.concat output_dir "hermes_util.ml" in
204204- let oc = open_out util_path in
205205- Printf.fprintf oc "(* %s - generated from atproto lexicons *)\n\n" module_name ;
206206- (* export each lexicon as a module alias *)
207207- Out_channel.output_string oc util_file ;
208208- close_out oc ;
209209- Printf.printf "Generated util file: %s\n" util_path ;
210144 Printf.printf "Done! Generated %d modules\n" (List.length lexicons)
211145212146let inputs =
+46-72
hermes-cli/lib/codegen.ml
···7272 let def_name = String.sub ref_str 1 (String.length ref_str - 1) in
7373 Naming.type_name def_name
7474 end
7575- else begin
7575+ else
7676 (* external ref: com.example.defs#someDef *)
7777- match String.split_on_char '#' ref_str with
7777+ begin match String.split_on_char '#' ref_str with
7878 | [ext_nsid; def_name] ->
7979 if ext_nsid = nsid then
8080 (* ref to same nsid, treat as local *)
···9494 end
9595 | _ ->
9696 "invalid_ref"
9797- end
9797+ end
98989999and gen_union_type_name refs = Naming.union_type_name refs
100100···298298299299let is_bytes_encoding encoding =
300300 encoding <> "" && encoding <> "application/json"
301301-302302-(* generate custom of_yojson/to_yojson attrs for query param array types *)
303303-let gen_query_array_yojson_attrs ~is_required (type_def : type_def) =
304304- match type_def with
305305- | Array {items; _} -> (
306306- match items with
307307- | String _ ->
308308- if is_required then
309309- ( " [@of_yojson Hermes_util.query_string_list_of_yojson]"
310310- , " [@to_yojson Hermes_util.query_string_list_to_yojson]" )
311311- else
312312- ( " [@of_yojson Hermes_util.query_string_list_option_of_yojson]"
313313- , " [@to_yojson Hermes_util.query_string_list_option_to_yojson]" )
314314- | Integer _ ->
315315- if is_required then
316316- ( " [@of_yojson Hermes_util.query_int_list_of_yojson]"
317317- , " [@to_yojson Hermes_util.query_int_list_to_yojson]" )
318318- else
319319- ( " [@of_yojson Hermes_util.query_int_list_option_of_yojson]"
320320- , " [@to_yojson Hermes_util.query_int_list_option_to_yojson]" )
321321- | _ ->
322322- ("", "") )
323323- | _ ->
324324- ("", "")
325301326302(* generate params type for query/procedure *)
327303let gen_params_type nsid out (spec : params_spec) =
···336312 let type_str = if is_required then base_type else base_type ^ " option" in
337313 let key_attr = Naming.key_annotation prop_name ocaml_name in
338314 let default_attr = if is_required then "" else " [@default None]" in
339339- let of_yojson_attr, to_yojson_attr =
340340- gen_query_array_yojson_attrs ~is_required prop.type_def
341341- in
342315 emitln out
343343- (Printf.sprintf " %s: %s%s%s%s%s;" ocaml_name type_str key_attr
344344- default_attr of_yojson_attr to_yojson_attr ) )
316316+ (Printf.sprintf " %s: %s%s%s;" ocaml_name type_str key_attr
317317+ default_attr ) )
345318 spec.properties ;
346319 emitln out " }" ;
347347- emitln out "[@@deriving yojson {strict= false}]" ;
320320+ emitln out "[@@xrpc_query]" ;
348321 emit_newline out
349322350323(* generate output type for query/procedure *)
···633606 | _ ->
634607 emitln out " let params = () in" ) ;
635608 (* generate the call based on input/output types *)
636636- if input_is_bytes then begin
609609+ if input_is_bytes then
637610 (* bytes input - choose between procedure_blob and procedure_bytes *)
638638- if output_is_bytes then
611611+ begin if output_is_bytes then
639612 (* bytes-in, bytes-out: use procedure_bytes *)
640613 emitln out
641614 (Printf.sprintf
···659632 (Bytes.of_string (Option.value input ~default:\"\")) \
660633 ~content_type:\"%s\" output_of_yojson"
661634 input_content_type )
662662- end
635635+ end
663636 else begin
664637 (* json input - build input and use procedure *)
665638 ( match spec.input with
···11351108 let def_name = String.sub ref_str 1 (String.length ref_str - 1) in
11361109 get_unique_type_name current_nsid def_name
11371110 end
11381138- else begin
11391139- match String.split_on_char '#' ref_str with
11111111+ else
11121112+ begin match String.split_on_char '#' ref_str with
11401113 | [ext_nsid; def_name] ->
11411114 if List.mem ext_nsid merged_nsids then
11421115 (* ref to another nsid in the merged group - use unique name *)
···11561129 end
11571130 | _ ->
11581131 "invalid_ref"
11591159- end
11321132+ end
11601133 in
11611134 (* generate converter expression for reading a type from json *)
11621135 (* returns (converter_expr, needs_result_unwrap) - if needs_result_unwrap is true, caller should apply Result.get_ok *)
···13771350 (* local ref: #foo -> current_nsid#foo *)
13781351 let def_name = String.sub ref_ 1 (String.length ref_ - 1) in
13791352 (current_nsid ^ "#" ^ def_name) :: acc
13801380- else begin
13811381- match String.split_on_char '#' ref_ with
13531353+ else
13541354+ begin match String.split_on_char '#' ref_ with
13821355 | [ext_nsid; def_name] when List.mem ext_nsid merged_nsids ->
13831356 (* cross-nsid ref within merged group *)
13841357 (ext_nsid ^ "#" ^ def_name) :: acc
13851358 | _ ->
13861359 acc
13871387- end
13601360+ end
13881361 | Union {refs; _} ->
13891362 List.fold_left
13901363 (fun a r ->
···18231796 let is_required = List.mem prop_name required in
18241797 let is_nullable = List.mem prop_name nullable in
18251798 let is_optional = (not is_required) || is_nullable in
18261826- if is_optional then begin
18271827- if needs_unwrap then
17991799+ if is_optional then
18001800+ begin if needs_unwrap then
18281801 emitln out
18291802 (Printf.sprintf
18301803 " let %s = json |> member \"%s\" |> to_option (fun x \
···18361809 (Printf.sprintf
18371810 " let %s = json |> member \"%s\" |> to_option %s in"
18381811 ocaml_name prop_name conv_expr )
18391839- end
18401840- else begin
18411841- if needs_unwrap then
18121812+ end
18131813+ else
18141814+ begin if needs_unwrap then
18421815 emitln out
18431816 (Printf.sprintf
18441817 " let %s = json |> member \"%s\" |> %s |> \
···18481821 emitln out
18491822 (Printf.sprintf " let %s = json |> member \"%s\" |> %s in"
18501823 ocaml_name prop_name conv_expr )
18511851- end )
18241824+ end )
18521825 spec.properties ;
18531826 emit out " Ok { " ;
18541827 emit out
···19501923 let regular_defs_in_scc =
19511924 List.filter_map (function `RegularDef x -> Some x | _ -> None) scc
19521925 in
19531953- if inline_unions_in_scc = [] then begin
19261926+ if inline_unions_in_scc = [] then
19541927 (* no inline unions - use standard generation with [@@deriving yojson] *)
19551955- if regular_defs_in_scc <> [] then gen_merged_scc regular_defs_in_scc
19561956- end
19281928+ begin if regular_defs_in_scc <> [] then
19291929+ gen_merged_scc regular_defs_in_scc
19301930+ end
19571931 else begin
19581932 (* has inline unions - generate all types first, then all converters *)
19591933 (* register inline union names *)
···19691943 @ List.map (fun x -> `Regular x) regular_defs_in_scc
19701944 in
19711945 let n = List.length all_items in
19721972- if n = 1 then begin
19461946+ if n = 1 then
19731947 (* single item - generate normally *)
19741974- match List.hd all_items with
19481948+ begin match List.hd all_items with
19751949 | `Inline (nsid, name, refs, spec) ->
19761950 let unique_name = get_unique_inline_union_name nsid name in
19771951 gen_inline_union_type_only nsid unique_name refs spec ;
···19911965 gen_object_converters nsid def.name rspec.record
19921966 | _ ->
19931967 gen_merged_scc [(nsid, def)] )
19941994- end
19681968+ end
19951969 else begin
19961970 (* multiple items - generate as mutually recursive types *)
19971971 (* first pass: register inline unions from objects *)
···23122286 let def_name = String.sub ref_str 1 (String.length ref_str - 1) in
23132287 get_shared_type_name current_nsid def_name
23142288 end
23152315- else begin
23162316- match String.split_on_char '#' ref_str with
22892289+ else
22902290+ begin match String.split_on_char '#' ref_str with
23172291 | [ext_nsid; def_name] ->
23182292 if List.mem ext_nsid shared_nsids then
23192293 (* ref to another nsid in the shared group *)
···23332307 end
23342308 | _ ->
23352309 "invalid_ref"
23362336- end
23102310+ end
23372311 in
23382312 (* generate type uri for shared context *)
23392313 let gen_shared_type_uri current_nsid ref_str =
···25592533 (* local ref: #foo -> current_nsid#foo *)
25602534 let def_name = String.sub ref_ 1 (String.length ref_ - 1) in
25612535 (current_nsid ^ "#" ^ def_name) :: acc
25622562- else begin
25632563- match String.split_on_char '#' ref_ with
25362536+ else
25372537+ begin match String.split_on_char '#' ref_ with
25642538 | [ext_nsid; def_name] when List.mem ext_nsid shared_nsids ->
25652539 (* cross-nsid ref within shared group *)
25662540 (ext_nsid ^ "#" ^ def_name) :: acc
25672541 | _ ->
25682542 acc
25692569- end
25432543+ end
25702544 | Union {refs; _} ->
25712545 List.fold_left
25722546 (fun a r ->
···27482722 let is_required = List.mem prop_name required in
27492723 let is_nullable = List.mem prop_name nullable in
27502724 let is_optional = (not is_required) || is_nullable in
27512751- if is_optional then begin
27522752- if needs_unwrap then
27252725+ if is_optional then
27262726+ begin if needs_unwrap then
27532727 emitln out
27542728 (Printf.sprintf
27552729 " let %s = json |> member \"%s\" |> to_option (fun x \
···27612735 (Printf.sprintf
27622736 " let %s = json |> member \"%s\" |> to_option %s in"
27632737 ocaml_name prop_name conv_expr )
27642764- end
27652765- else begin
27662766- if needs_unwrap then
27382738+ end
27392739+ else
27402740+ begin if needs_unwrap then
27672741 emitln out
27682742 (Printf.sprintf
27692743 " let %s = json |> member \"%s\" |> %s |> \
···27732747 emitln out
27742748 (Printf.sprintf " let %s = json |> member \"%s\" |> %s in"
27752749 ocaml_name prop_name conv_expr )
27762776- end )
27502750+ end )
27772751 spec.properties ;
27782752 emit out " Ok { " ;
27792753 emit out
···30162990 let regular_defs_in_scc =
30172991 List.filter_map (function `RegularDef x -> Some x | _ -> None) scc
30182992 in
30193019- if inline_unions_in_scc = [] then begin
29932993+ if inline_unions_in_scc = [] then
30202994 (* no inline unions - check if we still need mutual recursion *)
30213021- match regular_defs_in_scc with
29952995+ begin match regular_defs_in_scc with
30222996 | [] ->
30232997 ()
30242998 | [(nsid, def)] ->
···31043078 () )
31053079 obj_defs
31063080 end
31073107- end
30813081+ end
31083082 else begin
31093083 (* has inline unions - generate all types first, then all converters *)
31103084 List.iter
···31173091 @ List.map (fun x -> `Regular x) regular_defs_in_scc
31183092 in
31193093 let n = List.length all_items in
31203120- if n = 1 then begin
31213121- match List.hd all_items with
30943094+ if n = 1 then
30953095+ begin match List.hd all_items with
31223096 | `Inline (nsid, name, refs, spec) ->
31233097 gen_shared_inline_union_type_only nsid name refs spec ;
31243098 emit_newline out ;
···31373111 gen_shared_object_converters nsid def.name rspec.record
31383112 | _ ->
31393113 gen_shared_single_def (nsid, def) )
31403140- end
31143114+ end
31413115 else begin
31423116 (* multiple items - generate as mutually recursive types *)
31433117 List.iter
+6-6
hermes-cli/lib/scc.ml
···8080 (* local ref: #foo *)
8181 let def_name = String.sub ref_ 1 (String.length ref_ - 1) in
8282 def_name :: acc
8383- else begin
8383+ else
8484 (* check if it's a self-reference: nsid#foo *)
8585- match String.split_on_char '#' ref_ with
8585+ begin match String.split_on_char '#' ref_ with
8686 | [ext_nsid; def_name] when ext_nsid = nsid ->
8787 def_name :: acc
8888 | _ ->
8989 acc
9090- end
9090+ end
9191 | Union {refs; _} ->
9292 List.fold_left
9393 (fun a r ->
···168168 | Array {items; _} ->
169169 collect_from_type items
170170 | Ref {ref_; _} ->
171171- if String.length ref_ > 0 && ref_.[0] <> '#' then begin
172172- match String.split_on_char '#' ref_ with
171171+ if String.length ref_ > 0 && ref_.[0] <> '#' then
172172+ begin match String.split_on_char '#' ref_ with
173173 | ext_nsid :: _ ->
174174 add_nsid ext_nsid
175175 | [] ->
176176 ()
177177- end
177177+ end
178178 | Union {refs; _} ->
179179 List.iter
180180 (fun r ->
+36-7
hermes/README.md
···2525 - [Union Types](#union-types)
2626- [hermes_ppx](#hermes-ppx)
2727 - [Setup](#setup)
2828- - [Usage](#ppx-usage)
2828+ - [`[%xrpc ...]`](#ppx-xrpc)
2929+ - [`[@@xrpc_query]`](#ppx-xrpc-query)
29303031## quick start
3132···242243module Main = struct
243244 type params = {
244245 actor: string;
245245- } [@@deriving yojson]
246246+ limit: int option [@default None];
247247+ } [@@xrpc_query]
246248247249 type output = {
248250 did: string;
···253255254256 let nsid = "app.bsky.actor.getProfile"
255257256256- let call ~actor (client : Hermes.client) : output Lwt.t =
257257- let params = { actor } in
258258+ let call ~actor ?limit (client : Hermes.client) : output Lwt.t =
259259+ let params = { actor; limit } in
258260 Hermes.query client nsid (params_to_yojson params) output_of_yojson
259261end
262262+```
263263+264264+The generated `dune` file pulls in the required preprocessors:
265265+266266+```lisp
267267+(library
268268+ (name lexicons)
269269+ (libraries hermes yojson lwt)
270270+ (preprocess (pps hermes_ppx ppx_deriving_yojson)))
260271```
261272262273### type mappings
···294305295306<h2 id="hermes-ppx">hermes_ppx (PPX extension)</h2>
296307297297-transforms `[%xrpc ...]` into generated module calls.
308308+provides two rewrites: `[%xrpc ...]` for ergonomic API calls, and `[@@xrpc_query]` for XRPC server implementations.
298309299310### setup
300311···302313(library
303314 (name my_app)
304315 (libraries hermes hermes_ppx lwt)
305305- (preprocess (pps hermes_ppx)))
316316+ (preprocess (pps hermes_ppx ppx_deriving_yojson)))
306317```
307318308308-<h3 id="ppx-usage">usage</h3>
319319+If you use `[@@xrpc_query]`, you must include the `ppx_deriving_yojson` preprocessor after the `hermes_ppx` preprocessor.
320320+321321+<h3 id="ppx-xrpc"><code>[%xrpc ...]</code></h3>
322322+323323+transforms `[%xrpc ...]` into generated module calls.
309324310325```ocaml
311326let get_followers ~actor ~limit client =
···326341 ])
327342 client
328343```
344344+345345+<h3 id="ppx-xrpc-query"><code>[@@xrpc_query]</code></h3>
346346+347347+allows parsing query strings to yojson.
348348+349349+```ocaml
350350+type params = {
351351+ actor: string;
352352+ limit: int option [@default None];
353353+ collections: string list option [@default None];
354354+} [@@xrpc_query]
355355+```
356356+357357+Under the hood, the PPX assigns custom to_yojson/of_yojson functions to the record's fields, allowing for correctly parsing from an `Assoc` containing string or string list entries to the types specified in the record.
···181181 module Make (_ : Http_backend.S) : S
182182end
183183184184+module Query : sig
185185+ val query_int_of_yojson : Yojson.Safe.t -> (int, string) result
186186+187187+ val query_int_option_of_yojson : Yojson.Safe.t -> (int option, string) result
188188+189189+ val query_bool_of_yojson : Yojson.Safe.t -> (bool, string) result
190190+191191+ val query_bool_option_of_yojson :
192192+ Yojson.Safe.t -> (bool option, string) result
193193+194194+ val query_string_list_of_yojson :
195195+ Yojson.Safe.t -> (string list, string) result
196196+197197+ val query_string_list_to_yojson : string list -> Yojson.Safe.t
198198+199199+ val query_int_list_of_yojson : Yojson.Safe.t -> (int list, string) result
200200+201201+ val query_int_list_to_yojson : int list -> Yojson.Safe.t
202202+203203+ val query_string_list_option_of_yojson :
204204+ Yojson.Safe.t -> (string list option, string) result
205205+206206+ val query_string_list_option_to_yojson : string list option -> Yojson.Safe.t
207207+208208+ val query_int_list_option_of_yojson :
209209+ Yojson.Safe.t -> (int list option, string) result
210210+211211+ val query_int_list_option_to_yojson : int list option -> Yojson.Safe.t
212212+end
213213+184214module Credential_manager : sig
185215 type t = credential_manager
186216
+83
hermes/lib/query.ml
···11+let query_int_of_yojson = function
22+ | `Int n ->
33+ Ok n
44+ | `Intlit s | `String s -> (
55+ match int_of_string_opt s with
66+ | Some n ->
77+ Ok n
88+ | None ->
99+ Error "expected integer" )
1010+ | _ ->
1111+ Error "expected integer"
1212+1313+let query_int_option_of_yojson = function
1414+ | `Null ->
1515+ Ok None
1616+ | j ->
1717+ Result.map Option.some (query_int_of_yojson j)
1818+1919+let query_bool_of_yojson = function
2020+ | `Bool b ->
2121+ Ok b
2222+ | `String "true" ->
2323+ Ok true
2424+ | `String "false" ->
2525+ Ok false
2626+ | _ ->
2727+ Error "expected boolean"
2828+2929+let query_bool_option_of_yojson = function
3030+ | `Null ->
3131+ Ok None
3232+ | j ->
3333+ Result.map Option.some (query_bool_of_yojson j)
3434+3535+let query_string_list_of_yojson = function
3636+ | `List l ->
3737+ Ok (List.filter_map (function `String s -> Some s | _ -> None) l)
3838+ | `String s ->
3939+ Ok [s]
4040+ | `Null ->
4141+ Ok []
4242+ | _ ->
4343+ Error "expected string or string list"
4444+4545+let query_string_list_to_yojson l = `List (List.map (fun s -> `String s) l)
4646+4747+let query_int_list_of_yojson = function
4848+ | `List l ->
4949+ Ok (List.filter_map (fun j -> Result.to_option (query_int_of_yojson j)) l)
5050+ | `Null ->
5151+ Ok []
5252+ | j ->
5353+ Result.map (fun i -> [i]) (query_int_of_yojson j)
5454+5555+let query_int_list_to_yojson l = `List (List.map (fun i -> `Int i) l)
5656+5757+let query_string_list_option_of_yojson = function
5858+ | `List l ->
5959+ Ok (Some (List.filter_map (function `String s -> Some s | _ -> None) l))
6060+ | `String s ->
6161+ Ok (Some [s])
6262+ | `Null ->
6363+ Ok None
6464+ | _ ->
6565+ Error "expected string or string list"
6666+6767+let query_string_list_option_to_yojson = function
6868+ | Some l ->
6969+ `List (List.map (fun s -> `String s) l)
7070+ | None ->
7171+ `Null
7272+7373+let query_int_list_option_of_yojson = function
7474+ | `Null ->
7575+ Ok None
7676+ | j ->
7777+ Result.map Option.some (query_int_list_of_yojson j)
7878+7979+let query_int_list_option_to_yojson = function
8080+ | Some l ->
8181+ `List (List.map (fun i -> `Int i) l)
8282+ | None ->
8383+ `Null
+136-1
hermes_ppx/lib/hermes_ppx.ml
···57575858let rule = Context_free.Rule.extension xrpc_extension
59596060-let () = Driver.register_transformation "hermes_ppx" ~rules:[rule]
6060+(* rewrite record types annotated with [@@deriving xrpc_query] by injecting
6161+ Hermes_util [@of_yojson]/[@to_yojson] attrs on fields that need query string
6262+ coercion, then swaps the deriving to [@@deriving yojson {strict = false}]. *)
6363+6464+let hermes_query name ~loc =
6565+ Ast_builder.Default.pexp_ident ~loc
6666+ (Loc.make ~loc (Longident.Ldot (Ldot (Lident "Hermes", "Query"), name)))
6767+6868+let make_attr ~loc name expr =
6969+ { attr_name= Loc.make ~loc name
7070+ ; attr_payload= PStr [Ast_builder.Default.pstr_eval ~loc expr []]
7171+ ; attr_loc= loc }
7272+7373+(* classify a core_type and return attrs to inject *)
7474+let query_attrs_for_type (ct : core_type) =
7575+ let loc = ct.ptyp_loc in
7676+ let of_ n = make_attr ~loc "of_yojson" (hermes_query n ~loc) in
7777+ let to_ n = make_attr ~loc "to_yojson" (hermes_query n ~loc) in
7878+ match ct.ptyp_desc with
7979+ (* int *)
8080+ | Ptyp_constr ({txt= Lident "int"; _}, []) ->
8181+ [of_ "query_int_of_yojson"]
8282+ (* bool *)
8383+ | Ptyp_constr ({txt= Lident "bool"; _}, []) ->
8484+ [of_ "query_bool_of_yojson"]
8585+ (* T option -> inspect T *)
8686+ | Ptyp_constr ({txt= Lident "option"; _}, [inner]) -> (
8787+ match inner.ptyp_desc with
8888+ | Ptyp_constr ({txt= Lident "int"; _}, []) ->
8989+ [of_ "query_int_option_of_yojson"]
9090+ | Ptyp_constr ({txt= Lident "bool"; _}, []) ->
9191+ [of_ "query_bool_option_of_yojson"]
9292+ (* T list option -> inspect T *)
9393+ | Ptyp_constr ({txt= Lident "list"; _}, [list_inner]) -> (
9494+ match list_inner.ptyp_desc with
9595+ | Ptyp_constr ({txt= Lident "string"; _}, []) ->
9696+ [ of_ "query_string_list_option_of_yojson"
9797+ ; to_ "query_string_list_option_to_yojson" ]
9898+ | Ptyp_constr ({txt= Lident "int"; _}, []) ->
9999+ [ of_ "query_int_list_option_of_yojson"
100100+ ; to_ "query_int_list_option_to_yojson" ]
101101+ | _ ->
102102+ [] )
103103+ | _ ->
104104+ [] )
105105+ (* T list -> inspect T *)
106106+ | Ptyp_constr ({txt= Lident "list"; _}, [inner]) -> (
107107+ match inner.ptyp_desc with
108108+ | Ptyp_constr ({txt= Lident "string"; _}, []) ->
109109+ [of_ "query_string_list_of_yojson"; to_ "query_string_list_to_yojson"]
110110+ | Ptyp_constr ({txt= Lident "int"; _}, []) ->
111111+ [of_ "query_int_list_of_yojson"; to_ "query_int_list_to_yojson"]
112112+ | _ ->
113113+ [] )
114114+ | _ ->
115115+ []
116116+117117+let transform_label_decl (ld : label_declaration) : label_declaration =
118118+ let extra_attrs = query_attrs_for_type ld.pld_type in
119119+ {ld with pld_attributes= ld.pld_attributes @ extra_attrs}
120120+121121+(* build the [@@deriving yojson {strict = false}] attribute *)
122122+let yojson_deriving_attr ~loc =
123123+ let strict_false =
124124+ ( Loc.make ~loc (Lident "strict")
125125+ , Ast_builder.Default.pexp_construct ~loc
126126+ (Loc.make ~loc (Lident "false"))
127127+ None )
128128+ in
129129+ let yojson_expr =
130130+ Ast_builder.Default.pexp_apply ~loc
131131+ (Ast_builder.Default.pexp_ident ~loc (Loc.make ~loc (Lident "yojson")))
132132+ [(Nolabel, Ast_builder.Default.pexp_record ~loc [strict_false] None)]
133133+ in
134134+ { attr_name= Loc.make ~loc "deriving"
135135+ ; attr_payload= PStr [Ast_builder.Default.pstr_eval ~loc yojson_expr []]
136136+ ; attr_loc= loc }
137137+138138+let is_xrpc_query (attr : attribute) = attr.attr_name.txt = "xrpc_query"
139139+140140+let transform_type_decl (td : type_declaration) =
141141+ let has_xrpc_query = List.exists is_xrpc_query td.ptype_attributes in
142142+ if not has_xrpc_query then td
143143+ else
144144+ let kind =
145145+ match td.ptype_kind with
146146+ | Ptype_record fields ->
147147+ Ptype_record (List.map transform_label_decl fields)
148148+ | other ->
149149+ other
150150+ in
151151+ let attrs =
152152+ List.map
153153+ (fun attr ->
154154+ if is_xrpc_query attr then yojson_deriving_attr ~loc:attr.attr_loc
155155+ else attr )
156156+ td.ptype_attributes
157157+ in
158158+ {td with ptype_kind= kind; ptype_attributes= attrs}
159159+160160+let rec transform_structure str =
161161+ List.map
162162+ (fun (si : structure_item) ->
163163+ match si.pstr_desc with
164164+ | Pstr_type (rf, tds) ->
165165+ {si with pstr_desc= Pstr_type (rf, List.map transform_type_decl tds)}
166166+ | Pstr_module mb ->
167167+ { si with
168168+ pstr_desc=
169169+ Pstr_module {mb with pmb_expr= transform_module_expr mb.pmb_expr}
170170+ }
171171+ | Pstr_recmodule mbs ->
172172+ { si with
173173+ pstr_desc=
174174+ Pstr_recmodule
175175+ (List.map
176176+ (fun mb ->
177177+ {mb with pmb_expr= transform_module_expr mb.pmb_expr} )
178178+ mbs ) }
179179+ | _ ->
180180+ si )
181181+ str
182182+183183+and transform_module_expr (me : module_expr) =
184184+ match me.pmod_desc with
185185+ | Pmod_structure str ->
186186+ {me with pmod_desc= Pmod_structure (transform_structure str)}
187187+ | _ ->
188188+ me
189189+190190+let () =
191191+ Driver.register_transformation "hermes_ppx" ~rules:[rule]
192192+ ~instrument:
193193+ (Driver.Instrument.V2.make
194194+ (fun _ctx str -> transform_structure str)
195195+ ~position:Before )