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: 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 #[cfg(feature = "json")]
401 JsonLines(serde_json::Value),
402 #[cfg(feature = "term-svg")]
403 TermSvg(String),
404}
405
406impl Data {
416 pub fn binary(raw: impl Into<Vec<u8>>) -> Self {
418 Self::with_value(DataValue::Binary(raw.into()))
419 }
420
421 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 pub fn new() -> Self {
445 Self::text("")
446 }
447
448 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 pub fn raw(mut self) -> Self {
459 self.inner.filters = FilterSet::empty().newlines();
460 self
461 }
462
463 pub fn unordered(mut self) -> Self {
465 self.inner.filters = self.inner.filters.unordered();
466 self
467 }
468}
469
470impl 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 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 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 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 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 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 #[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 fn against(mut self, format: DataFormat) -> Data {
671 self.inner.filters = self.inner.filters.against(format);
672 self
673 }
674
675 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 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 #[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 #[allow(unreachable_patterns)]
755 #[cfg(feature = "json")]
756 (value, DataFormat::Json) => value,
757 #[allow(unreachable_patterns)]
759 #[cfg(feature = "json")]
760 (value, DataFormat::JsonLines) => value,
761 #[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 pub fn source(&self) -> Option<&DataSource> {
777 self.inner.source.as_ref()
778 }
779
780 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 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 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}