1use std::collections::BTreeSet;
25
26use itertools::Itertools;
27use ploidy_core::ir::{InlineTypeView, OperationView, SchemaTypeView, 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: &SchemaTypeView<'_>) -> Option<Self> {
55 let has_ungated_root_dependent = view
61 .dependents()
62 .filter_map(|v| v.into_schema().ok())
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: &InlineTypeView<'_>) -> Option<Self> {
92 let has_ungated_root_dependent = view
95 .dependents()
96 .filter_map(|v| v.into_schema().ok())
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(|v| v.into_schema().ok())
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: &OperationView<'_>) -> Option<Self> {
126 let pairs = view
129 .dependencies()
130 .filter_map(|v| v.into_schema().ok())
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, SchemaTypeView<'_>)],
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 arena::Arena,
275 ir::{RawGraph, Spec},
276 parse::Document,
277 };
278 use pretty_assertions::assert_eq;
279 use syn::parse_quote;
280
281 use crate::graph::CodegenGraph;
282
283 #[test]
286 fn test_single_feature() {
287 let cfg = CfgFeature::Single(CargoFeature::from_name("pets"));
288
289 let actual: syn::Attribute = parse_quote!(#cfg);
290 let expected: syn::Attribute = parse_quote!(#[cfg(feature = "pets")]);
291 assert_eq!(actual, expected);
292 }
293
294 #[test]
295 fn test_any_of_features() {
296 let cfg = CfgFeature::AnyOf(BTreeSet::from_iter([
297 CargoFeature::from_name("cats"),
298 CargoFeature::from_name("dogs"),
299 CargoFeature::from_name("aardvarks"),
300 ]));
301
302 let actual: syn::Attribute = parse_quote!(#cfg);
303 let expected: syn::Attribute =
304 parse_quote!(#[cfg(any(feature = "aardvarks", feature = "cats", feature = "dogs"))]);
305 assert_eq!(actual, expected);
306 }
307
308 #[test]
309 fn test_all_of_features() {
310 let cfg = CfgFeature::AllOf(BTreeSet::from_iter([
311 CargoFeature::from_name("cats"),
312 CargoFeature::from_name("dogs"),
313 CargoFeature::from_name("aardvarks"),
314 ]));
315
316 let actual: syn::Attribute = parse_quote!(#cfg);
317 let expected: syn::Attribute =
318 parse_quote!(#[cfg(all(feature = "aardvarks", feature = "cats", feature = "dogs"))]);
319 assert_eq!(actual, expected);
320 }
321
322 #[test]
323 fn test_own_and_used_by_feature() {
324 let cfg = CfgFeature::OwnAndUsedBy {
325 own: CargoFeature::from_name("own"),
326 used_by: BTreeSet::from_iter([
327 CargoFeature::from_name("a"),
328 CargoFeature::from_name("b"),
329 ]),
330 };
331
332 let actual: syn::Attribute = parse_quote!(#cfg);
333 let expected: syn::Attribute =
334 parse_quote!(#[cfg(all(feature = "own", any(feature = "a", feature = "b")))]);
335 assert_eq!(actual, expected);
336 }
337
338 #[test]
339 fn test_any_of_simplifies_single_feature() {
340 let cfg = CfgFeature::any_of(BTreeSet::from_iter([CargoFeature::from_name("pets")]));
341
342 let actual: syn::Attribute = parse_quote!(#cfg);
343 let expected: syn::Attribute = parse_quote!(#[cfg(feature = "pets")]);
344 assert_eq!(actual, expected);
345 }
346
347 #[test]
348 fn test_all_of_simplifies_single_feature() {
349 let cfg = CfgFeature::all_of(BTreeSet::from_iter([CargoFeature::from_name("pets")]));
350
351 let actual: syn::Attribute = parse_quote!(#cfg);
352 let expected: syn::Attribute = parse_quote!(#[cfg(feature = "pets")]);
353 assert_eq!(actual, expected);
354 }
355
356 #[test]
357 fn test_any_of_returns_none_for_empty() {
358 let cfg = CfgFeature::any_of(BTreeSet::new());
359 assert_eq!(cfg, None);
360 }
361
362 #[test]
363 fn test_any_of_returns_none_when_contains_default() {
364 let cfg = CfgFeature::any_of(BTreeSet::from_iter([
366 CargoFeature::from_name("customer"),
367 CargoFeature::Default,
368 CargoFeature::from_name("billing"),
369 ]));
370 assert_eq!(cfg, None);
371 }
372
373 #[test]
374 fn test_all_of_returns_none_for_empty() {
375 let cfg = CfgFeature::all_of(BTreeSet::new());
376 assert_eq!(cfg, None);
377 }
378
379 #[test]
380 fn test_all_of_returns_none_when_contains_default() {
381 let cfg = CfgFeature::all_of(BTreeSet::from_iter([
383 CargoFeature::from_name("customer"),
384 CargoFeature::Default,
385 CargoFeature::from_name("billing"),
386 ]));
387 assert_eq!(cfg, None);
388 }
389
390 #[test]
391 fn test_own_and_used_by_simplifies_single_used_by_to_all_of() {
392 let cfg = CfgFeature::own_and_used_by(
394 CargoFeature::from_name("own"),
395 BTreeSet::from_iter([CargoFeature::from_name("other")]),
396 );
397 assert_eq!(
398 cfg,
399 Some(CfgFeature::AllOf(BTreeSet::from_iter([
400 CargoFeature::from_name("other"),
401 CargoFeature::from_name("own"),
402 ])))
403 );
404 }
405
406 #[test]
407 fn test_own_and_used_by_simplifies_empty_to_single() {
408 let cfg = CfgFeature::own_and_used_by(CargoFeature::from_name("own"), BTreeSet::new());
410 assert_eq!(
411 cfg,
412 Some(CfgFeature::Single(CargoFeature::from_name("own")))
413 );
414 }
415
416 #[test]
417 fn test_own_and_used_by_returns_none_when_own_is_default() {
418 let cfg = CfgFeature::own_and_used_by(
420 CargoFeature::Default,
421 BTreeSet::from_iter([CargoFeature::from_name("other")]),
422 );
423 assert_eq!(cfg, None);
424 }
425
426 #[test]
427 fn test_own_and_used_by_returns_none_when_used_by_contains_default() {
428 let cfg = CfgFeature::own_and_used_by(
430 CargoFeature::from_name("own"),
431 BTreeSet::from_iter([CargoFeature::from_name("other"), CargoFeature::Default]),
432 );
433 assert_eq!(cfg, None);
434 }
435
436 #[test]
439 fn test_for_schema_type_returns_empty_when_no_named_resources() {
440 let doc = Document::from_yaml(indoc::indoc! {"
442 openapi: 3.0.0
443 info:
444 title: Test
445 version: 1.0.0
446 components:
447 schemas:
448 Customer:
449 type: object
450 properties:
451 id:
452 type: string
453 "})
454 .unwrap();
455
456 let arena = Arena::new();
457 let spec = Spec::from_doc(&arena, &doc).unwrap();
458 let graph = CodegenGraph::new(RawGraph::new(&arena, &spec).cook());
459
460 let customer = graph.schemas().find(|s| s.name() == "Customer").unwrap();
461
462 let cfg = CfgFeature::for_schema_type(&customer);
464 assert_eq!(cfg, None);
465 }
466
467 #[test]
470 fn test_for_schema_type_with_own_resource_no_deps() {
471 let doc = Document::from_yaml(indoc::indoc! {"
472 openapi: 3.0.0
473 info:
474 title: Test
475 version: 1.0.0
476 components:
477 schemas:
478 Customer:
479 type: object
480 x-resourceId: customer
481 properties:
482 id:
483 type: string
484 "})
485 .unwrap();
486
487 let arena = Arena::new();
488 let spec = Spec::from_doc(&arena, &doc).unwrap();
489 let graph = CodegenGraph::new(RawGraph::new(&arena, &spec).cook());
490
491 let customer = graph.schemas().find(|s| s.name() == "Customer").unwrap();
492 let cfg = CfgFeature::for_schema_type(&customer);
493
494 let actual: syn::Attribute = parse_quote!(#cfg);
495 let expected: syn::Attribute = parse_quote!(#[cfg(feature = "customer")]);
496 assert_eq!(actual, expected);
497 }
498
499 #[test]
500 fn test_for_schema_type_with_own_resource_and_unnamed_deps() {
501 let doc = Document::from_yaml(indoc::indoc! {"
504 openapi: 3.0.0
505 info:
506 title: Test
507 version: 1.0.0
508 components:
509 schemas:
510 Customer:
511 type: object
512 x-resourceId: customer
513 properties:
514 address:
515 $ref: '#/components/schemas/Address'
516 Address:
517 type: object
518 properties:
519 street:
520 type: string
521 "})
522 .unwrap();
523
524 let arena = Arena::new();
525 let spec = Spec::from_doc(&arena, &doc).unwrap();
526 let graph = CodegenGraph::new(RawGraph::new(&arena, &spec).cook());
527
528 let customer = graph.schemas().find(|s| s.name() == "Customer").unwrap();
530 let cfg = CfgFeature::for_schema_type(&customer);
531 let actual: syn::Attribute = parse_quote!(#cfg);
532 let expected: syn::Attribute = parse_quote!(#[cfg(feature = "customer")]);
533 assert_eq!(actual, expected);
534
535 let address = graph.schemas().find(|s| s.name() == "Address").unwrap();
537 let cfg = CfgFeature::for_schema_type(&address);
538 assert_eq!(cfg, None);
539 }
540
541 #[test]
544 fn test_for_schema_type_used_by_single_operation() {
545 let doc = Document::from_yaml(indoc::indoc! {"
546 openapi: 3.0.0
547 info:
548 title: Test
549 version: 1.0.0
550 paths:
551 /customers:
552 get:
553 operationId: listCustomers
554 x-resource-name: customer
555 responses:
556 '200':
557 description: OK
558 content:
559 application/json:
560 schema:
561 type: array
562 items:
563 $ref: '#/components/schemas/Customer'
564 components:
565 schemas:
566 Customer:
567 type: object
568 properties:
569 id:
570 type: string
571 "})
572 .unwrap();
573
574 let arena = Arena::new();
575 let spec = Spec::from_doc(&arena, &doc).unwrap();
576 let graph = CodegenGraph::new(RawGraph::new(&arena, &spec).cook());
577
578 let customer = graph.schemas().find(|s| s.name() == "Customer").unwrap();
579 let cfg = CfgFeature::for_schema_type(&customer);
580
581 let actual: syn::Attribute = parse_quote!(#cfg);
582 let expected: syn::Attribute = parse_quote!(#[cfg(feature = "customer")]);
583 assert_eq!(actual, expected);
584 }
585
586 #[test]
587 fn test_for_schema_type_used_by_multiple_operations() {
588 let doc = Document::from_yaml(indoc::indoc! {"
589 openapi: 3.0.0
590 info:
591 title: Test
592 version: 1.0.0
593 paths:
594 /customers:
595 get:
596 operationId: listCustomers
597 x-resource-name: customer
598 responses:
599 '200':
600 description: OK
601 content:
602 application/json:
603 schema:
604 type: array
605 items:
606 $ref: '#/components/schemas/Address'
607 /orders:
608 get:
609 operationId: listOrders
610 x-resource-name: orders
611 responses:
612 '200':
613 description: OK
614 content:
615 application/json:
616 schema:
617 type: array
618 items:
619 $ref: '#/components/schemas/Address'
620 components:
621 schemas:
622 Address:
623 type: object
624 properties:
625 street:
626 type: string
627 "})
628 .unwrap();
629
630 let arena = Arena::new();
631 let spec = Spec::from_doc(&arena, &doc).unwrap();
632 let graph = CodegenGraph::new(RawGraph::new(&arena, &spec).cook());
633
634 let address = graph.schemas().find(|s| s.name() == "Address").unwrap();
635 let cfg = CfgFeature::for_schema_type(&address);
636
637 let actual: syn::Attribute = parse_quote!(#cfg);
638 let expected: syn::Attribute =
639 parse_quote!(#[cfg(any(feature = "customer", feature = "orders"))]);
640 assert_eq!(actual, expected);
641 }
642
643 #[test]
646 fn test_for_schema_type_with_own_and_used_by() {
647 let doc = Document::from_yaml(indoc::indoc! {"
648 openapi: 3.0.0
649 info:
650 title: Test
651 version: 1.0.0
652 paths:
653 /billing:
654 get:
655 operationId: getBilling
656 x-resource-name: billing
657 responses:
658 '200':
659 description: OK
660 content:
661 application/json:
662 schema:
663 $ref: '#/components/schemas/Customer'
664 components:
665 schemas:
666 Customer:
667 type: object
668 x-resourceId: customer
669 properties:
670 id:
671 type: string
672 "})
673 .unwrap();
674
675 let arena = Arena::new();
676 let spec = Spec::from_doc(&arena, &doc).unwrap();
677 let graph = CodegenGraph::new(RawGraph::new(&arena, &spec).cook());
678
679 let customer = graph.schemas().find(|s| s.name() == "Customer").unwrap();
680 let cfg = CfgFeature::for_schema_type(&customer);
681
682 let actual: syn::Attribute = parse_quote!(#cfg);
683 let expected: syn::Attribute =
684 parse_quote!(#[cfg(all(feature = "billing", feature = "customer"))]);
685 assert_eq!(actual, expected);
686 }
687
688 #[test]
689 fn test_for_schema_type_with_own_and_multiple_used_by() {
690 let doc = Document::from_yaml(indoc::indoc! {"
691 openapi: 3.0.0
692 info:
693 title: Test
694 version: 1.0.0
695 paths:
696 /billing:
697 get:
698 operationId: getBilling
699 x-resource-name: billing
700 responses:
701 '200':
702 description: OK
703 content:
704 application/json:
705 schema:
706 $ref: '#/components/schemas/Customer'
707 /orders:
708 get:
709 operationId: getOrders
710 x-resource-name: orders
711 responses:
712 '200':
713 description: OK
714 content:
715 application/json:
716 schema:
717 $ref: '#/components/schemas/Customer'
718 components:
719 schemas:
720 Customer:
721 type: object
722 x-resourceId: customer
723 properties:
724 id:
725 type: string
726 "})
727 .unwrap();
728
729 let arena = Arena::new();
730 let spec = Spec::from_doc(&arena, &doc).unwrap();
731 let graph = CodegenGraph::new(RawGraph::new(&arena, &spec).cook());
732
733 let customer = graph.schemas().find(|s| s.name() == "Customer").unwrap();
734 let cfg = CfgFeature::for_schema_type(&customer);
735
736 let actual: syn::Attribute = parse_quote!(#cfg);
737 let expected: syn::Attribute = parse_quote!(
738 #[cfg(all(feature = "customer", any(feature = "billing", feature = "orders")))]
739 );
740 assert_eq!(actual, expected);
741 }
742
743 #[test]
744 fn test_for_schema_type_with_own_used_by_and_unnamed_deps() {
745 let doc = Document::from_yaml(indoc::indoc! {"
749 openapi: 3.0.0
750 info:
751 title: Test
752 version: 1.0.0
753 paths:
754 /billing:
755 get:
756 operationId: getBilling
757 x-resource-name: billing
758 responses:
759 '200':
760 description: OK
761 content:
762 application/json:
763 schema:
764 $ref: '#/components/schemas/Customer'
765 components:
766 schemas:
767 Customer:
768 type: object
769 x-resourceId: customer
770 properties:
771 address:
772 $ref: '#/components/schemas/Address'
773 Address:
774 type: object
775 properties:
776 street:
777 type: string
778 "})
779 .unwrap();
780
781 let arena = Arena::new();
782 let spec = Spec::from_doc(&arena, &doc).unwrap();
783 let graph = CodegenGraph::new(RawGraph::new(&arena, &spec).cook());
784
785 let customer = graph.schemas().find(|s| s.name() == "Customer").unwrap();
787 let cfg = CfgFeature::for_schema_type(&customer);
788 let actual: syn::Attribute = parse_quote!(#cfg);
789 let expected: syn::Attribute =
790 parse_quote!(#[cfg(all(feature = "billing", feature = "customer"))]);
791 assert_eq!(actual, expected);
792
793 let address = graph.schemas().find(|s| s.name() == "Address").unwrap();
796 let cfg = CfgFeature::for_schema_type(&address);
797 let actual: syn::Attribute = parse_quote!(#cfg);
798 let expected: syn::Attribute = parse_quote!(#[cfg(feature = "billing")]);
799 assert_eq!(actual, expected);
800 }
801
802 #[test]
805 fn test_for_schema_type_unnamed_no_operations() {
806 let doc = Document::from_yaml(indoc::indoc! {"
809 openapi: 3.0.0
810 info:
811 title: Test
812 version: 1.0.0
813 components:
814 schemas:
815 Simple:
816 type: object
817 properties:
818 id:
819 type: string
820 Customer:
821 type: object
822 x-resourceId: customer
823 properties:
824 name:
825 type: string
826 "})
827 .unwrap();
828
829 let arena = Arena::new();
830 let spec = Spec::from_doc(&arena, &doc).unwrap();
831 let graph = CodegenGraph::new(RawGraph::new(&arena, &spec).cook());
832
833 let simple = graph.schemas().find(|s| s.name() == "Simple").unwrap();
834 let cfg = CfgFeature::for_schema_type(&simple);
835
836 assert_eq!(cfg, None);
839 }
840
841 #[test]
844 fn test_for_schema_type_cycle_with_mixed_resources() {
845 let doc = Document::from_yaml(indoc::indoc! {"
849 openapi: 3.0.0
850 info:
851 title: Test
852 version: 1.0.0
853 components:
854 schemas:
855 A:
856 type: object
857 x-resourceId: a
858 properties:
859 b:
860 $ref: '#/components/schemas/B'
861 B:
862 type: object
863 properties:
864 c:
865 $ref: '#/components/schemas/C'
866 C:
867 type: object
868 x-resourceId: c
869 properties:
870 a:
871 $ref: '#/components/schemas/A'
872 "})
873 .unwrap();
874
875 let arena = Arena::new();
876 let spec = Spec::from_doc(&arena, &doc).unwrap();
877 let graph = CodegenGraph::new(RawGraph::new(&arena, &spec).cook());
878
879 let a = graph.schemas().find(|s| s.name() == "A").unwrap();
882 let cfg = CfgFeature::for_schema_type(&a);
883 assert_eq!(cfg, None);
884
885 let b = graph.schemas().find(|s| s.name() == "B").unwrap();
886 let cfg = CfgFeature::for_schema_type(&b);
887 assert_eq!(cfg, None);
888
889 let c = graph.schemas().find(|s| s.name() == "C").unwrap();
890 let cfg = CfgFeature::for_schema_type(&c);
891 assert_eq!(cfg, None);
892 }
893
894 #[test]
895 fn test_for_schema_type_cycle_with_all_named_resources() {
896 let doc = Document::from_yaml(indoc::indoc! {"
900 openapi: 3.0.0
901 info:
902 title: Test
903 version: 1.0.0
904 components:
905 schemas:
906 A:
907 type: object
908 x-resourceId: a
909 properties:
910 b:
911 $ref: '#/components/schemas/B'
912 B:
913 type: object
914 x-resourceId: b
915 properties:
916 c:
917 $ref: '#/components/schemas/C'
918 C:
919 type: object
920 x-resourceId: c
921 properties:
922 a:
923 $ref: '#/components/schemas/A'
924 "})
925 .unwrap();
926
927 let arena = Arena::new();
928 let spec = Spec::from_doc(&arena, &doc).unwrap();
929 let graph = CodegenGraph::new(RawGraph::new(&arena, &spec).cook());
930
931 let a = graph.schemas().find(|s| s.name() == "A").unwrap();
934 let cfg = CfgFeature::for_schema_type(&a);
935 let actual: syn::Attribute = parse_quote!(#cfg);
936 let expected: syn::Attribute = parse_quote!(#[cfg(feature = "a")]);
937 assert_eq!(actual, expected);
938
939 let b = graph.schemas().find(|s| s.name() == "B").unwrap();
940 let cfg = CfgFeature::for_schema_type(&b);
941 let actual: syn::Attribute = parse_quote!(#cfg);
942 let expected: syn::Attribute = parse_quote!(#[cfg(feature = "b")]);
943 assert_eq!(actual, expected);
944
945 let c = graph.schemas().find(|s| s.name() == "C").unwrap();
946 let cfg = CfgFeature::for_schema_type(&c);
947 let actual: syn::Attribute = parse_quote!(#cfg);
948 let expected: syn::Attribute = parse_quote!(#[cfg(feature = "c")]);
949 assert_eq!(actual, expected);
950 }
951
952 #[test]
955 fn test_for_inline_returns_empty_when_no_named_resources() {
956 let doc = Document::from_yaml(indoc::indoc! {"
958 openapi: 3.0.0
959 info:
960 title: Test API
961 version: 1.0.0
962 paths:
963 /items:
964 get:
965 operationId: getItems
966 parameters:
967 - name: filter
968 in: query
969 schema:
970 type: object
971 properties:
972 status:
973 type: string
974 responses:
975 '200':
976 description: OK
977 "})
978 .unwrap();
979
980 let arena = Arena::new();
981 let spec = Spec::from_doc(&arena, &doc).unwrap();
982 let graph = CodegenGraph::new(RawGraph::new(&arena, &spec).cook());
983
984 let ops = graph.operations().collect_vec();
985 let inlines = ops.iter().flat_map(|op| op.inlines()).collect_vec();
986 assert!(!inlines.is_empty());
987
988 let cfg = CfgFeature::for_inline_type(&inlines[0]);
990 assert_eq!(cfg, None);
991 }
992
993 #[test]
996 fn test_for_resource_module_with_named_feature() {
997 let cfg = CfgFeature::for_resource_module(&CargoFeature::from_name("pets"));
998
999 let actual: syn::Attribute = parse_quote!(#cfg);
1000 let expected: syn::Attribute = parse_quote!(#[cfg(feature = "pets")]);
1001 assert_eq!(actual, expected);
1002 }
1003
1004 #[test]
1005 fn test_for_resource_module_skips_default_feature() {
1006 let cfg = CfgFeature::for_resource_module(&CargoFeature::Default);
1010 assert_eq!(cfg, None);
1011 }
1012
1013 #[test]
1016 fn test_for_operation_reduces_transitive_chain() {
1017 let doc = Document::from_yaml(indoc::indoc! {"
1020 openapi: 3.0.0
1021 info:
1022 title: Test
1023 version: 1.0.0
1024 paths:
1025 /things:
1026 get:
1027 operationId: getThings
1028 responses:
1029 '200':
1030 description: OK
1031 content:
1032 application/json:
1033 schema:
1034 $ref: '#/components/schemas/A'
1035 components:
1036 schemas:
1037 A:
1038 type: object
1039 x-resourceId: a
1040 properties:
1041 b:
1042 $ref: '#/components/schemas/B'
1043 B:
1044 type: object
1045 x-resourceId: b
1046 properties:
1047 c:
1048 $ref: '#/components/schemas/C'
1049 C:
1050 type: object
1051 x-resourceId: c
1052 properties:
1053 value:
1054 type: string
1055 "})
1056 .unwrap();
1057
1058 let arena = Arena::new();
1059 let spec = Spec::from_doc(&arena, &doc).unwrap();
1060 let graph = CodegenGraph::new(RawGraph::new(&arena, &spec).cook());
1061
1062 let op = graph.operations().find(|o| o.id() == "getThings").unwrap();
1063 let cfg = CfgFeature::for_operation(&op);
1064
1065 let actual: syn::Attribute = parse_quote!(#cfg);
1066 let expected: syn::Attribute = parse_quote!(#[cfg(feature = "a")]);
1067 assert_eq!(actual, expected);
1068 }
1069
1070 #[test]
1071 fn test_for_operation_reduces_cycle() {
1072 let doc = Document::from_yaml(indoc::indoc! {"
1076 openapi: 3.0.0
1077 info:
1078 title: Test
1079 version: 1.0.0
1080 paths:
1081 /things:
1082 get:
1083 operationId: getThings
1084 responses:
1085 '200':
1086 description: OK
1087 content:
1088 application/json:
1089 schema:
1090 $ref: '#/components/schemas/A'
1091 components:
1092 schemas:
1093 A:
1094 type: object
1095 x-resourceId: a
1096 properties:
1097 b:
1098 $ref: '#/components/schemas/B'
1099 B:
1100 type: object
1101 x-resourceId: b
1102 properties:
1103 c:
1104 $ref: '#/components/schemas/C'
1105 C:
1106 type: object
1107 x-resourceId: c
1108 properties:
1109 a:
1110 $ref: '#/components/schemas/A'
1111 "})
1112 .unwrap();
1113
1114 let arena = Arena::new();
1115 let spec = Spec::from_doc(&arena, &doc).unwrap();
1116 let graph = CodegenGraph::new(RawGraph::new(&arena, &spec).cook());
1117
1118 let op = graph.operations().find(|o| o.id() == "getThings").unwrap();
1119 let cfg = CfgFeature::for_operation(&op);
1120
1121 let actual: syn::Attribute = parse_quote!(#cfg);
1123 let expected: syn::Attribute = parse_quote!(#[cfg(feature = "a")]);
1124 assert_eq!(actual, expected);
1125 }
1126
1127 #[test]
1128 fn test_for_operation_keeps_independent_features() {
1129 let doc = Document::from_yaml(indoc::indoc! {"
1132 openapi: 3.0.0
1133 info:
1134 title: Test
1135 version: 1.0.0
1136 paths:
1137 /things:
1138 get:
1139 operationId: getThings
1140 responses:
1141 '200':
1142 description: OK
1143 content:
1144 application/json:
1145 schema:
1146 type: object
1147 properties:
1148 a:
1149 $ref: '#/components/schemas/A'
1150 b:
1151 $ref: '#/components/schemas/B'
1152 components:
1153 schemas:
1154 A:
1155 type: object
1156 x-resourceId: a
1157 properties:
1158 value:
1159 type: string
1160 B:
1161 type: object
1162 x-resourceId: b
1163 properties:
1164 value:
1165 type: string
1166 "})
1167 .unwrap();
1168
1169 let arena = Arena::new();
1170 let spec = Spec::from_doc(&arena, &doc).unwrap();
1171 let graph = CodegenGraph::new(RawGraph::new(&arena, &spec).cook());
1172
1173 let op = graph.operations().find(|o| o.id() == "getThings").unwrap();
1174 let cfg = CfgFeature::for_operation(&op);
1175
1176 let actual: syn::Attribute = parse_quote!(#cfg);
1177 let expected: syn::Attribute = parse_quote!(#[cfg(all(feature = "a", feature = "b"))]);
1178 assert_eq!(actual, expected);
1179 }
1180
1181 #[test]
1182 fn test_for_operation_reduces_partial_deps() {
1183 let doc = Document::from_yaml(indoc::indoc! {"
1186 openapi: 3.0.0
1187 info:
1188 title: Test
1189 version: 1.0.0
1190 paths:
1191 /things:
1192 get:
1193 operationId: getThings
1194 responses:
1195 '200':
1196 description: OK
1197 content:
1198 application/json:
1199 schema:
1200 type: object
1201 properties:
1202 a:
1203 $ref: '#/components/schemas/A'
1204 c:
1205 $ref: '#/components/schemas/C'
1206 components:
1207 schemas:
1208 A:
1209 type: object
1210 x-resourceId: a
1211 properties:
1212 b:
1213 $ref: '#/components/schemas/B'
1214 B:
1215 type: object
1216 x-resourceId: b
1217 properties:
1218 value:
1219 type: string
1220 C:
1221 type: object
1222 x-resourceId: c
1223 properties:
1224 value:
1225 type: string
1226 "})
1227 .unwrap();
1228
1229 let arena = Arena::new();
1230 let spec = Spec::from_doc(&arena, &doc).unwrap();
1231 let graph = CodegenGraph::new(RawGraph::new(&arena, &spec).cook());
1232
1233 let op = graph.operations().find(|o| o.id() == "getThings").unwrap();
1234 let cfg = CfgFeature::for_operation(&op);
1235
1236 let actual: syn::Attribute = parse_quote!(#cfg);
1237 let expected: syn::Attribute = parse_quote!(#[cfg(all(feature = "a", feature = "c"))]);
1238 assert_eq!(actual, expected);
1239 }
1240
1241 #[test]
1242 fn test_for_operation_reduces_diamond_deps() {
1243 let doc = Document::from_yaml(indoc::indoc! {"
1246 openapi: 3.0.0
1247 info:
1248 title: Test
1249 version: 1.0.0
1250 paths:
1251 /things:
1252 get:
1253 operationId: getThings
1254 responses:
1255 '200':
1256 description: OK
1257 content:
1258 application/json:
1259 schema:
1260 $ref: '#/components/schemas/A'
1261 components:
1262 schemas:
1263 A:
1264 type: object
1265 x-resourceId: a
1266 properties:
1267 b:
1268 $ref: '#/components/schemas/B'
1269 c:
1270 $ref: '#/components/schemas/C'
1271 B:
1272 type: object
1273 x-resourceId: b
1274 properties:
1275 d:
1276 $ref: '#/components/schemas/D'
1277 C:
1278 type: object
1279 x-resourceId: c
1280 properties:
1281 d:
1282 $ref: '#/components/schemas/D'
1283 D:
1284 type: object
1285 x-resourceId: d
1286 properties:
1287 value:
1288 type: string
1289 "})
1290 .unwrap();
1291
1292 let arena = Arena::new();
1293 let spec = Spec::from_doc(&arena, &doc).unwrap();
1294 let graph = CodegenGraph::new(RawGraph::new(&arena, &spec).cook());
1295
1296 let op = graph.operations().find(|o| o.id() == "getThings").unwrap();
1297 let cfg = CfgFeature::for_operation(&op);
1298
1299 let actual: syn::Attribute = parse_quote!(#cfg);
1301 let expected: syn::Attribute = parse_quote!(#[cfg(feature = "a")]);
1302 assert_eq!(actual, expected);
1303 }
1304
1305 #[test]
1306 fn test_for_operation_with_no_types() {
1307 let doc = Document::from_yaml(indoc::indoc! {"
1309 openapi: 3.0.0
1310 info:
1311 title: Test
1312 version: 1.0.0
1313 paths:
1314 /health:
1315 get:
1316 operationId: healthCheck
1317 responses:
1318 '200':
1319 description: OK
1320 "})
1321 .unwrap();
1322
1323 let arena = Arena::new();
1324 let spec = Spec::from_doc(&arena, &doc).unwrap();
1325 let graph = CodegenGraph::new(RawGraph::new(&arena, &spec).cook());
1326
1327 let op = graph
1328 .operations()
1329 .find(|o| o.id() == "healthCheck")
1330 .unwrap();
1331 let cfg = CfgFeature::for_operation(&op);
1332
1333 assert_eq!(cfg, None);
1335 }
1336
1337 #[test]
1338 fn test_for_inline_type_reduces_transitive_features() {
1339 let doc = Document::from_yaml(indoc::indoc! {"
1342 openapi: 3.0.0
1343 info:
1344 title: Test
1345 version: 1.0.0
1346 paths:
1347 /things:
1348 get:
1349 operationId: getThings
1350 responses:
1351 '200':
1352 description: OK
1353 content:
1354 application/json:
1355 schema:
1356 type: object
1357 properties:
1358 a:
1359 $ref: '#/components/schemas/A'
1360 components:
1361 schemas:
1362 A:
1363 type: object
1364 x-resourceId: a
1365 properties:
1366 b:
1367 $ref: '#/components/schemas/B'
1368 B:
1369 type: object
1370 x-resourceId: b
1371 properties:
1372 c:
1373 $ref: '#/components/schemas/C'
1374 C:
1375 type: object
1376 x-resourceId: c
1377 properties:
1378 value:
1379 type: string
1380 "})
1381 .unwrap();
1382
1383 let arena = Arena::new();
1384 let spec = Spec::from_doc(&arena, &doc).unwrap();
1385 let graph = CodegenGraph::new(RawGraph::new(&arena, &spec).cook());
1386
1387 let ops = graph.operations().collect_vec();
1388 let inlines = ops.iter().flat_map(|op| op.inlines()).collect_vec();
1389 assert!(!inlines.is_empty());
1390
1391 let cfg = CfgFeature::for_inline_type(&inlines[0]);
1392
1393 let actual: syn::Attribute = parse_quote!(#cfg);
1394 let expected: syn::Attribute = parse_quote!(#[cfg(feature = "a")]);
1395 assert_eq!(actual, expected);
1396 }
1397}