Skip to main content

oxide_gen/emit/
rust_lib.rs

1//! Emit `src/lib.rs` for the generated crate.
2
3use 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
11/// Render the full `lib.rs` contents.
12pub 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    // -- Types -------------------------------------------------------------
47    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    // -- Client ------------------------------------------------------------
64    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
125// ---------------------------------------------------------------------------
126// Client + methods
127// ---------------------------------------------------------------------------
128
129fn 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    // Signature.
196    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    // Build URL with named-arg `format!`.
286    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    // Query params.
311    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    // Header params.
336    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    // Body.
361    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    // Build the variables JSON map.
390    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    // Build the GraphQL operation string. We synthesize a minimal query that
405    // returns the operation's top-level field; deep selection sets are out of
406    // scope for the basic generator (callers can hand-edit if they need them).
407    let op_word = match op.http_method {
408        HttpMethod::Post => "query", // Query type → POST; same word in GQL.
409        _ => "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    // Build the variables JSON map.
495    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    // Build the GraphQL operation string.
510    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
804// ---------------------------------------------------------------------------
805// Helpers
806// ---------------------------------------------------------------------------
807
808fn extract_path(endpoint: &str) -> String {
809    // Endpoint shape from the OpenAPI parser: "METHOD /path/with/{params}".
810    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}