From 98f582857b88c8d91dc4a1b886371697629f937a Mon Sep 17 00:00:00 2001 From: Midas Lambrichts Date: Sat, 10 Sep 2022 15:17:34 +0200 Subject: [PATCH 1/4] Added CHANGELOG and support for deserialization using serde. --- CHANGELOG.md | 26 +++++++++++++++++ Cargo.toml | 10 +++++-- src/lib.rs | 57 ++++++++++++++++++++++++++++++++----- src/serde_support.rs | 67 ++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 151 insertions(+), 9 deletions(-) create mode 100644 CHANGELOG.md create mode 100644 src/serde_support.rs diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..a6fa662 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,26 @@ +# Changelog +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] +### Added + +### Changed + +### Removed + +## [0.2.0] +### Added +* `serde` support behind the `serde` feature flag. +* `Eq, PartialEq, Ord, PartialOrd` are now implemented for `NonEmptyString`. +* `get` to retrieve a reference to the inner value. + +### Changed +* `new` constructor now returns a `Result` rather than an `Option`, which contains the original string + +### Removed + +[Unreleased]: https://github.com/MidasLamb/non-empty-string/v0.2.0...HEAD +[0.2.0]: https://github.com/MidasLamb/non-empty-string/compare/v0.1.0...v0.2.0 \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index 49e34d9..b635a45 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,17 +1,23 @@ [package] name = "non-empty-string" -version = "0.1.0" +version = "0.2.0" edition = "2018" authors = ["Midas Lambrichts "] license = "MIT OR Apache-2.0" description = "A simple type for non empty Strings, similar to NonZeroUsize and friends." repository = "https://github.com/MidasLamb/non-empty-string" -keywords = ["nonemptystring", "string", "str"] +keywords = ["nonemptystring", "string", "str", "non-empty", "nonempty"] [lib] name = "non_empty_string" [dependencies] +serde = { version = "1", optional = true } [dev-dependencies] assert_matches = "1.5.0" +serde_json = { version = "1" } + +[features] +default = [] +serde = ["dep:serde"] diff --git a/src/lib.rs b/src/lib.rs index b74864b..2a0e764 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,21 +1,31 @@ #![doc = include_str!("../README.md")] +#[cfg(feature = "serde")] +mod serde_support; + /// A simple String wrapper type, similar to NonZeroUsize and friends. /// Guarantees that the String contained inside is not of length 0. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] +#[repr(transparent)] pub struct NonEmptyString(String); impl NonEmptyString { /// Attempts to create a new NonEmptyString. - /// If the given `string` is empty, `None` is returned, `Some` otherwise. - pub fn new(string: String) -> Option { + /// If the given `string` is empty, `Err` is returned, containing the original `String`, `Ok` otherwise. + pub fn new(string: String) -> Result { if string.is_empty() { - None + Err(string) } else { - Some(NonEmptyString(string)) + Ok(NonEmptyString(string)) } } + /// Returns a reference to the contained value. + pub fn get(&self) -> &str { + &self.0 + } + + /// Consume the `NonEmptyString` to get the internal `String` out. pub fn into_inner(self) -> String { self.0 } @@ -33,6 +43,18 @@ impl std::convert::AsRef for NonEmptyString { } } +impl<'s> std::convert::TryFrom<&'s str> for NonEmptyString { + type Error = (); + + fn try_from(value: &'s str) -> Result { + if value.is_empty() { + Err(()) + } else { + Ok(NonEmptyString::new(value.to_owned()).expect("Value is not empty")) + } + } +} + #[cfg(test)] mod tests { use super::*; @@ -40,12 +62,12 @@ mod tests { #[test] fn empty_string_returns_none() { - assert_matches!(NonEmptyString::new("".to_owned()), None); + assert_eq!(NonEmptyString::new("".to_owned()), Err("".to_owned())); } #[test] fn non_empty_string_returns_some() { - assert_matches!(NonEmptyString::new("string".to_owned()), Some(_)); + assert_matches!(NonEmptyString::new("string".to_owned()), Ok(_)); } #[test] @@ -57,4 +79,25 @@ mod tests { "string".to_owned() ); } + + #[test] + fn as_ref_str_works() { + let nes = NonEmptyString::new("string".to_owned()).unwrap(); + let val: &str = nes.as_ref(); + assert_eq!(val, "string"); + } + + #[test] + fn as_ref_string_works() { + let nes = NonEmptyString::new("string".to_owned()).unwrap(); + let val: &String = nes.as_ref(); + assert_eq!(val, "string"); + } + + #[test] + fn calling_string_methods_works() { + let nes = NonEmptyString::new("string".to_owned()).unwrap(); + // `len` is a `String` method. + assert!(nes.get().len() > 0); + } } diff --git a/src/serde_support.rs b/src/serde_support.rs new file mode 100644 index 0000000..a010dcd --- /dev/null +++ b/src/serde_support.rs @@ -0,0 +1,67 @@ +use std::fmt; + +use serde::de::{self, Unexpected, Visitor}; + +use crate::NonEmptyString; + +struct NonEmptyStringVisitor; + +impl<'de> de::Deserialize<'de> for NonEmptyString { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + deserializer.deserialize_string(NonEmptyStringVisitor) + } +} + +pub enum DeserializeError {} + +type Result = std::result::Result; + +impl<'de> Visitor<'de> for NonEmptyStringVisitor { + type Value = NonEmptyString; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("an integer between -2^31 and 2^31") + } + + fn visit_str(self, value: &str) -> Result + where + E: de::Error, + { + self.visit_string(value.to_owned()) + } + + fn visit_string(self, value: String) -> Result + where + E: de::Error, + { + NonEmptyString::new(value).map_err(|e| de::Error::invalid_value(Unexpected::Str(&e), &self)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::*; + use assert_matches::assert_matches; + use serde_json::json; + + #[test] + fn deserialize_works() { + let e: Result = serde_json::from_value(json!("abc")); + + let expected = NonEmptyString("abc".to_owned()); + + assert_matches!(e, Ok(v) if v == expected) + } + + #[test] + fn deserialize_empty_fails() { + let e: Result = serde_json::from_value(json!("")); + + assert!(e.is_err()); + // assert_matches!(e, Ok(expected)) + } +} From 2d018ee815f94e7db99122d51baacccc2031729a Mon Sep 17 00:00:00 2001 From: Midas Lambrichts Date: Sat, 10 Sep 2022 15:24:08 +0200 Subject: [PATCH 2/4] Implement serde serialize --- src/serde_support.rs | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/src/serde_support.rs b/src/serde_support.rs index a010dcd..ae0d2ff 100644 --- a/src/serde_support.rs +++ b/src/serde_support.rs @@ -1,9 +1,21 @@ use std::fmt; -use serde::de::{self, Unexpected, Visitor}; +use serde::{ + de::{self, Unexpected, Visitor}, + ser::Serialize, +}; use crate::NonEmptyString; +impl Serialize for NonEmptyString { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.serialize_str(self.get()) + } +} + struct NonEmptyStringVisitor; impl<'de> de::Deserialize<'de> for NonEmptyString { @@ -48,6 +60,17 @@ mod tests { use assert_matches::assert_matches; use serde_json::json; + #[test] + fn serialize_works() { + let value = NonEmptyString("abc".to_owned()); + let result = serde_json::to_string(&value); + + assert!(result.is_ok()); + + let json = serde_json::to_string(&json!("abc")).unwrap(); + assert_eq!(result.unwrap(), json) + } + #[test] fn deserialize_works() { let e: Result = serde_json::from_value(json!("abc")); From ed564092b9e988a20ee80b2f9a97d7529152fdf6 Mon Sep 17 00:00:00 2001 From: Midas Lambrichts Date: Sat, 10 Sep 2022 15:27:50 +0200 Subject: [PATCH 3/4] Improve CI with features --- .github/workflows/generic.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/generic.yml b/.github/workflows/generic.yml index c4c5027..4bbd2ba 100644 --- a/.github/workflows/generic.yml +++ b/.github/workflows/generic.yml @@ -27,6 +27,7 @@ jobs: uses: actions-rs/cargo@v1 with: command: check + args: --all-features test: name: Test Suite @@ -46,6 +47,7 @@ jobs: uses: actions-rs/cargo@v1 with: command: test + args: --all-features lints: name: Lints From e80406034fee186c8e16f346aa93653dfd631add Mon Sep 17 00:00:00 2001 From: Midas Lambrichts Date: Sat, 10 Sep 2022 15:29:56 +0200 Subject: [PATCH 4/4] Use features for codecov. --- .github/workflows/coverage.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 32c18e1..2e9ac34 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -26,7 +26,7 @@ jobs: uses: actions-rs/tarpaulin@v0.1 with: version: "0.15.0" - args: "-- --test-threads 1" + args: "--all-features -- --test-threads 1" - name: Upload to codecov.io uses: codecov/codecov-action@v1.0.2