1use heck::ToSnakeCase;
4use std::fmt::Write;
5
6use crate::ir::{
7 ApiKind, ApiSpec, EnumVariant, Field, HttpMethod, Operation, Param, ParamLocation, Protocol,
8 StreamingMode, TypeDef,
9};
10
11pub fn render(spec: &ApiSpec) -> String {
13 let mut out = String::new();
14
15 writeln!(
16 out,
17 "//! `{}` client crate, generated by `oxide-gen`.",
18 spec.display_name
19 )
20 .unwrap();
21 if let Some(desc) = &spec.description {
22 for line in desc.lines() {
23 writeln!(out, "//! {line}").unwrap();
24 }
25 }
26 writeln!(out, "//!").unwrap();
27 writeln!(out, "//! Spec kind: {}", spec.kind.slug()).unwrap();
28 writeln!(out, "//! Operations: {}", spec.operations.len()).unwrap();
29 writeln!(out, "//!").unwrap();
30 writeln!(
31 out,
32 "//! Do not edit by hand — re-run `oxide-gen` to regenerate."
33 )
34 .unwrap();
35 writeln!(out).unwrap();
36
37 writeln!(
38 out,
39 "#![allow(clippy::all, dead_code, unused_imports, unused_variables, unused_mut)]"
40 )
41 .unwrap();
42 writeln!(out).unwrap();
43 writeln!(out, "use serde::{{Deserialize, Serialize}};").unwrap();
44 writeln!(out).unwrap();
45
46 if spec.kind == ApiKind::Grpc {
48 writeln!(out, "pub mod proto {{").unwrap();
49 writeln!(out, " tonic::include_proto!(\"{}\");", spec.display_name).unwrap();
50 writeln!(out, "}}").unwrap();
51 writeln!(out).unwrap();
52 for td in &spec.types {
53 writeln!(out, "pub use proto::{};", td.name()).unwrap();
54 }
55 writeln!(out).unwrap();
56 } else {
57 for td in &spec.types {
58 render_type(&mut out, td);
59 writeln!(out).unwrap();
60 }
61 }
62
63 render_client(&mut out, spec);
65
66 out
67}
68
69fn render_type(out: &mut String, td: &TypeDef) {
70 match td {
71 TypeDef::Struct {
72 name,
73 description,
74 fields,
75 } => {
76 emit_doc(out, description.as_deref(), "");
77 writeln!(out, "#[derive(Debug, Clone, Serialize, Deserialize)]").unwrap();
78 writeln!(out, "pub struct {name} {{").unwrap();
79 for field in fields {
80 render_field(out, field);
81 }
82 writeln!(out, "}}").unwrap();
83 }
84 TypeDef::Enum {
85 name,
86 description,
87 variants,
88 } => {
89 emit_doc(out, description.as_deref(), "");
90 writeln!(out, "#[derive(Debug, Clone, Serialize, Deserialize)]").unwrap();
91 writeln!(out, "pub enum {name} {{").unwrap();
92 for v in variants {
93 render_variant(out, v);
94 }
95 writeln!(out, "}}").unwrap();
96 }
97 TypeDef::Alias { name, target } => {
98 writeln!(out, "pub type {name} = {target};").unwrap();
99 }
100 }
101}
102
103fn render_field(out: &mut String, field: &Field) {
104 emit_doc(out, field.description.as_deref(), " ");
105 if let Some(rename) = &field.serde_rename {
106 writeln!(out, " #[serde(rename = \"{}\")]", escape(rename)).unwrap();
107 }
108 if field.optional {
109 writeln!(
110 out,
111 " #[serde(skip_serializing_if = \"Option::is_none\")]"
112 )
113 .unwrap();
114 }
115 writeln!(out, " pub {}: {},", field.name, field.rust_type).unwrap();
116}
117
118fn render_variant(out: &mut String, v: &EnumVariant) {
119 if let Some(rename) = &v.serde_rename {
120 writeln!(out, " #[serde(rename = \"{}\")]", escape(rename)).unwrap();
121 }
122 writeln!(out, " {},", v.name).unwrap();
123}
124
125fn render_client(out: &mut String, spec: &ApiSpec) {
130 let is_http = matches!(spec.kind, ApiKind::OpenApi | ApiKind::GraphQl);
131 let default_base = spec.base_url.clone().unwrap_or_else(|| match spec.kind {
132 ApiKind::OpenApi => "https://example.com".into(),
133 ApiKind::GraphQl => "https://example.com/graphql".into(),
134 ApiKind::Grpc => "http://localhost:50051".into(),
135 });
136
137 writeln!(out, "/// Asynchronous client for `{}`.", spec.display_name).unwrap();
138 writeln!(out, "#[derive(Debug, Clone)]").unwrap();
139 writeln!(out, "pub struct Client {{").unwrap();
140 writeln!(out, " pub base_url: String,").unwrap();
141 if is_http {
142 writeln!(out, " pub http: reqwest::Client,").unwrap();
143 }
144 writeln!(out, "}}").unwrap();
145 writeln!(out).unwrap();
146
147 writeln!(out, "impl Client {{").unwrap();
148 writeln!(out, " /// Build a client pointing at `base_url`.").unwrap();
149 writeln!(
150 out,
151 " pub fn new(base_url: impl Into<String>) -> Self {{"
152 )
153 .unwrap();
154 writeln!(out, " Self {{").unwrap();
155 writeln!(out, " base_url: base_url.into(),").unwrap();
156 if is_http {
157 writeln!(out, " http: reqwest::Client::new(),").unwrap();
158 }
159 writeln!(out, " }}").unwrap();
160 writeln!(out, " }}").unwrap();
161 writeln!(out).unwrap();
162 writeln!(
163 out,
164 " /// Build a client pointing at the default endpoint `{default_base}`."
165 )
166 .unwrap();
167 writeln!(out, " pub fn default_endpoint() -> Self {{").unwrap();
168 writeln!(out, " Self::new(\"{}\")", escape(&default_base)).unwrap();
169 writeln!(out, " }}").unwrap();
170
171 for op in &spec.operations {
172 writeln!(out).unwrap();
173 render_operation(out, spec, op);
174 }
175
176 writeln!(out, "}}").unwrap();
177}
178
179fn render_operation(out: &mut String, spec: &ApiSpec, op: &Operation) {
180 if let Some(desc) = &op.description {
181 for line in desc.lines() {
182 writeln!(out, " /// {line}").unwrap();
183 }
184 }
185 writeln!(out, " /// Endpoint: `{}`", op.endpoint).unwrap();
186 if op.streaming.is_streaming() {
187 writeln!(
188 out,
189 " /// Streaming mode: `{}` — this generated stub returns an error; wire your runtime (`tonic` / GraphQL subscriptions) to convert it into an `impl Stream`.",
190 op.streaming.label()
191 )
192 .unwrap();
193 }
194
195 let sig_params = op
197 .params
198 .iter()
199 .map(|p| {
200 let is_grpc_client_or_bidi_stream = (op.streaming == StreamingMode::ClientStream
201 || op.streaming == StreamingMode::BidiStream)
202 && op.protocol == Protocol::Grpc;
203 let ty = if is_grpc_client_or_bidi_stream {
204 format!(
205 "impl futures_util::Stream<Item = {}> + Send + 'static",
206 p.rust_type
207 )
208 } else if p.required {
209 p.rust_type.clone()
210 } else {
211 format!("Option<{}>", p.rust_type)
212 };
213 format!("{}: {}", p.name, ty)
214 })
215 .collect::<Vec<_>>()
216 .join(", ");
217
218 let prefix = if sig_params.is_empty() { "" } else { ", " };
219 let ret = if op.streaming.is_streaming() {
220 match op.protocol {
221 Protocol::GraphQl => {
222 format!(
223 "futures_util::stream::BoxStream<'static, anyhow::Result<{}>>",
224 op.return_type
225 )
226 }
227 Protocol::Grpc => {
228 format!("futures_util::stream::BoxStream<'static, std::result::Result<{}, tonic::Status>>", op.return_type)
229 }
230 _ => {
231 format!(
232 "futures_util::stream::BoxStream<'static, anyhow::Result<{}>>",
233 op.return_type
234 )
235 }
236 }
237 } else {
238 op.return_type.clone()
239 };
240 writeln!(
241 out,
242 " pub async fn {name}(&self{prefix}{sig_params}) -> anyhow::Result<{ret}> {{",
243 name = op.id,
244 ret = ret,
245 )
246 .unwrap();
247
248 if op.streaming.is_streaming() {
249 match op.protocol {
250 Protocol::GraphQl => render_graphql_subscription_body(out, op),
251 Protocol::Grpc => render_grpc_body(out, op),
252 _ => render_streaming_body(out, op),
253 }
254 } else {
255 match op.protocol {
256 Protocol::Rest => render_rest_body(out, spec, op),
257 Protocol::GraphQl => render_graphql_body(out, op),
258 Protocol::Grpc => render_grpc_body(out, op),
259 }
260 }
261
262 writeln!(out, " }}").unwrap();
263}
264
265fn render_streaming_body(out: &mut String, op: &Operation) {
266 writeln!(
267 out,
268 " // Streaming ({}) scaffold. Wire `tonic` server-streaming or a GraphQL subscription client to fulfil this method.",
269 op.streaming.label()
270 )
271 .unwrap();
272 for p in &op.params {
273 writeln!(out, " let _ = &{n};", n = p.name).unwrap();
274 }
275 writeln!(
276 out,
277 " anyhow::bail!(\"streaming operation `{}` ({}) not yet wired; regenerate after attaching a streaming runtime.\");",
278 op.original_id,
279 op.streaming.label()
280 )
281 .unwrap();
282}
283
284fn render_rest_body(out: &mut String, _spec: &ApiSpec, op: &Operation) {
285 let path = extract_path(&op.endpoint);
287 let path_params = path_params_in_order(&path, &op.params);
288 let mut url_template = path.clone();
289 for p in &path_params {
290 url_template = url_template.replace(
291 &format!("{{{}}}", p.original_name),
292 &format!("{{{}}}", p.name),
293 );
294 }
295 write!(
296 out,
297 " let url = format!(\"{{base}}{}\"",
298 url_template
299 )
300 .unwrap();
301 write!(out, ", base = self.base_url").unwrap();
302 for p in &path_params {
303 write!(out, ", {n} = {n}", n = p.name).unwrap();
304 }
305 writeln!(out, ");").unwrap();
306
307 let method_fn = op.http_method.reqwest_fn().unwrap_or("get");
308 writeln!(out, " let mut req = self.http.{method_fn}(&url);").unwrap();
309
310 for p in op
312 .params
313 .iter()
314 .filter(|p| p.location == ParamLocation::Query)
315 {
316 if p.required {
317 writeln!(
318 out,
319 " req = req.query(&[(\"{}\", {}.to_string())]);",
320 escape(&p.original_name),
321 p.name
322 )
323 .unwrap();
324 } else {
325 writeln!(
326 out,
327 " if let Some(v) = {n}.as_ref() {{ req = req.query(&[(\"{orig}\", v.to_string())]); }}",
328 n = p.name,
329 orig = escape(&p.original_name),
330 )
331 .unwrap();
332 }
333 }
334
335 for p in op
337 .params
338 .iter()
339 .filter(|p| p.location == ParamLocation::Header)
340 {
341 if p.required {
342 writeln!(
343 out,
344 " req = req.header(\"{}\", {}.to_string());",
345 escape(&p.original_name),
346 p.name
347 )
348 .unwrap();
349 } else {
350 writeln!(
351 out,
352 " if let Some(v) = {n}.as_ref() {{ req = req.header(\"{orig}\", v.to_string()); }}",
353 n = p.name,
354 orig = escape(&p.original_name),
355 )
356 .unwrap();
357 }
358 }
359
360 if let Some(body) = op.params.iter().find(|p| p.location == ParamLocation::Body) {
362 if body.required {
363 writeln!(out, " req = req.json(&{});", body.name).unwrap();
364 } else {
365 writeln!(
366 out,
367 " if let Some(v) = {n}.as_ref() {{ req = req.json(v); }}",
368 n = body.name
369 )
370 .unwrap();
371 }
372 }
373
374 writeln!(
375 out,
376 " let response = req.send().await?.error_for_status()?;"
377 )
378 .unwrap();
379 writeln!(
380 out,
381 " let parsed: {ret} = response.json().await?;",
382 ret = op.return_type
383 )
384 .unwrap();
385 writeln!(out, " Ok(parsed)").unwrap();
386}
387
388fn render_graphql_body(out: &mut String, op: &Operation) {
389 let var_decls = op
391 .params
392 .iter()
393 .filter(|p| p.location == ParamLocation::GraphQlVariable)
394 .map(|p| {
395 if p.required {
396 format!("vars.insert(\"{n}\".to_string(), serde_json::to_value(&{n})?);", n = p.name)
397 } else {
398 format!("if let Some(v) = {n}.as_ref() {{ vars.insert(\"{n}\".to_string(), serde_json::to_value(v)?); }}", n = p.name)
399 }
400 })
401 .collect::<Vec<_>>()
402 .join("\n ");
403
404 let op_word = match op.http_method {
408 HttpMethod::Post => "query", _ => "query",
410 };
411 let op_word = if op.endpoint.starts_with("POST") && op.id.starts_with("create") {
412 "mutation"
413 } else {
414 op_word
415 };
416
417 let arg_list = op
418 .params
419 .iter()
420 .filter(|p| p.location == ParamLocation::GraphQlVariable)
421 .map(|p| {
422 let ty = if p.required {
423 format!("{}!", p.rust_type)
424 } else {
425 p.rust_type.clone()
426 };
427 format!("${n}: {ty}", n = p.name)
428 })
429 .collect::<Vec<_>>()
430 .join(", ");
431 let arg_decl = if arg_list.is_empty() {
432 String::new()
433 } else {
434 format!("({})", arg_list)
435 };
436 let inner_args = op
437 .params
438 .iter()
439 .filter(|p| p.location == ParamLocation::GraphQlVariable)
440 .map(|p| format!("{n}: ${n}", n = p.name))
441 .collect::<Vec<_>>()
442 .join(", ");
443 let inner_args_block = if inner_args.is_empty() {
444 String::new()
445 } else {
446 format!("({inner_args})")
447 };
448
449 let query_str = format!(
450 "{op_word} {oid}{arg_decl} {{ {oid}{inner_args_block} }}",
451 oid = op.original_id,
452 );
453
454 writeln!(out, " let query = \"{}\";", escape(&query_str)).unwrap();
455 writeln!(
456 out,
457 " let mut vars: serde_json::Map<String, serde_json::Value> = serde_json::Map::new();"
458 )
459 .unwrap();
460 if !var_decls.is_empty() {
461 writeln!(out, " {var_decls}").unwrap();
462 }
463 writeln!(
464 out,
465 " let body = serde_json::json!({{ \"query\": query, \"variables\": serde_json::Value::Object(vars) }});"
466 )
467 .unwrap();
468 writeln!(
469 out,
470 " let response = self.http.post(&self.base_url).json(&body).send().await?.error_for_status()?;"
471 )
472 .unwrap();
473 writeln!(
474 out,
475 " let envelope: serde_json::Value = response.json().await?;"
476 )
477 .unwrap();
478 writeln!(
479 out,
480 " let data = envelope.get(\"data\").and_then(|d| d.get(\"{}\")).cloned().unwrap_or(serde_json::Value::Null);",
481 op.original_id
482 )
483 .unwrap();
484 writeln!(
485 out,
486 " let parsed: {ret} = serde_json::from_value(data)?;",
487 ret = op.return_type
488 )
489 .unwrap();
490 writeln!(out, " Ok(parsed)").unwrap();
491}
492
493fn render_graphql_subscription_body(out: &mut String, op: &Operation) {
494 let var_decls = op
496 .params
497 .iter()
498 .filter(|p| p.location == ParamLocation::GraphQlVariable)
499 .map(|p| {
500 if p.required {
501 format!("vars.insert(\"{n}\".to_string(), serde_json::to_value(&{n})?);", n = p.name)
502 } else {
503 format!("if let Some(v) = {n}.as_ref() {{ vars.insert(\"{n}\".to_string(), serde_json::to_value(v)?); }}", n = p.name)
504 }
505 })
506 .collect::<Vec<_>>()
507 .join("\n ");
508
509 let arg_list = op
511 .params
512 .iter()
513 .filter(|p| p.location == ParamLocation::GraphQlVariable)
514 .map(|p| {
515 let ty = if p.required {
516 format!("{}!", p.rust_type)
517 } else {
518 p.rust_type.clone()
519 };
520 format!("${n}: {ty}", n = p.name)
521 })
522 .collect::<Vec<_>>()
523 .join(", ");
524 let arg_decl = if arg_list.is_empty() {
525 String::new()
526 } else {
527 format!("({})", arg_list)
528 };
529 let inner_args = op
530 .params
531 .iter()
532 .filter(|p| p.location == ParamLocation::GraphQlVariable)
533 .map(|p| format!("{n}: ${n}", n = p.name))
534 .collect::<Vec<_>>()
535 .join(", ");
536 let inner_args_block = if inner_args.is_empty() {
537 String::new()
538 } else {
539 format!("({inner_args})")
540 };
541
542 let query_str = format!(
543 "subscription {oid}{arg_decl} {{ {oid}{inner_args_block} }}",
544 oid = op.original_id,
545 );
546
547 writeln!(out, " use futures_util::{{SinkExt, StreamExt}};").unwrap();
548 writeln!(out, " let ws_url = self.base_url.replace(\"https://\", \"wss://\").replace(\"http://\", \"ws://\");").unwrap();
549 writeln!(
550 out,
551 " let (ws_stream, _) = tokio_tungstenite::connect_async(&ws_url).await?;"
552 )
553 .unwrap();
554 writeln!(
555 out,
556 " let (mut write, mut read) = ws_stream.split();"
557 )
558 .unwrap();
559 writeln!(out).unwrap();
560
561 writeln!(
562 out,
563 " let init_msg = tokio_tungstenite::tungstenite::Message::Text("
564 )
565 .unwrap();
566 writeln!(
567 out,
568 " r#\"{{\"type\":\"connection_init\"}}\"#.into()"
569 )
570 .unwrap();
571 writeln!(out, " );").unwrap();
572 writeln!(out, " write.send(init_msg).await?;").unwrap();
573 writeln!(out).unwrap();
574
575 writeln!(out, " if let Some(msg) = read.next().await {{").unwrap();
576 writeln!(out, " let msg = msg?;").unwrap();
577 writeln!(
578 out,
579 " if let tokio_tungstenite::tungstenite::Message::Text(text) = msg {{"
580 )
581 .unwrap();
582 writeln!(
583 out,
584 " let ack: serde_json::Value = serde_json::from_str(&text)?;"
585 )
586 .unwrap();
587 writeln!(out, " if ack.get(\"type\").and_then(|t| t.as_str()) != Some(\"connection_ack\") {{").unwrap();
588 writeln!(
589 out,
590 " anyhow::bail!(\"Expected connection_ack, got: {{}}\", text);"
591 )
592 .unwrap();
593 writeln!(out, " }}").unwrap();
594 writeln!(out, " }} else {{").unwrap();
595 writeln!(
596 out,
597 " anyhow::bail!(\"Expected connection_ack, got non-text message\");"
598 )
599 .unwrap();
600 writeln!(out, " }}").unwrap();
601 writeln!(out, " }} else {{").unwrap();
602 writeln!(
603 out,
604 " anyhow::bail!(\"Connection closed during handshake\");"
605 )
606 .unwrap();
607 writeln!(out, " }}").unwrap();
608 writeln!(out).unwrap();
609
610 writeln!(out, " let query = \"{}\";", escape(&query_str)).unwrap();
611 writeln!(out, " let mut vars: serde_json::Map<String, serde_json::Value> = serde_json::Map::new();").unwrap();
612 if !var_decls.is_empty() {
613 writeln!(out, " {var_decls}").unwrap();
614 }
615 writeln!(out, " let payload = serde_json::json!({{").unwrap();
616 writeln!(out, " \"query\": query,").unwrap();
617 writeln!(
618 out,
619 " \"variables\": serde_json::Value::Object(vars),"
620 )
621 .unwrap();
622 writeln!(out, " }});").unwrap();
623 writeln!(out, " let sub_msg = serde_json::json!({{").unwrap();
624 writeln!(out, " \"id\": \"sub_1\",").unwrap();
625 writeln!(out, " \"type\": \"subscribe\",").unwrap();
626 writeln!(out, " \"payload\": payload,").unwrap();
627 writeln!(out, " }});").unwrap();
628 writeln!(
629 out,
630 " let sub_text = serde_json::to_string(&sub_msg)?;"
631 )
632 .unwrap();
633 writeln!(out, " write.send(tokio_tungstenite::tungstenite::Message::Text(sub_text.into())).await?;").unwrap();
634 writeln!(out).unwrap();
635
636 writeln!(out, " let stream = futures_util::stream::unfold((write, read), |(mut write, mut read)| async move {{").unwrap();
637 writeln!(out, " loop {{").unwrap();
638 writeln!(out, " match read.next().await {{").unwrap();
639 writeln!(out, " Some(Ok(msg)) => {{").unwrap();
640 writeln!(out, " match msg {{").unwrap();
641 writeln!(
642 out,
643 " tokio_tungstenite::tungstenite::Message::Text(text) => {{"
644 )
645 .unwrap();
646 writeln!(out, " let val: serde_json::Value = match serde_json::from_str(&text) {{").unwrap();
647 writeln!(out, " Ok(v) => v,").unwrap();
648 writeln!(out, " Err(e) => return Some((Err(anyhow::anyhow!(e)), (write, read))),").unwrap();
649 writeln!(out, " }};").unwrap();
650 writeln!(out, " let msg_type = val.get(\"type\").and_then(|t| t.as_str());").unwrap();
651 writeln!(out, " match msg_type {{").unwrap();
652 writeln!(
653 out,
654 " Some(\"ping\") => {{"
655 )
656 .unwrap();
657 writeln!(out, " let pong_msg = tokio_tungstenite::tungstenite::Message::Text(").unwrap();
658 writeln!(
659 out,
660 " r#\"{{\"type\":\"pong\"}}\"#.into()"
661 )
662 .unwrap();
663 writeln!(out, " );").unwrap();
664 writeln!(
665 out,
666 " if let Err(e) = write.send(pong_msg).await {{"
667 )
668 .unwrap();
669 writeln!(out, " return Some((Err(anyhow::anyhow!(e)), (write, read)));").unwrap();
670 writeln!(out, " }}").unwrap();
671 writeln!(out, " }}").unwrap();
672 writeln!(
673 out,
674 " Some(\"pong\") => {{}}"
675 )
676 .unwrap();
677 writeln!(
678 out,
679 " Some(\"next\") => {{"
680 )
681 .unwrap();
682 writeln!(
683 out,
684 " let data = val.get(\"payload\")"
685 )
686 .unwrap();
687 writeln!(
688 out,
689 " .and_then(|p| p.get(\"data\"))"
690 )
691 .unwrap();
692 writeln!(
693 out,
694 " .and_then(|d| d.get(\"{}\"))",
695 op.original_id
696 )
697 .unwrap();
698 writeln!(out, " .cloned()").unwrap();
699 writeln!(
700 out,
701 " .unwrap_or(serde_json::Value::Null);"
702 )
703 .unwrap();
704 writeln!(out, " let parsed = match serde_json::from_value::<{}>(data) {{", op.return_type).unwrap();
705 writeln!(
706 out,
707 " Ok(p) => p,"
708 )
709 .unwrap();
710 writeln!(out, " Err(e) => return Some((Err(anyhow::anyhow!(e)), (write, read))),").unwrap();
711 writeln!(out, " }};").unwrap();
712 writeln!(
713 out,
714 " return Some((Ok(parsed), (write, read)));"
715 )
716 .unwrap();
717 writeln!(out, " }}").unwrap();
718 writeln!(
719 out,
720 " Some(\"error\") => {{"
721 )
722 .unwrap();
723 writeln!(out, " let errors = val.get(\"payload\").cloned().unwrap_or(serde_json::Value::Null);").unwrap();
724 writeln!(out, " return Some((Err(anyhow::anyhow!(\"GraphQL subscription error: {{}}\", errors)), (write, read)));").unwrap();
725 writeln!(out, " }}").unwrap();
726 writeln!(
727 out,
728 " Some(\"complete\") => {{"
729 )
730 .unwrap();
731 writeln!(out, " return None;").unwrap();
732 writeln!(out, " }}").unwrap();
733 writeln!(out, " _ => {{}}").unwrap();
734 writeln!(out, " }}").unwrap();
735 writeln!(out, " }}").unwrap();
736 writeln!(
737 out,
738 " tokio_tungstenite::tungstenite::Message::Close(_) => {{"
739 )
740 .unwrap();
741 writeln!(out, " return None;").unwrap();
742 writeln!(out, " }}").unwrap();
743 writeln!(out, " _ => {{}}").unwrap();
744 writeln!(out, " }}").unwrap();
745 writeln!(out, " }}").unwrap();
746 writeln!(out, " Some(Err(e)) => {{").unwrap();
747 writeln!(
748 out,
749 " return Some((Err(anyhow::anyhow!(e)), (write, read)));"
750 )
751 .unwrap();
752 writeln!(out, " }}").unwrap();
753 writeln!(out, " None => {{").unwrap();
754 writeln!(out, " return None;").unwrap();
755 writeln!(out, " }}").unwrap();
756 writeln!(out, " }}").unwrap();
757 writeln!(out, " }}").unwrap();
758 writeln!(out, " }});").unwrap();
759 writeln!(out, " Ok(stream.boxed())").unwrap();
760}
761
762fn render_grpc_body(out: &mut String, op: &Operation) {
763 let parts: Vec<&str> = op.endpoint.split('/').collect();
764 if parts.len() < 3 {
765 writeln!(
766 out,
767 " anyhow::bail!(\"gRPC endpoint '{}' format invalid. Expected /Service/Method\");",
768 op.endpoint
769 )
770 .unwrap();
771 return;
772 }
773 let service_name = parts[1];
774 let method_name = parts[2];
775 let service_snake = service_name.to_snake_case();
776 let method_snake = method_name.to_snake_case();
777
778 writeln!(
779 out,
780 " let mut client = proto::{service_snake}_client::{service_name}Client::connect(self.base_url.clone()).await?;"
781 )
782 .unwrap();
783 if op.streaming == StreamingMode::ClientStream || op.streaming == StreamingMode::BidiStream {
784 writeln!(
785 out,
786 " let response = client.{method_snake}(request).await?;"
787 )
788 .unwrap();
789 } else {
790 writeln!(
791 out,
792 " let response = client.{method_snake}(tonic::Request::new(request)).await?;"
793 )
794 .unwrap();
795 }
796 if op.streaming.is_streaming() {
797 writeln!(out, " use futures_util::StreamExt;").unwrap();
798 writeln!(out, " Ok(response.into_inner().boxed())").unwrap();
799 } else {
800 writeln!(out, " Ok(response.into_inner())").unwrap();
801 }
802}
803
804fn extract_path(endpoint: &str) -> String {
809 let trimmed = endpoint.trim();
811 match trimmed.find(' ') {
812 Some(idx) => trimmed[idx + 1..].to_string(),
813 None => trimmed.to_string(),
814 }
815}
816
817fn path_params_in_order<'a>(path: &str, params: &'a [Param]) -> Vec<&'a Param> {
818 let mut out = Vec::new();
819 let bytes = path.as_bytes();
820 let mut i = 0;
821 while i < bytes.len() {
822 if bytes[i] == b'{' {
823 if let Some(end) = path[i + 1..].find('}') {
824 let name = &path[i + 1..i + 1 + end];
825 if let Some(p) = params
826 .iter()
827 .find(|p| p.location == ParamLocation::Path && p.original_name == name)
828 {
829 out.push(p);
830 }
831 i += end + 2;
832 continue;
833 }
834 }
835 i += 1;
836 }
837 out
838}
839
840fn emit_doc(out: &mut String, text: Option<&str>, indent: &str) {
841 let Some(t) = text else { return };
842 for line in t.lines() {
843 writeln!(out, "{indent}/// {line}").unwrap();
844 }
845}
846
847fn escape(s: &str) -> String {
848 s.replace('\\', "\\\\").replace('"', "\\\"")
849}