openapi_nexus/generators/
multipart.rs1use 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}