1use std::collections::BTreeSet;
25
26use itertools::Itertools;
27use ploidy_core::ir::{InlineIrTypeView, IrOperationView, IrTypeView, SchemaIrTypeView, View};
28use proc_macro2::TokenStream;
29use quote::{ToTokens, quote};
30
31use super::naming::CargoFeature;
32
33#[derive(Clone, Debug, Eq, PartialEq)]
35pub enum CfgFeature {
36 Single(CargoFeature),
38 AnyOf(BTreeSet<CargoFeature>),
40 AllOf(BTreeSet<CargoFeature>),
42 OwnAndUsedBy {
46 own: CargoFeature,
47 used_by: BTreeSet<CargoFeature>,
48 },
49}
50
51impl CfgFeature {
52 pub fn for_schema_type(view: &SchemaIrTypeView<'_>) -> Option<Self> {
55 let has_ungated_root_dependent = view
61 .dependents()
62 .filter_map(IrTypeView::as_schema)
63 .any(|s| s.resource().is_none() && s.used_by().all(|op| op.resource().is_none()));
64 if has_ungated_root_dependent {
65 return None;
66 }
67
68 let used_by_features: BTreeSet<_> = view
70 .used_by()
71 .filter_map(|op| op.resource())
72 .map(CargoFeature::from_name)
73 .collect();
74
75 match (view.resource(), used_by_features.is_empty()) {
76 (Some(name), false) => {
78 Self::own_and_used_by(CargoFeature::from_name(name), used_by_features)
79 }
80 (Some(name), true) => Some(Self::Single(CargoFeature::from_name(name))),
82 (None, false) => Self::any_of(used_by_features),
85 (None, true) => None,
87 }
88 }
89
90 pub fn for_inline_type(view: &InlineIrTypeView<'_>) -> Option<Self> {
92 let has_ungated_root_dependent = view
95 .dependents()
96 .filter_map(IrTypeView::as_schema)
97 .any(|s| s.resource().is_none() && s.used_by().all(|op| op.resource().is_none()));
98 if has_ungated_root_dependent {
99 return None;
100 }
101
102 let used_by_features: BTreeSet<_> = view
103 .used_by()
104 .filter_map(|op| op.resource())
105 .map(CargoFeature::from_name)
106 .collect();
107
108 if used_by_features.is_empty() {
109 let pairs = view
113 .dependencies()
114 .filter_map(IrTypeView::as_schema)
115 .filter_map(|ty| ty.resource().map(|r| (CargoFeature::from_name(r), ty)))
116 .collect_vec();
117 Self::all_of(reduce_transitive_features(&pairs))
118 } else {
119 Self::any_of(used_by_features)
121 }
122 }
123
124 pub fn for_operation(view: &IrOperationView<'_>) -> Option<Self> {
126 let pairs = view
129 .dependencies()
130 .filter_map(IrTypeView::as_schema)
131 .filter_map(|ty| ty.resource().map(|r| (CargoFeature::from_name(r), ty)))
132 .collect_vec();
133
134 Self::all_of(reduce_transitive_features(&pairs))
135 }
136
137 pub fn for_resource_module(feature: &CargoFeature) -> Option<Self> {
140 if matches!(feature, CargoFeature::Default) {
141 return None;
145 }
146 Some(Self::Single(feature.clone()))
147 }
148
149 fn any_of(mut features: BTreeSet<CargoFeature>) -> Option<Self> {
151 if features.contains(&CargoFeature::Default) {
152 return None;
154 }
155 let first = features.pop_first()?;
156 Some(if features.is_empty() {
157 Self::Single(first)
159 } else {
160 features.insert(first);
161 Self::AnyOf(features)
162 })
163 }
164
165 fn all_of(mut features: BTreeSet<CargoFeature>) -> Option<Self> {
167 if features.contains(&CargoFeature::Default) {
168 return None;
170 }
171 let first = features.pop_first()?;
172 Some(if features.is_empty() {
173 Self::Single(first)
175 } else {
176 features.insert(first);
177 Self::AllOf(features)
178 })
179 }
180
181 fn own_and_used_by(own: CargoFeature, mut used_by: BTreeSet<CargoFeature>) -> Option<Self> {
183 if matches!(own, CargoFeature::Default) || used_by.contains(&CargoFeature::Default) {
184 return None;
186 }
187 let Some(first) = used_by.pop_first() else {
188 return Some(Self::Single(own));
190 };
191 Some(if used_by.is_empty() {
192 Self::AllOf(BTreeSet::from_iter([own, first]))
194 } else {
195 used_by.insert(first);
197 Self::OwnAndUsedBy { own, used_by }
198 })
199 }
200}
201
202impl ToTokens for CfgFeature {
203 fn to_tokens(&self, tokens: &mut TokenStream) {
204 let predicate = match self {
205 Self::Single(feature) => {
206 let name = feature.display().to_string();
207 quote! { feature = #name }
208 }
209 Self::AnyOf(features) => {
210 let predicates = features.iter().map(|f| {
211 let name = f.display().to_string();
212 quote! { feature = #name }
213 });
214 quote! { any(#(#predicates),*) }
215 }
216 Self::AllOf(features) => {
217 let predicates = features.iter().map(|f| {
218 let name = f.display().to_string();
219 quote! { feature = #name }
220 });
221 quote! { all(#(#predicates),*) }
222 }
223 Self::OwnAndUsedBy { own, used_by } => {
224 let own_name = own.display().to_string();
225 let used_by_predicates = used_by.iter().map(|f| {
226 let name = f.display().to_string();
227 quote! { feature = #name }
228 });
229 quote! { all(feature = #own_name, any(#(#used_by_predicates),*)) }
230 }
231 };
232 tokens.extend(quote! { #[cfg(#predicate)] });
233 }
234}
235
236fn reduce_transitive_features(
241 pairs: &[(CargoFeature, SchemaIrTypeView<'_>)],
242) -> BTreeSet<CargoFeature> {
243 pairs
244 .iter()
245 .enumerate()
246 .filter(|&(i, (feature, ty))| {
247 let mut others = pairs.iter().enumerate().filter(|&(j, _)| i != j);
250 !others.any(|(_, (other_feature, other_ty))| {
251 if !other_ty.depends_on(ty) {
253 return false;
254 }
255 if ty.depends_on(other_ty) {
259 return other_feature < feature;
260 }
261 true
262 })
263 })
264 .map(|(_, (feature, _))| feature.clone())
265 .collect()
266}
267
268#[cfg(test)]
269mod tests {
270 use super::*;
271
272 use itertools::Itertools;
273 use ploidy_core::{
274 ir::{IrGraph, IrSpec},
275 parse::Document,
276 };
277 use pretty_assertions::assert_eq;
278 use syn::parse_quote;
279
280 use crate::graph::CodegenGraph;
281
282 #[test]
285 fn test_single_feature() {
286 let cfg = CfgFeature::Single(CargoFeature::from_name("pets"));
287
288 let actual: syn::Attribute = parse_quote!(#cfg);
289 let expected: syn::Attribute = parse_quote!(#[cfg(feature = "pets")]);
290 assert_eq!(actual, expected);
291 }
292
293 #[test]
294 fn test_any_of_features() {
295 let cfg = CfgFeature::AnyOf(BTreeSet::from_iter([
296 CargoFeature::from_name("cats"),
297 CargoFeature::from_name("dogs"),
298 CargoFeature::from_name("aardvarks"),
299 ]));
300
301 let actual: syn::Attribute = parse_quote!(#cfg);
302 let expected: syn::Attribute =
303 parse_quote!(#[cfg(any(feature = "aardvarks", feature = "cats", feature = "dogs"))]);
304 assert_eq!(actual, expected);
305 }
306
307 #[test]
308 fn test_all_of_features() {
309 let cfg = CfgFeature::AllOf(BTreeSet::from_iter([
310 CargoFeature::from_name("cats"),
311 CargoFeature::from_name("dogs"),
312 CargoFeature::from_name("aardvarks"),
313 ]));
314
315 let actual: syn::Attribute = parse_quote!(#cfg);
316 let expected: syn::Attribute =
317 parse_quote!(#[cfg(all(feature = "aardvarks", feature = "cats", feature = "dogs"))]);
318 assert_eq!(actual, expected);
319 }
320
321 #[test]
322 fn test_own_and_used_by_feature() {
323 let cfg = CfgFeature::OwnAndUsedBy {
324 own: CargoFeature::from_name("own"),
325 used_by: BTreeSet::from_iter([
326 CargoFeature::from_name("a"),
327 CargoFeature::from_name("b"),
328 ]),
329 };
330
331 let actual: syn::Attribute = parse_quote!(#cfg);
332 let expected: syn::Attribute =
333 parse_quote!(#[cfg(all(feature = "own", any(feature = "a", feature = "b")))]);
334 assert_eq!(actual, expected);
335 }
336
337 #[test]
338 fn test_any_of_simplifies_single_feature() {
339 let cfg = CfgFeature::any_of(BTreeSet::from_iter([CargoFeature::from_name("pets")]));
340
341 let actual: syn::Attribute = parse_quote!(#cfg);
342 let expected: syn::Attribute = parse_quote!(#[cfg(feature = "pets")]);
343 assert_eq!(actual, expected);
344 }
345
346 #[test]
347 fn test_all_of_simplifies_single_feature() {
348 let cfg = CfgFeature::all_of(BTreeSet::from_iter([CargoFeature::from_name("pets")]));
349
350 let actual: syn::Attribute = parse_quote!(#cfg);
351 let expected: syn::Attribute = parse_quote!(#[cfg(feature = "pets")]);
352 assert_eq!(actual, expected);
353 }
354
355 #[test]
356 fn test_any_of_returns_none_for_empty() {
357 let cfg = CfgFeature::any_of(BTreeSet::new());
358 assert_eq!(cfg, None);
359 }
360
361 #[test]
362 fn test_any_of_returns_none_when_contains_default() {
363 let cfg = CfgFeature::any_of(BTreeSet::from_iter([
365 CargoFeature::from_name("customer"),
366 CargoFeature::Default,
367 CargoFeature::from_name("billing"),
368 ]));
369 assert_eq!(cfg, None);
370 }
371
372 #[test]
373 fn test_all_of_returns_none_for_empty() {
374 let cfg = CfgFeature::all_of(BTreeSet::new());
375 assert_eq!(cfg, None);
376 }
377
378 #[test]
379 fn test_all_of_returns_none_when_contains_default() {
380 let cfg = CfgFeature::all_of(BTreeSet::from_iter([
382 CargoFeature::from_name("customer"),
383 CargoFeature::Default,
384 CargoFeature::from_name("billing"),
385 ]));
386 assert_eq!(cfg, None);
387 }
388
389 #[test]
390 fn test_own_and_used_by_simplifies_single_used_by_to_all_of() {
391 let cfg = CfgFeature::own_and_used_by(
393 CargoFeature::from_name("own"),
394 BTreeSet::from_iter([CargoFeature::from_name("other")]),
395 );
396 assert_eq!(
397 cfg,
398 Some(CfgFeature::AllOf(BTreeSet::from_iter([
399 CargoFeature::from_name("other"),
400 CargoFeature::from_name("own"),
401 ])))
402 );
403 }
404
405 #[test]
406 fn test_own_and_used_by_simplifies_empty_to_single() {
407 let cfg = CfgFeature::own_and_used_by(CargoFeature::from_name("own"), BTreeSet::new());
409 assert_eq!(
410 cfg,
411 Some(CfgFeature::Single(CargoFeature::from_name("own")))
412 );
413 }
414
415 #[test]
416 fn test_own_and_used_by_returns_none_when_own_is_default() {
417 let cfg = CfgFeature::own_and_used_by(
419 CargoFeature::Default,
420 BTreeSet::from_iter([CargoFeature::from_name("other")]),
421 );
422 assert_eq!(cfg, None);
423 }
424
425 #[test]
426 fn test_own_and_used_by_returns_none_when_used_by_contains_default() {
427 let cfg = CfgFeature::own_and_used_by(
429 CargoFeature::from_name("own"),
430 BTreeSet::from_iter([CargoFeature::from_name("other"), CargoFeature::Default]),
431 );
432 assert_eq!(cfg, None);
433 }
434
435 #[test]
438 fn test_for_schema_type_returns_empty_when_no_named_resources() {
439 let doc = Document::from_yaml(indoc::indoc! {"
441 openapi: 3.0.0
442 info:
443 title: Test
444 version: 1.0.0
445 components:
446 schemas:
447 Customer:
448 type: object
449 properties:
450 id:
451 type: string
452 "})
453 .unwrap();
454
455 let spec = IrSpec::from_doc(&doc).unwrap();
456 let ir_graph = IrGraph::new(&spec);
457 let graph = CodegenGraph::new(ir_graph);
458
459 let customer = graph.schemas().find(|s| s.name() == "Customer").unwrap();
460
461 let cfg = CfgFeature::for_schema_type(&customer);
463 assert_eq!(cfg, None);
464 }
465
466 #[test]
469 fn test_for_schema_type_with_own_resource_no_deps() {
470 let doc = Document::from_yaml(indoc::indoc! {"
471 openapi: 3.0.0
472 info:
473 title: Test
474 version: 1.0.0
475 components:
476 schemas:
477 Customer:
478 type: object
479 x-resourceId: customer
480 properties:
481 id:
482 type: string
483 "})
484 .unwrap();
485
486 let spec = IrSpec::from_doc(&doc).unwrap();
487 let ir_graph = IrGraph::new(&spec);
488 let graph = CodegenGraph::new(ir_graph);
489
490 let customer = graph.schemas().find(|s| s.name() == "Customer").unwrap();
491 let cfg = CfgFeature::for_schema_type(&customer);
492
493 let actual: syn::Attribute = parse_quote!(#cfg);
494 let expected: syn::Attribute = parse_quote!(#[cfg(feature = "customer")]);
495 assert_eq!(actual, expected);
496 }
497
498 #[test]
499 fn test_for_schema_type_with_own_resource_and_unnamed_deps() {
500 let doc = Document::from_yaml(indoc::indoc! {"
503 openapi: 3.0.0
504 info:
505 title: Test
506 version: 1.0.0
507 components:
508 schemas:
509 Customer:
510 type: object
511 x-resourceId: customer
512 properties:
513 address:
514 $ref: '#/components/schemas/Address'
515 Address:
516 type: object
517 properties:
518 street:
519 type: string
520 "})
521 .unwrap();
522
523 let spec = IrSpec::from_doc(&doc).unwrap();
524 let ir_graph = IrGraph::new(&spec);
525 let graph = CodegenGraph::new(ir_graph);
526
527 let customer = graph.schemas().find(|s| s.name() == "Customer").unwrap();
529 let cfg = CfgFeature::for_schema_type(&customer);
530 let actual: syn::Attribute = parse_quote!(#cfg);
531 let expected: syn::Attribute = parse_quote!(#[cfg(feature = "customer")]);
532 assert_eq!(actual, expected);
533
534 let address = graph.schemas().find(|s| s.name() == "Address").unwrap();
536 let cfg = CfgFeature::for_schema_type(&address);
537 assert_eq!(cfg, None);
538 }
539
540 #[test]
543 fn test_for_schema_type_used_by_single_operation() {
544 let doc = Document::from_yaml(indoc::indoc! {"
545 openapi: 3.0.0
546 info:
547 title: Test
548 version: 1.0.0
549 paths:
550 /customers:
551 get:
552 operationId: listCustomers
553 x-resource-name: customer
554 responses:
555 '200':
556 description: OK
557 content:
558 application/json:
559 schema:
560 type: array
561 items:
562 $ref: '#/components/schemas/Customer'
563 components:
564 schemas:
565 Customer:
566 type: object
567 properties:
568 id:
569 type: string
570 "})
571 .unwrap();
572
573 let spec = IrSpec::from_doc(&doc).unwrap();
574 let ir_graph = IrGraph::new(&spec);
575 let graph = CodegenGraph::new(ir_graph);
576
577 let customer = graph.schemas().find(|s| s.name() == "Customer").unwrap();
578 let cfg = CfgFeature::for_schema_type(&customer);
579
580 let actual: syn::Attribute = parse_quote!(#cfg);
581 let expected: syn::Attribute = parse_quote!(#[cfg(feature = "customer")]);
582 assert_eq!(actual, expected);
583 }
584
585 #[test]
586 fn test_for_schema_type_used_by_multiple_operations() {
587 let doc = Document::from_yaml(indoc::indoc! {"
588 openapi: 3.0.0
589 info:
590 title: Test
591 version: 1.0.0
592 paths:
593 /customers:
594 get:
595 operationId: listCustomers
596 x-resource-name: customer
597 responses:
598 '200':
599 description: OK
600 content:
601 application/json:
602 schema:
603 type: array
604 items:
605 $ref: '#/components/schemas/Address'
606 /orders:
607 get:
608 operationId: listOrders
609 x-resource-name: orders
610 responses:
611 '200':
612 description: OK
613 content:
614 application/json:
615 schema:
616 type: array
617 items:
618 $ref: '#/components/schemas/Address'
619 components:
620 schemas:
621 Address:
622 type: object
623 properties:
624 street:
625 type: string
626 "})
627 .unwrap();
628
629 let spec = IrSpec::from_doc(&doc).unwrap();
630 let ir_graph = IrGraph::new(&spec);
631 let graph = CodegenGraph::new(ir_graph);
632
633 let address = graph.schemas().find(|s| s.name() == "Address").unwrap();
634 let cfg = CfgFeature::for_schema_type(&address);
635
636 let actual: syn::Attribute = parse_quote!(#cfg);
637 let expected: syn::Attribute =
638 parse_quote!(#[cfg(any(feature = "customer", feature = "orders"))]);
639 assert_eq!(actual, expected);
640 }
641
642 #[test]
645 fn test_for_schema_type_with_own_and_used_by() {
646 let doc = Document::from_yaml(indoc::indoc! {"
647 openapi: 3.0.0
648 info:
649 title: Test
650 version: 1.0.0
651 paths:
652 /billing:
653 get:
654 operationId: getBilling
655 x-resource-name: billing
656 responses:
657 '200':
658 description: OK
659 content:
660 application/json:
661 schema:
662 $ref: '#/components/schemas/Customer'
663 components:
664 schemas:
665 Customer:
666 type: object
667 x-resourceId: customer
668 properties:
669 id:
670 type: string
671 "})
672 .unwrap();
673
674 let spec = IrSpec::from_doc(&doc).unwrap();
675 let ir_graph = IrGraph::new(&spec);
676 let graph = CodegenGraph::new(ir_graph);
677
678 let customer = graph.schemas().find(|s| s.name() == "Customer").unwrap();
679 let cfg = CfgFeature::for_schema_type(&customer);
680
681 let actual: syn::Attribute = parse_quote!(#cfg);
682 let expected: syn::Attribute =
683 parse_quote!(#[cfg(all(feature = "billing", feature = "customer"))]);
684 assert_eq!(actual, expected);
685 }
686
687 #[test]
688 fn test_for_schema_type_with_own_and_multiple_used_by() {
689 let doc = Document::from_yaml(indoc::indoc! {"
690 openapi: 3.0.0
691 info:
692 title: Test
693 version: 1.0.0
694 paths:
695 /billing:
696 get:
697 operationId: getBilling
698 x-resource-name: billing
699 responses:
700 '200':
701 description: OK
702 content:
703 application/json:
704 schema:
705 $ref: '#/components/schemas/Customer'
706 /orders:
707 get:
708 operationId: getOrders
709 x-resource-name: orders
710 responses:
711 '200':
712 description: OK
713 content:
714 application/json:
715 schema:
716 $ref: '#/components/schemas/Customer'
717 components:
718 schemas:
719 Customer:
720 type: object
721 x-resourceId: customer
722 properties:
723 id:
724 type: string
725 "})
726 .unwrap();
727
728 let spec = IrSpec::from_doc(&doc).unwrap();
729 let ir_graph = IrGraph::new(&spec);
730 let graph = CodegenGraph::new(ir_graph);
731
732 let customer = graph.schemas().find(|s| s.name() == "Customer").unwrap();
733 let cfg = CfgFeature::for_schema_type(&customer);
734
735 let actual: syn::Attribute = parse_quote!(#cfg);
736 let expected: syn::Attribute = parse_quote!(
737 #[cfg(all(feature = "customer", any(feature = "billing", feature = "orders")))]
738 );
739 assert_eq!(actual, expected);
740 }
741
742 #[test]
743 fn test_for_schema_type_with_own_used_by_and_unnamed_deps() {
744 let doc = Document::from_yaml(indoc::indoc! {"
748 openapi: 3.0.0
749 info:
750 title: Test
751 version: 1.0.0
752 paths:
753 /billing:
754 get:
755 operationId: getBilling
756 x-resource-name: billing
757 responses:
758 '200':
759 description: OK
760 content:
761 application/json:
762 schema:
763 $ref: '#/components/schemas/Customer'
764 components:
765 schemas:
766 Customer:
767 type: object
768 x-resourceId: customer
769 properties:
770 address:
771 $ref: '#/components/schemas/Address'
772 Address:
773 type: object
774 properties:
775 street:
776 type: string
777 "})
778 .unwrap();
779
780 let spec = IrSpec::from_doc(&doc).unwrap();
781 let ir_graph = IrGraph::new(&spec);
782 let graph = CodegenGraph::new(ir_graph);
783
784 let customer = graph.schemas().find(|s| s.name() == "Customer").unwrap();
786 let cfg = CfgFeature::for_schema_type(&customer);
787 let actual: syn::Attribute = parse_quote!(#cfg);
788 let expected: syn::Attribute =
789 parse_quote!(#[cfg(all(feature = "billing", feature = "customer"))]);
790 assert_eq!(actual, expected);
791
792 let address = graph.schemas().find(|s| s.name() == "Address").unwrap();
795 let cfg = CfgFeature::for_schema_type(&address);
796 let actual: syn::Attribute = parse_quote!(#cfg);
797 let expected: syn::Attribute = parse_quote!(#[cfg(feature = "billing")]);
798 assert_eq!(actual, expected);
799 }
800
801 #[test]
804 fn test_for_schema_type_unnamed_no_operations() {
805 let doc = Document::from_yaml(indoc::indoc! {"
808 openapi: 3.0.0
809 info:
810 title: Test
811 version: 1.0.0
812 components:
813 schemas:
814 Simple:
815 type: object
816 properties:
817 id:
818 type: string
819 Customer:
820 type: object
821 x-resourceId: customer
822 properties:
823 name:
824 type: string
825 "})
826 .unwrap();
827
828 let spec = IrSpec::from_doc(&doc).unwrap();
829 let ir_graph = IrGraph::new(&spec);
830 let graph = CodegenGraph::new(ir_graph);
831
832 let simple = graph.schemas().find(|s| s.name() == "Simple").unwrap();
833 let cfg = CfgFeature::for_schema_type(&simple);
834
835 assert_eq!(cfg, None);
838 }
839
840 #[test]
843 fn test_for_schema_type_cycle_with_mixed_resources() {
844 let doc = Document::from_yaml(indoc::indoc! {"
848 openapi: 3.0.0
849 info:
850 title: Test
851 version: 1.0.0
852 components:
853 schemas:
854 A:
855 type: object
856 x-resourceId: a
857 properties:
858 b:
859 $ref: '#/components/schemas/B'
860 B:
861 type: object
862 properties:
863 c:
864 $ref: '#/components/schemas/C'
865 C:
866 type: object
867 x-resourceId: c
868 properties:
869 a:
870 $ref: '#/components/schemas/A'
871 "})
872 .unwrap();
873
874 let spec = IrSpec::from_doc(&doc).unwrap();
875 let ir_graph = IrGraph::new(&spec);
876 let graph = CodegenGraph::new(ir_graph);
877
878 let a = graph.schemas().find(|s| s.name() == "A").unwrap();
881 let cfg = CfgFeature::for_schema_type(&a);
882 assert_eq!(cfg, None);
883
884 let b = graph.schemas().find(|s| s.name() == "B").unwrap();
885 let cfg = CfgFeature::for_schema_type(&b);
886 assert_eq!(cfg, None);
887
888 let c = graph.schemas().find(|s| s.name() == "C").unwrap();
889 let cfg = CfgFeature::for_schema_type(&c);
890 assert_eq!(cfg, None);
891 }
892
893 #[test]
894 fn test_for_schema_type_cycle_with_all_named_resources() {
895 let doc = Document::from_yaml(indoc::indoc! {"
899 openapi: 3.0.0
900 info:
901 title: Test
902 version: 1.0.0
903 components:
904 schemas:
905 A:
906 type: object
907 x-resourceId: a
908 properties:
909 b:
910 $ref: '#/components/schemas/B'
911 B:
912 type: object
913 x-resourceId: b
914 properties:
915 c:
916 $ref: '#/components/schemas/C'
917 C:
918 type: object
919 x-resourceId: c
920 properties:
921 a:
922 $ref: '#/components/schemas/A'
923 "})
924 .unwrap();
925
926 let spec = IrSpec::from_doc(&doc).unwrap();
927 let ir_graph = IrGraph::new(&spec);
928 let graph = CodegenGraph::new(ir_graph);
929
930 let a = graph.schemas().find(|s| s.name() == "A").unwrap();
933 let cfg = CfgFeature::for_schema_type(&a);
934 let actual: syn::Attribute = parse_quote!(#cfg);
935 let expected: syn::Attribute = parse_quote!(#[cfg(feature = "a")]);
936 assert_eq!(actual, expected);
937
938 let b = graph.schemas().find(|s| s.name() == "B").unwrap();
939 let cfg = CfgFeature::for_schema_type(&b);
940 let actual: syn::Attribute = parse_quote!(#cfg);
941 let expected: syn::Attribute = parse_quote!(#[cfg(feature = "b")]);
942 assert_eq!(actual, expected);
943
944 let c = graph.schemas().find(|s| s.name() == "C").unwrap();
945 let cfg = CfgFeature::for_schema_type(&c);
946 let actual: syn::Attribute = parse_quote!(#cfg);
947 let expected: syn::Attribute = parse_quote!(#[cfg(feature = "c")]);
948 assert_eq!(actual, expected);
949 }
950
951 #[test]
954 fn test_for_inline_returns_empty_when_no_named_resources() {
955 let doc = Document::from_yaml(indoc::indoc! {"
957 openapi: 3.0.0
958 info:
959 title: Test API
960 version: 1.0.0
961 paths:
962 /items:
963 get:
964 operationId: getItems
965 parameters:
966 - name: filter
967 in: query
968 schema:
969 type: object
970 properties:
971 status:
972 type: string
973 responses:
974 '200':
975 description: OK
976 "})
977 .unwrap();
978
979 let spec = IrSpec::from_doc(&doc).unwrap();
980 let ir_graph = IrGraph::new(&spec);
981 let graph = CodegenGraph::new(ir_graph);
982
983 let ops = graph.operations().collect_vec();
984 let inlines = ops.iter().flat_map(|op| op.inlines()).collect_vec();
985 assert!(!inlines.is_empty());
986
987 let cfg = CfgFeature::for_inline_type(&inlines[0]);
989 assert_eq!(cfg, None);
990 }
991
992 #[test]
995 fn test_for_resource_module_with_named_feature() {
996 let cfg = CfgFeature::for_resource_module(&CargoFeature::from_name("pets"));
997
998 let actual: syn::Attribute = parse_quote!(#cfg);
999 let expected: syn::Attribute = parse_quote!(#[cfg(feature = "pets")]);
1000 assert_eq!(actual, expected);
1001 }
1002
1003 #[test]
1004 fn test_for_resource_module_skips_default_feature() {
1005 let cfg = CfgFeature::for_resource_module(&CargoFeature::Default);
1009 assert_eq!(cfg, None);
1010 }
1011
1012 #[test]
1015 fn test_for_operation_reduces_transitive_chain() {
1016 let doc = Document::from_yaml(indoc::indoc! {"
1019 openapi: 3.0.0
1020 info:
1021 title: Test
1022 version: 1.0.0
1023 paths:
1024 /things:
1025 get:
1026 operationId: getThings
1027 responses:
1028 '200':
1029 description: OK
1030 content:
1031 application/json:
1032 schema:
1033 $ref: '#/components/schemas/A'
1034 components:
1035 schemas:
1036 A:
1037 type: object
1038 x-resourceId: a
1039 properties:
1040 b:
1041 $ref: '#/components/schemas/B'
1042 B:
1043 type: object
1044 x-resourceId: b
1045 properties:
1046 c:
1047 $ref: '#/components/schemas/C'
1048 C:
1049 type: object
1050 x-resourceId: c
1051 properties:
1052 value:
1053 type: string
1054 "})
1055 .unwrap();
1056
1057 let spec = IrSpec::from_doc(&doc).unwrap();
1058 let ir_graph = IrGraph::new(&spec);
1059 let graph = CodegenGraph::new(ir_graph);
1060
1061 let op = graph.operations().find(|o| o.id() == "getThings").unwrap();
1062 let cfg = CfgFeature::for_operation(&op);
1063
1064 let actual: syn::Attribute = parse_quote!(#cfg);
1065 let expected: syn::Attribute = parse_quote!(#[cfg(feature = "a")]);
1066 assert_eq!(actual, expected);
1067 }
1068
1069 #[test]
1070 fn test_for_operation_reduces_cycle() {
1071 let doc = Document::from_yaml(indoc::indoc! {"
1075 openapi: 3.0.0
1076 info:
1077 title: Test
1078 version: 1.0.0
1079 paths:
1080 /things:
1081 get:
1082 operationId: getThings
1083 responses:
1084 '200':
1085 description: OK
1086 content:
1087 application/json:
1088 schema:
1089 $ref: '#/components/schemas/A'
1090 components:
1091 schemas:
1092 A:
1093 type: object
1094 x-resourceId: a
1095 properties:
1096 b:
1097 $ref: '#/components/schemas/B'
1098 B:
1099 type: object
1100 x-resourceId: b
1101 properties:
1102 c:
1103 $ref: '#/components/schemas/C'
1104 C:
1105 type: object
1106 x-resourceId: c
1107 properties:
1108 a:
1109 $ref: '#/components/schemas/A'
1110 "})
1111 .unwrap();
1112
1113 let spec = IrSpec::from_doc(&doc).unwrap();
1114 let ir_graph = IrGraph::new(&spec);
1115 let graph = CodegenGraph::new(ir_graph);
1116
1117 let op = graph.operations().find(|o| o.id() == "getThings").unwrap();
1118 let cfg = CfgFeature::for_operation(&op);
1119
1120 let actual: syn::Attribute = parse_quote!(#cfg);
1122 let expected: syn::Attribute = parse_quote!(#[cfg(feature = "a")]);
1123 assert_eq!(actual, expected);
1124 }
1125
1126 #[test]
1127 fn test_for_operation_keeps_independent_features() {
1128 let doc = Document::from_yaml(indoc::indoc! {"
1131 openapi: 3.0.0
1132 info:
1133 title: Test
1134 version: 1.0.0
1135 paths:
1136 /things:
1137 get:
1138 operationId: getThings
1139 responses:
1140 '200':
1141 description: OK
1142 content:
1143 application/json:
1144 schema:
1145 type: object
1146 properties:
1147 a:
1148 $ref: '#/components/schemas/A'
1149 b:
1150 $ref: '#/components/schemas/B'
1151 components:
1152 schemas:
1153 A:
1154 type: object
1155 x-resourceId: a
1156 properties:
1157 value:
1158 type: string
1159 B:
1160 type: object
1161 x-resourceId: b
1162 properties:
1163 value:
1164 type: string
1165 "})
1166 .unwrap();
1167
1168 let spec = IrSpec::from_doc(&doc).unwrap();
1169 let ir_graph = IrGraph::new(&spec);
1170 let graph = CodegenGraph::new(ir_graph);
1171
1172 let op = graph.operations().find(|o| o.id() == "getThings").unwrap();
1173 let cfg = CfgFeature::for_operation(&op);
1174
1175 let actual: syn::Attribute = parse_quote!(#cfg);
1176 let expected: syn::Attribute = parse_quote!(#[cfg(all(feature = "a", feature = "b"))]);
1177 assert_eq!(actual, expected);
1178 }
1179
1180 #[test]
1181 fn test_for_operation_reduces_partial_deps() {
1182 let doc = Document::from_yaml(indoc::indoc! {"
1185 openapi: 3.0.0
1186 info:
1187 title: Test
1188 version: 1.0.0
1189 paths:
1190 /things:
1191 get:
1192 operationId: getThings
1193 responses:
1194 '200':
1195 description: OK
1196 content:
1197 application/json:
1198 schema:
1199 type: object
1200 properties:
1201 a:
1202 $ref: '#/components/schemas/A'
1203 c:
1204 $ref: '#/components/schemas/C'
1205 components:
1206 schemas:
1207 A:
1208 type: object
1209 x-resourceId: a
1210 properties:
1211 b:
1212 $ref: '#/components/schemas/B'
1213 B:
1214 type: object
1215 x-resourceId: b
1216 properties:
1217 value:
1218 type: string
1219 C:
1220 type: object
1221 x-resourceId: c
1222 properties:
1223 value:
1224 type: string
1225 "})
1226 .unwrap();
1227
1228 let spec = IrSpec::from_doc(&doc).unwrap();
1229 let ir_graph = IrGraph::new(&spec);
1230 let graph = CodegenGraph::new(ir_graph);
1231
1232 let op = graph.operations().find(|o| o.id() == "getThings").unwrap();
1233 let cfg = CfgFeature::for_operation(&op);
1234
1235 let actual: syn::Attribute = parse_quote!(#cfg);
1236 let expected: syn::Attribute = parse_quote!(#[cfg(all(feature = "a", feature = "c"))]);
1237 assert_eq!(actual, expected);
1238 }
1239
1240 #[test]
1241 fn test_for_operation_reduces_diamond_deps() {
1242 let doc = Document::from_yaml(indoc::indoc! {"
1245 openapi: 3.0.0
1246 info:
1247 title: Test
1248 version: 1.0.0
1249 paths:
1250 /things:
1251 get:
1252 operationId: getThings
1253 responses:
1254 '200':
1255 description: OK
1256 content:
1257 application/json:
1258 schema:
1259 $ref: '#/components/schemas/A'
1260 components:
1261 schemas:
1262 A:
1263 type: object
1264 x-resourceId: a
1265 properties:
1266 b:
1267 $ref: '#/components/schemas/B'
1268 c:
1269 $ref: '#/components/schemas/C'
1270 B:
1271 type: object
1272 x-resourceId: b
1273 properties:
1274 d:
1275 $ref: '#/components/schemas/D'
1276 C:
1277 type: object
1278 x-resourceId: c
1279 properties:
1280 d:
1281 $ref: '#/components/schemas/D'
1282 D:
1283 type: object
1284 x-resourceId: d
1285 properties:
1286 value:
1287 type: string
1288 "})
1289 .unwrap();
1290
1291 let spec = IrSpec::from_doc(&doc).unwrap();
1292 let ir_graph = IrGraph::new(&spec);
1293 let graph = CodegenGraph::new(ir_graph);
1294
1295 let op = graph.operations().find(|o| o.id() == "getThings").unwrap();
1296 let cfg = CfgFeature::for_operation(&op);
1297
1298 let actual: syn::Attribute = parse_quote!(#cfg);
1300 let expected: syn::Attribute = parse_quote!(#[cfg(feature = "a")]);
1301 assert_eq!(actual, expected);
1302 }
1303
1304 #[test]
1305 fn test_for_operation_with_no_types() {
1306 let doc = Document::from_yaml(indoc::indoc! {"
1308 openapi: 3.0.0
1309 info:
1310 title: Test
1311 version: 1.0.0
1312 paths:
1313 /health:
1314 get:
1315 operationId: healthCheck
1316 responses:
1317 '200':
1318 description: OK
1319 "})
1320 .unwrap();
1321
1322 let spec = IrSpec::from_doc(&doc).unwrap();
1323 let ir_graph = IrGraph::new(&spec);
1324 let graph = CodegenGraph::new(ir_graph);
1325
1326 let op = graph
1327 .operations()
1328 .find(|o| o.id() == "healthCheck")
1329 .unwrap();
1330 let cfg = CfgFeature::for_operation(&op);
1331
1332 assert_eq!(cfg, None);
1334 }
1335
1336 #[test]
1337 fn test_for_inline_type_reduces_transitive_features() {
1338 let doc = Document::from_yaml(indoc::indoc! {"
1341 openapi: 3.0.0
1342 info:
1343 title: Test
1344 version: 1.0.0
1345 paths:
1346 /things:
1347 get:
1348 operationId: getThings
1349 responses:
1350 '200':
1351 description: OK
1352 content:
1353 application/json:
1354 schema:
1355 type: object
1356 properties:
1357 a:
1358 $ref: '#/components/schemas/A'
1359 components:
1360 schemas:
1361 A:
1362 type: object
1363 x-resourceId: a
1364 properties:
1365 b:
1366 $ref: '#/components/schemas/B'
1367 B:
1368 type: object
1369 x-resourceId: b
1370 properties:
1371 c:
1372 $ref: '#/components/schemas/C'
1373 C:
1374 type: object
1375 x-resourceId: c
1376 properties:
1377 value:
1378 type: string
1379 "})
1380 .unwrap();
1381
1382 let spec = IrSpec::from_doc(&doc).unwrap();
1383 let ir_graph = IrGraph::new(&spec);
1384 let graph = CodegenGraph::new(ir_graph);
1385
1386 let ops = graph.operations().collect_vec();
1387 let inlines = ops.iter().flat_map(|op| op.inlines()).collect_vec();
1388 assert!(!inlines.is_empty());
1389
1390 let cfg = CfgFeature::for_inline_type(&inlines[0]);
1391
1392 let actual: syn::Attribute = parse_quote!(#cfg);
1393 let expected: syn::Attribute = parse_quote!(#[cfg(feature = "a")]);
1394 assert_eq!(actual, expected);
1395 }
1396}