Skip to main content

earl_core/
with.rs

1//! rkyv `ArchiveWith` wrappers for types that don't natively support rkyv.
2//!
3//! - [`AsJson`] serializes a `serde_json::Value` (and collections thereof) as a
4//!   JSON-encoded `String` in the archive.
5//! - [`AsPath`] serializes a `std::path::PathBuf` as a UTF-8 `String`.
6
7use std::collections::BTreeMap;
8use std::path::PathBuf;
9
10use rkyv::{
11    Archive, Archived, Place, Resolver,
12    rancor::{Fallible, Source},
13    ser::{Allocator, Writer},
14    with::{ArchiveWith, DeserializeWith, SerializeWith},
15};
16
17// ── AsPath ────────────────────────────────────────────────────────────────────
18
19/// Wrapper that archives a `PathBuf` as a UTF-8 `String`.
20///
21/// # Limitations
22///
23/// Non-UTF-8 path components are silently replaced with `U+FFFD` via
24/// `to_string_lossy`. A round-trip through this wrapper will produce a
25/// different (non-existent) path on such systems, causing a cache miss.
26/// Template files are developer-named HCL files and are virtually always
27/// UTF-8, so this is acceptable in practice.
28pub struct AsPath;
29
30impl ArchiveWith<PathBuf> for AsPath {
31    type Archived = Archived<String>;
32    type Resolver = Resolver<String>;
33
34    fn resolve_with(field: &PathBuf, resolver: Self::Resolver, out: Place<Self::Archived>) {
35        let s = field.to_string_lossy().into_owned();
36        Archive::resolve(&s, resolver, out);
37    }
38}
39
40impl<S: Fallible + Writer + Allocator + ?Sized> SerializeWith<PathBuf, S> for AsPath
41where
42    S::Error: Source,
43{
44    fn serialize_with(field: &PathBuf, s: &mut S) -> Result<Self::Resolver, S::Error> {
45        let path_str = field.to_string_lossy().into_owned();
46        rkyv::Serialize::serialize(&path_str, s)
47    }
48}
49
50impl<D: Fallible + ?Sized> DeserializeWith<Archived<String>, PathBuf, D> for AsPath {
51    fn deserialize_with(field: &Archived<String>, _d: &mut D) -> Result<PathBuf, D::Error> {
52        Ok(PathBuf::from(field.as_str()))
53    }
54}
55
56// ── AsJson ────────────────────────────────────────────────────────────────────
57
58/// Wrapper that archives a value by JSON-encoding it into a `String`.
59///
60/// Use this with `#[rkyv(with = AsJson)]` on fields whose types contain
61/// `serde_json::Value` (which does not implement rkyv's `Archive` trait).
62///
63/// Supported field types:
64/// - `serde_json::Value`
65/// - `BTreeMap<String, serde_json::Value>`
66/// - `Vec<serde_json::Value>`
67/// - `Option<serde_json::Value>`
68/// - `Option<BTreeMap<String, serde_json::Value>>`
69/// - `Option<Vec<serde_json::Value>>`
70/// - `Vec<(PathBuf, u64)>` (used for cache fingerprints)
71///
72/// # Limitations
73///
74/// The `Vec<(PathBuf, u64)>` impl uses `to_string_lossy` for path conversion.
75/// Non-UTF-8 path components are silently replaced with `U+FFFD`, causing
76/// fingerprint mismatches and perpetual cache misses on such paths.
77pub struct AsJson;
78
79// ── serde_json::Value ─────────────────────────────────────────────────────────
80
81impl ArchiveWith<serde_json::Value> for AsJson {
82    type Archived = Archived<String>;
83    type Resolver = Resolver<String>;
84
85    fn resolve_with(
86        field: &serde_json::Value,
87        resolver: Self::Resolver,
88        out: Place<Self::Archived>,
89    ) {
90        let s = serde_json::to_string(field).unwrap_or_default();
91        Archive::resolve(&s, resolver, out);
92    }
93}
94
95impl<S: Fallible + Writer + Allocator + ?Sized> SerializeWith<serde_json::Value, S> for AsJson
96where
97    S::Error: Source,
98{
99    fn serialize_with(field: &serde_json::Value, s: &mut S) -> Result<Self::Resolver, S::Error> {
100        let json = serde_json::to_string(field).unwrap_or_default();
101        rkyv::Serialize::serialize(&json, s)
102    }
103}
104
105impl<D: Fallible + ?Sized> DeserializeWith<Archived<String>, serde_json::Value, D> for AsJson {
106    fn deserialize_with(
107        field: &Archived<String>,
108        _d: &mut D,
109    ) -> Result<serde_json::Value, D::Error> {
110        Ok(serde_json::from_str(field.as_str()).unwrap_or(serde_json::Value::Null))
111    }
112}
113
114// ── BTreeMap<String, serde_json::Value> ──────────────────────────────────────
115
116impl ArchiveWith<BTreeMap<String, serde_json::Value>> for AsJson {
117    type Archived = Archived<String>;
118    type Resolver = Resolver<String>;
119
120    fn resolve_with(
121        field: &BTreeMap<String, serde_json::Value>,
122        resolver: Self::Resolver,
123        out: Place<Self::Archived>,
124    ) {
125        let s = serde_json::to_string(field).unwrap_or_default();
126        Archive::resolve(&s, resolver, out);
127    }
128}
129
130impl<S: Fallible + Writer + Allocator + ?Sized>
131    SerializeWith<BTreeMap<String, serde_json::Value>, S> for AsJson
132where
133    S::Error: Source,
134{
135    fn serialize_with(
136        field: &BTreeMap<String, serde_json::Value>,
137        s: &mut S,
138    ) -> Result<Self::Resolver, S::Error> {
139        let json = serde_json::to_string(field).unwrap_or_default();
140        rkyv::Serialize::serialize(&json, s)
141    }
142}
143
144impl<D: Fallible + ?Sized> DeserializeWith<Archived<String>, BTreeMap<String, serde_json::Value>, D>
145    for AsJson
146{
147    fn deserialize_with(
148        field: &Archived<String>,
149        _d: &mut D,
150    ) -> Result<BTreeMap<String, serde_json::Value>, D::Error> {
151        Ok(serde_json::from_str(field.as_str()).unwrap_or_default())
152    }
153}
154
155// ── Vec<serde_json::Value> ────────────────────────────────────────────────────
156
157impl ArchiveWith<Vec<serde_json::Value>> for AsJson {
158    type Archived = Archived<String>;
159    type Resolver = Resolver<String>;
160
161    fn resolve_with(
162        field: &Vec<serde_json::Value>,
163        resolver: Self::Resolver,
164        out: Place<Self::Archived>,
165    ) {
166        let s = serde_json::to_string(field).unwrap_or_default();
167        Archive::resolve(&s, resolver, out);
168    }
169}
170
171impl<S: Fallible + Writer + Allocator + ?Sized> SerializeWith<Vec<serde_json::Value>, S> for AsJson
172where
173    S::Error: Source,
174{
175    fn serialize_with(
176        field: &Vec<serde_json::Value>,
177        s: &mut S,
178    ) -> Result<Self::Resolver, S::Error> {
179        let json = serde_json::to_string(field).unwrap_or_default();
180        rkyv::Serialize::serialize(&json, s)
181    }
182}
183
184impl<D: Fallible + ?Sized> DeserializeWith<Archived<String>, Vec<serde_json::Value>, D> for AsJson {
185    fn deserialize_with(
186        field: &Archived<String>,
187        _d: &mut D,
188    ) -> Result<Vec<serde_json::Value>, D::Error> {
189        Ok(serde_json::from_str(field.as_str()).unwrap_or_default())
190    }
191}
192
193// ── Option<serde_json::Value> ─────────────────────────────────────────────────
194
195impl ArchiveWith<Option<serde_json::Value>> for AsJson {
196    type Archived = Archived<Option<String>>;
197    type Resolver = Resolver<Option<String>>;
198
199    fn resolve_with(
200        field: &Option<serde_json::Value>,
201        resolver: Self::Resolver,
202        out: Place<Self::Archived>,
203    ) {
204        let s: Option<String> = field
205            .as_ref()
206            .map(|v| serde_json::to_string(v).unwrap_or_default());
207        Archive::resolve(&s, resolver, out);
208    }
209}
210
211impl<S: Fallible + Writer + Allocator + ?Sized> SerializeWith<Option<serde_json::Value>, S>
212    for AsJson
213where
214    S::Error: Source,
215{
216    fn serialize_with(
217        field: &Option<serde_json::Value>,
218        s: &mut S,
219    ) -> Result<Self::Resolver, S::Error> {
220        let opt: Option<String> = field
221            .as_ref()
222            .map(|v| serde_json::to_string(v).unwrap_or_default());
223        rkyv::Serialize::serialize(&opt, s)
224    }
225}
226
227impl<D: Fallible + ?Sized> DeserializeWith<Archived<Option<String>>, Option<serde_json::Value>, D>
228    for AsJson
229{
230    fn deserialize_with(
231        field: &Archived<Option<String>>,
232        _d: &mut D,
233    ) -> Result<Option<serde_json::Value>, D::Error> {
234        match field.as_ref() {
235            None => Ok(None),
236            Some(s) => Ok(Some(
237                serde_json::from_str(s.as_str()).unwrap_or(serde_json::Value::Null),
238            )),
239        }
240    }
241}
242
243// ── Option<BTreeMap<String, serde_json::Value>> ───────────────────────────────
244
245impl ArchiveWith<Option<BTreeMap<String, serde_json::Value>>> for AsJson {
246    type Archived = Archived<Option<String>>;
247    type Resolver = Resolver<Option<String>>;
248
249    fn resolve_with(
250        field: &Option<BTreeMap<String, serde_json::Value>>,
251        resolver: Self::Resolver,
252        out: Place<Self::Archived>,
253    ) {
254        let s: Option<String> = field
255            .as_ref()
256            .map(|m| serde_json::to_string(m).unwrap_or_default());
257        Archive::resolve(&s, resolver, out);
258    }
259}
260
261impl<S: Fallible + Writer + Allocator + ?Sized>
262    SerializeWith<Option<BTreeMap<String, serde_json::Value>>, S> for AsJson
263where
264    S::Error: Source,
265{
266    fn serialize_with(
267        field: &Option<BTreeMap<String, serde_json::Value>>,
268        s: &mut S,
269    ) -> Result<Self::Resolver, S::Error> {
270        let opt: Option<String> = field
271            .as_ref()
272            .map(|m| serde_json::to_string(m).unwrap_or_default());
273        rkyv::Serialize::serialize(&opt, s)
274    }
275}
276
277impl<D: Fallible + ?Sized>
278    DeserializeWith<Archived<Option<String>>, Option<BTreeMap<String, serde_json::Value>>, D>
279    for AsJson
280{
281    fn deserialize_with(
282        field: &Archived<Option<String>>,
283        _d: &mut D,
284    ) -> Result<Option<BTreeMap<String, serde_json::Value>>, D::Error> {
285        match field.as_ref() {
286            None => Ok(None),
287            Some(s) => Ok(Some(serde_json::from_str(s.as_str()).unwrap_or_default())),
288        }
289    }
290}
291
292// ── Option<Vec<serde_json::Value>> ────────────────────────────────────────────
293
294impl ArchiveWith<Option<Vec<serde_json::Value>>> for AsJson {
295    type Archived = Archived<Option<String>>;
296    type Resolver = Resolver<Option<String>>;
297
298    fn resolve_with(
299        field: &Option<Vec<serde_json::Value>>,
300        resolver: Self::Resolver,
301        out: Place<Self::Archived>,
302    ) {
303        let s: Option<String> = field
304            .as_ref()
305            .map(|v| serde_json::to_string(v).unwrap_or_default());
306        Archive::resolve(&s, resolver, out);
307    }
308}
309
310impl<S: Fallible + Writer + Allocator + ?Sized> SerializeWith<Option<Vec<serde_json::Value>>, S>
311    for AsJson
312where
313    S::Error: Source,
314{
315    fn serialize_with(
316        field: &Option<Vec<serde_json::Value>>,
317        s: &mut S,
318    ) -> Result<Self::Resolver, S::Error> {
319        let opt: Option<String> = field
320            .as_ref()
321            .map(|v| serde_json::to_string(v).unwrap_or_default());
322        rkyv::Serialize::serialize(&opt, s)
323    }
324}
325
326impl<D: Fallible + ?Sized>
327    DeserializeWith<Archived<Option<String>>, Option<Vec<serde_json::Value>>, D> for AsJson
328{
329    fn deserialize_with(
330        field: &Archived<Option<String>>,
331        _d: &mut D,
332    ) -> Result<Option<Vec<serde_json::Value>>, D::Error> {
333        match field.as_ref() {
334            None => Ok(None),
335            Some(s) => Ok(Some(serde_json::from_str(s.as_str()).unwrap_or_default())),
336        }
337    }
338}
339
340// ── Vec<(PathBuf, u64)> ───────────────────────────────────────────────────────
341//
342// Used for the cache fingerprint list. Archived as a JSON string since
343// (PathBuf, u64) tuples are not Archive-able without wrapper indirection.
344
345impl ArchiveWith<Vec<(PathBuf, u64)>> for AsJson {
346    type Archived = Archived<String>;
347    type Resolver = Resolver<String>;
348
349    fn resolve_with(
350        field: &Vec<(PathBuf, u64)>,
351        resolver: Self::Resolver,
352        out: Place<Self::Archived>,
353    ) {
354        let pairs: Vec<(String, u64)> = field
355            .iter()
356            .map(|(p, t)| (p.to_string_lossy().into_owned(), *t))
357            .collect();
358        let s = serde_json::to_string(&pairs).unwrap_or_default();
359        Archive::resolve(&s, resolver, out);
360    }
361}
362
363impl<S: Fallible + Writer + Allocator + ?Sized> SerializeWith<Vec<(PathBuf, u64)>, S> for AsJson
364where
365    S::Error: Source,
366{
367    fn serialize_with(field: &Vec<(PathBuf, u64)>, s: &mut S) -> Result<Self::Resolver, S::Error> {
368        let pairs: Vec<(String, u64)> = field
369            .iter()
370            .map(|(p, t)| (p.to_string_lossy().into_owned(), *t))
371            .collect();
372        let json = serde_json::to_string(&pairs).unwrap_or_default();
373        rkyv::Serialize::serialize(&json, s)
374    }
375}
376
377impl<D: Fallible + ?Sized> DeserializeWith<Archived<String>, Vec<(PathBuf, u64)>, D> for AsJson {
378    fn deserialize_with(
379        field: &Archived<String>,
380        _d: &mut D,
381    ) -> Result<Vec<(PathBuf, u64)>, D::Error> {
382        let pairs: Vec<(String, u64)> = serde_json::from_str(field.as_str()).unwrap_or_default();
383        Ok(pairs
384            .into_iter()
385            .map(|(s, t)| (PathBuf::from(s), t))
386            .collect())
387    }
388}
389
390#[cfg(test)]
391mod tests {
392    use std::collections::BTreeMap;
393    use std::path::PathBuf;
394
395    use rkyv::rancor::Error as RkyvError;
396    use serde_json::{Value, json};
397
398    use super::*;
399
400    /// Helper: roundtrip a wrapper struct through rkyv serialize → deserialize.
401    macro_rules! roundtrip {
402        ($wrapper:ty, $field_ty:ty, $value:expr) => {{
403            #[derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
404            struct Wrapper {
405                #[rkyv(with = $wrapper)]
406                field: $field_ty,
407            }
408            let original = Wrapper { field: $value };
409            let bytes = rkyv::to_bytes::<RkyvError>(&original).expect("serialize");
410            let decoded: Wrapper =
411                rkyv::from_bytes::<Wrapper, RkyvError>(&bytes).expect("deserialize");
412            decoded.field
413        }};
414    }
415
416    #[test]
417    fn as_path_roundtrips_utf8_path() {
418        let path = PathBuf::from("/home/user/templates/github.hcl");
419        let decoded = roundtrip!(AsPath, PathBuf, path.clone());
420        assert_eq!(decoded, path);
421    }
422
423    #[test]
424    fn as_json_value_roundtrips_object() {
425        let v: Value = json!({"key": "value", "num": 42});
426        let decoded = roundtrip!(AsJson, Value, v.clone());
427        assert_eq!(decoded, v);
428    }
429
430    #[test]
431    fn as_json_value_roundtrips_null() {
432        let decoded = roundtrip!(AsJson, Value, Value::Null);
433        assert_eq!(decoded, Value::Null);
434    }
435
436    #[test]
437    fn as_json_btreemap_roundtrips() {
438        let mut m = BTreeMap::new();
439        m.insert("x".to_string(), json!(1));
440        m.insert("y".to_string(), json!("hello"));
441        let decoded = roundtrip!(AsJson, BTreeMap<String, Value>, m.clone());
442        assert_eq!(decoded, m);
443    }
444
445    #[test]
446    fn as_json_vec_value_roundtrips() {
447        let v = vec![json!(1), json!("two"), json!(null)];
448        let decoded = roundtrip!(AsJson, Vec<Value>, v.clone());
449        assert_eq!(decoded, v);
450    }
451
452    #[test]
453    fn as_json_option_value_some_roundtrips() {
454        let v: Option<Value> = Some(json!({"a": true}));
455        let decoded = roundtrip!(AsJson, Option<Value>, v.clone());
456        assert_eq!(decoded, v);
457    }
458
459    #[test]
460    fn as_json_option_value_none_roundtrips() {
461        let v: Option<Value> = None;
462        let decoded = roundtrip!(AsJson, Option<Value>, v);
463        assert_eq!(decoded, None);
464    }
465
466    #[test]
467    fn as_json_option_btreemap_some_roundtrips() {
468        let mut m = BTreeMap::new();
469        m.insert("k".to_string(), json!(99));
470        let v: Option<BTreeMap<String, Value>> = Some(m.clone());
471        let decoded = roundtrip!(AsJson, Option<BTreeMap<String, Value>>, v);
472        assert_eq!(decoded, Some(m));
473    }
474
475    #[test]
476    fn as_json_option_btreemap_none_roundtrips() {
477        let v: Option<BTreeMap<String, Value>> = None;
478        let decoded = roundtrip!(AsJson, Option<BTreeMap<String, Value>>, v);
479        assert_eq!(decoded, None);
480    }
481
482    #[test]
483    fn as_json_option_vec_value_some_roundtrips() {
484        let v: Option<Vec<Value>> = Some(vec![json!(1), json!(2)]);
485        let decoded = roundtrip!(AsJson, Option<Vec<Value>>, v.clone());
486        assert_eq!(decoded, v);
487    }
488
489    #[test]
490    fn as_json_option_vec_value_none_roundtrips() {
491        let v: Option<Vec<Value>> = None;
492        let decoded = roundtrip!(AsJson, Option<Vec<Value>>, v);
493        assert_eq!(decoded, None);
494    }
495
496    #[test]
497    fn as_json_fingerprint_roundtrips() {
498        let fp = vec![
499            (PathBuf::from("/tmp/a.hcl"), 1_700_000_000u64),
500            (PathBuf::from("/home/user/b.hcl"), 999u64),
501        ];
502        let decoded = roundtrip!(AsJson, Vec<(PathBuf, u64)>, fp.clone());
503        assert_eq!(decoded, fp);
504    }
505}