geop_core_editor/output/
export.rs1use geop_core_math::geop_error::{GeopError, GeopResult};
4use serde::{Serialize, de::DeserializeOwned};
5
6use super::{Output, read_options};
7
8#[derive(Clone, Debug, PartialEq, Serialize)]
11pub struct ExporterInfo {
12 pub kind: &'static str,
14 pub label: &'static str,
16 pub doc: &'static str,
18 pub formats: &'static [&'static str],
21}
22
23pub trait Exporter: 'static {
27 const INFO: ExporterInfo;
28 type Options: DeserializeOwned + Default;
31
32 fn export(output: &mut Output, format: &str, options: Self::Options) -> GeopResult<Export>;
34}
35
36pub trait ErasedExporter {
40 fn info(&self) -> &'static ExporterInfo;
41
42 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#[derive(Clone, Debug, PartialEq, Serialize)]
91pub struct Export {
92 pub name: String,
94 pub media_type: String,
96 #[serde(flatten)]
97 pub content: Content,
98}
99
100impl Export {
101 pub fn text(&self) -> Option<&str> {
103 match &self.content {
104 Content::Text(text) => Some(text),
105 Content::Bytes(_) => None,
106 }
107 }
108
109 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#[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
128fn base64<Ser: serde::Serializer>(bytes: &[u8], serializer: Ser) -> Result<Ser::Ok, Ser::Error> {
130 serializer.serialize_str(&geop_core_program::workspace::base64(bytes))
131}