unistructgen_openapi_parser/
client.rs1use crate::error::Result;
4use crate::options::OpenApiParserOptions;
5use crate::types::{extract_type_name_from_ref, sanitize_field_name, to_pascal_case};
6use openapiv3::{OpenAPI, Operation, PathItem, ReferenceOr};
7use unistructgen_core::{IRField, IRStruct, IRType, IRTypeRef, PrimitiveKind};
8
9pub struct ClientGenerator<'a> {
11 spec: &'a OpenAPI,
12 options: &'a OpenApiParserOptions,
13}
14
15impl<'a> ClientGenerator<'a> {
16 pub fn new(spec: &'a OpenAPI, options: &'a OpenApiParserOptions) -> Self {
18 Self { spec, options }
19 }
20
21 pub fn generate_client_types(&self) -> Result<Vec<IRType>> {
23 let mut types = Vec::new();
24
25 for (path, path_item) in &self.spec.paths.paths {
27 let path_item = match path_item {
28 ReferenceOr::Item(item) => item,
29 ReferenceOr::Reference { .. } => continue,
30 };
31
32 self.generate_path_types(path, path_item, &mut types)?;
33 }
34
35 Ok(types)
36 }
37
38 fn generate_path_types(
40 &self,
41 path: &str,
42 path_item: &PathItem,
43 types: &mut Vec<IRType>,
44 ) -> Result<()> {
45 if let Some(op) = &path_item.get {
47 self.generate_operation_types(path, "Get", op, types)?;
48 }
49 if let Some(op) = &path_item.post {
50 self.generate_operation_types(path, "Post", op, types)?;
51 }
52 if let Some(op) = &path_item.put {
53 self.generate_operation_types(path, "Put", op, types)?;
54 }
55 if let Some(op) = &path_item.delete {
56 self.generate_operation_types(path, "Delete", op, types)?;
57 }
58 if let Some(op) = &path_item.patch {
59 self.generate_operation_types(path, "Patch", op, types)?;
60 }
61
62 Ok(())
63 }
64
65 fn generate_operation_types(
67 &self,
68 path: &str,
69 method: &str,
70 operation: &Operation,
71 types: &mut Vec<IRType>,
72 ) -> Result<()> {
73 let operation_name = if let Some(operation_id) = &operation.operation_id {
75 to_pascal_case(operation_id)
76 } else {
77 let path_parts: Vec<_> = path
79 .split('/')
80 .filter(|s| !s.is_empty() && !s.starts_with('{'))
81 .collect();
82 format!("{}{}", method, path_parts.join(""))
83 };
84
85 if !operation.parameters.is_empty() || operation.request_body.is_some() {
87 let request_type = self.generate_request_type(&operation_name, operation)?;
88 if let Some(ty) = request_type {
89 types.push(ty);
90 }
91 }
92
93 for (status_code, response_ref) in &operation.responses.responses {
95 let response = match response_ref {
96 ReferenceOr::Item(resp) => resp,
97 ReferenceOr::Reference { .. } => continue,
98 };
99
100 let _response_name = format!("{}{}Response", operation_name, status_code);
101
102 if let Some(media_type) = response.content.get("application/json") {
104 if let Some(schema_ref) = &media_type.schema {
105 match schema_ref {
107 ReferenceOr::Reference { .. } => {
108 continue;
110 }
111 ReferenceOr::Item(_schema) => {
112 continue;
115 }
116 }
117 }
118 }
119 }
120
121 Ok(())
122 }
123
124 fn generate_request_type(
126 &self,
127 operation_name: &str,
128 operation: &Operation,
129 ) -> Result<Option<IRType>> {
130 let mut ir_struct = IRStruct::new(format!("{}Request", operation_name));
131
132 if self.options.generate_docs {
134 if let Some(summary) = &operation.summary {
135 ir_struct.doc = Some(format!("Request parameters for {}", summary));
136 }
137 }
138
139 if self.options.derive_serde {
141 ir_struct.add_derive("serde::Serialize".to_string());
142 ir_struct.add_derive("serde::Deserialize".to_string());
143 }
144 if self.options.derive_default {
145 ir_struct.add_derive("Default".to_string());
146 }
147
148 for param_ref in &operation.parameters {
150 let param = match param_ref {
151 ReferenceOr::Item(p) => p,
152 ReferenceOr::Reference { .. } => continue,
153 };
154
155 match param {
156 openapiv3::Parameter::Query { parameter_data, .. }
157 | openapiv3::Parameter::Path { parameter_data, .. }
158 | openapiv3::Parameter::Header { parameter_data, .. } => {
159 let field_name = sanitize_field_name(¶meter_data.name);
160
161 let field_type = match ¶meter_data.format {
163 openapiv3::ParameterSchemaOrContent::Schema(schema_ref) => {
164 match schema_ref {
165 ReferenceOr::Reference { reference } => {
166 IRTypeRef::Named(extract_type_name_from_ref(reference))
167 }
168 ReferenceOr::Item(_schema) => {
169 IRTypeRef::Primitive(PrimitiveKind::String)
171 }
172 }
173 }
174 openapiv3::ParameterSchemaOrContent::Content(_) => {
175 IRTypeRef::Primitive(PrimitiveKind::String)
176 }
177 };
178
179 let mut field = IRField::new(field_name.clone(), field_type);
180
181 if !parameter_data.required {
183 field.ty = field.ty.make_optional();
184 field.optional = true;
185 }
186
187 if self.options.generate_docs {
189 if let Some(desc) = ¶meter_data.description {
190 field.doc = Some(desc.clone());
191 }
192 }
193
194 if field_name != parameter_data.name {
196 field.source_name = Some(parameter_data.name.clone());
197 field.attributes.push(format!(
198 "#[serde(rename = \"{}\")]",
199 parameter_data.name
200 ));
201 }
202
203 ir_struct.add_field(field);
204 }
205 _ => {}
206 }
207 }
208
209 if ir_struct.fields.is_empty() {
211 return Ok(None);
212 }
213
214 Ok(Some(IRType::Struct(ir_struct)))
215 }
216
217 pub fn generate_client_trait_doc(&self) -> String {
219 let mut output = String::new();
220
221 output.push_str("// API Client Trait\n");
222 output.push_str("// This trait can be implemented to create an API client\n\n");
223 output.push_str("#[async_trait::async_trait]\n");
224 output.push_str("pub trait ApiClient {\n");
225
226 for (path, path_item) in &self.spec.paths.paths {
227 let path_item = match path_item {
228 ReferenceOr::Item(item) => item,
229 ReferenceOr::Reference { .. } => continue,
230 };
231
232 self.generate_client_methods(path, path_item, &mut output);
233 }
234
235 output.push_str("}\n");
236 output
237 }
238
239 fn generate_client_methods(&self, path: &str, path_item: &PathItem, output: &mut String) {
240 if let Some(op) = &path_item.get {
241 self.generate_client_method(path, "get", op, output);
242 }
243 if let Some(op) = &path_item.post {
244 self.generate_client_method(path, "post", op, output);
245 }
246 if let Some(op) = &path_item.put {
247 self.generate_client_method(path, "put", op, output);
248 }
249 if let Some(op) = &path_item.delete {
250 self.generate_client_method(path, "delete", op, output);
251 }
252 }
253
254 fn generate_client_method(
255 &self,
256 path: &str,
257 method: &str,
258 operation: &Operation,
259 output: &mut String,
260 ) {
261 let operation_name = if let Some(operation_id) = &operation.operation_id {
262 sanitize_field_name(operation_id)
263 } else {
264 format!("{}_{}", method, path.replace(['/', '{', '}'], "_"))
265 };
266
267 output.push_str(&format!(" async fn {}(", operation_name));
268 output.push_str("&self");
269
270 for param_ref in &operation.parameters {
272 if let ReferenceOr::Item(param) = param_ref {
273 match param {
274 openapiv3::Parameter::Path { parameter_data, .. } => {
275 let param_name = sanitize_field_name(¶meter_data.name);
276 output.push_str(&format!(", {}: &str", param_name));
277 }
278 _ => {}
279 }
280 }
281 }
282
283 output.push_str(") -> Result<serde_json::Value, Box<dyn std::error::Error>>;\n\n");
284 }
285}
286
287#[cfg(test)]
288mod tests {
289 use super::*;
290
291 #[test]
292 fn test_operation_name_generation() {
293 let name = to_pascal_case("get_users");
294 assert_eq!(name, "GetUsers");
295 }
296}