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