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