1use std::collections::BTreeSet;
25
26use ploidy_core::ir::{HasResource, InlineTypeView, OperationView, SchemaTypeView, View};
27use proc_macro2::TokenStream;
28use quote::{ToTokens, quote};
29
30use super::{
31 graph::CodegenGraph,
32 naming::{AsFeatureName, ResourceGroup, UniqueIdent},
33};
34
35#[derive(Clone, Debug, Eq, PartialEq)]
37pub enum CfgFeature<'a> {
38 Single(&'a UniqueIdent),
40 AnyOf(BTreeSet<&'a UniqueIdent>),
42 AllOf(BTreeSet<&'a UniqueIdent>),
44 OwnAndUsedBy {
48 own: &'a UniqueIdent,
49 used_by: BTreeSet<&'a UniqueIdent>,
50 },
51}
52
53impl<'a> CfgFeature<'a> {
54 pub fn for_schema_type(
57 graph: &CodegenGraph<'a>,
58 view: &SchemaTypeView<'_, 'a>,
59 ) -> Option<Self> {
60 if in_default_resource_group(graph, view)
64 || view
65 .dependents()
66 .filter_map(|ty| ty.into_schema().right())
67 .any(|schema| in_default_resource_group(graph, &schema))
68 {
69 return None;
70 }
71
72 let used_by_resources: BTreeSet<_> = view
74 .used_by()
75 .filter_map(|op| graph.resource_for(&op).name())
76 .collect();
77
78 match (graph.resource_for(view), used_by_resources.is_empty()) {
79 (ResourceGroup::Named(own), false) => {
81 Some(Self::own_and_used_by(own, used_by_resources))
82 }
83 (ResourceGroup::Named(own), true) => Some(Self::Single(own)),
85 (ResourceGroup::Default, false) => Self::any_of(used_by_resources),
88 (ResourceGroup::Default, true) => None,
90 }
91 }
92
93 pub fn for_inline_type(
95 graph: &CodegenGraph<'a>,
96 view: &InlineTypeView<'_, 'a>,
97 ) -> Option<Self> {
98 if view
101 .dependents()
102 .filter_map(|ty| ty.into_schema().right())
103 .any(|schema| in_default_resource_group(graph, &schema))
104 {
105 return None;
106 }
107
108 let used_by_resources: BTreeSet<_> = view
109 .used_by()
110 .filter_map(|op| {
111 use ResourceGroup::*;
112 match (graph.resource_for(view), graph.resource_for(&op)) {
113 (Default, Named(resource)) => Some(resource),
114 (Named(own), Named(resource)) if own != resource => Some(resource),
115 _ => None,
116 }
117 })
118 .collect();
119
120 if used_by_resources.is_empty() {
121 Self::for_transitive_dependencies(graph, view)
124 } else {
125 Self::any_of(used_by_resources)
127 }
128 }
129
130 pub fn for_operation(graph: &CodegenGraph<'a>, view: &OperationView<'_, 'a>) -> Option<Self> {
132 Self::for_transitive_dependencies(graph, view)
133 }
134
135 fn for_transitive_dependencies<'graph>(
142 graph: &'graph CodegenGraph<'a>,
143 view: &(impl HasResource<'a> + View<'graph, 'a>),
144 ) -> Option<Self> {
145 Self::all_of(
146 view.dependencies()
147 .filter_map(|ty| {
148 let schema = ty.into_schema().right()?;
149 let resource = graph.resource_for(&schema).name()?;
152 let implied = view
158 .dependencies()
159 .filter_map(|ty| ty.into_schema().right())
160 .any(|other_schema| {
161 let ResourceGroup::Named(other_resource) =
162 graph.resource_for(&other_schema)
163 else {
164 return false;
165 };
166 other_schema.depends_on(&schema)
167 && (!schema.depends_on(&other_schema) || other_resource < resource)
168 });
169 (!implied).then_some(resource)
170 })
171 .filter(|&resource| match graph.resource_for(view) {
172 ResourceGroup::Default => true,
173 ResourceGroup::Named(own) if own != resource => true,
174 _ => false,
175 })
176 .collect(),
177 )
178 }
179
180 fn any_of(mut features: BTreeSet<&'a UniqueIdent>) -> Option<Self> {
182 let first = features.pop_first()?;
183 Some(if features.is_empty() {
184 Self::Single(first)
186 } else {
187 features.insert(first);
188 Self::AnyOf(features)
189 })
190 }
191
192 fn all_of(mut features: BTreeSet<&'a UniqueIdent>) -> Option<Self> {
194 let first = features.pop_first()?;
195 Some(if features.is_empty() {
196 Self::Single(first)
198 } else {
199 features.insert(first);
200 Self::AllOf(features)
201 })
202 }
203
204 fn own_and_used_by(own: &'a UniqueIdent, mut used_by: BTreeSet<&'a UniqueIdent>) -> Self {
206 if used_by.contains(own) {
207 return Self::Single(own);
209 }
210 let Some(first) = used_by.pop_first() else {
211 return Self::Single(own);
213 };
214 if used_by.is_empty() {
215 Self::AllOf(BTreeSet::from_iter([own, first]))
217 } else {
218 used_by.insert(first);
220 Self::OwnAndUsedBy { own, used_by }
221 }
222 }
223}
224
225impl ToTokens for CfgFeature<'_> {
226 fn to_tokens(&self, tokens: &mut TokenStream) {
227 let predicate = match self {
228 Self::Single(resource) => {
229 let name = AsFeatureName(resource).to_string();
230 quote! { feature = #name }
231 }
232 Self::AnyOf(resources) => {
233 let predicates = resources.iter().map(|f| {
234 let name = AsFeatureName(f).to_string();
235 quote! { feature = #name }
236 });
237 quote! { any(#(#predicates),*) }
238 }
239 Self::AllOf(resources) => {
240 let predicates = resources.iter().map(|f| {
241 let name = AsFeatureName(f).to_string();
242 quote! { feature = #name }
243 });
244 quote! { all(#(#predicates),*) }
245 }
246 Self::OwnAndUsedBy { own, used_by } => {
247 let own_name = AsFeatureName(own).to_string();
248 let used_by_predicates = used_by.iter().map(|f| {
249 let name = AsFeatureName(f).to_string();
250 quote! { feature = #name }
251 });
252 quote! { all(feature = #own_name, any(#(#used_by_predicates),*)) }
253 }
254 };
255 tokens.extend(quote! { #[cfg(#predicate)] });
256 }
257}
258
259fn in_default_resource_group<'graph, 'a>(
260 graph: &'graph CodegenGraph<'a>,
261 view: &(impl View<'graph, 'a> + HasResource<'a>),
262) -> bool {
263 let mut used_by = view.used_by().map(|op| graph.resource_for(&op)).peekable();
264 graph.resource_for(view).is_default()
265 && (used_by.peek().is_none() || used_by.any(|resource| resource.is_default()))
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::{UniqueIdents, graph::CodegenGraph};
282
283 #[test]
286 fn test_single_feature() {
287 let arena = Arena::new();
288 let mut scope = UniqueIdents::new(&arena);
289 let cfg = CfgFeature::Single(scope.ident("pets"));
290
291 let actual: syn::Attribute = parse_quote!(#cfg);
292 let expected: syn::Attribute = parse_quote!(#[cfg(feature = "pets")]);
293 assert_eq!(actual, expected);
294 }
295
296 #[test]
297 fn test_any_of_features() {
298 let arena = Arena::new();
299 let mut scope = UniqueIdents::new(&arena);
300 let cfg = CfgFeature::AnyOf(BTreeSet::from_iter([
301 scope.ident("cats"),
302 scope.ident("dogs"),
303 scope.ident("aardvarks"),
304 ]));
305
306 let actual: syn::Attribute = parse_quote!(#cfg);
307 let expected: syn::Attribute =
308 parse_quote!(#[cfg(any(feature = "aardvarks", feature = "cats", feature = "dogs"))]);
309 assert_eq!(actual, expected);
310 }
311
312 #[test]
313 fn test_all_of_features() {
314 let arena = Arena::new();
315 let mut scope = UniqueIdents::new(&arena);
316 let cfg = CfgFeature::AllOf(BTreeSet::from_iter([
317 scope.ident("cats"),
318 scope.ident("dogs"),
319 scope.ident("aardvarks"),
320 ]));
321
322 let actual: syn::Attribute = parse_quote!(#cfg);
323 let expected: syn::Attribute =
324 parse_quote!(#[cfg(all(feature = "aardvarks", feature = "cats", feature = "dogs"))]);
325 assert_eq!(actual, expected);
326 }
327
328 #[test]
329 fn test_own_and_used_by_feature() {
330 let arena = Arena::new();
331 let mut scope = UniqueIdents::new(&arena);
332 let cfg = CfgFeature::OwnAndUsedBy {
333 own: scope.ident("own"),
334 used_by: BTreeSet::from_iter([scope.ident("a"), scope.ident("b")]),
335 };
336
337 let actual: syn::Attribute = parse_quote!(#cfg);
338 let expected: syn::Attribute =
339 parse_quote!(#[cfg(all(feature = "own", any(feature = "a", feature = "b")))]);
340 assert_eq!(actual, expected);
341 }
342
343 #[test]
344 fn test_any_of_simplifies_single_feature() {
345 let arena = Arena::new();
346 let mut scope = UniqueIdents::new(&arena);
347 let cfg = CfgFeature::any_of(BTreeSet::from_iter([scope.ident("pets")]));
348
349 let actual: syn::Attribute = parse_quote!(#cfg);
350 let expected: syn::Attribute = parse_quote!(#[cfg(feature = "pets")]);
351 assert_eq!(actual, expected);
352 }
353
354 #[test]
355 fn test_all_of_simplifies_single_feature() {
356 let arena = Arena::new();
357 let mut scope = UniqueIdents::new(&arena);
358 let cfg = CfgFeature::all_of(BTreeSet::from_iter([scope.ident("pets")]));
359
360 let actual: syn::Attribute = parse_quote!(#cfg);
361 let expected: syn::Attribute = parse_quote!(#[cfg(feature = "pets")]);
362 assert_eq!(actual, expected);
363 }
364
365 #[test]
366 fn test_any_of_returns_none_for_empty() {
367 let cfg = CfgFeature::any_of(BTreeSet::new());
368 assert_eq!(cfg, None);
369 }
370
371 #[test]
372 fn test_all_of_returns_none_for_empty() {
373 let cfg = CfgFeature::all_of(BTreeSet::new());
374 assert_eq!(cfg, None);
375 }
376
377 #[test]
378 fn test_own_and_used_by_simplifies_single_used_by_to_all_of() {
379 let arena = Arena::new();
380 let mut scope = UniqueIdents::new(&arena);
381 let own = scope.ident("own");
382 let other = scope.ident("other");
383 let cfg = CfgFeature::own_and_used_by(own, BTreeSet::from_iter([other]));
385 assert_eq!(cfg, CfgFeature::AllOf(BTreeSet::from_iter([other, own])),);
386 }
387
388 #[test]
389 fn test_own_and_used_by_simplifies_empty_to_single() {
390 let arena = Arena::new();
391 let mut scope = UniqueIdents::new(&arena);
392 let own = scope.ident("own");
393 let cfg = CfgFeature::own_and_used_by(own, BTreeSet::new());
395 assert_eq!(cfg, CfgFeature::Single(own));
396 }
397
398 #[test]
399 fn test_own_and_used_by_simplifies_own_used_by_to_single() {
400 let arena = Arena::new();
401 let mut scope = UniqueIdents::new(&arena);
402 let own = scope.ident("own");
403 let other = scope.ident("other");
404 let cfg = CfgFeature::own_and_used_by(own, BTreeSet::from_iter([own, other]));
405 assert_eq!(cfg, CfgFeature::Single(own));
406 }
407
408 #[test]
411 fn test_for_schema_type_returns_empty_when_no_named_resources() {
412 let doc = Document::from_yaml(indoc::indoc! {"
414 openapi: 3.0.0
415 info:
416 title: Test
417 version: 1.0.0
418 components:
419 schemas:
420 Customer:
421 type: object
422 properties:
423 id:
424 type: string
425 "})
426 .unwrap();
427
428 let arena = Arena::new();
429 let spec = Spec::from_doc(&arena, &doc).unwrap();
430 let graph = CodegenGraph::new(RawGraph::new(&arena, &spec).cook());
431
432 let customer = graph.schema("Customer").unwrap();
433
434 let cfg = CfgFeature::for_schema_type(&graph, &customer);
436 assert_eq!(cfg, None);
437 }
438
439 #[test]
442 fn test_for_schema_type_with_own_resource_no_deps() {
443 let doc = Document::from_yaml(indoc::indoc! {"
444 openapi: 3.0.0
445 info:
446 title: Test
447 version: 1.0.0
448 components:
449 schemas:
450 Customer:
451 type: object
452 x-resourceId: customer
453 properties:
454 id:
455 type: string
456 "})
457 .unwrap();
458
459 let arena = Arena::new();
460 let spec = Spec::from_doc(&arena, &doc).unwrap();
461 let graph = CodegenGraph::new(RawGraph::new(&arena, &spec).cook());
462
463 let customer = graph.schema("Customer").unwrap();
464 let cfg = CfgFeature::for_schema_type(&graph, &customer);
465
466 let actual: syn::Attribute = parse_quote!(#cfg);
467 let expected: syn::Attribute = parse_quote!(#[cfg(feature = "customer")]);
468 assert_eq!(actual, expected);
469 }
470
471 #[test]
472 fn test_for_schema_type_with_own_resource_and_unnamed_deps() {
473 let doc = Document::from_yaml(indoc::indoc! {"
476 openapi: 3.0.0
477 info:
478 title: Test
479 version: 1.0.0
480 components:
481 schemas:
482 Customer:
483 type: object
484 x-resourceId: customer
485 properties:
486 address:
487 $ref: '#/components/schemas/Address'
488 Address:
489 type: object
490 properties:
491 street:
492 type: string
493 "})
494 .unwrap();
495
496 let arena = Arena::new();
497 let spec = Spec::from_doc(&arena, &doc).unwrap();
498 let graph = CodegenGraph::new(RawGraph::new(&arena, &spec).cook());
499
500 let customer = graph.schema("Customer").unwrap();
502 let cfg = CfgFeature::for_schema_type(&graph, &customer);
503 let actual: syn::Attribute = parse_quote!(#cfg);
504 let expected: syn::Attribute = parse_quote!(#[cfg(feature = "customer")]);
505 assert_eq!(actual, expected);
506
507 let address = graph.schema("Address").unwrap();
509 let cfg = CfgFeature::for_schema_type(&graph, &address);
510 assert_eq!(cfg, None);
511 }
512
513 #[test]
514 fn test_for_schema_type_with_resource_id_named_default() {
515 let doc = Document::from_yaml(indoc::indoc! {"
518 openapi: 3.0.0
519 info:
520 title: Test
521 version: 1.0.0
522 components:
523 schemas:
524 Default:
525 type: object
526 x-resourceId: default
527 properties:
528 value:
529 type: string
530 "})
531 .unwrap();
532
533 let arena = Arena::new();
534 let spec = Spec::from_doc(&arena, &doc).unwrap();
535 let graph = CodegenGraph::new(RawGraph::new(&arena, &spec).cook());
536
537 let default_type = graph.schema("Default").unwrap();
538 let cfg = CfgFeature::for_schema_type(&graph, &default_type);
539
540 let actual: syn::Attribute = parse_quote!(#cfg);
541 let expected: syn::Attribute = parse_quote!(#[cfg(feature = "default2")]);
542 assert_eq!(actual, expected);
543 }
544
545 #[test]
548 fn test_for_schema_type_used_by_single_operation() {
549 let doc = Document::from_yaml(indoc::indoc! {"
550 openapi: 3.0.0
551 info:
552 title: Test
553 version: 1.0.0
554 paths:
555 /customers:
556 get:
557 operationId: listCustomers
558 x-resource-name: customer
559 responses:
560 '200':
561 description: OK
562 content:
563 application/json:
564 schema:
565 type: array
566 items:
567 $ref: '#/components/schemas/Customer'
568 components:
569 schemas:
570 Customer:
571 type: object
572 properties:
573 id:
574 type: string
575 "})
576 .unwrap();
577
578 let arena = Arena::new();
579 let spec = Spec::from_doc(&arena, &doc).unwrap();
580 let graph = CodegenGraph::new(RawGraph::new(&arena, &spec).cook());
581
582 let customer = graph.schema("Customer").unwrap();
583 let cfg = CfgFeature::for_schema_type(&graph, &customer);
584
585 let actual: syn::Attribute = parse_quote!(#cfg);
586 let expected: syn::Attribute = parse_quote!(#[cfg(feature = "customer")]);
587 assert_eq!(actual, expected);
588 }
589
590 #[test]
591 fn test_for_schema_type_used_by_multiple_operations() {
592 let doc = Document::from_yaml(indoc::indoc! {"
593 openapi: 3.0.0
594 info:
595 title: Test
596 version: 1.0.0
597 paths:
598 /customers:
599 get:
600 operationId: listCustomers
601 x-resource-name: customer
602 responses:
603 '200':
604 description: OK
605 content:
606 application/json:
607 schema:
608 type: array
609 items:
610 $ref: '#/components/schemas/Address'
611 /orders:
612 get:
613 operationId: listOrders
614 x-resource-name: orders
615 responses:
616 '200':
617 description: OK
618 content:
619 application/json:
620 schema:
621 type: array
622 items:
623 $ref: '#/components/schemas/Address'
624 components:
625 schemas:
626 Address:
627 type: object
628 properties:
629 street:
630 type: string
631 "})
632 .unwrap();
633
634 let arena = Arena::new();
635 let spec = Spec::from_doc(&arena, &doc).unwrap();
636 let graph = CodegenGraph::new(RawGraph::new(&arena, &spec).cook());
637
638 let address = graph.schema("Address").unwrap();
639 let cfg = CfgFeature::for_schema_type(&graph, &address);
640
641 let actual: syn::Attribute = parse_quote!(#cfg);
642 let expected: syn::Attribute =
643 parse_quote!(#[cfg(any(feature = "customer", feature = "orders"))]);
644 assert_eq!(actual, expected);
645 }
646
647 #[test]
650 fn test_for_schema_type_with_own_and_used_by() {
651 let doc = Document::from_yaml(indoc::indoc! {"
652 openapi: 3.0.0
653 info:
654 title: Test
655 version: 1.0.0
656 paths:
657 /billing:
658 get:
659 operationId: getBilling
660 x-resource-name: billing
661 responses:
662 '200':
663 description: OK
664 content:
665 application/json:
666 schema:
667 $ref: '#/components/schemas/Customer'
668 components:
669 schemas:
670 Customer:
671 type: object
672 x-resourceId: customer
673 properties:
674 id:
675 type: string
676 "})
677 .unwrap();
678
679 let arena = Arena::new();
680 let spec = Spec::from_doc(&arena, &doc).unwrap();
681 let graph = CodegenGraph::new(RawGraph::new(&arena, &spec).cook());
682
683 let customer = graph.schema("Customer").unwrap();
684 let cfg = CfgFeature::for_schema_type(&graph, &customer);
685
686 let actual: syn::Attribute = parse_quote!(#cfg);
687 let expected: syn::Attribute =
688 parse_quote!(#[cfg(all(feature = "billing", feature = "customer"))]);
689 assert_eq!(actual, expected);
690 }
691
692 #[test]
693 fn test_for_schema_type_with_own_and_same_used_by_uses_single_feature() {
694 let doc = Document::from_yaml(indoc::indoc! {"
695 openapi: 3.0.0
696 info:
697 title: Test
698 version: 1.0.0
699 paths:
700 /defaults:
701 get:
702 operationId: listDefaults
703 x-resource-name: default
704 responses:
705 '200':
706 description: OK
707 content:
708 application/json:
709 schema:
710 $ref: '#/components/schemas/Default'
711 components:
712 schemas:
713 Default:
714 type: object
715 x-resourceId: default
716 properties:
717 id:
718 type: string
719 "})
720 .unwrap();
721
722 let arena = Arena::new();
723 let spec = Spec::from_doc(&arena, &doc).unwrap();
724 let graph = CodegenGraph::new(RawGraph::new(&arena, &spec).cook());
725
726 let default_type = graph.schema("Default").unwrap();
727 let cfg = CfgFeature::for_schema_type(&graph, &default_type);
728
729 let actual: syn::Attribute = parse_quote!(#cfg);
730 let expected: syn::Attribute = parse_quote!(#[cfg(feature = "default2")]);
731 assert_eq!(actual, expected);
732 }
733
734 #[test]
735 fn test_for_schema_type_with_own_same_and_other_used_by_uses_single_feature() {
736 let doc = Document::from_yaml(indoc::indoc! {"
737 openapi: 3.0.0
738 info:
739 title: Test
740 version: 1.0.0
741 paths:
742 /customer:
743 get:
744 operationId: getCustomer
745 x-resource-name: customer
746 responses:
747 '200':
748 description: OK
749 content:
750 application/json:
751 schema:
752 $ref: '#/components/schemas/Customer'
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 id:
771 type: string
772 "})
773 .unwrap();
774
775 let arena = Arena::new();
776 let spec = Spec::from_doc(&arena, &doc).unwrap();
777 let graph = CodegenGraph::new(RawGraph::new(&arena, &spec).cook());
778
779 let customer = graph.schema("Customer").unwrap();
780 let cfg = CfgFeature::for_schema_type(&graph, &customer);
781
782 let actual: syn::Attribute = parse_quote!(#cfg);
783 let expected: syn::Attribute = parse_quote!(#[cfg(feature = "customer")]);
784 assert_eq!(actual, expected);
785 }
786
787 #[test]
788 fn test_for_schema_type_with_own_and_multiple_used_by() {
789 let doc = Document::from_yaml(indoc::indoc! {"
790 openapi: 3.0.0
791 info:
792 title: Test
793 version: 1.0.0
794 paths:
795 /billing:
796 get:
797 operationId: getBilling
798 x-resource-name: billing
799 responses:
800 '200':
801 description: OK
802 content:
803 application/json:
804 schema:
805 $ref: '#/components/schemas/Customer'
806 /orders:
807 get:
808 operationId: getOrders
809 x-resource-name: orders
810 responses:
811 '200':
812 description: OK
813 content:
814 application/json:
815 schema:
816 $ref: '#/components/schemas/Customer'
817 components:
818 schemas:
819 Customer:
820 type: object
821 x-resourceId: customer
822 properties:
823 id:
824 type: string
825 "})
826 .unwrap();
827
828 let arena = Arena::new();
829 let spec = Spec::from_doc(&arena, &doc).unwrap();
830 let graph = CodegenGraph::new(RawGraph::new(&arena, &spec).cook());
831
832 let customer = graph.schema("Customer").unwrap();
833 let cfg = CfgFeature::for_schema_type(&graph, &customer);
834
835 let actual: syn::Attribute = parse_quote!(#cfg);
836 let expected: syn::Attribute = parse_quote!(
837 #[cfg(all(feature = "customer", any(feature = "billing", feature = "orders")))]
838 );
839 assert_eq!(actual, expected);
840 }
841
842 #[test]
843 fn test_for_schema_type_with_own_used_by_and_unnamed_deps() {
844 let doc = Document::from_yaml(indoc::indoc! {"
848 openapi: 3.0.0
849 info:
850 title: Test
851 version: 1.0.0
852 paths:
853 /billing:
854 get:
855 operationId: getBilling
856 x-resource-name: billing
857 responses:
858 '200':
859 description: OK
860 content:
861 application/json:
862 schema:
863 $ref: '#/components/schemas/Customer'
864 components:
865 schemas:
866 Customer:
867 type: object
868 x-resourceId: customer
869 properties:
870 address:
871 $ref: '#/components/schemas/Address'
872 Address:
873 type: object
874 properties:
875 street:
876 type: string
877 "})
878 .unwrap();
879
880 let arena = Arena::new();
881 let spec = Spec::from_doc(&arena, &doc).unwrap();
882 let graph = CodegenGraph::new(RawGraph::new(&arena, &spec).cook());
883
884 let customer = graph.schema("Customer").unwrap();
886 let cfg = CfgFeature::for_schema_type(&graph, &customer);
887 let actual: syn::Attribute = parse_quote!(#cfg);
888 let expected: syn::Attribute =
889 parse_quote!(#[cfg(all(feature = "billing", feature = "customer"))]);
890 assert_eq!(actual, expected);
891
892 let address = graph.schema("Address").unwrap();
895 let cfg = CfgFeature::for_schema_type(&graph, &address);
896 let actual: syn::Attribute = parse_quote!(#cfg);
897 let expected: syn::Attribute = parse_quote!(#[cfg(feature = "billing")]);
898 assert_eq!(actual, expected);
899 }
900
901 #[test]
904 fn test_for_schema_type_unnamed_no_operations() {
905 let doc = Document::from_yaml(indoc::indoc! {"
908 openapi: 3.0.0
909 info:
910 title: Test
911 version: 1.0.0
912 components:
913 schemas:
914 Simple:
915 type: object
916 properties:
917 id:
918 type: string
919 Customer:
920 type: object
921 x-resourceId: customer
922 properties:
923 name:
924 type: string
925 "})
926 .unwrap();
927
928 let arena = Arena::new();
929 let spec = Spec::from_doc(&arena, &doc).unwrap();
930 let graph = CodegenGraph::new(RawGraph::new(&arena, &spec).cook());
931
932 let simple = graph.schema("Simple").unwrap();
933 let cfg = CfgFeature::for_schema_type(&graph, &simple);
934
935 assert_eq!(cfg, None);
938 }
939
940 #[test]
941 fn test_for_schema_type_used_by_unresourced_operation() {
942 let doc = Document::from_yaml(indoc::indoc! {"
947 openapi: 3.0.0
948 info:
949 title: Test
950 version: 1.0.0
951 paths:
952 /customers:
953 get:
954 operationId: listCustomers
955 x-resource-name: customer
956 responses:
957 '200':
958 description: OK
959 content:
960 application/json:
961 schema:
962 $ref: '#/components/schemas/Customer'
963 /health:
964 get:
965 operationId: healthCheck
966 responses:
967 '200':
968 description: OK
969 content:
970 application/json:
971 schema:
972 $ref: '#/components/schemas/Status'
973 components:
974 schemas:
975 Customer:
976 type: object
977 properties:
978 name:
979 type: string
980 Status:
981 type: object
982 properties:
983 ok:
984 type: boolean
985 "})
986 .unwrap();
987
988 let arena = Arena::new();
989 let spec = Spec::from_doc(&arena, &doc).unwrap();
990 let graph = CodegenGraph::new(RawGraph::new(&arena, &spec).cook());
991
992 let customer = graph.schema("Customer").unwrap();
994 let cfg = CfgFeature::for_schema_type(&graph, &customer);
995 let actual: syn::Attribute = parse_quote!(#cfg);
996 let expected: syn::Attribute = parse_quote!(#[cfg(feature = "customer")]);
997 assert_eq!(actual, expected);
998
999 let status = graph.schema("Status").unwrap();
1002 let cfg = CfgFeature::for_schema_type(&graph, &status);
1003 assert_eq!(cfg, None);
1004 }
1005
1006 #[test]
1007 fn test_for_schema_type_used_by_resourced_and_unresourced_operations() {
1008 let doc = Document::from_yaml(indoc::indoc! {"
1011 openapi: 3.0.0
1012 info:
1013 title: Test
1014 version: 1.0.0
1015 paths:
1016 /health:
1017 get:
1018 operationId: getHealth
1019 responses:
1020 '200':
1021 description: OK
1022 content:
1023 application/json:
1024 schema:
1025 $ref: '#/components/schemas/Status'
1026 /billing/status:
1027 get:
1028 operationId: getBillingStatus
1029 x-resource-name: billing
1030 responses:
1031 '200':
1032 description: OK
1033 content:
1034 application/json:
1035 schema:
1036 $ref: '#/components/schemas/Status'
1037 components:
1038 schemas:
1039 Status:
1040 type: object
1041 properties:
1042 ok:
1043 type: boolean
1044 "})
1045 .unwrap();
1046
1047 let arena = Arena::new();
1048 let spec = Spec::from_doc(&arena, &doc).unwrap();
1049 let graph = CodegenGraph::new(RawGraph::new(&arena, &spec).cook());
1050
1051 let status = graph.schema("Status").unwrap();
1052 let cfg = CfgFeature::for_schema_type(&graph, &status);
1053
1054 assert_eq!(cfg, None);
1055 }
1056
1057 #[test]
1060 fn test_for_schema_type_cycle_with_mixed_resources() {
1061 let doc = Document::from_yaml(indoc::indoc! {"
1065 openapi: 3.0.0
1066 info:
1067 title: Test
1068 version: 1.0.0
1069 components:
1070 schemas:
1071 A:
1072 type: object
1073 x-resourceId: a
1074 properties:
1075 b:
1076 $ref: '#/components/schemas/B'
1077 B:
1078 type: object
1079 properties:
1080 c:
1081 $ref: '#/components/schemas/C'
1082 C:
1083 type: object
1084 x-resourceId: c
1085 properties:
1086 a:
1087 $ref: '#/components/schemas/A'
1088 "})
1089 .unwrap();
1090
1091 let arena = Arena::new();
1092 let spec = Spec::from_doc(&arena, &doc).unwrap();
1093 let graph = CodegenGraph::new(RawGraph::new(&arena, &spec).cook());
1094
1095 let a = graph.schema("A").unwrap();
1098 let cfg = CfgFeature::for_schema_type(&graph, &a);
1099 assert_eq!(cfg, None);
1100
1101 let b = graph.schema("B").unwrap();
1102 let cfg = CfgFeature::for_schema_type(&graph, &b);
1103 assert_eq!(cfg, None);
1104
1105 let c = graph.schema("C").unwrap();
1106 let cfg = CfgFeature::for_schema_type(&graph, &c);
1107 assert_eq!(cfg, None);
1108 }
1109
1110 #[test]
1111 fn test_for_schema_type_cycle_with_all_named_resources() {
1112 let doc = Document::from_yaml(indoc::indoc! {"
1116 openapi: 3.0.0
1117 info:
1118 title: Test
1119 version: 1.0.0
1120 components:
1121 schemas:
1122 A:
1123 type: object
1124 x-resourceId: a
1125 properties:
1126 b:
1127 $ref: '#/components/schemas/B'
1128 B:
1129 type: object
1130 x-resourceId: b
1131 properties:
1132 c:
1133 $ref: '#/components/schemas/C'
1134 C:
1135 type: object
1136 x-resourceId: c
1137 properties:
1138 a:
1139 $ref: '#/components/schemas/A'
1140 "})
1141 .unwrap();
1142
1143 let arena = Arena::new();
1144 let spec = Spec::from_doc(&arena, &doc).unwrap();
1145 let graph = CodegenGraph::new(RawGraph::new(&arena, &spec).cook());
1146
1147 let a = graph.schema("A").unwrap();
1150 let cfg = CfgFeature::for_schema_type(&graph, &a);
1151 let actual: syn::Attribute = parse_quote!(#cfg);
1152 let expected: syn::Attribute = parse_quote!(#[cfg(feature = "a")]);
1153 assert_eq!(actual, expected);
1154
1155 let b = graph.schema("B").unwrap();
1156 let cfg = CfgFeature::for_schema_type(&graph, &b);
1157 let actual: syn::Attribute = parse_quote!(#cfg);
1158 let expected: syn::Attribute = parse_quote!(#[cfg(feature = "b")]);
1159 assert_eq!(actual, expected);
1160
1161 let c = graph.schema("C").unwrap();
1162 let cfg = CfgFeature::for_schema_type(&graph, &c);
1163 let actual: syn::Attribute = parse_quote!(#cfg);
1164 let expected: syn::Attribute = parse_quote!(#[cfg(feature = "c")]);
1165 assert_eq!(actual, expected);
1166 }
1167
1168 #[test]
1171 fn test_for_inline_returns_empty_when_no_named_resources() {
1172 let doc = Document::from_yaml(indoc::indoc! {"
1174 openapi: 3.0.0
1175 info:
1176 title: Test API
1177 version: 1.0.0
1178 paths:
1179 /items:
1180 get:
1181 operationId: getItems
1182 parameters:
1183 - name: filter
1184 in: query
1185 schema:
1186 type: object
1187 properties:
1188 status:
1189 type: string
1190 responses:
1191 '200':
1192 description: OK
1193 "})
1194 .unwrap();
1195
1196 let arena = Arena::new();
1197 let spec = Spec::from_doc(&arena, &doc).unwrap();
1198 let graph = CodegenGraph::new(RawGraph::new(&arena, &spec).cook());
1199
1200 let ops = graph.operations().collect_vec();
1201 let inlines = ops.iter().flat_map(|op| op.inlines()).collect_vec();
1202 assert!(!inlines.is_empty());
1203
1204 let cfg = CfgFeature::for_inline_type(&graph, &inlines[0]);
1206 assert_eq!(cfg, None);
1207 }
1208
1209 #[test]
1210 fn test_for_inline_type_used_by_own_resource_has_no_cfg() {
1211 let doc = Document::from_yaml(indoc::indoc! {"
1212 openapi: 3.0.0
1213 info:
1214 title: Test
1215 version: 1.0.0
1216 paths:
1217 /defaults:
1218 get:
1219 operationId: listDefaults
1220 x-resource-name: default
1221 responses:
1222 '200':
1223 description: OK
1224 content:
1225 application/json:
1226 schema:
1227 type: object
1228 properties:
1229 value:
1230 $ref: '#/components/schemas/Default'
1231 components:
1232 schemas:
1233 Default:
1234 type: object
1235 x-resourceId: default
1236 properties:
1237 id:
1238 type: string
1239 "})
1240 .unwrap();
1241
1242 let arena = Arena::new();
1243 let spec = Spec::from_doc(&arena, &doc).unwrap();
1244 let graph = CodegenGraph::new(RawGraph::new(&arena, &spec).cook());
1245
1246 let ops = graph.operations().collect_vec();
1247 let inlines = ops.iter().flat_map(|op| op.inlines()).collect_vec();
1248 assert!(!inlines.is_empty());
1249
1250 let cfg = CfgFeature::for_inline_type(&graph, &inlines[0]);
1251 assert_eq!(cfg, None);
1252 }
1253
1254 #[test]
1257 fn test_for_operation_reduces_transitive_chain() {
1258 let doc = Document::from_yaml(indoc::indoc! {"
1261 openapi: 3.0.0
1262 info:
1263 title: Test
1264 version: 1.0.0
1265 paths:
1266 /things:
1267 get:
1268 operationId: getThings
1269 responses:
1270 '200':
1271 description: OK
1272 content:
1273 application/json:
1274 schema:
1275 $ref: '#/components/schemas/A'
1276 components:
1277 schemas:
1278 A:
1279 type: object
1280 x-resourceId: a
1281 properties:
1282 b:
1283 $ref: '#/components/schemas/B'
1284 B:
1285 type: object
1286 x-resourceId: b
1287 properties:
1288 c:
1289 $ref: '#/components/schemas/C'
1290 C:
1291 type: object
1292 x-resourceId: c
1293 properties:
1294 value:
1295 type: string
1296 "})
1297 .unwrap();
1298
1299 let arena = Arena::new();
1300 let spec = Spec::from_doc(&arena, &doc).unwrap();
1301 let graph = CodegenGraph::new(RawGraph::new(&arena, &spec).cook());
1302
1303 let op = graph.operations().find(|o| o.id() == "getThings").unwrap();
1304 let cfg = CfgFeature::for_operation(&graph, &op);
1305
1306 let actual: syn::Attribute = parse_quote!(#cfg);
1307 let expected: syn::Attribute = parse_quote!(#[cfg(feature = "a")]);
1308 assert_eq!(actual, expected);
1309 }
1310
1311 #[test]
1312 fn test_for_operation_reduces_cycle() {
1313 let doc = Document::from_yaml(indoc::indoc! {"
1317 openapi: 3.0.0
1318 info:
1319 title: Test
1320 version: 1.0.0
1321 paths:
1322 /things:
1323 get:
1324 operationId: getThings
1325 responses:
1326 '200':
1327 description: OK
1328 content:
1329 application/json:
1330 schema:
1331 $ref: '#/components/schemas/A'
1332 components:
1333 schemas:
1334 A:
1335 type: object
1336 x-resourceId: a
1337 properties:
1338 b:
1339 $ref: '#/components/schemas/B'
1340 B:
1341 type: object
1342 x-resourceId: b
1343 properties:
1344 c:
1345 $ref: '#/components/schemas/C'
1346 C:
1347 type: object
1348 x-resourceId: c
1349 properties:
1350 a:
1351 $ref: '#/components/schemas/A'
1352 "})
1353 .unwrap();
1354
1355 let arena = Arena::new();
1356 let spec = Spec::from_doc(&arena, &doc).unwrap();
1357 let graph = CodegenGraph::new(RawGraph::new(&arena, &spec).cook());
1358
1359 let op = graph.operations().find(|o| o.id() == "getThings").unwrap();
1360 let cfg = CfgFeature::for_operation(&graph, &op);
1361
1362 let actual: syn::Attribute = parse_quote!(#cfg);
1364 let expected: syn::Attribute = parse_quote!(#[cfg(feature = "a")]);
1365 assert_eq!(actual, expected);
1366 }
1367
1368 #[test]
1369 fn test_for_operation_keeps_independent_features() {
1370 let doc = Document::from_yaml(indoc::indoc! {"
1373 openapi: 3.0.0
1374 info:
1375 title: Test
1376 version: 1.0.0
1377 paths:
1378 /things:
1379 get:
1380 operationId: getThings
1381 responses:
1382 '200':
1383 description: OK
1384 content:
1385 application/json:
1386 schema:
1387 type: object
1388 properties:
1389 a:
1390 $ref: '#/components/schemas/A'
1391 b:
1392 $ref: '#/components/schemas/B'
1393 components:
1394 schemas:
1395 A:
1396 type: object
1397 x-resourceId: a
1398 properties:
1399 value:
1400 type: string
1401 B:
1402 type: object
1403 x-resourceId: b
1404 properties:
1405 value:
1406 type: string
1407 "})
1408 .unwrap();
1409
1410 let arena = Arena::new();
1411 let spec = Spec::from_doc(&arena, &doc).unwrap();
1412 let graph = CodegenGraph::new(RawGraph::new(&arena, &spec).cook());
1413
1414 let op = graph.operations().find(|o| o.id() == "getThings").unwrap();
1415 let cfg = CfgFeature::for_operation(&graph, &op);
1416
1417 let actual: syn::Attribute = parse_quote!(#cfg);
1418 let expected: syn::Attribute = parse_quote!(#[cfg(all(feature = "a", feature = "b"))]);
1419 assert_eq!(actual, expected);
1420 }
1421
1422 #[test]
1423 fn test_for_operation_reduces_partial_deps() {
1424 let doc = Document::from_yaml(indoc::indoc! {"
1427 openapi: 3.0.0
1428 info:
1429 title: Test
1430 version: 1.0.0
1431 paths:
1432 /things:
1433 get:
1434 operationId: getThings
1435 responses:
1436 '200':
1437 description: OK
1438 content:
1439 application/json:
1440 schema:
1441 type: object
1442 properties:
1443 a:
1444 $ref: '#/components/schemas/A'
1445 c:
1446 $ref: '#/components/schemas/C'
1447 components:
1448 schemas:
1449 A:
1450 type: object
1451 x-resourceId: a
1452 properties:
1453 b:
1454 $ref: '#/components/schemas/B'
1455 B:
1456 type: object
1457 x-resourceId: b
1458 properties:
1459 value:
1460 type: string
1461 C:
1462 type: object
1463 x-resourceId: c
1464 properties:
1465 value:
1466 type: string
1467 "})
1468 .unwrap();
1469
1470 let arena = Arena::new();
1471 let spec = Spec::from_doc(&arena, &doc).unwrap();
1472 let graph = CodegenGraph::new(RawGraph::new(&arena, &spec).cook());
1473
1474 let op = graph.operations().find(|o| o.id() == "getThings").unwrap();
1475 let cfg = CfgFeature::for_operation(&graph, &op);
1476
1477 let actual: syn::Attribute = parse_quote!(#cfg);
1478 let expected: syn::Attribute = parse_quote!(#[cfg(all(feature = "a", feature = "c"))]);
1479 assert_eq!(actual, expected);
1480 }
1481
1482 #[test]
1483 fn test_for_operation_reduces_diamond_deps() {
1484 let doc = Document::from_yaml(indoc::indoc! {"
1487 openapi: 3.0.0
1488 info:
1489 title: Test
1490 version: 1.0.0
1491 paths:
1492 /things:
1493 get:
1494 operationId: getThings
1495 responses:
1496 '200':
1497 description: OK
1498 content:
1499 application/json:
1500 schema:
1501 $ref: '#/components/schemas/A'
1502 components:
1503 schemas:
1504 A:
1505 type: object
1506 x-resourceId: a
1507 properties:
1508 b:
1509 $ref: '#/components/schemas/B'
1510 c:
1511 $ref: '#/components/schemas/C'
1512 B:
1513 type: object
1514 x-resourceId: b
1515 properties:
1516 d:
1517 $ref: '#/components/schemas/D'
1518 C:
1519 type: object
1520 x-resourceId: c
1521 properties:
1522 d:
1523 $ref: '#/components/schemas/D'
1524 D:
1525 type: object
1526 x-resourceId: d
1527 properties:
1528 value:
1529 type: string
1530 "})
1531 .unwrap();
1532
1533 let arena = Arena::new();
1534 let spec = Spec::from_doc(&arena, &doc).unwrap();
1535 let graph = CodegenGraph::new(RawGraph::new(&arena, &spec).cook());
1536
1537 let op = graph.operations().find(|o| o.id() == "getThings").unwrap();
1538 let cfg = CfgFeature::for_operation(&graph, &op);
1539
1540 let actual: syn::Attribute = parse_quote!(#cfg);
1542 let expected: syn::Attribute = parse_quote!(#[cfg(feature = "a")]);
1543 assert_eq!(actual, expected);
1544 }
1545
1546 #[test]
1547 fn test_for_operation_with_no_types() {
1548 let doc = Document::from_yaml(indoc::indoc! {"
1550 openapi: 3.0.0
1551 info:
1552 title: Test
1553 version: 1.0.0
1554 paths:
1555 /health:
1556 get:
1557 operationId: healthCheck
1558 responses:
1559 '200':
1560 description: OK
1561 "})
1562 .unwrap();
1563
1564 let arena = Arena::new();
1565 let spec = Spec::from_doc(&arena, &doc).unwrap();
1566 let graph = CodegenGraph::new(RawGraph::new(&arena, &spec).cook());
1567
1568 let op = graph
1569 .operations()
1570 .find(|o| o.id() == "healthCheck")
1571 .unwrap();
1572 let cfg = CfgFeature::for_operation(&graph, &op);
1573
1574 assert_eq!(cfg, None);
1576 }
1577
1578 #[test]
1579 fn test_for_inline_type_reduces_transitive_features() {
1580 let doc = Document::from_yaml(indoc::indoc! {"
1583 openapi: 3.0.0
1584 info:
1585 title: Test
1586 version: 1.0.0
1587 paths:
1588 /things:
1589 get:
1590 operationId: getThings
1591 responses:
1592 '200':
1593 description: OK
1594 content:
1595 application/json:
1596 schema:
1597 type: object
1598 properties:
1599 a:
1600 $ref: '#/components/schemas/A'
1601 components:
1602 schemas:
1603 A:
1604 type: object
1605 x-resourceId: a
1606 properties:
1607 b:
1608 $ref: '#/components/schemas/B'
1609 B:
1610 type: object
1611 x-resourceId: b
1612 properties:
1613 c:
1614 $ref: '#/components/schemas/C'
1615 C:
1616 type: object
1617 x-resourceId: c
1618 properties:
1619 value:
1620 type: string
1621 "})
1622 .unwrap();
1623
1624 let arena = Arena::new();
1625 let spec = Spec::from_doc(&arena, &doc).unwrap();
1626 let graph = CodegenGraph::new(RawGraph::new(&arena, &spec).cook());
1627
1628 let ops = graph.operations().collect_vec();
1629 let inlines = ops.iter().flat_map(|op| op.inlines()).collect_vec();
1630 assert!(!inlines.is_empty());
1631
1632 let cfg = CfgFeature::for_inline_type(&graph, &inlines[0]);
1633
1634 let actual: syn::Attribute = parse_quote!(#cfg);
1635 let expected: syn::Attribute = parse_quote!(#[cfg(feature = "a")]);
1636 assert_eq!(actual, expected);
1637 }
1638}