Skip to main content

openapi_nexus/generators/
multipart.rs

1//! Shared multipart/form-data request-body planning.
2
3use crate::ir::types::{IrObject, IrPrimitive, IrRequestBody, IrSchemaKind, IrSpec, IrTypeExpr};
4
5#[derive(Debug, Clone)]
6pub struct MultipartPart {
7    pub wire_name: String,
8    pub default_filename: String,
9    pub type_expr: IrTypeExpr,
10    pub is_binary: bool,
11    pub required: bool,
12    pub content_type: String,
13    pub value_encoding: MultipartValueEncoding,
14}
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub enum MultipartValueEncoding {
18    Text,
19    Json,
20    Unsupported,
21}
22
23pub fn multipart_parts_for_request_body(
24    body: &IrRequestBody,
25    media_type: &str,
26    ir: &IrSpec,
27) -> Option<Vec<MultipartPart>> {
28    let t = body.content.get(media_type)?;
29    let media_encoding = body.encoding.get(media_type);
30    resolve_object(t, ir).map(|obj| {
31        obj.properties
32            .iter()
33            .map(|(wire_name, prop)| {
34                let explicit_content_type = media_encoding
35                    .and_then(|encoding| encoding.get(wire_name))
36                    .and_then(|encoding| encoding.content_type.clone());
37                multipart_part_from_property(
38                    wire_name,
39                    &prop.type_expr,
40                    prop.required && !prop.nullable,
41                    explicit_content_type,
42                    ir,
43                )
44            })
45            .collect()
46    })
47}
48
49fn multipart_part_from_property(
50    wire_name: &str,
51    type_expr: &IrTypeExpr,
52    required: bool,
53    explicit_content_type: Option<String>,
54    ir: &IrSpec,
55) -> MultipartPart {
56    let is_binary = is_binary_type(type_expr, ir);
57    let is_text = is_multipart_text_type(type_expr, ir);
58    let content_type = explicit_content_type.unwrap_or_else(|| {
59        if is_binary {
60            "application/octet-stream".to_string()
61        } else if is_text {
62            "text/plain".to_string()
63        } else {
64            "application/json".to_string()
65        }
66    });
67    let value_encoding = if is_binary {
68        MultipartValueEncoding::Text
69    } else if is_json_media_type(&content_type) {
70        MultipartValueEncoding::Json
71    } else if is_text {
72        MultipartValueEncoding::Text
73    } else {
74        MultipartValueEncoding::Unsupported
75    };
76
77    MultipartPart {
78        wire_name: wire_name.to_string(),
79        default_filename: wire_name.to_string(),
80        type_expr: type_expr.clone(),
81        is_binary,
82        required,
83        content_type,
84        value_encoding,
85    }
86}
87
88fn resolve_object<'a>(expr: &IrTypeExpr, ir: &'a IrSpec) -> Option<&'a IrObject> {
89    match expr {
90        IrTypeExpr::Named(name) => match ir.schemas.get(name).map(|schema| &schema.kind) {
91            Some(IrSchemaKind::Object(obj)) => Some(obj),
92            Some(IrSchemaKind::Alias(inner)) => resolve_object(inner, ir),
93            _ => None,
94        },
95        IrTypeExpr::Nullable(inner) => resolve_object(inner, ir),
96        _ => None,
97    }
98}
99
100fn is_binary_type(expr: &IrTypeExpr, ir: &IrSpec) -> bool {
101    match expr {
102        IrTypeExpr::Primitive(IrPrimitive::Binary) => true,
103        IrTypeExpr::Nullable(inner) => is_binary_type(inner, ir),
104        IrTypeExpr::Named(name) => ir.schemas.get(name).is_some_and(|schema| {
105            matches!(&schema.kind, IrSchemaKind::Alias(inner) if is_binary_type(inner, ir))
106        }),
107        _ => false,
108    }
109}
110
111fn is_multipart_text_type(expr: &IrTypeExpr, ir: &IrSpec) -> bool {
112    match expr {
113        IrTypeExpr::Primitive(_) | IrTypeExpr::StringLiteral(_) | IrTypeExpr::StringEnum(_) => true,
114        IrTypeExpr::Nullable(inner) => is_multipart_text_type(inner, ir),
115        IrTypeExpr::Named(name) => ir.schemas.get(name).is_some_and(|schema| {
116            matches!(&schema.kind, IrSchemaKind::Alias(inner) if is_multipart_text_type(inner, ir))
117        }),
118        _ => false,
119    }
120}
121
122pub fn is_json_media_type(media_type: &str) -> bool {
123    let base = media_type_base(media_type);
124    base == "application/json" || base.ends_with("+json")
125}
126
127pub fn media_type_base(media_type: &str) -> String {
128    media_type
129        .split(';')
130        .next()
131        .unwrap_or(media_type)
132        .trim()
133        .to_ascii_lowercase()
134}