miniextendr_api/dataframe/shape.rs
1//! [`DataFrameShape`]: the owned, GC-rooted return shape of the data.frame
2//! split verbs.
3//!
4//! Two producers share it:
5//!
6//! - `rows.into_dataframe_split()` ([`IntoDataFrameSplit`](super::IntoDataFrameSplit),
7//! from `#[derive(DataFrameRow)]` on an enum) — always
8//! [`Bare`](DataFrameShape::Bare) or [`PerVariantList`](DataFrameShape::PerVariantList);
9//! - the serde helpers `vec_to_dataframe_split` / `result_to_dataframe`
10//! (`feature = "serde"`) — every variant.
11//!
12//! The type lives here rather than under `serde` because the derive split is
13//! not serde-gated.
14
15use super::BuiltDataFrame;
16use crate::into_r::IntoR;
17use crate::{NamedListBuilder, Preserved, SEXP};
18
19/// Categorical return shape of the data.frame split verbs:
20/// [`IntoDataFrameSplit::into_dataframe_split`](super::IntoDataFrameSplit::into_dataframe_split)
21/// and the serde helpers `vec_to_dataframe_split` / `result_to_dataframe`
22/// (`feature = "serde"`).
23///
24/// Carries enough type information that downstream Rust code can `match`
25/// on the variant without dispatching on SEXP type. Convert to a SEXP via
26/// the [`IntoR`] impl (or just return it from a `#[miniextendr]`
27/// fn), which collapses every variant to the equivalent R value (bare
28/// data.frame / named list of data.frames / `list(results=, error=)`).
29///
30/// # GC discipline (rooted by construction, #1265, #1750)
31///
32/// Every frame inside the shape is an owned, GC-rooted
33/// [`BuiltDataFrame`], and the
34/// [`SplitResults::None`] sentinel carries its own [`Preserved`]
35/// root — so holding a `DataFrameShape` across R allocations is safe by
36/// construction; there is no convert-immediately contract. The
37/// [`IntoR`] conversion consumes the handles, re-rooting each
38/// child in the assembled R value.
39pub enum DataFrameShape {
40 /// Single data.frame.
41 ///
42 /// Used by:
43 /// - `into_dataframe_split` on a **single-variant** enum, always.
44 /// - serde `vec_to_dataframe_split` when only one variant is present (and
45 /// `SplitShape::PerVariantList` or `SplitShape::PerVariantListWithTag`
46 /// is selected — the single-variant short-circuit) or always under
47 /// `SplitShape::Collated`.
48 /// - serde `result_to_dataframe` under `ResultShape::Auto` when every row
49 /// is `Ok`, and always under `ResultShape::Collated`.
50 Bare(BuiltDataFrame),
51
52 /// `list(results = <df | sentinel>, error = df)`.
53 ///
54 /// Produced only by serde `result_to_dataframe`: under
55 /// `ResultShape::Auto` when at least one `Err` is present, and always
56 /// under `ResultShape::Split`. The split verbs never produce it.
57 Split {
58 /// The Ok partition.
59 results: SplitResults,
60 /// The error partition (always present, possibly zero-row).
61 error: BuiltDataFrame,
62 },
63
64 /// `list(VariantA = df, VariantB = df, …)`.
65 ///
66 /// Produced by:
67 /// - `into_dataframe_split` on a **multi-variant** enum, always: one
68 /// entry per declared variant, in declaration order, keyed by the
69 /// snake_case variant name. A variant absent from the input is a 0-row
70 /// frame with that variant's columns.
71 /// - serde `vec_to_dataframe_split` under `SplitShape::PerVariantList` /
72 /// `SplitShape::PerVariantListWithTag` when the input contains more
73 /// than one variant. Order matches first-seen order in the input slice.
74 PerVariantList(Vec<(String, BuiltDataFrame)>),
75}
76
77/// Result partition for [`DataFrameShape::Split`].
78///
79/// Used to distinguish "no Ok rows at all" (which lets the caller supply
80/// a sentinel value such as `NULL`, `NA`, `FALSE`, …) from a real
81/// zero-row data.frame.
82pub enum SplitResults {
83 /// At least one `Ok` row — partition has a concrete data.frame.
84 Some(BuiltDataFrame),
85 /// No `Ok` rows — sentinel value supplied by the caller via
86 /// `empty_ok_sentinel`, GC-rooted by its [`Preserved`] handle. The
87 /// sentinel is an arbitrary R value (`NULL`, `NA`, `FALSE`, a zero-row
88 /// data.frame, …), so it cannot ride in a [`BuiltDataFrame`].
89 ///
90 /// The handle is consumed by the [`IntoR`] impl on
91 /// [`DataFrameShape`]; until then its own root keeps the sentinel
92 /// reachable (#1265) — no immediate-conversion contract for this leg.
93 None(Preserved),
94}
95
96impl IntoR for DataFrameShape {
97 type Error = std::convert::Infallible;
98
99 fn try_into_sexp(self) -> Result<SEXP, Self::Error> {
100 match self {
101 DataFrameShape::Bare(df) => Ok(df.into_sexp()),
102 DataFrameShape::Split { results, error } => {
103 let mut builder = NamedListBuilder::with_capacity(2);
104 match results {
105 SplitResults::Some(df) => builder.push("results", df),
106 SplitResults::None(sentinel) => builder.push("results", sentinel),
107 }
108 builder.push("error", error);
109 Ok(builder.into_sexp())
110 }
111 DataFrameShape::PerVariantList(pairs) => {
112 let mut builder = NamedListBuilder::with_capacity(pairs.len());
113 for (name, df) in pairs {
114 builder.push(name, df);
115 }
116 Ok(builder.into_sexp())
117 }
118 }
119 }
120
121 unsafe fn try_into_sexp_unchecked(self) -> Result<SEXP, Self::Error> {
122 self.try_into_sexp()
123 }
124}