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 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 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 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 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, ¶ms, 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}