tinc_build/codegen/service/
openapi.rs

1use std::collections::BTreeMap;
2
3use anyhow::Context;
4use base64::Engine;
5use indexmap::IndexMap;
6use openapiv3_1::{Object, Ref, Schema, Type};
7use proc_macro2::TokenStream;
8use quote::quote;
9use tinc_cel::{CelValue, NumberTy};
10
11use crate::codegen::cel::compiler::{CompiledExpr, Compiler, CompilerTarget, ConstantCompiledExpr};
12use crate::codegen::cel::{CelExpression, CelExpressions, functions};
13use crate::codegen::utils::field_ident_from_str;
14use crate::types::{ProtoModifiedValueType, ProtoPath, ProtoType, ProtoTypeRegistry, ProtoValueType, ProtoWellKnownType};
15
16fn cel_to_json(cel: &CelValue<'static>, type_registry: &ProtoTypeRegistry) -> anyhow::Result<serde_json::Value> {
17    match cel {
18        CelValue::Null => Ok(serde_json::Value::Null),
19        CelValue::Bool(b) => Ok(serde_json::Value::Bool(*b)),
20        CelValue::Map(map) => Ok(serde_json::Value::Object(
21            map.iter()
22                .map(|(key, value)| {
23                    if let CelValue::String(key) = key {
24                        Ok((key.to_string(), cel_to_json(value, type_registry)?))
25                    } else {
26                        anyhow::bail!("map keys must be a string")
27                    }
28                })
29                .collect::<anyhow::Result<_>>()?,
30        )),
31        CelValue::List(list) => Ok(serde_json::Value::Array(
32            list.iter()
33                .map(|i| cel_to_json(i, type_registry))
34                .collect::<anyhow::Result<_>>()?,
35        )),
36        CelValue::String(s) => Ok(serde_json::Value::String(s.to_string())),
37        CelValue::Number(NumberTy::F64(f)) => Ok(serde_json::Value::Number(
38            serde_json::Number::from_f64(*f).context("f64 is not a valid float")?,
39        )),
40        CelValue::Number(NumberTy::I64(i)) => Ok(serde_json::Value::Number(
41            serde_json::Number::from_i128(*i as i128).context("i64 is not a valid int")?,
42        )),
43        CelValue::Number(NumberTy::U64(u)) => Ok(serde_json::Value::Number(
44            serde_json::Number::from_u128(*u as u128).context("u64 is not a valid uint")?,
45        )),
46        CelValue::Duration(duration) => Ok(serde_json::Value::String(duration.to_string())),
47        CelValue::Timestamp(timestamp) => Ok(serde_json::Value::String(timestamp.to_rfc3339())),
48        CelValue::Bytes(bytes) => Ok(serde_json::Value::String(
49            base64::engine::general_purpose::STANDARD.encode(bytes),
50        )),
51        CelValue::Enum(cel_enum) => {
52            let enum_ty = type_registry
53                .get_enum(&cel_enum.tag)
54                .with_context(|| format!("couldnt find enum {}", cel_enum.tag.as_ref()))?;
55            if enum_ty.options.repr_enum {
56                Ok(serde_json::Value::from(cel_enum.value))
57            } else {
58                let variant = enum_ty
59                    .variants
60                    .values()
61                    .find(|v| v.value == cel_enum.value)
62                    .with_context(|| format!("{} has no value for {}", cel_enum.tag.as_ref(), cel_enum.value))?;
63                Ok(serde_json::Value::from(variant.options.serde_name.clone()))
64            }
65        }
66    }
67}
68
69fn parse_resolve(compiler: &Compiler, expr: &str) -> anyhow::Result<CelValue<'static>> {
70    let expr = cel_parser::parse(expr).context("parse")?;
71    let resolved = compiler.resolve(&expr).context("resolve")?;
72    match resolved {
73        CompiledExpr::Constant(ConstantCompiledExpr { value }) => Ok(value),
74        CompiledExpr::Runtime(_) => anyhow::bail!("expression needs runtime evaluation"),
75    }
76}
77
78fn handle_expr(mut ctx: Compiler, ty: &ProtoType, expr: &CelExpression) -> anyhow::Result<Vec<Schema>> {
79    ctx.set_target(CompilerTarget::Serde);
80
81    if let Some(this) = expr.this.clone() {
82        ctx.add_variable("this", CompiledExpr::constant(this));
83    }
84
85    if let Some(ProtoValueType::Enum(path)) = ty.value_type() {
86        ctx.register_function(functions::Enum(Some(path.clone())));
87    }
88
89    let mut schemas = Vec::new();
90    for schema in &expr.jsonschemas {
91        let value = parse_resolve(&ctx, schema)?;
92        let value = cel_to_json(&value, ctx.registry())?;
93        if !value.is_null() {
94            schemas.push(serde_json::from_value(value).context("bad openapi schema")?);
95        }
96    }
97
98    Ok(schemas)
99}
100
101#[derive(Debug)]
102enum ExcludePaths {
103    True,
104    Child(BTreeMap<String, ExcludePaths>),
105}
106
107#[derive(Debug, Clone, Copy)]
108enum BytesEncoding {
109    Base64,
110    Binary,
111}
112
113#[derive(Debug, Clone, Copy)]
114pub(super) enum BodyMethod<'a> {
115    Text,
116    Json,
117    Binary(Option<&'a str>),
118}
119
120impl BodyMethod<'_> {
121    fn bytes_encoding(&self) -> BytesEncoding {
122        match self {
123            BodyMethod::Binary(_) => BytesEncoding::Binary,
124            _ => BytesEncoding::Base64,
125        }
126    }
127
128    fn deserialize_method(&self) -> syn::Ident {
129        match self {
130            BodyMethod::Text => syn::parse_quote!(deserialize_body_text),
131            BodyMethod::Binary(_) => syn::parse_quote!(deserialize_body_bytes),
132            BodyMethod::Json => syn::parse_quote!(deserialize_body_json),
133        }
134    }
135
136    fn content_type(&self) -> &str {
137        match self {
138            BodyMethod::Binary(ct) => ct.unwrap_or(self.default_content_type()),
139            _ => self.default_content_type(),
140        }
141    }
142
143    fn default_content_type(&self) -> &'static str {
144        match self {
145            BodyMethod::Binary(_) => "application/octet-stream",
146            BodyMethod::Json => "application/json",
147            BodyMethod::Text => "text/plain",
148        }
149    }
150}
151
152#[derive(Debug, Clone, Copy, PartialEq)]
153enum GenerateDirection {
154    Input,
155    Output,
156}
157
158struct FieldExtract {
159    full_name: ProtoPath,
160    tokens: proc_macro2::TokenStream,
161    ty: ProtoType,
162    cel: CelExpressions,
163    is_optional: bool,
164}
165
166fn input_field_getter_gen(
167    registry: &ProtoTypeRegistry,
168    ty: &ProtoValueType,
169    mut mapping: TokenStream,
170    field_str: &str,
171) -> anyhow::Result<FieldExtract> {
172    let ProtoValueType::Message(path) = ty else {
173        anyhow::bail!("cannot extract field on non-message type: {field_str}");
174    };
175
176    let mut next_message = Some(registry.get_message(path).unwrap());
177    let mut is_optional = false;
178    let mut kind = None;
179    let mut cel = None;
180    let mut full_name = None;
181    for part in field_str.split('.') {
182        let Some(field) = next_message.and_then(|message| message.fields.get(part)) else {
183            anyhow::bail!("message does not have field: {field_str}");
184        };
185
186        let field_ident = field_ident_from_str(part);
187
188        let optional_unwrap = is_optional.then(|| {
189            quote! {
190                let mut tracker = tracker.get_or_insert_default();
191                let mut target = target.get_or_insert_default();
192            }
193        });
194
195        full_name = Some(&field.full_name);
196        kind = Some(&field.ty);
197        cel = Some(&field.options.cel_exprs);
198        mapping = quote! {{
199            let (tracker, target) = #mapping;
200            #optional_unwrap
201            let tracker = tracker.#field_ident.get_or_insert_default();
202            let target = &mut target.#field_ident;
203            (tracker, target)
204        }};
205
206        is_optional = matches!(
207            field.ty,
208            ProtoType::Modified(ProtoModifiedValueType::Optional(_) | ProtoModifiedValueType::OneOf(_))
209        );
210        next_message = match &field.ty {
211            ProtoType::Value(ProtoValueType::Message(path))
212            | ProtoType::Modified(ProtoModifiedValueType::Optional(ProtoValueType::Message(path))) => {
213                Some(registry.get_message(path).unwrap())
214            }
215            _ => None,
216        }
217    }
218
219    Ok(FieldExtract {
220        full_name: full_name.unwrap().clone(),
221        tokens: mapping,
222        ty: kind.unwrap().clone(),
223        cel: cel.unwrap().clone(),
224        is_optional,
225    })
226}
227
228fn output_field_getter_gen(
229    registry: &ProtoTypeRegistry,
230    ty: &ProtoValueType,
231    mut mapping: TokenStream,
232    field_str: &str,
233) -> anyhow::Result<FieldExtract> {
234    let ProtoValueType::Message(path) = ty else {
235        anyhow::bail!("cannot extract field on non-message type: {field_str}");
236    };
237
238    let mut next_message = Some(registry.get_message(path).unwrap());
239    let mut was_optional = false;
240    let mut kind = None;
241    let mut cel = None;
242    let mut full_name = None;
243    for part in field_str.split('.') {
244        let Some(field) = next_message.and_then(|message| message.fields.get(part)) else {
245            anyhow::bail!("message does not have field: {field_str}");
246        };
247
248        let field_ident = field_ident_from_str(part);
249
250        full_name = Some(&field.full_name);
251        kind = Some(&field.ty);
252        cel = Some(&field.options.cel_exprs);
253        let is_optional = matches!(
254            field.ty,
255            ProtoType::Modified(ProtoModifiedValueType::Optional(_) | ProtoModifiedValueType::OneOf(_))
256        );
257
258        mapping = match (is_optional, was_optional) {
259            (true, true) => quote!(#mapping.and_then(|m| m.#field_ident.as_ref())),
260            (false, true) => quote!(#mapping.map(|m| &m.#field_ident)),
261            (true, false) => quote!(#mapping.#field_ident.as_ref()),
262            (false, false) => quote!(&#mapping.#field_ident),
263        };
264
265        was_optional = was_optional || is_optional;
266
267        next_message = match &field.ty {
268            ProtoType::Value(ProtoValueType::Message(path))
269            | ProtoType::Modified(ProtoModifiedValueType::Optional(ProtoValueType::Message(path))) => {
270                Some(registry.get_message(path).unwrap())
271            }
272            _ => None,
273        }
274    }
275
276    Ok(FieldExtract {
277        full_name: full_name.unwrap().clone(),
278        cel: cel.unwrap().clone(),
279        ty: kind.unwrap().clone(),
280        is_optional: was_optional,
281        tokens: mapping,
282    })
283}
284
285fn parse_route(route: &str) -> Vec<String> {
286    let mut params = Vec::new();
287    let mut chars = route.chars().peekable();
288
289    while let Some(ch) = chars.next() {
290        if ch != '{' {
291            continue;
292        }
293
294        // Skip escaped '{{'
295        if let Some(&'{') = chars.peek() {
296            chars.next();
297            continue;
298        }
299
300        let mut param = String::new();
301        for c in &mut chars {
302            if c == '}' {
303                params.push(param);
304                break;
305            }
306
307            param.push(c);
308        }
309    }
310
311    params
312}
313
314struct PathFields {
315    defs: Vec<proc_macro2::TokenStream>,
316    mappings: Vec<proc_macro2::TokenStream>,
317    param_schemas: IndexMap<String, (ProtoValueType, CelExpressions)>,
318}
319
320fn path_struct(
321    registry: &ProtoTypeRegistry,
322    ty: &ProtoValueType,
323    package: &str,
324    fields: &[String],
325    mapping: TokenStream,
326) -> anyhow::Result<PathFields> {
327    let mut defs = Vec::new();
328    let mut mappings = Vec::new();
329    let mut param_schemas = IndexMap::new();
330
331    let match_single_ty = |ty: &ProtoValueType| {
332        Some(match &ty {
333            ProtoValueType::Enum(path) => {
334                let path = registry.resolve_rust_path(package, path).expect("enum not found");
335                quote! {
336                    #path
337                }
338            }
339            ProtoValueType::Bool => quote! {
340                ::core::primitive::bool
341            },
342            ProtoValueType::Float => quote! {
343                ::core::primitive::f32
344            },
345            ProtoValueType::Double => quote! {
346                ::core::primitive::f64
347            },
348            ProtoValueType::Int32 => quote! {
349                ::core::primitive::i32
350            },
351            ProtoValueType::Int64 => quote! {
352                ::core::primitive::i64
353            },
354            ProtoValueType::UInt32 => quote! {
355                ::core::primitive::u32
356            },
357            ProtoValueType::UInt64 => quote! {
358                ::core::primitive::u64
359            },
360            ProtoValueType::String => quote! {
361                ::std::string::String
362            },
363            ProtoValueType::WellKnown(ProtoWellKnownType::Duration) => quote! {
364                ::tinc::__private::well_known::Duration
365            },
366            ProtoValueType::WellKnown(ProtoWellKnownType::Timestamp) => quote! {
367                ::tinc::__private::well_known::Timestamp
368            },
369            ProtoValueType::WellKnown(ProtoWellKnownType::Value) => quote! {
370                ::tinc::__private::well_known::Value
371            },
372            _ => return None,
373        })
374    };
375
376    match &ty {
377        ProtoValueType::Message(_) => {
378            for (idx, field) in fields.iter().enumerate() {
379                let field_str = field.as_ref();
380                let path_field_ident = quote::format_ident!("field_{idx}");
381                let FieldExtract {
382                    full_name: _full_name,
383                    cel,
384                    tokens,
385                    ty,
386                    is_optional,
387                } = input_field_getter_gen(registry, ty, mapping.clone(), field_str)?;
388
389                let setter = if is_optional {
390                    quote! {
391                        tracker.get_or_insert_default();
392                        target.insert(path.#path_field_ident.into());
393                    }
394                } else {
395                    quote! {
396                        *target = path.#path_field_ident.into();
397                    }
398                };
399
400                mappings.push(quote! {{
401                    let (tracker, target) = #tokens;
402                    #setter;
403                }});
404
405                let ty = match ty {
406                    ProtoType::Modified(ProtoModifiedValueType::Optional(value)) | ProtoType::Value(value) => Some(value),
407                    _ => None,
408                };
409
410                let Some(tokens) = ty.as_ref().and_then(match_single_ty) else {
411                    anyhow::bail!("type cannot be mapped: {ty:?}");
412                };
413
414                let ty = ty.unwrap();
415
416                param_schemas.insert(field.clone(), (ty, cel));
417
418                defs.push(quote! {
419                    #[serde(rename = #field_str)]
420                    #path_field_ident: #tokens
421                });
422            }
423        }
424        ty => {
425            let Some(ty) = match_single_ty(ty) else {
426                anyhow::bail!("type cannot be mapped: {ty:?}");
427            };
428
429            if fields.len() != 1 {
430                anyhow::bail!("well-known type can only have one field");
431            }
432
433            if fields[0] != "value" {
434                anyhow::bail!("well-known type can only have field 'value'");
435            }
436
437            mappings.push(quote! {{
438                let (_, target) = #mapping;
439                *target = path.value.into();
440            }});
441
442            defs.push(quote! {
443                #[serde(rename = "value")]
444                value: #ty
445            });
446        }
447    }
448
449    Ok(PathFields {
450        defs,
451        mappings,
452        param_schemas,
453    })
454}
455
456pub(super) struct InputGenerator<'a> {
457    used_paths: BTreeMap<String, ExcludePaths>,
458    types: &'a ProtoTypeRegistry,
459    components: &'a mut openapiv3_1::Components,
460    package: &'a str,
461    root_ty: ProtoValueType,
462    tracker_ident: syn::Ident,
463    target_ident: syn::Ident,
464    state_ident: syn::Ident,
465}
466
467#[derive(Default)]
468pub(super) struct GeneratedParams {
469    pub tokens: TokenStream,
470    pub params: Vec<openapiv3_1::path::Parameter>,
471}
472
473pub(super) struct GeneratedBody<B> {
474    pub tokens: TokenStream,
475    pub body: B,
476}
477
478impl<'a> InputGenerator<'a> {
479    pub(super) fn new(
480        types: &'a ProtoTypeRegistry,
481        components: &'a mut openapiv3_1::Components,
482        package: &'a str,
483        ty: ProtoValueType,
484        tracker_ident: syn::Ident,
485        target_ident: syn::Ident,
486        state_ident: syn::Ident,
487    ) -> Self {
488        Self {
489            components,
490            types,
491            used_paths: BTreeMap::new(),
492            package,
493            root_ty: ty,
494            target_ident,
495            tracker_ident,
496            state_ident,
497        }
498    }
499}
500
501pub(super) struct OutputGenerator<'a> {
502    types: &'a ProtoTypeRegistry,
503    components: &'a mut openapiv3_1::Components,
504    root_ty: ProtoValueType,
505    response_ident: syn::Ident,
506    builder_ident: syn::Ident,
507}
508
509impl<'a> OutputGenerator<'a> {
510    pub(super) fn new(
511        types: &'a ProtoTypeRegistry,
512        components: &'a mut openapiv3_1::Components,
513        ty: ProtoValueType,
514        response_ident: syn::Ident,
515        builder_ident: syn::Ident,
516    ) -> Self {
517        Self {
518            components,
519            types,
520            root_ty: ty,
521            response_ident,
522            builder_ident,
523        }
524    }
525}
526
527impl InputGenerator<'_> {
528    fn consume_field(&mut self, field: &str) -> anyhow::Result<()> {
529        let mut parts = field.split('.').peekable();
530        let first_part = parts.next().expect("parts empty").to_owned();
531
532        // Start with the first part of the path
533        let mut current_map = self.used_paths.entry(first_part).or_insert(if parts.peek().is_none() {
534            ExcludePaths::True
535        } else {
536            ExcludePaths::Child(BTreeMap::new())
537        });
538
539        // Iterate over the remaining parts of the path
540        while let Some(part) = parts.next() {
541            match current_map {
542                ExcludePaths::True => anyhow::bail!("duplicate path: {field}"),
543                ExcludePaths::Child(map) => {
544                    current_map = map.entry(part.to_owned()).or_insert(if parts.peek().is_none() {
545                        ExcludePaths::True
546                    } else {
547                        ExcludePaths::Child(BTreeMap::new())
548                    });
549                }
550            }
551        }
552
553        anyhow::ensure!(matches!(current_map, ExcludePaths::True), "duplicate path: {field}");
554
555        Ok(())
556    }
557
558    fn base_extract(&self) -> TokenStream {
559        let tracker = &self.tracker_ident;
560        let target = &self.target_ident;
561        quote!((&mut #tracker, &mut #target))
562    }
563
564    pub(super) fn generate_query_parameter(&mut self, field: Option<&str>) -> anyhow::Result<GeneratedParams> {
565        let mut params = Vec::new();
566
567        let extract = if let Some(field) = field {
568            input_field_getter_gen(self.types, &self.root_ty, self.base_extract(), field)?
569        } else {
570            FieldExtract {
571                // openapi cannot have cross-field expressions on parameters. so it doesnt matter
572                // if we keep the cel exprs.
573                full_name: ProtoPath::new(self.root_ty.proto_path()),
574                cel: CelExpressions::default(),
575                tokens: self.base_extract(),
576                is_optional: false,
577                ty: ProtoType::Value(self.root_ty.clone()),
578            }
579        };
580
581        let exclude_paths = if let Some(field) = field {
582            match self.used_paths.get(field) {
583                Some(ExcludePaths::Child(c)) => Some(c),
584                Some(ExcludePaths::True) => anyhow::bail!("{field} is already used by another operation"),
585                None => None,
586            }
587        } else {
588            Some(&self.used_paths)
589        };
590
591        if extract.ty.nested() {
592            anyhow::bail!("query string cannot be used on nested types.")
593        }
594
595        let message_ty = match extract.ty.value_type() {
596            Some(ProtoValueType::Message(path)) => self.types.get_message(path).unwrap(),
597            Some(ProtoValueType::WellKnown(ProtoWellKnownType::Empty)) => {
598                return Ok(GeneratedParams::default());
599            }
600            _ => anyhow::bail!("query string can only be used on message types."),
601        };
602
603        for (name, field) in &message_ty.fields {
604            let exclude_paths = match exclude_paths.and_then(|exclude_paths| exclude_paths.get(name)) {
605                Some(ExcludePaths::True) => continue,
606                Some(ExcludePaths::Child(child)) => Some(child),
607                None => None,
608            };
609            params.push(
610                openapiv3_1::path::Parameter::builder()
611                    .name(field.options.serde_name.clone())
612                    .required(!field.options.serde_omittable.is_true())
613                    .explode(true)
614                    .style(openapiv3_1::path::ParameterStyle::DeepObject)
615                    .schema(generate(
616                        &FieldInfo {
617                            full_name: &field.full_name,
618                            ty: &field.ty,
619                            cel: &field.options.cel_exprs,
620                        },
621                        self.components,
622                        self.types,
623                        exclude_paths.unwrap_or(&BTreeMap::new()),
624                        GenerateDirection::Input,
625                        BytesEncoding::Base64,
626                    )?)
627                    .parameter_in(openapiv3_1::path::ParameterIn::Query)
628                    .build(),
629            )
630        }
631
632        let extract = &extract.tokens;
633        let state_ident = &self.state_ident;
634
635        Ok(GeneratedParams {
636            params,
637            tokens: quote!({
638                let (mut tracker, mut target) = #extract;
639                if let Err(err) = ::tinc::__private::deserialize_query_string(
640                    &parts,
641                    tracker,
642                    target,
643                    &mut #state_ident,
644                ) {
645                    return err;
646                }
647            }),
648        })
649    }
650
651    pub(super) fn generate_path_parameter(&mut self, path: &str) -> anyhow::Result<GeneratedParams> {
652        let params = parse_route(path);
653        if params.is_empty() {
654            return Ok(GeneratedParams::default());
655        }
656
657        let PathFields {
658            defs,
659            mappings,
660            param_schemas,
661        } = path_struct(self.types, &self.root_ty, self.package, &params, self.base_extract())?;
662        let mut params = Vec::new();
663
664        for (path, (ty, cel)) in param_schemas {
665            self.consume_field(&path)?;
666            let full_field_path = ProtoPath::new(format!("{}.{}", self.root_ty.proto_path(), path));
667
668            params.push(
669                openapiv3_1::path::Parameter::builder()
670                    .name(path)
671                    .required(true)
672                    .schema(generate(
673                        &FieldInfo {
674                            full_name: &full_field_path,
675                            ty: &ProtoType::Value(ty.clone()),
676                            cel: &cel,
677                        },
678                        self.components,
679                        self.types,
680                        &BTreeMap::new(),
681                        GenerateDirection::Input,
682                        BytesEncoding::Base64,
683                    )?)
684                    .parameter_in(openapiv3_1::path::ParameterIn::Path)
685                    .build(),
686            )
687        }
688
689        Ok(GeneratedParams {
690            params,
691            tokens: quote!({
692                #[derive(::tinc::reexports::serde::Deserialize)]
693                #[allow(non_snake_case, dead_code)]
694                struct ____PathContent {
695                    #(#defs),*
696                }
697
698                let path = match ::tinc::__private::deserialize_path::<____PathContent>(&mut parts).await {
699                    Ok(path) => path,
700                    Err(err) => return err,
701                };
702
703                #(#mappings)*
704            }),
705        })
706    }
707
708    pub(super) fn generate_body(
709        &mut self,
710        cel: &[CelExpression],
711        body_method: BodyMethod,
712        field: Option<&str>,
713        content_type_field: Option<&str>,
714    ) -> anyhow::Result<GeneratedBody<openapiv3_1::request_body::RequestBody>> {
715        let content_type = if let Some(content_type_field) = content_type_field {
716            self.consume_field(content_type_field)?;
717            let extract = input_field_getter_gen(self.types, &self.root_ty, self.base_extract(), content_type_field)?;
718
719            anyhow::ensure!(
720                matches!(extract.ty.value_type(), Some(ProtoValueType::String)),
721                "content-type must be a string type"
722            );
723
724            anyhow::ensure!(!extract.ty.nested(), "content-type cannot be nested");
725
726            let modifier = if extract.is_optional {
727                quote! {
728                    tracker.get_or_insert_default();
729                    target.insert(ct.into());
730                }
731            } else {
732                quote! {
733                    let _ = tracker;
734                    *target = ct.into();
735                }
736            };
737
738            let extract = extract.tokens;
739
740            quote! {
741                if let Some(ct) = parts.headers.get(::tinc::reexports::http::header::CONTENT_TYPE).and_then(|h| h.to_str().ok()) {
742                    let (mut tracker, mut target) = #extract;
743                    #modifier
744                }
745            }
746        } else {
747            TokenStream::new()
748        };
749
750        let exclude_paths = if let Some(field) = field {
751            match self.used_paths.get(field) {
752                Some(ExcludePaths::Child(c)) => Some(c),
753                Some(ExcludePaths::True) => anyhow::bail!("{field} is already used by another operation"),
754                None => None,
755            }
756        } else {
757            Some(&self.used_paths)
758        };
759
760        let extract = if let Some(field) = field {
761            input_field_getter_gen(self.types, &self.root_ty, self.base_extract(), field)?
762        } else {
763            FieldExtract {
764                full_name: ProtoPath::new(self.root_ty.proto_path()),
765                cel: CelExpressions {
766                    field: cel.to_vec(),
767                    ..Default::default()
768                },
769                is_optional: false,
770                tokens: self.base_extract(),
771                ty: ProtoType::Value(self.root_ty.clone()),
772            }
773        };
774
775        match body_method {
776            BodyMethod::Json => {}
777            BodyMethod::Binary(_) => {
778                anyhow::ensure!(
779                    matches!(extract.ty.value_type(), Some(ProtoValueType::Bytes)),
780                    "binary bodies must be on bytes fields."
781                );
782
783                anyhow::ensure!(!extract.ty.nested(), "binary bodies cannot be nested");
784            }
785            BodyMethod::Text => {
786                anyhow::ensure!(
787                    matches!(extract.ty.value_type(), Some(ProtoValueType::String)),
788                    "text bodies must be on string fields."
789                );
790
791                anyhow::ensure!(!extract.ty.nested(), "text bodies cannot be nested");
792            }
793        }
794
795        let func = body_method.deserialize_method();
796        let tokens = &extract.tokens;
797        let state_ident = &self.state_ident;
798
799        Ok(GeneratedBody {
800            tokens: quote! {{
801                #content_type
802                let (tracker, target) = #tokens;
803                if let Err(err) = ::tinc::__private::#func(&parts, body, tracker, target, &mut #state_ident).await {
804                    return err;
805                }
806            }},
807            body: openapiv3_1::request_body::RequestBody::builder()
808                .content(
809                    body_method.content_type(),
810                    openapiv3_1::content::Content::new(Some(generate(
811                        &FieldInfo {
812                            full_name: &extract.full_name,
813                            ty: &extract.ty,
814                            cel: &extract.cel,
815                        },
816                        self.components,
817                        self.types,
818                        exclude_paths.unwrap_or(&BTreeMap::new()),
819                        GenerateDirection::Input,
820                        body_method.bytes_encoding(),
821                    )?)),
822                )
823                .build(),
824        })
825    }
826}
827
828impl OutputGenerator<'_> {
829    fn base_extract(&self) -> TokenStream {
830        let response_ident = &self.response_ident;
831        quote!((&#response_ident))
832    }
833
834    pub(super) fn generate_body(
835        &mut self,
836        body_method: BodyMethod,
837        field: Option<&str>,
838        content_type_field: Option<&str>,
839    ) -> anyhow::Result<GeneratedBody<openapiv3_1::response::Response>> {
840        let builder_ident = &self.builder_ident;
841
842        let content_type = if let Some(content_type_field) = content_type_field {
843            let extract = output_field_getter_gen(self.types, &self.root_ty, self.base_extract(), content_type_field)?;
844
845            anyhow::ensure!(
846                matches!(extract.ty.value_type(), Some(ProtoValueType::String)),
847                "content-type must be a string type"
848            );
849
850            anyhow::ensure!(!extract.ty.nested(), "content-type cannot be nested");
851
852            let modifier = if extract.is_optional { quote!(Some(ct)) } else { quote!(ct) };
853
854            let extract = extract.tokens;
855            let default_ct = body_method.default_content_type();
856
857            quote! {
858                if let #modifier = #extract {
859                    #builder_ident.header(::tinc::reexports::http::header::CONTENT_TYPE, ct)
860                } else {
861                    #builder_ident.header(::tinc::reexports::http::header::CONTENT_TYPE, #default_ct)
862                }
863            }
864        } else {
865            let default_ct = body_method.default_content_type();
866            quote! {
867                #builder_ident.header(::tinc::reexports::http::header::CONTENT_TYPE, #default_ct)
868            }
869        };
870
871        let extract = if let Some(field) = field {
872            output_field_getter_gen(self.types, &self.root_ty, self.base_extract(), field)?
873        } else {
874            FieldExtract {
875                full_name: ProtoPath::new(self.root_ty.proto_path()),
876                cel: CelExpressions::default(),
877                is_optional: false,
878                tokens: self.base_extract(),
879                ty: ProtoType::Value(self.root_ty.clone()),
880            }
881        };
882
883        let tokens = extract.tokens;
884
885        let tokens = match body_method {
886            BodyMethod::Json => quote!({
887                let mut writer = ::tinc::reexports::bytes::BufMut::writer(
888                    ::tinc::reexports::bytes::BytesMut::with_capacity(128)
889                );
890                match ::tinc::reexports::serde_json::to_writer(&mut writer, #tokens) {
891                    ::core::result::Result::Ok(()) => {},
892                    ::core::result::Result::Err(err) => return ::tinc::__private::handle_response_build_error(err),
893                }
894                (#content_type)
895                    .body(::tinc::reexports::axum::body::Body::from(writer.into_inner().freeze()))
896            }),
897            BodyMethod::Binary(_) => {
898                anyhow::ensure!(
899                    matches!(extract.ty.value_type(), Some(ProtoValueType::Bytes)),
900                    "binary bodies must be on bytes fields."
901                );
902
903                anyhow::ensure!(!extract.ty.nested(), "binary bodies cannot be nested");
904
905                let matcher = if extract.is_optional {
906                    quote!(Some(bytes))
907                } else {
908                    quote!(bytes)
909                };
910
911                quote!({
912                    (#content_type)
913                        .body(if let #matcher = #tokens {
914                            ::tinc::reexports::axum::body::Body::from(bytes.clone())
915                        } else {
916                            ::tinc::reexports::axum::body::Body::empty()
917                        })
918                })
919            }
920            BodyMethod::Text => {
921                anyhow::ensure!(
922                    matches!(extract.ty.value_type(), Some(ProtoValueType::String)),
923                    "text bodies must be on string fields."
924                );
925
926                anyhow::ensure!(!extract.ty.nested(), "text bodies cannot be nested");
927
928                let matcher = if extract.is_optional {
929                    quote!(Some(text))
930                } else {
931                    quote!(text)
932                };
933
934                quote!({
935                    (#content_type)
936                        .body(if let #matcher = #tokens {
937                            ::tinc::reexports::axum::body::Body::from(text.clone())
938                        } else {
939                            ::tinc::reexports::axum::body::Body::empty()
940                        })
941                })
942            }
943        };
944
945        Ok(GeneratedBody {
946            tokens,
947            body: openapiv3_1::Response::builder()
948                .content(
949                    body_method.content_type(),
950                    openapiv3_1::Content::new(Some(generate(
951                        &FieldInfo {
952                            full_name: &extract.full_name,
953                            ty: &extract.ty,
954                            cel: &extract.cel,
955                        },
956                        self.components,
957                        self.types,
958                        &BTreeMap::new(),
959                        GenerateDirection::Output,
960                        body_method.bytes_encoding(),
961                    )?)),
962                )
963                .description("")
964                .build(),
965        })
966    }
967}
968
969struct FieldInfo<'a> {
970    full_name: &'a ProtoPath,
971    ty: &'a ProtoType,
972    cel: &'a CelExpressions,
973}
974
975fn generate(
976    field_info: &FieldInfo,
977    components: &mut openapiv3_1::Components,
978    types: &ProtoTypeRegistry,
979    used_paths: &BTreeMap<String, ExcludePaths>,
980    direction: GenerateDirection,
981    bytes: BytesEncoding,
982) -> anyhow::Result<Schema> {
983    fn internal_generate(
984        field_info: &FieldInfo,
985        components: &mut openapiv3_1::Components,
986        types: &ProtoTypeRegistry,
987        used_paths: &BTreeMap<String, ExcludePaths>,
988        direction: GenerateDirection,
989        bytes: BytesEncoding,
990    ) -> anyhow::Result<Schema> {
991        let mut schemas = Vec::new();
992        let ty = field_info.ty.clone();
993        let cel = field_info.cel;
994        let full_field_name = field_info.full_name;
995
996        let compiler = Compiler::new(types);
997        if !matches!(ty, ProtoType::Modified(ProtoModifiedValueType::Optional(_))) {
998            for expr in &cel.field {
999                schemas.extend(handle_expr(compiler.child(), &ty, expr)?);
1000            }
1001        }
1002
1003        schemas.push(match ty {
1004            ProtoType::Modified(ProtoModifiedValueType::Map(key, value)) => Schema::object(
1005                Object::builder()
1006                    .schema_type(Type::Object)
1007                    .property_names(match key {
1008                        ProtoValueType::String => {
1009                            let mut schemas = Vec::with_capacity(1 + cel.map_key.len());
1010
1011                            for expr in &cel.map_key {
1012                                schemas.extend(handle_expr(compiler.child(), &ProtoType::Value(key.clone()), expr)?);
1013                            }
1014
1015                            schemas.push(Schema::object(Object::builder().schema_type(Type::String)));
1016
1017                            Object::all_ofs(schemas)
1018                        }
1019                        ProtoValueType::Int32 | ProtoValueType::Int64 => {
1020                            Object::builder().schema_type(Type::String).pattern("^-?[0-9]+$").build()
1021                        }
1022                        ProtoValueType::UInt32 | ProtoValueType::UInt64 => {
1023                            Object::builder().schema_type(Type::String).pattern("^[0-9]+$").build()
1024                        }
1025                        ProtoValueType::Bool => Object::builder()
1026                            .schema_type(Type::String)
1027                            .enum_values(["true", "false"])
1028                            .build(),
1029                        _ => Object::builder().schema_type(Type::String).build(),
1030                    })
1031                    .additional_properties({
1032                        let mut schemas = Vec::with_capacity(1 + cel.map_value.len());
1033                        for expr in &cel.map_value {
1034                            schemas.extend(handle_expr(compiler.child(), &ProtoType::Value(value.clone()), expr)?);
1035                        }
1036
1037                        schemas.push(internal_generate(
1038                            &FieldInfo {
1039                                full_name: full_field_name,
1040                                ty: &ProtoType::Value(value.clone()),
1041                                cel: &CelExpressions::default(),
1042                            },
1043                            components,
1044                            types,
1045                            &BTreeMap::new(),
1046                            direction,
1047                            bytes,
1048                        )?);
1049
1050                        Object::all_ofs(schemas)
1051                    })
1052                    .build(),
1053            ),
1054            ProtoType::Modified(ProtoModifiedValueType::Repeated(item)) => Schema::object(
1055                Object::builder()
1056                    .schema_type(Type::Array)
1057                    .items(internal_generate(
1058                        &FieldInfo {
1059                            full_name: full_field_name,
1060                            ty: &ProtoType::Value(item.clone()),
1061                            cel,
1062                        },
1063                        components,
1064                        types,
1065                        used_paths,
1066                        direction,
1067                        bytes,
1068                    )?)
1069                    .build(),
1070            ),
1071            ProtoType::Modified(ProtoModifiedValueType::OneOf(oneof)) => Schema::object(
1072                Object::builder()
1073                    .schema_type(Type::Object)
1074                    .title(oneof.full_name.to_string())
1075                    .one_ofs(if let Some(tagged) = oneof.options.tagged {
1076                        oneof
1077                            .fields
1078                            .into_iter()
1079                            .filter(|(_, field)| match direction {
1080                                GenerateDirection::Input => field.options.visibility.has_input(),
1081                                GenerateDirection::Output => field.options.visibility.has_output(),
1082                            })
1083                            .map(|(name, field)| {
1084                                let ty = internal_generate(
1085                                    &FieldInfo {
1086                                        full_name: &field.full_name,
1087                                        ty: &ProtoType::Value(field.ty.clone()),
1088                                        cel: &field.options.cel_exprs,
1089                                    },
1090                                    components,
1091                                    types,
1092                                    &BTreeMap::new(),
1093                                    direction,
1094                                    bytes,
1095                                )?;
1096
1097                                anyhow::Ok(Schema::object(
1098                                    Object::builder()
1099                                        .schema_type(Type::Object)
1100                                        .title(name)
1101                                        .description(field.comments.to_string())
1102                                        .properties({
1103                                            let mut properties = IndexMap::new();
1104                                            properties.insert(
1105                                                tagged.tag.clone(),
1106                                                Schema::object(
1107                                                    Object::builder()
1108                                                        .schema_type(Type::String)
1109                                                        .const_value(field.options.serde_name.clone())
1110                                                        .build(),
1111                                                ),
1112                                            );
1113                                            properties.insert(tagged.content.clone(), ty);
1114                                            properties
1115                                        })
1116                                        .unevaluated_properties(false)
1117                                        .build(),
1118                                ))
1119                            })
1120                            .collect::<anyhow::Result<Vec<_>>>()?
1121                    } else {
1122                        oneof
1123                            .fields
1124                            .into_iter()
1125                            .filter(|(_, field)| match direction {
1126                                GenerateDirection::Input => field.options.visibility.has_input(),
1127                                GenerateDirection::Output => field.options.visibility.has_output(),
1128                            })
1129                            .map(|(name, field)| {
1130                                let ty = internal_generate(
1131                                    &FieldInfo {
1132                                        full_name: &field.full_name,
1133                                        ty: &ProtoType::Value(field.ty.clone()),
1134                                        cel: &field.options.cel_exprs,
1135                                    },
1136                                    components,
1137                                    types,
1138                                    &BTreeMap::new(),
1139                                    direction,
1140                                    bytes,
1141                                )?;
1142
1143                                anyhow::Ok(Schema::object(
1144                                    Object::builder()
1145                                        .schema_type(Type::Object)
1146                                        .title(name)
1147                                        .description(field.comments.to_string())
1148                                        .properties({
1149                                            let mut properties = IndexMap::new();
1150                                            properties.insert(&field.options.serde_name, ty);
1151                                            properties
1152                                        })
1153                                        .unevaluated_properties(false)
1154                                        .build(),
1155                                ))
1156                            })
1157                            .collect::<anyhow::Result<Vec<_>>>()?
1158                    })
1159                    .unevaluated_properties(false)
1160                    .build(),
1161            ),
1162            ProtoType::Modified(ProtoModifiedValueType::Optional(value)) => Schema::object(
1163                Object::builder()
1164                    .one_ofs([
1165                        Schema::object(Object::builder().schema_type(Type::Null).build()),
1166                        internal_generate(
1167                            &FieldInfo {
1168                                full_name: full_field_name,
1169                                ty: &ProtoType::Value(value.clone()),
1170                                cel,
1171                            },
1172                            components,
1173                            types,
1174                            used_paths,
1175                            direction,
1176                            bytes,
1177                        )?,
1178                    ])
1179                    .build(),
1180            ),
1181            ProtoType::Value(ProtoValueType::Bool) => Schema::object(Object::builder().schema_type(Type::Boolean).build()),
1182            ProtoType::Value(ProtoValueType::Bytes) => Schema::object(
1183                Object::builder()
1184                    .schema_type(Type::String)
1185                    .content_encoding(match bytes {
1186                        BytesEncoding::Base64 => "base64",
1187                        BytesEncoding::Binary => "binary",
1188                    })
1189                    .build(),
1190            ),
1191            ProtoType::Value(ProtoValueType::Double | ProtoValueType::Float) => {
1192                if types.support_non_finite_vals(full_field_name) {
1193                    Schema::object(
1194                        Object::builder()
1195                            .one_ofs([
1196                                Schema::object(Object::builder().schema_type(Type::Number).build()),
1197                                Schema::object(
1198                                    Object::builder()
1199                                        .schema_type(Type::String)
1200                                        .enum_values(vec![
1201                                            serde_json::Value::from("Infinity"),
1202                                            serde_json::Value::from("-Infinity"),
1203                                            serde_json::Value::from("NaN"),
1204                                        ])
1205                                        .build(),
1206                                ),
1207                            ])
1208                            .build(),
1209                    )
1210                } else {
1211                    Schema::object(Object::builder().schema_type(Type::Number).build())
1212                }
1213            }
1214            ProtoType::Value(ProtoValueType::Int32) => Schema::object(Object::int32()),
1215            ProtoType::Value(ProtoValueType::UInt32) => Schema::object(Object::uint32()),
1216            ProtoType::Value(ProtoValueType::Int64) => Schema::object(Object::int64()),
1217            ProtoType::Value(ProtoValueType::UInt64) => Schema::object(Object::uint64()),
1218            ProtoType::Value(ProtoValueType::String) => Schema::object(Object::builder().schema_type(Type::String).build()),
1219            ProtoType::Value(ProtoValueType::Enum(enum_path)) => {
1220                let ety = types
1221                    .get_enum(&enum_path)
1222                    .with_context(|| format!("missing enum: {enum_path}"))?;
1223                let schema_name = if ety
1224                    .variants
1225                    .values()
1226                    .any(|v| v.options.visibility.has_input() != v.options.visibility.has_output())
1227                {
1228                    format!("{direction:?}.{enum_path}")
1229                } else {
1230                    enum_path.to_string()
1231                };
1232
1233                if !components.schemas.contains_key(enum_path.as_ref()) {
1234                    components.add_schema(
1235                        schema_name.clone(),
1236                        Schema::object(
1237                            Object::builder()
1238                                .schema_type(if ety.options.repr_enum { Type::Integer } else { Type::String })
1239                                .enum_values(
1240                                    ety.variants
1241                                        .values()
1242                                        .filter(|v| match direction {
1243                                            GenerateDirection::Input => v.options.visibility.has_input(),
1244                                            GenerateDirection::Output => v.options.visibility.has_output(),
1245                                        })
1246                                        .map(|v| {
1247                                            if ety.options.repr_enum {
1248                                                serde_json::Value::from(v.value)
1249                                            } else {
1250                                                serde_json::Value::from(v.options.serde_name.clone())
1251                                            }
1252                                        })
1253                                        .collect::<Vec<_>>(),
1254                                )
1255                                .title(enum_path.to_string())
1256                                .description(ety.comments.to_string())
1257                                .build(),
1258                        ),
1259                    );
1260                }
1261
1262                Schema::object(Ref::from_schema_name(schema_name))
1263            }
1264            ref ty @ ProtoType::Value(ProtoValueType::Message(ref message_path)) => {
1265                let message_ty = types
1266                    .get_message(message_path)
1267                    .with_context(|| format!("missing message: {message_path}"))?;
1268
1269                let schema_name = if message_ty
1270                    .fields
1271                    .values()
1272                    .any(|v| v.options.visibility.has_input() != v.options.visibility.has_output())
1273                {
1274                    format!("{direction:?}.{message_path}")
1275                } else {
1276                    message_path.to_string()
1277                };
1278
1279                if !components.schemas.contains_key(&schema_name) || !used_paths.is_empty() {
1280                    if used_paths.is_empty() {
1281                        components.schemas.insert(schema_name.clone(), Schema::Bool(false));
1282                    }
1283                    let mut properties = IndexMap::new();
1284                    let mut required = Vec::new();
1285                    let mut schemas = Vec::with_capacity(1);
1286
1287                    for expr in &message_ty.options.cel {
1288                        schemas.extend(handle_expr(compiler.child(), ty, expr)?);
1289                    }
1290
1291                    for (name, field) in message_ty.fields.iter().filter(|(_, field)| match direction {
1292                        GenerateDirection::Input => field.options.visibility.has_input(),
1293                        GenerateDirection::Output => field.options.visibility.has_output(),
1294                    }) {
1295                        let exclude_paths = match used_paths.get(name) {
1296                            Some(ExcludePaths::True) => continue,
1297                            Some(ExcludePaths::Child(child)) => Some(child),
1298                            None => None,
1299                        };
1300                        if !field.options.serde_omittable.is_true() {
1301                            required.push(field.options.serde_name.clone());
1302                        }
1303
1304                        let ty = match (!field.options.nullable || field.options.flatten, &field.ty) {
1305                            (true, ProtoType::Modified(ProtoModifiedValueType::Optional(ty))) => {
1306                                ProtoType::Value(ty.clone())
1307                            }
1308                            _ => field.ty.clone(),
1309                        };
1310
1311                        let field_schema = internal_generate(
1312                            &FieldInfo {
1313                                full_name: &field.full_name,
1314                                ty: &ty,
1315                                cel: &field.options.cel_exprs,
1316                            },
1317                            components,
1318                            types,
1319                            exclude_paths.unwrap_or(&BTreeMap::new()),
1320                            direction,
1321                            bytes,
1322                        )?;
1323
1324                        if field.options.flatten {
1325                            schemas.push(field_schema);
1326                        } else {
1327                            let schema = if field.options.nullable
1328                                && !matches!(&field.ty, ProtoType::Modified(ProtoModifiedValueType::Optional(_)))
1329                            {
1330                                Schema::object(
1331                                    Object::builder()
1332                                        .one_ofs([Object::builder().schema_type(Type::Null).build().into(), field_schema])
1333                                        .build(),
1334                                )
1335                            } else {
1336                                field_schema
1337                            };
1338
1339                            properties.insert(
1340                                field.options.serde_name.clone(),
1341                                Schema::object(Object::all_ofs([
1342                                    schema,
1343                                    Schema::object(Object::builder().description(field.comments.to_string()).build()),
1344                                ])),
1345                            );
1346                        }
1347                    }
1348
1349                    schemas.push(Schema::object(
1350                        Object::builder()
1351                            .schema_type(Type::Object)
1352                            .title(message_path.to_string())
1353                            .description(message_ty.comments.to_string())
1354                            .properties(properties)
1355                            .required(required)
1356                            .unevaluated_properties(false)
1357                            .build(),
1358                    ));
1359
1360                    if used_paths.is_empty() {
1361                        components.add_schema(schema_name.clone(), Object::all_ofs(schemas).into_optimized());
1362                        Schema::object(Ref::from_schema_name(schema_name))
1363                    } else {
1364                        Schema::object(Object::all_ofs(schemas))
1365                    }
1366                } else {
1367                    Schema::object(Ref::from_schema_name(schema_name))
1368                }
1369            }
1370            ProtoType::Value(ProtoValueType::WellKnown(ProtoWellKnownType::Timestamp)) => {
1371                Schema::object(Object::builder().schema_type(Type::String).format("date-time").build())
1372            }
1373            ProtoType::Value(ProtoValueType::WellKnown(ProtoWellKnownType::Duration)) => {
1374                Schema::object(Object::builder().schema_type(Type::String).format("duration").build())
1375            }
1376            ProtoType::Value(ProtoValueType::WellKnown(ProtoWellKnownType::Empty)) => Schema::object(
1377                Object::builder()
1378                    .schema_type(Type::Object)
1379                    .unevaluated_properties(false)
1380                    .build(),
1381            ),
1382            ProtoType::Value(ProtoValueType::WellKnown(ProtoWellKnownType::ListValue)) => {
1383                Schema::object(Object::builder().schema_type(Type::Array).build())
1384            }
1385            ProtoType::Value(ProtoValueType::WellKnown(ProtoWellKnownType::Value)) => Schema::object(
1386                Object::builder()
1387                    .schema_type(vec![
1388                        Type::Null,
1389                        Type::Boolean,
1390                        Type::Object,
1391                        Type::Array,
1392                        Type::Number,
1393                        Type::String,
1394                    ])
1395                    .build(),
1396            ),
1397            ProtoType::Value(ProtoValueType::WellKnown(ProtoWellKnownType::Struct)) => {
1398                Schema::object(Object::builder().schema_type(Type::Object).build())
1399            }
1400            ProtoType::Value(ProtoValueType::WellKnown(ProtoWellKnownType::Any)) => Schema::object(
1401                Object::builder()
1402                    .schema_type(Type::Object)
1403                    .property("@type", Object::builder().schema_type(Type::String))
1404                    .build(),
1405            ),
1406        });
1407
1408        Ok(Schema::object(Object::all_ofs(schemas)))
1409    }
1410
1411    internal_generate(field_info, components, types, used_paths, direction, bytes).map(|schema| schema.into_optimized())
1412}