My aggregated monorepo of OCaml code, automaintained
0
fork

Configure Feed

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

Add ocaml-openapi: OpenAPI code generator for OCaml

A code generator that produces type-safe OCaml API clients from
OpenAPI 3.x specifications, using:
- jsont for JSON encoding/decoding
- requests for HTTP client (Eio-based)
- ptime for date-time handling

Features:
- Generates typed request bodies and responses
- Produces .mli interface files
- Creates wrapped module structure (e.g., Api.Types, Api.Client)
- Optional dune.inc regeneration rules (--regen flag)
- OCamldoc comments from OpenAPI descriptions
- Topological sorting of schema dependencies

Generated code structure:
- types.ml/mli: Type definitions with jsont codecs
- client.ml/mli: API client functions
- <pkg>.ml/mli: Wrapped main module
- dune/dune.inc: Build configuration

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

+2675
+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)
+2
ocaml-openapi/dune-project
··· 1 + (lang dune 3.0) 2 + (name openapi)
+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}}}
+11
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 + *) 8 + 9 + module Spec = Openapi_spec 10 + module Codegen = Openapi_codegen 11 + module Runtime = Openapi_runtime
+797
ocaml-openapi/lib/openapi_codegen.ml
··· 1 + (** Code generation from OpenAPI specifications. 2 + 3 + This module generates OCaml code from parsed OpenAPI specs: 4 + - Type definitions with jsont codecs 5 + - Client functions using the requests library 6 + - Interface files (.mli) 7 + - Dune build rules with regeneration support 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 + (** OCaml reserved keywords that need escaping *) 18 + let ocaml_keywords = StringSet.of_list [ 19 + "and"; "as"; "assert"; "asr"; "begin"; "class"; "constraint"; "do"; "done"; 20 + "downto"; "else"; "end"; "exception"; "external"; "false"; "for"; "fun"; 21 + "function"; "functor"; "if"; "in"; "include"; "inherit"; "initializer"; 22 + "land"; "lazy"; "let"; "lor"; "lsl"; "lsr"; "lxor"; "match"; "method"; 23 + "mod"; "module"; "mutable"; "new"; "nonrec"; "object"; "of"; "open"; "or"; 24 + "private"; "rec"; "sig"; "struct"; "then"; "to"; "true"; "try"; "type"; 25 + "val"; "virtual"; "when"; "while"; "with" 26 + ] 27 + 28 + let escape_keyword s = 29 + if StringSet.mem s ocaml_keywords then s ^ "_" else s 30 + 31 + (** Convert a string to a valid OCaml identifier (snake_case) *) 32 + let to_snake_case s = 33 + let buf = Buffer.create (String.length s) in 34 + let prev_upper = ref false in 35 + String.iteri (fun i c -> 36 + match c with 37 + | 'A'..'Z' -> 38 + if i > 0 && not !prev_upper then Buffer.add_char buf '_'; 39 + Buffer.add_char buf (Char.lowercase_ascii c); 40 + prev_upper := true 41 + | 'a'..'z' | '0'..'9' | '_' -> 42 + Buffer.add_char buf c; 43 + prev_upper := false 44 + | '-' | ' ' | '.' | '/' -> 45 + Buffer.add_char buf '_'; 46 + prev_upper := false 47 + | _ -> 48 + prev_upper := false 49 + ) s; 50 + escape_keyword (Buffer.contents buf) 51 + 52 + (** Convert to a module name (PascalCase) *) 53 + let to_module_name s = 54 + let snake = to_snake_case s in 55 + let parts = String.split_on_char '_' snake in 56 + String.concat "" (List.map String.capitalize_ascii parts) 57 + 58 + (** Convert to a type name (lowercase) *) 59 + let to_type_name s = 60 + String.lowercase_ascii (to_snake_case s) 61 + 62 + (** Convert to a variant constructor (capitalize first letter) *) 63 + let to_variant_name s = 64 + let snake = to_snake_case s in 65 + String.capitalize_ascii snake 66 + 67 + (** Generate an operation function name from method and path *) 68 + let operation_name ~(method_ : string) ~(path : string) ~(operation_id : string option) = 69 + match operation_id with 70 + | Some id -> to_snake_case id 71 + | None -> 72 + let method_name = String.lowercase_ascii method_ in 73 + let path_parts = String.split_on_char '/' path 74 + |> List.filter (fun s -> s <> "" && not (String.length s > 0 && s.[0] = '{')) 75 + in 76 + let path_name = String.concat "_" (List.map to_snake_case path_parts) in 77 + method_name ^ "_" ^ path_name 78 + end 79 + 80 + (** {1 OCamldoc Helpers} *) 81 + 82 + (** Format a description string as an OCamldoc comment *) 83 + let format_doc ?(indent=0) description = 84 + let prefix = String.make indent ' ' in 85 + match description with 86 + | None -> "" 87 + | Some "" -> "" 88 + | Some desc -> 89 + (* Escape special characters and wrap lines *) 90 + let escaped = String.concat "\\}" (String.split_on_char '}' desc) in 91 + let escaped = String.concat "\\{" (String.split_on_char '{' escaped) in 92 + Printf.sprintf "%s(** %s *)\n" prefix escaped 93 + 94 + (** Format a short inline doc *) 95 + let format_inline_doc description = 96 + match description with 97 + | None -> "" 98 + | Some "" -> "" 99 + | Some desc -> 100 + let escaped = String.concat "\\}" (String.split_on_char '}' desc) in 101 + let escaped = String.concat "\\{" (String.split_on_char '{' escaped) in 102 + Printf.sprintf " (** %s *)" escaped 103 + 104 + (** {1 JSON Helpers} *) 105 + 106 + let json_string = function 107 + | Jsont.String (s, _) -> Some s 108 + | _ -> None 109 + 110 + let json_object = function 111 + | Jsont.Object (mems, _) -> Some mems 112 + | _ -> None 113 + 114 + let get_ref json = 115 + match json_object json with 116 + | Some mems -> 117 + List.find_map (fun ((n, _), v) -> 118 + if n = "$ref" then json_string v else None 119 + ) mems 120 + | None -> None 121 + 122 + let get_member name json = 123 + match json_object json with 124 + | Some mems -> 125 + List.find_map (fun ((n, _), v) -> 126 + if n = name then Some v else None 127 + ) mems 128 + | None -> None 129 + 130 + let get_string_member name json = 131 + Option.bind (get_member name json) json_string 132 + 133 + (** {1 Dependency Analysis and Topological Sort} *) 134 + 135 + let schema_name_from_ref (ref_ : string) : string option = 136 + match String.split_on_char '/' ref_ with 137 + | ["#"; "components"; "schemas"; name] -> Some name 138 + | _ -> None 139 + 140 + let rec find_refs_in_json (json : Jsont.json) : string list = 141 + match json with 142 + | Jsont.Object (mems, _) -> 143 + let ref_opt = List.find_map (fun ((n, _), v) -> 144 + if n = "$ref" then json_string v else None 145 + ) mems in 146 + (match ref_opt with 147 + | Some ref_ -> Option.to_list (schema_name_from_ref ref_) 148 + | None -> List.concat_map (fun (_, v) -> find_refs_in_json v) mems) 149 + | Jsont.Array (items, _) -> List.concat_map find_refs_in_json items 150 + | _ -> [] 151 + 152 + let find_schema_dependencies (schema : Spec.schema) : string list = 153 + let from_properties = 154 + List.concat_map (fun (_, json) -> find_refs_in_json json) schema.properties 155 + in 156 + let refs_from_list = Option.fold ~none:[] ~some:(List.concat_map find_refs_in_json) in 157 + let from_items = Option.fold ~none:[] ~some:find_refs_in_json schema.items in 158 + List.sort_uniq String.compare 159 + (from_properties @ from_items @ refs_from_list schema.all_of 160 + @ refs_from_list schema.one_of @ refs_from_list schema.any_of) 161 + 162 + let topological_sort_schemas (schemas : (string * Spec.schema Spec.or_ref) list) 163 + : (string * Spec.schema Spec.or_ref) list = 164 + let deps = List.filter_map (fun (name, schema_or_ref) -> 165 + match schema_or_ref with 166 + | Spec.Ref _ -> None 167 + | Spec.Value schema -> Some (name, find_schema_dependencies schema) 168 + ) schemas in 169 + 170 + let all_names = List.map fst schemas in 171 + let name_set = List.sort_uniq String.compare all_names in 172 + 173 + let in_degree name = 174 + match List.assoc_opt name deps with 175 + | None -> 0 176 + | Some dep_list -> List.length (List.filter (fun d -> List.mem d name_set) dep_list) 177 + in 178 + 179 + let rec process queue result in_degrees = 180 + match queue with 181 + | [] -> List.rev result 182 + | name :: rest -> 183 + let new_zero = List.filter_map (fun (dependent, dep_list) -> 184 + if List.mem name dep_list then 185 + let new_deg = List.assoc dependent in_degrees - 1 in 186 + if new_deg = 0 then Some dependent else None 187 + else None 188 + ) deps in 189 + let updated_degrees = List.map (fun (n, d) -> 190 + if List.exists (fun (dep, dl) -> n = dep && List.mem name dl) deps 191 + then (n, d - 1) else (n, d) 192 + ) in_degrees in 193 + process (rest @ new_zero) (name :: result) updated_degrees 194 + in 195 + 196 + let initial_degrees = List.map (fun n -> (n, in_degree n)) name_set in 197 + let initial_queue = List.filter (fun n -> in_degree n = 0) name_set in 198 + let sorted_names = process initial_queue [] initial_degrees in 199 + let remaining = List.filter (fun n -> not (List.mem n sorted_names)) name_set in 200 + let all_sorted = sorted_names @ remaining in 201 + List.filter_map (fun name -> 202 + List.find_opt (fun (n, _) -> n = name) schemas 203 + ) all_sorted 204 + 205 + (** {1 Type Generation} *) 206 + 207 + module TypeGen = struct 208 + let type_of_ref (ref_ : string) : string = 209 + match String.split_on_char '/' ref_ with 210 + | ["#"; "components"; "schemas"; name] -> Name.to_module_name name ^ ".t" 211 + | _ -> "Jsont.json" 212 + 213 + let rec jsont_of_type = function 214 + | "string" -> "Jsont.string" 215 + | "int" -> "Jsont.int" 216 + | "int32" -> "Jsont.int32" 217 + | "int64" -> "Jsont.int64" 218 + | "float" -> "Jsont.number" 219 + | "bool" -> "Jsont.bool" 220 + | "Ptime.t" -> "Openapi.Runtime.ptime_jsont" 221 + | "Jsont.json" -> "Jsont.json" 222 + | s when String.ends_with ~suffix:" list" s -> 223 + let elem = String.sub s 0 (String.length s - 5) in 224 + Printf.sprintf "Jsont.(list %s)" (jsont_of_type elem) 225 + | s when String.ends_with ~suffix:".t" s -> 226 + let module_name = String.sub s 0 (String.length s - 2) in 227 + module_name ^ ".t_jsont" 228 + | _ -> "Jsont.json" 229 + 230 + (** Get type and description from a JSON property schema *) 231 + let property_info json : (string * bool * string option) = 232 + let description = get_string_member "description" json in 233 + match get_ref json with 234 + | Some ref_ -> (type_of_ref ref_, false, description) 235 + | None -> 236 + match get_string_member "type" json with 237 + | Some "string" -> 238 + let ty = match get_string_member "format" json with 239 + | Some "date-time" -> "Ptime.t" 240 + | _ -> "string" 241 + in (ty, false, description) 242 + | Some "integer" -> 243 + let ty = match get_string_member "format" json with 244 + | Some "int64" -> "int64" 245 + | Some "int32" -> "int32" 246 + | _ -> "int" 247 + in (ty, false, description) 248 + | Some "number" -> ("float", false, description) 249 + | Some "boolean" -> ("bool", false, description) 250 + | Some "array" -> 251 + (match get_member "items" json with 252 + | Some items_json -> 253 + (match get_ref items_json with 254 + | Some ref_ -> (type_of_ref ref_ ^ " list", false, description) 255 + | None -> 256 + match get_string_member "type" items_json with 257 + | Some "string" -> ("string list", false, description) 258 + | Some "integer" -> ("int list", false, description) 259 + | Some "number" -> ("float list", false, description) 260 + | Some "boolean" -> ("bool list", false, description) 261 + | _ -> ("Jsont.json list", false, description)) 262 + | None -> ("Jsont.json list", false, description)) 263 + | Some "object" -> ("Jsont.json", false, description) 264 + | _ -> 265 + let nullable = match get_member "nullable" json with 266 + | Some (Jsont.Bool (b, _)) -> b 267 + | _ -> false 268 + in ("Jsont.json", nullable, description) 269 + 270 + (** Generate an enum type with OCamldoc *) 271 + let gen_enum_type ~name ~description (values : Jsont.json list) : string * string = 272 + let variants = List.filter_map (fun json -> 273 + match json with 274 + | Jsont.String (s, _) -> Some (Name.to_variant_name s, s) 275 + | _ -> None 276 + ) values in 277 + if variants = [] then 278 + let impl = Printf.sprintf "%stype %s = string\n\nlet %s_jsont = Jsont.string" 279 + (format_doc description) name name in 280 + let intf = Printf.sprintf "%stype %s = string\n\nval %s_jsont : %s Jsont.t" 281 + (format_doc description) name name name in 282 + (impl, intf) 283 + else 284 + let type_def = Printf.sprintf "%stype %s =\n%s\n" 285 + (format_doc description) name 286 + (String.concat "\n" (List.map (fun (v, _) -> " | " ^ v) variants)) 287 + in 288 + let dec_cases = String.concat "\n" (List.map (fun (v, raw) -> 289 + Printf.sprintf " | %S -> %s" raw v 290 + ) variants) in 291 + let enc_cases = String.concat "\n" (List.map (fun (v, raw) -> 292 + Printf.sprintf " | %s -> %S" v raw 293 + ) variants) in 294 + let impl = Printf.sprintf "%s\nlet %s_jsont : %s Jsont.t =\n Jsont.map Jsont.string ~kind:%S\n ~dec:(function\n%s\n | s -> Jsont.Error.msgf Jsont.Meta.none \"Unknown %s: %%s\" s)\n ~enc:(function\n%s)" 295 + type_def name name name dec_cases name enc_cases 296 + in 297 + let intf = Printf.sprintf "%s\nval %s_jsont : %s Jsont.t" 298 + type_def name name in 299 + (impl, intf) 300 + 301 + (** Generate a record type with OCamldoc *) 302 + let gen_record_type ~name ~description (schema : Spec.schema) : string * string = 303 + let fields = List.map (fun (field_name, field_json) -> 304 + let ocaml_name = Name.to_snake_case field_name in 305 + let is_required = List.mem field_name schema.required in 306 + let (base_type, json_nullable, field_desc) = property_info field_json in 307 + let is_nullable = json_nullable || not is_required in 308 + let ocaml_type = if is_nullable then base_type ^ " option" else base_type in 309 + (ocaml_name, field_name, ocaml_type, base_type, is_nullable, is_required, field_desc) 310 + ) schema.properties in 311 + 312 + if fields = [] then 313 + let impl = Printf.sprintf "%stype %s = Jsont.json\n\nlet %s_jsont = Jsont.json" 314 + (format_doc description) name name in 315 + let intf = Printf.sprintf "%stype %s = Jsont.json\n\nval %s_jsont : %s Jsont.t" 316 + (format_doc description) name name name in 317 + (impl, intf) 318 + else 319 + let type_fields = String.concat "\n" (List.map 320 + (fun (ocaml_name, _, ocaml_type, _, _, _, field_desc) -> 321 + Printf.sprintf " %s : %s;%s" ocaml_name ocaml_type 322 + (format_inline_doc field_desc) 323 + ) fields) 324 + in 325 + let type_def = Printf.sprintf "%stype %s = {\n%s\n}" 326 + (format_doc description) name type_fields 327 + in 328 + 329 + let make_params = String.concat " " (List.map (fun (n, _, _, _, _, _, _) -> n) fields) in 330 + let make_body = String.concat "; " (List.map (fun (n, _, _, _, _, _, _) -> n) fields) in 331 + 332 + let jsont_members = String.concat "\n" (List.map 333 + (fun (ocaml_name, json_name, _, base_type, is_nullable, is_required, _) -> 334 + let codec = jsont_of_type base_type in 335 + if is_nullable then 336 + if is_required then 337 + Printf.sprintf " |> Jsont.Object.mem %S (Jsont.option %s)\n ~dec_absent:None ~enc_omit:Option.is_none ~enc:(fun r -> r.%s)" 338 + json_name codec ocaml_name 339 + else 340 + Printf.sprintf " |> Jsont.Object.opt_mem %S %s ~enc:(fun r -> r.%s)" 341 + json_name codec ocaml_name 342 + else 343 + Printf.sprintf " |> Jsont.Object.mem %S %s ~enc:(fun r -> r.%s)" 344 + json_name codec ocaml_name 345 + ) fields) 346 + in 347 + 348 + let impl = Printf.sprintf "%s\n\nlet %s_jsont : %s Jsont.t =\n Jsont.Object.map ~kind:%S\n (fun %s -> { %s })\n%s\n |> Jsont.Object.skip_unknown\n |> Jsont.Object.finish" 349 + type_def name name name make_params make_body jsont_members 350 + in 351 + let intf = Printf.sprintf "%s\n\nval %s_jsont : %s Jsont.t" type_def name name in 352 + (impl, intf) 353 + 354 + let primitive_type_code ~description = function 355 + | Some "string" -> 356 + (Printf.sprintf "%stype t = string\nlet t_jsont = Jsont.string" (format_doc description), 357 + Printf.sprintf "%stype t = string\nval t_jsont : t Jsont.t" (format_doc description)) 358 + | Some "integer" -> 359 + (Printf.sprintf "%stype t = int\nlet t_jsont = Jsont.int" (format_doc description), 360 + Printf.sprintf "%stype t = int\nval t_jsont : t Jsont.t" (format_doc description)) 361 + | Some "number" -> 362 + (Printf.sprintf "%stype t = float\nlet t_jsont = Jsont.number" (format_doc description), 363 + Printf.sprintf "%stype t = float\nval t_jsont : t Jsont.t" (format_doc description)) 364 + | Some "boolean" -> 365 + (Printf.sprintf "%stype t = bool\nlet t_jsont = Jsont.bool" (format_doc description), 366 + Printf.sprintf "%stype t = bool\nval t_jsont : t Jsont.t" (format_doc description)) 367 + | Some "array" -> 368 + (Printf.sprintf "%stype t = Jsont.json list\nlet t_jsont = Jsont.(list json)" (format_doc description), 369 + Printf.sprintf "%stype t = Jsont.json list\nval t_jsont : t Jsont.t" (format_doc description)) 370 + | _ -> 371 + (Printf.sprintf "%stype t = Jsont.json\nlet t_jsont = Jsont.json" (format_doc description), 372 + Printf.sprintf "%stype t = Jsont.json\nval t_jsont : t Jsont.t" (format_doc description)) 373 + 374 + (** Generate code and interface for a schema *) 375 + let code_for_schema (schema : Spec.schema) : string * string = 376 + let desc = schema.description in 377 + match schema.enum with 378 + | Some values -> gen_enum_type ~name:"t" ~description:desc values 379 + | None when schema.properties <> [] -> gen_record_type ~name:"t" ~description:desc schema 380 + | None when Option.is_some schema.one_of -> 381 + let doc = format_doc desc in 382 + (Printf.sprintf "%s(* oneOf variant - using JSON *)\ntype t = Jsont.json\nlet t_jsont = Jsont.json" doc, 383 + Printf.sprintf "%s(* oneOf variant *)\ntype t = Jsont.json\nval t_jsont : t Jsont.t" doc) 384 + | None when Option.is_some schema.all_of -> 385 + let doc = format_doc desc in 386 + (Printf.sprintf "%s(* allOf - using JSON *)\ntype t = Jsont.json\nlet t_jsont = Jsont.json" doc, 387 + Printf.sprintf "%s(* allOf *)\ntype t = Jsont.json\nval t_jsont : t Jsont.t" doc) 388 + | None -> primitive_type_code ~description:desc schema.type_ 389 + 390 + (** Generate types for all schemas in components *) 391 + let generate_types (spec : Spec.t) : (string * string * string) list = 392 + match spec.components with 393 + | None -> [] 394 + | Some components -> 395 + let sorted_schemas = topological_sort_schemas components.schemas in 396 + List.filter_map (fun (name, (schema_or_ref : Spec.schema Spec.or_ref)) -> 397 + match schema_or_ref with 398 + | Spec.Ref _ -> None 399 + | Spec.Value (schema : Spec.schema) -> 400 + let (impl, intf) = code_for_schema schema in 401 + Some (Name.to_module_name name, impl, intf) 402 + ) sorted_schemas 403 + end 404 + 405 + (** {1 Client Generation} *) 406 + 407 + module ClientGen = struct 408 + (** Type information for request/response *) 409 + type type_info = { 410 + ocaml_type : string; (** e.g., "Types.User.t" or "Jsont.json" *) 411 + codec : string; (** e.g., "Types.User.t_jsont" or "Jsont.json" *) 412 + } 413 + 414 + let json_type_info = { ocaml_type = "Jsont.json"; codec = "Jsont.json" } 415 + 416 + (** Extract type info from a $ref string *) 417 + let type_info_of_ref (ref_ : string) : type_info option = 418 + match String.split_on_char '/' ref_ with 419 + | ["#"; "components"; "schemas"; name] -> 420 + let module_name = Name.to_module_name name in 421 + Some { ocaml_type = "Types." ^ module_name ^ ".t"; 422 + codec = "Types." ^ module_name ^ ".t_jsont" } 423 + | _ -> None 424 + 425 + (** Extract type info from a schema or_ref *) 426 + let type_info_of_schema_or_ref (sor : Spec.schema Spec.or_ref) : type_info = 427 + match sor with 428 + | Spec.Ref ref_ -> Option.value ~default:json_type_info (type_info_of_ref ref_) 429 + | Spec.Value schema -> 430 + (* Check if it's an array of refs *) 431 + match schema.type_, schema.items with 432 + | Some "array", Some items_json -> 433 + (match get_ref items_json with 434 + | Some ref_ -> 435 + (match type_info_of_ref ref_ with 436 + | Some ti -> { ocaml_type = ti.ocaml_type ^ " list"; 437 + codec = Printf.sprintf "(Jsont.list %s)" ti.codec } 438 + | None -> json_type_info) 439 + | None -> json_type_info) 440 + | _ -> json_type_info 441 + 442 + (** Get response type from operation responses *) 443 + let response_type_info (responses : Spec.responses) : type_info = 444 + let find_json_schema (content : (string * Spec.media_type) list) = 445 + List.find_map (fun (content_type, (media : Spec.media_type)) -> 446 + if String.length content_type >= 16 && 447 + String.sub content_type 0 16 = "application/json" then 448 + Option.map type_info_of_schema_or_ref media.schema 449 + else None 450 + ) content 451 + in 452 + (* Try 200, 201, then default *) 453 + let try_status status = 454 + List.find_map (fun (code, resp_or_ref) -> 455 + if code = status then 456 + match resp_or_ref with 457 + | Spec.Ref _ -> None 458 + | Spec.Value (resp : Spec.response) -> find_json_schema resp.content 459 + else None 460 + ) responses.responses 461 + in 462 + Option.value ~default:json_type_info 463 + (match try_status "200" with 464 + | Some ti -> Some ti 465 + | None -> match try_status "201" with 466 + | Some ti -> Some ti 467 + | None -> match responses.default with 468 + | Some (Spec.Value (resp : Spec.response)) -> find_json_schema resp.content 469 + | _ -> None) 470 + 471 + (** Get request body type from operation *) 472 + let request_body_type_info (rb : Spec.request_body Spec.or_ref option) : type_info option = 473 + match rb with 474 + | None -> None 475 + | Some (Spec.Ref _) -> Some json_type_info 476 + | Some (Spec.Value (rb : Spec.request_body)) -> 477 + (* Find application/json content *) 478 + let ti = List.find_map (fun (content_type, (media : Spec.media_type)) -> 479 + if String.length content_type >= 16 && 480 + String.sub content_type 0 16 = "application/json" then 481 + Option.map type_info_of_schema_or_ref media.schema 482 + else None 483 + ) rb.content in 484 + Some (Option.value ~default:json_type_info ti) 485 + 486 + type operation_info = { 487 + func_name : string; 488 + description : string option; 489 + path_params : (string * string * bool) list; (* ocaml_name, json_name, required *) 490 + query_params : (string * string * bool) list; 491 + body_type : type_info option; 492 + response_type : type_info; 493 + method_ : string; 494 + path : string; 495 + } 496 + 497 + let collect_operation_info ~path ~method_ (op : Spec.operation) : operation_info = 498 + let func_name = Name.operation_name ~method_ ~path ~operation_id:op.operation_id in 499 + let params = List.filter_map (fun (param_or_ref : Spec.parameter Spec.or_ref) -> 500 + match param_or_ref with 501 + | Spec.Value (p : Spec.parameter) -> Some p 502 + | Spec.Ref _ -> None 503 + ) op.parameters in 504 + 505 + let path_params = List.filter_map (fun (p : Spec.parameter) -> 506 + if p.in_ = Spec.Path then Some (Name.to_snake_case p.name, p.name, p.required) 507 + else None 508 + ) params in 509 + 510 + let query_params = List.filter_map (fun (p : Spec.parameter) -> 511 + if p.in_ = Spec.Query then Some (Name.to_snake_case p.name, p.name, p.required) 512 + else None 513 + ) params in 514 + 515 + let method_lower = String.lowercase_ascii method_ in 516 + let supports_body = method_lower = "post" || method_lower = "put" || method_lower = "patch" in 517 + let body_type = 518 + if supports_body then request_body_type_info op.request_body 519 + else None 520 + in 521 + let response_type = response_type_info op.responses in 522 + 523 + let description = match op.summary, op.description with 524 + | Some s, Some d -> Some (s ^ "\n\n" ^ d) 525 + | Some s, None -> Some s 526 + | None, Some d -> Some d 527 + | None, None -> None 528 + in 529 + 530 + { func_name; description; path_params; query_params; body_type; response_type; method_; path } 531 + 532 + (** Generate function implementation *) 533 + let gen_operation_impl (info : operation_info) : string = 534 + let path_args = List.map (fun (n, _, _) -> Printf.sprintf "~%s" n) info.path_params in 535 + let query_args = List.map (fun (n, _, req) -> 536 + if req then Printf.sprintf "~%s" n else Printf.sprintf "?%s" n 537 + ) info.query_params in 538 + let body_arg = match info.body_type with Some _ -> ["~body"] | None -> [] in 539 + let all_args = path_args @ query_args @ body_arg @ ["t"; "()"] in 540 + 541 + let path_render = 542 + if info.path_params = [] then Printf.sprintf "%S" info.path 543 + else 544 + let bindings = List.map (fun (ocaml, json, _) -> 545 + Printf.sprintf "(%S, %s)" json ocaml 546 + ) info.path_params in 547 + Printf.sprintf "Openapi.Runtime.Path.render ~params:[%s] %S" 548 + (String.concat "; " bindings) info.path 549 + in 550 + 551 + let query_build = 552 + if info.query_params = [] then "\"\"" 553 + else 554 + let parts = List.map (fun (ocaml, json, req) -> 555 + if req then 556 + Printf.sprintf "Openapi.Runtime.Query.singleton ~key:%S ~value:%s" json ocaml 557 + else 558 + Printf.sprintf "Openapi.Runtime.Query.optional ~key:%S ~value:%s" json ocaml 559 + ) info.query_params in 560 + Printf.sprintf "Openapi.Runtime.Query.encode (List.concat [\n %s\n ])" 561 + (String.concat ";\n " parts) 562 + in 563 + 564 + let method_lower = String.lowercase_ascii info.method_ in 565 + let http_call = match info.body_type with 566 + | Some ti -> 567 + Printf.sprintf "Requests.%s t.session ~body:(Requests.Body.json (Openapi.Runtime.Json.encode_json %s body)) url" 568 + method_lower ti.codec 569 + | None -> 570 + Printf.sprintf "Requests.%s t.session url" method_lower 571 + in 572 + 573 + let decode_response = 574 + if info.response_type.codec = "Jsont.json" then 575 + "Requests.Response.json response" 576 + else 577 + Printf.sprintf "Openapi.Runtime.Json.decode_json_exn %s (Requests.Response.json response)" 578 + info.response_type.codec 579 + in 580 + 581 + Printf.sprintf "%slet %s %s =\n let url_path = %s in\n let query = %s in\n let url = t.base_url ^ url_path ^ query in\n let response = %s in\n if Requests.Response.ok response then\n %s\n else\n failwith (Printf.sprintf \"HTTP %%d: %%s\" (Requests.Response.status_code response) (Requests.Response.text response))" 582 + (format_doc info.description) 583 + info.func_name 584 + (String.concat " " all_args) 585 + path_render query_build http_call decode_response 586 + 587 + (** Generate function signature for mli *) 588 + let gen_operation_sig (info : operation_info) : string = 589 + let path_args = List.map (fun (n, _, _) -> Printf.sprintf "%s:string" n) info.path_params in 590 + let query_args = List.map (fun (n, _, req) -> 591 + if req then Printf.sprintf "%s:string" n else Printf.sprintf "?%s:string" n 592 + ) info.query_params in 593 + let body_arg = match info.body_type with 594 + | Some ti -> [Printf.sprintf "body:%s" ti.ocaml_type] 595 + | None -> [] 596 + in 597 + let all_args = path_args @ query_args @ body_arg @ ["t"; "unit"; info.response_type.ocaml_type] in 598 + 599 + Printf.sprintf "%sval %s : %s" 600 + (format_doc info.description) 601 + info.func_name 602 + (String.concat " -> " all_args) 603 + end 604 + 605 + (** {1 Full Generation} *) 606 + 607 + type config = { 608 + output_dir : string; 609 + package_name : string; 610 + spec_path : string option; (** Path to spec for regeneration rule *) 611 + } 612 + 613 + let generate_types_ml (spec : Spec.t) : string = 614 + let types = TypeGen.generate_types spec in 615 + let api_desc = Option.value ~default:"Generated types from OpenAPI spec." 616 + spec.info.description in 617 + let modules = List.map (fun (name, impl, _) -> 618 + Printf.sprintf "module %s = struct\n%s\nend" name 619 + (String.split_on_char '\n' impl |> List.map (fun l -> " " ^ l) |> String.concat "\n") 620 + ) types in 621 + Printf.sprintf "(** %s\n\n @version %s *)\n\n%s\n" 622 + api_desc spec.info.version (String.concat "\n\n" modules) 623 + 624 + let generate_types_mli (spec : Spec.t) : string = 625 + let types = TypeGen.generate_types spec in 626 + let api_desc = Option.value ~default:"Generated types from OpenAPI spec." 627 + spec.info.description in 628 + let modules = List.map (fun (name, _, intf) -> 629 + Printf.sprintf "module %s : sig\n%s\nend" name 630 + (String.split_on_char '\n' intf |> List.map (fun l -> " " ^ l) |> String.concat "\n") 631 + ) types in 632 + Printf.sprintf "(** %s\n\n @version %s *)\n\n%s\n" 633 + api_desc spec.info.version (String.concat "\n\n" modules) 634 + 635 + let generate_client_ml (spec : Spec.t) : string = 636 + let operations = List.concat_map (fun (path, path_item) -> 637 + let ops = [ 638 + ("GET", path_item.Spec.get); ("POST", path_item.post); ("PUT", path_item.put); 639 + ("DELETE", path_item.delete); ("PATCH", path_item.patch); 640 + ("HEAD", path_item.head); ("OPTIONS", path_item.options); 641 + ] in 642 + List.filter_map (fun (method_, op_opt) -> 643 + Option.map (fun op -> ClientGen.collect_operation_info ~path ~method_ op) op_opt 644 + ) ops 645 + ) spec.paths in 646 + 647 + let funcs = List.map ClientGen.gen_operation_impl operations in 648 + let api_desc = Option.value ~default:"Generated API client." spec.info.description in 649 + 650 + Printf.sprintf {|(** %s 651 + 652 + @version %s *) 653 + 654 + (** Client connection type *) 655 + type t = { 656 + session : Requests.t; 657 + base_url : string; 658 + } 659 + 660 + (** Create a new API client. 661 + @param session Optional existing requests session 662 + @param sw Eio switch for managing resources 663 + @param env Eio environment 664 + @param base_url Base URL for the API *) 665 + let create ?session ~sw env ~base_url = 666 + let session = match session with 667 + | Some s -> s 668 + | None -> Requests.create ~sw env 669 + in 670 + { session; base_url } 671 + 672 + (** Get the base URL *) 673 + let base_url t = t.base_url 674 + 675 + (** Get the underlying requests session *) 676 + let session t = t.session 677 + 678 + %s 679 + |} api_desc spec.info.version (String.concat "\n\n" funcs) 680 + 681 + let generate_client_mli (spec : Spec.t) : string = 682 + let operations = List.concat_map (fun (path, path_item) -> 683 + let ops = [ 684 + ("GET", path_item.Spec.get); ("POST", path_item.post); ("PUT", path_item.put); 685 + ("DELETE", path_item.delete); ("PATCH", path_item.patch); 686 + ("HEAD", path_item.head); ("OPTIONS", path_item.options); 687 + ] in 688 + List.filter_map (fun (method_, op_opt) -> 689 + Option.map (fun op -> ClientGen.collect_operation_info ~path ~method_ op) op_opt 690 + ) ops 691 + ) spec.paths in 692 + 693 + let sigs = List.map ClientGen.gen_operation_sig operations in 694 + let api_desc = Option.value ~default:"Generated API client." spec.info.description in 695 + 696 + Printf.sprintf {|(** %s 697 + 698 + @version %s *) 699 + 700 + (** Client connection type *) 701 + type t 702 + 703 + (** Create a new API client. 704 + @param session Optional existing requests session 705 + @param sw Eio switch for managing resources 706 + @param env Eio environment 707 + @param base_url Base URL for the API *) 708 + val create : 709 + ?session:Requests.t -> 710 + sw:Eio.Switch.t -> 711 + < net : _ Eio.Net.t ; fs : Eio.Fs.dir_ty Eio.Path.t ; clock : _ Eio.Time.clock ; .. > -> 712 + base_url:string -> 713 + t 714 + 715 + (** Get the base URL *) 716 + val base_url : t -> string 717 + 718 + (** Get the underlying requests session *) 719 + val session : t -> Requests.t 720 + 721 + %s 722 + |} api_desc spec.info.version (String.concat "\n\n" sigs) 723 + 724 + let generate_main_ml (spec : Spec.t) (package_name : string) : string = 725 + let module_name = Name.to_module_name package_name in 726 + let api_desc = Option.value ~default:"Generated OpenAPI client." spec.info.description in 727 + Printf.sprintf {|(** {1 %s} 728 + 729 + %s 730 + 731 + @version %s *) 732 + 733 + module Types = Types 734 + module Client = Client 735 + |} module_name api_desc spec.info.version 736 + 737 + let generate_main_mli (spec : Spec.t) (package_name : string) : string = 738 + let module_name = Name.to_module_name package_name in 739 + let api_desc = Option.value ~default:"Generated OpenAPI client." spec.info.description in 740 + Printf.sprintf {|(** {1 %s} 741 + 742 + %s 743 + 744 + @version %s *) 745 + 746 + (** Type definitions for API data structures *) 747 + module Types = Types 748 + 749 + (** API client functions *) 750 + module Client = Client 751 + |} module_name api_desc spec.info.version 752 + 753 + let generate_dune (package_name : string) : string = 754 + Printf.sprintf {|(library 755 + (name %s) 756 + (libraries openapi jsont jsont.bytesrw requests ptime) 757 + (wrapped true)) 758 + 759 + (include dune.inc) 760 + |} package_name 761 + 762 + let generate_dune_inc ~(spec_path : string option) (package_name : string) : string = 763 + match spec_path with 764 + | None -> "; No spec path provided - regeneration rules not generated\n" 765 + | Some path -> 766 + Printf.sprintf {|; Generated rules for OpenAPI code regeneration 767 + ; Run: dune build @gen --auto-promote 768 + 769 + (rule 770 + (alias gen) 771 + (mode (promote (until-clean))) 772 + (targets types.ml types.mli client.ml client.mli %s.ml %s.mli) 773 + (deps %s) 774 + (action 775 + (run openapi-gen generate -o . -n %s %%{deps}))) 776 + |} package_name package_name path package_name 777 + 778 + let generate ~(config : config) (spec : Spec.t) : (string * string) list = 779 + let package_name = config.package_name in 780 + [ 781 + ("dune", generate_dune package_name); 782 + ("dune.inc", generate_dune_inc ~spec_path:config.spec_path package_name); 783 + ("types.ml", generate_types_ml spec); 784 + ("types.mli", generate_types_mli spec); 785 + ("client.ml", generate_client_ml spec); 786 + ("client.mli", generate_client_mli spec); 787 + (package_name ^ ".ml", generate_main_ml spec package_name); 788 + (package_name ^ ".mli", generate_main_mli spec package_name); 789 + ] 790 + 791 + let write_files ~(output_dir : string) (files : (string * string) list) : unit = 792 + List.iter (fun (filename, content) -> 793 + let path = Filename.concat output_dir filename in 794 + let oc = open_out path in 795 + output_string oc content; 796 + close_out oc 797 + ) files
+180
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
+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
+29
ocaml-openapi/openapi.opam
··· 1 + opam-version: "2.0" 2 + name: "openapi" 3 + version: "0.1.0" 4 + synopsis: "OpenAPI code generator for OCaml with requests and jsont" 5 + description: """ 6 + Generate type-safe OCaml API clients from OpenAPI 3.x specifications. 7 + Uses the requests HTTP library and jsont for JSON codecs. 8 + """ 9 + maintainer: ["Your Name <your@email.com>"] 10 + authors: ["Your Name <your@email.com>"] 11 + license: "ISC" 12 + homepage: "https://github.com/example/ocaml-openapi" 13 + bug-reports: "https://github.com/example/ocaml-openapi/issues" 14 + depends: [ 15 + "ocaml" {>= "5.1.0"} 16 + "dune" {>= "3.0"} 17 + "jsont" 18 + "jsont-bytesrw" 19 + "fmt" 20 + "logs" 21 + "ptime" 22 + "cmdliner" {>= "1.2.0"} 23 + "alcotest" {with-test} 24 + ] 25 + build: [ 26 + ["dune" "subst"] {dev} 27 + ["dune" "build" "-p" name "-j" jobs] 28 + ["dune" "runtest" "-p" name "-j" jobs] {with-test} 29 + ]
+3
ocaml-openapi/test/dune
··· 1 + (test 2 + (name test_openapi) 3 + (libraries openapi alcotest))
+301
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_gen_enum_type () = 249 + let meta = Jsont.Meta.none in 250 + let (impl, intf) = Codegen.TypeGen.gen_enum_type ~name:"t" ~description:None 251 + [Jsont.String ("active", meta); Jsont.String ("inactive", meta); Jsont.String ("pending", meta)] in 252 + Alcotest.(check bool) "contains type impl" true (String.length impl > 0); 253 + Alcotest.(check bool) "contains Active" true (contains_substring impl "Active"); 254 + Alcotest.(check bool) "interface generated" true (String.length intf > 0) 255 + 256 + (** {1 Test Suites} *) 257 + 258 + let path_tests = [ 259 + "render simple", `Quick, test_path_render_simple; 260 + "render one param", `Quick, test_path_render_one_param; 261 + "render multiple params", `Quick, test_path_render_multiple_params; 262 + "extract parameters", `Quick, test_path_parameters; 263 + ] 264 + 265 + let query_tests = [ 266 + "singleton", `Quick, test_query_singleton; 267 + "optional some", `Quick, test_query_optional_some; 268 + "optional none", `Quick, test_query_optional_none; 269 + "encode empty", `Quick, test_query_encode_empty; 270 + "encode single", `Quick, test_query_encode_single; 271 + "encode multiple", `Quick, test_query_encode_multiple; 272 + "encode special chars", `Quick, test_query_encode_special_chars; 273 + ] 274 + 275 + let name_tests = [ 276 + "snake case simple", `Quick, test_snake_case_simple; 277 + "snake case dashes", `Quick, test_snake_case_with_dashes; 278 + "snake case reserved", `Quick, test_snake_case_reserved; 279 + "module name", `Quick, test_module_name; 280 + "variant name", `Quick, test_variant_name; 281 + ] 282 + 283 + let spec_tests = [ 284 + "parse minimal", `Quick, test_parse_minimal_spec; 285 + "parse schema", `Quick, test_parse_schema; 286 + "parse enum", `Quick, test_parse_enum; 287 + "parse paths", `Quick, test_parse_paths; 288 + ] 289 + 290 + let codegen_tests = [ 291 + "gen enum type", `Quick, test_gen_enum_type; 292 + ] 293 + 294 + let () = 295 + Alcotest.run "openapi" [ 296 + "Path", path_tests; 297 + "Query", query_tests; 298 + "Name", name_tests; 299 + "Spec", spec_tests; 300 + "Codegen", codegen_tests; 301 + ]