mockforge-core 0.3.115

Shared logic for MockForge - routing, validation, latency, proxy
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
//! Backend code generation utilities
//!
//! This module provides shared utilities for generating backend server code
//! from OpenAPI specifications. These utilities can be used by backend generator
//! plugins to extract routes, convert schemas, and generate common patterns.

use crate::openapi::spec::OpenApiSpec;
use crate::Result;
use openapiv3::{Operation, ParameterSchemaOrContent, PathItem, ReferenceOr, Schema};
use std::collections::HashMap;

/// Information about a route extracted from OpenAPI spec
#[derive(Debug, Clone)]
pub struct RouteInfo {
    /// HTTP method (GET, POST, etc.)
    pub method: String,
    /// API path (e.g., /users/{id})
    pub path: String,
    /// Operation ID from spec
    pub operation_id: Option<String>,
    /// Summary from spec
    pub summary: Option<String>,
    /// Description from spec
    pub description: Option<String>,
    /// Path parameters (e.g., {id} -> ["id"])
    pub path_params: Vec<String>,
    /// Query parameters
    pub query_params: Vec<QueryParamInfo>,
    /// Request body schema (if any)
    pub request_body_schema: Option<Schema>,
    /// Response schemas mapped by status code
    pub responses: HashMap<u16, ResponseInfo>,
    /// Tags for grouping
    pub tags: Vec<String>,
}

/// Query parameter information
#[derive(Debug, Clone)]
pub struct QueryParamInfo {
    /// Parameter name
    pub name: String,
    /// Whether parameter is required
    pub required: bool,
    /// Parameter schema
    pub schema: Option<Schema>,
    /// Parameter description
    pub description: Option<String>,
}

/// Response information
#[derive(Debug, Clone)]
pub struct ResponseInfo {
    /// HTTP status code
    pub status_code: u16,
    /// Response description
    pub description: Option<String>,
    /// Response schema (if any)
    pub schema: Option<Schema>,
    /// Example response (if any)
    pub example: Option<serde_json::Value>,
}

/// Extract all routes from an OpenAPI specification
///
/// # Arguments
/// * `spec` - The OpenAPI specification to extract routes from
///
/// # Returns
/// Vector of route information for all operations in the spec
pub fn extract_routes(spec: &OpenApiSpec) -> Result<Vec<RouteInfo>> {
    let mut routes = Vec::new();

    for (path, path_item) in &spec.spec.paths.paths {
        if let Some(item) = path_item.as_item() {
            // Extract routes for each HTTP method
            if let Some(op) = &item.get {
                routes.push(extract_route_info("GET", path, op, item)?);
            }
            if let Some(op) = &item.post {
                routes.push(extract_route_info("POST", path, op, item)?);
            }
            if let Some(op) = &item.put {
                routes.push(extract_route_info("PUT", path, op, item)?);
            }
            if let Some(op) = &item.delete {
                routes.push(extract_route_info("DELETE", path, op, item)?);
            }
            if let Some(op) = &item.patch {
                routes.push(extract_route_info("PATCH", path, op, item)?);
            }
            if let Some(op) = &item.head {
                routes.push(extract_route_info("HEAD", path, op, item)?);
            }
            if let Some(op) = &item.options {
                routes.push(extract_route_info("OPTIONS", path, op, item)?);
            }
            if let Some(op) = &item.trace {
                routes.push(extract_route_info("TRACE", path, op, item)?);
            }
        }
    }

    Ok(routes)
}

/// Extract route information from an OpenAPI operation
fn extract_route_info(
    method: &str,
    path: &str,
    operation: &Operation,
    _path_item: &PathItem,
) -> Result<RouteInfo> {
    // Extract path parameters from the path string
    let path_params = extract_path_parameters(path);

    // Extract query parameters
    let mut query_params = Vec::new();
    for param_ref in &operation.parameters {
        if let Some(openapiv3::Parameter::Query { parameter_data, .. }) = param_ref.as_item() {
            let schema =
                if let ParameterSchemaOrContent::Schema(schema_ref) = &parameter_data.format {
                    schema_ref.as_item().cloned()
                } else {
                    None
                };

            query_params.push(QueryParamInfo {
                name: parameter_data.name.clone(),
                required: parameter_data.required,
                schema,
                description: parameter_data.description.clone(),
            });
        }
    }

    // Extract request body schema
    let request_body_schema = operation
        .request_body
        .as_ref()
        .and_then(|body_ref| body_ref.as_item())
        .and_then(|body| {
            body.content
                .get("application/json")
                .and_then(|content| content.schema.as_ref())
                .and_then(|schema_ref| schema_ref.as_item().cloned())
        });

    // Extract responses
    let mut responses = HashMap::new();
    for (status_code, response_ref) in &operation.responses.responses {
        let status = match status_code {
            openapiv3::StatusCode::Code(code) => *code,
            openapiv3::StatusCode::Range(range) if *range == 2 => 200,
            openapiv3::StatusCode::Range(range) if *range == 4 => 400,
            openapiv3::StatusCode::Range(range) if *range == 5 => 500,
            _ => continue,
        };

        if let Some(response) = response_ref.as_item() {
            let schema = response
                .content
                .get("application/json")
                .and_then(|content| content.schema.as_ref())
                .and_then(|schema_ref| schema_ref.as_item().cloned());

            let example = response.content.get("application/json").and_then(|content| {
                content.example.clone().or_else(|| {
                    content.examples.iter().next().and_then(|(_, example_ref)| {
                        example_ref.as_item().and_then(|example_item| example_item.value.clone())
                    })
                })
            });

            responses.insert(
                status,
                ResponseInfo {
                    status_code: status,
                    description: Some(response.description.clone()),
                    schema,
                    example,
                },
            );
        }
    }

    Ok(RouteInfo {
        method: method.to_string(),
        path: path.to_string(),
        operation_id: operation.operation_id.clone(),
        summary: operation.summary.clone(),
        description: operation.description.clone(),
        path_params,
        query_params,
        request_body_schema,
        responses,
        tags: operation.tags.clone(),
    })
}

/// Extract path parameters from an OpenAPI path string
///
/// # Arguments
/// * `path` - The path string (e.g., "/users/{id}/posts/{postId}")
///
/// # Returns
/// Vector of parameter names found in the path
pub fn extract_path_parameters(path: &str) -> Vec<String> {
    let mut params = Vec::new();
    let mut in_param = false;
    let mut current_param = String::new();

    for ch in path.chars() {
        match ch {
            '{' => {
                in_param = true;
                current_param.clear();
            }
            '}' => {
                if in_param && !current_param.is_empty() {
                    params.push(current_param.clone());
                    in_param = false;
                }
            }
            ch if in_param => {
                current_param.push(ch);
            }
            _ => {}
        }
    }

    params
}

/// Get all schemas from OpenAPI components
///
/// # Arguments
/// * `spec` - The OpenAPI specification
///
/// # Returns
/// Map of schema name to schema definition
pub fn extract_schemas(spec: &OpenApiSpec) -> HashMap<String, Schema> {
    let mut schemas = HashMap::new();

    if let Some(components) = &spec.spec.components {
        if !components.schemas.is_empty() {
            for (name, schema_ref) in &components.schemas {
                if let ReferenceOr::Item(schema) = schema_ref {
                    schemas.insert(name.clone(), schema.clone());
                }
            }
        }
    }

    schemas
}

/// Convert OpenAPI schema type to a Rust type name
///
/// # Arguments
/// * `schema` - The OpenAPI schema
/// * `schema_name` - Optional name for the schema (used for object types)
///
/// # Returns
/// Rust type name as a string
pub fn schema_to_rust_type(schema: &Schema, schema_name: Option<&str>) -> String {
    match &schema.schema_kind {
        openapiv3::SchemaKind::Type(openapiv3::Type::String(_)) => "String".to_string(),
        openapiv3::SchemaKind::Type(openapiv3::Type::Integer(_)) => "i64".to_string(),
        openapiv3::SchemaKind::Type(openapiv3::Type::Number(_)) => "f64".to_string(),
        openapiv3::SchemaKind::Type(openapiv3::Type::Boolean(_)) => "bool".to_string(),
        openapiv3::SchemaKind::Type(openapiv3::Type::Array(array_type)) => {
            let item_type = array_type
                .items
                .as_ref()
                .and_then(|item_ref| item_ref.as_item())
                .map(|item_schema| schema_to_rust_type(item_schema, None))
                .unwrap_or_else(|| "serde_json::Value".to_string());

            format!("Vec<{}>", item_type)
        }
        openapiv3::SchemaKind::Type(openapiv3::Type::Object(_)) => schema_name
            .map(to_pascal_case)
            .unwrap_or_else(|| "serde_json::Value".to_string()),
        _ => "serde_json::Value".to_string(),
    }
}

/// Convert a string to PascalCase
pub fn to_pascal_case(s: &str) -> String {
    s.split(['-', '_', ' '])
        .filter(|s| !s.is_empty())
        .map(|word| {
            let mut chars = word.chars();
            match chars.next() {
                None => String::new(),
                Some(first) => {
                    first.to_uppercase().collect::<String>() + &chars.as_str().to_lowercase()
                }
            }
        })
        .collect()
}

/// Convert a string to snake_case
pub fn to_snake_case(s: &str) -> String {
    let mut result = String::new();
    let mut prev_lower = false;

    for ch in s.chars() {
        if ch.is_uppercase() && prev_lower {
            result.push('_');
        }
        result.push(ch.to_lowercase().next().unwrap_or(ch));
        prev_lower = ch.is_lowercase() || ch.is_numeric();
    }

    result
}

/// Generate a handler function name from route information
///
/// # Arguments
/// * `route` - The route information
///
/// # Returns
/// Function name in snake_case
pub fn generate_handler_name(route: &RouteInfo) -> String {
    if let Some(ref op_id) = route.operation_id {
        // Use operation ID if available, convert to snake_case
        to_snake_case(op_id)
    } else {
        // Generate from method + path
        let method_lower = route.method.to_lowercase();
        let path_part = route
            .path
            .replace('/', "_")
            .replace(['{', '}'], "")
            .replace('-', "_")
            .trim_matches('_')
            .to_string();

        format!("{}_{}", method_lower, to_snake_case(&path_part))
    }
}

/// Sanitize a name for use in Rust identifiers
///
/// Removes or replaces invalid characters to create a valid Rust identifier
pub fn sanitize_name(name: &str) -> String {
    name.chars()
        .map(|c| {
            if c.is_alphanumeric() || c == '_' {
                c
            } else {
                '_'
            }
        })
        .collect::<String>()
        .trim_matches('_')
        .to_string()
        .to_lowercase()
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_extract_path_parameters() {
        assert_eq!(extract_path_parameters("/users"), Vec::<String>::new());
        assert_eq!(extract_path_parameters("/users/{id}"), vec!["id"]);
        assert_eq!(extract_path_parameters("/users/{id}/posts/{postId}"), vec!["id", "postId"]);
    }

    #[test]
    fn test_to_pascal_case() {
        assert_eq!(to_pascal_case("user"), "User");
        assert_eq!(to_pascal_case("user_profile"), "UserProfile");
        assert_eq!(to_pascal_case("user-profile"), "UserProfile");
        assert_eq!(to_pascal_case("get_user_by_id"), "GetUserById");
    }

    #[test]
    fn test_to_snake_case() {
        assert_eq!(to_snake_case("User"), "user");
        assert_eq!(to_snake_case("UserProfile"), "user_profile");
        assert_eq!(to_snake_case("getUserById"), "get_user_by_id");
        assert_eq!(to_snake_case("GetUserById"), "get_user_by_id");
    }

    #[test]
    fn test_generate_handler_name() {
        let route = RouteInfo {
            method: "GET".to_string(),
            path: "/users/{id}".to_string(),
            operation_id: Some("getUser".to_string()),
            summary: None,
            description: None,
            path_params: vec!["id".to_string()],
            query_params: Vec::new(),
            request_body_schema: None,
            responses: HashMap::new(),
            tags: Vec::new(),
        };

        assert_eq!(generate_handler_name(&route), "get_user");
    }
}