OpenAPI generator for OCaml with Requests/Eio/Jsont
1(** Tests for ocaml-openapi *)
2
3module Spec = Openapi.Spec
4module Codegen = Openapi.Codegen
5module Runtime = Openapi.Runtime
6
7(** {1 Path Template Tests} *)
8
9let test_path_render_simple () =
10 let result = Runtime.Path.render ~params:[] "/users" in
11 Alcotest.(check string) "no params" "/users" result
12
13let 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
17let 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
23let 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
29let 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
33let 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
37let 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
41let test_query_encode_empty () =
42 let result = Runtime.Query.encode [] in
43 Alcotest.(check string) "empty query" "" result
44
45let test_query_encode_single () =
46 let result = Runtime.Query.encode [("name", "alice")] in
47 Alcotest.(check string) "single query" "?name=alice" result
48
49let 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
53let 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
59let 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
63let 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
67let test_snake_case_reserved () =
68 let result = Codegen.Name.to_snake_case "type" in
69 Alcotest.(check string) "reserved word" "type_" result
70
71let test_module_name () =
72 let result = Codegen.Name.to_module_name "user_profile" in
73 Alcotest.(check string) "module name" "UserProfile" result
74
75let 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
81let minimal_spec = {|{
82 "openapi": "3.0.0",
83 "info": {
84 "title": "Test API",
85 "version": "1.0.0"
86 },
87 "paths": {}
88}|}
89
90let 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
98let 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
120let 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
136let 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
153let 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
169let 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
217let 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
236let 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
248let test_split_schema_name () =
249 let p, s = Codegen.Name.split_schema_name "AlbumResponseDto" in
250 Alcotest.(check string) "prefix" "Album" p;
251 Alcotest.(check string) "suffix" "ResponseDto" s
252
253let test_split_schema_name_no_suffix () =
254 let p, s = Codegen.Name.split_schema_name "User" in
255 Alcotest.(check string) "prefix" "User" p;
256 Alcotest.(check string) "suffix" "T" s
257
258let test_generate_files () =
259 match Spec.of_string spec_with_schema with
260 | Error e -> Alcotest.fail e
261 | Ok spec ->
262 let config = Codegen.{
263 output_dir = ".";
264 package_name = "test_api";
265 spec_path = None;
266 } in
267 let files = Codegen.generate ~config spec in
268 Alcotest.(check int) "file count" 4 (List.length files);
269 let ml = List.assoc_opt "test_api.ml" files in
270 (match ml with
271 | None -> Alcotest.fail "missing .ml file"
272 | Some content ->
273 Alcotest.(check bool) "contains module User" true
274 (contains_substring content "module User"))
275
276let test_generate_enum_schema () =
277 match Spec.of_string spec_with_enum with
278 | Error e -> Alcotest.fail e
279 | Ok spec ->
280 let config = Codegen.{
281 output_dir = ".";
282 package_name = "test_enum";
283 spec_path = None;
284 } in
285 let files = Codegen.generate ~config spec in
286 let ml = List.assoc_opt "test_enum.ml" files in
287 (match ml with
288 | None -> Alcotest.fail "missing .ml file"
289 | Some content ->
290 Alcotest.(check bool) "contains Active variant" true
291 (contains_substring content "Active"))
292
293(** {1 Test Suites} *)
294
295let path_tests = [
296 "render simple", `Quick, test_path_render_simple;
297 "render one param", `Quick, test_path_render_one_param;
298 "render multiple params", `Quick, test_path_render_multiple_params;
299 "extract parameters", `Quick, test_path_parameters;
300]
301
302let query_tests = [
303 "singleton", `Quick, test_query_singleton;
304 "optional some", `Quick, test_query_optional_some;
305 "optional none", `Quick, test_query_optional_none;
306 "encode empty", `Quick, test_query_encode_empty;
307 "encode single", `Quick, test_query_encode_single;
308 "encode multiple", `Quick, test_query_encode_multiple;
309 "encode special chars", `Quick, test_query_encode_special_chars;
310]
311
312let name_tests = [
313 "snake case simple", `Quick, test_snake_case_simple;
314 "snake case dashes", `Quick, test_snake_case_with_dashes;
315 "snake case reserved", `Quick, test_snake_case_reserved;
316 "module name", `Quick, test_module_name;
317 "variant name", `Quick, test_variant_name;
318]
319
320let spec_tests = [
321 "parse minimal", `Quick, test_parse_minimal_spec;
322 "parse schema", `Quick, test_parse_schema;
323 "parse enum", `Quick, test_parse_enum;
324 "parse paths", `Quick, test_parse_paths;
325]
326
327let codegen_tests = [
328 "split schema name", `Quick, test_split_schema_name;
329 "split schema name no suffix", `Quick, test_split_schema_name_no_suffix;
330 "generate files", `Quick, test_generate_files;
331 "generate enum schema", `Quick, test_generate_enum_schema;
332]
333
334let () =
335 Alcotest.run "openapi" [
336 "Path", path_tests;
337 "Query", query_tests;
338 "Name", name_tests;
339 "Spec", spec_tests;
340 "Codegen", codegen_tests;
341 ]