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 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}