My aggregated monorepo of OCaml code, automaintained
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Remove ocaml-openapi for subtree re-add

-3306
-70
ocaml-openapi/CLAUDE.md
··· 1 - # OCaml OpenAPI 2 - 3 - OpenAPI code generator for OCaml, specialized for: 4 - - **requests** HTTP library (Eio-based) 5 - - **jsont** for JSON codecs 6 - - Stdlib-first approach (avoiding Base/Core) 7 - - OCamldoc comments from OpenAPI descriptions 8 - 9 - ## Quick Start 10 - 11 - ```bash 12 - # Generate client from OpenAPI spec 13 - opam exec -- dune exec -- openapi-gen generate spec.json -o ./generated -n my_api 14 - 15 - # Include dune regeneration rules 16 - opam exec -- dune exec -- openapi-gen generate spec.json -o ./generated -n my_api --regen 17 - ``` 18 - 19 - ## Generated Code Structure 20 - 21 - ``` 22 - output/ 23 - ├── dune # Build configuration (wrapped library) 24 - ├── dune.inc # Regeneration rules (if --regen used) 25 - ├── types.ml # Type definitions with jsont codecs 26 - ├── types.mli # Type interfaces 27 - ├── client.ml # API client functions using requests 28 - ├── client.mli # Client interface 29 - ├── <api_name>.ml # Wrapped main module 30 - └── <api_name>.mli # Main module interface 31 - ``` 32 - 33 - ## Usage 34 - 35 - After generation, you can use the API like: 36 - 37 - ```ocaml 38 - (* Access types through the wrapped module *) 39 - let album : Immich.Types.AlbumResponseDto.t = ... 40 - 41 - (* Create a client and make requests *) 42 - Eio_main.run @@ fun env -> 43 - Eio.Switch.run @@ fun sw -> 44 - let client = Immich.Client.create ~sw env ~base_url:"http://localhost:2283/api" in 45 - let json = Immich.Client.get_activities ~album_id:"..." client () in 46 - ... 47 - ``` 48 - 49 - ## Regeneration with Dune 50 - 51 - When using `--regen`, the generated `dune.inc` contains rules for: 52 - 53 - ```bash 54 - # Regenerate and promote changes 55 - dune build @gen --auto-promote 56 - ``` 57 - 58 - ## Architecture 59 - 60 - - `lib/openapi_spec.ml` - OpenAPI 3.x specification types with jsont codecs 61 - - `lib/openapi_codegen.ml` - Code generation from spec to OCaml 62 - - `lib/openapi_runtime.ml` - Runtime utilities for generated clients 63 - - `bin/openapi_cli.ml` - CLI tool 64 - 65 - ## Build & Test 66 - 67 - ```bash 68 - opam exec -- dune build 69 - opam exec -- dune test 70 - ```
-4
ocaml-openapi/bin/dune
··· 1 - (executable 2 - (name openapi_cli) 3 - (public_name openapi-gen) 4 - (libraries openapi cmdliner fmt logs logs.fmt fmt.tty unix))
-141
ocaml-openapi/bin/openapi_cli.ml
··· 1 - (** OpenAPI code generator CLI. *) 2 - 3 - let setup_logging style_renderer level = 4 - Fmt_tty.setup_std_outputs ?style_renderer (); 5 - Logs.set_level level; 6 - Logs.set_reporter (Logs_fmt.reporter ()) 7 - 8 - let read_file path = 9 - let ic = open_in path in 10 - let n = in_channel_length ic in 11 - let s = really_input_string ic n in 12 - close_in ic; 13 - s 14 - 15 - (** Parse spec file and run action, handling errors uniformly *) 16 - let with_spec spec_path f = 17 - let spec_content = read_file spec_path in 18 - match Openapi.Spec.of_string spec_content with 19 - | Error e -> 20 - Logs.err (fun m -> m "Failed to parse OpenAPI spec: %s" e); 21 - 1 22 - | Ok spec -> f spec 23 - 24 - let generate_cmd spec_path output_dir package_name include_regen_rule = 25 - setup_logging None (Some Logs.Info); 26 - Logs.info (fun m -> m "Reading OpenAPI spec from %s" spec_path); 27 - with_spec spec_path (fun spec -> 28 - Logs.info (fun m -> m "Parsed OpenAPI spec: %s v%s" 29 - spec.info.title spec.info.version); 30 - 31 - let package_name = Option.value package_name 32 - ~default:(Openapi.Codegen.Name.to_snake_case spec.info.title) in 33 - 34 - (* Use spec_path for dune.inc regeneration rule if requested *) 35 - let spec_path_for_dune = if include_regen_rule then Some spec_path else None in 36 - let config = Openapi.Codegen.{ output_dir; package_name; spec_path = spec_path_for_dune } in 37 - let files = Openapi.Codegen.generate ~config spec in 38 - 39 - (try Unix.mkdir output_dir 0o755 with Unix.Unix_error (Unix.EEXIST, _, _) -> ()); 40 - Openapi.Codegen.write_files ~output_dir files; 41 - 42 - Logs.info (fun m -> m "Generated %d files in %s" (List.length files) output_dir); 43 - List.iter (fun (name, _) -> Logs.info (fun m -> m " - %s" name)) files; 44 - 0) 45 - 46 - let inspect_cmd spec_path = 47 - setup_logging None (Some Logs.Info); 48 - with_spec spec_path (fun spec -> 49 - Fmt.pr "@[<v>"; 50 - Fmt.pr "OpenAPI Specification@,"; 51 - Fmt.pr "====================@,@,"; 52 - Fmt.pr "Title: %s@," spec.info.title; 53 - Fmt.pr "Version: %s@," spec.info.version; 54 - Option.iter (fun d -> Fmt.pr "Description: %s@," d) spec.info.description; 55 - Fmt.pr "@,"; 56 - 57 - Fmt.pr "Servers:@,"; 58 - List.iter (fun (s : Openapi.Spec.server) -> 59 - Fmt.pr " - %s@," s.url 60 - ) spec.servers; 61 - Fmt.pr "@,"; 62 - 63 - Fmt.pr "Paths (%d):@," (List.length spec.paths); 64 - List.iter (fun (path, _item) -> 65 - Fmt.pr " - %s@," path 66 - ) spec.paths; 67 - Fmt.pr "@,"; 68 - 69 - (match spec.components with 70 - | Some c -> 71 - Fmt.pr "Schemas (%d):@," (List.length c.schemas); 72 - List.iter (fun (name, _) -> 73 - Fmt.pr " - %s@," name 74 - ) c.schemas 75 - | None -> ()); 76 - 77 - Fmt.pr "@]"; 78 - 0) 79 - 80 - (* Cmdliner setup *) 81 - open Cmdliner 82 - 83 - let spec_path = 84 - let doc = "Path to the OpenAPI specification file (JSON)." in 85 - Arg.(required & pos 0 (some file) None & info [] ~docv:"SPEC" ~doc) 86 - 87 - let output_dir = 88 - let doc = "Output directory for generated code." in 89 - Arg.(required & opt (some string) None & info ["o"; "output"] ~docv:"DIR" ~doc) 90 - 91 - let package_name = 92 - let doc = "Package name for generated code (defaults to API title)." in 93 - Arg.(value & opt (some string) None & info ["n"; "name"] ~docv:"NAME" ~doc) 94 - 95 - let include_regen_rule = 96 - let doc = "Include dune.inc regeneration rule with spec path." in 97 - Arg.(value & flag & info ["regen"; "include-regen-rule"] ~doc) 98 - 99 - let generate_term = 100 - Term.(const generate_cmd $ spec_path $ output_dir $ package_name $ include_regen_rule) 101 - 102 - let generate_info = 103 - let doc = "Generate OCaml code from an OpenAPI specification." in 104 - let man = [ 105 - `S Manpage.s_description; 106 - `P "Generates OCaml types and client code from an OpenAPI 3.x specification."; 107 - `P "The generated code uses:"; 108 - `I ("$(b,jsont)", "for JSON encoding/decoding"); 109 - `I ("$(b,requests)", "for HTTP client (Eio-based)"); 110 - `I ("$(b,ptime)", "for date-time handling"); 111 - `S Manpage.s_examples; 112 - `P "Generate client from local spec:"; 113 - `Pre " openapi generate spec.json -o ./client -n my_api"; 114 - `P "Generate with regeneration rule for dune:"; 115 - `Pre " openapi generate spec.json -o ./client -n my_api --regen"; 116 - ] in 117 - Cmd.info "generate" ~doc ~man 118 - 119 - let inspect_term = 120 - Term.(const inspect_cmd $ spec_path) 121 - 122 - let inspect_info = 123 - let doc = "Inspect an OpenAPI specification." in 124 - Cmd.info "inspect" ~doc 125 - 126 - let main_info = 127 - let doc = "OpenAPI code generator for OCaml." in 128 - let man = [ 129 - `S Manpage.s_description; 130 - `P "Generate OCaml API clients from OpenAPI 3.x specifications."; 131 - `P "Use $(b,generate) to create client code, or $(b,inspect) to view spec details."; 132 - ] in 133 - Cmd.info "openapi" ~version:"0.1.0" ~doc ~man 134 - 135 - let main_cmd = 136 - Cmd.group main_info [ 137 - Cmd.v generate_info generate_term; 138 - Cmd.v inspect_info inspect_term; 139 - ] 140 - 141 - let () = exit (Cmd.eval' main_cmd)
-29
ocaml-openapi/dune-project
··· 1 - (lang dune 3.21) 2 - 3 - (name openapi) 4 - 5 - (generate_opam_files true) 6 - 7 - (license ISC) 8 - (authors "Anil Madhavapeddy") 9 - (homepage "https://tangled.org/@anil.recoil.org/ocaml-openapi") 10 - (maintainers "Anil Madhavapeddy <anil@recoil.org>") 11 - (maintenance_intent "(latest)") 12 - (source (tangled anil.recoil.org/ocaml-openapi)) 13 - 14 - (package 15 - (name openapi) 16 - (synopsis "OpenAPI code generator for OCaml with requests and jsont") 17 - (description 18 - "Generate type-safe OCaml API clients from OpenAPI 3.x specifications. 19 - Uses the requests HTTP library and jsont for JSON codecs.") 20 - (depends 21 - (ocaml (>= 5.1.0)) 22 - jsont 23 - bytesrw 24 - fmt 25 - logs 26 - ptime 27 - (cmdliner (>= 1.2.0)) 28 - (odoc :with-doc) 29 - (alcotest :with-test)))
-7
ocaml-openapi/lib/dune
··· 1 - (library 2 - (name openapi) 3 - (public_name openapi) 4 - (libraries jsont jsont.bytesrw fmt logs ptime)) 5 - 6 - (documentation 7 - (package openapi))
-254
ocaml-openapi/lib/index.mld
··· 1 - {0 OCaml OpenAPI} 2 - 3 - {1 Overview} 4 - 5 - [openapi] generates type-safe OCaml API clients from OpenAPI 3.x 6 - specifications. The generated code uses: 7 - 8 - - {b jsont} for JSON encoding/decoding 9 - - {b requests} for HTTP client (Eio-based) 10 - - {b ptime} for date-time handling 11 - 12 - {1 Installation} 13 - 14 - {[ 15 - opam install openapi 16 - ]} 17 - 18 - {1 Generating a Client} 19 - 20 - Use the [openapi-gen] CLI tool to generate OCaml code from an OpenAPI spec: 21 - 22 - {[ 23 - # Basic generation 24 - openapi-gen generate spec.json -o ./my_api -n my_api 25 - 26 - # With dune regeneration rules 27 - openapi-gen generate spec.json -o ./my_api -n my_api --regen 28 - ]} 29 - 30 - {2 CLI Options} 31 - 32 - {ul 33 - {- [-o], [--output] — Output directory for generated code (required)} 34 - {- [-n], [--name] — Package name for generated library (defaults to API title)} 35 - {- [--regen] — Include dune.inc rules for [dune build @gen --auto-promote]}} 36 - 37 - {1 Generated Code Structure} 38 - 39 - The generator produces a complete dune library: 40 - 41 - {[ 42 - my_api/ 43 - ├── dune # Library configuration (wrapped) 44 - ├── dune.inc # Regeneration rules (if --regen used) 45 - ├── types.ml # Type definitions with jsont codecs 46 - ├── types.mli # Type interfaces 47 - ├── client.ml # API client functions 48 - ├── client.mli # Client interface 49 - ├── my_api.ml # Main wrapped module 50 - └── my_api.mli # Main module interface 51 - ]} 52 - 53 - {1 Using Generated Code} 54 - 55 - {2 Accessing Types} 56 - 57 - All schema types are generated as modules within [Types]: 58 - 59 - {[ 60 - (* Access a type *) 61 - let user : My_api.Types.User.t = { 62 - id = 123; 63 - name = "Alice"; 64 - email = Some "alice@example.com"; 65 - } 66 - 67 - (* Encode to JSON *) 68 - let json = Jsont.encode My_api.Types.User.t_jsont user 69 - 70 - (* Decode from JSON *) 71 - let user' = Jsont.decode My_api.Types.User.t_jsont json 72 - ]} 73 - 74 - {2 Making API Requests} 75 - 76 - Create a client and call API operations: 77 - 78 - {[ 79 - let () = 80 - Eio_main.run @@ fun env -> 81 - Eio.Switch.run @@ fun sw -> 82 - 83 - (* Create the client *) 84 - let client = My_api.Client.create ~sw env 85 - ~base_url:"https://api.example.com" in 86 - 87 - (* Make a request - returns typed value *) 88 - let user = My_api.Client.get_user ~id:"123" client () in 89 - Printf.printf "User: %s\n" user.name 90 - 91 - (* List endpoints return typed lists *) 92 - let users = My_api.Client.list_users client () in 93 - List.iter (fun u -> Printf.printf "- %s\n" u.name) users 94 - ]} 95 - 96 - {2 Request Bodies} 97 - 98 - For POST/PUT/PATCH requests, pass the typed value directly: 99 - 100 - {[ 101 - (* Create typed request body *) 102 - let new_user : My_api.Types.CreateUserDto.t = { 103 - name = "Bob"; 104 - email = "bob@example.com"; 105 - } in 106 - 107 - (* Pass as the body parameter - encoding is automatic *) 108 - let created = My_api.Client.create_user ~body:new_user client () 109 - ]} 110 - 111 - {1 Keeping Generated Code Updated} 112 - 113 - If you used [--regen], the generated [dune.inc] includes rules to regenerate 114 - the client when the spec changes: 115 - 116 - {[ 117 - # Regenerate and promote changes 118 - dune build @gen --auto-promote 119 - ]} 120 - 121 - This is useful for CI pipelines to ensure generated code stays in sync with 122 - the OpenAPI specification. 123 - 124 - {1 Library Modules} 125 - 126 - {2 Core Modules} 127 - 128 - {ul 129 - {- {!module:Openapi.Spec} — OpenAPI 3.x specification types with jsont codecs} 130 - {- {!module:Openapi.Codegen} — Code generation from spec to OCaml} 131 - {- {!module:Openapi.Runtime} — Runtime utilities for generated clients}} 132 - 133 - {2 Runtime Utilities} 134 - 135 - The {!module:Openapi.Runtime} module provides helpers used by generated code: 136 - 137 - {[ 138 - (* Path template rendering *) 139 - Openapi.Runtime.Path.render 140 - ~params:[("userId", "123"); ("postId", "456")] 141 - "/users/{userId}/posts/{postId}" 142 - (* => "/users/123/posts/456" *) 143 - 144 - (* Query string encoding *) 145 - Openapi.Runtime.Query.encode [("page", "1"); ("limit", "10")] 146 - (* => "?page=1&limit=10" *) 147 - ]} 148 - 149 - {1 Example: Immich API} 150 - 151 - Here's a complete example generating a client for the Immich photo server: 152 - 153 - {[ 154 - # Generate the client 155 - openapi-gen generate immich-openapi-specs.json -o ./immich -n immich 156 - 157 - # In your code: 158 - let () = 159 - Eio_main.run @@ fun env -> 160 - Eio.Switch.run @@ fun sw -> 161 - let client = Immich.Client.create ~sw env 162 - ~base_url:"http://localhost:2283/api" in 163 - 164 - (* List albums *) 165 - let albums_json = Immich.Client.get_all_albums client () in 166 - 167 - (* Get server info *) 168 - let info = Immich.Client.get_server_info client () in 169 - ... 170 - ]} 171 - 172 - {1 Limitations} 173 - 174 - {2 Schema Generation} 175 - 176 - {ul 177 - {- {b oneOf/anyOf} — Union types are mapped to [Jsont.json]. Proper 178 - implementation would generate OCaml variant types with discriminator-based 179 - decoding. See {{:#union-types}Union Types} below for details.} 180 - {- {b allOf} — Composition schemas are mapped to [Jsont.json]. Proper 181 - implementation would merge all referenced schemas into a single record type.} 182 - {- {b additionalProperties} — Dynamic object properties are parsed but not 183 - used in code generation. Objects with [additionalProperties: true] become 184 - [Jsont.json].} 185 - {- {b Recursive schemas} — Schemas that reference themselves are not fully 186 - supported and may cause infinite loops during generation.} 187 - {- {b Nested $ref} — References to references are not resolved; only direct 188 - schema references work.}} 189 - 190 - {2 Client Generation} 191 - 192 - {ul 193 - {- {b Error responses} — Error schemas (4xx, 5xx) are not generated. Errors 194 - are raised as exceptions with the HTTP status code and body text.} 195 - {- {b Authentication} — Security schemes (apiKey, http, oauth2) are parsed 196 - but not applied to requests. Add headers manually via the requests session.} 197 - {- {b Header parameters} — Header parameters are parsed but not included in 198 - generated function signatures.} 199 - {- {b Cookie parameters} — Cookie parameters are parsed but not included in 200 - generated functions.} 201 - {- {b Parameter references} — [$ref] in parameters are skipped; only inline 202 - parameters are used.}} 203 - 204 - {2 Content Types} 205 - 206 - {ul 207 - {- {b File uploads} — [multipart/form-data] is not supported. Binary file 208 - uploads require special handling not yet implemented.} 209 - {- {b XML} — Only [application/json] content types are supported.} 210 - {- {b Form encoding} — [application/x-www-form-urlencoded] is not supported.}} 211 - 212 - {2 Advanced Features} 213 - 214 - {ul 215 - {- {b Callbacks} — Webhook callbacks are parsed but no server code is 216 - generated.} 217 - {- {b Links} — Response links are parsed but not used in code generation.} 218 - {- {b External references} — Only internal [$ref] pointers starting with 219 - [#/] are supported. External file references are not resolved.}} 220 - 221 - {1:union-types Implementing Union Types} 222 - 223 - To properly support [oneOf]/[anyOf], the generator would need to: 224 - 225 - {ol 226 - {- Analyze schemas in the union to determine variant names} 227 - {- Use the [discriminator] property if present to determine the tag field} 228 - {- Generate an OCaml variant type with one constructor per schema} 229 - {- Generate a decoder that: 230 - {ul 231 - {- Reads the discriminator field if present} 232 - {- Pattern matches to select the appropriate decoder} 233 - {- Falls back to trying each decoder in order for anyOf}}} 234 - {- Generate an encoder that pattern matches on the variant}} 235 - 236 - Example of what generated code might look like: 237 - 238 - {[ 239 - (* For oneOf with discriminator *) 240 - type pet = 241 - | Dog of Dog.t 242 - | Cat of Cat.t 243 - 244 - let pet_jsont : pet Jsont.t = 245 - (* Read discriminator field "petType" to determine variant *) 246 - ... 247 - ]} 248 - 249 - {1 See Also} 250 - 251 - {ul 252 - {- {{:https://spec.openapis.org/oas/v3.0.3} OpenAPI 3.0 Specification}} 253 - {- {{:https://erratique.ch/software/jsont} jsont documentation}} 254 - {- {{:https://github.com/tarides/requests} requests library}}}
-13
ocaml-openapi/lib/openapi.ml
··· 1 - (** OCaml OpenAPI - Code generator for OpenAPI specifications. 2 - 3 - This library provides: 4 - - {!module:Spec} - OpenAPI 3.x specification types with jsont codecs 5 - - {!module:Codegen} - Code generation from spec to OCaml 6 - - {!module:Runtime} - Runtime utilities for generated clients 7 - - {!module:Nestjs} - NestJS/Express error handling (optional) 8 - *) 9 - 10 - module Spec = Openapi_spec 11 - module Codegen = Openapi_codegen 12 - module Runtime = Openapi_runtime 13 - module Nestjs = Openapi_nestjs
-1069
ocaml-openapi/lib/openapi_codegen.ml
··· 1 - (** Code generation from OpenAPI specifications. 2 - 3 - This module generates OCaml code from parsed OpenAPI specs: 4 - - Nested module structure grouped by common schema prefixes 5 - - Abstract types with accessor and constructor functions 6 - - Client functions placed in relevant type modules 7 - - Proper Eio error handling with context 8 - *) 9 - 10 - module Spec = Openapi_spec 11 - 12 - (** {1 Name Conversion} *) 13 - 14 - module Name = struct 15 - module StringSet = Set.Make(String) 16 - 17 - let ocaml_keywords = StringSet.of_list [ 18 - "and"; "as"; "assert"; "asr"; "begin"; "class"; "constraint"; "do"; "done"; 19 - "downto"; "else"; "end"; "exception"; "external"; "false"; "for"; "fun"; 20 - "function"; "functor"; "if"; "in"; "include"; "inherit"; "initializer"; 21 - "land"; "lazy"; "let"; "lor"; "lsl"; "lsr"; "lxor"; "match"; "method"; 22 - "mod"; "module"; "mutable"; "new"; "nonrec"; "object"; "of"; "open"; "or"; 23 - "private"; "rec"; "sig"; "struct"; "then"; "to"; "true"; "try"; "type"; 24 - "val"; "virtual"; "when"; "while"; "with" 25 - ] 26 - 27 - let escape_keyword s = 28 - if StringSet.mem s ocaml_keywords then s ^ "_" else s 29 - 30 - let to_snake_case s = 31 - let buf = Buffer.create (String.length s) in 32 - let prev_upper = ref false in 33 - String.iteri (fun i c -> 34 - match c with 35 - | 'A'..'Z' -> 36 - if i > 0 && not !prev_upper then Buffer.add_char buf '_'; 37 - Buffer.add_char buf (Char.lowercase_ascii c); 38 - prev_upper := true 39 - | 'a'..'z' | '0'..'9' | '_' -> 40 - Buffer.add_char buf c; 41 - prev_upper := false 42 - | '-' | ' ' | '.' | '/' -> 43 - Buffer.add_char buf '_'; 44 - prev_upper := false 45 - | _ -> 46 - prev_upper := false 47 - ) s; 48 - escape_keyword (Buffer.contents buf) 49 - 50 - let to_module_name s = 51 - let snake = to_snake_case s in 52 - let parts = String.split_on_char '_' snake in 53 - String.concat "" (List.map String.capitalize_ascii parts) 54 - 55 - let to_type_name s = String.lowercase_ascii (to_snake_case s) 56 - 57 - let to_variant_name s = String.capitalize_ascii (to_snake_case s) 58 - 59 - (** Split a schema name into prefix and suffix for nested modules. 60 - E.g., "AlbumResponseDto" -> ("Album", "ResponseDto") *) 61 - let split_schema_name (name : string) : string * string = 62 - (* Common suffixes to look for *) 63 - let suffixes = [ 64 - "ResponseDto"; "RequestDto"; "CreateDto"; "UpdateDto"; "Dto"; 65 - "Response"; "Request"; "Create"; "Update"; "Config"; "Info"; 66 - "Status"; "Type"; "Entity"; "Item"; "Entry"; "Data"; "Result" 67 - ] in 68 - let found = List.find_opt (fun suffix -> 69 - String.length name > String.length suffix && 70 - String.ends_with ~suffix name 71 - ) suffixes in 72 - match found with 73 - | Some suffix -> 74 - let prefix_len = String.length name - String.length suffix in 75 - let prefix = String.sub name 0 prefix_len in 76 - if prefix = "" then (name, "T") 77 - else (prefix, suffix) 78 - | None -> 79 - (* No known suffix, use as-is with submodule T *) 80 - (name, "T") 81 - 82 - let operation_name ~(method_ : string) ~(path : string) ~(operation_id : string option) = 83 - match operation_id with 84 - | Some id -> to_snake_case id 85 - | None -> 86 - let method_name = String.lowercase_ascii method_ in 87 - let path_parts = String.split_on_char '/' path 88 - |> List.filter (fun s -> s <> "" && not (String.length s > 0 && s.[0] = '{')) 89 - in 90 - let path_name = String.concat "_" (List.map to_snake_case path_parts) in 91 - method_name ^ "_" ^ path_name 92 - end 93 - 94 - (** {1 OCamldoc Helpers} *) 95 - 96 - let escape_doc s = 97 - let s = String.concat "\\}" (String.split_on_char '}' s) in 98 - String.concat "\\{" (String.split_on_char '{' s) 99 - 100 - let format_doc ?(indent=0) description = 101 - let prefix = String.make indent ' ' in 102 - match description with 103 - | None | Some "" -> "" 104 - | Some desc -> Printf.sprintf "%s(** %s *)\n" prefix (escape_doc desc) 105 - 106 - let format_doc_block ?(indent=0) ~summary ?description () = 107 - let prefix = String.make indent ' ' in 108 - match summary, description with 109 - | None, None -> "" 110 - | Some s, None -> Printf.sprintf "%s(** %s *)\n" prefix (escape_doc s) 111 - | None, Some d -> Printf.sprintf "%s(** %s *)\n" prefix (escape_doc d) 112 - | Some s, Some d -> 113 - Printf.sprintf "%s(** %s\n\n%s %s *)\n" prefix (escape_doc s) prefix (escape_doc d) 114 - 115 - let format_param_doc name description = 116 - match description with 117 - | None | Some "" -> "" 118 - | Some d -> Printf.sprintf " @param %s %s\n" name (escape_doc d) 119 - 120 - (** {1 JSON Helpers} *) 121 - 122 - let json_string = function 123 - | Jsont.String (s, _) -> Some s 124 - | _ -> None 125 - 126 - let json_object = function 127 - | Jsont.Object (mems, _) -> Some mems 128 - | _ -> None 129 - 130 - let get_ref json = 131 - Option.bind (json_object json) (fun mems -> 132 - List.find_map (fun ((n, _), v) -> 133 - if n = "$ref" then json_string v else None 134 - ) mems) 135 - 136 - let get_member name json = 137 - Option.bind (json_object json) (fun mems -> 138 - List.find_map (fun ((n, _), v) -> 139 - if n = name then Some v else None 140 - ) mems) 141 - 142 - let get_string_member name json = 143 - Option.bind (get_member name json) json_string 144 - 145 - (** {1 Schema Analysis} *) 146 - 147 - let schema_name_from_ref (ref_ : string) : string option = 148 - match String.split_on_char '/' ref_ with 149 - | ["#"; "components"; "schemas"; name] -> Some name 150 - | _ -> None 151 - 152 - let rec find_refs_in_json (json : Jsont.json) : string list = 153 - match json with 154 - | Jsont.Object (mems, _) -> 155 - (match List.find_map (fun ((n, _), v) -> 156 - if n = "$ref" then json_string v else None) mems with 157 - | Some ref_ -> Option.to_list (schema_name_from_ref ref_) 158 - | None -> List.concat_map (fun (_, v) -> find_refs_in_json v) mems) 159 - | Jsont.Array (items, _) -> List.concat_map find_refs_in_json items 160 - | _ -> [] 161 - 162 - let find_schema_dependencies (schema : Spec.schema) : string list = 163 - let from_properties = List.concat_map (fun (_, json) -> find_refs_in_json json) schema.properties in 164 - let refs_from_list = Option.fold ~none:[] ~some:(List.concat_map find_refs_in_json) in 165 - let from_items = Option.fold ~none:[] ~some:find_refs_in_json schema.items in 166 - List.sort_uniq String.compare 167 - (from_properties @ from_items @ refs_from_list schema.all_of 168 - @ refs_from_list schema.one_of @ refs_from_list schema.any_of) 169 - 170 - (** {1 Module Tree Structure} *) 171 - 172 - module StringMap = Map.Make(String) 173 - module StringSet = Set.Make(String) 174 - 175 - (** {1 Topological Sort} *) 176 - 177 - (** Kahn's algorithm for topological sorting. 178 - Returns nodes in dependency order (dependencies first). *) 179 - let topological_sort (nodes : string list) (deps : string -> string list) : string list = 180 - (* Build adjacency list and in-degree map *) 181 - let nodes_set = StringSet.of_list nodes in 182 - let in_degree = List.fold_left (fun m node -> 183 - StringMap.add node 0 m 184 - ) StringMap.empty nodes in 185 - let adj = List.fold_left (fun m node -> 186 - StringMap.add node [] m 187 - ) StringMap.empty nodes in 188 - (* Add edges: if A depends on B, add edge B -> A *) 189 - let (in_degree, adj) = List.fold_left (fun (in_degree, adj) node -> 190 - let node_deps = deps node |> List.filter (fun d -> StringSet.mem d nodes_set) in 191 - let in_degree = StringMap.add node (List.length node_deps) in_degree in 192 - let adj = List.fold_left (fun adj dep -> 193 - let existing = Option.value ~default:[] (StringMap.find_opt dep adj) in 194 - StringMap.add dep (node :: existing) adj 195 - ) adj node_deps in 196 - (in_degree, adj) 197 - ) (in_degree, adj) nodes in 198 - (* Start with nodes that have no dependencies *) 199 - let queue = List.filter (fun n -> 200 - StringMap.find n in_degree = 0 201 - ) nodes in 202 - let rec process queue in_degree result = 203 - match queue with 204 - | [] -> List.rev result 205 - | node :: rest -> 206 - let result = node :: result in 207 - let dependents = Option.value ~default:[] (StringMap.find_opt node adj) in 208 - let (queue', in_degree) = List.fold_left (fun (q, deg) dep -> 209 - let new_deg = StringMap.find dep deg - 1 in 210 - let deg = StringMap.add dep new_deg deg in 211 - if new_deg = 0 then (dep :: q, deg) else (q, deg) 212 - ) (rest, in_degree) dependents in 213 - process queue' in_degree result 214 - in 215 - process queue in_degree [] 216 - 217 - type field_info = { 218 - ocaml_name : string; 219 - json_name : string; 220 - ocaml_type : string; 221 - base_type : string; 222 - is_optional : bool; 223 - is_required : bool; 224 - description : string option; 225 - } 226 - 227 - type schema_info = { 228 - original_name : string; 229 - prefix : string; 230 - suffix : string; 231 - schema : Spec.schema; 232 - fields : field_info list; 233 - is_enum : bool; 234 - enum_variants : (string * string) list; (* ocaml_name, json_value *) 235 - description : string option; 236 - } 237 - 238 - type operation_info = { 239 - func_name : string; 240 - operation_id : string option; 241 - summary : string option; 242 - description : string option; 243 - tags : string list; 244 - path : string; 245 - method_ : string; 246 - path_params : (string * string * string option * bool) list; (* ocaml, json, desc, required *) 247 - query_params : (string * string * string option * bool) list; 248 - body_schema_ref : string option; 249 - response_schema_ref : string option; 250 - } 251 - 252 - type module_node = { 253 - name : string; 254 - schemas : schema_info list; 255 - operations : operation_info list; 256 - dependencies : StringSet.t; (* Other prefix modules this depends on *) 257 - children : module_node StringMap.t; 258 - } 259 - 260 - let empty_node name = { name; schemas = []; operations = []; dependencies = StringSet.empty; children = StringMap.empty } 261 - 262 - (** {1 Type Resolution} *) 263 - 264 - let rec type_of_json_schema (json : Jsont.json) : string * bool = 265 - (* Check if the schema is nullable *) 266 - let is_nullable = match get_member "nullable" json with 267 - | Some (Jsont.Bool (b, _)) -> b 268 - | _ -> false 269 - in 270 - match get_ref json with 271 - | Some ref_ -> 272 - (match schema_name_from_ref ref_ with 273 - | Some name -> 274 - let prefix, suffix = Name.split_schema_name name in 275 - (Printf.sprintf "%s.%s.t" (Name.to_module_name prefix) (Name.to_module_name suffix), is_nullable) 276 - | None -> ("Jsont.json", is_nullable)) 277 - | None -> 278 - match get_string_member "type" json with 279 - | Some "string" -> 280 - (match get_string_member "format" json with 281 - | Some "date-time" -> ("Ptime.t", is_nullable) 282 - | _ -> ("string", is_nullable)) 283 - | Some "integer" -> 284 - (match get_string_member "format" json with 285 - | Some "int64" -> ("int64", is_nullable) 286 - | Some "int32" -> ("int32", is_nullable) 287 - | _ -> ("int", is_nullable)) 288 - | Some "number" -> ("float", is_nullable) 289 - | Some "boolean" -> ("bool", is_nullable) 290 - | Some "array" -> 291 - (match get_member "items" json with 292 - | Some items -> 293 - let (elem_type, _) = type_of_json_schema items in 294 - (elem_type ^ " list", is_nullable) 295 - | None -> ("Jsont.json list", is_nullable)) 296 - | Some "object" -> ("Jsont.json", is_nullable) 297 - | _ -> ("Jsont.json", is_nullable) 298 - 299 - let rec jsont_of_base_type = function 300 - | "string" -> "Jsont.string" 301 - | "int" -> "Jsont.int" 302 - | "int32" -> "Jsont.int32" 303 - | "int64" -> "Jsont.int64" 304 - | "float" -> "Jsont.number" 305 - | "bool" -> "Jsont.bool" 306 - | "Ptime.t" -> "Openapi.Runtime.ptime_jsont" 307 - | "Jsont.json" -> "Jsont.json" 308 - | s when String.ends_with ~suffix:" list" s -> 309 - let elem = String.sub s 0 (String.length s - 5) in 310 - Printf.sprintf "(Jsont.list %s)" (jsont_of_base_type elem) 311 - | s when String.ends_with ~suffix:".t" s -> 312 - let module_path = String.sub s 0 (String.length s - 2) in 313 - module_path ^ ".jsont" 314 - | _ -> "Jsont.json" 315 - 316 - (** {1 Schema Processing} *) 317 - 318 - let analyze_schema (name : string) (schema : Spec.schema) : schema_info = 319 - let prefix, suffix = Name.split_schema_name name in 320 - let is_enum = Option.is_some schema.enum in 321 - let enum_variants = match schema.enum with 322 - | Some values -> 323 - List.filter_map (fun json -> 324 - match json with 325 - | Jsont.String (s, _) -> Some (Name.to_variant_name s, s) 326 - | _ -> None 327 - ) values 328 - | None -> [] 329 - in 330 - let fields = List.map (fun (field_name, field_json) -> 331 - let ocaml_name = Name.to_snake_case field_name in 332 - let is_required = List.mem field_name schema.required in 333 - let (base_type, json_nullable) = type_of_json_schema field_json in 334 - let is_optional = json_nullable || not is_required in 335 - let ocaml_type = if is_optional then base_type ^ " option" else base_type in 336 - let description = get_string_member "description" field_json in 337 - { ocaml_name; json_name = field_name; ocaml_type; base_type; is_optional; is_required; description } 338 - ) schema.properties in 339 - { original_name = name; prefix; suffix; schema; fields; is_enum; enum_variants; 340 - description = schema.description } 341 - 342 - (** {1 Operation Processing} *) 343 - 344 - let analyze_operation ~path ~method_ (op : Spec.operation) : operation_info = 345 - let func_name = Name.operation_name ~method_ ~path ~operation_id:op.operation_id in 346 - let params = List.filter_map (fun (p : Spec.parameter Spec.or_ref) -> 347 - match p with Spec.Value p -> Some p | Spec.Ref _ -> None 348 - ) op.parameters in 349 - 350 - let path_params = List.filter_map (fun (p : Spec.parameter) -> 351 - if p.in_ = Spec.Path then 352 - Some (Name.to_snake_case p.name, p.name, p.description, p.required) 353 - else None 354 - ) params in 355 - 356 - let query_params = List.filter_map (fun (p : Spec.parameter) -> 357 - if p.in_ = Spec.Query then 358 - Some (Name.to_snake_case p.name, p.name, p.description, p.required) 359 - else None 360 - ) params in 361 - 362 - let body_schema_ref = match op.request_body with 363 - | Some (Spec.Value (rb : Spec.request_body)) -> 364 - List.find_map (fun (ct, (media : Spec.media_type)) -> 365 - if String.length ct >= 16 && String.sub ct 0 16 = "application/json" then 366 - match media.schema with 367 - | Some (Spec.Ref r) -> schema_name_from_ref r 368 - | _ -> None 369 - else None 370 - ) rb.content 371 - | _ -> None 372 - in 373 - 374 - let response_schema_ref = 375 - let find_in_content content = 376 - List.find_map (fun (ct, (media : Spec.media_type)) -> 377 - if String.length ct >= 16 && String.sub ct 0 16 = "application/json" then 378 - match media.schema with 379 - | Some (Spec.Ref r) -> schema_name_from_ref r 380 - | Some (Spec.Value s) when s.type_ = Some "array" -> 381 - Option.bind s.items (fun items -> Option.bind (get_ref items) schema_name_from_ref) 382 - | _ -> None 383 - else None 384 - ) content 385 - in 386 - let try_status status = 387 - List.find_map (fun (code, resp) -> 388 - if code = status then 389 - match resp with 390 - | Spec.Value (r : Spec.response) -> find_in_content r.content 391 - | _ -> None 392 - else None 393 - ) op.responses.responses 394 - in 395 - match try_status "200" with 396 - | Some r -> Some r 397 - | None -> match try_status "201" with 398 - | Some r -> Some r 399 - | None -> match op.responses.default with 400 - | Some (Spec.Value (r : Spec.response)) -> find_in_content r.content 401 - | _ -> None 402 - in 403 - 404 - { func_name; operation_id = op.operation_id; summary = op.summary; 405 - description = op.description; tags = op.tags; path; method_; 406 - path_params; query_params; body_schema_ref; response_schema_ref } 407 - 408 - (** {1 Module Tree Building} *) 409 - 410 - (** Extract prefix module dependencies from a schema's fields *) 411 - let schema_prefix_deps (schema : schema_info) : StringSet.t = 412 - let deps = List.filter_map (fun (f : field_info) -> 413 - (* Check if the type references another module *) 414 - if String.contains f.base_type '.' then 415 - (* Extract first component before the dot *) 416 - match String.split_on_char '.' f.base_type with 417 - | prefix :: _ when prefix <> "Jsont" && prefix <> "Ptime" && prefix <> "Openapi" -> 418 - Some prefix 419 - | _ -> None 420 - else None 421 - ) schema.fields in 422 - StringSet.of_list deps 423 - 424 - (** Extract prefix module dependencies from an operation's types *) 425 - let operation_prefix_deps (op : operation_info) : StringSet.t = 426 - let body_dep = match op.body_schema_ref with 427 - | Some name -> 428 - let prefix, _ = Name.split_schema_name name in 429 - Some (Name.to_module_name prefix) 430 - | None -> None 431 - in 432 - let response_dep = match op.response_schema_ref with 433 - | Some name -> 434 - let prefix, _ = Name.split_schema_name name in 435 - Some (Name.to_module_name prefix) 436 - | None -> None 437 - in 438 - StringSet.of_list (List.filter_map Fun.id [body_dep; response_dep]) 439 - 440 - let build_module_tree (schemas : schema_info list) (operations : operation_info list) : module_node * string list = 441 - let root = empty_node "Root" in 442 - 443 - (* Add schemas to tree and track dependencies *) 444 - let root = List.fold_left (fun root schema -> 445 - let prefix_mod = Name.to_module_name schema.prefix in 446 - let child = match StringMap.find_opt prefix_mod root.children with 447 - | Some c -> c 448 - | None -> empty_node prefix_mod 449 - in 450 - let schema_deps = schema_prefix_deps schema in 451 - (* Remove self-dependency *) 452 - let schema_deps = StringSet.remove prefix_mod schema_deps in 453 - let child = { child with 454 - schemas = schema :: child.schemas; 455 - dependencies = StringSet.union child.dependencies schema_deps 456 - } in 457 - { root with children = StringMap.add prefix_mod child root.children } 458 - ) root schemas in 459 - 460 - (* Add operations to tree based on response type, and track operation dependencies *) 461 - let root = List.fold_left (fun root op -> 462 - match op.response_schema_ref with 463 - | Some ref_name -> 464 - let prefix, _ = Name.split_schema_name ref_name in 465 - let prefix_mod = Name.to_module_name prefix in 466 - let child = match StringMap.find_opt prefix_mod root.children with 467 - | Some c -> c 468 - | None -> empty_node prefix_mod 469 - in 470 - let op_deps = operation_prefix_deps op in 471 - (* Remove self-dependency *) 472 - let op_deps = StringSet.remove prefix_mod op_deps in 473 - let child = { child with 474 - operations = op :: child.operations; 475 - dependencies = StringSet.union child.dependencies op_deps 476 - } in 477 - { root with children = StringMap.add prefix_mod child root.children } 478 - | None -> 479 - (* Put in Client module for operations without typed response *) 480 - let child = match StringMap.find_opt "Client" root.children with 481 - | Some c -> c 482 - | None -> empty_node "Client" 483 - in 484 - let op_deps = operation_prefix_deps op in 485 - let op_deps = StringSet.remove "Client" op_deps in 486 - let child = { child with 487 - operations = op :: child.operations; 488 - dependencies = StringSet.union child.dependencies op_deps 489 - } in 490 - { root with children = StringMap.add "Client" child root.children } 491 - ) root operations in 492 - 493 - (* Get sorted list of module names (dependencies first) *) 494 - let module_names = StringMap.fold (fun name _ acc -> name :: acc) root.children [] in 495 - let deps_of name = 496 - match StringMap.find_opt name root.children with 497 - | Some node -> StringSet.elements node.dependencies 498 - | None -> [] 499 - in 500 - let sorted = topological_sort module_names deps_of in 501 - 502 - (root, sorted) 503 - 504 - (** {1 Code Generation} *) 505 - 506 - let gen_enum_impl (schema : schema_info) : string = 507 - let doc = format_doc schema.description in 508 - if schema.enum_variants = [] then 509 - Printf.sprintf "%stype t = string\n\nlet jsont = Jsont.string" doc 510 - else 511 - let type_def = Printf.sprintf "%stype t = [\n%s\n]" doc 512 - (String.concat "\n" (List.map (fun (v, _) -> " | `" ^ v) schema.enum_variants)) 513 - in 514 - let dec_cases = String.concat "\n" (List.map (fun (v, raw) -> 515 - Printf.sprintf " | %S -> `%s" raw v 516 - ) schema.enum_variants) in 517 - let enc_cases = String.concat "\n" (List.map (fun (v, raw) -> 518 - Printf.sprintf " | `%s -> %S" v raw 519 - ) schema.enum_variants) in 520 - Printf.sprintf {|%s 521 - 522 - let jsont : t Jsont.t = 523 - Jsont.map Jsont.string ~kind:%S 524 - ~dec:(function 525 - %s 526 - | s -> Jsont.Error.msgf Jsont.Meta.none "Unknown value: %%s" s) 527 - ~enc:(function 528 - %s)|} type_def schema.original_name dec_cases enc_cases 529 - 530 - let gen_enum_intf (schema : schema_info) : string = 531 - let doc = format_doc schema.description in 532 - if schema.enum_variants = [] then 533 - Printf.sprintf "%stype t = string\n\nval jsont : t Jsont.t" doc 534 - else 535 - let type_def = Printf.sprintf "%stype t = [\n%s\n]" doc 536 - (String.concat "\n" (List.map (fun (v, _) -> " | `" ^ v) schema.enum_variants)) 537 - in 538 - Printf.sprintf "%s\n\nval jsont : t Jsont.t" type_def 539 - 540 - (** Localize an OCaml type string by stripping the current_prefix module *) 541 - let localize_type ~current_prefix (type_str : string) : string = 542 - (* Handle patterns like "User.ResponseDto.t" -> "ResponseDto.t" if current_prefix = "User" *) 543 - let prefix_dot = current_prefix ^ "." in 544 - let strip_prefix s = 545 - if String.length s >= String.length prefix_dot && 546 - String.sub s 0 (String.length prefix_dot) = prefix_dot then 547 - String.sub s (String.length prefix_dot) (String.length s - String.length prefix_dot) 548 - else s 549 - in 550 - (* Handle "X list", "X option", and nested combinations *) 551 - let rec localize s = 552 - if String.ends_with ~suffix:" list" s then 553 - let elem = String.sub s 0 (String.length s - 5) in 554 - (localize elem) ^ " list" 555 - else if String.ends_with ~suffix:" option" s then 556 - let elem = String.sub s 0 (String.length s - 7) in 557 - (localize elem) ^ " option" 558 - else 559 - strip_prefix s 560 - in 561 - localize type_str 562 - 563 - (** Localize a jsont codec string by stripping the current_prefix module *) 564 - let rec localize_jsont ~current_prefix (jsont_str : string) : string = 565 - let prefix_dot = current_prefix ^ "." in 566 - (* Handle patterns like "User.ResponseDto.jsont" -> "ResponseDto.jsont" *) 567 - (* Also handle "(Jsont.list User.ResponseDto.jsont)" *) 568 - if String.length jsont_str >= String.length prefix_dot then 569 - if String.sub jsont_str 0 (String.length prefix_dot) = prefix_dot then 570 - String.sub jsont_str (String.length prefix_dot) (String.length jsont_str - String.length prefix_dot) 571 - else if String.length jsont_str > 12 && String.sub jsont_str 0 12 = "(Jsont.list " then 572 - let inner = String.sub jsont_str 12 (String.length jsont_str - 13) in 573 - "(Jsont.list " ^ localize_jsont ~current_prefix inner ^ ")" 574 - else 575 - jsont_str 576 - else 577 - jsont_str 578 - 579 - let gen_record_impl ~current_prefix (schema : schema_info) : string = 580 - let loc_type = localize_type ~current_prefix in 581 - let loc_jsont = localize_jsont ~current_prefix in 582 - let doc = format_doc schema.description in 583 - if schema.fields = [] then 584 - Printf.sprintf "%stype t = Jsont.json\n\nlet jsont = Jsont.json\n\nlet v () = Jsont.Null ((), Jsont.Meta.none)" doc 585 - else 586 - (* Private type definition *) 587 - let type_fields = String.concat "\n" (List.map (fun (f : field_info) -> 588 - let field_doc = match f.description with 589 - | Some d -> Printf.sprintf " (** %s *)" (escape_doc d) 590 - | None -> "" 591 - in 592 - Printf.sprintf " %s : %s;%s" f.ocaml_name (loc_type f.ocaml_type) field_doc 593 - ) schema.fields) in 594 - 595 - let type_def = Printf.sprintf "%stype t = {\n%s\n}" doc type_fields in 596 - 597 - (* Constructor function v *) 598 - let required_fields = List.filter (fun (f : field_info) -> not f.is_optional) schema.fields in 599 - let optional_fields = List.filter (fun (f : field_info) -> f.is_optional) schema.fields in 600 - let v_params = 601 - (List.map (fun (f : field_info) -> Printf.sprintf "~%s" f.ocaml_name) required_fields) @ 602 - (List.map (fun (f : field_info) -> Printf.sprintf "?%s" f.ocaml_name) optional_fields) @ 603 - ["()"] 604 - in 605 - let v_body = String.concat "; " (List.map (fun (f : field_info) -> f.ocaml_name) schema.fields) in 606 - let v_func = Printf.sprintf "let v %s = { %s }" (String.concat " " v_params) v_body in 607 - 608 - (* Accessor functions *) 609 - let accessors = String.concat "\n" (List.map (fun (f : field_info) -> 610 - Printf.sprintf "let %s t = t.%s" f.ocaml_name f.ocaml_name 611 - ) schema.fields) in 612 - 613 - (* Jsont codec *) 614 - let make_params = String.concat " " (List.map (fun (f : field_info) -> f.ocaml_name) schema.fields) in 615 - let jsont_members = String.concat "\n" (List.map (fun (f : field_info) -> 616 - let codec = loc_jsont (jsont_of_base_type f.base_type) in 617 - if f.is_optional then 618 - if f.is_required then 619 - Printf.sprintf " |> Jsont.Object.mem %S (Jsont.option %s)\n ~dec_absent:None ~enc_omit:Option.is_none ~enc:(fun r -> r.%s)" 620 - f.json_name codec f.ocaml_name 621 - else 622 - Printf.sprintf " |> Jsont.Object.opt_mem %S %s ~enc:(fun r -> r.%s)" 623 - f.json_name codec f.ocaml_name 624 - else 625 - Printf.sprintf " |> Jsont.Object.mem %S %s ~enc:(fun r -> r.%s)" 626 - f.json_name codec f.ocaml_name 627 - ) schema.fields) in 628 - 629 - Printf.sprintf {|%s 630 - 631 - %s 632 - 633 - %s 634 - 635 - let jsont : t Jsont.t = 636 - Jsont.Object.map ~kind:%S 637 - (fun %s -> { %s }) 638 - %s 639 - |> Jsont.Object.skip_unknown 640 - |> Jsont.Object.finish|} 641 - type_def v_func accessors schema.original_name make_params v_body jsont_members 642 - 643 - let gen_record_intf ~current_prefix (schema : schema_info) : string = 644 - let loc_type = localize_type ~current_prefix in 645 - let doc = format_doc schema.description in 646 - if schema.fields = [] then 647 - Printf.sprintf "%stype t\n\nval jsont : t Jsont.t\n\nval v : unit -> t" doc 648 - else 649 - (* Abstract type *) 650 - let type_decl = Printf.sprintf "%stype t" doc in 651 - 652 - (* Constructor signature *) 653 - let required_fields = List.filter (fun (f : field_info) -> not f.is_optional) schema.fields in 654 - let optional_fields = List.filter (fun (f : field_info) -> f.is_optional) schema.fields in 655 - let v_param_docs = String.concat "" 656 - ((List.map (fun (f : field_info) -> format_param_doc f.ocaml_name f.description) required_fields) @ 657 - (List.map (fun (f : field_info) -> format_param_doc f.ocaml_name f.description) optional_fields)) 658 - in 659 - let v_params = 660 - (List.map (fun (f : field_info) -> Printf.sprintf "%s:%s" f.ocaml_name (loc_type f.base_type)) required_fields) @ 661 - (List.map (fun (f : field_info) -> Printf.sprintf "?%s:%s" f.ocaml_name (loc_type f.base_type)) optional_fields) @ 662 - ["unit"; "t"] 663 - in 664 - let v_doc = if v_param_docs = "" then "(** Construct a value *)\n" 665 - else Printf.sprintf "(** Construct a value\n%s*)\n" v_param_docs in 666 - let v_sig = Printf.sprintf "%sval v : %s" v_doc (String.concat " -> " v_params) in 667 - 668 - (* Accessor signatures *) 669 - let accessor_sigs = String.concat "\n\n" (List.map (fun (f : field_info) -> 670 - let acc_doc = match f.description with 671 - | Some d -> Printf.sprintf "(** %s *)\n" (escape_doc d) 672 - | None -> "" 673 - in 674 - Printf.sprintf "%sval %s : t -> %s" acc_doc f.ocaml_name (loc_type f.ocaml_type) 675 - ) schema.fields) in 676 - 677 - Printf.sprintf "%s\n\n%s\n\n%s\n\nval jsont : t Jsont.t" 678 - type_decl v_sig accessor_sigs 679 - 680 - (** Format a jsont codec reference, stripping the current_prefix if present *) 681 - let format_jsont_ref ~current_prefix (schema_ref : string) : string = 682 - let prefix, suffix = Name.split_schema_name schema_ref in 683 - let prefix_mod = Name.to_module_name prefix in 684 - let suffix_mod = Name.to_module_name suffix in 685 - if prefix_mod = current_prefix then 686 - Printf.sprintf "%s.jsont" suffix_mod 687 - else 688 - Printf.sprintf "%s.%s.jsont" prefix_mod suffix_mod 689 - 690 - let gen_operation_impl ~current_prefix (op : operation_info) : string = 691 - let doc = format_doc_block ~summary:op.summary ?description:op.description () in 692 - let param_docs = String.concat "" 693 - ((List.map (fun (n, _, d, _) -> format_param_doc n d) op.path_params) @ 694 - (List.map (fun (n, _, d, _) -> format_param_doc n d) op.query_params)) in 695 - let full_doc = if param_docs = "" then doc 696 - else if doc = "" then Printf.sprintf "(**\n%s*)\n" param_docs 697 - else String.sub doc 0 (String.length doc - 3) ^ param_docs ^ "*)\n" in 698 - 699 - let path_args = List.map (fun (n, _, _, _) -> Printf.sprintf "~%s" n) op.path_params in 700 - let query_args = List.map (fun (n, _, _, req) -> 701 - if req then Printf.sprintf "~%s" n else Printf.sprintf "?%s" n 702 - ) op.query_params in 703 - (* DELETE and HEAD don't support body in the requests library *) 704 - let method_supports_body = not (List.mem op.method_ ["DELETE"; "HEAD"; "OPTIONS"]) in 705 - let body_arg = match op.body_schema_ref, method_supports_body with 706 - | Some _, true -> ["~body"] 707 - | _ -> [] 708 - in 709 - let all_args = path_args @ query_args @ body_arg @ ["client"; "()"] in 710 - 711 - let path_render = 712 - if op.path_params = [] then Printf.sprintf "%S" op.path 713 - else 714 - let bindings = List.map (fun (ocaml, json, _, _) -> 715 - Printf.sprintf "(%S, %s)" json ocaml 716 - ) op.path_params in 717 - Printf.sprintf "Openapi.Runtime.Path.render ~params:[%s] %S" 718 - (String.concat "; " bindings) op.path 719 - in 720 - 721 - let query_build = 722 - if op.query_params = [] then "\"\"" 723 - else 724 - let parts = List.map (fun (ocaml, json, _, req) -> 725 - if req then Printf.sprintf "Openapi.Runtime.Query.singleton ~key:%S ~value:%s" json ocaml 726 - else Printf.sprintf "Openapi.Runtime.Query.optional ~key:%S ~value:%s" json ocaml 727 - ) op.query_params in 728 - Printf.sprintf "Openapi.Runtime.Query.encode (List.concat [%s])" (String.concat "; " parts) 729 - in 730 - 731 - let method_lower = String.lowercase_ascii op.method_ in 732 - let body_codec = match op.body_schema_ref with 733 - | Some name -> format_jsont_ref ~current_prefix name 734 - | None -> "Jsont.json" 735 - in 736 - (* DELETE and HEAD don't support body in the requests library *) 737 - let method_supports_body = not (List.mem op.method_ ["DELETE"; "HEAD"; "OPTIONS"]) in 738 - let http_call = match op.body_schema_ref, method_supports_body with 739 - | Some _, true -> 740 - Printf.sprintf "Requests.%s client.session ~body:(Requests.Body.json (Openapi.Runtime.Json.encode_json %s body)) url" 741 - method_lower body_codec 742 - | Some _, false -> 743 - (* Method doesn't support body - ignore the body parameter *) 744 - Printf.sprintf "Requests.%s client.session url" method_lower 745 - | None, _ -> 746 - Printf.sprintf "Requests.%s client.session url" method_lower 747 - in 748 - 749 - let response_codec = match op.response_schema_ref with 750 - | Some name -> format_jsont_ref ~current_prefix name 751 - | None -> "Jsont.json" 752 - in 753 - 754 - let decode = if response_codec = "Jsont.json" then 755 - "Requests.Response.json response" 756 - else 757 - Printf.sprintf "Openapi.Runtime.Json.decode_json_exn %s (Requests.Response.json response)" response_codec 758 - in 759 - 760 - Printf.sprintf {|%slet %s %s = 761 - let op_name = %S in 762 - let url_path = %s in 763 - let query = %s in 764 - let url = client.base_url ^ url_path ^ query in 765 - let response = 766 - try %s 767 - with Eio.Io _ as ex -> 768 - let bt = Printexc.get_raw_backtrace () in 769 - Eio.Exn.reraise_with_context ex bt "calling %%s %%s" %S url 770 - in 771 - if Requests.Response.ok response then 772 - %s 773 - else 774 - raise (Openapi.Runtime.Api_error { 775 - operation = op_name; 776 - method_ = %S; 777 - url; 778 - status = Requests.Response.status_code response; 779 - body = Requests.Response.text response; 780 - })|} 781 - full_doc op.func_name (String.concat " " all_args) 782 - op.func_name path_render query_build http_call op.method_ decode op.method_ 783 - 784 - (** Format a type reference, stripping the current_prefix if present *) 785 - let format_type_ref ~current_prefix (schema_ref : string) : string = 786 - let prefix, suffix = Name.split_schema_name schema_ref in 787 - let prefix_mod = Name.to_module_name prefix in 788 - let suffix_mod = Name.to_module_name suffix in 789 - if prefix_mod = current_prefix then 790 - (* Local reference - use unqualified name *) 791 - Printf.sprintf "%s.t" suffix_mod 792 - else 793 - Printf.sprintf "%s.%s.t" prefix_mod suffix_mod 794 - 795 - let gen_operation_intf ~current_prefix (op : operation_info) : string = 796 - let doc = format_doc_block ~summary:op.summary ?description:op.description () in 797 - let param_docs = String.concat "" 798 - ((List.map (fun (n, _, d, _) -> format_param_doc n d) op.path_params) @ 799 - (List.map (fun (n, _, d, _) -> format_param_doc n d) op.query_params)) in 800 - let full_doc = if param_docs = "" then doc 801 - else if doc = "" then Printf.sprintf "(**\n%s*)\n" param_docs 802 - else String.sub doc 0 (String.length doc - 3) ^ param_docs ^ "*)\n" in 803 - 804 - let path_args = List.map (fun (n, _, _, _) -> Printf.sprintf "%s:string" n) op.path_params in 805 - let query_args = List.map (fun (n, _, _, req) -> 806 - if req then Printf.sprintf "%s:string" n else Printf.sprintf "?%s:string" n 807 - ) op.query_params in 808 - let method_supports_body = not (List.mem op.method_ ["DELETE"; "HEAD"; "OPTIONS"]) in 809 - let body_arg = match op.body_schema_ref, method_supports_body with 810 - | Some name, true -> [Printf.sprintf "body:%s" (format_type_ref ~current_prefix name)] 811 - | _ -> [] 812 - in 813 - let response_type = match op.response_schema_ref with 814 - | Some name -> format_type_ref ~current_prefix name 815 - | None -> "Jsont.json" 816 - in 817 - let all_args = path_args @ query_args @ body_arg @ ["t"; "unit"; response_type] in 818 - 819 - Printf.sprintf "%sval %s : %s" full_doc op.func_name (String.concat " -> " all_args) 820 - 821 - (** {1 Full Module Generation} *) 822 - 823 - let gen_submodule_impl ~current_prefix (schema : schema_info) : string = 824 - let suffix_mod = Name.to_module_name schema.suffix in 825 - let content = if schema.is_enum then gen_enum_impl schema else gen_record_impl ~current_prefix schema in 826 - let indented = String.split_on_char '\n' content |> List.map (fun l -> " " ^ l) |> String.concat "\n" in 827 - Printf.sprintf "module %s = struct\n%s\nend" suffix_mod indented 828 - 829 - let gen_submodule_intf ~current_prefix (schema : schema_info) : string = 830 - let suffix_mod = Name.to_module_name schema.suffix in 831 - let content = if schema.is_enum then gen_enum_intf schema else gen_record_intf ~current_prefix schema in 832 - let indented = String.split_on_char '\n' content |> List.map (fun l -> " " ^ l) |> String.concat "\n" in 833 - Printf.sprintf "module %s : sig\n%s\nend" suffix_mod indented 834 - 835 - (** Extract suffix module dependencies within the same prefix *) 836 - let schema_suffix_deps ~current_prefix (schema : schema_info) : string list = 837 - List.filter_map (fun (f : field_info) -> 838 - (* Check if the type references a sibling module (same prefix) *) 839 - if String.contains f.base_type '.' then 840 - match String.split_on_char '.' f.base_type with 841 - | prefix :: suffix :: _ when prefix = current_prefix -> 842 - Some (Name.to_module_name suffix) 843 - | _ -> None 844 - else None 845 - ) schema.fields 846 - 847 - (** Sort schemas within a prefix module by their dependencies *) 848 - let sort_schemas_by_deps ~current_prefix (schemas : schema_info list) : schema_info list = 849 - let suffix_of schema = Name.to_module_name schema.suffix in 850 - let suffix_names = List.map suffix_of schemas in 851 - let deps_of suffix = 852 - match List.find_opt (fun s -> suffix_of s = suffix) schemas with 853 - | Some schema -> schema_suffix_deps ~current_prefix schema |> List.filter (fun d -> List.mem d suffix_names) 854 - | None -> [] 855 - in 856 - let sorted = topological_sort suffix_names deps_of in 857 - List.filter_map (fun suffix -> 858 - List.find_opt (fun s -> suffix_of s = suffix) schemas 859 - ) sorted 860 - 861 - let gen_prefix_module_impl (node : module_node) : string = 862 - let sorted_schemas = sort_schemas_by_deps ~current_prefix:node.name node.schemas in 863 - let schema_mods = List.map (gen_submodule_impl ~current_prefix:node.name) sorted_schemas in 864 - let op_impls = List.map (gen_operation_impl ~current_prefix:node.name) (List.rev node.operations) in 865 - let content = String.concat "\n\n" (schema_mods @ op_impls) in 866 - let indented = String.split_on_char '\n' content |> List.map (fun l -> " " ^ l) |> String.concat "\n" in 867 - Printf.sprintf "module %s = struct\n%s\nend" node.name indented 868 - 869 - let gen_prefix_module_intf (node : module_node) : string = 870 - let sorted_schemas = sort_schemas_by_deps ~current_prefix:node.name node.schemas in 871 - let schema_mods = List.map (gen_submodule_intf ~current_prefix:node.name) sorted_schemas in 872 - let op_intfs = List.map (gen_operation_intf ~current_prefix:node.name) (List.rev node.operations) in 873 - let content = String.concat "\n\n" (schema_mods @ op_intfs) in 874 - let indented = String.split_on_char '\n' content |> List.map (fun l -> " " ^ l) |> String.concat "\n" in 875 - Printf.sprintf "module %s : sig\n%s\nend" node.name indented 876 - 877 - (** {1 Top-Level Generation} *) 878 - 879 - type config = { 880 - output_dir : string; 881 - package_name : string; 882 - spec_path : string option; 883 - } 884 - 885 - let generate_ml (spec : Spec.t) (package_name : string) : string = 886 - let api_desc = Option.value ~default:"Generated API client." spec.info.description in 887 - 888 - (* Collect schemas *) 889 - let schemas = match spec.components with 890 - | None -> [] 891 - | Some c -> List.filter_map (fun (name, sor) -> 892 - match sor with 893 - | Spec.Ref _ -> None 894 - | Spec.Value s -> Some (analyze_schema name s) 895 - ) c.schemas 896 - in 897 - 898 - (* Collect operations *) 899 - let operations = List.concat_map (fun (path, pi) -> 900 - let ops = [ 901 - ("GET", pi.Spec.get); ("POST", pi.post); ("PUT", pi.put); 902 - ("DELETE", pi.delete); ("PATCH", pi.patch); 903 - ("HEAD", pi.head); ("OPTIONS", pi.options); 904 - ] in 905 - List.filter_map (fun (method_, op_opt) -> 906 - Option.map (fun op -> analyze_operation ~path ~method_ op) op_opt 907 - ) ops 908 - ) spec.paths in 909 - 910 - (* Build module tree *) 911 - let (tree, sorted_modules) = build_module_tree schemas operations in 912 - 913 - (* Generate top-level client type and functions *) 914 - let client_impl = {|type t = { 915 - session : Requests.t; 916 - base_url : string; 917 - } 918 - 919 - let create ?session ~sw env ~base_url = 920 - let session = match session with 921 - | Some s -> s 922 - | None -> Requests.create ~sw env 923 - in 924 - { session; base_url } 925 - 926 - let base_url t = t.base_url 927 - let session t = t.session|} in 928 - 929 - (* Generate prefix modules in dependency order *) 930 - let prefix_mods = List.filter_map (fun name -> 931 - match StringMap.find_opt name tree.children with 932 - | None -> None 933 - | Some node -> 934 - if node.name = "Client" then 935 - (* Generate Client operations inline *) 936 - let ops = List.map (gen_operation_impl ~current_prefix:"Client") (List.rev node.operations) in 937 - if ops = [] then None 938 - else 939 - let content = String.concat "\n\n" ops in 940 - let indented = String.split_on_char '\n' content |> List.map (fun l -> " " ^ l) |> String.concat "\n" in 941 - Some (Printf.sprintf "module Client = struct\n%s\nend" indented) 942 - else 943 - Some (gen_prefix_module_impl node) 944 - ) sorted_modules in 945 - 946 - Printf.sprintf {|(** {1 %s} 947 - 948 - %s 949 - 950 - @version %s *) 951 - 952 - %s 953 - 954 - %s 955 - |} 956 - (Name.to_module_name package_name) (escape_doc api_desc) spec.info.version 957 - client_impl (String.concat "\n\n" prefix_mods) 958 - 959 - let generate_mli (spec : Spec.t) (package_name : string) : string = 960 - let api_desc = Option.value ~default:"Generated API client." spec.info.description in 961 - 962 - (* Collect schemas *) 963 - let schemas = match spec.components with 964 - | None -> [] 965 - | Some c -> List.filter_map (fun (name, sor) -> 966 - match sor with 967 - | Spec.Ref _ -> None 968 - | Spec.Value s -> Some (analyze_schema name s) 969 - ) c.schemas 970 - in 971 - 972 - (* Collect operations *) 973 - let operations = List.concat_map (fun (path, pi) -> 974 - let ops = [ 975 - ("GET", pi.Spec.get); ("POST", pi.post); ("PUT", pi.put); 976 - ("DELETE", pi.delete); ("PATCH", pi.patch); 977 - ("HEAD", pi.head); ("OPTIONS", pi.options); 978 - ] in 979 - List.filter_map (fun (method_, op_opt) -> 980 - Option.map (fun op -> analyze_operation ~path ~method_ op) op_opt 981 - ) ops 982 - ) spec.paths in 983 - 984 - (* Build module tree *) 985 - let (tree, sorted_modules) = build_module_tree schemas operations in 986 - 987 - (* Generate top-level client type and function interfaces *) 988 - let client_intf = {|type t 989 - 990 - val create : 991 - ?session:Requests.t -> 992 - sw:Eio.Switch.t -> 993 - < net : _ Eio.Net.t ; fs : Eio.Fs.dir_ty Eio.Path.t ; clock : _ Eio.Time.clock ; .. > -> 994 - base_url:string -> 995 - t 996 - 997 - val base_url : t -> string 998 - val session : t -> Requests.t|} in 999 - 1000 - (* Generate prefix modules in dependency order *) 1001 - let prefix_mods = List.filter_map (fun name -> 1002 - match StringMap.find_opt name tree.children with 1003 - | None -> None 1004 - | Some node -> 1005 - if node.name = "Client" then 1006 - let ops = List.map (gen_operation_intf ~current_prefix:"Client") (List.rev node.operations) in 1007 - if ops = [] then None 1008 - else 1009 - let content = String.concat "\n\n" ops in 1010 - let indented = String.split_on_char '\n' content |> List.map (fun l -> " " ^ l) |> String.concat "\n" in 1011 - Some (Printf.sprintf "module Client : sig\n%s\nend" indented) 1012 - else 1013 - Some (gen_prefix_module_intf node) 1014 - ) sorted_modules in 1015 - 1016 - Printf.sprintf {|(** {1 %s} 1017 - 1018 - %s 1019 - 1020 - @version %s *) 1021 - 1022 - %s 1023 - 1024 - %s 1025 - |} 1026 - (Name.to_module_name package_name) (escape_doc api_desc) spec.info.version 1027 - client_intf (String.concat "\n\n" prefix_mods) 1028 - 1029 - let generate_dune (package_name : string) : string = 1030 - Printf.sprintf {|(library 1031 - (name %s) 1032 - (libraries openapi jsont jsont.bytesrw requests ptime eio) 1033 - (wrapped true)) 1034 - 1035 - (include dune.inc) 1036 - |} package_name 1037 - 1038 - let generate_dune_inc ~(spec_path : string option) (package_name : string) : string = 1039 - match spec_path with 1040 - | None -> "; No spec path provided - regeneration rules not generated\n" 1041 - | Some path -> 1042 - Printf.sprintf {|; Generated rules for OpenAPI code regeneration 1043 - ; Run: dune build @gen --auto-promote 1044 - 1045 - (rule 1046 - (alias gen) 1047 - (mode (promote (until-clean))) 1048 - (targets %s.ml %s.mli) 1049 - (deps %s) 1050 - (action 1051 - (run openapi-gen generate -o . -n %s %%{deps}))) 1052 - |} package_name package_name path package_name 1053 - 1054 - let generate ~(config : config) (spec : Spec.t) : (string * string) list = 1055 - let package_name = config.package_name in 1056 - [ 1057 - ("dune", generate_dune package_name); 1058 - ("dune.inc", generate_dune_inc ~spec_path:config.spec_path package_name); 1059 - (package_name ^ ".ml", generate_ml spec package_name); 1060 - (package_name ^ ".mli", generate_mli spec package_name); 1061 - ] 1062 - 1063 - let write_files ~(output_dir : string) (files : (string * string) list) : unit = 1064 - List.iter (fun (filename, content) -> 1065 - let path = Filename.concat output_dir filename in 1066 - let oc = open_out path in 1067 - output_string oc content; 1068 - close_out oc 1069 - ) files
-156
ocaml-openapi/lib/openapi_nestjs.ml
··· 1 - (*--------------------------------------------------------------------------- 2 - Copyright (c) 2025 Anil Madhavapeddy. All rights reserved. 3 - SPDX-License-Identifier: ISC 4 - ---------------------------------------------------------------------------*) 5 - 6 - (** NestJS-style API error handling. 7 - 8 - NestJS/Express applications return errors in a standard format: 9 - {[ 10 - { 11 - "message": "Missing required permission: person.read", 12 - "error": "Forbidden", 13 - "statusCode": 403, 14 - "correlationId": "koskgk9d" 15 - } 16 - ]} 17 - 18 - This module provides types and utilities for parsing and handling 19 - these errors in a structured way. 20 - 21 - {2 Usage} 22 - 23 - {[ 24 - match Immich.People.get_all_people client () with 25 - | people -> ... 26 - | exception Openapi.Runtime.Api_error e -> 27 - match Openapi.Nestjs.of_api_error e with 28 - | Some nestjs_error -> 29 - Fmt.epr "Error: %s (correlation: %s)@." 30 - nestjs_error.message 31 - (Option.value ~default:"none" nestjs_error.correlation_id) 32 - | None -> 33 - (* Not a NestJS error, use raw body *) 34 - Fmt.epr "Error: %s@." e.body 35 - ]} 36 - *) 37 - 38 - (** {1 Error Types} *) 39 - 40 - (** A structured NestJS HTTP exception. *) 41 - type t = { 42 - status_code : int; 43 - (** HTTP status code (e.g., 403, 404, 500). *) 44 - 45 - error : string option; 46 - (** Error category (e.g., "Forbidden", "Not Found", "Internal Server Error"). *) 47 - 48 - message : string; 49 - (** Human-readable error message. Can be a single string or concatenated 50 - from an array of validation messages. *) 51 - 52 - correlation_id : string option; 53 - (** Request correlation ID for debugging/support. *) 54 - } 55 - 56 - (** {1 JSON Codec} *) 57 - 58 - (** Jsont codec for NestJS errors. 59 - 60 - Handles both string and array message formats: 61 - - {[ "message": "error text" ]} 62 - - {[ "message": ["validation error 1", "validation error 2"] ]} *) 63 - let jsont : t Jsont.t = 64 - (* Message can be string or array of strings *) 65 - let message_jsont = 66 - Jsont.map Jsont.json ~kind:"message" 67 - ~dec:(fun json -> 68 - match json with 69 - | Jsont.String (s, _) -> s 70 - | Jsont.Array (items, _) -> 71 - items 72 - |> List.filter_map (function 73 - | Jsont.String (s, _) -> Some s 74 - | _ -> None) 75 - |> String.concat "; " 76 - | _ -> "Unknown error") 77 - ~enc:(fun s -> Jsont.String (s, Jsont.Meta.none)) 78 - in 79 - Jsont.Object.map ~kind:"NestjsError" 80 - (fun status_code error message correlation_id -> 81 - { status_code; error; message; correlation_id }) 82 - |> Jsont.Object.mem "statusCode" Jsont.int ~enc:(fun e -> e.status_code) 83 - |> Jsont.Object.opt_mem "error" Jsont.string ~enc:(fun e -> e.error) 84 - |> Jsont.Object.mem "message" message_jsont ~enc:(fun e -> e.message) 85 - |> Jsont.Object.opt_mem "correlationId" Jsont.string ~enc:(fun e -> e.correlation_id) 86 - |> Jsont.Object.skip_unknown 87 - |> Jsont.Object.finish 88 - 89 - (** {1 Parsing} *) 90 - 91 - (** Parse a JSON string into a NestJS error. 92 - Returns [None] if the string is not valid NestJS error JSON. *) 93 - let of_string (s : string) : t option = 94 - match Jsont_bytesrw.decode_string jsont s with 95 - | Ok e -> Some e 96 - | Error _ -> None 97 - 98 - (** Parse an {!Openapi.Runtime.Api_error} into a structured NestJS error. 99 - Returns [None] if the error body is not valid NestJS error JSON. *) 100 - let of_api_error (e : Openapi_runtime.api_error) : t option = 101 - of_string e.body 102 - 103 - (** {1 Convenience Functions} *) 104 - 105 - (** Check if this is a permission/authorization error (401 or 403). *) 106 - let is_auth_error (e : t) : bool = 107 - e.status_code = 401 || e.status_code = 403 108 - 109 - (** Check if this is a "not found" error (404). *) 110 - let is_not_found (e : t) : bool = 111 - e.status_code = 404 112 - 113 - (** Check if this is a validation error (400 with message array). *) 114 - let is_validation_error (e : t) : bool = 115 - e.status_code = 400 116 - 117 - (** Check if this is a server error (5xx). *) 118 - let is_server_error (e : t) : bool = 119 - e.status_code >= 500 && e.status_code < 600 120 - 121 - (** {1 Pretty Printing} *) 122 - 123 - (** Pretty-print a NestJS error. *) 124 - let pp ppf (e : t) = 125 - match e.correlation_id with 126 - | Some cid -> 127 - Format.fprintf ppf "%s [%d] (correlationId: %s)" 128 - e.message e.status_code cid 129 - | None -> 130 - Format.fprintf ppf "%s [%d]" e.message e.status_code 131 - 132 - (** Convert to a human-readable string. *) 133 - let to_string (e : t) : string = 134 - Format.asprintf "%a" pp e 135 - 136 - (** {1 Exception Handling} *) 137 - 138 - (** Exception for NestJS-specific errors. 139 - Use this when you want to distinguish NestJS errors from generic API errors. *) 140 - exception Error of t 141 - 142 - (** Register a pretty printer for the exception. *) 143 - let () = 144 - Printexc.register_printer (function 145 - | Error e -> Some (Format.asprintf "Nestjs.Error: %a" pp e) 146 - | _ -> None) 147 - 148 - (** Handle an {!Openapi.Runtime.Api_error}, converting it to a NestJS error 149 - if possible. 150 - 151 - @raise Error if the error body parses as a NestJS error 152 - @raise Openapi.Runtime.Api_error if parsing fails (re-raises original) *) 153 - let raise_if_nestjs (e : Openapi_runtime.api_error) = 154 - match of_api_error e with 155 - | Some nestjs -> raise (Error nestjs) 156 - | None -> raise (Openapi_runtime.Api_error e)
-104
ocaml-openapi/lib/openapi_nestjs.mli
··· 1 - (*--------------------------------------------------------------------------- 2 - Copyright (c) 2025 Anil Madhavapeddy. All rights reserved. 3 - SPDX-License-Identifier: ISC 4 - ---------------------------------------------------------------------------*) 5 - 6 - (** NestJS-style API error handling. 7 - 8 - NestJS/Express applications return errors in a standard format: 9 - {[ 10 - { 11 - "message": "Missing required permission: person.read", 12 - "error": "Forbidden", 13 - "statusCode": 403, 14 - "correlationId": "koskgk9d" 15 - } 16 - ]} 17 - 18 - This module provides types and utilities for parsing and handling 19 - these errors in a structured way. 20 - 21 - {2 Usage} 22 - 23 - {[ 24 - match Immich.People.get_all_people client () with 25 - | people -> ... 26 - | exception Openapi.Runtime.Api_error e -> 27 - match Openapi.Nestjs.of_api_error e with 28 - | Some nestjs_error -> 29 - Fmt.epr "Error: %s (correlation: %s)@." 30 - nestjs_error.message 31 - (Option.value ~default:"none" nestjs_error.correlation_id) 32 - | None -> 33 - (* Not a NestJS error, use raw body *) 34 - Fmt.epr "Error: %s@." e.body 35 - ]} 36 - *) 37 - 38 - (** {1 Error Types} *) 39 - 40 - (** A structured NestJS HTTP exception. *) 41 - type t = { 42 - status_code : int; 43 - (** HTTP status code (e.g., 403, 404, 500). *) 44 - 45 - error : string option; 46 - (** Error category (e.g., "Forbidden", "Not Found", "Internal Server Error"). *) 47 - 48 - message : string; 49 - (** Human-readable error message. Can be a single string or concatenated 50 - from an array of validation messages. *) 51 - 52 - correlation_id : string option; 53 - (** Request correlation ID for debugging/support. *) 54 - } 55 - 56 - (** {1 JSON Codec} *) 57 - 58 - val jsont : t Jsont.t 59 - (** Jsont codec for NestJS errors. *) 60 - 61 - (** {1 Parsing} *) 62 - 63 - val of_string : string -> t option 64 - (** Parse a JSON string into a NestJS error. 65 - Returns [None] if the string is not valid NestJS error JSON. *) 66 - 67 - val of_api_error : Openapi_runtime.api_error -> t option 68 - (** Parse an {!Openapi_runtime.api_error} into a structured NestJS error. 69 - Returns [None] if the error body is not valid NestJS error JSON. *) 70 - 71 - (** {1 Convenience Functions} *) 72 - 73 - val is_auth_error : t -> bool 74 - (** Check if this is a permission/authorization error (401 or 403). *) 75 - 76 - val is_not_found : t -> bool 77 - (** Check if this is a "not found" error (404). *) 78 - 79 - val is_validation_error : t -> bool 80 - (** Check if this is a validation error (400 with message array). *) 81 - 82 - val is_server_error : t -> bool 83 - (** Check if this is a server error (5xx). *) 84 - 85 - (** {1 Pretty Printing} *) 86 - 87 - val pp : Format.formatter -> t -> unit 88 - (** Pretty-print a NestJS error. *) 89 - 90 - val to_string : t -> string 91 - (** Convert to a human-readable string. *) 92 - 93 - (** {1 Exception Handling} *) 94 - 95 - exception Error of t 96 - (** Exception for NestJS-specific errors. 97 - Use this when you want to distinguish NestJS errors from generic API errors. *) 98 - 99 - val raise_if_nestjs : Openapi_runtime.api_error -> 'a 100 - (** Handle an {!Openapi_runtime.api_error}, converting it to a NestJS error 101 - if possible. 102 - 103 - @raise Error if the error body parses as a NestJS error 104 - @raise Openapi_runtime.Api_error if parsing fails (re-raises original) *)
-200
ocaml-openapi/lib/openapi_runtime.ml
··· 1 - (** Runtime utilities for generated OpenAPI clients. 2 - 3 - This module provides utilities used by generated client code: 4 - - Path template rendering 5 - - Query parameter building 6 - - JSON encoding/decoding helpers 7 - *) 8 - 9 - (** {1 Path Templates} *) 10 - 11 - module Path = struct 12 - (** Render a path template like "/users/{id}/posts/{postId}" with parameters *) 13 - let render ~(params : (string * string) list) (template : string) : string = 14 - List.fold_left 15 - (fun path (name, value) -> 16 - match String.split_on_char '{' path with 17 - | [only] -> only 18 - | parts -> 19 - String.concat "" (List.mapi (fun i part -> 20 - if i = 0 then part 21 - else 22 - match String.split_on_char '}' part with 23 - | [var; rest] when var = name -> value ^ rest 24 - | _ -> "{" ^ part 25 - ) parts)) 26 - template params 27 - 28 - (** Extract parameter names from a path template *) 29 - let parameters (template : string) : string list = 30 - let rec extract acc s = 31 - match String.index_opt s '{' with 32 - | None -> List.rev acc 33 - | Some i -> 34 - let rest = String.sub s (i + 1) (String.length s - i - 1) in 35 - match String.index_opt rest '}' with 36 - | None -> List.rev acc 37 - | Some j -> 38 - let name = String.sub rest 0 j in 39 - let remaining = String.sub rest (j + 1) (String.length rest - j - 1) in 40 - extract (name :: acc) remaining 41 - in 42 - extract [] template 43 - end 44 - 45 - (** {1 Query Parameters} *) 46 - 47 - module Query = struct 48 - type param = string * string 49 - 50 - (** Helper for optional parameters with custom stringifier *) 51 - let optional_with ~key ~value ~to_string : param list = 52 - Option.fold ~none:[] ~some:(fun v -> [(key, to_string v)]) value 53 - 54 - let singleton ~key ~value : param list = [(key, value)] 55 - 56 - let optional ~key ~value : param list = 57 - optional_with ~key ~value ~to_string:Fun.id 58 - 59 - let list ~key ~values : param list = 60 - List.map (fun v -> (key, v)) values 61 - 62 - let int ~key ~value : param list = [(key, string_of_int value)] 63 - 64 - let int_opt ~key ~value : param list = 65 - optional_with ~key ~value ~to_string:string_of_int 66 - 67 - let bool ~key ~value : param list = 68 - [(key, if value then "true" else "false")] 69 - 70 - let bool_opt ~key ~value : param list = 71 - optional_with ~key ~value ~to_string:(fun b -> if b then "true" else "false") 72 - 73 - let float ~key ~value : param list = [(key, string_of_float value)] 74 - 75 - let float_opt ~key ~value : param list = 76 - optional_with ~key ~value ~to_string:string_of_float 77 - 78 - let encode (params : param list) : string = 79 - if params = [] then "" 80 - else 81 - "?" ^ 82 - String.concat "&" (List.map (fun (k, v) -> 83 - (* URL encode the value *) 84 - let encode_char c = 85 - match c with 86 - | 'A'..'Z' | 'a'..'z' | '0'..'9' | '-' | '_' | '.' | '~' -> 87 - String.make 1 c 88 - | c -> 89 - Printf.sprintf "%%%02X" (Char.code c) 90 - in 91 - let encoded_v = String.to_seq v 92 - |> Seq.map encode_char 93 - |> List.of_seq 94 - |> String.concat "" 95 - in 96 - k ^ "=" ^ encoded_v 97 - ) params) 98 - end 99 - 100 - (** {1 JSON Helpers} *) 101 - 102 - module Json = struct 103 - let decode codec s = 104 - Jsont_bytesrw.decode_string codec s 105 - 106 - let decode' codec s = 107 - Jsont_bytesrw.decode_string' codec s 108 - 109 - let encode codec v = 110 - Jsont_bytesrw.encode_string codec v 111 - 112 - let encode' codec v = 113 - Jsont_bytesrw.encode_string' codec v 114 - 115 - let encode_compact codec v = 116 - Jsont_bytesrw.encode_string ~format:Jsont.Minify codec v 117 - 118 - (** Decode a Jsont.json value through a codec. 119 - Encodes to string then decodes - not optimal but works. *) 120 - let decode_json (codec : 'a Jsont.t) (json : Jsont.json) : ('a, string) result = 121 - match Jsont_bytesrw.encode_string Jsont.json json with 122 - | Ok s -> Jsont_bytesrw.decode_string codec s 123 - | Error e -> Error e 124 - 125 - (** Decode a Jsont.json value, raising on error *) 126 - let decode_json_exn (codec : 'a Jsont.t) (json : Jsont.json) : 'a = 127 - match decode_json codec json with 128 - | Ok v -> v 129 - | Error e -> failwith e 130 - 131 - (** Encode a value to Jsont.json *) 132 - let encode_json (codec : 'a Jsont.t) (v : 'a) : Jsont.json = 133 - match Jsont_bytesrw.encode_string codec v with 134 - | Ok s -> 135 - (match Jsont_bytesrw.decode_string Jsont.json s with 136 - | Ok json -> json 137 - | Error _ -> Jsont.Null ((), Jsont.Meta.none)) 138 - | Error _ -> Jsont.Null ((), Jsont.Meta.none) 139 - end 140 - 141 - (** {1 HTTP Method} *) 142 - 143 - type http_method = Get | Post | Put | Patch | Delete | Head | Options 144 - 145 - let string_of_method = function 146 - | Get -> "GET" 147 - | Post -> "POST" 148 - | Put -> "PUT" 149 - | Patch -> "PATCH" 150 - | Delete -> "DELETE" 151 - | Head -> "HEAD" 152 - | Options -> "OPTIONS" 153 - 154 - (** {1 Common Types} *) 155 - 156 - (** ISO 8601 date-time codec *) 157 - let ptime_jsont : Ptime.t Jsont.t = 158 - Jsont.map Jsont.string ~kind:"datetime" 159 - ~dec:(fun s -> 160 - match Ptime.of_rfc3339 s with 161 - | Ok (t, _, _) -> t 162 - | Error _ -> Jsont.Error.msgf Jsont.Meta.none "Invalid datetime: %s" s) 163 - ~enc:(fun t -> Ptime.to_rfc3339 t) 164 - 165 - (** UUID as string *) 166 - let uuid_jsont : string Jsont.t = Jsont.string 167 - 168 - (** Base64 encoded bytes *) 169 - let base64_jsont : string Jsont.t = Jsont.string 170 - 171 - (** {1 Nullable wrapper} *) 172 - 173 - let nullable (codec : 'a Jsont.t) : 'a option Jsont.t = 174 - Jsont.option codec 175 - 176 - (** {1 Any JSON value wrapper} *) 177 - 178 - type json = Jsont.json 179 - 180 - let json_jsont : json Jsont.t = Jsont.json 181 - 182 - (** {1 API Errors} *) 183 - 184 - (** Error raised when an API call fails with a non-2xx status code *) 185 - type api_error = { 186 - operation : string; 187 - method_ : string; 188 - url : string; 189 - status : int; 190 - body : string; 191 - } 192 - 193 - exception Api_error of api_error 194 - 195 - let () = 196 - Printexc.register_printer (function 197 - | Api_error e -> 198 - Some (Printf.sprintf "Api_error: %s %s returned %d: %s" 199 - e.method_ e.url e.status e.body) 200 - | _ -> None)
-876
ocaml-openapi/lib/openapi_spec.ml
··· 1 - (** OpenAPI 3.x specification types with jsont codecs. 2 - 3 - This module defines types that mirror the OpenAPI 3.0/3.1 specification, 4 - with bidirectional JSON codecs using jsont. *) 5 - 6 - (** {1 Reference handling} *) 7 - 8 - type 'a or_ref = 9 - | Ref of string (** A $ref pointer like "#/components/schemas/Pet" *) 10 - | Value of 'a (** An inline value *) 11 - 12 - (** Find a member by name in an object's member list *) 13 - let find_member name (mems : Jsont.mem list) : Jsont.json option = 14 - List.find_map (fun ((n, _meta), v) -> 15 - if n = name then Some v else None 16 - ) mems 17 - 18 - (** Create an or_ref codec that handles $ref pointers. 19 - Uses JSON as intermediate to detect $ref field. *) 20 - let or_ref_jsont (value_jsont : 'a Jsont.t) : 'a or_ref Jsont.t = 21 - Jsont.map Jsont.json ~kind:"or_ref" 22 - ~dec:(fun json -> 23 - match json with 24 - | Jsont.Object (mems, _meta) -> 25 - (match find_member "$ref" mems with 26 - | Some (Jsont.String (ref_str, _)) -> Ref ref_str 27 - | _ -> 28 - (* Not a $ref, decode as value using bytesrw *) 29 - match Jsont_bytesrw.decode_string value_jsont 30 - (Result.get_ok (Jsont_bytesrw.encode_string Jsont.json json)) with 31 - | Ok v -> Value v 32 - | Error e -> Jsont.Error.msg Jsont.Meta.none e) 33 - | _ -> 34 - (* Non-object, decode as value *) 35 - match Jsont_bytesrw.decode_string value_jsont 36 - (Result.get_ok (Jsont_bytesrw.encode_string Jsont.json json)) with 37 - | Ok v -> Value v 38 - | Error e -> Jsont.Error.msg Jsont.Meta.none e) 39 - ~enc:(function 40 - | Ref r -> Jsont.Object ([(("$ref", Jsont.Meta.none), Jsont.String (r, Jsont.Meta.none))], Jsont.Meta.none) 41 - | Value v -> 42 - match Jsont_bytesrw.encode_string value_jsont v with 43 - | Ok s -> 44 - (match Jsont_bytesrw.decode_string Jsont.json s with 45 - | Ok json -> json 46 - | Error _ -> Jsont.Null ((), Jsont.Meta.none)) 47 - | Error _ -> Jsont.Null ((), Jsont.Meta.none)) 48 - 49 - (** {1 String Map} *) 50 - 51 - module StringMap = Map.Make(String) 52 - 53 - let string_map_jsont (value_jsont : 'a Jsont.t) : (string * 'a) list Jsont.t = 54 - let map_jsont = Jsont.Object.as_string_map value_jsont in 55 - Jsont.map ~kind:"string_map" 56 - ~dec:(fun m -> StringMap.bindings m) 57 - ~enc:(fun pairs -> List.fold_left (fun m (k, v) -> StringMap.add k v m) StringMap.empty pairs) 58 - map_jsont 59 - 60 - (** {1 Contact} *) 61 - 62 - type contact = { 63 - name : string option; 64 - url : string option; 65 - email : string option; 66 - } 67 - 68 - let contact_jsont : contact Jsont.t = 69 - Jsont.Object.map ~kind:"Contact" 70 - (fun name url email -> { name; url; email }) 71 - |> Jsont.Object.opt_mem "name" Jsont.string ~enc:(fun c -> c.name) 72 - |> Jsont.Object.opt_mem "url" Jsont.string ~enc:(fun c -> c.url) 73 - |> Jsont.Object.opt_mem "email" Jsont.string ~enc:(fun c -> c.email) 74 - |> Jsont.Object.skip_unknown 75 - |> Jsont.Object.finish 76 - 77 - (** {1 License} *) 78 - 79 - type license = { 80 - name : string; 81 - url : string option; 82 - } 83 - 84 - let license_jsont : license Jsont.t = 85 - Jsont.Object.map ~kind:"License" 86 - (fun name url -> { name; url }) 87 - |> Jsont.Object.mem "name" Jsont.string ~enc:(fun l -> l.name) 88 - |> Jsont.Object.opt_mem "url" Jsont.string ~enc:(fun l -> l.url) 89 - |> Jsont.Object.skip_unknown 90 - |> Jsont.Object.finish 91 - 92 - (** {1 Info} *) 93 - 94 - type info = { 95 - title : string; 96 - description : string option; 97 - terms_of_service : string option; 98 - contact : contact option; 99 - license : license option; 100 - version : string; 101 - } 102 - 103 - let info_jsont : info Jsont.t = 104 - Jsont.Object.map ~kind:"Info" 105 - (fun title description terms_of_service contact license version -> 106 - { title; description; terms_of_service; contact; license; version }) 107 - |> Jsont.Object.mem "title" Jsont.string ~enc:(fun i -> i.title) 108 - |> Jsont.Object.opt_mem "description" Jsont.string ~enc:(fun i -> i.description) 109 - |> Jsont.Object.opt_mem "termsOfService" Jsont.string ~enc:(fun i -> i.terms_of_service) 110 - |> Jsont.Object.opt_mem "contact" contact_jsont ~enc:(fun i -> i.contact) 111 - |> Jsont.Object.opt_mem "license" license_jsont ~enc:(fun i -> i.license) 112 - |> Jsont.Object.mem "version" Jsont.string ~enc:(fun i -> i.version) 113 - |> Jsont.Object.skip_unknown 114 - |> Jsont.Object.finish 115 - 116 - (** {1 Server} *) 117 - 118 - type server_variable = { 119 - enum : string list option; 120 - default : string; 121 - description : string option; 122 - } 123 - 124 - let server_variable_jsont : server_variable Jsont.t = 125 - Jsont.Object.map ~kind:"ServerVariable" 126 - (fun enum default description -> { enum; default; description }) 127 - |> Jsont.Object.opt_mem "enum" Jsont.(list string) ~enc:(fun sv -> sv.enum) 128 - |> Jsont.Object.mem "default" Jsont.string ~enc:(fun sv -> sv.default) 129 - |> Jsont.Object.opt_mem "description" Jsont.string ~enc:(fun sv -> sv.description) 130 - |> Jsont.Object.skip_unknown 131 - |> Jsont.Object.finish 132 - 133 - type server = { 134 - url : string; 135 - description : string option; 136 - variables : (string * server_variable) list; 137 - } 138 - 139 - let server_jsont : server Jsont.t = 140 - Jsont.Object.map ~kind:"Server" 141 - (fun url description variables -> { url; description; variables }) 142 - |> Jsont.Object.mem "url" Jsont.string ~enc:(fun s -> s.url) 143 - |> Jsont.Object.opt_mem "description" Jsont.string ~enc:(fun s -> s.description) 144 - |> Jsont.Object.mem "variables" (string_map_jsont server_variable_jsont) 145 - ~dec_absent:[] ~enc:(fun s -> s.variables) 146 - |> Jsont.Object.skip_unknown 147 - |> Jsont.Object.finish 148 - 149 - (** {1 External Documentation} *) 150 - 151 - type external_docs = { 152 - description : string option; 153 - url : string; 154 - } 155 - 156 - let external_docs_jsont : external_docs Jsont.t = 157 - Jsont.Object.map ~kind:"ExternalDocs" 158 - (fun description url -> { description; url }) 159 - |> Jsont.Object.opt_mem "description" Jsont.string ~enc:(fun ed -> ed.description) 160 - |> Jsont.Object.mem "url" Jsont.string ~enc:(fun ed -> ed.url) 161 - |> Jsont.Object.skip_unknown 162 - |> Jsont.Object.finish 163 - 164 - (** {1 Tag} *) 165 - 166 - type tag = { 167 - name : string; 168 - description : string option; 169 - external_docs : external_docs option; 170 - } 171 - 172 - let tag_jsont : tag Jsont.t = 173 - Jsont.Object.map ~kind:"Tag" 174 - (fun name description external_docs -> { name; description; external_docs }) 175 - |> Jsont.Object.mem "name" Jsont.string ~enc:(fun t -> t.name) 176 - |> Jsont.Object.opt_mem "description" Jsont.string ~enc:(fun t -> t.description) 177 - |> Jsont.Object.opt_mem "externalDocs" external_docs_jsont ~enc:(fun t -> t.external_docs) 178 - |> Jsont.Object.skip_unknown 179 - |> Jsont.Object.finish 180 - 181 - (** {1 Discriminator} *) 182 - 183 - type discriminator = { 184 - property_name : string; 185 - mapping : (string * string) list; 186 - } 187 - 188 - let discriminator_jsont : discriminator Jsont.t = 189 - Jsont.Object.map ~kind:"Discriminator" 190 - (fun property_name mapping -> { property_name; mapping }) 191 - |> Jsont.Object.mem "propertyName" Jsont.string ~enc:(fun d -> d.property_name) 192 - |> Jsont.Object.mem "mapping" (string_map_jsont Jsont.string) 193 - ~dec_absent:[] ~enc:(fun d -> d.mapping) 194 - |> Jsont.Object.skip_unknown 195 - |> Jsont.Object.finish 196 - 197 - (** {1 Schema} 198 - 199 - JSON Schema with OpenAPI extensions. We use a simplified approach 200 - where references are stored as schema or_ref. *) 201 - 202 - type schema = { 203 - title : string option; 204 - description : string option; 205 - type_ : string option; 206 - format : string option; 207 - default : Jsont.json option; 208 - nullable : bool; 209 - read_only : bool; 210 - write_only : bool; 211 - deprecated : bool; 212 - (* Validation *) 213 - enum : Jsont.json list option; 214 - const : Jsont.json option; 215 - minimum : float option; 216 - maximum : float option; 217 - exclusive_minimum : float option; 218 - exclusive_maximum : float option; 219 - multiple_of : float option; 220 - min_length : int option; 221 - max_length : int option; 222 - pattern : string option; 223 - min_items : int option; 224 - max_items : int option; 225 - unique_items : bool; 226 - min_properties : int option; 227 - max_properties : int option; 228 - (* Composition - stored as JSON for simplicity *) 229 - all_of : Jsont.json list option; 230 - one_of : Jsont.json list option; 231 - any_of : Jsont.json list option; 232 - not_ : Jsont.json option; 233 - (* Object - stored as JSON for simplicity *) 234 - properties : (string * Jsont.json) list; 235 - required : string list; 236 - additional_properties : Jsont.json option; 237 - (* Array *) 238 - items : Jsont.json option; 239 - (* Discriminator *) 240 - discriminator : discriminator option; 241 - (* Examples *) 242 - example : Jsont.json option; 243 - } 244 - 245 - let empty_schema = { 246 - title = None; description = None; type_ = None; format = None; default = None; 247 - nullable = false; read_only = false; write_only = false; deprecated = false; 248 - enum = None; const = None; minimum = None; maximum = None; 249 - exclusive_minimum = None; exclusive_maximum = None; multiple_of = None; 250 - min_length = None; max_length = None; pattern = None; 251 - min_items = None; max_items = None; unique_items = false; 252 - min_properties = None; max_properties = None; 253 - all_of = None; one_of = None; any_of = None; not_ = None; 254 - properties = []; required = []; additional_properties = None; 255 - items = None; discriminator = None; example = None; 256 - } 257 - 258 - let schema_jsont : schema Jsont.t = 259 - Jsont.Object.map ~kind:"Schema" 260 - (fun title description type_ format default nullable read_only write_only 261 - deprecated enum const minimum maximum exclusive_minimum exclusive_maximum 262 - multiple_of min_length max_length pattern min_items max_items unique_items 263 - min_properties max_properties all_of one_of any_of not_ properties required 264 - additional_properties items discriminator example -> 265 - { title; description; type_; format; default; nullable; read_only; write_only; 266 - deprecated; enum; const; minimum; maximum; exclusive_minimum; exclusive_maximum; 267 - multiple_of; min_length; max_length; pattern; min_items; max_items; unique_items; 268 - min_properties; max_properties; all_of; one_of; any_of; not_; properties; required; 269 - additional_properties; items; discriminator; example }) 270 - |> Jsont.Object.opt_mem "title" Jsont.string ~enc:(fun s -> s.title) 271 - |> Jsont.Object.opt_mem "description" Jsont.string ~enc:(fun s -> s.description) 272 - |> Jsont.Object.opt_mem "type" Jsont.string ~enc:(fun s -> s.type_) 273 - |> Jsont.Object.opt_mem "format" Jsont.string ~enc:(fun s -> s.format) 274 - |> Jsont.Object.opt_mem "default" Jsont.json ~enc:(fun s -> s.default) 275 - |> Jsont.Object.mem "nullable" Jsont.bool ~dec_absent:false ~enc:(fun s -> s.nullable) 276 - |> Jsont.Object.mem "readOnly" Jsont.bool ~dec_absent:false ~enc:(fun s -> s.read_only) 277 - |> Jsont.Object.mem "writeOnly" Jsont.bool ~dec_absent:false ~enc:(fun s -> s.write_only) 278 - |> Jsont.Object.mem "deprecated" Jsont.bool ~dec_absent:false ~enc:(fun s -> s.deprecated) 279 - |> Jsont.Object.opt_mem "enum" Jsont.(list json) ~enc:(fun s -> s.enum) 280 - |> Jsont.Object.opt_mem "const" Jsont.json ~enc:(fun s -> s.const) 281 - |> Jsont.Object.opt_mem "minimum" Jsont.number ~enc:(fun s -> s.minimum) 282 - |> Jsont.Object.opt_mem "maximum" Jsont.number ~enc:(fun s -> s.maximum) 283 - |> Jsont.Object.opt_mem "exclusiveMinimum" Jsont.number ~enc:(fun s -> s.exclusive_minimum) 284 - |> Jsont.Object.opt_mem "exclusiveMaximum" Jsont.number ~enc:(fun s -> s.exclusive_maximum) 285 - |> Jsont.Object.opt_mem "multipleOf" Jsont.number ~enc:(fun s -> s.multiple_of) 286 - |> Jsont.Object.opt_mem "minLength" Jsont.int ~enc:(fun s -> s.min_length) 287 - |> Jsont.Object.opt_mem "maxLength" Jsont.int ~enc:(fun s -> s.max_length) 288 - |> Jsont.Object.opt_mem "pattern" Jsont.string ~enc:(fun s -> s.pattern) 289 - |> Jsont.Object.opt_mem "minItems" Jsont.int ~enc:(fun s -> s.min_items) 290 - |> Jsont.Object.opt_mem "maxItems" Jsont.int ~enc:(fun s -> s.max_items) 291 - |> Jsont.Object.mem "uniqueItems" Jsont.bool ~dec_absent:false ~enc:(fun s -> s.unique_items) 292 - |> Jsont.Object.opt_mem "minProperties" Jsont.int ~enc:(fun s -> s.min_properties) 293 - |> Jsont.Object.opt_mem "maxProperties" Jsont.int ~enc:(fun s -> s.max_properties) 294 - |> Jsont.Object.opt_mem "allOf" Jsont.(list json) ~enc:(fun s -> s.all_of) 295 - |> Jsont.Object.opt_mem "oneOf" Jsont.(list json) ~enc:(fun s -> s.one_of) 296 - |> Jsont.Object.opt_mem "anyOf" Jsont.(list json) ~enc:(fun s -> s.any_of) 297 - |> Jsont.Object.opt_mem "not" Jsont.json ~enc:(fun s -> s.not_) 298 - |> Jsont.Object.mem "properties" (string_map_jsont Jsont.json) 299 - ~dec_absent:[] ~enc:(fun s -> s.properties) 300 - |> Jsont.Object.mem "required" Jsont.(list string) 301 - ~dec_absent:[] ~enc:(fun s -> s.required) 302 - |> Jsont.Object.opt_mem "additionalProperties" Jsont.json 303 - ~enc:(fun s -> s.additional_properties) 304 - |> Jsont.Object.opt_mem "items" Jsont.json ~enc:(fun s -> s.items) 305 - |> Jsont.Object.opt_mem "discriminator" discriminator_jsont ~enc:(fun s -> s.discriminator) 306 - |> Jsont.Object.opt_mem "example" Jsont.json ~enc:(fun s -> s.example) 307 - |> Jsont.Object.skip_unknown 308 - |> Jsont.Object.finish 309 - 310 - let schema_or_ref_jsont = or_ref_jsont schema_jsont 311 - 312 - (** {1 Parameter} *) 313 - 314 - type parameter_location = Query | Header | Path | Cookie 315 - 316 - let parameter_location_jsont : parameter_location Jsont.t = 317 - Jsont.map Jsont.string ~kind:"parameter_location" 318 - ~dec:(function 319 - | "query" -> Query 320 - | "header" -> Header 321 - | "path" -> Path 322 - | "cookie" -> Cookie 323 - | s -> Jsont.Error.msgf Jsont.Meta.none "Unknown parameter location: %s" s) 324 - ~enc:(function 325 - | Query -> "query" 326 - | Header -> "header" 327 - | Path -> "path" 328 - | Cookie -> "cookie") 329 - 330 - type parameter_style = 331 - | Matrix | Label | Form | Simple | SpaceDelimited 332 - | PipeDelimited | DeepObject 333 - 334 - let parameter_style_jsont : parameter_style Jsont.t = 335 - Jsont.map Jsont.string ~kind:"parameter_style" 336 - ~dec:(function 337 - | "matrix" -> Matrix 338 - | "label" -> Label 339 - | "form" -> Form 340 - | "simple" -> Simple 341 - | "spaceDelimited" -> SpaceDelimited 342 - | "pipeDelimited" -> PipeDelimited 343 - | "deepObject" -> DeepObject 344 - | s -> Jsont.Error.msgf Jsont.Meta.none "Unknown parameter style: %s" s) 345 - ~enc:(function 346 - | Matrix -> "matrix" 347 - | Label -> "label" 348 - | Form -> "form" 349 - | Simple -> "simple" 350 - | SpaceDelimited -> "spaceDelimited" 351 - | PipeDelimited -> "pipeDelimited" 352 - | DeepObject -> "deepObject") 353 - 354 - (** {1 Example} *) 355 - 356 - type example = { 357 - summary : string option; 358 - description : string option; 359 - value : Jsont.json option; 360 - external_value : string option; 361 - } 362 - 363 - let example_jsont : example Jsont.t = 364 - Jsont.Object.map ~kind:"Example" 365 - (fun summary description value external_value -> 366 - { summary; description; value; external_value }) 367 - |> Jsont.Object.opt_mem "summary" Jsont.string ~enc:(fun e -> e.summary) 368 - |> Jsont.Object.opt_mem "description" Jsont.string ~enc:(fun e -> e.description) 369 - |> Jsont.Object.opt_mem "value" Jsont.json ~enc:(fun e -> e.value) 370 - |> Jsont.Object.opt_mem "externalValue" Jsont.string ~enc:(fun e -> e.external_value) 371 - |> Jsont.Object.skip_unknown 372 - |> Jsont.Object.finish 373 - 374 - let example_or_ref_jsont = or_ref_jsont example_jsont 375 - 376 - (** {1 Header} *) 377 - 378 - type header = { 379 - description : string option; 380 - required : bool; 381 - deprecated : bool; 382 - schema : schema or_ref option; 383 - } 384 - 385 - let header_jsont : header Jsont.t = 386 - Jsont.Object.map ~kind:"Header" 387 - (fun description required deprecated schema -> 388 - { description; required; deprecated; schema }) 389 - |> Jsont.Object.opt_mem "description" Jsont.string ~enc:(fun h -> h.description) 390 - |> Jsont.Object.mem "required" Jsont.bool ~dec_absent:false ~enc:(fun h -> h.required) 391 - |> Jsont.Object.mem "deprecated" Jsont.bool ~dec_absent:false ~enc:(fun h -> h.deprecated) 392 - |> Jsont.Object.opt_mem "schema" schema_or_ref_jsont ~enc:(fun h -> h.schema) 393 - |> Jsont.Object.skip_unknown 394 - |> Jsont.Object.finish 395 - 396 - let header_or_ref_jsont = or_ref_jsont header_jsont 397 - 398 - (** {1 Encoding} *) 399 - 400 - type encoding = { 401 - content_type : string option; 402 - headers : (string * header or_ref) list; 403 - style : parameter_style option; 404 - explode : bool option; 405 - allow_reserved : bool; 406 - } 407 - 408 - let encoding_jsont : encoding Jsont.t = 409 - Jsont.Object.map ~kind:"Encoding" 410 - (fun content_type headers style explode allow_reserved -> 411 - { content_type; headers; style; explode; allow_reserved }) 412 - |> Jsont.Object.opt_mem "contentType" Jsont.string ~enc:(fun e -> e.content_type) 413 - |> Jsont.Object.mem "headers" (string_map_jsont header_or_ref_jsont) 414 - ~dec_absent:[] ~enc:(fun e -> e.headers) 415 - |> Jsont.Object.opt_mem "style" parameter_style_jsont ~enc:(fun e -> e.style) 416 - |> Jsont.Object.opt_mem "explode" Jsont.bool ~enc:(fun e -> e.explode) 417 - |> Jsont.Object.mem "allowReserved" Jsont.bool ~dec_absent:false ~enc:(fun e -> e.allow_reserved) 418 - |> Jsont.Object.skip_unknown 419 - |> Jsont.Object.finish 420 - 421 - (** {1 Media Type} *) 422 - 423 - type media_type = { 424 - schema : schema or_ref option; 425 - example : Jsont.json option; 426 - examples : (string * example or_ref) list; 427 - encoding : (string * encoding) list; 428 - } 429 - 430 - let media_type_jsont : media_type Jsont.t = 431 - Jsont.Object.map ~kind:"MediaType" 432 - (fun schema example examples encoding -> 433 - { schema; example; examples; encoding }) 434 - |> Jsont.Object.opt_mem "schema" schema_or_ref_jsont ~enc:(fun mt -> mt.schema) 435 - |> Jsont.Object.opt_mem "example" Jsont.json ~enc:(fun mt -> mt.example) 436 - |> Jsont.Object.mem "examples" (string_map_jsont example_or_ref_jsont) 437 - ~dec_absent:[] ~enc:(fun mt -> mt.examples) 438 - |> Jsont.Object.mem "encoding" (string_map_jsont encoding_jsont) 439 - ~dec_absent:[] ~enc:(fun mt -> mt.encoding) 440 - |> Jsont.Object.skip_unknown 441 - |> Jsont.Object.finish 442 - 443 - (** {1 Parameter} *) 444 - 445 - type parameter = { 446 - name : string; 447 - in_ : parameter_location; 448 - description : string option; 449 - required : bool; 450 - deprecated : bool; 451 - allow_empty_value : bool; 452 - style : parameter_style option; 453 - explode : bool option; 454 - allow_reserved : bool; 455 - schema : schema or_ref option; 456 - example : Jsont.json option; 457 - content : (string * media_type) list; 458 - } 459 - 460 - let parameter_jsont : parameter Jsont.t = 461 - Jsont.Object.map ~kind:"Parameter" 462 - (fun name in_ description required deprecated allow_empty_value style 463 - explode allow_reserved schema example content -> 464 - { name; in_; description; required; deprecated; allow_empty_value; 465 - style; explode; allow_reserved; schema; example; content }) 466 - |> Jsont.Object.mem "name" Jsont.string ~enc:(fun p -> p.name) 467 - |> Jsont.Object.mem "in" parameter_location_jsont ~enc:(fun p -> p.in_) 468 - |> Jsont.Object.opt_mem "description" Jsont.string ~enc:(fun p -> p.description) 469 - |> Jsont.Object.mem "required" Jsont.bool ~dec_absent:false ~enc:(fun p -> p.required) 470 - |> Jsont.Object.mem "deprecated" Jsont.bool ~dec_absent:false ~enc:(fun p -> p.deprecated) 471 - |> Jsont.Object.mem "allowEmptyValue" Jsont.bool ~dec_absent:false ~enc:(fun p -> p.allow_empty_value) 472 - |> Jsont.Object.opt_mem "style" parameter_style_jsont ~enc:(fun p -> p.style) 473 - |> Jsont.Object.opt_mem "explode" Jsont.bool ~enc:(fun p -> p.explode) 474 - |> Jsont.Object.mem "allowReserved" Jsont.bool ~dec_absent:false ~enc:(fun p -> p.allow_reserved) 475 - |> Jsont.Object.opt_mem "schema" schema_or_ref_jsont ~enc:(fun p -> p.schema) 476 - |> Jsont.Object.opt_mem "example" Jsont.json ~enc:(fun p -> p.example) 477 - |> Jsont.Object.mem "content" (string_map_jsont media_type_jsont) 478 - ~dec_absent:[] ~enc:(fun p -> p.content) 479 - |> Jsont.Object.skip_unknown 480 - |> Jsont.Object.finish 481 - 482 - let parameter_or_ref_jsont = or_ref_jsont parameter_jsont 483 - 484 - (** {1 Request Body} *) 485 - 486 - type request_body = { 487 - description : string option; 488 - content : (string * media_type) list; 489 - required : bool; 490 - } 491 - 492 - let request_body_jsont : request_body Jsont.t = 493 - Jsont.Object.map ~kind:"RequestBody" 494 - (fun description content required -> { description; content; required }) 495 - |> Jsont.Object.opt_mem "description" Jsont.string ~enc:(fun rb -> rb.description) 496 - |> Jsont.Object.mem "content" (string_map_jsont media_type_jsont) 497 - ~dec_absent:[] ~enc:(fun rb -> rb.content) 498 - |> Jsont.Object.mem "required" Jsont.bool ~dec_absent:false ~enc:(fun rb -> rb.required) 499 - |> Jsont.Object.skip_unknown 500 - |> Jsont.Object.finish 501 - 502 - let request_body_or_ref_jsont = or_ref_jsont request_body_jsont 503 - 504 - (** {1 Link} *) 505 - 506 - type link = { 507 - operation_ref : string option; 508 - operation_id : string option; 509 - parameters : (string * Jsont.json) list; 510 - request_body : Jsont.json option; 511 - description : string option; 512 - server : server option; 513 - } 514 - 515 - let link_jsont : link Jsont.t = 516 - Jsont.Object.map ~kind:"Link" 517 - (fun operation_ref operation_id parameters request_body description server -> 518 - { operation_ref; operation_id; parameters; request_body; description; server }) 519 - |> Jsont.Object.opt_mem "operationRef" Jsont.string ~enc:(fun l -> l.operation_ref) 520 - |> Jsont.Object.opt_mem "operationId" Jsont.string ~enc:(fun l -> l.operation_id) 521 - |> Jsont.Object.mem "parameters" (string_map_jsont Jsont.json) 522 - ~dec_absent:[] ~enc:(fun l -> l.parameters) 523 - |> Jsont.Object.opt_mem "requestBody" Jsont.json ~enc:(fun l -> l.request_body) 524 - |> Jsont.Object.opt_mem "description" Jsont.string ~enc:(fun l -> l.description) 525 - |> Jsont.Object.opt_mem "server" server_jsont ~enc:(fun l -> l.server) 526 - |> Jsont.Object.skip_unknown 527 - |> Jsont.Object.finish 528 - 529 - let link_or_ref_jsont = or_ref_jsont link_jsont 530 - 531 - (** {1 Response} *) 532 - 533 - type response = { 534 - description : string; 535 - headers : (string * header or_ref) list; 536 - content : (string * media_type) list; 537 - links : (string * link or_ref) list; 538 - } 539 - 540 - let response_jsont : response Jsont.t = 541 - Jsont.Object.map ~kind:"Response" 542 - (fun description headers content links -> 543 - { description; headers; content; links }) 544 - |> Jsont.Object.mem "description" Jsont.string ~dec_absent:"" ~enc:(fun r -> r.description) 545 - |> Jsont.Object.mem "headers" (string_map_jsont header_or_ref_jsont) 546 - ~dec_absent:[] ~enc:(fun r -> r.headers) 547 - |> Jsont.Object.mem "content" (string_map_jsont media_type_jsont) 548 - ~dec_absent:[] ~enc:(fun r -> r.content) 549 - |> Jsont.Object.mem "links" (string_map_jsont link_or_ref_jsont) 550 - ~dec_absent:[] ~enc:(fun r -> r.links) 551 - |> Jsont.Object.skip_unknown 552 - |> Jsont.Object.finish 553 - 554 - let response_or_ref_jsont = or_ref_jsont response_jsont 555 - 556 - (** {1 Responses} *) 557 - 558 - type responses = { 559 - default : response or_ref option; 560 - responses : (string * response or_ref) list; (* status code -> response *) 561 - } 562 - 563 - let responses_jsont : responses Jsont.t = 564 - (* Responses is an object where keys are status codes or "default" *) 565 - Jsont.map (Jsont.Object.as_string_map response_or_ref_jsont) ~kind:"Responses" 566 - ~dec:(fun m -> 567 - let default = StringMap.find_opt "default" m in 568 - let responses = 569 - StringMap.bindings m 570 - |> List.filter (fun (k, _) -> k <> "default") 571 - in 572 - { default; responses }) 573 - ~enc:(fun r -> 574 - let m = List.fold_left (fun m (k, v) -> StringMap.add k v m) StringMap.empty r.responses in 575 - match r.default with 576 - | Some d -> StringMap.add "default" d m 577 - | None -> m) 578 - 579 - (** {1 Security Requirement} *) 580 - 581 - type security_requirement = (string * string list) list 582 - 583 - let security_requirement_jsont : security_requirement Jsont.t = 584 - string_map_jsont Jsont.(list string) 585 - 586 - (** {1 Callback - simplified to JSON} *) 587 - 588 - type callback = Jsont.json 589 - 590 - let callback_jsont : callback Jsont.t = Jsont.json 591 - let callback_or_ref_jsont = or_ref_jsont callback_jsont 592 - 593 - (** {1 Operation} *) 594 - 595 - type operation = { 596 - tags : string list; 597 - summary : string option; 598 - description : string option; 599 - external_docs : external_docs option; 600 - operation_id : string option; 601 - parameters : parameter or_ref list; 602 - request_body : request_body or_ref option; 603 - responses : responses; 604 - callbacks : (string * callback or_ref) list; 605 - deprecated : bool; 606 - security : security_requirement list option; 607 - servers : server list; 608 - } 609 - 610 - let operation_jsont : operation Jsont.t = 611 - Jsont.Object.map ~kind:"Operation" 612 - (fun tags summary description external_docs operation_id parameters 613 - request_body responses callbacks deprecated security servers -> 614 - { tags; summary; description; external_docs; operation_id; parameters; 615 - request_body; responses; callbacks; deprecated; security; servers }) 616 - |> Jsont.Object.mem "tags" Jsont.(list string) ~dec_absent:[] ~enc:(fun o -> o.tags) 617 - |> Jsont.Object.opt_mem "summary" Jsont.string ~enc:(fun o -> o.summary) 618 - |> Jsont.Object.opt_mem "description" Jsont.string ~enc:(fun o -> o.description) 619 - |> Jsont.Object.opt_mem "externalDocs" external_docs_jsont ~enc:(fun o -> o.external_docs) 620 - |> Jsont.Object.opt_mem "operationId" Jsont.string ~enc:(fun o -> o.operation_id) 621 - |> Jsont.Object.mem "parameters" Jsont.(list parameter_or_ref_jsont) 622 - ~dec_absent:[] ~enc:(fun o -> o.parameters) 623 - |> Jsont.Object.opt_mem "requestBody" request_body_or_ref_jsont ~enc:(fun o -> o.request_body) 624 - |> Jsont.Object.mem "responses" responses_jsont 625 - ~dec_absent:{ default = None; responses = [] } ~enc:(fun o -> o.responses) 626 - |> Jsont.Object.mem "callbacks" (string_map_jsont callback_or_ref_jsont) 627 - ~dec_absent:[] ~enc:(fun o -> o.callbacks) 628 - |> Jsont.Object.mem "deprecated" Jsont.bool ~dec_absent:false ~enc:(fun o -> o.deprecated) 629 - |> Jsont.Object.opt_mem "security" Jsont.(list security_requirement_jsont) 630 - ~enc:(fun o -> o.security) 631 - |> Jsont.Object.mem "servers" Jsont.(list server_jsont) 632 - ~dec_absent:[] ~enc:(fun o -> o.servers) 633 - |> Jsont.Object.skip_unknown 634 - |> Jsont.Object.finish 635 - 636 - (** {1 Path Item} *) 637 - 638 - type path_item = { 639 - ref_ : string option; 640 - summary : string option; 641 - description : string option; 642 - get : operation option; 643 - put : operation option; 644 - post : operation option; 645 - delete : operation option; 646 - options : operation option; 647 - head : operation option; 648 - patch : operation option; 649 - trace : operation option; 650 - servers : server list; 651 - parameters : parameter or_ref list; 652 - } 653 - 654 - let path_item_jsont : path_item Jsont.t = 655 - Jsont.Object.map ~kind:"PathItem" 656 - (fun ref_ summary description get put post delete options head patch trace 657 - servers parameters -> 658 - { ref_; summary; description; get; put; post; delete; options; head; 659 - patch; trace; servers; parameters }) 660 - |> Jsont.Object.opt_mem "$ref" Jsont.string ~enc:(fun pi -> pi.ref_) 661 - |> Jsont.Object.opt_mem "summary" Jsont.string ~enc:(fun pi -> pi.summary) 662 - |> Jsont.Object.opt_mem "description" Jsont.string ~enc:(fun pi -> pi.description) 663 - |> Jsont.Object.opt_mem "get" operation_jsont ~enc:(fun pi -> pi.get) 664 - |> Jsont.Object.opt_mem "put" operation_jsont ~enc:(fun pi -> pi.put) 665 - |> Jsont.Object.opt_mem "post" operation_jsont ~enc:(fun pi -> pi.post) 666 - |> Jsont.Object.opt_mem "delete" operation_jsont ~enc:(fun pi -> pi.delete) 667 - |> Jsont.Object.opt_mem "options" operation_jsont ~enc:(fun pi -> pi.options) 668 - |> Jsont.Object.opt_mem "head" operation_jsont ~enc:(fun pi -> pi.head) 669 - |> Jsont.Object.opt_mem "patch" operation_jsont ~enc:(fun pi -> pi.patch) 670 - |> Jsont.Object.opt_mem "trace" operation_jsont ~enc:(fun pi -> pi.trace) 671 - |> Jsont.Object.mem "servers" Jsont.(list server_jsont) 672 - ~dec_absent:[] ~enc:(fun pi -> pi.servers) 673 - |> Jsont.Object.mem "parameters" Jsont.(list parameter_or_ref_jsont) 674 - ~dec_absent:[] ~enc:(fun pi -> pi.parameters) 675 - |> Jsont.Object.skip_unknown 676 - |> Jsont.Object.finish 677 - 678 - let path_item_or_ref_jsont = or_ref_jsont path_item_jsont 679 - 680 - (** {1 Security Scheme} *) 681 - 682 - type security_scheme_type = 683 - | ApiKey 684 - | Http 685 - | OAuth2 686 - | OpenIdConnect 687 - 688 - let security_scheme_type_jsont : security_scheme_type Jsont.t = 689 - Jsont.map Jsont.string ~kind:"security_scheme_type" 690 - ~dec:(function 691 - | "apiKey" -> ApiKey 692 - | "http" -> Http 693 - | "oauth2" -> OAuth2 694 - | "openIdConnect" -> OpenIdConnect 695 - | s -> Jsont.Error.msgf Jsont.Meta.none "Unknown security scheme type: %s" s) 696 - ~enc:(function 697 - | ApiKey -> "apiKey" 698 - | Http -> "http" 699 - | OAuth2 -> "oauth2" 700 - | OpenIdConnect -> "openIdConnect") 701 - 702 - type oauth_flow = { 703 - authorization_url : string option; 704 - token_url : string option; 705 - refresh_url : string option; 706 - scopes : (string * string) list; 707 - } 708 - 709 - let oauth_flow_jsont : oauth_flow Jsont.t = 710 - Jsont.Object.map ~kind:"OAuthFlow" 711 - (fun authorization_url token_url refresh_url scopes -> 712 - { authorization_url; token_url; refresh_url; scopes }) 713 - |> Jsont.Object.opt_mem "authorizationUrl" Jsont.string ~enc:(fun f -> f.authorization_url) 714 - |> Jsont.Object.opt_mem "tokenUrl" Jsont.string ~enc:(fun f -> f.token_url) 715 - |> Jsont.Object.opt_mem "refreshUrl" Jsont.string ~enc:(fun f -> f.refresh_url) 716 - |> Jsont.Object.mem "scopes" (string_map_jsont Jsont.string) 717 - ~dec_absent:[] ~enc:(fun f -> f.scopes) 718 - |> Jsont.Object.skip_unknown 719 - |> Jsont.Object.finish 720 - 721 - type oauth_flows = { 722 - implicit : oauth_flow option; 723 - password : oauth_flow option; 724 - client_credentials : oauth_flow option; 725 - authorization_code : oauth_flow option; 726 - } 727 - 728 - let oauth_flows_jsont : oauth_flows Jsont.t = 729 - Jsont.Object.map ~kind:"OAuthFlows" 730 - (fun implicit password client_credentials authorization_code -> 731 - { implicit; password; client_credentials; authorization_code }) 732 - |> Jsont.Object.opt_mem "implicit" oauth_flow_jsont ~enc:(fun f -> f.implicit) 733 - |> Jsont.Object.opt_mem "password" oauth_flow_jsont ~enc:(fun f -> f.password) 734 - |> Jsont.Object.opt_mem "clientCredentials" oauth_flow_jsont ~enc:(fun f -> f.client_credentials) 735 - |> Jsont.Object.opt_mem "authorizationCode" oauth_flow_jsont ~enc:(fun f -> f.authorization_code) 736 - |> Jsont.Object.skip_unknown 737 - |> Jsont.Object.finish 738 - 739 - type security_scheme = { 740 - type_ : security_scheme_type; 741 - description : string option; 742 - name : string option; 743 - in_ : parameter_location option; 744 - scheme : string option; 745 - bearer_format : string option; 746 - flows : oauth_flows option; 747 - open_id_connect_url : string option; 748 - } 749 - 750 - let security_scheme_jsont : security_scheme Jsont.t = 751 - Jsont.Object.map ~kind:"SecurityScheme" 752 - (fun type_ description name in_ scheme bearer_format flows open_id_connect_url -> 753 - { type_; description; name; in_; scheme; bearer_format; flows; open_id_connect_url }) 754 - |> Jsont.Object.mem "type" security_scheme_type_jsont ~enc:(fun ss -> ss.type_) 755 - |> Jsont.Object.opt_mem "description" Jsont.string ~enc:(fun ss -> ss.description) 756 - |> Jsont.Object.opt_mem "name" Jsont.string ~enc:(fun ss -> ss.name) 757 - |> Jsont.Object.opt_mem "in" parameter_location_jsont ~enc:(fun ss -> ss.in_) 758 - |> Jsont.Object.opt_mem "scheme" Jsont.string ~enc:(fun ss -> ss.scheme) 759 - |> Jsont.Object.opt_mem "bearerFormat" Jsont.string ~enc:(fun ss -> ss.bearer_format) 760 - |> Jsont.Object.opt_mem "flows" oauth_flows_jsont ~enc:(fun ss -> ss.flows) 761 - |> Jsont.Object.opt_mem "openIdConnectUrl" Jsont.string ~enc:(fun ss -> ss.open_id_connect_url) 762 - |> Jsont.Object.skip_unknown 763 - |> Jsont.Object.finish 764 - 765 - let security_scheme_or_ref_jsont = or_ref_jsont security_scheme_jsont 766 - 767 - (** {1 Components} *) 768 - 769 - type components = { 770 - schemas : (string * schema or_ref) list; 771 - responses : (string * response or_ref) list; 772 - parameters : (string * parameter or_ref) list; 773 - examples : (string * example or_ref) list; 774 - request_bodies : (string * request_body or_ref) list; 775 - headers : (string * header or_ref) list; 776 - security_schemes : (string * security_scheme or_ref) list; 777 - links : (string * link or_ref) list; 778 - callbacks : (string * callback or_ref) list; 779 - path_items : (string * path_item or_ref) list; 780 - } 781 - 782 - let components_jsont : components Jsont.t = 783 - Jsont.Object.map ~kind:"Components" 784 - (fun schemas responses parameters examples request_bodies headers 785 - security_schemes links callbacks path_items -> 786 - { schemas; responses; parameters; examples; request_bodies; 787 - headers; security_schemes; links; callbacks; path_items }) 788 - |> Jsont.Object.mem "schemas" (string_map_jsont schema_or_ref_jsont) 789 - ~dec_absent:[] ~enc:(fun c -> c.schemas) 790 - |> Jsont.Object.mem "responses" (string_map_jsont response_or_ref_jsont) 791 - ~dec_absent:[] ~enc:(fun c -> c.responses) 792 - |> Jsont.Object.mem "parameters" (string_map_jsont parameter_or_ref_jsont) 793 - ~dec_absent:[] ~enc:(fun c -> c.parameters) 794 - |> Jsont.Object.mem "examples" (string_map_jsont example_or_ref_jsont) 795 - ~dec_absent:[] ~enc:(fun c -> c.examples) 796 - |> Jsont.Object.mem "requestBodies" (string_map_jsont request_body_or_ref_jsont) 797 - ~dec_absent:[] ~enc:(fun c -> c.request_bodies) 798 - |> Jsont.Object.mem "headers" (string_map_jsont header_or_ref_jsont) 799 - ~dec_absent:[] ~enc:(fun c -> c.headers) 800 - |> Jsont.Object.mem "securitySchemes" (string_map_jsont security_scheme_or_ref_jsont) 801 - ~dec_absent:[] ~enc:(fun c -> c.security_schemes) 802 - |> Jsont.Object.mem "links" (string_map_jsont link_or_ref_jsont) 803 - ~dec_absent:[] ~enc:(fun c -> c.links) 804 - |> Jsont.Object.mem "callbacks" (string_map_jsont callback_or_ref_jsont) 805 - ~dec_absent:[] ~enc:(fun c -> c.callbacks) 806 - |> Jsont.Object.mem "pathItems" (string_map_jsont path_item_or_ref_jsont) 807 - ~dec_absent:[] ~enc:(fun c -> c.path_items) 808 - |> Jsont.Object.skip_unknown 809 - |> Jsont.Object.finish 810 - 811 - (** {1 OpenAPI Document} *) 812 - 813 - type t = { 814 - openapi : string; 815 - info : info; 816 - servers : server list; 817 - paths : (string * path_item) list; 818 - webhooks : (string * path_item or_ref) list; 819 - components : components option; 820 - security : security_requirement list; 821 - tags : tag list; 822 - external_docs : external_docs option; 823 - } 824 - 825 - let jsont : t Jsont.t = 826 - Jsont.Object.map ~kind:"OpenAPI" 827 - (fun openapi info servers paths webhooks components security tags external_docs -> 828 - { openapi; info; servers; paths; webhooks; components; security; tags; external_docs }) 829 - |> Jsont.Object.mem "openapi" Jsont.string ~enc:(fun t -> t.openapi) 830 - |> Jsont.Object.mem "info" info_jsont ~enc:(fun t -> t.info) 831 - |> Jsont.Object.mem "servers" Jsont.(list server_jsont) 832 - ~dec_absent:[] ~enc:(fun t -> t.servers) 833 - |> Jsont.Object.mem "paths" (string_map_jsont path_item_jsont) 834 - ~dec_absent:[] ~enc:(fun t -> t.paths) 835 - |> Jsont.Object.mem "webhooks" (string_map_jsont path_item_or_ref_jsont) 836 - ~dec_absent:[] ~enc:(fun t -> t.webhooks) 837 - |> Jsont.Object.opt_mem "components" components_jsont ~enc:(fun t -> t.components) 838 - |> Jsont.Object.mem "security" Jsont.(list security_requirement_jsont) 839 - ~dec_absent:[] ~enc:(fun t -> t.security) 840 - |> Jsont.Object.mem "tags" Jsont.(list tag_jsont) 841 - ~dec_absent:[] ~enc:(fun t -> t.tags) 842 - |> Jsont.Object.opt_mem "externalDocs" external_docs_jsont ~enc:(fun t -> t.external_docs) 843 - |> Jsont.Object.skip_unknown 844 - |> Jsont.Object.finish 845 - 846 - (** {1 Parsing} *) 847 - 848 - let of_string s = 849 - Jsont_bytesrw.decode_string jsont s 850 - 851 - let of_string' s = 852 - Jsont_bytesrw.decode_string' jsont s 853 - 854 - let to_string t = 855 - Jsont_bytesrw.encode_string ~format:Jsont.Indent jsont t 856 - 857 - let to_string' t = 858 - Jsont_bytesrw.encode_string' ~format:Jsont.Indent jsont t 859 - 860 - (** {1 Reference Resolution} *) 861 - 862 - let resolve_schema_ref (ref_str : string) (spec : t) : schema option = 863 - (* Parse $ref like "#/components/schemas/Pet" *) 864 - if not (String.length ref_str > 0 && ref_str.[0] = '#') then None 865 - else 866 - let parts = String.split_on_char '/' ref_str in 867 - match parts with 868 - | ["#"; "components"; "schemas"; name] -> 869 - (match spec.components with 870 - | None -> None 871 - | Some c -> 872 - match List.assoc_opt name c.schemas with 873 - | Some (Value s) -> Some s 874 - | Some (Ref _) -> None (* nested refs not supported yet *) 875 - | None -> None) 876 - | _ -> None
-39
ocaml-openapi/openapi.opam
··· 1 - # This file is generated by dune, edit dune-project instead 2 - opam-version: "2.0" 3 - synopsis: "OpenAPI code generator for OCaml with requests and jsont" 4 - description: """ 5 - Generate type-safe OCaml API clients from OpenAPI 3.x specifications. 6 - Uses the requests HTTP library and jsont for JSON codecs.""" 7 - maintainer: ["Anil Madhavapeddy <anil@recoil.org>"] 8 - authors: ["Anil Madhavapeddy"] 9 - license: "ISC" 10 - homepage: "https://tangled.org/@anil.recoil.org/ocaml-openapi" 11 - bug-reports: "https://tangled.org/anil.recoil.org/ocaml-openapi/issues" 12 - depends: [ 13 - "dune" {>= "3.21"} 14 - "ocaml" {>= "5.1.0"} 15 - "jsont" 16 - "bytesrw" 17 - "fmt" 18 - "logs" 19 - "ptime" 20 - "cmdliner" {>= "1.2.0"} 21 - "odoc" {with-doc} 22 - "alcotest" {with-test} 23 - ] 24 - build: [ 25 - ["dune" "subst"] {dev} 26 - [ 27 - "dune" 28 - "build" 29 - "-p" 30 - name 31 - "-j" 32 - jobs 33 - "@install" 34 - "@runtest" {with-test} 35 - "@doc" {with-doc} 36 - ] 37 - ] 38 - dev-repo: "git+https://tangled.org/anil.recoil.org/ocaml-openapi" 39 - x-maintenance-intent: ["(latest)"]
-3
ocaml-openapi/test/dune
··· 1 - (test 2 - (name test_openapi) 3 - (libraries openapi alcotest))
-341
ocaml-openapi/test/test_openapi.ml
··· 1 - (** Tests for ocaml-openapi *) 2 - 3 - module Spec = Openapi.Spec 4 - module Codegen = Openapi.Codegen 5 - module Runtime = Openapi.Runtime 6 - 7 - (** {1 Path Template Tests} *) 8 - 9 - let test_path_render_simple () = 10 - let result = Runtime.Path.render ~params:[] "/users" in 11 - Alcotest.(check string) "no params" "/users" result 12 - 13 - let test_path_render_one_param () = 14 - let result = Runtime.Path.render ~params:[("id", "123")] "/users/{id}" in 15 - Alcotest.(check string) "one param" "/users/123" result 16 - 17 - let test_path_render_multiple_params () = 18 - let result = Runtime.Path.render 19 - ~params:[("userId", "42"); ("postId", "99")] 20 - "/users/{userId}/posts/{postId}" in 21 - Alcotest.(check string) "multiple params" "/users/42/posts/99" result 22 - 23 - let test_path_parameters () = 24 - let params = Runtime.Path.parameters "/users/{userId}/posts/{postId}" in 25 - Alcotest.(check (list string)) "extract params" ["userId"; "postId"] params 26 - 27 - (** {1 Query Parameter Tests} *) 28 - 29 - let test_query_singleton () = 30 - let params = Runtime.Query.singleton ~key:"name" ~value:"alice" in 31 - Alcotest.(check (list (pair string string))) "singleton" [("name", "alice")] params 32 - 33 - let test_query_optional_some () = 34 - let params = Runtime.Query.optional ~key:"name" ~value:(Some "alice") in 35 - Alcotest.(check (list (pair string string))) "optional some" [("name", "alice")] params 36 - 37 - let test_query_optional_none () = 38 - let params = Runtime.Query.optional ~key:"name" ~value:None in 39 - Alcotest.(check (list (pair string string))) "optional none" [] params 40 - 41 - let test_query_encode_empty () = 42 - let result = Runtime.Query.encode [] in 43 - Alcotest.(check string) "empty query" "" result 44 - 45 - let test_query_encode_single () = 46 - let result = Runtime.Query.encode [("name", "alice")] in 47 - Alcotest.(check string) "single query" "?name=alice" result 48 - 49 - let test_query_encode_multiple () = 50 - let result = Runtime.Query.encode [("name", "alice"); ("age", "30")] in 51 - Alcotest.(check string) "multiple query" "?name=alice&age=30" result 52 - 53 - let test_query_encode_special_chars () = 54 - let result = Runtime.Query.encode [("q", "hello world")] in 55 - Alcotest.(check string) "special chars" "?q=hello%20world" result 56 - 57 - (** {1 Name Conversion Tests} *) 58 - 59 - let test_snake_case_simple () = 60 - let result = Codegen.Name.to_snake_case "getUserById" in 61 - Alcotest.(check string) "camel to snake" "get_user_by_id" result 62 - 63 - let test_snake_case_with_dashes () = 64 - let result = Codegen.Name.to_snake_case "user-name" in 65 - Alcotest.(check string) "dashes to underscore" "user_name" result 66 - 67 - let test_snake_case_reserved () = 68 - let result = Codegen.Name.to_snake_case "type" in 69 - Alcotest.(check string) "reserved word" "type_" result 70 - 71 - let test_module_name () = 72 - let result = Codegen.Name.to_module_name "user_profile" in 73 - Alcotest.(check string) "module name" "UserProfile" result 74 - 75 - let test_variant_name () = 76 - let result = Codegen.Name.to_variant_name "active_user" in 77 - Alcotest.(check string) "variant name" "Active_user" result 78 - 79 - (** {1 Spec Parsing Tests} *) 80 - 81 - let minimal_spec = {|{ 82 - "openapi": "3.0.0", 83 - "info": { 84 - "title": "Test API", 85 - "version": "1.0.0" 86 - }, 87 - "paths": {} 88 - }|} 89 - 90 - let test_parse_minimal_spec () = 91 - match Spec.of_string minimal_spec with 92 - | Error e -> Alcotest.fail e 93 - | Ok spec -> 94 - Alcotest.(check string) "openapi version" "3.0.0" spec.openapi; 95 - Alcotest.(check string) "title" "Test API" spec.info.title; 96 - Alcotest.(check string) "version" "1.0.0" spec.info.version 97 - 98 - let spec_with_schema = {|{ 99 - "openapi": "3.0.0", 100 - "info": { 101 - "title": "Test API", 102 - "version": "1.0.0" 103 - }, 104 - "paths": {}, 105 - "components": { 106 - "schemas": { 107 - "User": { 108 - "type": "object", 109 - "properties": { 110 - "id": { "type": "integer" }, 111 - "name": { "type": "string" }, 112 - "email": { "type": "string", "format": "email" } 113 - }, 114 - "required": ["id", "name"] 115 - } 116 - } 117 - } 118 - }|} 119 - 120 - let test_parse_schema () = 121 - match Spec.of_string spec_with_schema with 122 - | Error e -> Alcotest.fail e 123 - | Ok spec -> 124 - match spec.components with 125 - | None -> Alcotest.fail "expected components" 126 - | Some c -> 127 - Alcotest.(check int) "schema count" 1 (List.length c.schemas); 128 - match List.assoc_opt "User" c.schemas with 129 - | None -> Alcotest.fail "expected User schema" 130 - | Some (Spec.Ref _) -> Alcotest.fail "expected value not ref" 131 - | Some (Spec.Value s) -> 132 - Alcotest.(check (option string)) "type" (Some "object") s.type_; 133 - Alcotest.(check int) "properties" 3 (List.length s.properties); 134 - Alcotest.(check (list string)) "required" ["id"; "name"] s.required 135 - 136 - let spec_with_enum = {|{ 137 - "openapi": "3.0.0", 138 - "info": { 139 - "title": "Test API", 140 - "version": "1.0.0" 141 - }, 142 - "paths": {}, 143 - "components": { 144 - "schemas": { 145 - "Status": { 146 - "type": "string", 147 - "enum": ["active", "inactive", "pending"] 148 - } 149 - } 150 - } 151 - }|} 152 - 153 - let test_parse_enum () = 154 - match Spec.of_string spec_with_enum with 155 - | Error e -> Alcotest.fail e 156 - | Ok spec -> 157 - match spec.components with 158 - | None -> Alcotest.fail "expected components" 159 - | Some c -> 160 - match List.assoc_opt "Status" c.schemas with 161 - | None -> Alcotest.fail "expected Status schema" 162 - | Some (Spec.Ref _) -> Alcotest.fail "expected value not ref" 163 - | Some (Spec.Value s) -> 164 - match s.enum with 165 - | None -> Alcotest.fail "expected enum" 166 - | Some values -> 167 - Alcotest.(check int) "enum count" 3 (List.length values) 168 - 169 - let spec_with_paths = {|{ 170 - "openapi": "3.0.0", 171 - "info": { 172 - "title": "Test API", 173 - "version": "1.0.0" 174 - }, 175 - "paths": { 176 - "/users": { 177 - "get": { 178 - "operationId": "listUsers", 179 - "summary": "List all users", 180 - "responses": { 181 - "200": { 182 - "description": "Success" 183 - } 184 - } 185 - }, 186 - "post": { 187 - "operationId": "createUser", 188 - "summary": "Create a user", 189 - "responses": { 190 - "201": { 191 - "description": "Created" 192 - } 193 - } 194 - } 195 - }, 196 - "/users/{id}": { 197 - "get": { 198 - "operationId": "getUser", 199 - "parameters": [ 200 - { 201 - "name": "id", 202 - "in": "path", 203 - "required": true, 204 - "schema": { "type": "integer" } 205 - } 206 - ], 207 - "responses": { 208 - "200": { 209 - "description": "Success" 210 - } 211 - } 212 - } 213 - } 214 - } 215 - }|} 216 - 217 - let test_parse_paths () = 218 - match Spec.of_string spec_with_paths with 219 - | Error e -> Alcotest.fail e 220 - | Ok spec -> 221 - Alcotest.(check int) "path count" 2 (List.length spec.paths); 222 - match List.assoc_opt "/users" spec.paths with 223 - | None -> Alcotest.fail "expected /users path" 224 - | Some path_item -> 225 - (match path_item.get with 226 - | None -> Alcotest.fail "expected GET" 227 - | Some op -> 228 - Alcotest.(check (option string)) "operation id" (Some "listUsers") op.operation_id); 229 - (match path_item.post with 230 - | None -> Alcotest.fail "expected POST" 231 - | Some op -> 232 - Alcotest.(check (option string)) "operation id" (Some "createUser") op.operation_id) 233 - 234 - (** {1 Code Generation Tests} *) 235 - 236 - let contains_substring s sub = 237 - let len_s = String.length s in 238 - let len_sub = String.length sub in 239 - if len_sub > len_s then false 240 - else 241 - let rec check i = 242 - if i > len_s - len_sub then false 243 - else if String.sub s i len_sub = sub then true 244 - else check (i + 1) 245 - in 246 - check 0 247 - 248 - let test_split_schema_name () = 249 - let p, s = Codegen.Name.split_schema_name "AlbumResponseDto" in 250 - Alcotest.(check string) "prefix" "Album" p; 251 - Alcotest.(check string) "suffix" "ResponseDto" s 252 - 253 - let test_split_schema_name_no_suffix () = 254 - let p, s = Codegen.Name.split_schema_name "User" in 255 - Alcotest.(check string) "prefix" "User" p; 256 - Alcotest.(check string) "suffix" "T" s 257 - 258 - let test_generate_files () = 259 - match Spec.of_string spec_with_schema with 260 - | Error e -> Alcotest.fail e 261 - | Ok spec -> 262 - let config = Codegen.{ 263 - output_dir = "."; 264 - package_name = "test_api"; 265 - spec_path = None; 266 - } in 267 - let files = Codegen.generate ~config spec in 268 - Alcotest.(check int) "file count" 4 (List.length files); 269 - let ml = List.assoc_opt "test_api.ml" files in 270 - (match ml with 271 - | None -> Alcotest.fail "missing .ml file" 272 - | Some content -> 273 - Alcotest.(check bool) "contains module User" true 274 - (contains_substring content "module User")) 275 - 276 - let test_generate_enum_schema () = 277 - match Spec.of_string spec_with_enum with 278 - | Error e -> Alcotest.fail e 279 - | Ok spec -> 280 - let config = Codegen.{ 281 - output_dir = "."; 282 - package_name = "test_enum"; 283 - spec_path = None; 284 - } in 285 - let files = Codegen.generate ~config spec in 286 - let ml = List.assoc_opt "test_enum.ml" files in 287 - (match ml with 288 - | None -> Alcotest.fail "missing .ml file" 289 - | Some content -> 290 - Alcotest.(check bool) "contains Active variant" true 291 - (contains_substring content "Active")) 292 - 293 - (** {1 Test Suites} *) 294 - 295 - let path_tests = [ 296 - "render simple", `Quick, test_path_render_simple; 297 - "render one param", `Quick, test_path_render_one_param; 298 - "render multiple params", `Quick, test_path_render_multiple_params; 299 - "extract parameters", `Quick, test_path_parameters; 300 - ] 301 - 302 - let query_tests = [ 303 - "singleton", `Quick, test_query_singleton; 304 - "optional some", `Quick, test_query_optional_some; 305 - "optional none", `Quick, test_query_optional_none; 306 - "encode empty", `Quick, test_query_encode_empty; 307 - "encode single", `Quick, test_query_encode_single; 308 - "encode multiple", `Quick, test_query_encode_multiple; 309 - "encode special chars", `Quick, test_query_encode_special_chars; 310 - ] 311 - 312 - let name_tests = [ 313 - "snake case simple", `Quick, test_snake_case_simple; 314 - "snake case dashes", `Quick, test_snake_case_with_dashes; 315 - "snake case reserved", `Quick, test_snake_case_reserved; 316 - "module name", `Quick, test_module_name; 317 - "variant name", `Quick, test_variant_name; 318 - ] 319 - 320 - let spec_tests = [ 321 - "parse minimal", `Quick, test_parse_minimal_spec; 322 - "parse schema", `Quick, test_parse_schema; 323 - "parse enum", `Quick, test_parse_enum; 324 - "parse paths", `Quick, test_parse_paths; 325 - ] 326 - 327 - let codegen_tests = [ 328 - "split schema name", `Quick, test_split_schema_name; 329 - "split schema name no suffix", `Quick, test_split_schema_name_no_suffix; 330 - "generate files", `Quick, test_generate_files; 331 - "generate enum schema", `Quick, test_generate_enum_schema; 332 - ] 333 - 334 - let () = 335 - Alcotest.run "openapi" [ 336 - "Path", path_tests; 337 - "Query", query_tests; 338 - "Name", name_tests; 339 - "Spec", spec_tests; 340 - "Codegen", codegen_tests; 341 - ]