1mod 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
18pub 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#[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#[allow(clippy::wrong_self_convention)]
78pub trait IntoData: Sized {
79 fn raw(self) -> Data {
81 self.into_data().raw()
82 }
83
84 fn unordered(self) -> Data {
104 self.into_data().unordered()
105 }
106
107 fn is(self, format: DataFormat) -> Data {
124 self.into_data().is(format)
125 }
126
127 #[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 #[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 #[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 fn against(self, format: DataFormat) -> Data {
213 self.into_data().against(format)
214 }
215
216 #[cfg(feature = "json")]
232 fn against_json(self) -> Data {
233 self.against(DataFormat::Json)
234 }
235
236 #[cfg(feature = "json")]
252 fn against_jsonlines(self) -> Data {
253 self.against(DataFormat::JsonLines)
254 }
255
256 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#[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#[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#[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 #[cfg(feature = "json")]
396 JsonLines(serde_json::Value),
397 #[cfg(feature = "term-svg")]
398 TermSvg(String),
399}
400
401impl Data {
411 pub fn binary(raw: impl Into<Vec<u8>>) -> Self {
413 Self::with_inner(DataInner::Binary(raw.into()))
414 }
415
416 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 pub fn new() -> Self {
440 Self::text("")
441 }
442
443 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 pub fn raw(mut self) -> Self {
454 self.filters = FilterSet::empty().newlines();
455 self
456 }
457
458 pub fn unordered(mut self) -> Self {
460 self.filters = self.filters.unordered();
461 self
462 }
463}
464
465impl 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 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 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 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 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 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 #[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 fn against(mut self, format: DataFormat) -> Data {
660 self.filters = self.filters.against(format);
661 self
662 }
663
664 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 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 #[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 #[allow(unreachable_patterns)]
744 #[cfg(feature = "json")]
745 (inner, DataFormat::Json) => inner,
746 #[allow(unreachable_patterns)]
748 #[cfg(feature = "json")]
749 (inner, DataFormat::JsonLines) => inner,
750 #[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 pub fn source(&self) -> Option<&DataSource> {
764 self.source.as_ref()
765 }
766
767 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 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 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}