Skip to main content

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}