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