Skip to main content

geop_core_editor/output/
export.rs

1//! [`Exporter`]: a file written from what a program built.
2
3use geop_core_math::geop_error::{GeopError, GeopResult};
4use serde::{Serialize, de::DeserializeOwned};
5
6use super::{Output, read_options};
7
8/// What an exporter is to a front end: how it is asked for, what it is
9/// offered as, and the formats it writes.
10#[derive(Clone, Debug, PartialEq, Serialize)]
11pub struct ExporterInfo {
12    /// The name it is asked for by: `stl`.
13    pub kind: &'static str,
14    /// What it is offered as: `STL`.
15    pub label: &'static str,
16    /// What the file is for, in a few words: `mesh, for 3-D printing`.
17    pub doc: &'static str,
18    /// The formats it writes, by the extension of their files, the one
19    /// written unless asked for another first: `["svg", "dxf"]`.
20    pub formats: &'static [&'static str],
21}
22
23/// A kind of file written from what a program built: a mesh, a STEP file,
24/// a drawing, a robot. An exporter is a unit struct; what it is told
25/// beyond the part is its [`Exporter::Options`].
26pub trait Exporter: 'static {
27    const INFO: ExporterInfo;
28    /// What it is told besides the part and the format, as a front end
29    /// sends it (JSON); the defaults where it sends none.
30    type Options: DeserializeOwned + Default;
31
32    /// The file of `output` in `format`, one of [`ExporterInfo::formats`].
33    fn export(output: &mut Output, format: &str, options: Self::Options) -> GeopResult<Export>;
34}
35
36/// An [`Exporter`] with its options erased, so that exporters of all kinds
37/// can be kept in one [`super::Outputs`]. Every exporter is one, through
38/// its value.
39pub trait ErasedExporter {
40    fn info(&self) -> &'static ExporterInfo;
41
42    /// The file of `output` in `format` — its first, if `None` — with
43    /// `options` as sent.
44    fn export(
45        &self,
46        output: &mut Output,
47        format: Option<&str>,
48        options: &serde_json::Value,
49    ) -> GeopResult<Export>;
50}
51
52impl<E: Exporter> ErasedExporter for E {
53    fn info(&self) -> &'static ExporterInfo {
54        &E::INFO
55    }
56
57    fn export(
58        &self,
59        output: &mut Output,
60        format: Option<&str>,
61        options: &serde_json::Value,
62    ) -> GeopResult<Export> {
63        let info = &E::INFO;
64        let format = match format {
65            None => info.formats.first().copied().ok_or_else(|| {
66                GeopError::new(format!("exporter `{}` writes no format", info.kind))
67            })?,
68            Some(format) => (info.formats.iter().copied())
69                .find(|f| f.eq_ignore_ascii_case(format))
70                .ok_or_else(|| {
71                    GeopError::new(format!(
72                        "exporter `{}` writes no `{format}`; it writes: {}",
73                        info.kind,
74                        info.formats.join(", ")
75                    ))
76                })?,
77        };
78        let options = read_options::<E::Options>(info.kind, options)?;
79        let file = output.file_name().to_string();
80        E::export(output, format, options).map_err(|e| {
81            e.with_context(format!(
82                "export(exporter={}, format={format}, file={file:?})",
83                info.kind
84            ))
85        })
86    }
87}
88
89/// A file written for a front end to save.
90#[derive(Clone, Debug, PartialEq, Serialize)]
91pub struct Export {
92    /// What to call it: `part.stl`.
93    pub name: String,
94    /// Its media type, to save it as: `model/stl`.
95    pub media_type: String,
96    #[serde(flatten)]
97    pub content: Content,
98}
99
100impl Export {
101    /// Its text, if it is a text file.
102    pub fn text(&self) -> Option<&str> {
103        match &self.content {
104            Content::Text(text) => Some(text),
105            Content::Bytes(_) => None,
106        }
107    }
108
109    /// What it holds, as bytes.
110    pub fn bytes(&self) -> &[u8] {
111        match &self.content {
112            Content::Text(text) => text.as_bytes(),
113            Content::Bytes(bytes) => bytes,
114        }
115    }
116}
117
118/// What an exported file holds: text — sent as `text` — or bytes, sent as
119/// `bytes`, in base64.
120#[derive(Clone, Debug, PartialEq, Serialize)]
121#[serde(rename_all = "snake_case")]
122pub enum Content {
123    Text(String),
124    #[serde(serialize_with = "base64")]
125    Bytes(Vec<u8>),
126}
127
128/// `bytes` as base64 text (RFC 4648, padded).
129fn base64<Ser: serde::Serializer>(bytes: &[u8], serializer: Ser) -> Result<Ser::Ok, Ser::Error> {
130    serializer.serialize_str(&geop_core_program::workspace::base64(bytes))
131}