Skip to main content

snapbox/data/
mod.rs

1//! `actual` and `expected` [`Data`] for testing code
2
3mod filters;
4mod format;
5mod runtime;
6mod source;
7#[cfg(test)]
8mod tests;
9
10pub use format::DataFormat;
11pub use source::DataSource;
12pub use source::Inline;
13#[doc(hidden)]
14pub use source::Position;
15
16use filters::FilterSet;
17
18/// Capture the pretty debug representation of a value
19///
20/// Note: this is fairly brittle as debug representations are not generally subject to semver
21/// guarantees.
22///
23/// ```rust,no_run
24/// use snapbox::ToDebug as _;
25///
26/// fn some_function() -> usize {
27///     // ...
28/// # 5
29/// }
30///
31/// let actual = some_function();
32/// let expected = snapbox::str![["5"]];
33/// snapbox::assert_data_eq!(actual.to_debug(), expected);
34/// ```
35pub trait ToDebug {
36    fn to_debug(&self) -> Data;
37}
38
39impl<D: std::fmt::Debug> ToDebug for D {
40    fn to_debug(&self) -> Data {
41        Data::text(format!("{self:#?}\n"))
42    }
43}
44
45/// Capture the serde representation of a value
46///
47/// # Examples
48///
49/// ```rust,no_run
50/// use snapbox::IntoJson as _;
51///
52/// fn some_function() -> usize {
53///     // ...
54/// # 5
55/// }
56///
57/// let actual = some_function();
58/// let expected = snapbox::str![["5"]];
59/// snapbox::assert_data_eq!(actual.into_json(), expected);
60/// ```
61#[cfg(feature = "json")]
62pub trait IntoJson {
63    fn into_json(self) -> Data;
64}
65
66#[cfg(feature = "json")]
67impl<S: serde::Serialize> IntoJson for S {
68    fn into_json(self) -> Data {
69        match serde_json::to_value(self) {
70            Ok(value) => Data::json(value),
71            Err(err) => Data::error(err.to_string(), DataFormat::Json),
72        }
73    }
74}
75
76/// Convert to [`Data`] with modifiers for `expected` data
77#[allow(clippy::wrong_self_convention)]
78pub trait IntoData: Sized {
79    /// Remove default [`filters`][crate::filter] from this `expected` result
80    fn raw(self) -> Data {
81        self.into_data().raw()
82    }
83
84    /// Treat lines and json arrays as unordered
85    ///
86    /// # Examples
87    ///
88    /// ```rust
89    /// # #[cfg(feature = "json")] {
90    /// use snapbox::prelude::*;
91    /// use snapbox::str;
92    /// use snapbox::assert_data_eq;
93    ///
94    /// let actual = str![[r#"["world", "hello"]"#]]
95    ///     .is(snapbox::data::DataFormat::Json)
96    ///     .unordered();
97    /// let expected = str![[r#"["hello", "world"]"#]]
98    ///     .is(snapbox::data::DataFormat::Json)
99    ///     .unordered();
100    /// assert_data_eq!(actual, expected);
101    /// # }
102    /// ```
103    fn unordered(self) -> Data {
104        self.into_data().unordered()
105    }
106
107    /// Initialize as [`format`][DataFormat] or [`Error`][DataFormat::Error]
108    ///
109    /// This is generally used for `expected` data
110    ///
111    /// # Examples
112    ///
113    /// ```rust
114    /// # #[cfg(feature = "json")] {
115    /// use snapbox::prelude::*;
116    /// use snapbox::str;
117    ///
118    /// let expected = str![[r#"{"hello": "world"}"#]]
119    ///     .is(snapbox::data::DataFormat::Json);
120    /// assert_eq!(expected.format(), snapbox::data::DataFormat::Json);
121    /// # }
122    /// ```
123    fn is(self, format: DataFormat) -> Data {
124        self.into_data().is(format)
125    }
126
127    /// Initialize as json or [`Error`][DataFormat::Error]
128    ///
129    /// This is generally used for `expected` data
130    ///
131    /// # Examples
132    ///
133    /// ```rust
134    /// # #[cfg(feature = "json")] {
135    /// use snapbox::prelude::*;
136    /// use snapbox::str;
137    ///
138    /// let expected = str![[r#"{"hello": "world"}"#]]
139    ///     .is_json();
140    /// assert_eq!(expected.format(), snapbox::data::DataFormat::Json);
141    /// # }
142    /// ```
143    #[cfg(feature = "json")]
144    fn is_json(self) -> Data {
145        self.is(DataFormat::Json)
146    }
147
148    #[cfg(feature = "json")]
149    #[deprecated(since = "0.6.13", note = "Replaced with `IntoData::is_json`")]
150    fn json(self) -> Data {
151        self.is_json()
152    }
153
154    /// Initialize as json lines or [`Error`][DataFormat::Error]
155    ///
156    /// This is generally used for `expected` data
157    ///
158    /// # Examples
159    ///
160    /// ```rust
161    /// # #[cfg(feature = "json")] {
162    /// use snapbox::prelude::*;
163    /// use snapbox::str;
164    ///
165    /// let expected = str![[r#"{"hello": "world"}"#]]
166    ///     .is_jsonlines();
167    /// assert_eq!(expected.format(), snapbox::data::DataFormat::JsonLines);
168    /// # }
169    /// ```
170    #[cfg(feature = "json")]
171    fn is_jsonlines(self) -> Data {
172        self.is(DataFormat::JsonLines)
173    }
174
175    #[cfg(feature = "json")]
176    #[deprecated(since = "0.6.13", note = "Replaced with `IntoData::is_jsonlines`")]
177    fn json_lines(self) -> Data {
178        self.is_jsonlines()
179    }
180
181    /// Initialize as Term SVG
182    ///
183    /// This is generally used for `expected` data
184    #[cfg(feature = "term-svg")]
185    fn is_termsvg(self) -> Data {
186        self.is(DataFormat::TermSvg)
187    }
188
189    #[cfg(feature = "term-svg")]
190    #[deprecated(since = "0.6.13", note = "Replaced with `IntoData::is_termsvg`")]
191    fn term_svg(self) -> Data {
192        self.is_termsvg()
193    }
194
195    /// Override the type this snapshot will be compared against
196    ///
197    /// Normally, the `actual` data is coerced to [`IntoData::is`].
198    /// This allows overriding that so you can store your snapshot in a more readable, diffable
199    /// format.
200    ///
201    /// # Examples
202    ///
203    /// ```rust
204    /// # #[cfg(feature = "json")] {
205    /// use snapbox::prelude::*;
206    /// use snapbox::str;
207    ///
208    /// let expected = str![[r#"{"hello": "world"}"#]]
209    ///     .against(snapbox::data::DataFormat::JsonLines);
210    /// # }
211    /// ```
212    fn against(self, format: DataFormat) -> Data {
213        self.into_data().against(format)
214    }
215
216    /// Initialize as json or [`Error`][DataFormat::Error]
217    ///
218    /// This is generally used for `expected` data
219    ///
220    /// # Examples
221    ///
222    /// ```rust
223    /// # #[cfg(feature = "json")] {
224    /// use snapbox::prelude::*;
225    /// use snapbox::str;
226    ///
227    /// let expected = str![[r#"{"hello": "world"}"#]]
228    ///     .is_json();
229    /// # }
230    /// ```
231    #[cfg(feature = "json")]
232    fn against_json(self) -> Data {
233        self.against(DataFormat::Json)
234    }
235
236    /// Initialize as json lines or [`Error`][DataFormat::Error]
237    ///
238    /// This is generally used for `expected` data
239    ///
240    /// # Examples
241    ///
242    /// ```rust
243    /// # #[cfg(feature = "json")] {
244    /// use snapbox::prelude::*;
245    /// use snapbox::str;
246    ///
247    /// let expected = str![[r#"{"hello": "world"}"#]]
248    ///     .against_jsonlines();
249    /// # }
250    /// ```
251    #[cfg(feature = "json")]
252    fn against_jsonlines(self) -> Data {
253        self.against(DataFormat::JsonLines)
254    }
255
256    /// Convert to [`Data`], applying defaults
257    fn into_data(self) -> Data;
258}
259
260impl IntoData for Data {
261    fn into_data(self) -> Data {
262        self
263    }
264}
265
266impl IntoData for &'_ Data {
267    fn into_data(self) -> Data {
268        self.clone()
269    }
270}
271
272impl IntoData for Vec<u8> {
273    fn into_data(self) -> Data {
274        Data::binary(self)
275    }
276}
277
278impl IntoData for &'_ [u8] {
279    fn into_data(self) -> Data {
280        self.to_owned().into_data()
281    }
282}
283
284impl IntoData for String {
285    fn into_data(self) -> Data {
286        Data::text(self)
287    }
288}
289
290impl IntoData for &'_ String {
291    fn into_data(self) -> Data {
292        self.to_owned().into_data()
293    }
294}
295
296impl IntoData for &'_ str {
297    fn into_data(self) -> Data {
298        self.to_owned().into_data()
299    }
300}
301
302impl IntoData for Inline {
303    fn into_data(self) -> Data {
304        let trimmed = self.trimmed();
305        Data::text(trimmed).with_source(self)
306    }
307}
308
309/// Declare an expected value for an assert from a file
310///
311/// This is relative to the source file the macro is run from
312///
313/// Output type: [`Data`]
314///
315/// ```
316/// # #[cfg(feature = "json")] {
317/// # use snapbox::file;
318/// file!["./test_data/bar.json"];
319/// file!["./test_data/bar.json": Text];  // do textual rather than structural comparisons
320/// file![_];
321/// file![_: Json];  // ensure its treated as json since a type can't be inferred
322/// # }
323/// ```
324#[macro_export]
325macro_rules! file {
326    [_] => {{
327        let path = $crate::data::generate_snapshot_path($crate::fn_path!(), None);
328        $crate::Data::read_from(&path, None)
329    }};
330    [_ : $type:ident] => {{
331        let format = $crate::data::DataFormat:: $type;
332        let path = $crate::data::generate_snapshot_path($crate::fn_path!(), Some(format));
333        $crate::Data::read_from(&path, Some($crate::data::DataFormat:: $type))
334    }};
335    [$path:literal] => {{
336        let mut path = $crate::utils::current_dir!();
337        path.push($path);
338        $crate::Data::read_from(&path, None)
339    }};
340    [$path:literal : $type:ident] => {{
341        let mut path = $crate::utils::current_dir!();
342        path.push($path);
343        $crate::Data::read_from(&path, Some($crate::data::DataFormat:: $type))
344    }};
345}
346
347/// Declare an expected value from within Rust source
348///
349/// Output type: [`Inline`], see [`IntoData`] for operations
350///
351/// ```
352/// # use snapbox::str;
353/// str![["
354///     Foo { value: 92 }
355/// "]];
356/// str![r#"{"Foo": 92}"#];
357/// ```
358#[macro_export]
359macro_rules! str {
360    [$data:literal] => { $crate::str![[$data]] };
361    [[$data:literal]] => {{
362        let position = $crate::data::Position {
363            file: $crate::utils::current_rs!(),
364            line: line!(),
365            column: column!(),
366        };
367        let inline = $crate::data::Inline {
368            position,
369            data: $data,
370        };
371        inline
372    }};
373    [] => { $crate::str![[""]] };
374    [[]] => { $crate::str![[""]] };
375}
376
377/// Test fixture, actual output, or expected result
378///
379/// This provides conveniences for tracking the intended format (binary vs text).
380#[derive(Clone, Debug)]
381pub struct Data {
382    pub(crate) inner: Box<DataInner>,
383}
384
385#[derive(Clone, Debug)]
386pub(crate) struct DataInner {
387    pub(crate) value: DataValue,
388    pub(crate) source: Option<DataSource>,
389    pub(crate) filters: FilterSet,
390}
391
392#[derive(Clone, Debug)]
393pub(crate) enum DataValue {
394    Error(DataError),
395    Binary(Vec<u8>),
396    Text(String),
397    #[cfg(feature = "json")]
398    Json(serde_json::Value),
399    // Always a `Value::Array` but using `Value` for easier bookkeeping
400    #[cfg(feature = "json")]
401    JsonLines(serde_json::Value),
402    #[cfg(feature = "term-svg")]
403    TermSvg(String),
404}
405
406/// # Constructors
407///
408/// See also
409/// - [`str!`] for inline snapshots
410/// - [`file!`] for external snapshots
411/// - [`ToString`] for verifying a `Display` representation
412/// - [`ToDebug`] for verifying a debug representation
413/// - [`IntoJson`] for verifying the serde representation
414/// - [`IntoData`] for modifying `expected`
415impl Data {
416    /// Mark the data as binary (no post-processing)
417    pub fn binary(raw: impl Into<Vec<u8>>) -> Self {
418        Self::with_value(DataValue::Binary(raw.into()))
419    }
420
421    /// Mark the data as text (post-processing)
422    pub fn text(raw: impl Into<String>) -> Self {
423        Self::with_value(DataValue::Text(raw.into()))
424    }
425
426    #[cfg(feature = "json")]
427    pub fn json(raw: impl Into<serde_json::Value>) -> Self {
428        Self::with_value(DataValue::Json(raw.into()))
429    }
430
431    #[cfg(feature = "json")]
432    pub fn jsonlines(raw: impl Into<Vec<serde_json::Value>>) -> Self {
433        Self::with_value(DataValue::JsonLines(serde_json::Value::Array(raw.into())))
434    }
435
436    fn error(raw: impl Into<crate::assert::Error>, intended: DataFormat) -> Self {
437        Self::with_value(DataValue::Error(DataError {
438            error: raw.into(),
439            intended,
440        }))
441    }
442
443    /// Empty test data
444    pub fn new() -> Self {
445        Self::text("")
446    }
447
448    /// Load `expected` data from a file
449    pub fn read_from(path: &std::path::Path, data_format: Option<DataFormat>) -> Self {
450        match Self::try_read_from(path, data_format) {
451            Ok(data) => data,
452            Err(err) => Self::error(err, data_format.unwrap_or_else(|| DataFormat::from(path)))
453                .with_path(path),
454        }
455    }
456
457    /// Remove default [`filters`][crate::filter] from this `expected` result
458    pub fn raw(mut self) -> Self {
459        self.inner.filters = FilterSet::empty().newlines();
460        self
461    }
462
463    /// Treat lines and json arrays as unordered
464    pub fn unordered(mut self) -> Self {
465        self.inner.filters = self.inner.filters.unordered();
466        self
467    }
468}
469
470/// # Assertion frameworks operations
471///
472/// For example, see [`OutputAssert`][crate::cmd::OutputAssert]
473impl Data {
474    pub(crate) fn with_value(value: DataValue) -> Self {
475        Self {
476            inner: Box::new(DataInner {
477                value,
478                source: None,
479                filters: FilterSet::new(),
480            }),
481        }
482    }
483
484    fn with_source(mut self, source: impl Into<DataSource>) -> Self {
485        self.inner.source = Some(source.into());
486        self
487    }
488
489    fn with_path(self, path: impl Into<std::path::PathBuf>) -> Self {
490        self.with_source(path.into())
491    }
492
493    /// Load `expected` data from a file
494    pub fn try_read_from(
495        path: &std::path::Path,
496        data_format: Option<DataFormat>,
497    ) -> crate::assert::Result<Self> {
498        let data =
499            std::fs::read(path).map_err(|e| format!("Failed to read {}: {}", path.display(), e))?;
500        let data = Self::binary(data);
501        let data = match data_format {
502            Some(df) => data.is(df),
503            None => {
504                let inferred_format = DataFormat::from(path);
505                match inferred_format {
506                    #[cfg(feature = "json")]
507                    DataFormat::Json | DataFormat::JsonLines => data.coerce_to(inferred_format),
508                    #[cfg(feature = "term-svg")]
509                    DataFormat::TermSvg => {
510                        let data = data.coerce_to(DataFormat::Text);
511                        data.is(inferred_format)
512                    }
513                    _ => data.coerce_to(DataFormat::Text),
514                }
515            }
516        };
517        Ok(data.with_path(path))
518    }
519
520    /// Overwrite a snapshot
521    pub fn write_to(&self, source: &DataSource) -> crate::assert::Result<()> {
522        match &source.inner {
523            source::DataSourceInner::Path(p) => self.write_to_path(p),
524            source::DataSourceInner::Inline(p) => runtime::get()
525                .write(self, p)
526                .map_err(|err| err.to_string().into()),
527        }
528    }
529
530    /// Overwrite a snapshot
531    pub fn write_to_path(&self, path: &std::path::Path) -> crate::assert::Result<()> {
532        if let Some(parent) = path.parent() {
533            std::fs::create_dir_all(parent).map_err(|e| {
534                format!("Failed to create parent dir for {}: {}", path.display(), e)
535            })?;
536        }
537        let bytes = self.to_bytes()?;
538        std::fs::write(path, bytes)
539            .map_err(|e| format!("Failed to write {}: {}", path.display(), e).into())
540    }
541
542    /// Return the underlying `String`
543    ///
544    /// Note: this will not inspect binary data for being a valid `String`.
545    pub fn render(&self) -> Option<String> {
546        match &self.inner.value {
547            DataValue::Error(_) => None,
548            DataValue::Binary(_) => None,
549            DataValue::Text(data) => Some(data.to_owned()),
550            #[cfg(feature = "json")]
551            DataValue::Json(_) => Some(self.to_string()),
552            #[cfg(feature = "json")]
553            DataValue::JsonLines(_) => Some(self.to_string()),
554            #[cfg(feature = "term-svg")]
555            DataValue::TermSvg(data) => Some(data.to_owned()),
556        }
557    }
558
559    pub fn to_bytes(&self) -> crate::assert::Result<Vec<u8>> {
560        match &self.inner.value {
561            DataValue::Error(err) => Err(err.error.clone()),
562            DataValue::Binary(data) => Ok(data.clone()),
563            DataValue::Text(data) => Ok(data.clone().into_bytes()),
564            #[cfg(feature = "json")]
565            DataValue::Json(_) => Ok(self.to_string().into_bytes()),
566            #[cfg(feature = "json")]
567            DataValue::JsonLines(_) => Ok(self.to_string().into_bytes()),
568            #[cfg(feature = "term-svg")]
569            DataValue::TermSvg(data) => Ok(data.clone().into_bytes()),
570        }
571    }
572
573    /// Initialize `Self` as [`format`][DataFormat] or [`Error`][DataFormat::Error]
574    ///
575    /// This is generally used for `expected` data
576    pub fn is(self, format: DataFormat) -> Self {
577        let filters = self.inner.filters;
578        let source = self.inner.source.clone();
579        match self.try_is(format) {
580            Ok(new) => new,
581            Err(err) => {
582                let value = DataValue::Error(DataError {
583                    error: err,
584                    intended: format,
585                });
586                Self {
587                    inner: Box::new(DataInner {
588                        value,
589                        source,
590                        filters,
591                    }),
592                }
593            }
594        }
595    }
596
597    fn try_is(self, format: DataFormat) -> crate::assert::Result<Self> {
598        let original = self.format();
599        let source = self.inner.source;
600        let filters = self.inner.filters;
601        let value = match (self.inner.value, format) {
602            (DataValue::Error(inner), _) => DataValue::Error(inner),
603            (DataValue::Binary(inner), DataFormat::Binary) => DataValue::Binary(inner),
604            (DataValue::Text(inner), DataFormat::Text) => DataValue::Text(inner),
605            #[cfg(feature = "json")]
606            (DataValue::Json(inner), DataFormat::Json) => DataValue::Json(inner),
607            #[cfg(feature = "json")]
608            (DataValue::JsonLines(inner), DataFormat::JsonLines) => DataValue::JsonLines(inner),
609            #[cfg(feature = "term-svg")]
610            (DataValue::TermSvg(inner), DataFormat::TermSvg) => DataValue::TermSvg(inner),
611            (DataValue::Binary(inner), _) => {
612                let value = String::from_utf8(inner).map_err(|_err| "invalid UTF-8".to_owned())?;
613                Self::text(value).try_is(format)?.inner.value
614            }
615            #[cfg(feature = "json")]
616            (DataValue::Text(inner), DataFormat::Json) => {
617                let value = serde_json::from_str::<serde_json::Value>(&inner)
618                    .map_err(|err| err.to_string())?;
619                DataValue::Json(value)
620            }
621            #[cfg(feature = "json")]
622            (DataValue::Text(inner), DataFormat::JsonLines) => {
623                let value = parse_jsonlines(&inner).map_err(|err| err.to_string())?;
624                DataValue::JsonLines(serde_json::Value::Array(value))
625            }
626            #[cfg(feature = "term-svg")]
627            (DataValue::Text(inner), DataFormat::TermSvg) => DataValue::TermSvg(inner),
628            (value, DataFormat::Binary) => {
629                let remake = Self::with_value(value);
630                DataValue::Binary(remake.to_bytes().expect("error case handled"))
631            }
632            // This variant is already covered unless structured data is enabled
633            #[cfg(feature = "structured-data")]
634            (value, DataFormat::Text) => {
635                if let Some(str) = Self::with_value(value).render() {
636                    DataValue::Text(str)
637                } else {
638                    return Err(format!("cannot convert {original:?} to {format:?}").into());
639                }
640            }
641            (_, _) => return Err(format!("cannot convert {original:?} to {format:?}").into()),
642        };
643        Ok(Self {
644            inner: Box::new(DataInner {
645                value,
646                source,
647                filters,
648            }),
649        })
650    }
651
652    /// Override the type this snapshot will be compared against
653    ///
654    /// Normally, the `actual` data is coerced to [`Data::is`].
655    /// This allows overriding that so you can store your snapshot in a more readable, diffable
656    /// format.
657    ///
658    /// # Examples
659    ///
660    /// ```rust
661    /// # #[cfg(feature = "json")] {
662    /// use snapbox::prelude::*;
663    /// use snapbox::str;
664    ///
665    /// let expected = str![[r#"{"hello": "world"}"#]]
666    ///     .is(snapbox::data::DataFormat::Json)
667    ///     .against(snapbox::data::DataFormat::JsonLines);
668    /// # }
669    /// ```
670    fn against(mut self, format: DataFormat) -> Data {
671        self.inner.filters = self.inner.filters.against(format);
672        self
673    }
674
675    /// Convert `Self` to [`format`][DataFormat] if possible
676    ///
677    /// This is generally used on `actual` data to make it match `expected`
678    pub fn coerce_to(self, format: DataFormat) -> Self {
679        let source = self.inner.source;
680        let filters = self.inner.filters;
681        let value = match (self.inner.value, format) {
682            (DataValue::Error(inner), _) => DataValue::Error(inner),
683            (value, DataFormat::Error) => value,
684            (DataValue::Binary(inner), DataFormat::Binary) => DataValue::Binary(inner),
685            (DataValue::Text(inner), DataFormat::Text) => DataValue::Text(inner),
686            #[cfg(feature = "json")]
687            (DataValue::Json(inner), DataFormat::Json) => DataValue::Json(inner),
688            #[cfg(feature = "json")]
689            (DataValue::JsonLines(inner), DataFormat::JsonLines) => DataValue::JsonLines(inner),
690            #[cfg(feature = "json")]
691            (DataValue::JsonLines(inner), DataFormat::Json) => DataValue::Json(inner),
692            #[cfg(feature = "json")]
693            (DataValue::Json(inner), DataFormat::JsonLines) => DataValue::JsonLines(inner),
694            #[cfg(feature = "term-svg")]
695            (DataValue::TermSvg(inner), DataFormat::TermSvg) => DataValue::TermSvg(inner),
696            (DataValue::Binary(inner), _) => {
697                if is_binary(&inner) {
698                    DataValue::Binary(inner)
699                } else {
700                    match String::from_utf8(inner) {
701                        Ok(str) => {
702                            let coerced = Self::text(str).coerce_to(format);
703                            // if the Text cannot be coerced into the correct format
704                            // reset it back to Binary
705                            let coerced = if coerced.format() != format {
706                                coerced.coerce_to(DataFormat::Binary)
707                            } else {
708                                coerced
709                            };
710                            coerced.inner.value
711                        }
712                        Err(err) => {
713                            let bin = err.into_bytes();
714                            DataValue::Binary(bin)
715                        }
716                    }
717                }
718            }
719            #[cfg(feature = "json")]
720            (DataValue::Text(inner), DataFormat::Json) => {
721                if let Ok(json) = serde_json::from_str::<serde_json::Value>(&inner) {
722                    DataValue::Json(json)
723                } else {
724                    DataValue::Text(inner)
725                }
726            }
727            #[cfg(feature = "json")]
728            (DataValue::Text(inner), DataFormat::JsonLines) => {
729                if let Ok(jsonlines) = parse_jsonlines(&inner) {
730                    DataValue::JsonLines(serde_json::Value::Array(jsonlines))
731                } else {
732                    DataValue::Text(inner)
733                }
734            }
735            #[cfg(feature = "term-svg")]
736            (DataValue::Text(inner), DataFormat::TermSvg) => {
737                DataValue::TermSvg(anstyle_svg::Term::new().render_svg(&inner))
738            }
739            (value, DataFormat::Binary) => {
740                let remake = Self::with_value(value);
741                DataValue::Binary(remake.to_bytes().expect("error case handled"))
742            }
743            // This variant is already covered unless structured data is enabled
744            #[cfg(feature = "structured-data")]
745            (value, DataFormat::Text) => {
746                let remake = Self::with_value(value);
747                if let Some(str) = remake.render() {
748                    DataValue::Text(str)
749                } else {
750                    remake.inner.value
751                }
752            }
753            // reachable if more than one structured data format is enabled
754            #[allow(unreachable_patterns)]
755            #[cfg(feature = "json")]
756            (value, DataFormat::Json) => value,
757            // reachable if more than one structured data format is enabled
758            #[allow(unreachable_patterns)]
759            #[cfg(feature = "json")]
760            (value, DataFormat::JsonLines) => value,
761            // reachable if more than one structured data format is enabled
762            #[allow(unreachable_patterns)]
763            #[cfg(feature = "term-svg")]
764            (value, DataFormat::TermSvg) => value,
765        };
766        Self {
767            inner: Box::new(DataInner {
768                value,
769                source,
770                filters,
771            }),
772        }
773    }
774
775    /// Location the data came from
776    pub fn source(&self) -> Option<&DataSource> {
777        self.inner.source.as_ref()
778    }
779
780    /// Outputs the current `DataFormat` of the underlying data
781    pub fn format(&self) -> DataFormat {
782        match &self.inner.value {
783            DataValue::Error(_) => DataFormat::Error,
784            DataValue::Binary(_) => DataFormat::Binary,
785            DataValue::Text(_) => DataFormat::Text,
786            #[cfg(feature = "json")]
787            DataValue::Json(_) => DataFormat::Json,
788            #[cfg(feature = "json")]
789            DataValue::JsonLines(_) => DataFormat::JsonLines,
790            #[cfg(feature = "term-svg")]
791            DataValue::TermSvg(_) => DataFormat::TermSvg,
792        }
793    }
794
795    pub(crate) fn intended_format(&self) -> DataFormat {
796        match &self.inner.value {
797            DataValue::Error(DataError { intended, .. }) => *intended,
798            DataValue::Binary(_) => DataFormat::Binary,
799            DataValue::Text(_) => DataFormat::Text,
800            #[cfg(feature = "json")]
801            DataValue::Json(_) => DataFormat::Json,
802            #[cfg(feature = "json")]
803            DataValue::JsonLines(_) => DataFormat::JsonLines,
804            #[cfg(feature = "term-svg")]
805            DataValue::TermSvg(_) => DataFormat::TermSvg,
806        }
807    }
808
809    pub(crate) fn against_format(&self) -> DataFormat {
810        self.inner
811            .filters
812            .get_against()
813            .unwrap_or_else(|| self.intended_format())
814    }
815
816    pub(crate) fn relevant(&self) -> Option<&str> {
817        match &self.inner.value {
818            DataValue::Error(_) => None,
819            DataValue::Binary(_) => None,
820            DataValue::Text(_) => None,
821            #[cfg(feature = "json")]
822            DataValue::Json(_) => None,
823            #[cfg(feature = "json")]
824            DataValue::JsonLines(_) => None,
825            #[cfg(feature = "term-svg")]
826            DataValue::TermSvg(data) => term_svg_body(data),
827        }
828    }
829}
830
831impl std::fmt::Display for Data {
832    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
833        match &self.inner.value {
834            DataValue::Error(data) => data.fmt(f),
835            DataValue::Binary(data) => String::from_utf8_lossy(data).fmt(f),
836            DataValue::Text(data) => data.fmt(f),
837            #[cfg(feature = "json")]
838            DataValue::Json(data) => serde_json::to_string_pretty(data).unwrap().fmt(f),
839            #[cfg(feature = "json")]
840            DataValue::JsonLines(data) => {
841                let array = data.as_array().expect("jsonlines is always an array");
842                for value in array {
843                    writeln!(f, "{}", serde_json::to_string(value).unwrap())?;
844                }
845                Ok(())
846            }
847            #[cfg(feature = "term-svg")]
848            DataValue::TermSvg(data) => data.fmt(f),
849        }
850    }
851}
852
853impl PartialEq for Data {
854    fn eq(&self, other: &Data) -> bool {
855        match (&self.inner.value, &other.inner.value) {
856            (DataValue::Error(left), DataValue::Error(right)) => left == right,
857            (DataValue::Binary(left), DataValue::Binary(right)) => left == right,
858            (DataValue::Text(left), DataValue::Text(right)) => left == right,
859            #[cfg(feature = "json")]
860            (DataValue::Json(left), DataValue::Json(right)) => left == right,
861            #[cfg(feature = "json")]
862            (DataValue::JsonLines(left), DataValue::JsonLines(right)) => left == right,
863            #[cfg(feature = "term-svg")]
864            (DataValue::TermSvg(left), DataValue::TermSvg(right)) => {
865                // HACK: avoid including `width` and `height` in the comparison
866                let left = term_svg_body(left.as_str()).unwrap_or(left.as_str());
867                let right = term_svg_body(right.as_str()).unwrap_or(right.as_str());
868                left == right
869            }
870            (_, _) => false,
871        }
872    }
873}
874
875#[derive(Clone, Debug, PartialEq, Eq)]
876pub(crate) struct DataError {
877    error: crate::assert::Error,
878    intended: DataFormat,
879}
880
881impl std::fmt::Display for DataError {
882    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
883        self.error.fmt(f)
884    }
885}
886
887#[cfg(feature = "json")]
888fn parse_jsonlines(text: &str) -> Result<Vec<serde_json::Value>, serde_json::Error> {
889    let mut lines = Vec::new();
890    for line in text.lines() {
891        let line = line.trim();
892        if line.is_empty() {
893            continue;
894        }
895        let json = serde_json::from_str::<serde_json::Value>(line)?;
896        lines.push(json);
897    }
898    Ok(lines)
899}
900
901#[cfg(feature = "term-svg")]
902fn term_svg_body(svg: &str) -> Option<&str> {
903    let (_header, body, _footer) = split_term_svg(svg)?;
904    Some(body)
905}
906
907#[cfg(feature = "term-svg")]
908pub(crate) fn split_term_svg(svg: &str) -> Option<(&str, &str, &str)> {
909    let open_elem_start_idx = svg.find("<text")?;
910    _ = svg[open_elem_start_idx..].find('>')?;
911    let open_elem_line_start_idx = svg[..open_elem_start_idx]
912        .rfind('\n')
913        .map(|idx| idx + 1)
914        .unwrap_or(svg.len());
915
916    let close_elem = "</text>";
917    let close_elem_start_idx = svg.rfind(close_elem).unwrap_or(svg.len());
918    let close_elem_line_end_idx = svg[close_elem_start_idx..]
919        .find('\n')
920        .map(|idx| idx + close_elem_start_idx + 1)
921        .unwrap_or(svg.len());
922
923    let header = &svg[..open_elem_line_start_idx];
924    let body = &svg[open_elem_line_start_idx..close_elem_line_end_idx];
925    let footer = &svg[close_elem_line_end_idx..];
926    Some((header, body, footer))
927}
928
929impl Eq for Data {}
930
931impl Default for Data {
932    fn default() -> Self {
933        Self::new()
934    }
935}
936
937impl<'d> From<&'d Data> for Data {
938    fn from(other: &'d Data) -> Self {
939        other.into_data()
940    }
941}
942
943impl From<Vec<u8>> for Data {
944    fn from(other: Vec<u8>) -> Self {
945        other.into_data()
946    }
947}
948
949impl<'b> From<&'b [u8]> for Data {
950    fn from(other: &'b [u8]) -> Self {
951        other.into_data()
952    }
953}
954
955impl From<String> for Data {
956    fn from(other: String) -> Self {
957        other.into_data()
958    }
959}
960
961impl<'s> From<&'s String> for Data {
962    fn from(other: &'s String) -> Self {
963        other.into_data()
964    }
965}
966
967impl<'s> From<&'s str> for Data {
968    fn from(other: &'s str) -> Self {
969        other.into_data()
970    }
971}
972
973impl From<Inline> for Data {
974    fn from(other: Inline) -> Self {
975        other.into_data()
976    }
977}
978
979#[cfg(feature = "detect-encoding")]
980fn is_binary(data: &[u8]) -> bool {
981    match content_inspector::inspect(data) {
982        content_inspector::ContentType::BINARY |
983        // We don't support these
984        content_inspector::ContentType::UTF_16LE |
985        content_inspector::ContentType::UTF_16BE |
986        content_inspector::ContentType::UTF_32LE |
987        content_inspector::ContentType::UTF_32BE => {
988            true
989        },
990        content_inspector::ContentType::UTF_8 |
991        content_inspector::ContentType::UTF_8_BOM => {
992            false
993        },
994    }
995}
996
997#[cfg(not(feature = "detect-encoding"))]
998fn is_binary(_data: &[u8]) -> bool {
999    false
1000}
1001
1002#[doc(hidden)]
1003pub fn generate_snapshot_path(fn_path: &str, format: Option<DataFormat>) -> std::path::PathBuf {
1004    use std::fmt::Write as _;
1005
1006    let fn_path_normalized = fn_path.replace("::", "__");
1007    let mut path = format!("tests/snapshots/{fn_path_normalized}");
1008    let count = runtime::get().count(&path);
1009    if 0 < count {
1010        write!(&mut path, "@{count}").unwrap();
1011    }
1012    path.push('.');
1013    path.push_str(format.unwrap_or(DataFormat::Text).ext());
1014    path.into()
1015}
1016
1017#[cfg(test)]
1018mod test {
1019    use super::*;
1020
1021    #[track_caller]
1022    fn validate_cases(cases: &[(&str, bool)], input_format: DataFormat) {
1023        for (input, valid) in cases.iter().copied() {
1024            let (expected_is_format, expected_coerced_format) = if valid {
1025                (input_format, input_format)
1026            } else {
1027                (DataFormat::Error, DataFormat::Text)
1028            };
1029
1030            let actual_is = Data::text(input).is(input_format);
1031            assert_eq!(
1032                actual_is.format(),
1033                expected_is_format,
1034                "\n{input}\n{actual_is}"
1035            );
1036
1037            let actual_coerced = Data::text(input).coerce_to(input_format);
1038            assert_eq!(
1039                actual_coerced.format(),
1040                expected_coerced_format,
1041                "\n{input}\n{actual_coerced}"
1042            );
1043
1044            if valid {
1045                assert_eq!(actual_is, actual_coerced);
1046
1047                let rendered = actual_is.render().unwrap();
1048                let bytes = actual_is.to_bytes().unwrap();
1049                assert_eq!(rendered, std::str::from_utf8(&bytes).unwrap());
1050
1051                assert_eq!(Data::text(&rendered).is(input_format), actual_is);
1052            }
1053        }
1054    }
1055
1056    #[test]
1057    fn text() {
1058        let cases = [("", true), ("good", true), ("{}", true), ("\"\"", true)];
1059        validate_cases(&cases, DataFormat::Text);
1060    }
1061
1062    #[cfg(feature = "json")]
1063    #[test]
1064    fn json() {
1065        let cases = [("", false), ("bad", false), ("{}", true), ("\"\"", true)];
1066        validate_cases(&cases, DataFormat::Json);
1067    }
1068
1069    #[cfg(feature = "json")]
1070    #[test]
1071    fn jsonlines() {
1072        let cases = [
1073            ("", true),
1074            ("bad", false),
1075            ("{}", true),
1076            ("\"\"", true),
1077            (
1078                "
1079{}
1080{}
1081", true,
1082            ),
1083            (
1084                "
1085{}
1086
1087{}
1088", true,
1089            ),
1090            (
1091                "
1092{}
1093bad
1094{}
1095",
1096                false,
1097            ),
1098        ];
1099        validate_cases(&cases, DataFormat::JsonLines);
1100    }
1101}