Skip to main content

httpgenerator_openapi/
inspect.rs

1use serde_json::{Map, Value};
2
3use crate::{
4    OpenApiInspectionError, OpenApiSpecificationVersion, RawOpenApiDocument, load_raw_document,
5};
6
7const SUPPORTED_METHODS: &[&str] = &[
8    "get", "put", "post", "delete", "options", "head", "patch", "trace",
9];
10
11#[derive(Debug, Clone, PartialEq, Eq, Default)]
12pub struct OpenApiStats {
13    pub parameter_count: usize,
14    pub schema_count: usize,
15    pub path_item_count: usize,
16    pub request_body_count: usize,
17    pub response_count: usize,
18    pub operation_count: usize,
19    pub link_count: usize,
20    pub callback_count: usize,
21}
22
23#[derive(Debug, Clone, PartialEq, Eq)]
24pub struct OpenApiInspection {
25    pub specification_version: OpenApiSpecificationVersion,
26    pub stats: OpenApiStats,
27}
28
29pub fn inspect_document(input: &str) -> Result<OpenApiInspection, OpenApiInspectionError> {
30    let raw = load_raw_document(input).map_err(OpenApiInspectionError::Load)?;
31    inspect_raw_document(&raw)
32}
33
34pub fn inspect_raw_document(
35    document: &RawOpenApiDocument,
36) -> Result<OpenApiInspection, OpenApiInspectionError> {
37    let specification_version = document
38        .specification_version()
39        .map_err(OpenApiInspectionError::VersionDetection)?;
40
41    Ok(OpenApiInspection {
42        specification_version,
43        stats: collect_stats(document.value(), specification_version),
44    })
45}
46
47fn collect_stats(root: &Value, specification_version: OpenApiSpecificationVersion) -> OpenApiStats {
48    let mut stats = OpenApiStats::default();
49
50    if let Some(paths) = root.get("paths").and_then(Value::as_object) {
51        stats.path_item_count = paths.len();
52
53        for path_item in paths.values() {
54            let Some(path_item) = path_item.as_object() else {
55                continue;
56            };
57
58            if let Some(parameters) = path_item.get("parameters").and_then(Value::as_array) {
59                stats.parameter_count += count_parameter_entries(parameters);
60                stats.request_body_count += count_swagger2_body_parameters(parameters);
61                stats.schema_count += count_parameter_schemas(parameters);
62            }
63
64            for method in SUPPORTED_METHODS {
65                let Some(operation) = path_item.get(*method).and_then(Value::as_object) else {
66                    continue;
67                };
68
69                stats.operation_count += 1;
70
71                if let Some(parameters) = operation.get("parameters").and_then(Value::as_array) {
72                    stats.parameter_count += count_parameter_entries(parameters);
73                    stats.request_body_count += count_swagger2_body_parameters(parameters);
74                    stats.schema_count += count_parameter_schemas(parameters);
75                }
76
77                if let Some(request_body) = operation.get("requestBody") {
78                    stats.request_body_count += count_request_body_entries(request_body);
79                    stats.schema_count += count_request_body_schemas(request_body);
80                }
81
82                if let Some(responses) = operation.get("responses").and_then(Value::as_object) {
83                    stats.response_count += 1;
84                    stats.schema_count += count_response_schemas(responses);
85                    stats.schema_count += count_response_header_schemas(responses);
86                    stats.link_count += count_response_links(responses);
87                }
88
89                if let Some(callbacks) = operation.get("callbacks").and_then(Value::as_object) {
90                    stats.callback_count += count_callback_entries(callbacks);
91                }
92            }
93        }
94    }
95
96    stats.parameter_count += count_component_parameters(root, specification_version);
97    stats.schema_count += count_component_schemas(root, specification_version);
98    stats.schema_count += count_component_parameter_schemas(root, specification_version);
99    stats.request_body_count += count_component_request_bodies(root, specification_version);
100    stats.schema_count += count_component_request_body_schemas(root, specification_version);
101    stats.schema_count += count_component_header_schemas(root, specification_version);
102    stats.schema_count += count_component_response_schemas(root, specification_version);
103    stats.link_count += count_component_links(root, specification_version);
104    stats.link_count += count_component_response_links(root, specification_version);
105    stats.callback_count += count_component_callbacks(root, specification_version);
106    stats
107}
108
109fn count_parameter_entries(parameters: &[Value]) -> usize {
110    parameters
111        .iter()
112        .filter(|parameter| {
113            parameter
114                .as_object()
115                .is_some_and(|parameter| !is_reference_object(parameter))
116        })
117        .count()
118}
119
120fn count_request_body_entries(request_body: &Value) -> usize {
121    request_body
122        .as_object()
123        .filter(|request_body| !is_reference_object(request_body))
124        .map(|_| 1)
125        .unwrap_or_default()
126}
127
128fn count_callback_entries(callbacks: &Map<String, Value>) -> usize {
129    callbacks
130        .values()
131        .filter(|callback| {
132            callback
133                .as_object()
134                .is_some_and(|callback| !is_reference_object(callback))
135        })
136        .count()
137}
138
139fn component_entries<'a>(
140    root: &'a Value,
141    specification_version: OpenApiSpecificationVersion,
142    component_name: &str,
143) -> Option<&'a Map<String, Value>> {
144    match specification_version {
145        OpenApiSpecificationVersion::Swagger2 => None,
146        OpenApiSpecificationVersion::OpenApi30 | OpenApiSpecificationVersion::OpenApi31 => root
147            .get("components")
148            .and_then(|components| components.get(component_name))
149            .and_then(Value::as_object),
150    }
151}
152
153fn count_component_schemas(
154    root: &Value,
155    specification_version: OpenApiSpecificationVersion,
156) -> usize {
157    match specification_version {
158        OpenApiSpecificationVersion::Swagger2 => root
159            .get("definitions")
160            .and_then(Value::as_object)
161            .map(|definitions| {
162                definitions
163                    .values()
164                    .map(count_schema_objects)
165                    .sum::<usize>()
166            })
167            .unwrap_or_default(),
168        OpenApiSpecificationVersion::OpenApi30 | OpenApiSpecificationVersion::OpenApi31 => root
169            .get("components")
170            .and_then(|components| components.get("schemas"))
171            .and_then(Value::as_object)
172            .map(|schemas| schemas.values().map(count_schema_objects).sum::<usize>())
173            .unwrap_or_default(),
174    }
175}
176
177fn count_component_parameters(
178    root: &Value,
179    specification_version: OpenApiSpecificationVersion,
180) -> usize {
181    component_entries(root, specification_version, "parameters")
182        .map(|parameters| {
183            parameters
184                .values()
185                .filter(|parameter| {
186                    parameter
187                        .as_object()
188                        .is_some_and(|parameter| !is_reference_object(parameter))
189                })
190                .count()
191        })
192        .unwrap_or_default()
193}
194
195fn count_component_parameter_schemas(
196    root: &Value,
197    specification_version: OpenApiSpecificationVersion,
198) -> usize {
199    component_entries(root, specification_version, "parameters")
200        .map(|parameters| {
201            parameters
202                .values()
203                .map(|parameter| {
204                    parameter
205                        .as_object()
206                        .map(count_parameter_schema_for_object)
207                        .unwrap_or_default()
208                })
209                .sum::<usize>()
210        })
211        .unwrap_or_default()
212}
213
214fn count_component_request_bodies(
215    root: &Value,
216    specification_version: OpenApiSpecificationVersion,
217) -> usize {
218    component_entries(root, specification_version, "requestBodies")
219        .map(|request_bodies| {
220            request_bodies
221                .values()
222                .filter(|request_body| {
223                    request_body
224                        .as_object()
225                        .is_some_and(|request_body| !is_reference_object(request_body))
226                })
227                .count()
228        })
229        .unwrap_or_default()
230}
231
232fn count_component_request_body_schemas(
233    root: &Value,
234    specification_version: OpenApiSpecificationVersion,
235) -> usize {
236    component_entries(root, specification_version, "requestBodies")
237        .map(|request_bodies| {
238            request_bodies
239                .values()
240                .map(count_request_body_schemas)
241                .sum::<usize>()
242        })
243        .unwrap_or_default()
244}
245
246fn count_component_header_schemas(
247    root: &Value,
248    specification_version: OpenApiSpecificationVersion,
249) -> usize {
250    component_entries(root, specification_version, "headers")
251        .map(|headers| {
252            headers
253                .values()
254                .map(|header| {
255                    header
256                        .as_object()
257                        .map(count_header_schema_for_object)
258                        .unwrap_or_default()
259                })
260                .sum::<usize>()
261        })
262        .unwrap_or_default()
263}
264
265fn count_component_response_schemas(
266    root: &Value,
267    specification_version: OpenApiSpecificationVersion,
268) -> usize {
269    component_entries(root, specification_version, "responses")
270        .map(|responses| {
271            responses
272                .values()
273                .filter_map(Value::as_object)
274                .map(|response| {
275                    let response_value = Value::Object(response.clone());
276                    count_response_value_schemas(&response_value)
277                        + count_response_header_schemas_for_object(response)
278                })
279                .sum::<usize>()
280        })
281        .unwrap_or_default()
282}
283
284fn count_component_links(
285    root: &Value,
286    specification_version: OpenApiSpecificationVersion,
287) -> usize {
288    component_entries(root, specification_version, "links")
289        .map(|links| {
290            links
291                .values()
292                .filter(|link| {
293                    link.as_object()
294                        .is_some_and(|link| !is_reference_object(link))
295                })
296                .count()
297        })
298        .unwrap_or_default()
299}
300
301fn count_component_response_links(
302    root: &Value,
303    specification_version: OpenApiSpecificationVersion,
304) -> usize {
305    component_entries(root, specification_version, "responses")
306        .map(|responses| {
307            responses
308                .values()
309                .filter_map(Value::as_object)
310                .map(count_response_links_for_object)
311                .sum::<usize>()
312        })
313        .unwrap_or_default()
314}
315
316fn count_component_callbacks(
317    root: &Value,
318    specification_version: OpenApiSpecificationVersion,
319) -> usize {
320    component_entries(root, specification_version, "callbacks")
321        .map(|callbacks| count_callback_entries(callbacks))
322        .unwrap_or_default()
323}
324
325fn count_swagger2_body_parameters(parameters: &[Value]) -> usize {
326    parameters
327        .iter()
328        .filter(|parameter| {
329            parameter
330                .as_object()
331                .is_some_and(|parameter| !is_reference_object(parameter))
332                && parameter
333                    .get("in")
334                    .and_then(Value::as_str)
335                    .is_some_and(|location| location == "body")
336        })
337        .count()
338}
339
340fn count_parameter_schemas(parameters: &[Value]) -> usize {
341    parameters
342        .iter()
343        .map(|parameter| {
344            parameter
345                .as_object()
346                .map(count_parameter_schema_for_object)
347                .unwrap_or_default()
348        })
349        .sum::<usize>()
350}
351
352fn count_request_body_schemas(request_body: &Value) -> usize {
353    let Some(request_body) = request_body.as_object() else {
354        return 0;
355    };
356
357    if is_reference_object(request_body) {
358        return 0;
359    }
360
361    request_body
362        .get("content")
363        .and_then(Value::as_object)
364        .map(count_media_type_schemas)
365        .unwrap_or_default()
366}
367
368fn count_media_type_schemas(content: &Map<String, Value>) -> usize {
369    content
370        .values()
371        .filter_map(|media_type| media_type.get("schema"))
372        .map(count_schema_objects)
373        .sum::<usize>()
374}
375
376fn count_response_schemas(responses: &Map<String, Value>) -> usize {
377    responses
378        .values()
379        .map(count_response_value_schemas)
380        .sum::<usize>()
381}
382
383fn count_response_value_schemas(response: &Value) -> usize {
384    let Some(response) = response.as_object() else {
385        return 0;
386    };
387
388    if is_reference_object(response) {
389        return 0;
390    }
391
392    response
393        .get("schema")
394        .map(count_schema_objects)
395        .unwrap_or_default()
396        + response
397            .get("content")
398            .and_then(Value::as_object)
399            .map(count_media_type_schemas)
400            .unwrap_or_default()
401}
402
403fn count_response_header_schemas(responses: &Map<String, Value>) -> usize {
404    responses
405        .values()
406        .filter_map(Value::as_object)
407        .map(count_response_header_schemas_for_object)
408        .sum::<usize>()
409}
410
411fn count_response_header_schemas_for_object(response: &Map<String, Value>) -> usize {
412    if is_reference_object(response) {
413        return 0;
414    }
415
416    response
417        .get("headers")
418        .and_then(Value::as_object)
419        .map(count_header_schemas)
420        .unwrap_or_default()
421}
422
423fn count_header_schemas(headers: &Map<String, Value>) -> usize {
424    headers
425        .values()
426        .filter_map(Value::as_object)
427        .map(count_header_schema_for_object)
428        .sum::<usize>()
429}
430
431fn count_response_links(responses: &Map<String, Value>) -> usize {
432    responses
433        .values()
434        .filter_map(Value::as_object)
435        .map(count_response_links_for_object)
436        .sum::<usize>()
437}
438
439fn count_response_links_for_object(response: &Map<String, Value>) -> usize {
440    if is_reference_object(response) {
441        return 0;
442    }
443
444    response
445        .get("links")
446        .and_then(Value::as_object)
447        .map(|links| {
448            links
449                .values()
450                .filter(|link| {
451                    link.as_object()
452                        .is_some_and(|link| !is_reference_object(link))
453                })
454                .count()
455        })
456        .unwrap_or_default()
457}
458
459fn count_parameter_schema_for_object(parameter: &Map<String, Value>) -> usize {
460    if is_reference_object(parameter) {
461        return 0;
462    }
463
464    if let Some(schema) = parameter.get("schema") {
465        return count_schema_objects(schema);
466    }
467
468    synthesized_parameter_schema(parameter)
469        .map(|schema| count_schema_objects(&schema))
470        .unwrap_or_default()
471}
472
473fn count_header_schema_for_object(header: &Map<String, Value>) -> usize {
474    if is_reference_object(header) {
475        return 0;
476    }
477
478    header
479        .get("schema")
480        .map(count_schema_objects)
481        .unwrap_or_default()
482}
483
484fn synthesized_parameter_schema(parameter: &Map<String, Value>) -> Option<Value> {
485    let mut schema = Map::new();
486
487    for field_name in ["type", "items", "allOf", "oneOf", "anyOf", "properties"] {
488        if let Some(value) = parameter.get(field_name) {
489            schema.insert(field_name.to_string(), value.clone());
490        }
491    }
492
493    if schema.is_empty() {
494        None
495    } else {
496        Some(Value::Object(schema))
497    }
498}
499
500fn count_schema_objects(value: &Value) -> usize {
501    let Some(schema) = value.as_object() else {
502        return 0;
503    };
504
505    let mut count = usize::from(is_schema_object(schema));
506
507    if let Some(properties) = schema.get("properties").and_then(Value::as_object) {
508        count += properties.values().map(count_schema_objects).sum::<usize>();
509    }
510
511    if let Some(items) = schema.get("items") {
512        count += count_schema_objects(items);
513    }
514
515    if let Some(additional_properties) = schema.get("additionalProperties") {
516        count += count_schema_objects(additional_properties);
517    }
518
519    for field_name in ["allOf", "oneOf", "anyOf"] {
520        if let Some(values) = schema.get(field_name).and_then(Value::as_array) {
521            count += values.iter().map(count_schema_objects).sum::<usize>();
522        }
523    }
524
525    count
526}
527
528fn is_schema_object(schema: &Map<String, Value>) -> bool {
529    schema.contains_key("type")
530        || schema.contains_key("properties")
531        || schema.contains_key("items")
532        || schema.contains_key("allOf")
533        || schema.contains_key("oneOf")
534        || schema.contains_key("anyOf")
535        || schema.contains_key("additionalProperties")
536}
537
538fn is_reference_object(value: &Map<String, Value>) -> bool {
539    value.contains_key("$ref") && value.len() == 1
540}
541
542#[cfg(test)]
543mod tests {
544    use std::path::PathBuf;
545
546    use crate::{OpenApiSource, OpenApiSpecificationVersion, decode_raw_document};
547
548    use super::{OpenApiStats, inspect_raw_document};
549
550    #[test]
551    fn inspects_petstore_v30_with_dotnet_parity_counts() {
552        let raw = decode_raw_document(
553            OpenApiSource::Path(PathBuf::from("test/OpenAPI/v3.0/petstore.json")),
554            include_str!("../../../../test/OpenAPI/v3.0/petstore.json"),
555        )
556        .unwrap();
557
558        let inspection = inspect_raw_document(&raw).unwrap();
559
560        assert_eq!(
561            inspection.specification_version,
562            OpenApiSpecificationVersion::OpenApi30
563        );
564        assert_eq!(inspection.stats.path_item_count, 13);
565        assert_eq!(inspection.stats.operation_count, 19);
566        assert_eq!(inspection.stats.parameter_count, 17);
567        assert_eq!(inspection.stats.request_body_count, 9);
568        assert_eq!(inspection.stats.response_count, 19);
569        assert_eq!(inspection.stats.link_count, 0);
570        assert_eq!(inspection.stats.callback_count, 0);
571        assert_eq!(inspection.stats.schema_count, 73);
572    }
573
574    #[test]
575    fn inspects_petstore_v20_with_body_parameters_as_request_bodies() {
576        let raw = decode_raw_document(
577            OpenApiSource::Path(PathBuf::from("test/OpenAPI/v2.0/petstore.json")),
578            include_str!("../../../../test/OpenAPI/v2.0/petstore.json"),
579        )
580        .unwrap();
581
582        let inspection = inspect_raw_document(&raw).unwrap();
583
584        assert_eq!(
585            inspection.specification_version,
586            OpenApiSpecificationVersion::Swagger2
587        );
588        assert!(inspection.stats.request_body_count > 0);
589        assert!(inspection.stats.schema_count > 0);
590    }
591
592    #[test]
593    fn inspects_callback_examples_with_callbacks() {
594        let raw = decode_raw_document(
595            OpenApiSource::Path(PathBuf::from("test/OpenAPI/v3.0/callback-example.json")),
596            include_str!("../../../../test/OpenAPI/v3.0/callback-example.json"),
597        )
598        .unwrap();
599
600        let inspection = inspect_raw_document(&raw).unwrap();
601
602        assert!(inspection.stats.callback_count > 0);
603    }
604
605    #[test]
606    fn empty_stats_start_at_zero() {
607        let stats = OpenApiStats::default();
608
609        assert_eq!(stats.path_item_count, 0);
610        assert_eq!(stats.operation_count, 0);
611        assert_eq!(stats.parameter_count, 0);
612        assert_eq!(stats.request_body_count, 0);
613        assert_eq!(stats.response_count, 0);
614        assert_eq!(stats.link_count, 0);
615        assert_eq!(stats.callback_count, 0);
616        assert_eq!(stats.schema_count, 0);
617    }
618}