···11-{0 OCaml OpenAPI}
22-33-{1 Overview}
44-55-[openapi] generates type-safe OCaml API clients from OpenAPI 3.x
66-specifications. The generated code uses:
77-88-- {b jsont} for JSON encoding/decoding
99-- {b requests} for HTTP client (Eio-based)
1010-- {b ptime} for date-time handling
1111-1212-{1 Installation}
1313-1414-{[
1515-opam install openapi
1616-]}
1717-1818-{1 Generating a Client}
1919-2020-Use the [openapi-gen] CLI tool to generate OCaml code from an OpenAPI spec:
2121-2222-{[
2323-# Basic generation
2424-openapi-gen generate spec.json -o ./my_api -n my_api
2525-2626-# With dune regeneration rules
2727-openapi-gen generate spec.json -o ./my_api -n my_api --regen
2828-]}
2929-3030-{2 CLI Options}
3131-3232-{ul
3333-{- [-o], [--output] — Output directory for generated code (required)}
3434-{- [-n], [--name] — Package name for generated library (defaults to API title)}
3535-{- [--regen] — Include dune.inc rules for [dune build @gen --auto-promote]}}
3636-3737-{1 Generated Code Structure}
3838-3939-The generator produces a complete dune library:
4040-4141-{[
4242-my_api/
4343-├── dune # Library configuration (wrapped)
4444-├── dune.inc # Regeneration rules (if --regen used)
4545-├── types.ml # Type definitions with jsont codecs
4646-├── types.mli # Type interfaces
4747-├── client.ml # API client functions
4848-├── client.mli # Client interface
4949-├── my_api.ml # Main wrapped module
5050-└── my_api.mli # Main module interface
5151-]}
5252-5353-{1 Using Generated Code}
5454-5555-{2 Accessing Types}
5656-5757-All schema types are generated as modules within [Types]:
5858-5959-{[
6060-(* Access a type *)
6161-let user : My_api.Types.User.t = {
6262- id = 123;
6363- name = "Alice";
6464- email = Some "alice@example.com";
6565-}
6666-6767-(* Encode to JSON *)
6868-let json = Jsont.encode My_api.Types.User.t_jsont user
6969-7070-(* Decode from JSON *)
7171-let user' = Jsont.decode My_api.Types.User.t_jsont json
7272-]}
7373-7474-{2 Making API Requests}
7575-7676-Create a client and call API operations:
7777-7878-{[
7979-let () =
8080- Eio_main.run @@ fun env ->
8181- Eio.Switch.run @@ fun sw ->
8282-8383- (* Create the client *)
8484- let client = My_api.Client.create ~sw env
8585- ~base_url:"https://api.example.com" in
8686-8787- (* Make a request - returns typed value *)
8888- let user = My_api.Client.get_user ~id:"123" client () in
8989- Printf.printf "User: %s\n" user.name
9090-9191- (* List endpoints return typed lists *)
9292- let users = My_api.Client.list_users client () in
9393- List.iter (fun u -> Printf.printf "- %s\n" u.name) users
9494-]}
9595-9696-{2 Request Bodies}
9797-9898-For POST/PUT/PATCH requests, pass the typed value directly:
9999-100100-{[
101101-(* Create typed request body *)
102102-let new_user : My_api.Types.CreateUserDto.t = {
103103- name = "Bob";
104104- email = "bob@example.com";
105105-} in
106106-107107-(* Pass as the body parameter - encoding is automatic *)
108108-let created = My_api.Client.create_user ~body:new_user client ()
109109-]}
110110-111111-{1 Keeping Generated Code Updated}
112112-113113-If you used [--regen], the generated [dune.inc] includes rules to regenerate
114114-the client when the spec changes:
115115-116116-{[
117117-# Regenerate and promote changes
118118-dune build @gen --auto-promote
119119-]}
120120-121121-This is useful for CI pipelines to ensure generated code stays in sync with
122122-the OpenAPI specification.
123123-124124-{1 Library Modules}
125125-126126-{2 Core Modules}
127127-128128-{ul
129129-{- {!module:Openapi.Spec} — OpenAPI 3.x specification types with jsont codecs}
130130-{- {!module:Openapi.Codegen} — Code generation from spec to OCaml}
131131-{- {!module:Openapi.Runtime} — Runtime utilities for generated clients}}
132132-133133-{2 Runtime Utilities}
134134-135135-The {!module:Openapi.Runtime} module provides helpers used by generated code:
136136-137137-{[
138138-(* Path template rendering *)
139139-Openapi.Runtime.Path.render
140140- ~params:[("userId", "123"); ("postId", "456")]
141141- "/users/{userId}/posts/{postId}"
142142-(* => "/users/123/posts/456" *)
143143-144144-(* Query string encoding *)
145145-Openapi.Runtime.Query.encode [("page", "1"); ("limit", "10")]
146146-(* => "?page=1&limit=10" *)
147147-]}
148148-149149-{1 Example: Immich API}
150150-151151-Here's a complete example generating a client for the Immich photo server:
152152-153153-{[
154154-# Generate the client
155155-openapi-gen generate immich-openapi-specs.json -o ./immich -n immich
156156-157157-# In your code:
158158-let () =
159159- Eio_main.run @@ fun env ->
160160- Eio.Switch.run @@ fun sw ->
161161- let client = Immich.Client.create ~sw env
162162- ~base_url:"http://localhost:2283/api" in
163163-164164- (* List albums *)
165165- let albums_json = Immich.Client.get_all_albums client () in
166166-167167- (* Get server info *)
168168- let info = Immich.Client.get_server_info client () in
169169- ...
170170-]}
171171-172172-{1 Limitations}
173173-174174-{2 Schema Generation}
175175-176176-{ul
177177-{- {b oneOf/anyOf} — Union types are mapped to [Jsont.json]. Proper
178178- implementation would generate OCaml variant types with discriminator-based
179179- decoding. See {{:#union-types}Union Types} below for details.}
180180-{- {b allOf} — Composition schemas are mapped to [Jsont.json]. Proper
181181- implementation would merge all referenced schemas into a single record type.}
182182-{- {b additionalProperties} — Dynamic object properties are parsed but not
183183- used in code generation. Objects with [additionalProperties: true] become
184184- [Jsont.json].}
185185-{- {b Recursive schemas} — Schemas that reference themselves are not fully
186186- supported and may cause infinite loops during generation.}
187187-{- {b Nested $ref} — References to references are not resolved; only direct
188188- schema references work.}}
189189-190190-{2 Client Generation}
191191-192192-{ul
193193-{- {b Error responses} — Error schemas (4xx, 5xx) are not generated. Errors
194194- are raised as exceptions with the HTTP status code and body text.}
195195-{- {b Authentication} — Security schemes (apiKey, http, oauth2) are parsed
196196- but not applied to requests. Add headers manually via the requests session.}
197197-{- {b Header parameters} — Header parameters are parsed but not included in
198198- generated function signatures.}
199199-{- {b Cookie parameters} — Cookie parameters are parsed but not included in
200200- generated functions.}
201201-{- {b Parameter references} — [$ref] in parameters are skipped; only inline
202202- parameters are used.}}
203203-204204-{2 Content Types}
205205-206206-{ul
207207-{- {b File uploads} — [multipart/form-data] is not supported. Binary file
208208- uploads require special handling not yet implemented.}
209209-{- {b XML} — Only [application/json] content types are supported.}
210210-{- {b Form encoding} — [application/x-www-form-urlencoded] is not supported.}}
211211-212212-{2 Advanced Features}
213213-214214-{ul
215215-{- {b Callbacks} — Webhook callbacks are parsed but no server code is
216216- generated.}
217217-{- {b Links} — Response links are parsed but not used in code generation.}
218218-{- {b External references} — Only internal [$ref] pointers starting with
219219- [#/] are supported. External file references are not resolved.}}
220220-221221-{1:union-types Implementing Union Types}
222222-223223-To properly support [oneOf]/[anyOf], the generator would need to:
224224-225225-{ol
226226-{- Analyze schemas in the union to determine variant names}
227227-{- Use the [discriminator] property if present to determine the tag field}
228228-{- Generate an OCaml variant type with one constructor per schema}
229229-{- Generate a decoder that:
230230- {ul
231231- {- Reads the discriminator field if present}
232232- {- Pattern matches to select the appropriate decoder}
233233- {- Falls back to trying each decoder in order for anyOf}}}
234234-{- Generate an encoder that pattern matches on the variant}}
235235-236236-Example of what generated code might look like:
237237-238238-{[
239239-(* For oneOf with discriminator *)
240240-type pet =
241241- | Dog of Dog.t
242242- | Cat of Cat.t
243243-244244-let pet_jsont : pet Jsont.t =
245245- (* Read discriminator field "petType" to determine variant *)
246246- ...
247247-]}
248248-249249-{1 See Also}
250250-251251-{ul
252252-{- {{:https://spec.openapis.org/oas/v3.0.3} OpenAPI 3.0 Specification}}
253253-{- {{:https://erratique.ch/software/jsont} jsont documentation}}
254254-{- {{:https://github.com/tarides/requests} requests library}}}
···11-(** Code generation from OpenAPI specifications.
22-33- This module generates OCaml code from parsed OpenAPI specs:
44- - Nested module structure grouped by common schema prefixes
55- - Abstract types with accessor and constructor functions
66- - Client functions placed in relevant type modules
77- - Proper Eio error handling with context
88-*)
99-1010-module Spec = Openapi_spec
1111-1212-(** {1 Name Conversion} *)
1313-1414-module Name = struct
1515- module StringSet = Set.Make(String)
1616-1717- let ocaml_keywords = StringSet.of_list [
1818- "and"; "as"; "assert"; "asr"; "begin"; "class"; "constraint"; "do"; "done";
1919- "downto"; "else"; "end"; "exception"; "external"; "false"; "for"; "fun";
2020- "function"; "functor"; "if"; "in"; "include"; "inherit"; "initializer";
2121- "land"; "lazy"; "let"; "lor"; "lsl"; "lsr"; "lxor"; "match"; "method";
2222- "mod"; "module"; "mutable"; "new"; "nonrec"; "object"; "of"; "open"; "or";
2323- "private"; "rec"; "sig"; "struct"; "then"; "to"; "true"; "try"; "type";
2424- "val"; "virtual"; "when"; "while"; "with"
2525- ]
2626-2727- let escape_keyword s =
2828- if StringSet.mem s ocaml_keywords then s ^ "_" else s
2929-3030- let to_snake_case s =
3131- let buf = Buffer.create (String.length s) in
3232- let prev_upper = ref false in
3333- String.iteri (fun i c ->
3434- match c with
3535- | 'A'..'Z' ->
3636- if i > 0 && not !prev_upper then Buffer.add_char buf '_';
3737- Buffer.add_char buf (Char.lowercase_ascii c);
3838- prev_upper := true
3939- | 'a'..'z' | '0'..'9' | '_' ->
4040- Buffer.add_char buf c;
4141- prev_upper := false
4242- | '-' | ' ' | '.' | '/' ->
4343- Buffer.add_char buf '_';
4444- prev_upper := false
4545- | _ ->
4646- prev_upper := false
4747- ) s;
4848- escape_keyword (Buffer.contents buf)
4949-5050- let to_module_name s =
5151- let snake = to_snake_case s in
5252- let parts = String.split_on_char '_' snake in
5353- String.concat "" (List.map String.capitalize_ascii parts)
5454-5555- let to_type_name s = String.lowercase_ascii (to_snake_case s)
5656-5757- let to_variant_name s = String.capitalize_ascii (to_snake_case s)
5858-5959- (** Split a schema name into prefix and suffix for nested modules.
6060- E.g., "AlbumResponseDto" -> ("Album", "ResponseDto") *)
6161- let split_schema_name (name : string) : string * string =
6262- (* Common suffixes to look for *)
6363- let suffixes = [
6464- "ResponseDto"; "RequestDto"; "CreateDto"; "UpdateDto"; "Dto";
6565- "Response"; "Request"; "Create"; "Update"; "Config"; "Info";
6666- "Status"; "Type"; "Entity"; "Item"; "Entry"; "Data"; "Result"
6767- ] in
6868- let found = List.find_opt (fun suffix ->
6969- String.length name > String.length suffix &&
7070- String.ends_with ~suffix name
7171- ) suffixes in
7272- match found with
7373- | Some suffix ->
7474- let prefix_len = String.length name - String.length suffix in
7575- let prefix = String.sub name 0 prefix_len in
7676- if prefix = "" then (name, "T")
7777- else (prefix, suffix)
7878- | None ->
7979- (* No known suffix, use as-is with submodule T *)
8080- (name, "T")
8181-8282- let operation_name ~(method_ : string) ~(path : string) ~(operation_id : string option) =
8383- match operation_id with
8484- | Some id -> to_snake_case id
8585- | None ->
8686- let method_name = String.lowercase_ascii method_ in
8787- let path_parts = String.split_on_char '/' path
8888- |> List.filter (fun s -> s <> "" && not (String.length s > 0 && s.[0] = '{'))
8989- in
9090- let path_name = String.concat "_" (List.map to_snake_case path_parts) in
9191- method_name ^ "_" ^ path_name
9292-end
9393-9494-(** {1 OCamldoc Helpers} *)
9595-9696-let escape_doc s =
9797- let s = String.concat "\\}" (String.split_on_char '}' s) in
9898- String.concat "\\{" (String.split_on_char '{' s)
9999-100100-let format_doc ?(indent=0) description =
101101- let prefix = String.make indent ' ' in
102102- match description with
103103- | None | Some "" -> ""
104104- | Some desc -> Printf.sprintf "%s(** %s *)\n" prefix (escape_doc desc)
105105-106106-let format_doc_block ?(indent=0) ~summary ?description () =
107107- let prefix = String.make indent ' ' in
108108- match summary, description with
109109- | None, None -> ""
110110- | Some s, None -> Printf.sprintf "%s(** %s *)\n" prefix (escape_doc s)
111111- | None, Some d -> Printf.sprintf "%s(** %s *)\n" prefix (escape_doc d)
112112- | Some s, Some d ->
113113- Printf.sprintf "%s(** %s\n\n%s %s *)\n" prefix (escape_doc s) prefix (escape_doc d)
114114-115115-let format_param_doc name description =
116116- match description with
117117- | None | Some "" -> ""
118118- | Some d -> Printf.sprintf " @param %s %s\n" name (escape_doc d)
119119-120120-(** {1 JSON Helpers} *)
121121-122122-let json_string = function
123123- | Jsont.String (s, _) -> Some s
124124- | _ -> None
125125-126126-let json_object = function
127127- | Jsont.Object (mems, _) -> Some mems
128128- | _ -> None
129129-130130-let get_ref json =
131131- Option.bind (json_object json) (fun mems ->
132132- List.find_map (fun ((n, _), v) ->
133133- if n = "$ref" then json_string v else None
134134- ) mems)
135135-136136-let get_member name json =
137137- Option.bind (json_object json) (fun mems ->
138138- List.find_map (fun ((n, _), v) ->
139139- if n = name then Some v else None
140140- ) mems)
141141-142142-let get_string_member name json =
143143- Option.bind (get_member name json) json_string
144144-145145-(** {1 Schema Analysis} *)
146146-147147-let schema_name_from_ref (ref_ : string) : string option =
148148- match String.split_on_char '/' ref_ with
149149- | ["#"; "components"; "schemas"; name] -> Some name
150150- | _ -> None
151151-152152-let rec find_refs_in_json (json : Jsont.json) : string list =
153153- match json with
154154- | Jsont.Object (mems, _) ->
155155- (match List.find_map (fun ((n, _), v) ->
156156- if n = "$ref" then json_string v else None) mems with
157157- | Some ref_ -> Option.to_list (schema_name_from_ref ref_)
158158- | None -> List.concat_map (fun (_, v) -> find_refs_in_json v) mems)
159159- | Jsont.Array (items, _) -> List.concat_map find_refs_in_json items
160160- | _ -> []
161161-162162-let find_schema_dependencies (schema : Spec.schema) : string list =
163163- let from_properties = List.concat_map (fun (_, json) -> find_refs_in_json json) schema.properties in
164164- let refs_from_list = Option.fold ~none:[] ~some:(List.concat_map find_refs_in_json) in
165165- let from_items = Option.fold ~none:[] ~some:find_refs_in_json schema.items in
166166- List.sort_uniq String.compare
167167- (from_properties @ from_items @ refs_from_list schema.all_of
168168- @ refs_from_list schema.one_of @ refs_from_list schema.any_of)
169169-170170-(** {1 Module Tree Structure} *)
171171-172172-module StringMap = Map.Make(String)
173173-module StringSet = Set.Make(String)
174174-175175-(** {1 Topological Sort} *)
176176-177177-(** Kahn's algorithm for topological sorting.
178178- Returns nodes in dependency order (dependencies first). *)
179179-let topological_sort (nodes : string list) (deps : string -> string list) : string list =
180180- (* Build adjacency list and in-degree map *)
181181- let nodes_set = StringSet.of_list nodes in
182182- let in_degree = List.fold_left (fun m node ->
183183- StringMap.add node 0 m
184184- ) StringMap.empty nodes in
185185- let adj = List.fold_left (fun m node ->
186186- StringMap.add node [] m
187187- ) StringMap.empty nodes in
188188- (* Add edges: if A depends on B, add edge B -> A *)
189189- let (in_degree, adj) = List.fold_left (fun (in_degree, adj) node ->
190190- let node_deps = deps node |> List.filter (fun d -> StringSet.mem d nodes_set) in
191191- let in_degree = StringMap.add node (List.length node_deps) in_degree in
192192- let adj = List.fold_left (fun adj dep ->
193193- let existing = Option.value ~default:[] (StringMap.find_opt dep adj) in
194194- StringMap.add dep (node :: existing) adj
195195- ) adj node_deps in
196196- (in_degree, adj)
197197- ) (in_degree, adj) nodes in
198198- (* Start with nodes that have no dependencies *)
199199- let queue = List.filter (fun n ->
200200- StringMap.find n in_degree = 0
201201- ) nodes in
202202- let rec process queue in_degree result =
203203- match queue with
204204- | [] -> List.rev result
205205- | node :: rest ->
206206- let result = node :: result in
207207- let dependents = Option.value ~default:[] (StringMap.find_opt node adj) in
208208- let (queue', in_degree) = List.fold_left (fun (q, deg) dep ->
209209- let new_deg = StringMap.find dep deg - 1 in
210210- let deg = StringMap.add dep new_deg deg in
211211- if new_deg = 0 then (dep :: q, deg) else (q, deg)
212212- ) (rest, in_degree) dependents in
213213- process queue' in_degree result
214214- in
215215- process queue in_degree []
216216-217217-type field_info = {
218218- ocaml_name : string;
219219- json_name : string;
220220- ocaml_type : string;
221221- base_type : string;
222222- is_optional : bool;
223223- is_required : bool;
224224- description : string option;
225225-}
226226-227227-type schema_info = {
228228- original_name : string;
229229- prefix : string;
230230- suffix : string;
231231- schema : Spec.schema;
232232- fields : field_info list;
233233- is_enum : bool;
234234- enum_variants : (string * string) list; (* ocaml_name, json_value *)
235235- description : string option;
236236-}
237237-238238-type operation_info = {
239239- func_name : string;
240240- operation_id : string option;
241241- summary : string option;
242242- description : string option;
243243- tags : string list;
244244- path : string;
245245- method_ : string;
246246- path_params : (string * string * string option * bool) list; (* ocaml, json, desc, required *)
247247- query_params : (string * string * string option * bool) list;
248248- body_schema_ref : string option;
249249- response_schema_ref : string option;
250250-}
251251-252252-type module_node = {
253253- name : string;
254254- schemas : schema_info list;
255255- operations : operation_info list;
256256- dependencies : StringSet.t; (* Other prefix modules this depends on *)
257257- children : module_node StringMap.t;
258258-}
259259-260260-let empty_node name = { name; schemas = []; operations = []; dependencies = StringSet.empty; children = StringMap.empty }
261261-262262-(** {1 Type Resolution} *)
263263-264264-let rec type_of_json_schema (json : Jsont.json) : string * bool =
265265- (* Check if the schema is nullable *)
266266- let is_nullable = match get_member "nullable" json with
267267- | Some (Jsont.Bool (b, _)) -> b
268268- | _ -> false
269269- in
270270- match get_ref json with
271271- | Some ref_ ->
272272- (match schema_name_from_ref ref_ with
273273- | Some name ->
274274- let prefix, suffix = Name.split_schema_name name in
275275- (Printf.sprintf "%s.%s.t" (Name.to_module_name prefix) (Name.to_module_name suffix), is_nullable)
276276- | None -> ("Jsont.json", is_nullable))
277277- | None ->
278278- match get_string_member "type" json with
279279- | Some "string" ->
280280- (match get_string_member "format" json with
281281- | Some "date-time" -> ("Ptime.t", is_nullable)
282282- | _ -> ("string", is_nullable))
283283- | Some "integer" ->
284284- (match get_string_member "format" json with
285285- | Some "int64" -> ("int64", is_nullable)
286286- | Some "int32" -> ("int32", is_nullable)
287287- | _ -> ("int", is_nullable))
288288- | Some "number" -> ("float", is_nullable)
289289- | Some "boolean" -> ("bool", is_nullable)
290290- | Some "array" ->
291291- (match get_member "items" json with
292292- | Some items ->
293293- let (elem_type, _) = type_of_json_schema items in
294294- (elem_type ^ " list", is_nullable)
295295- | None -> ("Jsont.json list", is_nullable))
296296- | Some "object" -> ("Jsont.json", is_nullable)
297297- | _ -> ("Jsont.json", is_nullable)
298298-299299-let rec jsont_of_base_type = function
300300- | "string" -> "Jsont.string"
301301- | "int" -> "Jsont.int"
302302- | "int32" -> "Jsont.int32"
303303- | "int64" -> "Jsont.int64"
304304- | "float" -> "Jsont.number"
305305- | "bool" -> "Jsont.bool"
306306- | "Ptime.t" -> "Openapi.Runtime.ptime_jsont"
307307- | "Jsont.json" -> "Jsont.json"
308308- | s when String.ends_with ~suffix:" list" s ->
309309- let elem = String.sub s 0 (String.length s - 5) in
310310- Printf.sprintf "(Jsont.list %s)" (jsont_of_base_type elem)
311311- | s when String.ends_with ~suffix:".t" s ->
312312- let module_path = String.sub s 0 (String.length s - 2) in
313313- module_path ^ ".jsont"
314314- | _ -> "Jsont.json"
315315-316316-(** {1 Schema Processing} *)
317317-318318-let analyze_schema (name : string) (schema : Spec.schema) : schema_info =
319319- let prefix, suffix = Name.split_schema_name name in
320320- let is_enum = Option.is_some schema.enum in
321321- let enum_variants = match schema.enum with
322322- | Some values ->
323323- List.filter_map (fun json ->
324324- match json with
325325- | Jsont.String (s, _) -> Some (Name.to_variant_name s, s)
326326- | _ -> None
327327- ) values
328328- | None -> []
329329- in
330330- let fields = List.map (fun (field_name, field_json) ->
331331- let ocaml_name = Name.to_snake_case field_name in
332332- let is_required = List.mem field_name schema.required in
333333- let (base_type, json_nullable) = type_of_json_schema field_json in
334334- let is_optional = json_nullable || not is_required in
335335- let ocaml_type = if is_optional then base_type ^ " option" else base_type in
336336- let description = get_string_member "description" field_json in
337337- { ocaml_name; json_name = field_name; ocaml_type; base_type; is_optional; is_required; description }
338338- ) schema.properties in
339339- { original_name = name; prefix; suffix; schema; fields; is_enum; enum_variants;
340340- description = schema.description }
341341-342342-(** {1 Operation Processing} *)
343343-344344-let analyze_operation ~path ~method_ (op : Spec.operation) : operation_info =
345345- let func_name = Name.operation_name ~method_ ~path ~operation_id:op.operation_id in
346346- let params = List.filter_map (fun (p : Spec.parameter Spec.or_ref) ->
347347- match p with Spec.Value p -> Some p | Spec.Ref _ -> None
348348- ) op.parameters in
349349-350350- let path_params = List.filter_map (fun (p : Spec.parameter) ->
351351- if p.in_ = Spec.Path then
352352- Some (Name.to_snake_case p.name, p.name, p.description, p.required)
353353- else None
354354- ) params in
355355-356356- let query_params = List.filter_map (fun (p : Spec.parameter) ->
357357- if p.in_ = Spec.Query then
358358- Some (Name.to_snake_case p.name, p.name, p.description, p.required)
359359- else None
360360- ) params in
361361-362362- let body_schema_ref = match op.request_body with
363363- | Some (Spec.Value (rb : Spec.request_body)) ->
364364- List.find_map (fun (ct, (media : Spec.media_type)) ->
365365- if String.length ct >= 16 && String.sub ct 0 16 = "application/json" then
366366- match media.schema with
367367- | Some (Spec.Ref r) -> schema_name_from_ref r
368368- | _ -> None
369369- else None
370370- ) rb.content
371371- | _ -> None
372372- in
373373-374374- let response_schema_ref =
375375- let find_in_content content =
376376- List.find_map (fun (ct, (media : Spec.media_type)) ->
377377- if String.length ct >= 16 && String.sub ct 0 16 = "application/json" then
378378- match media.schema with
379379- | Some (Spec.Ref r) -> schema_name_from_ref r
380380- | Some (Spec.Value s) when s.type_ = Some "array" ->
381381- Option.bind s.items (fun items -> Option.bind (get_ref items) schema_name_from_ref)
382382- | _ -> None
383383- else None
384384- ) content
385385- in
386386- let try_status status =
387387- List.find_map (fun (code, resp) ->
388388- if code = status then
389389- match resp with
390390- | Spec.Value (r : Spec.response) -> find_in_content r.content
391391- | _ -> None
392392- else None
393393- ) op.responses.responses
394394- in
395395- match try_status "200" with
396396- | Some r -> Some r
397397- | None -> match try_status "201" with
398398- | Some r -> Some r
399399- | None -> match op.responses.default with
400400- | Some (Spec.Value (r : Spec.response)) -> find_in_content r.content
401401- | _ -> None
402402- in
403403-404404- { func_name; operation_id = op.operation_id; summary = op.summary;
405405- description = op.description; tags = op.tags; path; method_;
406406- path_params; query_params; body_schema_ref; response_schema_ref }
407407-408408-(** {1 Module Tree Building} *)
409409-410410-(** Extract prefix module dependencies from a schema's fields *)
411411-let schema_prefix_deps (schema : schema_info) : StringSet.t =
412412- let deps = List.filter_map (fun (f : field_info) ->
413413- (* Check if the type references another module *)
414414- if String.contains f.base_type '.' then
415415- (* Extract first component before the dot *)
416416- match String.split_on_char '.' f.base_type with
417417- | prefix :: _ when prefix <> "Jsont" && prefix <> "Ptime" && prefix <> "Openapi" ->
418418- Some prefix
419419- | _ -> None
420420- else None
421421- ) schema.fields in
422422- StringSet.of_list deps
423423-424424-(** Extract prefix module dependencies from an operation's types *)
425425-let operation_prefix_deps (op : operation_info) : StringSet.t =
426426- let body_dep = match op.body_schema_ref with
427427- | Some name ->
428428- let prefix, _ = Name.split_schema_name name in
429429- Some (Name.to_module_name prefix)
430430- | None -> None
431431- in
432432- let response_dep = match op.response_schema_ref with
433433- | Some name ->
434434- let prefix, _ = Name.split_schema_name name in
435435- Some (Name.to_module_name prefix)
436436- | None -> None
437437- in
438438- StringSet.of_list (List.filter_map Fun.id [body_dep; response_dep])
439439-440440-let build_module_tree (schemas : schema_info list) (operations : operation_info list) : module_node * string list =
441441- let root = empty_node "Root" in
442442-443443- (* Add schemas to tree and track dependencies *)
444444- let root = List.fold_left (fun root schema ->
445445- let prefix_mod = Name.to_module_name schema.prefix in
446446- let child = match StringMap.find_opt prefix_mod root.children with
447447- | Some c -> c
448448- | None -> empty_node prefix_mod
449449- in
450450- let schema_deps = schema_prefix_deps schema in
451451- (* Remove self-dependency *)
452452- let schema_deps = StringSet.remove prefix_mod schema_deps in
453453- let child = { child with
454454- schemas = schema :: child.schemas;
455455- dependencies = StringSet.union child.dependencies schema_deps
456456- } in
457457- { root with children = StringMap.add prefix_mod child root.children }
458458- ) root schemas in
459459-460460- (* Add operations to tree based on response type, and track operation dependencies *)
461461- let root = List.fold_left (fun root op ->
462462- match op.response_schema_ref with
463463- | Some ref_name ->
464464- let prefix, _ = Name.split_schema_name ref_name in
465465- let prefix_mod = Name.to_module_name prefix in
466466- let child = match StringMap.find_opt prefix_mod root.children with
467467- | Some c -> c
468468- | None -> empty_node prefix_mod
469469- in
470470- let op_deps = operation_prefix_deps op in
471471- (* Remove self-dependency *)
472472- let op_deps = StringSet.remove prefix_mod op_deps in
473473- let child = { child with
474474- operations = op :: child.operations;
475475- dependencies = StringSet.union child.dependencies op_deps
476476- } in
477477- { root with children = StringMap.add prefix_mod child root.children }
478478- | None ->
479479- (* Put in Client module for operations without typed response *)
480480- let child = match StringMap.find_opt "Client" root.children with
481481- | Some c -> c
482482- | None -> empty_node "Client"
483483- in
484484- let op_deps = operation_prefix_deps op in
485485- let op_deps = StringSet.remove "Client" op_deps in
486486- let child = { child with
487487- operations = op :: child.operations;
488488- dependencies = StringSet.union child.dependencies op_deps
489489- } in
490490- { root with children = StringMap.add "Client" child root.children }
491491- ) root operations in
492492-493493- (* Get sorted list of module names (dependencies first) *)
494494- let module_names = StringMap.fold (fun name _ acc -> name :: acc) root.children [] in
495495- let deps_of name =
496496- match StringMap.find_opt name root.children with
497497- | Some node -> StringSet.elements node.dependencies
498498- | None -> []
499499- in
500500- let sorted = topological_sort module_names deps_of in
501501-502502- (root, sorted)
503503-504504-(** {1 Code Generation} *)
505505-506506-let gen_enum_impl (schema : schema_info) : string =
507507- let doc = format_doc schema.description in
508508- if schema.enum_variants = [] then
509509- Printf.sprintf "%stype t = string\n\nlet jsont = Jsont.string" doc
510510- else
511511- let type_def = Printf.sprintf "%stype t = [\n%s\n]" doc
512512- (String.concat "\n" (List.map (fun (v, _) -> " | `" ^ v) schema.enum_variants))
513513- in
514514- let dec_cases = String.concat "\n" (List.map (fun (v, raw) ->
515515- Printf.sprintf " | %S -> `%s" raw v
516516- ) schema.enum_variants) in
517517- let enc_cases = String.concat "\n" (List.map (fun (v, raw) ->
518518- Printf.sprintf " | `%s -> %S" v raw
519519- ) schema.enum_variants) in
520520- Printf.sprintf {|%s
521521-522522-let jsont : t Jsont.t =
523523- Jsont.map Jsont.string ~kind:%S
524524- ~dec:(function
525525-%s
526526- | s -> Jsont.Error.msgf Jsont.Meta.none "Unknown value: %%s" s)
527527- ~enc:(function
528528-%s)|} type_def schema.original_name dec_cases enc_cases
529529-530530-let gen_enum_intf (schema : schema_info) : string =
531531- let doc = format_doc schema.description in
532532- if schema.enum_variants = [] then
533533- Printf.sprintf "%stype t = string\n\nval jsont : t Jsont.t" doc
534534- else
535535- let type_def = Printf.sprintf "%stype t = [\n%s\n]" doc
536536- (String.concat "\n" (List.map (fun (v, _) -> " | `" ^ v) schema.enum_variants))
537537- in
538538- Printf.sprintf "%s\n\nval jsont : t Jsont.t" type_def
539539-540540-(** Localize an OCaml type string by stripping the current_prefix module *)
541541-let localize_type ~current_prefix (type_str : string) : string =
542542- (* Handle patterns like "User.ResponseDto.t" -> "ResponseDto.t" if current_prefix = "User" *)
543543- let prefix_dot = current_prefix ^ "." in
544544- let strip_prefix s =
545545- if String.length s >= String.length prefix_dot &&
546546- String.sub s 0 (String.length prefix_dot) = prefix_dot then
547547- String.sub s (String.length prefix_dot) (String.length s - String.length prefix_dot)
548548- else s
549549- in
550550- (* Handle "X list", "X option", and nested combinations *)
551551- let rec localize s =
552552- if String.ends_with ~suffix:" list" s then
553553- let elem = String.sub s 0 (String.length s - 5) in
554554- (localize elem) ^ " list"
555555- else if String.ends_with ~suffix:" option" s then
556556- let elem = String.sub s 0 (String.length s - 7) in
557557- (localize elem) ^ " option"
558558- else
559559- strip_prefix s
560560- in
561561- localize type_str
562562-563563-(** Localize a jsont codec string by stripping the current_prefix module *)
564564-let rec localize_jsont ~current_prefix (jsont_str : string) : string =
565565- let prefix_dot = current_prefix ^ "." in
566566- (* Handle patterns like "User.ResponseDto.jsont" -> "ResponseDto.jsont" *)
567567- (* Also handle "(Jsont.list User.ResponseDto.jsont)" *)
568568- if String.length jsont_str >= String.length prefix_dot then
569569- if String.sub jsont_str 0 (String.length prefix_dot) = prefix_dot then
570570- String.sub jsont_str (String.length prefix_dot) (String.length jsont_str - String.length prefix_dot)
571571- else if String.length jsont_str > 12 && String.sub jsont_str 0 12 = "(Jsont.list " then
572572- let inner = String.sub jsont_str 12 (String.length jsont_str - 13) in
573573- "(Jsont.list " ^ localize_jsont ~current_prefix inner ^ ")"
574574- else
575575- jsont_str
576576- else
577577- jsont_str
578578-579579-let gen_record_impl ~current_prefix (schema : schema_info) : string =
580580- let loc_type = localize_type ~current_prefix in
581581- let loc_jsont = localize_jsont ~current_prefix in
582582- let doc = format_doc schema.description in
583583- if schema.fields = [] then
584584- Printf.sprintf "%stype t = Jsont.json\n\nlet jsont = Jsont.json\n\nlet v () = Jsont.Null ((), Jsont.Meta.none)" doc
585585- else
586586- (* Private type definition *)
587587- let type_fields = String.concat "\n" (List.map (fun (f : field_info) ->
588588- let field_doc = match f.description with
589589- | Some d -> Printf.sprintf " (** %s *)" (escape_doc d)
590590- | None -> ""
591591- in
592592- Printf.sprintf " %s : %s;%s" f.ocaml_name (loc_type f.ocaml_type) field_doc
593593- ) schema.fields) in
594594-595595- let type_def = Printf.sprintf "%stype t = {\n%s\n}" doc type_fields in
596596-597597- (* Constructor function v *)
598598- let required_fields = List.filter (fun (f : field_info) -> not f.is_optional) schema.fields in
599599- let optional_fields = List.filter (fun (f : field_info) -> f.is_optional) schema.fields in
600600- let v_params =
601601- (List.map (fun (f : field_info) -> Printf.sprintf "~%s" f.ocaml_name) required_fields) @
602602- (List.map (fun (f : field_info) -> Printf.sprintf "?%s" f.ocaml_name) optional_fields) @
603603- ["()"]
604604- in
605605- let v_body = String.concat "; " (List.map (fun (f : field_info) -> f.ocaml_name) schema.fields) in
606606- let v_func = Printf.sprintf "let v %s = { %s }" (String.concat " " v_params) v_body in
607607-608608- (* Accessor functions *)
609609- let accessors = String.concat "\n" (List.map (fun (f : field_info) ->
610610- Printf.sprintf "let %s t = t.%s" f.ocaml_name f.ocaml_name
611611- ) schema.fields) in
612612-613613- (* Jsont codec *)
614614- let make_params = String.concat " " (List.map (fun (f : field_info) -> f.ocaml_name) schema.fields) in
615615- let jsont_members = String.concat "\n" (List.map (fun (f : field_info) ->
616616- let codec = loc_jsont (jsont_of_base_type f.base_type) in
617617- if f.is_optional then
618618- if f.is_required then
619619- Printf.sprintf " |> Jsont.Object.mem %S (Jsont.option %s)\n ~dec_absent:None ~enc_omit:Option.is_none ~enc:(fun r -> r.%s)"
620620- f.json_name codec f.ocaml_name
621621- else
622622- Printf.sprintf " |> Jsont.Object.opt_mem %S %s ~enc:(fun r -> r.%s)"
623623- f.json_name codec f.ocaml_name
624624- else
625625- Printf.sprintf " |> Jsont.Object.mem %S %s ~enc:(fun r -> r.%s)"
626626- f.json_name codec f.ocaml_name
627627- ) schema.fields) in
628628-629629- Printf.sprintf {|%s
630630-631631-%s
632632-633633-%s
634634-635635-let jsont : t Jsont.t =
636636- Jsont.Object.map ~kind:%S
637637- (fun %s -> { %s })
638638-%s
639639- |> Jsont.Object.skip_unknown
640640- |> Jsont.Object.finish|}
641641- type_def v_func accessors schema.original_name make_params v_body jsont_members
642642-643643-let gen_record_intf ~current_prefix (schema : schema_info) : string =
644644- let loc_type = localize_type ~current_prefix in
645645- let doc = format_doc schema.description in
646646- if schema.fields = [] then
647647- Printf.sprintf "%stype t\n\nval jsont : t Jsont.t\n\nval v : unit -> t" doc
648648- else
649649- (* Abstract type *)
650650- let type_decl = Printf.sprintf "%stype t" doc in
651651-652652- (* Constructor signature *)
653653- let required_fields = List.filter (fun (f : field_info) -> not f.is_optional) schema.fields in
654654- let optional_fields = List.filter (fun (f : field_info) -> f.is_optional) schema.fields in
655655- let v_param_docs = String.concat ""
656656- ((List.map (fun (f : field_info) -> format_param_doc f.ocaml_name f.description) required_fields) @
657657- (List.map (fun (f : field_info) -> format_param_doc f.ocaml_name f.description) optional_fields))
658658- in
659659- let v_params =
660660- (List.map (fun (f : field_info) -> Printf.sprintf "%s:%s" f.ocaml_name (loc_type f.base_type)) required_fields) @
661661- (List.map (fun (f : field_info) -> Printf.sprintf "?%s:%s" f.ocaml_name (loc_type f.base_type)) optional_fields) @
662662- ["unit"; "t"]
663663- in
664664- let v_doc = if v_param_docs = "" then "(** Construct a value *)\n"
665665- else Printf.sprintf "(** Construct a value\n%s*)\n" v_param_docs in
666666- let v_sig = Printf.sprintf "%sval v : %s" v_doc (String.concat " -> " v_params) in
667667-668668- (* Accessor signatures *)
669669- let accessor_sigs = String.concat "\n\n" (List.map (fun (f : field_info) ->
670670- let acc_doc = match f.description with
671671- | Some d -> Printf.sprintf "(** %s *)\n" (escape_doc d)
672672- | None -> ""
673673- in
674674- Printf.sprintf "%sval %s : t -> %s" acc_doc f.ocaml_name (loc_type f.ocaml_type)
675675- ) schema.fields) in
676676-677677- Printf.sprintf "%s\n\n%s\n\n%s\n\nval jsont : t Jsont.t"
678678- type_decl v_sig accessor_sigs
679679-680680-(** Format a jsont codec reference, stripping the current_prefix if present *)
681681-let format_jsont_ref ~current_prefix (schema_ref : string) : string =
682682- let prefix, suffix = Name.split_schema_name schema_ref in
683683- let prefix_mod = Name.to_module_name prefix in
684684- let suffix_mod = Name.to_module_name suffix in
685685- if prefix_mod = current_prefix then
686686- Printf.sprintf "%s.jsont" suffix_mod
687687- else
688688- Printf.sprintf "%s.%s.jsont" prefix_mod suffix_mod
689689-690690-let gen_operation_impl ~current_prefix (op : operation_info) : string =
691691- let doc = format_doc_block ~summary:op.summary ?description:op.description () in
692692- let param_docs = String.concat ""
693693- ((List.map (fun (n, _, d, _) -> format_param_doc n d) op.path_params) @
694694- (List.map (fun (n, _, d, _) -> format_param_doc n d) op.query_params)) in
695695- let full_doc = if param_docs = "" then doc
696696- else if doc = "" then Printf.sprintf "(**\n%s*)\n" param_docs
697697- else String.sub doc 0 (String.length doc - 3) ^ param_docs ^ "*)\n" in
698698-699699- let path_args = List.map (fun (n, _, _, _) -> Printf.sprintf "~%s" n) op.path_params in
700700- let query_args = List.map (fun (n, _, _, req) ->
701701- if req then Printf.sprintf "~%s" n else Printf.sprintf "?%s" n
702702- ) op.query_params in
703703- (* DELETE and HEAD don't support body in the requests library *)
704704- let method_supports_body = not (List.mem op.method_ ["DELETE"; "HEAD"; "OPTIONS"]) in
705705- let body_arg = match op.body_schema_ref, method_supports_body with
706706- | Some _, true -> ["~body"]
707707- | _ -> []
708708- in
709709- let all_args = path_args @ query_args @ body_arg @ ["client"; "()"] in
710710-711711- let path_render =
712712- if op.path_params = [] then Printf.sprintf "%S" op.path
713713- else
714714- let bindings = List.map (fun (ocaml, json, _, _) ->
715715- Printf.sprintf "(%S, %s)" json ocaml
716716- ) op.path_params in
717717- Printf.sprintf "Openapi.Runtime.Path.render ~params:[%s] %S"
718718- (String.concat "; " bindings) op.path
719719- in
720720-721721- let query_build =
722722- if op.query_params = [] then "\"\""
723723- else
724724- let parts = List.map (fun (ocaml, json, _, req) ->
725725- if req then Printf.sprintf "Openapi.Runtime.Query.singleton ~key:%S ~value:%s" json ocaml
726726- else Printf.sprintf "Openapi.Runtime.Query.optional ~key:%S ~value:%s" json ocaml
727727- ) op.query_params in
728728- Printf.sprintf "Openapi.Runtime.Query.encode (List.concat [%s])" (String.concat "; " parts)
729729- in
730730-731731- let method_lower = String.lowercase_ascii op.method_ in
732732- let body_codec = match op.body_schema_ref with
733733- | Some name -> format_jsont_ref ~current_prefix name
734734- | None -> "Jsont.json"
735735- in
736736- (* DELETE and HEAD don't support body in the requests library *)
737737- let method_supports_body = not (List.mem op.method_ ["DELETE"; "HEAD"; "OPTIONS"]) in
738738- let http_call = match op.body_schema_ref, method_supports_body with
739739- | Some _, true ->
740740- Printf.sprintf "Requests.%s client.session ~body:(Requests.Body.json (Openapi.Runtime.Json.encode_json %s body)) url"
741741- method_lower body_codec
742742- | Some _, false ->
743743- (* Method doesn't support body - ignore the body parameter *)
744744- Printf.sprintf "Requests.%s client.session url" method_lower
745745- | None, _ ->
746746- Printf.sprintf "Requests.%s client.session url" method_lower
747747- in
748748-749749- let response_codec = match op.response_schema_ref with
750750- | Some name -> format_jsont_ref ~current_prefix name
751751- | None -> "Jsont.json"
752752- in
753753-754754- let decode = if response_codec = "Jsont.json" then
755755- "Requests.Response.json response"
756756- else
757757- Printf.sprintf "Openapi.Runtime.Json.decode_json_exn %s (Requests.Response.json response)" response_codec
758758- in
759759-760760- Printf.sprintf {|%slet %s %s =
761761- let op_name = %S in
762762- let url_path = %s in
763763- let query = %s in
764764- let url = client.base_url ^ url_path ^ query in
765765- let response =
766766- try %s
767767- with Eio.Io _ as ex ->
768768- let bt = Printexc.get_raw_backtrace () in
769769- Eio.Exn.reraise_with_context ex bt "calling %%s %%s" %S url
770770- in
771771- if Requests.Response.ok response then
772772- %s
773773- else
774774- raise (Openapi.Runtime.Api_error {
775775- operation = op_name;
776776- method_ = %S;
777777- url;
778778- status = Requests.Response.status_code response;
779779- body = Requests.Response.text response;
780780- })|}
781781- full_doc op.func_name (String.concat " " all_args)
782782- op.func_name path_render query_build http_call op.method_ decode op.method_
783783-784784-(** Format a type reference, stripping the current_prefix if present *)
785785-let format_type_ref ~current_prefix (schema_ref : string) : string =
786786- let prefix, suffix = Name.split_schema_name schema_ref in
787787- let prefix_mod = Name.to_module_name prefix in
788788- let suffix_mod = Name.to_module_name suffix in
789789- if prefix_mod = current_prefix then
790790- (* Local reference - use unqualified name *)
791791- Printf.sprintf "%s.t" suffix_mod
792792- else
793793- Printf.sprintf "%s.%s.t" prefix_mod suffix_mod
794794-795795-let gen_operation_intf ~current_prefix (op : operation_info) : string =
796796- let doc = format_doc_block ~summary:op.summary ?description:op.description () in
797797- let param_docs = String.concat ""
798798- ((List.map (fun (n, _, d, _) -> format_param_doc n d) op.path_params) @
799799- (List.map (fun (n, _, d, _) -> format_param_doc n d) op.query_params)) in
800800- let full_doc = if param_docs = "" then doc
801801- else if doc = "" then Printf.sprintf "(**\n%s*)\n" param_docs
802802- else String.sub doc 0 (String.length doc - 3) ^ param_docs ^ "*)\n" in
803803-804804- let path_args = List.map (fun (n, _, _, _) -> Printf.sprintf "%s:string" n) op.path_params in
805805- let query_args = List.map (fun (n, _, _, req) ->
806806- if req then Printf.sprintf "%s:string" n else Printf.sprintf "?%s:string" n
807807- ) op.query_params in
808808- let method_supports_body = not (List.mem op.method_ ["DELETE"; "HEAD"; "OPTIONS"]) in
809809- let body_arg = match op.body_schema_ref, method_supports_body with
810810- | Some name, true -> [Printf.sprintf "body:%s" (format_type_ref ~current_prefix name)]
811811- | _ -> []
812812- in
813813- let response_type = match op.response_schema_ref with
814814- | Some name -> format_type_ref ~current_prefix name
815815- | None -> "Jsont.json"
816816- in
817817- let all_args = path_args @ query_args @ body_arg @ ["t"; "unit"; response_type] in
818818-819819- Printf.sprintf "%sval %s : %s" full_doc op.func_name (String.concat " -> " all_args)
820820-821821-(** {1 Full Module Generation} *)
822822-823823-let gen_submodule_impl ~current_prefix (schema : schema_info) : string =
824824- let suffix_mod = Name.to_module_name schema.suffix in
825825- let content = if schema.is_enum then gen_enum_impl schema else gen_record_impl ~current_prefix schema in
826826- let indented = String.split_on_char '\n' content |> List.map (fun l -> " " ^ l) |> String.concat "\n" in
827827- Printf.sprintf "module %s = struct\n%s\nend" suffix_mod indented
828828-829829-let gen_submodule_intf ~current_prefix (schema : schema_info) : string =
830830- let suffix_mod = Name.to_module_name schema.suffix in
831831- let content = if schema.is_enum then gen_enum_intf schema else gen_record_intf ~current_prefix schema in
832832- let indented = String.split_on_char '\n' content |> List.map (fun l -> " " ^ l) |> String.concat "\n" in
833833- Printf.sprintf "module %s : sig\n%s\nend" suffix_mod indented
834834-835835-(** Extract suffix module dependencies within the same prefix *)
836836-let schema_suffix_deps ~current_prefix (schema : schema_info) : string list =
837837- List.filter_map (fun (f : field_info) ->
838838- (* Check if the type references a sibling module (same prefix) *)
839839- if String.contains f.base_type '.' then
840840- match String.split_on_char '.' f.base_type with
841841- | prefix :: suffix :: _ when prefix = current_prefix ->
842842- Some (Name.to_module_name suffix)
843843- | _ -> None
844844- else None
845845- ) schema.fields
846846-847847-(** Sort schemas within a prefix module by their dependencies *)
848848-let sort_schemas_by_deps ~current_prefix (schemas : schema_info list) : schema_info list =
849849- let suffix_of schema = Name.to_module_name schema.suffix in
850850- let suffix_names = List.map suffix_of schemas in
851851- let deps_of suffix =
852852- match List.find_opt (fun s -> suffix_of s = suffix) schemas with
853853- | Some schema -> schema_suffix_deps ~current_prefix schema |> List.filter (fun d -> List.mem d suffix_names)
854854- | None -> []
855855- in
856856- let sorted = topological_sort suffix_names deps_of in
857857- List.filter_map (fun suffix ->
858858- List.find_opt (fun s -> suffix_of s = suffix) schemas
859859- ) sorted
860860-861861-let gen_prefix_module_impl (node : module_node) : string =
862862- let sorted_schemas = sort_schemas_by_deps ~current_prefix:node.name node.schemas in
863863- let schema_mods = List.map (gen_submodule_impl ~current_prefix:node.name) sorted_schemas in
864864- let op_impls = List.map (gen_operation_impl ~current_prefix:node.name) (List.rev node.operations) in
865865- let content = String.concat "\n\n" (schema_mods @ op_impls) in
866866- let indented = String.split_on_char '\n' content |> List.map (fun l -> " " ^ l) |> String.concat "\n" in
867867- Printf.sprintf "module %s = struct\n%s\nend" node.name indented
868868-869869-let gen_prefix_module_intf (node : module_node) : string =
870870- let sorted_schemas = sort_schemas_by_deps ~current_prefix:node.name node.schemas in
871871- let schema_mods = List.map (gen_submodule_intf ~current_prefix:node.name) sorted_schemas in
872872- let op_intfs = List.map (gen_operation_intf ~current_prefix:node.name) (List.rev node.operations) in
873873- let content = String.concat "\n\n" (schema_mods @ op_intfs) in
874874- let indented = String.split_on_char '\n' content |> List.map (fun l -> " " ^ l) |> String.concat "\n" in
875875- Printf.sprintf "module %s : sig\n%s\nend" node.name indented
876876-877877-(** {1 Top-Level Generation} *)
878878-879879-type config = {
880880- output_dir : string;
881881- package_name : string;
882882- spec_path : string option;
883883-}
884884-885885-let generate_ml (spec : Spec.t) (package_name : string) : string =
886886- let api_desc = Option.value ~default:"Generated API client." spec.info.description in
887887-888888- (* Collect schemas *)
889889- let schemas = match spec.components with
890890- | None -> []
891891- | Some c -> List.filter_map (fun (name, sor) ->
892892- match sor with
893893- | Spec.Ref _ -> None
894894- | Spec.Value s -> Some (analyze_schema name s)
895895- ) c.schemas
896896- in
897897-898898- (* Collect operations *)
899899- let operations = List.concat_map (fun (path, pi) ->
900900- let ops = [
901901- ("GET", pi.Spec.get); ("POST", pi.post); ("PUT", pi.put);
902902- ("DELETE", pi.delete); ("PATCH", pi.patch);
903903- ("HEAD", pi.head); ("OPTIONS", pi.options);
904904- ] in
905905- List.filter_map (fun (method_, op_opt) ->
906906- Option.map (fun op -> analyze_operation ~path ~method_ op) op_opt
907907- ) ops
908908- ) spec.paths in
909909-910910- (* Build module tree *)
911911- let (tree, sorted_modules) = build_module_tree schemas operations in
912912-913913- (* Generate top-level client type and functions *)
914914- let client_impl = {|type t = {
915915- session : Requests.t;
916916- base_url : string;
917917-}
918918-919919-let create ?session ~sw env ~base_url =
920920- let session = match session with
921921- | Some s -> s
922922- | None -> Requests.create ~sw env
923923- in
924924- { session; base_url }
925925-926926-let base_url t = t.base_url
927927-let session t = t.session|} in
928928-929929- (* Generate prefix modules in dependency order *)
930930- let prefix_mods = List.filter_map (fun name ->
931931- match StringMap.find_opt name tree.children with
932932- | None -> None
933933- | Some node ->
934934- if node.name = "Client" then
935935- (* Generate Client operations inline *)
936936- let ops = List.map (gen_operation_impl ~current_prefix:"Client") (List.rev node.operations) in
937937- if ops = [] then None
938938- else
939939- let content = String.concat "\n\n" ops in
940940- let indented = String.split_on_char '\n' content |> List.map (fun l -> " " ^ l) |> String.concat "\n" in
941941- Some (Printf.sprintf "module Client = struct\n%s\nend" indented)
942942- else
943943- Some (gen_prefix_module_impl node)
944944- ) sorted_modules in
945945-946946- Printf.sprintf {|(** {1 %s}
947947-948948- %s
949949-950950- @version %s *)
951951-952952-%s
953953-954954-%s
955955-|}
956956- (Name.to_module_name package_name) (escape_doc api_desc) spec.info.version
957957- client_impl (String.concat "\n\n" prefix_mods)
958958-959959-let generate_mli (spec : Spec.t) (package_name : string) : string =
960960- let api_desc = Option.value ~default:"Generated API client." spec.info.description in
961961-962962- (* Collect schemas *)
963963- let schemas = match spec.components with
964964- | None -> []
965965- | Some c -> List.filter_map (fun (name, sor) ->
966966- match sor with
967967- | Spec.Ref _ -> None
968968- | Spec.Value s -> Some (analyze_schema name s)
969969- ) c.schemas
970970- in
971971-972972- (* Collect operations *)
973973- let operations = List.concat_map (fun (path, pi) ->
974974- let ops = [
975975- ("GET", pi.Spec.get); ("POST", pi.post); ("PUT", pi.put);
976976- ("DELETE", pi.delete); ("PATCH", pi.patch);
977977- ("HEAD", pi.head); ("OPTIONS", pi.options);
978978- ] in
979979- List.filter_map (fun (method_, op_opt) ->
980980- Option.map (fun op -> analyze_operation ~path ~method_ op) op_opt
981981- ) ops
982982- ) spec.paths in
983983-984984- (* Build module tree *)
985985- let (tree, sorted_modules) = build_module_tree schemas operations in
986986-987987- (* Generate top-level client type and function interfaces *)
988988- let client_intf = {|type t
989989-990990-val create :
991991- ?session:Requests.t ->
992992- sw:Eio.Switch.t ->
993993- < net : _ Eio.Net.t ; fs : Eio.Fs.dir_ty Eio.Path.t ; clock : _ Eio.Time.clock ; .. > ->
994994- base_url:string ->
995995- t
996996-997997-val base_url : t -> string
998998-val session : t -> Requests.t|} in
999999-10001000- (* Generate prefix modules in dependency order *)
10011001- let prefix_mods = List.filter_map (fun name ->
10021002- match StringMap.find_opt name tree.children with
10031003- | None -> None
10041004- | Some node ->
10051005- if node.name = "Client" then
10061006- let ops = List.map (gen_operation_intf ~current_prefix:"Client") (List.rev node.operations) in
10071007- if ops = [] then None
10081008- else
10091009- let content = String.concat "\n\n" ops in
10101010- let indented = String.split_on_char '\n' content |> List.map (fun l -> " " ^ l) |> String.concat "\n" in
10111011- Some (Printf.sprintf "module Client : sig\n%s\nend" indented)
10121012- else
10131013- Some (gen_prefix_module_intf node)
10141014- ) sorted_modules in
10151015-10161016- Printf.sprintf {|(** {1 %s}
10171017-10181018- %s
10191019-10201020- @version %s *)
10211021-10221022-%s
10231023-10241024-%s
10251025-|}
10261026- (Name.to_module_name package_name) (escape_doc api_desc) spec.info.version
10271027- client_intf (String.concat "\n\n" prefix_mods)
10281028-10291029-let generate_dune (package_name : string) : string =
10301030- Printf.sprintf {|(library
10311031- (name %s)
10321032- (libraries openapi jsont jsont.bytesrw requests ptime eio)
10331033- (wrapped true))
10341034-10351035-(include dune.inc)
10361036-|} package_name
10371037-10381038-let generate_dune_inc ~(spec_path : string option) (package_name : string) : string =
10391039- match spec_path with
10401040- | None -> "; No spec path provided - regeneration rules not generated\n"
10411041- | Some path ->
10421042- Printf.sprintf {|; Generated rules for OpenAPI code regeneration
10431043-; Run: dune build @gen --auto-promote
10441044-10451045-(rule
10461046- (alias gen)
10471047- (mode (promote (until-clean)))
10481048- (targets %s.ml %s.mli)
10491049- (deps %s)
10501050- (action
10511051- (run openapi-gen generate -o . -n %s %%{deps})))
10521052-|} package_name package_name path package_name
10531053-10541054-let generate ~(config : config) (spec : Spec.t) : (string * string) list =
10551055- let package_name = config.package_name in
10561056- [
10571057- ("dune", generate_dune package_name);
10581058- ("dune.inc", generate_dune_inc ~spec_path:config.spec_path package_name);
10591059- (package_name ^ ".ml", generate_ml spec package_name);
10601060- (package_name ^ ".mli", generate_mli spec package_name);
10611061- ]
10621062-10631063-let write_files ~(output_dir : string) (files : (string * string) list) : unit =
10641064- List.iter (fun (filename, content) ->
10651065- let path = Filename.concat output_dir filename in
10661066- let oc = open_out path in
10671067- output_string oc content;
10681068- close_out oc
10691069- ) files
-156
ocaml-openapi/lib/openapi_nestjs.ml
···11-(*---------------------------------------------------------------------------
22- Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
33- SPDX-License-Identifier: ISC
44- ---------------------------------------------------------------------------*)
55-66-(** NestJS-style API error handling.
77-88- NestJS/Express applications return errors in a standard format:
99- {[
1010- {
1111- "message": "Missing required permission: person.read",
1212- "error": "Forbidden",
1313- "statusCode": 403,
1414- "correlationId": "koskgk9d"
1515- }
1616- ]}
1717-1818- This module provides types and utilities for parsing and handling
1919- these errors in a structured way.
2020-2121- {2 Usage}
2222-2323- {[
2424- match Immich.People.get_all_people client () with
2525- | people -> ...
2626- | exception Openapi.Runtime.Api_error e ->
2727- match Openapi.Nestjs.of_api_error e with
2828- | Some nestjs_error ->
2929- Fmt.epr "Error: %s (correlation: %s)@."
3030- nestjs_error.message
3131- (Option.value ~default:"none" nestjs_error.correlation_id)
3232- | None ->
3333- (* Not a NestJS error, use raw body *)
3434- Fmt.epr "Error: %s@." e.body
3535- ]}
3636-*)
3737-3838-(** {1 Error Types} *)
3939-4040-(** A structured NestJS HTTP exception. *)
4141-type t = {
4242- status_code : int;
4343- (** HTTP status code (e.g., 403, 404, 500). *)
4444-4545- error : string option;
4646- (** Error category (e.g., "Forbidden", "Not Found", "Internal Server Error"). *)
4747-4848- message : string;
4949- (** Human-readable error message. Can be a single string or concatenated
5050- from an array of validation messages. *)
5151-5252- correlation_id : string option;
5353- (** Request correlation ID for debugging/support. *)
5454-}
5555-5656-(** {1 JSON Codec} *)
5757-5858-(** Jsont codec for NestJS errors.
5959-6060- Handles both string and array message formats:
6161- - {[ "message": "error text" ]}
6262- - {[ "message": ["validation error 1", "validation error 2"] ]} *)
6363-let jsont : t Jsont.t =
6464- (* Message can be string or array of strings *)
6565- let message_jsont =
6666- Jsont.map Jsont.json ~kind:"message"
6767- ~dec:(fun json ->
6868- match json with
6969- | Jsont.String (s, _) -> s
7070- | Jsont.Array (items, _) ->
7171- items
7272- |> List.filter_map (function
7373- | Jsont.String (s, _) -> Some s
7474- | _ -> None)
7575- |> String.concat "; "
7676- | _ -> "Unknown error")
7777- ~enc:(fun s -> Jsont.String (s, Jsont.Meta.none))
7878- in
7979- Jsont.Object.map ~kind:"NestjsError"
8080- (fun status_code error message correlation_id ->
8181- { status_code; error; message; correlation_id })
8282- |> Jsont.Object.mem "statusCode" Jsont.int ~enc:(fun e -> e.status_code)
8383- |> Jsont.Object.opt_mem "error" Jsont.string ~enc:(fun e -> e.error)
8484- |> Jsont.Object.mem "message" message_jsont ~enc:(fun e -> e.message)
8585- |> Jsont.Object.opt_mem "correlationId" Jsont.string ~enc:(fun e -> e.correlation_id)
8686- |> Jsont.Object.skip_unknown
8787- |> Jsont.Object.finish
8888-8989-(** {1 Parsing} *)
9090-9191-(** Parse a JSON string into a NestJS error.
9292- Returns [None] if the string is not valid NestJS error JSON. *)
9393-let of_string (s : string) : t option =
9494- match Jsont_bytesrw.decode_string jsont s with
9595- | Ok e -> Some e
9696- | Error _ -> None
9797-9898-(** Parse an {!Openapi.Runtime.Api_error} into a structured NestJS error.
9999- Returns [None] if the error body is not valid NestJS error JSON. *)
100100-let of_api_error (e : Openapi_runtime.api_error) : t option =
101101- of_string e.body
102102-103103-(** {1 Convenience Functions} *)
104104-105105-(** Check if this is a permission/authorization error (401 or 403). *)
106106-let is_auth_error (e : t) : bool =
107107- e.status_code = 401 || e.status_code = 403
108108-109109-(** Check if this is a "not found" error (404). *)
110110-let is_not_found (e : t) : bool =
111111- e.status_code = 404
112112-113113-(** Check if this is a validation error (400 with message array). *)
114114-let is_validation_error (e : t) : bool =
115115- e.status_code = 400
116116-117117-(** Check if this is a server error (5xx). *)
118118-let is_server_error (e : t) : bool =
119119- e.status_code >= 500 && e.status_code < 600
120120-121121-(** {1 Pretty Printing} *)
122122-123123-(** Pretty-print a NestJS error. *)
124124-let pp ppf (e : t) =
125125- match e.correlation_id with
126126- | Some cid ->
127127- Format.fprintf ppf "%s [%d] (correlationId: %s)"
128128- e.message e.status_code cid
129129- | None ->
130130- Format.fprintf ppf "%s [%d]" e.message e.status_code
131131-132132-(** Convert to a human-readable string. *)
133133-let to_string (e : t) : string =
134134- Format.asprintf "%a" pp e
135135-136136-(** {1 Exception Handling} *)
137137-138138-(** Exception for NestJS-specific errors.
139139- Use this when you want to distinguish NestJS errors from generic API errors. *)
140140-exception Error of t
141141-142142-(** Register a pretty printer for the exception. *)
143143-let () =
144144- Printexc.register_printer (function
145145- | Error e -> Some (Format.asprintf "Nestjs.Error: %a" pp e)
146146- | _ -> None)
147147-148148-(** Handle an {!Openapi.Runtime.Api_error}, converting it to a NestJS error
149149- if possible.
150150-151151- @raise Error if the error body parses as a NestJS error
152152- @raise Openapi.Runtime.Api_error if parsing fails (re-raises original) *)
153153-let raise_if_nestjs (e : Openapi_runtime.api_error) =
154154- match of_api_error e with
155155- | Some nestjs -> raise (Error nestjs)
156156- | None -> raise (Openapi_runtime.Api_error e)
-104
ocaml-openapi/lib/openapi_nestjs.mli
···11-(*---------------------------------------------------------------------------
22- Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
33- SPDX-License-Identifier: ISC
44- ---------------------------------------------------------------------------*)
55-66-(** NestJS-style API error handling.
77-88- NestJS/Express applications return errors in a standard format:
99- {[
1010- {
1111- "message": "Missing required permission: person.read",
1212- "error": "Forbidden",
1313- "statusCode": 403,
1414- "correlationId": "koskgk9d"
1515- }
1616- ]}
1717-1818- This module provides types and utilities for parsing and handling
1919- these errors in a structured way.
2020-2121- {2 Usage}
2222-2323- {[
2424- match Immich.People.get_all_people client () with
2525- | people -> ...
2626- | exception Openapi.Runtime.Api_error e ->
2727- match Openapi.Nestjs.of_api_error e with
2828- | Some nestjs_error ->
2929- Fmt.epr "Error: %s (correlation: %s)@."
3030- nestjs_error.message
3131- (Option.value ~default:"none" nestjs_error.correlation_id)
3232- | None ->
3333- (* Not a NestJS error, use raw body *)
3434- Fmt.epr "Error: %s@." e.body
3535- ]}
3636-*)
3737-3838-(** {1 Error Types} *)
3939-4040-(** A structured NestJS HTTP exception. *)
4141-type t = {
4242- status_code : int;
4343- (** HTTP status code (e.g., 403, 404, 500). *)
4444-4545- error : string option;
4646- (** Error category (e.g., "Forbidden", "Not Found", "Internal Server Error"). *)
4747-4848- message : string;
4949- (** Human-readable error message. Can be a single string or concatenated
5050- from an array of validation messages. *)
5151-5252- correlation_id : string option;
5353- (** Request correlation ID for debugging/support. *)
5454-}
5555-5656-(** {1 JSON Codec} *)
5757-5858-val jsont : t Jsont.t
5959-(** Jsont codec for NestJS errors. *)
6060-6161-(** {1 Parsing} *)
6262-6363-val of_string : string -> t option
6464-(** Parse a JSON string into a NestJS error.
6565- Returns [None] if the string is not valid NestJS error JSON. *)
6666-6767-val of_api_error : Openapi_runtime.api_error -> t option
6868-(** Parse an {!Openapi_runtime.api_error} into a structured NestJS error.
6969- Returns [None] if the error body is not valid NestJS error JSON. *)
7070-7171-(** {1 Convenience Functions} *)
7272-7373-val is_auth_error : t -> bool
7474-(** Check if this is a permission/authorization error (401 or 403). *)
7575-7676-val is_not_found : t -> bool
7777-(** Check if this is a "not found" error (404). *)
7878-7979-val is_validation_error : t -> bool
8080-(** Check if this is a validation error (400 with message array). *)
8181-8282-val is_server_error : t -> bool
8383-(** Check if this is a server error (5xx). *)
8484-8585-(** {1 Pretty Printing} *)
8686-8787-val pp : Format.formatter -> t -> unit
8888-(** Pretty-print a NestJS error. *)
8989-9090-val to_string : t -> string
9191-(** Convert to a human-readable string. *)
9292-9393-(** {1 Exception Handling} *)
9494-9595-exception Error of t
9696-(** Exception for NestJS-specific errors.
9797- Use this when you want to distinguish NestJS errors from generic API errors. *)
9898-9999-val raise_if_nestjs : Openapi_runtime.api_error -> 'a
100100-(** Handle an {!Openapi_runtime.api_error}, converting it to a NestJS error
101101- if possible.
102102-103103- @raise Error if the error body parses as a NestJS error
104104- @raise Openapi_runtime.Api_error if parsing fails (re-raises original) *)
-200
ocaml-openapi/lib/openapi_runtime.ml
···11-(** Runtime utilities for generated OpenAPI clients.
22-33- This module provides utilities used by generated client code:
44- - Path template rendering
55- - Query parameter building
66- - JSON encoding/decoding helpers
77-*)
88-99-(** {1 Path Templates} *)
1010-1111-module Path = struct
1212- (** Render a path template like "/users/{id}/posts/{postId}" with parameters *)
1313- let render ~(params : (string * string) list) (template : string) : string =
1414- List.fold_left
1515- (fun path (name, value) ->
1616- match String.split_on_char '{' path with
1717- | [only] -> only
1818- | parts ->
1919- String.concat "" (List.mapi (fun i part ->
2020- if i = 0 then part
2121- else
2222- match String.split_on_char '}' part with
2323- | [var; rest] when var = name -> value ^ rest
2424- | _ -> "{" ^ part
2525- ) parts))
2626- template params
2727-2828- (** Extract parameter names from a path template *)
2929- let parameters (template : string) : string list =
3030- let rec extract acc s =
3131- match String.index_opt s '{' with
3232- | None -> List.rev acc
3333- | Some i ->
3434- let rest = String.sub s (i + 1) (String.length s - i - 1) in
3535- match String.index_opt rest '}' with
3636- | None -> List.rev acc
3737- | Some j ->
3838- let name = String.sub rest 0 j in
3939- let remaining = String.sub rest (j + 1) (String.length rest - j - 1) in
4040- extract (name :: acc) remaining
4141- in
4242- extract [] template
4343-end
4444-4545-(** {1 Query Parameters} *)
4646-4747-module Query = struct
4848- type param = string * string
4949-5050- (** Helper for optional parameters with custom stringifier *)
5151- let optional_with ~key ~value ~to_string : param list =
5252- Option.fold ~none:[] ~some:(fun v -> [(key, to_string v)]) value
5353-5454- let singleton ~key ~value : param list = [(key, value)]
5555-5656- let optional ~key ~value : param list =
5757- optional_with ~key ~value ~to_string:Fun.id
5858-5959- let list ~key ~values : param list =
6060- List.map (fun v -> (key, v)) values
6161-6262- let int ~key ~value : param list = [(key, string_of_int value)]
6363-6464- let int_opt ~key ~value : param list =
6565- optional_with ~key ~value ~to_string:string_of_int
6666-6767- let bool ~key ~value : param list =
6868- [(key, if value then "true" else "false")]
6969-7070- let bool_opt ~key ~value : param list =
7171- optional_with ~key ~value ~to_string:(fun b -> if b then "true" else "false")
7272-7373- let float ~key ~value : param list = [(key, string_of_float value)]
7474-7575- let float_opt ~key ~value : param list =
7676- optional_with ~key ~value ~to_string:string_of_float
7777-7878- let encode (params : param list) : string =
7979- if params = [] then ""
8080- else
8181- "?" ^
8282- String.concat "&" (List.map (fun (k, v) ->
8383- (* URL encode the value *)
8484- let encode_char c =
8585- match c with
8686- | 'A'..'Z' | 'a'..'z' | '0'..'9' | '-' | '_' | '.' | '~' ->
8787- String.make 1 c
8888- | c ->
8989- Printf.sprintf "%%%02X" (Char.code c)
9090- in
9191- let encoded_v = String.to_seq v
9292- |> Seq.map encode_char
9393- |> List.of_seq
9494- |> String.concat ""
9595- in
9696- k ^ "=" ^ encoded_v
9797- ) params)
9898-end
9999-100100-(** {1 JSON Helpers} *)
101101-102102-module Json = struct
103103- let decode codec s =
104104- Jsont_bytesrw.decode_string codec s
105105-106106- let decode' codec s =
107107- Jsont_bytesrw.decode_string' codec s
108108-109109- let encode codec v =
110110- Jsont_bytesrw.encode_string codec v
111111-112112- let encode' codec v =
113113- Jsont_bytesrw.encode_string' codec v
114114-115115- let encode_compact codec v =
116116- Jsont_bytesrw.encode_string ~format:Jsont.Minify codec v
117117-118118- (** Decode a Jsont.json value through a codec.
119119- Encodes to string then decodes - not optimal but works. *)
120120- let decode_json (codec : 'a Jsont.t) (json : Jsont.json) : ('a, string) result =
121121- match Jsont_bytesrw.encode_string Jsont.json json with
122122- | Ok s -> Jsont_bytesrw.decode_string codec s
123123- | Error e -> Error e
124124-125125- (** Decode a Jsont.json value, raising on error *)
126126- let decode_json_exn (codec : 'a Jsont.t) (json : Jsont.json) : 'a =
127127- match decode_json codec json with
128128- | Ok v -> v
129129- | Error e -> failwith e
130130-131131- (** Encode a value to Jsont.json *)
132132- let encode_json (codec : 'a Jsont.t) (v : 'a) : Jsont.json =
133133- match Jsont_bytesrw.encode_string codec v with
134134- | Ok s ->
135135- (match Jsont_bytesrw.decode_string Jsont.json s with
136136- | Ok json -> json
137137- | Error _ -> Jsont.Null ((), Jsont.Meta.none))
138138- | Error _ -> Jsont.Null ((), Jsont.Meta.none)
139139-end
140140-141141-(** {1 HTTP Method} *)
142142-143143-type http_method = Get | Post | Put | Patch | Delete | Head | Options
144144-145145-let string_of_method = function
146146- | Get -> "GET"
147147- | Post -> "POST"
148148- | Put -> "PUT"
149149- | Patch -> "PATCH"
150150- | Delete -> "DELETE"
151151- | Head -> "HEAD"
152152- | Options -> "OPTIONS"
153153-154154-(** {1 Common Types} *)
155155-156156-(** ISO 8601 date-time codec *)
157157-let ptime_jsont : Ptime.t Jsont.t =
158158- Jsont.map Jsont.string ~kind:"datetime"
159159- ~dec:(fun s ->
160160- match Ptime.of_rfc3339 s with
161161- | Ok (t, _, _) -> t
162162- | Error _ -> Jsont.Error.msgf Jsont.Meta.none "Invalid datetime: %s" s)
163163- ~enc:(fun t -> Ptime.to_rfc3339 t)
164164-165165-(** UUID as string *)
166166-let uuid_jsont : string Jsont.t = Jsont.string
167167-168168-(** Base64 encoded bytes *)
169169-let base64_jsont : string Jsont.t = Jsont.string
170170-171171-(** {1 Nullable wrapper} *)
172172-173173-let nullable (codec : 'a Jsont.t) : 'a option Jsont.t =
174174- Jsont.option codec
175175-176176-(** {1 Any JSON value wrapper} *)
177177-178178-type json = Jsont.json
179179-180180-let json_jsont : json Jsont.t = Jsont.json
181181-182182-(** {1 API Errors} *)
183183-184184-(** Error raised when an API call fails with a non-2xx status code *)
185185-type api_error = {
186186- operation : string;
187187- method_ : string;
188188- url : string;
189189- status : int;
190190- body : string;
191191-}
192192-193193-exception Api_error of api_error
194194-195195-let () =
196196- Printexc.register_printer (function
197197- | Api_error e ->
198198- Some (Printf.sprintf "Api_error: %s %s returned %d: %s"
199199- e.method_ e.url e.status e.body)
200200- | _ -> None)