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: DataInner,
383    pub(crate) source: Option<DataSource>,
384    pub(crate) filters: FilterSet,
385}
386
387#[derive(Clone, Debug)]
388pub(crate) enum DataInner {
389    Error(DataError),
390    Binary(Vec<u8>),
391    Text(String),
392    #[cfg(feature = "json")]
393    Json(serde_json::Value),
394    // Always a `Value::Array` but using `Value` for easier bookkeeping
395    #[cfg(feature = "json")]
396    JsonLines(serde_json::Value),
397    #[cfg(feature = "term-svg")]
398    TermSvg(String),
399}
400
401/// # Constructors
402///
403/// See also
404/// - [`str!`] for inline snapshots
405/// - [`file!`] for external snapshots
406/// - [`ToString`] for verifying a `Display` representation
407/// - [`ToDebug`] for verifying a debug representation
408/// - [`IntoJson`] for verifying the serde representation
409/// - [`IntoData`] for modifying `expected`
410impl Data {
411    /// Mark the data as binary (no post-processing)
412    pub fn binary(raw: impl Into<Vec<u8>>) -> Self {
413        Self::with_inner(DataInner::Binary(raw.into()))
414    }
415
416    /// Mark the data as text (post-processing)
417    pub fn text(raw: impl Into<String>) -> Self {
418        Self::with_inner(DataInner::Text(raw.into()))
419    }
420
421    #[cfg(feature = "json")]
422    pub fn json(raw: impl Into<serde_json::Value>) -> Self {
423        Self::with_inner(DataInner::Json(raw.into()))
424    }
425
426    #[cfg(feature = "json")]
427    pub fn jsonlines(raw: impl Into<Vec<serde_json::Value>>) -> Self {
428        Self::with_inner(DataInner::JsonLines(serde_json::Value::Array(raw.into())))
429    }
430
431    fn error(raw: impl Into<crate::assert::Error>, intended: DataFormat) -> Self {
432        Self::with_inner(DataInner::Error(DataError {
433            error: raw.into(),
434            intended,
435        }))
436    }
437
438    /// Empty test data
439    pub fn new() -> Self {
440        Self::text("")
441    }
442
443    /// Load `expected` data from a file
444    pub fn read_from(path: &std::path::Path, data_format: Option<DataFormat>) -> Self {
445        match Self::try_read_from(path, data_format) {
446            Ok(data) => data,
447            Err(err) => Self::error(err, data_format.unwrap_or_else(|| DataFormat::from(path)))
448                .with_path(path),
449        }
450    }
451
452    /// Remove default [`filters`][crate::filter] from this `expected` result
453    pub fn raw(mut self) -> Self {
454        self.filters = FilterSet::empty().newlines();
455        self
456    }
457
458    /// Treat lines and json arrays as unordered
459    pub fn unordered(mut self) -> Self {
460        self.filters = self.filters.unordered();
461        self
462    }
463}
464
465/// # Assertion frameworks operations
466///
467/// For example, see [`OutputAssert`][crate::cmd::OutputAssert]
468impl Data {
469    pub(crate) fn with_inner(inner: DataInner) -> Self {
470        Self {
471            inner,
472            source: None,
473            filters: FilterSet::new(),
474        }
475    }
476
477    fn with_source(mut self, source: impl Into<DataSource>) -> Self {
478        self.source = Some(source.into());
479        self
480    }
481
482    fn with_path(self, path: impl Into<std::path::PathBuf>) -> Self {
483        self.with_source(path.into())
484    }
485
486    /// Load `expected` data from a file
487    pub fn try_read_from(
488        path: &std::path::Path,
489        data_format: Option<DataFormat>,
490    ) -> crate::assert::Result<Self> {
491        let data =
492            std::fs::read(path).map_err(|e| format!("Failed to read {}: {}", path.display(), e))?;
493        let data = Self::binary(data);
494        let data = match data_format {
495            Some(df) => data.is(df),
496            None => {
497                let inferred_format = DataFormat::from(path);
498                match inferred_format {
499                    #[cfg(feature = "json")]
500                    DataFormat::Json | DataFormat::JsonLines => data.coerce_to(inferred_format),
501                    #[cfg(feature = "term-svg")]
502                    DataFormat::TermSvg => {
503                        let data = data.coerce_to(DataFormat::Text);
504                        data.is(inferred_format)
505                    }
506                    _ => data.coerce_to(DataFormat::Text),
507                }
508            }
509        };
510        Ok(data.with_path(path))
511    }
512
513    /// Overwrite a snapshot
514    pub fn write_to(&self, source: &DataSource) -> crate::assert::Result<()> {
515        match &source.inner {
516            source::DataSourceInner::Path(p) => self.write_to_path(p),
517            source::DataSourceInner::Inline(p) => runtime::get()
518                .write(self, p)
519                .map_err(|err| err.to_string().into()),
520        }
521    }
522
523    /// Overwrite a snapshot
524    pub fn write_to_path(&self, path: &std::path::Path) -> crate::assert::Result<()> {
525        if let Some(parent) = path.parent() {
526            std::fs::create_dir_all(parent).map_err(|e| {
527                format!("Failed to create parent dir for {}: {}", path.display(), e)
528            })?;
529        }
530        let bytes = self.to_bytes()?;
531        std::fs::write(path, bytes)
532            .map_err(|e| format!("Failed to write {}: {}", path.display(), e).into())
533    }
534
535    /// Return the underlying `String`
536    ///
537    /// Note: this will not inspect binary data for being a valid `String`.
538    pub fn render(&self) -> Option<String> {
539        match &self.inner {
540            DataInner::Error(_) => None,
541            DataInner::Binary(_) => None,
542            DataInner::Text(data) => Some(data.to_owned()),
543            #[cfg(feature = "json")]
544            DataInner::Json(_) => Some(self.to_string()),
545            #[cfg(feature = "json")]
546            DataInner::JsonLines(_) => Some(self.to_string()),
547            #[cfg(feature = "term-svg")]
548            DataInner::TermSvg(data) => Some(data.to_owned()),
549        }
550    }
551
552    pub fn to_bytes(&self) -> crate::assert::Result<Vec<u8>> {
553        match &self.inner {
554            DataInner::Error(err) => Err(err.error.clone()),
555            DataInner::Binary(data) => Ok(data.clone()),
556            DataInner::Text(data) => Ok(data.clone().into_bytes()),
557            #[cfg(feature = "json")]
558            DataInner::Json(_) => Ok(self.to_string().into_bytes()),
559            #[cfg(feature = "json")]
560            DataInner::JsonLines(_) => Ok(self.to_string().into_bytes()),
561            #[cfg(feature = "term-svg")]
562            DataInner::TermSvg(data) => Ok(data.clone().into_bytes()),
563        }
564    }
565
566    /// Initialize `Self` as [`format`][DataFormat] or [`Error`][DataFormat::Error]
567    ///
568    /// This is generally used for `expected` data
569    pub fn is(self, format: DataFormat) -> Self {
570        let filters = self.filters;
571        let source = self.source.clone();
572        match self.try_is(format) {
573            Ok(new) => new,
574            Err(err) => {
575                let inner = DataInner::Error(DataError {
576                    error: err,
577                    intended: format,
578                });
579                Self {
580                    inner,
581                    source,
582                    filters,
583                }
584            }
585        }
586    }
587
588    fn try_is(self, format: DataFormat) -> crate::assert::Result<Self> {
589        let original = self.format();
590        let source = self.source;
591        let filters = self.filters;
592        let inner = match (self.inner, format) {
593            (DataInner::Error(inner), _) => DataInner::Error(inner),
594            (DataInner::Binary(inner), DataFormat::Binary) => DataInner::Binary(inner),
595            (DataInner::Text(inner), DataFormat::Text) => DataInner::Text(inner),
596            #[cfg(feature = "json")]
597            (DataInner::Json(inner), DataFormat::Json) => DataInner::Json(inner),
598            #[cfg(feature = "json")]
599            (DataInner::JsonLines(inner), DataFormat::JsonLines) => DataInner::JsonLines(inner),
600            #[cfg(feature = "term-svg")]
601            (DataInner::TermSvg(inner), DataFormat::TermSvg) => DataInner::TermSvg(inner),
602            (DataInner::Binary(inner), _) => {
603                let inner = String::from_utf8(inner).map_err(|_err| "invalid UTF-8".to_owned())?;
604                Self::text(inner).try_is(format)?.inner
605            }
606            #[cfg(feature = "json")]
607            (DataInner::Text(inner), DataFormat::Json) => {
608                let inner = serde_json::from_str::<serde_json::Value>(&inner)
609                    .map_err(|err| err.to_string())?;
610                DataInner::Json(inner)
611            }
612            #[cfg(feature = "json")]
613            (DataInner::Text(inner), DataFormat::JsonLines) => {
614                let inner = parse_jsonlines(&inner).map_err(|err| err.to_string())?;
615                DataInner::JsonLines(serde_json::Value::Array(inner))
616            }
617            #[cfg(feature = "term-svg")]
618            (DataInner::Text(inner), DataFormat::TermSvg) => DataInner::TermSvg(inner),
619            (inner, DataFormat::Binary) => {
620                let remake = Self::with_inner(inner);
621                DataInner::Binary(remake.to_bytes().expect("error case handled"))
622            }
623            // This variant is already covered unless structured data is enabled
624            #[cfg(feature = "structured-data")]
625            (inner, DataFormat::Text) => {
626                if let Some(str) = Self::with_inner(inner).render() {
627                    DataInner::Text(str)
628                } else {
629                    return Err(format!("cannot convert {original:?} to {format:?}").into());
630                }
631            }
632            (_, _) => return Err(format!("cannot convert {original:?} to {format:?}").into()),
633        };
634        Ok(Self {
635            inner,
636            source,
637            filters,
638        })
639    }
640
641    /// Override the type this snapshot will be compared against
642    ///
643    /// Normally, the `actual` data is coerced to [`Data::is`].
644    /// This allows overriding that so you can store your snapshot in a more readable, diffable
645    /// format.
646    ///
647    /// # Examples
648    ///
649    /// ```rust
650    /// # #[cfg(feature = "json")] {
651    /// use snapbox::prelude::*;
652    /// use snapbox::str;
653    ///
654    /// let expected = str![[r#"{"hello": "world"}"#]]
655    ///     .is(snapbox::data::DataFormat::Json)
656    ///     .against(snapbox::data::DataFormat::JsonLines);
657    /// # }
658    /// ```
659    fn against(mut self, format: DataFormat) -> Data {
660        self.filters = self.filters.against(format);
661        self
662    }
663
664    /// Convert `Self` to [`format`][DataFormat] if possible
665    ///
666    /// This is generally used on `actual` data to make it match `expected`
667    pub fn coerce_to(self, format: DataFormat) -> Self {
668        let source = self.source;
669        let filters = self.filters;
670        let inner = match (self.inner, format) {
671            (DataInner::Error(inner), _) => DataInner::Error(inner),
672            (inner, DataFormat::Error) => inner,
673            (DataInner::Binary(inner), DataFormat::Binary) => DataInner::Binary(inner),
674            (DataInner::Text(inner), DataFormat::Text) => DataInner::Text(inner),
675            #[cfg(feature = "json")]
676            (DataInner::Json(inner), DataFormat::Json) => DataInner::Json(inner),
677            #[cfg(feature = "json")]
678            (DataInner::JsonLines(inner), DataFormat::JsonLines) => DataInner::JsonLines(inner),
679            #[cfg(feature = "json")]
680            (DataInner::JsonLines(inner), DataFormat::Json) => DataInner::Json(inner),
681            #[cfg(feature = "json")]
682            (DataInner::Json(inner), DataFormat::JsonLines) => DataInner::JsonLines(inner),
683            #[cfg(feature = "term-svg")]
684            (DataInner::TermSvg(inner), DataFormat::TermSvg) => DataInner::TermSvg(inner),
685            (DataInner::Binary(inner), _) => {
686                if is_binary(&inner) {
687                    DataInner::Binary(inner)
688                } else {
689                    match String::from_utf8(inner) {
690                        Ok(str) => {
691                            let coerced = Self::text(str).coerce_to(format);
692                            // if the Text cannot be coerced into the correct format
693                            // reset it back to Binary
694                            let coerced = if coerced.format() != format {
695                                coerced.coerce_to(DataFormat::Binary)
696                            } else {
697                                coerced
698                            };
699                            coerced.inner
700                        }
701                        Err(err) => {
702                            let bin = err.into_bytes();
703                            DataInner::Binary(bin)
704                        }
705                    }
706                }
707            }
708            #[cfg(feature = "json")]
709            (DataInner::Text(inner), DataFormat::Json) => {
710                if let Ok(json) = serde_json::from_str::<serde_json::Value>(&inner) {
711                    DataInner::Json(json)
712                } else {
713                    DataInner::Text(inner)
714                }
715            }
716            #[cfg(feature = "json")]
717            (DataInner::Text(inner), DataFormat::JsonLines) => {
718                if let Ok(jsonlines) = parse_jsonlines(&inner) {
719                    DataInner::JsonLines(serde_json::Value::Array(jsonlines))
720                } else {
721                    DataInner::Text(inner)
722                }
723            }
724            #[cfg(feature = "term-svg")]
725            (DataInner::Text(inner), DataFormat::TermSvg) => {
726                DataInner::TermSvg(anstyle_svg::Term::new().render_svg(&inner))
727            }
728            (inner, DataFormat::Binary) => {
729                let remake = Self::with_inner(inner);
730                DataInner::Binary(remake.to_bytes().expect("error case handled"))
731            }
732            // This variant is already covered unless structured data is enabled
733            #[cfg(feature = "structured-data")]
734            (inner, DataFormat::Text) => {
735                let remake = Self::with_inner(inner);
736                if let Some(str) = remake.render() {
737                    DataInner::Text(str)
738                } else {
739                    remake.inner
740                }
741            }
742            // reachable if more than one structured data format is enabled
743            #[allow(unreachable_patterns)]
744            #[cfg(feature = "json")]
745            (inner, DataFormat::Json) => inner,
746            // reachable if more than one structured data format is enabled
747            #[allow(unreachable_patterns)]
748            #[cfg(feature = "json")]
749            (inner, DataFormat::JsonLines) => inner,
750            // reachable if more than one structured data format is enabled
751            #[allow(unreachable_patterns)]
752            #[cfg(feature = "term-svg")]
753            (inner, DataFormat::TermSvg) => inner,
754        };
755        Self {
756            inner,
757            source,
758            filters,
759        }
760    }
761
762    /// Location the data came from
763    pub fn source(&self) -> Option<&DataSource> {
764        self.source.as_ref()
765    }
766
767    /// Outputs the current `DataFormat` of the underlying data
768    pub fn format(&self) -> DataFormat {
769        match &self.inner {
770            DataInner::Error(_) => DataFormat::Error,
771            DataInner::Binary(_) => DataFormat::Binary,
772            DataInner::Text(_) => DataFormat::Text,
773            #[cfg(feature = "json")]
774            DataInner::Json(_) => DataFormat::Json,
775            #[cfg(feature = "json")]
776            DataInner::JsonLines(_) => DataFormat::JsonLines,
777            #[cfg(feature = "term-svg")]
778            DataInner::TermSvg(_) => DataFormat::TermSvg,
779        }
780    }
781
782    pub(crate) fn intended_format(&self) -> DataFormat {
783        match &self.inner {
784            DataInner::Error(DataError { intended, .. }) => *intended,
785            DataInner::Binary(_) => DataFormat::Binary,
786            DataInner::Text(_) => DataFormat::Text,
787            #[cfg(feature = "json")]
788            DataInner::Json(_) => DataFormat::Json,
789            #[cfg(feature = "json")]
790            DataInner::JsonLines(_) => DataFormat::JsonLines,
791            #[cfg(feature = "term-svg")]
792            DataInner::TermSvg(_) => DataFormat::TermSvg,
793        }
794    }
795
796    pub(crate) fn against_format(&self) -> DataFormat {
797        self.filters
798            .get_against()
799            .unwrap_or_else(|| self.intended_format())
800    }
801
802    pub(crate) fn relevant(&self) -> Option<&str> {
803        match &self.inner {
804            DataInner::Error(_) => None,
805            DataInner::Binary(_) => None,
806            DataInner::Text(_) => None,
807            #[cfg(feature = "json")]
808            DataInner::Json(_) => None,
809            #[cfg(feature = "json")]
810            DataInner::JsonLines(_) => None,
811            #[cfg(feature = "term-svg")]
812            DataInner::TermSvg(data) => term_svg_body(data),
813        }
814    }
815}
816
817impl std::fmt::Display for Data {
818    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
819        match &self.inner {
820            DataInner::Error(data) => data.fmt(f),
821            DataInner::Binary(data) => String::from_utf8_lossy(data).fmt(f),
822            DataInner::Text(data) => data.fmt(f),
823            #[cfg(feature = "json")]
824            DataInner::Json(data) => serde_json::to_string_pretty(data).unwrap().fmt(f),
825            #[cfg(feature = "json")]
826            DataInner::JsonLines(data) => {
827                let array = data.as_array().expect("jsonlines is always an array");
828                for value in array {
829                    writeln!(f, "{}", serde_json::to_string(value).unwrap())?;
830                }
831                Ok(())
832            }
833            #[cfg(feature = "term-svg")]
834            DataInner::TermSvg(data) => data.fmt(f),
835        }
836    }
837}
838
839impl PartialEq for Data {
840    fn eq(&self, other: &Data) -> bool {
841        match (&self.inner, &other.inner) {
842            (DataInner::Error(left), DataInner::Error(right)) => left == right,
843            (DataInner::Binary(left), DataInner::Binary(right)) => left == right,
844            (DataInner::Text(left), DataInner::Text(right)) => left == right,
845            #[cfg(feature = "json")]
846            (DataInner::Json(left), DataInner::Json(right)) => left == right,
847            #[cfg(feature = "json")]
848            (DataInner::JsonLines(left), DataInner::JsonLines(right)) => left == right,
849            #[cfg(feature = "term-svg")]
850            (DataInner::TermSvg(left), DataInner::TermSvg(right)) => {
851                // HACK: avoid including `width` and `height` in the comparison
852                let left = term_svg_body(left.as_str()).unwrap_or(left.as_str());
853                let right = term_svg_body(right.as_str()).unwrap_or(right.as_str());
854                left == right
855            }
856            (_, _) => false,
857        }
858    }
859}
860
861#[derive(Clone, Debug, PartialEq, Eq)]
862pub(crate) struct DataError {
863    error: crate::assert::Error,
864    intended: DataFormat,
865}
866
867impl std::fmt::Display for DataError {
868    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
869        self.error.fmt(f)
870    }
871}
872
873#[cfg(feature = "json")]
874fn parse_jsonlines(text: &str) -> Result<Vec<serde_json::Value>, serde_json::Error> {
875    let mut lines = Vec::new();
876    for line in text.lines() {
877        let line = line.trim();
878        if line.is_empty() {
879            continue;
880        }
881        let json = serde_json::from_str::<serde_json::Value>(line)?;
882        lines.push(json);
883    }
884    Ok(lines)
885}
886
887#[cfg(feature = "term-svg")]
888fn term_svg_body(svg: &str) -> Option<&str> {
889    let (_header, body, _footer) = split_term_svg(svg)?;
890    Some(body)
891}
892
893#[cfg(feature = "term-svg")]
894pub(crate) fn split_term_svg(svg: &str) -> Option<(&str, &str, &str)> {
895    let open_elem_start_idx = svg.find("<text")?;
896    _ = svg[open_elem_start_idx..].find('>')?;
897    let open_elem_line_start_idx = svg[..open_elem_start_idx]
898        .rfind('\n')
899        .map(|idx| idx + 1)
900        .unwrap_or(svg.len());
901
902    let close_elem = "</text>";
903    let close_elem_start_idx = svg.rfind(close_elem).unwrap_or(svg.len());
904    let close_elem_line_end_idx = svg[close_elem_start_idx..]
905        .find('\n')
906        .map(|idx| idx + close_elem_start_idx + 1)
907        .unwrap_or(svg.len());
908
909    let header = &svg[..open_elem_line_start_idx];
910    let body = &svg[open_elem_line_start_idx..close_elem_line_end_idx];
911    let footer = &svg[close_elem_line_end_idx..];
912    Some((header, body, footer))
913}
914
915impl Eq for Data {}
916
917impl Default for Data {
918    fn default() -> Self {
919        Self::new()
920    }
921}
922
923impl<'d> From<&'d Data> for Data {
924    fn from(other: &'d Data) -> Self {
925        other.into_data()
926    }
927}
928
929impl From<Vec<u8>> for Data {
930    fn from(other: Vec<u8>) -> Self {
931        other.into_data()
932    }
933}
934
935impl<'b> From<&'b [u8]> for Data {
936    fn from(other: &'b [u8]) -> Self {
937        other.into_data()
938    }
939}
940
941impl From<String> for Data {
942    fn from(other: String) -> Self {
943        other.into_data()
944    }
945}
946
947impl<'s> From<&'s String> for Data {
948    fn from(other: &'s String) -> Self {
949        other.into_data()
950    }
951}
952
953impl<'s> From<&'s str> for Data {
954    fn from(other: &'s str) -> Self {
955        other.into_data()
956    }
957}
958
959impl From<Inline> for Data {
960    fn from(other: Inline) -> Self {
961        other.into_data()
962    }
963}
964
965#[cfg(feature = "detect-encoding")]
966fn is_binary(data: &[u8]) -> bool {
967    match content_inspector::inspect(data) {
968        content_inspector::ContentType::BINARY |
969        // We don't support these
970        content_inspector::ContentType::UTF_16LE |
971        content_inspector::ContentType::UTF_16BE |
972        content_inspector::ContentType::UTF_32LE |
973        content_inspector::ContentType::UTF_32BE => {
974            true
975        },
976        content_inspector::ContentType::UTF_8 |
977        content_inspector::ContentType::UTF_8_BOM => {
978            false
979        },
980    }
981}
982
983#[cfg(not(feature = "detect-encoding"))]
984fn is_binary(_data: &[u8]) -> bool {
985    false
986}
987
988#[doc(hidden)]
989pub fn generate_snapshot_path(fn_path: &str, format: Option<DataFormat>) -> std::path::PathBuf {
990    use std::fmt::Write as _;
991
992    let fn_path_normalized = fn_path.replace("::", "__");
993    let mut path = format!("tests/snapshots/{fn_path_normalized}");
994    let count = runtime::get().count(&path);
995    if 0 < count {
996        write!(&mut path, "@{count}").unwrap();
997    }
998    path.push('.');
999    path.push_str(format.unwrap_or(DataFormat::Text).ext());
1000    path.into()
1001}
1002
1003#[cfg(test)]
1004mod test {
1005    use super::*;
1006
1007    #[track_caller]
1008    fn validate_cases(cases: &[(&str, bool)], input_format: DataFormat) {
1009        for (input, valid) in cases.iter().copied() {
1010            let (expected_is_format, expected_coerced_format) = if valid {
1011                (input_format, input_format)
1012            } else {
1013                (DataFormat::Error, DataFormat::Text)
1014            };
1015
1016            let actual_is = Data::text(input).is(input_format);
1017            assert_eq!(
1018                actual_is.format(),
1019                expected_is_format,
1020                "\n{input}\n{actual_is}"
1021            );
1022
1023            let actual_coerced = Data::text(input).coerce_to(input_format);
1024            assert_eq!(
1025                actual_coerced.format(),
1026                expected_coerced_format,
1027                "\n{input}\n{actual_coerced}"
1028            );
1029
1030            if valid {
1031                assert_eq!(actual_is, actual_coerced);
1032
1033                let rendered = actual_is.render().unwrap();
1034                let bytes = actual_is.to_bytes().unwrap();
1035                assert_eq!(rendered, std::str::from_utf8(&bytes).unwrap());
1036
1037                assert_eq!(Data::text(&rendered).is(input_format), actual_is);
1038            }
1039        }
1040    }
1041
1042    #[test]
1043    fn text() {
1044        let cases = [("", true), ("good", true), ("{}", true), ("\"\"", true)];
1045        validate_cases(&cases, DataFormat::Text);
1046    }
1047
1048    #[cfg(feature = "json")]
1049    #[test]
1050    fn json() {
1051        let cases = [("", false), ("bad", false), ("{}", true), ("\"\"", true)];
1052        validate_cases(&cases, DataFormat::Json);
1053    }
1054
1055    #[cfg(feature = "json")]
1056    #[test]
1057    fn jsonlines() {
1058        let cases = [
1059            ("", true),
1060            ("bad", false),
1061            ("{}", true),
1062            ("\"\"", true),
1063            (
1064                "
1065{}
1066{}
1067", true,
1068            ),
1069            (
1070                "
1071{}
1072
1073{}
1074", true,
1075            ),
1076            (
1077                "
1078{}
1079bad
1080{}
1081",
1082                false,
1083            ),
1084        ];
1085        validate_cases(&cases, DataFormat::JsonLines);
1086    }
1087}