ducpy 3.1.0__tar.gz → 3.1.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {ducpy-3.1.0 → ducpy-3.1.1}/Cargo.lock +9 -7
- {ducpy-3.1.0 → ducpy-3.1.1}/Cargo.toml +0 -1
- {ducpy-3.1.0 → ducpy-3.1.1}/PKG-INFO +1 -1
- {ducpy-3.1.0 → ducpy-3.1.1}/packages/ducpy/crate/Cargo.toml +1 -1
- ducpy-3.1.1/packages/ducrs/src/lib.rs +297 -0
- ducpy-3.1.1/packages/ducrs/src/serde_utils.rs +90 -0
- {ducpy-3.1.0 → ducpy-3.1.1}/packages/ducrs/src/types.rs +22 -0
- ducpy-3.1.0/packages/ducrs/src/lib.rs +0 -94
- {ducpy-3.1.0 → ducpy-3.1.1}/LICENSE +0 -0
- {ducpy-3.1.0 → ducpy-3.1.1}/README.md +0 -0
- {ducpy-3.1.0 → ducpy-3.1.1}/packages/ducpy/crate/src/lib.rs +0 -0
- {ducpy-3.1.0 → ducpy-3.1.1}/packages/ducrs/.gitignore +0 -0
- {ducpy-3.1.0 → ducpy-3.1.1}/packages/ducrs/Cargo.toml +0 -0
- {ducpy-3.1.0 → ducpy-3.1.1}/packages/ducrs/LICENSE +0 -0
- {ducpy-3.1.0 → ducpy-3.1.1}/packages/ducrs/README.md +0 -0
- {ducpy-3.1.0 → ducpy-3.1.1}/packages/ducrs/build.rs +0 -0
- {ducpy-3.1.0 → ducpy-3.1.1}/packages/ducrs/package.json +0 -0
- {ducpy-3.1.0 → ducpy-3.1.1}/packages/ducrs/release.config.cjs +0 -0
- {ducpy-3.1.0 → ducpy-3.1.1}/packages/ducrs/src/api/document.rs +0 -0
- {ducpy-3.1.0 → ducpy-3.1.1}/packages/ducrs/src/api/meta.rs +0 -0
- {ducpy-3.1.0 → ducpy-3.1.1}/packages/ducrs/src/api/mod.rs +0 -0
- {ducpy-3.1.0 → ducpy-3.1.1}/packages/ducrs/src/api/version_control.rs +0 -0
- {ducpy-3.1.0 → ducpy-3.1.1}/packages/ducrs/src/db/bootstrap.rs +0 -0
- {ducpy-3.1.0 → ducpy-3.1.1}/packages/ducrs/src/db/mod.rs +0 -0
- {ducpy-3.1.0 → ducpy-3.1.1}/packages/ducrs/src/db/native.rs +0 -0
- {ducpy-3.1.0 → ducpy-3.1.1}/packages/ducrs/src/db/wasm.rs +0 -0
- {ducpy-3.1.0 → ducpy-3.1.1}/packages/ducrs/src/parse.rs +0 -0
- {ducpy-3.1.0 → ducpy-3.1.1}/packages/ducrs/src/serialize.rs +0 -0
- {ducpy-3.1.0 → ducpy-3.1.1}/packages/ducrs/tests/.gitignore +0 -0
- {ducpy-3.1.0 → ducpy-3.1.1}/pyproject.toml +0 -0
- {ducpy-3.1.0 → ducpy-3.1.1}/src/ducpy_native/__init__.py +0 -0
|
@@ -411,7 +411,7 @@ dependencies = [
|
|
|
411
411
|
|
|
412
412
|
[[package]]
|
|
413
413
|
name = "ducpy-native"
|
|
414
|
-
version = "3.1.
|
|
414
|
+
version = "3.1.1"
|
|
415
415
|
dependencies = [
|
|
416
416
|
"duc",
|
|
417
417
|
"pyo3",
|
|
@@ -713,10 +713,11 @@ checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c"
|
|
|
713
713
|
|
|
714
714
|
[[package]]
|
|
715
715
|
name = "hipdf"
|
|
716
|
-
version = "1.3.
|
|
716
|
+
version = "1.3.7"
|
|
717
717
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
718
|
-
checksum = "
|
|
718
|
+
checksum = "2dfe672796312ef1e60b2866d6dac03b67c32dc7276a71c6d34b573dd00502f1"
|
|
719
719
|
dependencies = [
|
|
720
|
+
"image",
|
|
720
721
|
"jpeg-decoder",
|
|
721
722
|
"lopdf",
|
|
722
723
|
"miniz_oxide",
|
|
@@ -764,6 +765,7 @@ dependencies = [
|
|
|
764
765
|
"byteorder-lite",
|
|
765
766
|
"color_quant",
|
|
766
767
|
"gif",
|
|
768
|
+
"image-webp",
|
|
767
769
|
"moxcms",
|
|
768
770
|
"num-traits",
|
|
769
771
|
"png 0.18.0",
|
|
@@ -2477,18 +2479,18 @@ dependencies = [
|
|
|
2477
2479
|
|
|
2478
2480
|
[[package]]
|
|
2479
2481
|
name = "zerocopy"
|
|
2480
|
-
version = "0.8.
|
|
2482
|
+
version = "0.8.42"
|
|
2481
2483
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
2482
|
-
checksum = "
|
|
2484
|
+
checksum = "f2578b716f8a7a858b7f02d5bd870c14bf4ddbbcf3a4c05414ba6503640505e3"
|
|
2483
2485
|
dependencies = [
|
|
2484
2486
|
"zerocopy-derive",
|
|
2485
2487
|
]
|
|
2486
2488
|
|
|
2487
2489
|
[[package]]
|
|
2488
2490
|
name = "zerocopy-derive"
|
|
2489
|
-
version = "0.8.
|
|
2491
|
+
version = "0.8.42"
|
|
2490
2492
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
2491
|
-
checksum = "
|
|
2493
|
+
checksum = "7e6cc098ea4d3bd6246687de65af3f920c430e236bee1e3bf2e441463f08a02f"
|
|
2492
2494
|
dependencies = [
|
|
2493
2495
|
"proc-macro2",
|
|
2494
2496
|
"quote",
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
#[allow(unused_imports)]
|
|
2
|
+
#[allow(non_camel_case_types)]
|
|
3
|
+
#[allow(non_snake_case)]
|
|
4
|
+
#[allow(non_upper_case_globals)]
|
|
5
|
+
pub mod parse;
|
|
6
|
+
pub mod serde_utils;
|
|
7
|
+
pub mod serialize;
|
|
8
|
+
pub mod types;
|
|
9
|
+
|
|
10
|
+
// SQLite storage layer and high-level document API
|
|
11
|
+
pub mod db;
|
|
12
|
+
pub mod api;
|
|
13
|
+
|
|
14
|
+
#[cfg(test)]
|
|
15
|
+
mod tests {
|
|
16
|
+
use std::collections::HashSet;
|
|
17
|
+
use std::fs;
|
|
18
|
+
use std::path::{Path, PathBuf};
|
|
19
|
+
|
|
20
|
+
use crate::parse;
|
|
21
|
+
use crate::serialize;
|
|
22
|
+
use crate::types::*;
|
|
23
|
+
|
|
24
|
+
fn assets_dir() -> PathBuf {
|
|
25
|
+
Path::new(env!("CARGO_MANIFEST_DIR")).join("../../assets/testing/duc-files")
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
fn all_duc_files() -> Vec<PathBuf> {
|
|
29
|
+
let dir = assets_dir();
|
|
30
|
+
assert!(dir.exists(), "assets dir missing: {}", dir.display());
|
|
31
|
+
let mut files: Vec<PathBuf> = fs::read_dir(&dir)
|
|
32
|
+
.expect("read assets dir")
|
|
33
|
+
.filter_map(|e| e.ok())
|
|
34
|
+
.map(|e| e.path())
|
|
35
|
+
.filter(|p| p.extension().is_some_and(|ext| ext == "duc"))
|
|
36
|
+
.collect();
|
|
37
|
+
files.sort();
|
|
38
|
+
assert!(!files.is_empty(), "no .duc files in {}", dir.display());
|
|
39
|
+
files
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
fn load(path: &Path) -> Vec<u8> {
|
|
43
|
+
fs::read(path).unwrap_or_else(|e| panic!("read {}: {e}", path.display()))
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ── Parse every asset ────────────────────────────────────────────────────
|
|
47
|
+
|
|
48
|
+
#[test]
|
|
49
|
+
fn parse_all_assets() {
|
|
50
|
+
for path in all_duc_files() {
|
|
51
|
+
let buf = load(&path);
|
|
52
|
+
let state = parse::parse(&buf)
|
|
53
|
+
.unwrap_or_else(|e| panic!("parse {} failed: {e}", path.display()));
|
|
54
|
+
|
|
55
|
+
assert!(!state.version.is_empty(), "{}: version must not be empty", path.display());
|
|
56
|
+
assert!(!state.source.is_empty(), "{}: source must not be empty", path.display());
|
|
57
|
+
assert!(!state.elements.is_empty() || !state.layers.is_empty(),
|
|
58
|
+
"{}: must have elements or layers", path.display());
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ── Round-trip: parse → serialize → re-parse preserves data ──────────────
|
|
63
|
+
|
|
64
|
+
#[test]
|
|
65
|
+
fn roundtrip_all_assets() {
|
|
66
|
+
for path in all_duc_files() {
|
|
67
|
+
let original = load(&path);
|
|
68
|
+
let parsed = parse::parse(&original)
|
|
69
|
+
.unwrap_or_else(|e| panic!("parse {} failed: {e}", path.display()));
|
|
70
|
+
|
|
71
|
+
let serialized = serialize::serialize(&parsed)
|
|
72
|
+
.unwrap_or_else(|e| panic!("serialize {} failed: {e}", path.display()));
|
|
73
|
+
assert!(!serialized.is_empty(), "{}: serialized must not be empty", path.display());
|
|
74
|
+
|
|
75
|
+
let reparsed = parse::parse(&serialized)
|
|
76
|
+
.unwrap_or_else(|e| panic!("re-parse {} failed: {e}", path.display()));
|
|
77
|
+
|
|
78
|
+
assert_eq!(
|
|
79
|
+
parsed.elements.len(), reparsed.elements.len(),
|
|
80
|
+
"{}: element count mismatch", path.display()
|
|
81
|
+
);
|
|
82
|
+
assert_eq!(
|
|
83
|
+
parsed.layers.len(), reparsed.layers.len(),
|
|
84
|
+
"{}: layer count mismatch", path.display()
|
|
85
|
+
);
|
|
86
|
+
assert_eq!(
|
|
87
|
+
parsed.blocks.len(), reparsed.blocks.len(),
|
|
88
|
+
"{}: block count mismatch", path.display()
|
|
89
|
+
);
|
|
90
|
+
assert_eq!(
|
|
91
|
+
parsed.block_instances.len(), reparsed.block_instances.len(),
|
|
92
|
+
"{}: block instance count mismatch", path.display()
|
|
93
|
+
);
|
|
94
|
+
assert_eq!(
|
|
95
|
+
parsed.groups.len(), reparsed.groups.len(),
|
|
96
|
+
"{}: group count mismatch", path.display()
|
|
97
|
+
);
|
|
98
|
+
assert_eq!(
|
|
99
|
+
parsed.regions.len(), reparsed.regions.len(),
|
|
100
|
+
"{}: region count mismatch", path.display()
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ── Lazy parse skips external files ──────────────────────────────────────
|
|
106
|
+
|
|
107
|
+
#[test]
|
|
108
|
+
fn lazy_parse_skips_external_files() {
|
|
109
|
+
for path in all_duc_files() {
|
|
110
|
+
let buf = load(&path);
|
|
111
|
+
let full = parse::parse(&buf)
|
|
112
|
+
.unwrap_or_else(|e| panic!("parse {} failed: {e}", path.display()));
|
|
113
|
+
let lazy = parse::parse_lazy(&buf)
|
|
114
|
+
.unwrap_or_else(|e| panic!("parse_lazy {} failed: {e}", path.display()));
|
|
115
|
+
|
|
116
|
+
assert!(lazy.external_files.is_none(), "{}: lazy parse must omit external_files", path.display());
|
|
117
|
+
assert_eq!(full.elements.len(), lazy.elements.len(), "{}: element count diverged", path.display());
|
|
118
|
+
assert_eq!(full.layers.len(), lazy.layers.len(), "{}: layer count diverged", path.display());
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// ── Element IDs are unique within each file ──────────────────────────────
|
|
123
|
+
|
|
124
|
+
#[test]
|
|
125
|
+
fn element_ids_unique() {
|
|
126
|
+
for path in all_duc_files() {
|
|
127
|
+
let buf = load(&path);
|
|
128
|
+
let state = parse::parse(&buf)
|
|
129
|
+
.unwrap_or_else(|e| panic!("parse {} failed: {e}", path.display()));
|
|
130
|
+
|
|
131
|
+
let mut seen = HashSet::new();
|
|
132
|
+
for wrapper in &state.elements {
|
|
133
|
+
let id = element_id(&wrapper.element);
|
|
134
|
+
assert!(seen.insert(id.clone()), "{}: duplicate element id '{id}'", path.display());
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// ── Every element references a valid layer ───────────────────────────────
|
|
140
|
+
|
|
141
|
+
#[test]
|
|
142
|
+
fn element_layer_refs_valid() {
|
|
143
|
+
for path in all_duc_files() {
|
|
144
|
+
let buf = load(&path);
|
|
145
|
+
let state = parse::parse(&buf)
|
|
146
|
+
.unwrap_or_else(|e| panic!("parse {} failed: {e}", path.display()));
|
|
147
|
+
|
|
148
|
+
let layer_ids: HashSet<String> = state.layers.iter().map(|l| l.id.clone()).collect();
|
|
149
|
+
|
|
150
|
+
for wrapper in &state.elements {
|
|
151
|
+
let base = element_base(&wrapper.element);
|
|
152
|
+
if let Some(ref lid) = base.layer_id {
|
|
153
|
+
assert!(
|
|
154
|
+
layer_ids.contains(lid),
|
|
155
|
+
"{}: element '{}' references non-existent layer '{lid}'",
|
|
156
|
+
path.display(), base.id
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// ── Block instances reference valid blocks ───────────────────────────────
|
|
164
|
+
|
|
165
|
+
#[test]
|
|
166
|
+
fn block_instance_refs_valid() {
|
|
167
|
+
for path in all_duc_files() {
|
|
168
|
+
let buf = load(&path);
|
|
169
|
+
let state = parse::parse(&buf)
|
|
170
|
+
.unwrap_or_else(|e| panic!("parse {} failed: {e}", path.display()));
|
|
171
|
+
|
|
172
|
+
let block_ids: HashSet<String> = state.blocks.iter().map(|b| b.id.clone()).collect();
|
|
173
|
+
|
|
174
|
+
for inst in &state.block_instances {
|
|
175
|
+
assert!(
|
|
176
|
+
block_ids.contains(&inst.block_id),
|
|
177
|
+
"{}: block instance '{}' references non-existent block '{}'",
|
|
178
|
+
path.display(), inst.id, inst.block_id
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// ── External files: list ↔ get round-trip ────────────────────────────────
|
|
185
|
+
|
|
186
|
+
#[test]
|
|
187
|
+
fn external_files_list_get_consistent() {
|
|
188
|
+
for path in all_duc_files() {
|
|
189
|
+
let buf = load(&path);
|
|
190
|
+
let listing = parse::list_external_files(&buf)
|
|
191
|
+
.unwrap_or_else(|e| panic!("list_external_files {} failed: {e}", path.display()));
|
|
192
|
+
|
|
193
|
+
for meta in &listing {
|
|
194
|
+
let file = parse::get_external_file(&buf, &meta.id)
|
|
195
|
+
.unwrap_or_else(|e| panic!("get_external_file {} id={} failed: {e}", path.display(), meta.id));
|
|
196
|
+
assert!(file.is_some(), "{}: listed file '{}' not found via get", path.display(), meta.id);
|
|
197
|
+
let file = file.unwrap();
|
|
198
|
+
assert_eq!(file.id, meta.id);
|
|
199
|
+
assert!(!file.revisions.is_empty(), "{}: file '{}' has no revisions", path.display(), meta.id);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// ── DB layer: open in-memory, schema bootstraps correctly ────────────────
|
|
205
|
+
|
|
206
|
+
#[test]
|
|
207
|
+
fn db_open_memory_and_schema() {
|
|
208
|
+
use crate::api::DucDocument;
|
|
209
|
+
|
|
210
|
+
let doc = DucDocument::open_memory().expect("open_memory");
|
|
211
|
+
let version = doc.schema_version().expect("schema_version");
|
|
212
|
+
assert!(version > 0, "schema version must be positive, got {version}");
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// ── Compression: serialized output is smaller than raw SQLite ────────────
|
|
216
|
+
|
|
217
|
+
#[test]
|
|
218
|
+
fn serialized_is_compressed() {
|
|
219
|
+
for path in all_duc_files() {
|
|
220
|
+
let buf = load(&path);
|
|
221
|
+
assert!(!parse::is_sqlite_header(&buf), "{}: assets should be compressed, not raw SQLite", path.display());
|
|
222
|
+
|
|
223
|
+
let raw = parse::decompress_duc_bytes(&buf)
|
|
224
|
+
.unwrap_or_else(|e| panic!("decompress {} failed: {e}", path.display()));
|
|
225
|
+
|
|
226
|
+
let state = parse::parse(&buf)
|
|
227
|
+
.unwrap_or_else(|e| panic!("parse {} failed: {e}", path.display()));
|
|
228
|
+
let recompressed = serialize::serialize(&state)
|
|
229
|
+
.unwrap_or_else(|e| panic!("serialize {} failed: {e}", path.display()));
|
|
230
|
+
|
|
231
|
+
assert!(
|
|
232
|
+
recompressed.len() < raw.len(),
|
|
233
|
+
"{}: compressed ({} B) should be smaller than raw SQLite ({} B)",
|
|
234
|
+
path.display(), recompressed.len(), raw.len()
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// ── Global state invariants ──────────────────────────────────────────────
|
|
240
|
+
|
|
241
|
+
#[test]
|
|
242
|
+
fn global_state_valid() {
|
|
243
|
+
for path in all_duc_files() {
|
|
244
|
+
let buf = load(&path);
|
|
245
|
+
let state = parse::parse(&buf)
|
|
246
|
+
.unwrap_or_else(|e| panic!("parse {} failed: {e}", path.display()));
|
|
247
|
+
|
|
248
|
+
if let Some(ref gs) = state.duc_global_state {
|
|
249
|
+
assert!(!gs.view_background_color.is_empty(), "{}: background color must not be empty", path.display());
|
|
250
|
+
assert!(!gs.main_scope.is_empty(), "{}: main_scope must not be empty", path.display());
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// ── Error handling: garbage input produces a clear error ──────────────────
|
|
256
|
+
|
|
257
|
+
#[test]
|
|
258
|
+
fn parse_rejects_garbage() {
|
|
259
|
+
let cases: &[(&str, &[u8])] = &[
|
|
260
|
+
("empty", &[]),
|
|
261
|
+
("random", &[0xFF; 64]),
|
|
262
|
+
("text", b"Not a DUC file"),
|
|
263
|
+
];
|
|
264
|
+
for (label, data) in cases {
|
|
265
|
+
assert!(
|
|
266
|
+
parse::parse(data).is_err(),
|
|
267
|
+
"parse should reject {label} input"
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// ── Helpers ──────────────────────────────────────────────────────────────
|
|
273
|
+
|
|
274
|
+
fn element_id(el: &DucElementEnum) -> String {
|
|
275
|
+
element_base(el).id.clone()
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
fn element_base(el: &DucElementEnum) -> &DucElementBase {
|
|
279
|
+
match el {
|
|
280
|
+
DucElementEnum::DucRectangleElement(e) => &e.base,
|
|
281
|
+
DucElementEnum::DucPolygonElement(e) => &e.base,
|
|
282
|
+
DucElementEnum::DucEllipseElement(e) => &e.base,
|
|
283
|
+
DucElementEnum::DucEmbeddableElement(e) => &e.base,
|
|
284
|
+
DucElementEnum::DucPdfElement(e) => &e.base,
|
|
285
|
+
DucElementEnum::DucTableElement(e) => &e.base,
|
|
286
|
+
DucElementEnum::DucImageElement(e) => &e.base,
|
|
287
|
+
DucElementEnum::DucTextElement(e) => &e.base,
|
|
288
|
+
DucElementEnum::DucLinearElement(e) => &e.linear_base.base,
|
|
289
|
+
DucElementEnum::DucArrowElement(e) => &e.linear_base.base,
|
|
290
|
+
DucElementEnum::DucFreeDrawElement(e) => &e.base,
|
|
291
|
+
DucElementEnum::DucFrameElement(e) => &e.stack_element_base.base,
|
|
292
|
+
DucElementEnum::DucPlotElement(e) => &e.stack_element_base.base,
|
|
293
|
+
DucElementEnum::DucDocElement(e) => &e.base,
|
|
294
|
+
DucElementEnum::DucModelElement(e) => &e.base,
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
use serde::Deserializer;
|
|
2
|
+
|
|
3
|
+
/// Deserializes an `i32` field from any JSON number, truncating floats instead of erroring.
|
|
4
|
+
/// Annotate struct fields with: `#[serde(deserialize_with = "crate::serde_utils::trunc_i32")]`
|
|
5
|
+
pub fn trunc_i32<'de, D>(deserializer: D) -> Result<i32, D::Error>
|
|
6
|
+
where
|
|
7
|
+
D: Deserializer<'de>,
|
|
8
|
+
{
|
|
9
|
+
struct Visitor;
|
|
10
|
+
|
|
11
|
+
impl<'de> serde::de::Visitor<'de> for Visitor {
|
|
12
|
+
type Value = i32;
|
|
13
|
+
|
|
14
|
+
fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
|
15
|
+
f.write_str("integer or float representable as i32")
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
fn visit_i8<E: serde::de::Error>(self, v: i8) -> Result<i32, E> {
|
|
19
|
+
Ok(v as i32)
|
|
20
|
+
}
|
|
21
|
+
fn visit_i16<E: serde::de::Error>(self, v: i16) -> Result<i32, E> {
|
|
22
|
+
Ok(v as i32)
|
|
23
|
+
}
|
|
24
|
+
fn visit_i32<E: serde::de::Error>(self, v: i32) -> Result<i32, E> {
|
|
25
|
+
Ok(v)
|
|
26
|
+
}
|
|
27
|
+
fn visit_i64<E: serde::de::Error>(self, v: i64) -> Result<i32, E> {
|
|
28
|
+
i32::try_from(v).map_err(E::custom)
|
|
29
|
+
}
|
|
30
|
+
fn visit_u8<E: serde::de::Error>(self, v: u8) -> Result<i32, E> {
|
|
31
|
+
Ok(v as i32)
|
|
32
|
+
}
|
|
33
|
+
fn visit_u16<E: serde::de::Error>(self, v: u16) -> Result<i32, E> {
|
|
34
|
+
Ok(v as i32)
|
|
35
|
+
}
|
|
36
|
+
fn visit_u32<E: serde::de::Error>(self, v: u32) -> Result<i32, E> {
|
|
37
|
+
i32::try_from(v).map_err(E::custom)
|
|
38
|
+
}
|
|
39
|
+
fn visit_u64<E: serde::de::Error>(self, v: u64) -> Result<i32, E> {
|
|
40
|
+
i32::try_from(v).map_err(E::custom)
|
|
41
|
+
}
|
|
42
|
+
fn visit_f32<E: serde::de::Error>(self, v: f32) -> Result<i32, E> {
|
|
43
|
+
Ok(v.trunc() as i32)
|
|
44
|
+
}
|
|
45
|
+
fn visit_f64<E: serde::de::Error>(self, v: f64) -> Result<i32, E> {
|
|
46
|
+
Ok(v.trunc() as i32)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
deserializer.deserialize_any(Visitor)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
struct FlexI32Seed;
|
|
54
|
+
|
|
55
|
+
impl<'de> serde::de::DeserializeSeed<'de> for FlexI32Seed {
|
|
56
|
+
type Value = i32;
|
|
57
|
+
fn deserialize<D: Deserializer<'de>>(self, deserializer: D) -> Result<i32, D::Error> {
|
|
58
|
+
trunc_i32(deserializer)
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/// Deserializes a `Vec<i32>` from a JSON array of numbers, truncating any floats.
|
|
63
|
+
/// Annotate struct fields with: `#[serde(deserialize_with = "crate::serde_utils::trunc_vec_i32")]`
|
|
64
|
+
pub fn trunc_vec_i32<'de, D>(deserializer: D) -> Result<Vec<i32>, D::Error>
|
|
65
|
+
where
|
|
66
|
+
D: Deserializer<'de>,
|
|
67
|
+
{
|
|
68
|
+
struct SeqVisitor;
|
|
69
|
+
|
|
70
|
+
impl<'de> serde::de::Visitor<'de> for SeqVisitor {
|
|
71
|
+
type Value = Vec<i32>;
|
|
72
|
+
|
|
73
|
+
fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
|
74
|
+
f.write_str("a sequence of integers or floats representable as i32")
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
fn visit_seq<A>(self, mut seq: A) -> Result<Vec<i32>, A::Error>
|
|
78
|
+
where
|
|
79
|
+
A: serde::de::SeqAccess<'de>,
|
|
80
|
+
{
|
|
81
|
+
let mut v = Vec::with_capacity(seq.size_hint().unwrap_or(0));
|
|
82
|
+
while let Some(n) = seq.next_element_seed(FlexI32Seed)? {
|
|
83
|
+
v.push(n);
|
|
84
|
+
}
|
|
85
|
+
Ok(v)
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
deserializer.deserialize_seq(SeqVisitor)
|
|
90
|
+
}
|
|
@@ -349,6 +349,7 @@ pub struct Viewer3DMaterial {
|
|
|
349
349
|
#[serde(rename_all = "camelCase")]
|
|
350
350
|
pub struct Viewer3DZebra {
|
|
351
351
|
pub active: bool,
|
|
352
|
+
#[serde(deserialize_with = "crate::serde_utils::trunc_i32")]
|
|
352
353
|
pub stripe_count: i32,
|
|
353
354
|
pub stripe_direction: f64,
|
|
354
355
|
/** Available: "blackwhite" | "colorful" | "grayscale" */
|
|
@@ -593,17 +594,20 @@ pub struct DucElementBase {
|
|
|
593
594
|
* Random integer used to seed shape generation.
|
|
594
595
|
* Doesn't differ across renders.
|
|
595
596
|
*/
|
|
597
|
+
#[serde(deserialize_with = "crate::serde_utils::trunc_i32")]
|
|
596
598
|
pub seed: i32,
|
|
597
599
|
/**
|
|
598
600
|
* Integer that is sequentially incremented on each change. Used to reconcile
|
|
599
601
|
* elements during collaboration or when saving to server.
|
|
600
602
|
*/
|
|
603
|
+
#[serde(deserialize_with = "crate::serde_utils::trunc_i32")]
|
|
601
604
|
pub version: i32,
|
|
602
605
|
/**
|
|
603
606
|
* Random integer that is regenerated on each change.
|
|
604
607
|
* Used for deterministic reconciliation of updates during collaboration,
|
|
605
608
|
* in case the versions (see above) are identical.
|
|
606
609
|
*/
|
|
610
|
+
#[serde(deserialize_with = "crate::serde_utils::trunc_i32")]
|
|
607
611
|
pub version_nonce: i32,
|
|
608
612
|
/** Epoch timestamp (ms) of last element update */
|
|
609
613
|
pub updated: i64,
|
|
@@ -673,6 +677,7 @@ pub struct DucHead {
|
|
|
673
677
|
#[serde(rename_all = "camelCase")]
|
|
674
678
|
pub struct PointBindingPoint {
|
|
675
679
|
/** The index of the target point within the element. */
|
|
680
|
+
#[serde(deserialize_with = "crate::serde_utils::trunc_i32")]
|
|
676
681
|
pub index: i32,
|
|
677
682
|
/**
|
|
678
683
|
* The offset from the point. Ranges from -1 to 1: 0 corresponds to the actual point.
|
|
@@ -709,6 +714,7 @@ pub struct DucPointBinding {
|
|
|
709
714
|
#[serde(rename_all = "camelCase")]
|
|
710
715
|
pub struct DucLineReference {
|
|
711
716
|
/** Index of the point in the points array */
|
|
717
|
+
#[serde(deserialize_with = "crate::serde_utils::trunc_i32")]
|
|
712
718
|
pub index: i32,
|
|
713
719
|
/** Bezier handle of the point on the line segment */
|
|
714
720
|
pub handle: Option<GeometricPoint>,
|
|
@@ -724,6 +730,7 @@ pub struct DucLine {
|
|
|
724
730
|
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
|
|
725
731
|
#[serde(rename_all = "camelCase")]
|
|
726
732
|
pub struct DucPath {
|
|
733
|
+
#[serde(deserialize_with = "crate::serde_utils::trunc_vec_i32")]
|
|
727
734
|
pub line_indices: Vec<i32>,
|
|
728
735
|
/** Override the background and stroke from the base if different than null */
|
|
729
736
|
pub background: Option<ElementBackground>,
|
|
@@ -990,6 +997,7 @@ pub struct DucPolygonElement {
|
|
|
990
997
|
#[serde(flatten)]
|
|
991
998
|
pub base: DucElementBase,
|
|
992
999
|
/** Number of sides of the polygon */
|
|
1000
|
+
#[serde(deserialize_with = "crate::serde_utils::trunc_i32")]
|
|
993
1001
|
pub sides: i32,
|
|
994
1002
|
}
|
|
995
1003
|
|
|
@@ -1015,6 +1023,7 @@ pub struct DucEmbeddableElement {
|
|
|
1015
1023
|
#[serde(rename_all = "camelCase")]
|
|
1016
1024
|
pub struct DocumentGridConfig {
|
|
1017
1025
|
/** 1 = single, 2 = two-up, n = grid */
|
|
1026
|
+
#[serde(deserialize_with = "crate::serde_utils::trunc_i32")]
|
|
1018
1027
|
pub columns: i32,
|
|
1019
1028
|
/** Horizontal spacing (px) */
|
|
1020
1029
|
pub gap_x: f64,
|
|
@@ -1201,7 +1210,9 @@ pub struct DucModelElement {
|
|
|
1201
1210
|
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
|
|
1202
1211
|
#[serde(rename_all = "camelCase")]
|
|
1203
1212
|
pub struct DucBlockDuplicationArray {
|
|
1213
|
+
#[serde(deserialize_with = "crate::serde_utils::trunc_i32")]
|
|
1204
1214
|
pub rows: i32,
|
|
1215
|
+
#[serde(deserialize_with = "crate::serde_utils::trunc_i32")]
|
|
1205
1216
|
pub cols: i32,
|
|
1206
1217
|
pub row_spacing: f64,
|
|
1207
1218
|
pub col_spacing: f64,
|
|
@@ -1211,6 +1222,7 @@ pub struct DucBlockDuplicationArray {
|
|
|
1211
1222
|
#[serde(rename_all = "camelCase")]
|
|
1212
1223
|
pub struct DucBlockMetadata {
|
|
1213
1224
|
pub source: Option<String>,
|
|
1225
|
+
#[serde(deserialize_with = "crate::serde_utils::trunc_i32")]
|
|
1214
1226
|
pub usage_count: i32,
|
|
1215
1227
|
/** Epoch timestamp (ms) of block creation */
|
|
1216
1228
|
pub created_at: i64,
|
|
@@ -1237,6 +1249,7 @@ pub struct DucBlock {
|
|
|
1237
1249
|
pub id: String,
|
|
1238
1250
|
pub label: String,
|
|
1239
1251
|
pub description: Option<String>,
|
|
1252
|
+
#[serde(deserialize_with = "crate::serde_utils::trunc_i32")]
|
|
1240
1253
|
pub version: i32,
|
|
1241
1254
|
pub metadata: Option<DucBlockMetadata>,
|
|
1242
1255
|
#[serde(with = "serde_bytes", default, skip_serializing_if = "Option::is_none")]
|
|
@@ -1250,6 +1263,7 @@ pub struct DucBlockInstance {
|
|
|
1250
1263
|
/** The reference to the DucBlock definition this instance is based on */
|
|
1251
1264
|
pub block_id: String,
|
|
1252
1265
|
/** The version that should match the block_id's version, incremented on each change */
|
|
1266
|
+
#[serde(deserialize_with = "crate::serde_utils::trunc_i32")]
|
|
1253
1267
|
pub version: i32,
|
|
1254
1268
|
pub element_overrides: Option<Vec<StringValueEntry>>,
|
|
1255
1269
|
pub duplication_array: Option<DucBlockDuplicationArray>,
|
|
@@ -1353,6 +1367,7 @@ pub struct DucLocalState {
|
|
|
1353
1367
|
* The user needs to manually update the graph for new versions to be saved in version control.
|
|
1354
1368
|
*/
|
|
1355
1369
|
pub manual_save_mode: bool,
|
|
1370
|
+
#[serde(deserialize_with = "crate::serde_utils::trunc_i32")]
|
|
1356
1371
|
pub decimal_places: i32,
|
|
1357
1372
|
}
|
|
1358
1373
|
|
|
@@ -1393,6 +1408,7 @@ pub struct Checkpoint {
|
|
|
1393
1408
|
#[serde(flatten)]
|
|
1394
1409
|
pub base: VersionBase,
|
|
1395
1410
|
pub version_number: i64,
|
|
1411
|
+
#[serde(deserialize_with = "crate::serde_utils::trunc_i32")]
|
|
1396
1412
|
pub schema_version: i32,
|
|
1397
1413
|
pub is_schema_boundary: bool,
|
|
1398
1414
|
#[serde(with = "serde_bytes")]
|
|
@@ -1406,6 +1422,7 @@ pub struct Delta {
|
|
|
1406
1422
|
#[serde(flatten)]
|
|
1407
1423
|
pub base: VersionBase,
|
|
1408
1424
|
pub version_number: i64,
|
|
1425
|
+
#[serde(deserialize_with = "crate::serde_utils::trunc_i32")]
|
|
1409
1426
|
pub schema_version: i32,
|
|
1410
1427
|
pub base_checkpoint_id: String,
|
|
1411
1428
|
/** Compressed binary data for the delta (zlib). When present, patch_string is ignored. */
|
|
@@ -1417,7 +1434,9 @@ pub struct Delta {
|
|
|
1417
1434
|
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
|
|
1418
1435
|
#[serde(rename_all = "camelCase")]
|
|
1419
1436
|
pub struct SchemaMigration {
|
|
1437
|
+
#[serde(deserialize_with = "crate::serde_utils::trunc_i32")]
|
|
1420
1438
|
pub from_schema_version: i32,
|
|
1439
|
+
#[serde(deserialize_with = "crate::serde_utils::trunc_i32")]
|
|
1421
1440
|
pub to_schema_version: i32,
|
|
1422
1441
|
pub migration_name: String,
|
|
1423
1442
|
pub migration_checksum: Option<String>,
|
|
@@ -1429,6 +1448,7 @@ pub struct SchemaMigration {
|
|
|
1429
1448
|
#[serde(rename_all = "camelCase")]
|
|
1430
1449
|
pub struct VersionChain {
|
|
1431
1450
|
pub id: String,
|
|
1451
|
+
#[serde(deserialize_with = "crate::serde_utils::trunc_i32")]
|
|
1432
1452
|
pub schema_version: i32,
|
|
1433
1453
|
pub start_version: i64,
|
|
1434
1454
|
pub end_version: Option<i64>,
|
|
@@ -1440,7 +1460,9 @@ pub struct VersionChain {
|
|
|
1440
1460
|
#[serde(rename_all = "camelCase")]
|
|
1441
1461
|
pub struct VersionGraphMetadata {
|
|
1442
1462
|
pub current_version: i64,
|
|
1463
|
+
#[serde(deserialize_with = "crate::serde_utils::trunc_i32")]
|
|
1443
1464
|
pub current_schema_version: i32,
|
|
1465
|
+
#[serde(deserialize_with = "crate::serde_utils::trunc_i32")]
|
|
1444
1466
|
pub chain_count: i32,
|
|
1445
1467
|
pub last_pruned: i64,
|
|
1446
1468
|
pub total_size: i64,
|
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
#[allow(unused_imports)]
|
|
2
|
-
#[allow(non_camel_case_types)]
|
|
3
|
-
#[allow(non_snake_case)]
|
|
4
|
-
#[allow(non_upper_case_globals)]
|
|
5
|
-
pub mod parse;
|
|
6
|
-
pub mod serialize;
|
|
7
|
-
pub mod types;
|
|
8
|
-
|
|
9
|
-
// SQLite storage layer and high-level document API
|
|
10
|
-
pub mod db;
|
|
11
|
-
pub mod api;
|
|
12
|
-
|
|
13
|
-
#[cfg(test)]
|
|
14
|
-
mod tests {
|
|
15
|
-
use std::fs;
|
|
16
|
-
use std::path::Path;
|
|
17
|
-
|
|
18
|
-
// Initialize logging for tests
|
|
19
|
-
fn init_logging() {
|
|
20
|
-
let _ = env_logger::builder()
|
|
21
|
-
.filter_level(log::LevelFilter::Info)
|
|
22
|
-
.is_test(true)
|
|
23
|
-
.try_init();
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
#[test]
|
|
27
|
-
fn test_parse_with_logging() {
|
|
28
|
-
init_logging();
|
|
29
|
-
|
|
30
|
-
// Try to load a test DUC file from assets folder
|
|
31
|
-
let test_file_path = Path::new("assets/testing/duc-files/plot_elements.duc");
|
|
32
|
-
|
|
33
|
-
if test_file_path.exists() {
|
|
34
|
-
match fs::read(test_file_path) {
|
|
35
|
-
Ok(data) => {
|
|
36
|
-
println!("Loaded test file: {} bytes", data.len());
|
|
37
|
-
|
|
38
|
-
// Call the parse function to trigger the logging
|
|
39
|
-
match crate::parse::parse(&data) {
|
|
40
|
-
Ok(parsed) => {
|
|
41
|
-
println!("✅ Successfully parsed DUC file");
|
|
42
|
-
println!("Layers count: {}", parsed.layers.len());
|
|
43
|
-
}
|
|
44
|
-
Err(e) => {
|
|
45
|
-
println!("❌ Parse error: {}", e);
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
Err(e) => {
|
|
50
|
-
println!("❌ Failed to read test file: {}", e);
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
} else {
|
|
54
|
-
println!("❌ Test file not found: {}", test_file_path.display());
|
|
55
|
-
// Create minimal test data to at least test the parsing pipeline
|
|
56
|
-
let minimal_data = vec![0u8; 100];
|
|
57
|
-
match crate::parse::parse(&minimal_data) {
|
|
58
|
-
Ok(_) => println!("✅ Minimal parse succeeded"),
|
|
59
|
-
Err(e) => println!("❌ Minimal parse failed: {}", e),
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
#[test]
|
|
65
|
-
fn test_serialize_roundtrip_compressed() {
|
|
66
|
-
init_logging();
|
|
67
|
-
|
|
68
|
-
let test_file_path = Path::new(env!("CARGO_MANIFEST_DIR"))
|
|
69
|
-
.join("../../assets/testing/duc-files/universal.duc");
|
|
70
|
-
if !test_file_path.exists() {
|
|
71
|
-
println!("Skipped — test file not found: {}", test_file_path.display());
|
|
72
|
-
return;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
let original = fs::read(test_file_path).expect("read test file");
|
|
76
|
-
let parsed = crate::parse::parse(&original).expect("parse original");
|
|
77
|
-
|
|
78
|
-
let compressed = crate::serialize::serialize(&parsed).expect("serialize");
|
|
79
|
-
|
|
80
|
-
assert!(!compressed.is_empty(), "serialized output must not be empty");
|
|
81
|
-
println!(
|
|
82
|
-
"original={} bytes, compressed={} bytes ({:.1}%)",
|
|
83
|
-
original.len(),
|
|
84
|
-
compressed.len(),
|
|
85
|
-
compressed.len() as f64 / original.len() as f64 * 100.0
|
|
86
|
-
);
|
|
87
|
-
|
|
88
|
-
let reparsed = crate::parse::parse(&compressed).expect("parse compressed");
|
|
89
|
-
assert_eq!(parsed.elements.len(), reparsed.elements.len());
|
|
90
|
-
assert_eq!(parsed.layers.len(), reparsed.layers.len());
|
|
91
|
-
assert_eq!(parsed.blocks.len(), reparsed.blocks.len());
|
|
92
|
-
println!("Round-trip OK — {} elements preserved", reparsed.elements.len());
|
|
93
|
-
}
|
|
94
|
-
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|