Skip to main content

rightkit_docs/
model.rs

1//! Wire types: sources, read options and results, write ops and receipts. Shapes are serde
2//! camelCase with a `kind` or `type` discriminator, like `rightkit-pdf`'s commands.
3
4use std::path::{Path, PathBuf};
5
6use serde::{Deserialize, Serialize};
7
8use crate::limits::Limits;
9
10/// Where `read` gets its bytes. The format is detected from the content, never the file name.
11#[derive(Clone, Copy, Debug)]
12pub enum Source<'a> {
13    Path(&'a Path),
14    Bytes(&'a [u8]),
15}
16
17#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
18#[serde(rename_all = "lowercase")]
19pub enum Format {
20    Pdf,
21    Docx,
22    Xlsx,
23}
24
25impl Format {
26    pub fn as_str(self) -> &'static str {
27        match self {
28            Format::Pdf => "pdf",
29            Format::Docx => "docx",
30            Format::Xlsx => "xlsx",
31        }
32    }
33}
34
35/// Page, sheet and range selection plus the output caps.
36#[derive(Clone, Debug, Serialize, Deserialize)]
37#[serde(rename_all = "camelCase", default)]
38pub struct ReadOptions {
39    pub limits: Limits,
40    /// PDF only: 1-based pages to read. `None` reads every page.
41    pub pages: Option<Vec<u32>>,
42    /// XLSX only: sheet names to read. `None` reads every sheet.
43    pub sheets: Option<Vec<String>>,
44    /// XLSX only: an A1 range (`A1:C20`, `B2`) applied to each selected sheet.
45    pub range: Option<String>,
46    /// Cap on characters of text (and of markdown) returned. Past it `truncated` is set.
47    pub max_chars: usize,
48    /// Cap on cells returned across all sheets. Past it `truncated` is set.
49    pub max_cells: usize,
50    /// Build the markdown rendering (the plain-text rendering is always built).
51    pub markdown: bool,
52}
53
54impl Default for ReadOptions {
55    fn default() -> Self {
56        Self {
57            limits: Limits::default(),
58            pages: None,
59            sheets: None,
60            range: None,
61            max_chars: 200_000,
62            max_cells: 50_000,
63            markdown: true,
64        }
65    }
66}
67
68#[derive(Clone, Debug, Serialize, Deserialize)]
69#[serde(rename_all = "camelCase")]
70pub struct DocumentRead {
71    pub format: Format,
72    /// SHA-256 of the exact bytes read. Pass it back as `expect_sha256` to make an edit refuse a
73    /// file that changed in between.
74    pub sha256: String,
75    pub size_bytes: u64,
76    /// Plain-text rendering, capped at `max_chars`.
77    pub text: String,
78    pub markdown: Option<String>,
79    /// A cap (`max_chars`, `max_cells`) cut the result short.
80    pub truncated: bool,
81    pub content: Content,
82    /// What the file holds that this read does not represent (comments, tracked changes,
83    /// footnotes, images, charts, external links not followed...). Never empty by omission: if
84    /// something is not in `content`, it is named here.
85    pub omitted: Vec<String>,
86}
87
88#[derive(Clone, Debug, Serialize, Deserialize)]
89#[serde(tag = "kind", rename_all = "camelCase")]
90pub enum Content {
91    Docx(DocxContent),
92    Xlsx(XlsxContent),
93    Pdf(PdfContent),
94}
95
96#[derive(Clone, Debug, Serialize, Deserialize)]
97#[serde(rename_all = "camelCase")]
98pub struct DocxContent {
99    pub blocks: Vec<DocxBlock>,
100    pub headers: Vec<String>,
101    pub footers: Vec<String>,
102}
103
104#[derive(Clone, Debug, Serialize, Deserialize)]
105#[serde(tag = "kind", rename_all = "camelCase")]
106pub enum DocxBlock {
107    /// `style` is the style's display name (`heading 1`, `Title`, `List Paragraph`), not its id.
108    Paragraph {
109        style: Option<String>,
110        list: bool,
111        text: String,
112    },
113    Table {
114        rows: Vec<Vec<String>>,
115    },
116}
117
118#[derive(Clone, Debug, Serialize, Deserialize)]
119#[serde(rename_all = "camelCase")]
120pub struct XlsxContent {
121    pub sheets: Vec<SheetRead>,
122}
123
124#[derive(Clone, Debug, Serialize, Deserialize)]
125#[serde(rename_all = "camelCase")]
126pub struct SheetRead {
127    pub name: String,
128    /// `visible`, `hidden` or `veryHidden`.
129    pub state: String,
130    /// The used range of the whole sheet (`A1:D20`), before range selection; `None` when empty.
131    pub used_range: Option<String>,
132    /// Non-empty cells inside the selection, row-major. A formula cell with no stored result
133    /// (written by an edit, not yet calculated by a spreadsheet application) has an `empty` value
134    /// and a `formula`.
135    pub cells: Vec<CellRead>,
136    pub truncated: bool,
137}
138
139#[derive(Clone, Debug, Serialize, Deserialize)]
140#[serde(rename_all = "camelCase")]
141pub struct CellRead {
142    /// A1 address.
143    pub cell: String,
144    /// 1-based.
145    pub row: u32,
146    /// 1-based.
147    pub col: u32,
148    pub value: CellValue,
149    /// The formula text without the leading `=`, with the cached result in `value`.
150    pub formula: Option<String>,
151}
152
153#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
154#[serde(tag = "type", content = "value", rename_all = "camelCase")]
155pub enum CellValue {
156    Empty,
157    Text(String),
158    Number(f64),
159    Bool(bool),
160    /// An Excel date or time: the serial number and, when it is a plausible date, ISO 8601.
161    DateTime {
162        serial: f64,
163        iso: Option<String>,
164    },
165    /// An Excel duration in days.
166    Duration(f64),
167    /// `#DIV/0!`, `#N/A`, ...
168    Error(String),
169}
170
171#[derive(Clone, Debug, Serialize, Deserialize)]
172#[serde(rename_all = "camelCase")]
173pub struct PdfContent {
174    pub page_count: u32,
175    pub pages: Vec<PdfPageText>,
176}
177
178#[derive(Clone, Debug, Serialize, Deserialize)]
179#[serde(rename_all = "camelCase")]
180pub struct PdfPageText {
181    /// 1-based.
182    pub page: u32,
183    pub text: String,
184}
185
186// ---------------------------------------------------------------------------------------------
187// Writes
188// ---------------------------------------------------------------------------------------------
189
190/// A block of new DOCX content.
191#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
192#[serde(tag = "kind", rename_all = "camelCase")]
193pub enum Block {
194    /// `level` 1 to 6.
195    Heading {
196        level: u8,
197        text: String,
198    },
199    Paragraph {
200        text: String,
201    },
202    Bullet {
203        text: String,
204    },
205    Numbered {
206        text: String,
207    },
208    Table {
209        rows: Vec<Vec<String>>,
210        header: bool,
211    },
212    PageBreak,
213}
214
215/// A value to write into a cell. `Text` is always a string, whatever it starts with: only
216/// `Formula` is a formula.
217#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
218#[serde(tag = "type", content = "value", rename_all = "camelCase")]
219pub enum CellInput {
220    Text(String),
221    Number(f64),
222    Bool(bool),
223    /// With or without the leading `=`. No cached result is written.
224    Formula(String),
225    Blank,
226}
227
228#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
229#[serde(rename_all = "camelCase")]
230pub struct SheetSpec {
231    pub name: String,
232    /// Row-major from A1.
233    pub rows: Vec<Vec<CellInput>>,
234}
235
236#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
237#[serde(rename_all = "camelCase")]
238pub struct CellSet {
239    /// A1 address.
240    pub cell: String,
241    pub value: CellInput,
242}
243
244/// One typed write op. A `write` call applies its ops in order to one in-memory document and
245/// publishes the result once.
246#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
247#[serde(tag = "kind", rename_all = "camelCase")]
248pub enum Op {
249    /// Create a DOCX from blocks. The target must not exist unless `overwrite`.
250    CreateDocx { blocks: Vec<Block> },
251    /// Create an XLSX. The target must not exist unless `overwrite`.
252    CreateXlsx { sheets: Vec<SheetSpec> },
253    /// DOCX (body, headers, footers; across run boundaries) or PDF (source text, per island).
254    ReplaceText { find: String, replace: String },
255    /// DOCX: append blocks at the end of the body.
256    AppendBlocks { blocks: Vec<Block> },
257    /// XLSX: set cells on a sheet.
258    SetCells { sheet: String, cells: Vec<CellSet> },
259    /// XLSX: append rows after the last populated row of a sheet.
260    AppendRows {
261        sheet: String,
262        rows: Vec<Vec<CellInput>>,
263    },
264    /// PDF: add a line of text at a point (PDF user space, origin bottom-left, points).
265    AddPdfText {
266        page: u32,
267        x: f64,
268        y: f64,
269        text: String,
270        size_pt: Option<f64>,
271    },
272    /// PDF: keep only these 1-based pages, in this order.
273    SelectPdfPages { pages: Vec<u32> },
274}
275
276#[derive(Clone, Debug, Default, Serialize, Deserialize)]
277#[serde(rename_all = "camelCase", default)]
278pub struct WriteOptions {
279    pub limits: Limits,
280    /// For an edit: refuse (`source_changed`) unless the target's current SHA-256 equals this, and
281    /// again at publish time. Use the `sha256` of the `read` the edit is based on. An edit always
282    /// rechecks that the file did not change between its own read and its publish, with or without
283    /// this.
284    pub expect_sha256: Option<String>,
285    /// Allow a create op to replace an existing file.
286    pub overwrite: bool,
287}
288
289/// Something the edit could not keep, or deliberately changed. Empty `losses` means every part
290/// the edit did not touch is byte-identical to the source.
291#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
292#[serde(rename_all = "camelCase")]
293pub struct Loss {
294    pub part: String,
295    pub kind: String,
296    pub detail: String,
297}
298
299#[derive(Clone, Debug, Serialize, Deserialize)]
300#[serde(rename_all = "camelCase")]
301pub struct WriteReceipt {
302    pub path: PathBuf,
303    pub format: Format,
304    /// The target did not exist before.
305    pub created: bool,
306    /// SHA-256 of the file this edit started from; `None` for a create.
307    pub source_sha256: Option<String>,
308    pub sha256: String,
309    pub bytes_written: u64,
310    pub ops_applied: usize,
311    /// Text replacements made (`ReplaceText`), cells set and rows appended are in `warnings`-free
312    /// counters below.
313    pub replacements: usize,
314    pub cells_set: usize,
315    pub rows_appended: usize,
316    /// Package parts rewritten by the edit (OOXML). Every other part is byte-identical.
317    pub touched_parts: Vec<String>,
318    pub losses: Vec<Loss>,
319    pub warnings: Vec<String>,
320}