schemadoc_diff/schemas/swagger2/
converter.rs

1use crate::core::{Either, ReferenceDescriptor};
2use indexmap::IndexMap;
3
4use crate::schema as core;
5
6use crate::schemas::swagger2::context::*;
7use crate::schemas::swagger2::schema::*;
8
9pub const VERSION: &str = "0.1.0";
10
11struct ConvertContext<'a> {
12    pub consumes: &'a Option<Vec<String>>,
13    pub produces: &'a Option<Vec<String>>,
14    // pub definitions: &'a Option<IndexMap<String, V2Schema>>,
15    pub parameters: &'a Option<IndexMap<String, Parameter>>,
16    pub responses: &'a Option<IndexMap<String, Response>>,
17}
18
19impl From<SwaggerV2> for core::HttpSchema {
20    fn from(spec: SwaggerV2) -> Self {
21        let paths = {
22            let context = ConvertContext {
23                consumes: &spec.consumes,
24                produces: &spec.produces,
25                // definitions: &spec.definitions,
26                parameters: &spec.parameters,
27                responses: &spec.responses,
28            };
29
30            spec.paths.map(|paths| convert_paths(paths, &context))
31        };
32
33        let parameters = spec.parameters.map(|parameters| {
34            parameters
35                .into_iter()
36                .map(|(key, parameter)| {
37                    (key, core::MayBeRef::Value(convert_parameter(parameter)))
38                })
39                .collect::<IndexMap<_, _>>()
40        });
41
42        let responses = spec.responses.map(|responses| {
43            responses
44                .into_iter()
45                .map(|(key, response)| {
46                    (
47                        key,
48                        core::MayBeRef::Value(convert_response(
49                            response,
50                            &["application/json".to_string()],
51                        )),
52                    )
53                })
54                .collect::<IndexMap<_, _>>()
55        });
56
57        let schemas = spec.definitions.map(|definitions| {
58            definitions
59                .into_iter()
60                .map(|(key, schema)| {
61                    (key, core::MayBeRef::Value(convert_schema(schema)))
62                })
63                .collect::<IndexMap<_, _>>()
64        });
65
66        let components = core::Components {
67            schemas,
68            responses,
69            parameters,
70            examples: None,
71            request_bodies: None,
72            headers: None,
73            security_schemes: None,
74            links: None,
75        };
76
77        let info = spec.info.map(convert_info);
78
79        core::HttpSchema {
80            version: spec.swagger,
81
82            schema_source: SwaggerV2::id().to_owned(),
83            schema_source_version: VERSION.to_owned(),
84            schema_version: core::HttpSchema::schema_version().to_owned(),
85
86            info,
87            servers: None,
88            paths,
89            components: Some(components),
90            tags: None,
91            external_docs: None,
92        }
93    }
94}
95
96fn convert_paths(
97    paths: IndexMap<String, Path>,
98    context: &ConvertContext,
99) -> IndexMap<String, core::MayBeRef<core::Path>> {
100    paths
101        .into_iter()
102        .map(|(key, path)| {
103            (key, core::MayBeRef::Value(convert_path(path, context)))
104        })
105        .collect()
106}
107
108fn convert_path(path: Path, context: &ConvertContext) -> core::Path {
109    let parameters = &path.parameters;
110
111    core::Path {
112        get: path
113            .get
114            .map(|op| convert_operation(op, parameters, context)),
115        put: path
116            .put
117            .map(|op| convert_operation(op, parameters, context)),
118        post: path
119            .post
120            .map(|op| convert_operation(op, parameters, context)),
121        delete: path
122            .delete
123            .map(|op| convert_operation(op, parameters, context)),
124        options: path
125            .options
126            .map(|op| convert_operation(op, parameters, context)),
127        head: path
128            .head
129            .map(|op| convert_operation(op, parameters, context)),
130        patch: path
131            .patch
132            .map(|op| convert_operation(op, parameters, context)),
133        trace: None,
134        servers: None,
135        summary: None,
136        description: None,
137    }
138}
139
140fn convert_info(info: Info) -> core::Info {
141    core::Info {
142        title: info.title,
143        version: info.version,
144        description: info.description,
145        terms_of_service: info.terms_of_service,
146        contact: info.contact.map(convert_contact),
147        license: info.license.map(convert_license),
148    }
149}
150
151fn convert_contact(contact: Contact) -> core::Contact {
152    core::Contact {
153        url: contact.url,
154        name: contact.name,
155        email: contact.email,
156    }
157}
158
159fn convert_license(license: License) -> core::License {
160    core::License {
161        url: license.url,
162        name: license.name,
163    }
164}
165
166fn merge_parameters(
167    components: &Option<IndexMap<String, Parameter>>,
168    parameters_refs: Option<Vec<MayBeRef200<Parameter>>>,
169    path_parameters_refs: &Option<Vec<MayBeRef200<Parameter>>>,
170) -> Vec<(String, String, MayBeRef200<Parameter>)> {
171    let mut parameters: Vec<MayBeRef200<Parameter>> = Vec::new();
172
173    if let Some(parameters_refs) = parameters_refs {
174        parameters.extend(parameters_refs);
175    }
176
177    if let Some(parameters_refs) = path_parameters_refs {
178        parameters.extend(parameters_refs.clone());
179    }
180
181    let mut visited = Vec::new();
182
183    let mut result = Vec::with_capacity(parameters.len());
184
185    for may_be_parameter in parameters {
186        let key = match &may_be_parameter {
187            MayBeRef200::Ref(value) => {
188                if let Some(parameter) =
189                    deref_parameter(components, value.reference())
190                {
191                    (parameter.name.clone(), parameter.r#in.clone())
192                } else {
193                    // TODO: handle the case where ref not found
194                    continue;
195                }
196            }
197            MayBeRef200::Value(value) => {
198                (value.name.clone(), value.r#in.clone())
199            }
200        };
201
202        if visited.contains(&key) {
203            continue;
204        }
205
206        result.push((key.0.clone(), key.1.clone(), may_be_parameter));
207
208        visited.push(key);
209    }
210
211    result
212}
213
214fn convert_operation(
215    operation: Operation,
216    path_parameters: &Option<Vec<MayBeRef200<Parameter>>>,
217    context: &ConvertContext,
218) -> core::Operation {
219    let merged_parameters = merge_parameters(
220        context.parameters, // TODO: pass components
221        operation.parameters,
222        path_parameters,
223    );
224
225    if operation.operation_id == Some("workspace-regions_create".to_string()) {
226        println!("found");
227    }
228
229    let mut body_parameters: Vec<_> = Vec::with_capacity(1);
230    let mut parameters: Vec<_> = Vec::with_capacity(merged_parameters.len());
231
232    for (_name, loc, parameter) in merged_parameters {
233        if loc == "body" || loc == "formData" {
234            body_parameters.push(parameter)
235        } else {
236            parameters.push(convert_parameter_ref(parameter))
237        }
238    }
239
240    let request_body =
241        convert_to_request_body(body_parameters, &operation.consumes, context);
242
243    let mut produces = operation
244        .produces
245        .unwrap_or_else(|| context.produces.clone().unwrap_or_default());
246
247    if produces.is_empty() {
248        produces.push("application/json".to_string())
249    }
250
251    let unref = context
252        .produces
253        .as_ref()
254        .map(|global_produces| global_produces != &produces)
255        .unwrap_or_else(|| produces != vec!["application/json".to_string()]);
256
257    let responses = operation
258        .responses
259        .into_iter()
260        .map(|(code, response_ref)| {
261            (
262                code,
263                convert_response_ref(response_ref, &produces, context, unref),
264            )
265        })
266        .collect();
267
268    let external_docs = operation.external_docs.map(convert_external_docs);
269
270    core::Operation {
271        tags: Some(operation.tags),
272        summary: operation.summary,
273        description: operation.description,
274        external_docs,
275        operation_id: operation.operation_id,
276        responses: Some(responses),
277        request_body,
278        servers: None,
279        parameters: Some(parameters),
280        security: None, // TODO: add security policies
281        deprecated: operation.deprecated,
282    }
283}
284
285fn convert_to_request_body(
286    parameters: Vec<MayBeRef200<Parameter>>,
287    consumes: &Option<Vec<String>>,
288    context: &ConvertContext,
289) -> Option<core::MayBeRef<core::RequestBody>> {
290    if let Some(body) = parameters.first() {
291        let parameter = match body {
292            MayBeRef200::Ref(value) => {
293                deref_parameter(context.parameters, value.reference())
294            }
295            MayBeRef200::Value(value) => Some(value),
296        };
297
298        // deref parameter since to do not move any parameters to components
299        if let Some(parameter) = parameter {
300            let schema = if let Some(schema) = parameter.schema.clone() {
301                schema
302            } else {
303                MayBeRef200::Value(Schema {
304                    multiple_of: parameter.multiple_of,
305                    maximum: parameter.maximum,
306                    exclusive_maximum: parameter.exclusive_maximum,
307                    minimum: parameter.minimum,
308                    exclusive_minimum: parameter.exclusive_minimum,
309                    max_length: parameter.max_length,
310                    min_length: parameter.min_length,
311                    pattern: parameter.pattern.clone(),
312                    max_items: parameter.max_items,
313                    min_items: parameter.min_items,
314                    unique_items: parameter.unique_items,
315                    r#enum: parameter.r#enum.clone(),
316                    r#type: parameter.r#type.clone(),
317                    items: Box::new(parameter.items.clone()),
318                    format: parameter.format.clone(),
319                    default: parameter.default.clone(),
320                    ..Default::default()
321                })
322            };
323
324            let mut media_types = consumes.clone().unwrap_or_else(|| {
325                context.consumes.clone().unwrap_or_default()
326            });
327
328            if media_types.is_empty() {
329                media_types.push("application/json".to_string())
330            }
331
332            let content = convert_schema_to_media_type(schema, media_types);
333
334            Some(core::MayBeRef::Value(core::RequestBody {
335                content: Some(content),
336                required: parameter.required,
337                description: parameter.description.clone(),
338            }))
339        } else {
340            None
341        }
342    } else {
343        None
344    }
345}
346
347fn convert_response_ref(
348    response_ref: MayBeRef200<Response>,
349    produces: &[String],
350    context: &ConvertContext,
351    unref: bool,
352) -> core::MayBeRef<core::Response> {
353    match response_ref {
354        MayBeRef200::Ref(value) => {
355            if unref {
356                let reference = value.reference.replace("#/responses/", "");
357
358                let response = context
359                    .responses
360                    .as_ref()
361                    .and_then(|responses| responses.get(&reference).cloned())
362                    .unwrap();
363
364                core::MayBeRef::Value(convert_response(response, produces))
365            } else {
366                core::MayBeRef::Ref(core::HttpSchemaRef {
367                    reference: value
368                        .reference
369                        .replace("#/responses", "#/components/responses"),
370                })
371            }
372        }
373        MayBeRef200::Value(response) => {
374            core::MayBeRef::Value(convert_response(response, produces))
375        }
376    }
377}
378
379fn convert_response(
380    response: Response,
381    produces: &[String],
382) -> core::Response {
383    let headers = response.headers.map(|headers| {
384        headers
385            .into_iter()
386            .map(|(key, header)| {
387                (key, core::MayBeRef::Value(convert_header(header)))
388            })
389            .collect()
390    });
391
392    let content = response
393        .schema
394        .map(|sc| convert_schema_to_media_type(sc, produces.to_owned()));
395
396    core::Response {
397        description: response.description,
398        headers,
399        content,
400        links: None,
401    }
402}
403
404fn convert_schema_to_media_type(
405    schema: MayBeRef200<Schema>,
406    media_types: Vec<String>,
407) -> IndexMap<String, core::MediaType> {
408    let media_type = core::MediaType {
409        schema: Some(convert_schema_ref(schema)),
410        examples: None,
411        encoding: None,
412    };
413
414    media_types
415        .into_iter()
416        .map(|mime_type| (mime_type, media_type.clone()))
417        .collect()
418}
419
420fn convert_header(header: Header) -> core::Header {
421    let items = header.items.map(convert_schema_ref);
422
423    let explode = header.format.as_ref().map(|format| format == "multi");
424
425    let schema = core::Schema {
426        multiple_of: header.multiple_of,
427        maximum: header.maximum,
428        exclusive_maximum: header.exclusive_maximum,
429        minimum: header.minimum,
430        exclusive_minimum: header.exclusive_minimum,
431        max_length: header.max_length,
432        min_length: header.min_length,
433        pattern: header.pattern,
434        max_items: header.max_items,
435        min_items: header.min_items,
436        unique_items: header.unique_items,
437        r#enum: header.r#enum,
438        r#type: Some(Either::Left(header.r#type)),
439        items: Box::new(items),
440        format: header.format,
441        default: header.default,
442        ..Default::default()
443    };
444
445    // collectionFormat in ('ssv', 'pipes', 'tsv') not supported
446
447    core::Header {
448        schema: Some(core::MayBeRef::Value(schema)),
449        description: header.description,
450        required: None,
451        deprecated: None,
452        allow_empty_value: None,
453        style: None,
454        explode,
455        allow_reserved: None,
456        examples: None,
457        content: None,
458        custom_fields: Default::default(),
459    }
460}
461
462fn convert_parameter_ref(
463    parameter_ref: MayBeRef200<Parameter>,
464) -> core::MayBeRef<core::Parameter> {
465    match parameter_ref {
466        MayBeRef200::Ref(value) => core::MayBeRef::Ref(core::HttpSchemaRef {
467            reference: value
468                .reference
469                .replace("#/parameters", "#/components/parameters"),
470        }),
471        MayBeRef200::Value(parameter) => {
472            core::MayBeRef::Value(convert_parameter(parameter))
473        }
474    }
475}
476
477fn convert_parameter(parameter: Parameter) -> core::Parameter {
478    let schema = if let Some(schema) = parameter.schema.map(convert_schema_ref)
479    {
480        Some(schema)
481    } else {
482        let all_of = parameter.all_of.map(|all_of| {
483            all_of.into_iter().map(convert_schema_ref).collect()
484        });
485
486        let one_of = parameter.one_of.map(|one_of| {
487            one_of.into_iter().map(convert_schema_ref).collect()
488        });
489
490        let any_of = parameter.any_of.map(|any_of| {
491            any_of.into_iter().map(convert_schema_ref).collect()
492        });
493
494        let not = parameter
495            .not
496            .map(|not| not.into_iter().map(convert_schema_ref).collect());
497
498        let schema = core::Schema {
499            title: None,
500            multiple_of: None,
501            maximum: parameter.maximum,
502            exclusive_maximum: parameter.exclusive_maximum,
503            minimum: parameter.minimum,
504            exclusive_minimum: parameter.exclusive_minimum,
505            max_length: parameter.max_length,
506            min_length: parameter.min_length,
507            pattern: parameter.pattern,
508            max_items: parameter.max_items,
509            min_items: parameter.min_items,
510            unique_items: parameter.unique_items,
511            max_properties: None,
512            min_properties: None,
513            required: None,
514            r#enum: parameter.r#enum,
515            r#type: parameter.r#type,
516
517            all_of,
518            one_of,
519            any_of,
520            not,
521
522            items: Box::new(parameter.items.map(convert_schema_ref)),
523            properties: None,
524            additional_properties: None,
525            description: None,
526            format: parameter.format,
527            default: parameter.default,
528            discriminator: None,
529            read_only: None,
530            write_only: None,
531            xml: None,
532            external_docs: None,
533            example: None,
534            deprecated: None,
535            custom_fields: Default::default(),
536        };
537
538        Some(core::MayBeRef::Value(schema))
539    };
540
541    // TODO: add collection_format
542
543    core::Parameter {
544        name: parameter.name,
545        r#in: parameter.r#in,
546        description: parameter.description,
547        required: parameter.required,
548        deprecated: None,
549        allow_empty_value: parameter.allow_empty_value,
550        style: None,
551        explode: None,
552        allow_reserved: None,
553        schema,
554        examples: None,
555        content: None,
556        custom_fields: parameter.custom_fields,
557    }
558}
559
560fn convert_schema_ref(
561    schema_ref: MayBeRef200<Schema>,
562) -> core::MayBeRef<core::Schema> {
563    match schema_ref {
564        MayBeRef200::Ref(value) => core::MayBeRef::Ref(core::HttpSchemaRef {
565            reference: value
566                .reference
567                .replace("#/definitions", "#/components/schemas"),
568        }),
569        MayBeRef200::Value(schema) => {
570            core::MayBeRef::Value(convert_schema(schema))
571        }
572    }
573}
574
575fn convert_schema(schema: Schema) -> core::Schema {
576    let all_of = schema
577        .all_of
578        .map(|all_of| all_of.into_iter().map(convert_schema_ref).collect());
579
580    let one_of = schema
581        .one_of
582        .map(|one_of| one_of.into_iter().map(convert_schema_ref).collect());
583
584    let any_of = schema
585        .any_of
586        .map(|any_of| any_of.into_iter().map(convert_schema_ref).collect());
587
588    let not = schema
589        .not
590        .map(|not| not.into_iter().map(convert_schema_ref).collect());
591
592    let properties = schema.properties.map(|properties| {
593        properties
594            .into_iter()
595            .map(|(name, schema)| (name, convert_schema_ref(schema)))
596            .collect()
597    });
598
599    let additional_properties =
600        schema.additional_properties.map(|additional_properties| {
601            match additional_properties {
602                Either::Left(value) => Either::Left(value),
603                Either::Right(schema_ref) => {
604                    Either::Right(Box::new(convert_schema_ref(*schema_ref)))
605                }
606            }
607        });
608
609    let discriminator =
610        schema
611            .discriminator
612            .map(|discriminator| core::Discriminator {
613                property_name: Some(discriminator),
614                mapping: None,
615            });
616
617    let items = schema.items.map(convert_schema_ref);
618
619    let external_docs = schema.external_docs.map(convert_external_docs);
620
621    core::Schema {
622        title: schema.title,
623        multiple_of: schema.multiple_of,
624        maximum: schema.maximum,
625        exclusive_maximum: schema.exclusive_maximum,
626        minimum: schema.minimum,
627        exclusive_minimum: schema.exclusive_minimum,
628        max_length: schema.max_length,
629        min_length: schema.min_length,
630        pattern: schema.pattern,
631        max_items: schema.max_items,
632        min_items: schema.min_items,
633        unique_items: schema.unique_items,
634        max_properties: schema.max_properties,
635        min_properties: schema.min_properties,
636        required: schema.required,
637        r#enum: schema.r#enum,
638        r#type: schema.r#type,
639        all_of,
640        one_of,
641        any_of,
642        not,
643        items: Box::new(items),
644        properties,
645        additional_properties,
646        description: schema.description,
647        format: schema.format,
648        default: schema.default,
649        discriminator,
650        read_only: schema.read_only,
651        write_only: None,
652        xml: None,
653        external_docs,
654        example: schema.example,
655        deprecated: None,
656        custom_fields: schema.custom_fields,
657    }
658}
659
660fn convert_external_docs(external_docs: ExternalDoc) -> core::ExternalDoc {
661    core::ExternalDoc {
662        url: Some(external_docs.url),
663        description: external_docs.description,
664    }
665}
666
667#[cfg(test)]
668mod tests {
669    // use crate::schema::HttpSchema;
670    // use crate::schemas::swagger2::schema::SwaggerV2;
671
672    #[test]
673    fn test_converter() {
674        // let src_schema_content = include_str!("../../../tmp/cvt.json");
675        // let tgt_schema_content = include_str!("../../../tmp/cvt-altered.json");
676        //
677        // let src_schema_v2 = serde_json::from_str::<SwaggerV2>(src_schema_content).unwrap();
678        // let tgt_schema_v2 = serde_json::from_str::<SwaggerV2>(tgt_schema_content).unwrap();
679        //
680        // let src_schema: HttpSchema = src_schema_v2.into();
681        // let tgt_schema: HttpSchema = tgt_schema_v2.into();
682    }
683}