openapi-to-rust 0.2.1

Generate strongly-typed Rust structs, HTTP clients, and SSE streaming clients from OpenAPI 3.1 specifications
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
use openapi_to_rust::{CodeGenerator, GeneratorConfig};
use std::path::PathBuf;

/// Helper to create a minimal generator config
fn create_test_config() -> GeneratorConfig {
    GeneratorConfig {
        spec_path: PathBuf::from("test.json"),
        output_dir: PathBuf::from("test_output"),
        module_name: "test".to_string(),
        enable_async_client: true,
        ..Default::default()
    }
}

#[test]
fn test_http_client_struct_generation() {
    let config = create_test_config();
    let generator = CodeGenerator::new(config);

    let client_code = generator.generate_http_client_struct();
    let code_str = client_code.to_string();

    // Verify the struct is generated
    assert!(
        code_str.contains("pub struct HttpClient"),
        "Should generate HttpClient struct"
    );

    // Verify fields
    assert!(code_str.contains("base_url"), "Should have base_url field");
    assert!(code_str.contains("api_key"), "Should have api_key field");
    assert!(
        code_str.contains("http_client"),
        "Should have http_client field"
    );
    assert!(
        code_str.contains("custom_headers"),
        "Should have custom_headers field"
    );

    // Verify middleware types
    assert!(
        code_str.contains("ClientWithMiddleware"),
        "Should use reqwest_middleware::ClientWithMiddleware"
    );
    assert!(
        code_str.contains("BTreeMap"),
        "Should use BTreeMap for headers"
    );
}

#[test]
fn test_constructor_without_retry() {
    let config = GeneratorConfig {
        spec_path: PathBuf::from("test.json"),
        output_dir: PathBuf::from("test_output"),
        module_name: "test".to_string(),
        enable_async_client: true,
        retry_config: None,
        tracing_enabled: false,
        ..Default::default()
    };

    let generator = CodeGenerator::new(config);
    let client_code = generator.generate_http_client_struct();
    let code_str = client_code.to_string();

    // Verify new() constructor exists
    assert!(
        code_str.contains("pub fn new"),
        "Should generate new() constructor"
    );

    // Verify no RetryConfig struct when retry disabled
    assert!(
        !code_str.contains("pub struct RetryConfig"),
        "Should NOT generate RetryConfig when retry is disabled"
    );

    // Verify no retry middleware when disabled
    assert!(
        !code_str.contains("RetryTransientMiddleware"),
        "Should NOT include retry middleware when disabled"
    );

    // Verify no tracing middleware when disabled
    assert!(
        !code_str.contains("TracingMiddleware"),
        "Should NOT include tracing middleware when disabled"
    );
}

#[test]
fn test_constructor_with_retry() {
    let config = GeneratorConfig {
        spec_path: PathBuf::from("test.json"),
        output_dir: PathBuf::from("test_output"),
        module_name: "test".to_string(),
        enable_async_client: true,
        retry_config: Some(openapi_to_rust::http_config::RetryConfig {
            max_retries: 3,
            initial_delay_ms: 500,
            max_delay_ms: 16000,
        }),
        tracing_enabled: false,
        ..Default::default()
    };

    let generator = CodeGenerator::new(config);
    let client_code = generator.generate_http_client_struct();
    let code_str = client_code.to_string();

    // Verify RetryConfig struct is generated
    assert!(
        code_str.contains("pub struct RetryConfig"),
        "Should generate RetryConfig struct when retry enabled"
    );

    // Verify RetryConfig fields
    assert!(
        code_str.contains("max_retries"),
        "RetryConfig should have max_retries field"
    );
    assert!(
        code_str.contains("initial_delay_ms"),
        "RetryConfig should have initial_delay_ms field"
    );
    assert!(
        code_str.contains("max_delay_ms"),
        "RetryConfig should have max_delay_ms field"
    );

    // Verify Default implementation
    assert!(
        code_str.contains("impl Default for RetryConfig"),
        "RetryConfig should have Default impl"
    );

    // Verify retry middleware is included
    assert!(
        code_str.contains("RetryTransientMiddleware"),
        "Should include retry middleware when enabled"
    );
    assert!(
        code_str.contains("ExponentialBackoff"),
        "Should use exponential backoff policy"
    );

    // Verify with_config method accepts retry_config
    assert!(
        code_str.contains("pub fn with_config"),
        "Should generate with_config method"
    );
    assert!(
        code_str.contains("retry_config : Option < RetryConfig >"),
        "with_config should accept retry_config parameter"
    );
}

#[test]
fn test_constructor_with_tracing() {
    let config = GeneratorConfig {
        spec_path: PathBuf::from("test.json"),
        output_dir: PathBuf::from("test_output"),
        module_name: "test".to_string(),
        enable_async_client: true,
        retry_config: None,
        tracing_enabled: true,
        ..Default::default()
    };

    let generator = CodeGenerator::new(config);
    let client_code = generator.generate_http_client_struct();
    let code_str = client_code.to_string();

    // Verify tracing middleware is included
    assert!(
        code_str.contains("TracingMiddleware"),
        "Should include tracing middleware when enabled"
    );

    // Verify with_config method accepts enable_tracing
    assert!(
        code_str.contains("pub fn with_config"),
        "Should generate with_config method"
    );
    assert!(
        code_str.contains("enable_tracing : bool"),
        "with_config should accept enable_tracing parameter"
    );
}

#[test]
fn test_builder_methods_generated() {
    let config = create_test_config();
    let generator = CodeGenerator::new(config);

    let client_code = generator.generate_http_client_struct();
    let code_str = client_code.to_string();

    // Verify with_base_url method
    assert!(
        code_str.contains("pub fn with_base_url"),
        "Should generate with_base_url method"
    );
    assert!(
        code_str.contains("base_url . into ()"),
        "with_base_url should accept Into<String>"
    );

    // Verify with_api_key method
    assert!(
        code_str.contains("pub fn with_api_key"),
        "Should generate with_api_key method"
    );
    assert!(
        code_str.contains("api_key . into ()"),
        "with_api_key should accept Into<String>"
    );

    // Verify with_header method
    assert!(
        code_str.contains("pub fn with_header"),
        "Should generate with_header method"
    );

    // Verify with_headers method
    assert!(
        code_str.contains("pub fn with_headers"),
        "Should generate with_headers method"
    );
    assert!(
        code_str.contains("headers : BTreeMap < String , String >"),
        "with_headers should accept BTreeMap"
    );
}

#[test]
fn test_retry_config_struct_when_enabled() {
    let config = GeneratorConfig {
        spec_path: PathBuf::from("test.json"),
        output_dir: PathBuf::from("test_output"),
        module_name: "test".to_string(),
        enable_async_client: true,
        retry_config: Some(openapi_to_rust::http_config::RetryConfig {
            max_retries: 5,
            initial_delay_ms: 1000,
            max_delay_ms: 30000,
        }),
        ..Default::default()
    };

    let generator = CodeGenerator::new(config);
    let client_code = generator.generate_http_client_struct();
    let code_str = client_code.to_string();

    // Verify RetryConfig struct exists
    assert!(
        code_str.contains("pub struct RetryConfig"),
        "Should generate RetryConfig struct"
    );

    // Verify all fields are present
    assert!(
        code_str.contains("pub max_retries : u32"),
        "Should have max_retries field"
    );
    assert!(
        code_str.contains("pub initial_delay_ms : u64"),
        "Should have initial_delay_ms field"
    );
    assert!(
        code_str.contains("pub max_delay_ms : u64"),
        "Should have max_delay_ms field"
    );

    // Verify Default trait implementation
    assert!(
        code_str.contains("impl Default for RetryConfig"),
        "Should implement Default for RetryConfig"
    );
    assert!(
        code_str.contains("fn default () -> Self"),
        "Default impl should have default() method"
    );
}

#[test]
fn test_no_retry_config_when_disabled() {
    let config = GeneratorConfig {
        spec_path: PathBuf::from("test.json"),
        output_dir: PathBuf::from("test_output"),
        module_name: "test".to_string(),
        enable_async_client: true,
        retry_config: None,
        ..Default::default()
    };

    let generator = CodeGenerator::new(config);
    let client_code = generator.generate_http_client_struct();
    let code_str = client_code.to_string();

    // Verify RetryConfig struct is NOT generated
    assert!(
        !code_str.contains("pub struct RetryConfig"),
        "Should NOT generate RetryConfig when retry is disabled"
    );

    // Verify retry middleware is NOT included
    assert!(
        !code_str.contains("RetryTransientMiddleware"),
        "Should NOT include retry middleware when disabled"
    );
    assert!(
        !code_str.contains("ExponentialBackoff"),
        "Should NOT include backoff policy when retry disabled"
    );
}

#[test]
fn test_middleware_stack_order() {
    let config = GeneratorConfig {
        spec_path: PathBuf::from("test.json"),
        output_dir: PathBuf::from("test_output"),
        module_name: "test".to_string(),
        enable_async_client: true,
        retry_config: Some(openapi_to_rust::http_config::RetryConfig {
            max_retries: 3,
            initial_delay_ms: 500,
            max_delay_ms: 16000,
        }),
        tracing_enabled: true,
        ..Default::default()
    };

    let generator = CodeGenerator::new(config);
    let client_code = generator.generate_http_client_struct();
    let code_str = client_code.to_string();

    // Verify both middleware are present
    assert!(
        code_str.contains("TracingMiddleware"),
        "Should include tracing middleware"
    );
    assert!(
        code_str.contains("RetryTransientMiddleware"),
        "Should include retry middleware"
    );

    // Find positions of middleware setup
    let tracing_pos = code_str.find("TracingMiddleware");
    let retry_pos = code_str.find("RetryTransientMiddleware");

    assert!(tracing_pos.is_some(), "TracingMiddleware should be present");
    assert!(
        retry_pos.is_some(),
        "RetryTransientMiddleware should be present"
    );

    // Verify tracing comes before retry in the code
    // (This ensures middleware stack order: tracing first, then retry)
    assert!(
        tracing_pos.unwrap() < retry_pos.unwrap(),
        "TracingMiddleware should be added before RetryTransientMiddleware in code order"
    );
}

#[test]
fn test_both_retry_and_tracing_enabled() {
    let config = GeneratorConfig {
        spec_path: PathBuf::from("test.json"),
        output_dir: PathBuf::from("test_output"),
        module_name: "test".to_string(),
        enable_async_client: true,
        retry_config: Some(openapi_to_rust::http_config::RetryConfig {
            max_retries: 3,
            initial_delay_ms: 500,
            max_delay_ms: 16000,
        }),
        tracing_enabled: true,
        ..Default::default()
    };

    let generator = CodeGenerator::new(config);
    let client_code = generator.generate_http_client_struct();
    let code_str = client_code.to_string();

    // Verify with_config has both parameters
    assert!(
        code_str.contains("retry_config : Option < RetryConfig >"),
        "with_config should have retry_config parameter"
    );
    assert!(
        code_str.contains("enable_tracing : bool"),
        "with_config should have enable_tracing parameter"
    );

    // Verify new() constructor calls with_config with correct defaults
    assert!(
        code_str.contains("pub fn new"),
        "Should generate new() constructor"
    );
    assert!(
        code_str.contains("Self :: with_config"),
        "new() should call with_config"
    );
}

#[test]
fn test_generated_code_compiles() {
    let config = GeneratorConfig {
        spec_path: PathBuf::from("test.json"),
        output_dir: PathBuf::from("test_output"),
        module_name: "test".to_string(),
        enable_async_client: true,
        retry_config: Some(openapi_to_rust::http_config::RetryConfig {
            max_retries: 3,
            initial_delay_ms: 500,
            max_delay_ms: 16000,
        }),
        tracing_enabled: true,
        ..Default::default()
    };

    let generator = CodeGenerator::new(config);
    let client_code = generator.generate_http_client_struct();

    // Try to parse the generated code as valid Rust syntax
    let code_str = client_code.to_string();

    // This is a basic check - the code should at least be parseable
    assert!(!code_str.is_empty(), "Generated code should not be empty");

    // Verify it has proper structure markers
    assert!(
        code_str.contains("impl HttpClient"),
        "Should have HttpClient impl block"
    );
}

#[test]
fn test_default_retry_config_values() {
    let config = GeneratorConfig {
        spec_path: PathBuf::from("test.json"),
        output_dir: PathBuf::from("test_output"),
        module_name: "test".to_string(),
        enable_async_client: true,
        retry_config: Some(openapi_to_rust::http_config::RetryConfig {
            max_retries: 3,
            initial_delay_ms: 500,
            max_delay_ms: 16000,
        }),
        ..Default::default()
    };

    let generator = CodeGenerator::new(config);
    let client_code = generator.generate_http_client_struct();
    let code_str = client_code.to_string();

    // Verify default values in Default impl
    assert!(
        code_str.contains("max_retries : 3"),
        "Default max_retries should be 3"
    );
    assert!(
        code_str.contains("initial_delay_ms : 500"),
        "Default initial_delay_ms should be 500"
    );
    assert!(
        code_str.contains("max_delay_ms : 16000"),
        "Default max_delay_ms should be 16000"
    );
}