Skip to main content

ploidy_codegen_rust/
cargo.rs

1use std::collections::{BTreeMap, BTreeSet};
2
3use cargo_toml::{Dependency, DependencyDetail, Edition, Manifest};
4use itertools::Itertools;
5use ploidy_core::{codegen::IntoCode, ir::View};
6use serde::{Deserialize, Serialize};
7
8use super::{config::CodegenConfig, graph::CodegenGraph, naming::CargoFeature};
9
10const PLOIDY_VERSION: &str = env!("CARGO_PKG_VERSION");
11
12#[derive(Clone, Debug)]
13pub struct CodegenCargoManifest<'a> {
14    graph: &'a CodegenGraph<'a>,
15    manifest: &'a Manifest<CargoMetadata>,
16}
17
18impl<'a> CodegenCargoManifest<'a> {
19    #[inline]
20    pub fn new(graph: &'a CodegenGraph<'a>, manifest: &'a Manifest<CargoMetadata>) -> Self {
21        Self { graph, manifest }
22    }
23
24    pub fn to_manifest(self) -> Manifest<CargoMetadata> {
25        let mut manifest = self.manifest.clone();
26
27        // Ploidy generates Rust 2024-compatible code.
28        manifest
29            .package
30            .as_mut()
31            .unwrap()
32            .edition
33            .set(Edition::E2024);
34
35        // `ploidy-util` is our only runtime dependency.
36        manifest.dependencies.insert(
37            "ploidy-util".to_owned(),
38            Dependency::Detailed(
39                DependencyDetail {
40                    version: Some(PLOIDY_VERSION.to_owned()),
41                    ..Default::default()
42                }
43                .into(),
44            ),
45        );
46
47        // Translate resource names from operations and schemas into
48        // Cargo feature names with dependencies.
49        let features = {
50            let mut deps_by_feature = BTreeMap::new();
51
52            // For each schema type with an explicitly declared resource name,
53            // use the resource name as the feature name, and enable features
54            // for all its transitive dependencies.
55            for schema in self.graph.schemas() {
56                let feature = match schema.resource().map(CargoFeature::from_name) {
57                    Some(CargoFeature::Named(name)) => CargoFeature::Named(name),
58                    _ => continue,
59                };
60                let entry: &mut BTreeSet<_> = deps_by_feature.entry(feature).or_default();
61                for dep in schema.dependencies().filter_map(|ty| {
62                    match CargoFeature::from_name(ty.into_schema().ok()?.resource()?) {
63                        CargoFeature::Named(name) => Some(CargoFeature::Named(name)),
64                        CargoFeature::Default => None,
65                    }
66                }) {
67                    entry.insert(dep);
68                }
69            }
70
71            // For each operation with an explicitly declared resource name,
72            // use the resource name as the feature name, and enable features for
73            // all the types that are reachable from the operation.
74            for op in self.graph.operations() {
75                let feature = match op.resource().map(CargoFeature::from_name) {
76                    Some(CargoFeature::Named(name)) => CargoFeature::Named(name),
77                    _ => continue,
78                };
79                let entry = deps_by_feature.entry(feature).or_default();
80                for dep in op.dependencies().filter_map(|ty| {
81                    match CargoFeature::from_name(ty.into_schema().ok()?.resource()?) {
82                        CargoFeature::Named(name) => Some(CargoFeature::Named(name)),
83                        CargoFeature::Default => None,
84                    }
85                }) {
86                    entry.insert(dep);
87                }
88            }
89
90            // Build the `features` section of the manifest.
91            let mut features: BTreeMap<_, _> = deps_by_feature
92                .iter()
93                .map(|(feature, deps)| {
94                    (
95                        feature.display().to_string(),
96                        deps.iter()
97                            .map(|dep| dep.display().to_string())
98                            .collect_vec(),
99                    )
100                })
101                .collect();
102            if features.is_empty() {
103                BTreeMap::new()
104            } else {
105                // `default` enables all other features.
106                features.insert(
107                    "default".to_owned(),
108                    deps_by_feature
109                        .keys()
110                        .map(|feature| feature.display().to_string())
111                        .collect_vec(),
112                );
113                features
114            }
115        };
116
117        Manifest {
118            features,
119            ..manifest
120        }
121    }
122}
123
124impl IntoCode for CodegenCargoManifest<'_> {
125    type Code = (&'static str, Manifest<CargoMetadata>);
126
127    fn into_code(self) -> Self::Code {
128        ("Cargo.toml", self.to_manifest())
129    }
130}
131
132/// Cargo metadata for the generated crate.
133#[derive(Clone, Debug, Default, Deserialize, Serialize)]
134pub struct CargoMetadata {
135    #[serde(default)]
136    pub ploidy: Option<CodegenConfig>,
137}
138
139#[cfg(test)]
140mod tests {
141    use super::*;
142
143    use cargo_toml::Package;
144    use ploidy_core::{
145        arena::Arena,
146        ir::{RawGraph, Spec},
147        parse::Document,
148    };
149
150    use crate::tests::assert_matches;
151
152    fn default_manifest() -> Manifest<CargoMetadata> {
153        Manifest {
154            package: Some(Package::new("test-client", "0.1.0")),
155            ..Default::default()
156        }
157    }
158
159    // MARK: Feature collection
160
161    #[test]
162    fn test_schema_with_x_resource_id_creates_feature() {
163        let doc = Document::from_yaml(indoc::indoc! {"
164            openapi: 3.0.0
165            info:
166              title: Test
167              version: 1.0.0
168            components:
169              schemas:
170                Customer:
171                  type: object
172                  x-resourceId: customer
173                  properties:
174                    id:
175                      type: string
176        "})
177        .unwrap();
178
179        let arena = Arena::new();
180        let spec = Spec::from_doc(&arena, &doc).unwrap();
181        let graph = CodegenGraph::new(RawGraph::new(&arena, &spec).cook());
182        let manifest = CodegenCargoManifest::new(&graph, &default_manifest()).to_manifest();
183
184        let keys = manifest
185            .features
186            .keys()
187            .map(|feature| feature.as_str())
188            .collect_vec();
189        assert_matches!(&*keys, ["customer", "default"]);
190    }
191
192    #[test]
193    fn test_operation_with_x_resource_name_creates_feature() {
194        let doc = Document::from_yaml(indoc::indoc! {"
195            openapi: 3.0.0
196            info:
197              title: Test
198              version: 1.0.0
199            paths:
200              /pets:
201                get:
202                  operationId: listPets
203                  x-resource-name: pets
204                  responses:
205                    '200':
206                      description: OK
207        "})
208        .unwrap();
209
210        let arena = Arena::new();
211        let spec = Spec::from_doc(&arena, &doc).unwrap();
212        let graph = CodegenGraph::new(RawGraph::new(&arena, &spec).cook());
213        let manifest = CodegenCargoManifest::new(&graph, &default_manifest()).to_manifest();
214
215        let keys = manifest
216            .features
217            .keys()
218            .map(|feature| feature.as_str())
219            .collect_vec();
220        assert_matches!(&*keys, ["default", "pets"]);
221    }
222
223    #[test]
224    fn test_unnamed_schema_creates_no_features() {
225        let doc = Document::from_yaml(indoc::indoc! {"
226            openapi: 3.0.0
227            info:
228              title: Test
229              version: 1.0.0
230            components:
231              schemas:
232                Simple:
233                  type: object
234                  properties:
235                    id:
236                      type: string
237        "})
238        .unwrap();
239
240        let arena = Arena::new();
241        let spec = Spec::from_doc(&arena, &doc).unwrap();
242        let graph = CodegenGraph::new(RawGraph::new(&arena, &spec).cook());
243        let manifest = CodegenCargoManifest::new(&graph, &default_manifest()).to_manifest();
244
245        let keys = manifest
246            .features
247            .keys()
248            .map(|feature| feature.as_str())
249            .collect_vec();
250        assert_matches!(&*keys, []);
251    }
252
253    // MARK: Schema feature dependencies
254
255    #[test]
256    fn test_schema_dependency_creates_feature_dependency() {
257        let doc = Document::from_yaml(indoc::indoc! {"
258            openapi: 3.0.0
259            info:
260              title: Test
261              version: 1.0.0
262            components:
263              schemas:
264                Customer:
265                  type: object
266                  x-resourceId: customer
267                  properties:
268                    billing:
269                      $ref: '#/components/schemas/BillingInfo'
270                BillingInfo:
271                  type: object
272                  x-resourceId: billing
273                  properties:
274                    card:
275                      type: string
276        "})
277        .unwrap();
278
279        let arena = Arena::new();
280        let spec = Spec::from_doc(&arena, &doc).unwrap();
281        let graph = CodegenGraph::new(RawGraph::new(&arena, &spec).cook());
282        let manifest = CodegenCargoManifest::new(&graph, &default_manifest()).to_manifest();
283
284        // `Customer` depends on `BillingInfo`, so the `customer` feature
285        // should depend on `billing`.
286        let customer_deps = manifest.features["customer"]
287            .iter()
288            .map(|dep| dep.as_str())
289            .collect_vec();
290        assert_matches!(&*customer_deps, ["billing"]);
291    }
292
293    #[test]
294    fn test_transitive_schema_dependency_creates_feature_dependency() {
295        let doc = Document::from_yaml(indoc::indoc! {"
296            openapi: 3.0.0
297            info:
298              title: Test
299              version: 1.0.0
300            components:
301              schemas:
302                Order:
303                  type: object
304                  x-resourceId: orders
305                  properties:
306                    customer:
307                      $ref: '#/components/schemas/Customer'
308                Customer:
309                  type: object
310                  x-resourceId: customer
311                  properties:
312                    billing:
313                      $ref: '#/components/schemas/BillingInfo'
314                BillingInfo:
315                  type: object
316                  x-resourceId: billing
317                  properties:
318                    card:
319                      type: string
320        "})
321        .unwrap();
322
323        let arena = Arena::new();
324        let spec = Spec::from_doc(&arena, &doc).unwrap();
325        let graph = CodegenGraph::new(RawGraph::new(&arena, &spec).cook());
326        let manifest = CodegenCargoManifest::new(&graph, &default_manifest()).to_manifest();
327
328        // `Order` → `Customer` → `BillingInfo`, so `order` should
329        // depend on both `customer` and `billing`.
330        let order_deps = manifest.features["orders"]
331            .iter()
332            .map(|dep| dep.as_str())
333            .collect_vec();
334        assert_matches!(&*order_deps, ["billing", "customer"]);
335    }
336
337    #[test]
338    fn test_unnamed_dependency_does_not_create_feature_dependency() {
339        let doc = Document::from_yaml(indoc::indoc! {"
340            openapi: 3.0.0
341            info:
342              title: Test
343              version: 1.0.0
344            components:
345              schemas:
346                Customer:
347                  type: object
348                  x-resourceId: customer
349                  properties:
350                    address:
351                      $ref: '#/components/schemas/Address'
352                Address:
353                  type: object
354                  properties:
355                    street:
356                      type: string
357        "})
358        .unwrap();
359
360        let arena = Arena::new();
361        let spec = Spec::from_doc(&arena, &doc).unwrap();
362        let graph = CodegenGraph::new(RawGraph::new(&arena, &spec).cook());
363        let manifest = CodegenCargoManifest::new(&graph, &default_manifest()).to_manifest();
364
365        // `Customer` depends on `Address`, which doesn't have a resource.
366        // The `customer` feature should _not_ depend on `default`;
367        // that's handled via `cfg` attributes instead.
368        let customer_deps = manifest.features["customer"]
369            .iter()
370            .map(|dep| dep.as_str())
371            .collect_vec();
372        assert_matches!(&*customer_deps, []);
373    }
374
375    #[test]
376    fn test_feature_does_not_depend_on_itself() {
377        let doc = Document::from_yaml(indoc::indoc! {"
378            openapi: 3.0.0
379            info:
380              title: Test
381              version: 1.0.0
382            components:
383              schemas:
384                Node:
385                  type: object
386                  x-resourceId: nodes
387                  properties:
388                    children:
389                      type: array
390                      items:
391                        $ref: '#/components/schemas/Node'
392        "})
393        .unwrap();
394
395        let arena = Arena::new();
396        let spec = Spec::from_doc(&arena, &doc).unwrap();
397        let graph = CodegenGraph::new(RawGraph::new(&arena, &spec).cook());
398        let manifest = CodegenCargoManifest::new(&graph, &default_manifest()).to_manifest();
399
400        // Self-referential schemas should not create self-dependencies.
401        let node_deps = manifest.features["nodes"]
402            .iter()
403            .map(|dep| dep.as_str())
404            .collect_vec();
405        assert_matches!(&*node_deps, []);
406    }
407
408    // MARK: Operation feature dependencies
409
410    #[test]
411    fn test_operation_type_dependency_creates_feature_dependency() {
412        let doc = Document::from_yaml(indoc::indoc! {"
413            openapi: 3.0.0
414            info:
415              title: Test
416              version: 1.0.0
417            paths:
418              /orders:
419                get:
420                  operationId: listOrders
421                  x-resource-name: orders
422                  responses:
423                    '200':
424                      description: OK
425                      content:
426                        application/json:
427                          schema:
428                            type: array
429                            items:
430                              $ref: '#/components/schemas/Order'
431            components:
432              schemas:
433                Order:
434                  type: object
435                  properties:
436                    customer:
437                      $ref: '#/components/schemas/Customer'
438                Customer:
439                  type: object
440                  x-resourceId: customer
441                  properties:
442                    id:
443                      type: string
444        "})
445        .unwrap();
446
447        let arena = Arena::new();
448        let spec = Spec::from_doc(&arena, &doc).unwrap();
449        let graph = CodegenGraph::new(RawGraph::new(&arena, &spec).cook());
450        let manifest = CodegenCargoManifest::new(&graph, &default_manifest()).to_manifest();
451
452        // `listOrders` returns `Order`, which references `Customer`, so
453        // `orders` should depend on `customer`.
454        let orders_deps = manifest.features["orders"]
455            .iter()
456            .map(|dep| dep.as_str())
457            .collect_vec();
458        assert_matches!(&*orders_deps, ["customer"]);
459    }
460
461    #[test]
462    fn test_operation_with_unnamed_type_dependency_does_not_create_full_dependency() {
463        let doc = Document::from_yaml(indoc::indoc! {"
464            openapi: 3.0.0
465            info:
466              title: Test
467              version: 1.0.0
468            paths:
469              /customers:
470                get:
471                  operationId: listCustomers
472                  x-resource-name: customer
473                  responses:
474                    '200':
475                      description: OK
476                      content:
477                        application/json:
478                          schema:
479                            type: array
480                            items:
481                              $ref: '#/components/schemas/Customer'
482            components:
483              schemas:
484                Customer:
485                  type: object
486                  properties:
487                    address:
488                      $ref: '#/components/schemas/Address'
489                Address:
490                  type: object
491                  properties:
492                    street:
493                      type: string
494        "})
495        .unwrap();
496
497        let arena = Arena::new();
498        let spec = Spec::from_doc(&arena, &doc).unwrap();
499        let graph = CodegenGraph::new(RawGraph::new(&arena, &spec).cook());
500        let manifest = CodegenCargoManifest::new(&graph, &default_manifest()).to_manifest();
501
502        // `listOrders` returns `Customer`, which references `Address`, but
503        // `customer` should _not_ depend on `default`.
504        let customer_deps = manifest.features["customer"]
505            .iter()
506            .map(|dep| dep.as_str())
507            .collect_vec();
508        assert_matches!(&*customer_deps, []);
509    }
510
511    // MARK: Diamond dependencies
512
513    #[test]
514    fn test_diamond_dependency_deduplicates_feature() {
515        // A -> B, A -> C, B -> D, C -> D. All have resources.
516        // A's feature should depend on B, C, and D; D should appear once.
517        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                A:
525                  type: object
526                  x-resourceId: a
527                  properties:
528                    b:
529                      $ref: '#/components/schemas/B'
530                    c:
531                      $ref: '#/components/schemas/C'
532                B:
533                  type: object
534                  x-resourceId: b
535                  properties:
536                    d:
537                      $ref: '#/components/schemas/D'
538                C:
539                  type: object
540                  x-resourceId: c
541                  properties:
542                    d:
543                      $ref: '#/components/schemas/D'
544                D:
545                  type: object
546                  x-resourceId: d
547                  properties:
548                    value:
549                      type: string
550        "})
551        .unwrap();
552
553        let arena = Arena::new();
554        let spec = Spec::from_doc(&arena, &doc).unwrap();
555        let graph = CodegenGraph::new(RawGraph::new(&arena, &spec).cook());
556        let manifest = CodegenCargoManifest::new(&graph, &default_manifest()).to_manifest();
557
558        // `a` depends directly on `b`, `c`; transitively on `d` though `b` and `c`.
559        let a_deps = manifest.features["a"]
560            .iter()
561            .map(|dep| dep.as_str())
562            .collect_vec();
563        assert_matches!(&*a_deps, ["b", "c", "d"]);
564
565        // `b` and `c` each depend on `d`.
566        let b_deps = manifest.features["b"]
567            .iter()
568            .map(|dep| dep.as_str())
569            .collect_vec();
570        assert_matches!(&*b_deps, ["d"]);
571
572        let c_deps = manifest.features["c"]
573            .iter()
574            .map(|dep| dep.as_str())
575            .collect_vec();
576        assert_matches!(&*c_deps, ["d"]);
577
578        // `d` has no dependencies.
579        let d_deps = manifest.features["d"]
580            .iter()
581            .map(|dep| dep.as_str())
582            .collect_vec();
583        assert_matches!(&*d_deps, []);
584    }
585
586    // MARK: Cycles with mixed resources
587
588    #[test]
589    fn test_cycle_with_mixed_resources_does_not_create_feature_dependency() {
590        // Type A (resource `a`) -> Type B (no resource) -> Type C (resource `c`) -> Type A.
591        // Since B doesn't have a resource, we don't create a dependency on it;
592        // that's handled via `#[cfg(...)]` attributes.
593        let doc = Document::from_yaml(indoc::indoc! {"
594            openapi: 3.0.0
595            info:
596              title: Test
597              version: 1.0.0
598            components:
599              schemas:
600                A:
601                  type: object
602                  x-resourceId: a
603                  properties:
604                    b:
605                      $ref: '#/components/schemas/B'
606                B:
607                  type: object
608                  properties:
609                    c:
610                      $ref: '#/components/schemas/C'
611                C:
612                  type: object
613                  x-resourceId: c
614                  properties:
615                    a:
616                      $ref: '#/components/schemas/A'
617        "})
618        .unwrap();
619
620        let arena = Arena::new();
621        let spec = Spec::from_doc(&arena, &doc).unwrap();
622        let graph = CodegenGraph::new(RawGraph::new(&arena, &spec).cook());
623        let manifest = CodegenCargoManifest::new(&graph, &default_manifest()).to_manifest();
624
625        // A depends on B (unnamed) and C. Since B is unnamed, A only depends on C.
626        let a_deps = manifest.features["a"]
627            .iter()
628            .map(|dep| dep.as_str())
629            .collect_vec();
630        assert_matches!(&*a_deps, ["c"]);
631
632        // C depends on A (which depends on B, unnamed). C only depends on A.
633        let c_deps = manifest.features["c"]
634            .iter()
635            .map(|dep| dep.as_str())
636            .collect_vec();
637        assert_matches!(&*c_deps, ["a"]);
638
639        // `default` should include both named features.
640        let default_deps = manifest.features["default"]
641            .iter()
642            .map(|dep| dep.as_str())
643            .collect_vec();
644        assert_matches!(&*default_deps, ["a", "c"]);
645    }
646
647    #[test]
648    fn test_cycle_with_all_named_resources_creates_mutual_dependencies() {
649        // Type A (resource `a`) -> Type B (resource `b`) -> Type C (resource `c`) -> Type A.
650        // Each feature should depend on the others in the cycle.
651        let doc = Document::from_yaml(indoc::indoc! {"
652            openapi: 3.0.0
653            info:
654              title: Test
655              version: 1.0.0
656            components:
657              schemas:
658                A:
659                  type: object
660                  x-resourceId: a
661                  properties:
662                    b:
663                      $ref: '#/components/schemas/B'
664                B:
665                  type: object
666                  x-resourceId: b
667                  properties:
668                    c:
669                      $ref: '#/components/schemas/C'
670                C:
671                  type: object
672                  x-resourceId: c
673                  properties:
674                    a:
675                      $ref: '#/components/schemas/A'
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        let manifest = CodegenCargoManifest::new(&graph, &default_manifest()).to_manifest();
683
684        // A transitively depends on B and C.
685        let a_deps = manifest.features["a"]
686            .iter()
687            .map(|dep| dep.as_str())
688            .collect_vec();
689        assert_matches!(&*a_deps, ["b", "c"]);
690
691        // B transitively depends on A and C.
692        let b_deps = manifest.features["b"]
693            .iter()
694            .map(|dep| dep.as_str())
695            .collect_vec();
696        assert_matches!(&*b_deps, ["a", "c"]);
697
698        // C transitively depends on A and B.
699        let c_deps = manifest.features["c"]
700            .iter()
701            .map(|dep| dep.as_str())
702            .collect_vec();
703        assert_matches!(&*c_deps, ["a", "b"]);
704
705        // `default` should include all three.
706        let default_deps = manifest.features["default"]
707            .iter()
708            .map(|dep| dep.as_str())
709            .collect_vec();
710        assert_matches!(&*default_deps, ["a", "b", "c"]);
711    }
712
713    // MARK: Default feature
714
715    #[test]
716    fn test_default_feature_includes_all_other_features() {
717        let doc = Document::from_yaml(indoc::indoc! {"
718            openapi: 3.0.0
719            info:
720              title: Test
721              version: 1.0.0
722            paths:
723              /pets:
724                get:
725                  operationId: listPets
726                  x-resource-name: pets
727                  responses:
728                    '200':
729                      description: OK
730            components:
731              schemas:
732                Customer:
733                  type: object
734                  x-resourceId: customer
735                  properties:
736                    id:
737                      type: string
738                Order:
739                  type: object
740                  x-resourceId: orders
741                  properties:
742                    id:
743                      type: string
744        "})
745        .unwrap();
746
747        let arena = Arena::new();
748        let spec = Spec::from_doc(&arena, &doc).unwrap();
749        let graph = CodegenGraph::new(RawGraph::new(&arena, &spec).cook());
750        let manifest = CodegenCargoManifest::new(&graph, &default_manifest()).to_manifest();
751
752        // The `default` feature should include all other features, but not itself.
753        let default_deps = manifest.features["default"]
754            .iter()
755            .map(|dep| dep.as_str())
756            .collect_vec();
757        assert_matches!(&*default_deps, ["customer", "orders", "pets"]);
758    }
759
760    #[test]
761    fn test_default_feature_includes_all_named_features() {
762        let doc = Document::from_yaml(indoc::indoc! {"
763            openapi: 3.0.0
764            info:
765              title: Test
766              version: 1.0.0
767            components:
768              schemas:
769                Customer:
770                  type: object
771                  x-resourceId: customer
772                  properties:
773                    id:
774                      type: string
775        "})
776        .unwrap();
777
778        let arena = Arena::new();
779        let spec = Spec::from_doc(&arena, &doc).unwrap();
780        let graph = CodegenGraph::new(RawGraph::new(&arena, &spec).cook());
781        let manifest = CodegenCargoManifest::new(&graph, &default_manifest()).to_manifest();
782
783        // The `default` feature should include all named features.
784        let default_deps = manifest.features["default"]
785            .iter()
786            .map(|dep| dep.as_str())
787            .collect_vec();
788        assert_matches!(&*default_deps, ["customer"]);
789    }
790
791    // MARK: Dependencies
792
793    #[test]
794    fn test_preserves_existing_dependencies() {
795        let doc = Document::from_yaml(indoc::indoc! {"
796            openapi: 3.0.0
797            info:
798              title: Test
799              version: 1.0.0
800            paths: {}
801        "})
802        .unwrap();
803
804        let mut manifest = default_manifest();
805        manifest
806            .dependencies
807            .insert("serde".to_owned(), Dependency::Simple("1.0".to_owned()));
808
809        let arena = Arena::new();
810        let spec = Spec::from_doc(&arena, &doc).unwrap();
811        let graph = CodegenGraph::new(RawGraph::new(&arena, &spec).cook());
812        let manifest = CodegenCargoManifest::new(&graph, &manifest).to_manifest();
813
814        let dep_names = manifest
815            .dependencies
816            .keys()
817            .map(|k| k.as_str())
818            .collect_vec();
819        assert_matches!(&*dep_names, ["ploidy-util", "serde"]);
820    }
821}