Repository: ParkMyCar/compact_str
Pull request #159 (2022-07-13 23:53:48)

add AddAssign operator
Easy peasy addition


Patch:
diff --git a/.github/workflows/clippy.yml b/.github/workflows/clippy.yml
index d129b69b..47982787 100644
--- a/.github/workflows/clippy.yml
+++ b/.github/workflows/clippy.yml
@@ -56,7 +56,7 @@ jobs:
     name: cargo doc
     runs-on: ubuntu-latest
     env:
-      RUSTDOCFLAGS: "-D warnings"
+      RUSTDOCFLAGS: "-D warnings --cfg docsrs"
     steps:
       - uses: actions/checkout@v2
         name: Checkout compact_str
@@ -64,7 +64,8 @@ jobs:
         name: Install Rust
         with:
           profile: minimal
-          toolchain: stable
+          # docs.rs uses a nightly toolchain
+          toolchain: nightly
           override: true
       - uses: actions-rs/cargo@v1
         name: Run rustdoc
diff --git a/README.md b/README.md
index d8afb42e..8f951370 100644
--- a/README.md
+++ b/README.md
@@ -68,9 +68,13 @@ Provides two methods `join_compact(seperator: impl AsRef<str>)` and `concat_comp
 This crate exposes one macro `format_compact!` that can be used to create `CompactString`s from arguments, like you can `String`s with the `std::format!` macro.
 
 ### Features
-`compact_str` has the following features:
-1. `serde`, which implements [`Deserialize`](https://docs.rs/serde/latest/serde/trait.Deserialize.html) and [`Serialize`](https://docs.rs/serde/latest/serde/trait.Serialize.html) from the popular [`serde`](https://docs.rs/serde/latest/serde/) crate, for `CompactString`.
-2. `bytes`, which provides two methods `from_utf8_buf<B: Buf>(buf: &mut B)` and `from_utf8_buf_unchecked<B: Buf>(buf: &mut B)`, which allows for the creation of a `CompactString` from a [`bytes::Buf`](https://docs.rs/bytes/latest/bytes/trait.Buf.html)
+`compact_str` has the following optional features:
+* `serde`, which implements [`Deserialize`](https://docs.rs/serde/1/serde/trait.Deserialize.html) and [`Serialize`](https://docs.rs/serde/1/serde/trait.Serialize.html) from the popular [`serde`](https://docs.rs/serde/1/serde/) crate, for `CompactString`.
+* `bytes`, which provides two methods `from_utf8_buf<B: Buf>(buf: &mut B)` and `from_utf8_buf_unchecked<B: Buf>(buf: &mut B)`, which allows for the creation of a `CompactString` from a [`bytes::Buf`](https://docs.rs/bytes/1/bytes/trait.Buf.html)
+* `markup`, which implements [`Render`](https://docs.rs/markup/0.13/markup/trait.Render.html) trait, so `CompactString`s can be used in templates as HTML escaped strings
+* `arbitrary`, which implements the [`arbitrary::Arbitrary`](https://docs.rs/arbitrary/1/arbitrary/trait.Arbitrary.html) trait for fuzzing
+* `proptest`, which implements the [`proptest::arbitrary::Arbitrary`](https://docs.rs/proptest/1/proptest/arbitrary/trait.Arbitrary.html) trait for fuzzing
+* `quickcheck`, which implements the [`quickcheck::Arbitrary`](https://docs.rs/quickcheck/1/quickcheck/trait.Arbitrary.html) trait for fuzzing
 
 ### How it works
 Note: this explanation assumes a 64-bit architecture, for 32-bit architectures generally divide any number by 2.
diff --git a/compact_str/Cargo.toml b/compact_str/Cargo.toml
index 99ebb980..bd6df039 100644
--- a/compact_str/Cargo.toml
+++ b/compact_str/Cargo.toml
@@ -14,6 +14,7 @@ categories = ["encoding", "parsing", "memory-management", "text-processing"]
 [dependencies]
 arbitrary = { version = "1", optional = true, default-features = false }
 bytes = { version = "1", optional = true }
+markup = { version = "0.13", optional = true, default-features = false }
 proptest = { version = "1", optional = true, default-features = false }
 quickcheck = { version = "1", optional = true, default-features = false }
 serde = { version = "1", optional = true }
@@ -27,3 +28,7 @@ proptest = { version = "1", default-features = false, features = ["std"] }
 quickcheck_macros = "1"
 rayon = "1"
 test-strategy = "0.2"
+
+[package.metadata.docs.rs]
+all-features = true
+rustdoc-args = ["--cfg", "docsrs"]
diff --git a/compact_str/README.md b/compact_str/README.md
new file mode 120000
index 00000000..32d46ee8
--- /dev/null
+++ b/compact_str/README.md
@@ -0,0 +1,1 @@
+../README.md
\ No newline at end of file
diff --git a/compact_str/src/features/arbitrary.rs b/compact_str/src/features/arbitrary.rs
index e0b4f91b..235fd06f 100644
--- a/compact_str/src/features/arbitrary.rs
+++ b/compact_str/src/features/arbitrary.rs
@@ -8,6 +8,7 @@ use arbitrary::{
 
 use crate::CompactString;
 
+#[cfg_attr(docsrs, doc(cfg(feature = "arbitrary")))]
 impl<'a> Arbitrary<'a> for CompactString {
     fn arbitrary(u: &mut Unstructured<'a>) -> Result<Self> {
         <&str as Arbitrary>::arbitrary(u).map(CompactString::new)
diff --git a/compact_str/src/features/bytes.rs b/compact_str/src/features/bytes.rs
index 59d2958a..d0598376 100644
--- a/compact_str/src/features/bytes.rs
+++ b/compact_str/src/features/bytes.rs
@@ -35,6 +35,7 @@ impl CompactString {
     /// // The provided buffer is invalid, so trying to create a `CompactString` will fail
     /// assert!(CompactString::from_utf8_buf(&mut invalid).is_err());
     /// ```
+    #[cfg_attr(docsrs, doc(cfg(feature = "bytes")))]
     pub fn from_utf8_buf<B: Buf>(buf: &mut B) -> Result<Self, Utf8Error> {
         Repr::from_utf8_buf(buf).map(|repr| CompactString { repr })
     }
@@ -59,6 +60,7 @@ impl CompactString {
     ///
     /// assert_eq!(compact_str, word);
     /// ```
+    #[cfg_attr(docsrs, doc(cfg(feature = "bytes")))]
     pub unsafe fn from_utf8_buf_unchecked<B: Buf>(buf: &mut B) -> Self {
         let repr = Repr::from_utf8_buf_unchecked(buf);
         CompactString { repr }
diff --git a/compact_str/src/features/markup.rs b/compact_str/src/features/markup.rs
new file mode 100644
index 00000000..9d3fdc1c
--- /dev/null
+++ b/compact_str/src/features/markup.rs
@@ -0,0 +1,38 @@
+use std::fmt;
+
+use markup::Render;
+
+use crate::CompactString;
+
+#[cfg_attr(docsrs, doc(cfg(feature = "markup")))]
+impl Render for CompactString {
+    #[inline]
+    fn render(&self, writer: &mut impl fmt::Write) -> fmt::Result {
+        self.as_str().render(writer)
+    }
+}
+
+#[cfg(test)]
+#[test]
+fn test_markup() {
+    const TEXT: &str = "<script>alert('Hello, world!')</script>";
+
+    markup::define!(Template<M: Render>(msg: M) {
+        textarea { @msg }
+    });
+
+    let compact = Template {
+        msg: CompactString::from(TEXT),
+    };
+    let control = Template {
+        msg: String::from(TEXT),
+    };
+    assert_eq!(
+        compact.to_string(),
+        "<textarea>&lt;script&gt;alert('Hello, world!')&lt;/script&gt;</textarea>",
+    );
+    assert_eq!(
+        control.to_string(),
+        "<textarea>&lt;script&gt;alert('Hello, world!')&lt;/script&gt;</textarea>",
+    );
+}
diff --git a/compact_str/src/features/mod.rs b/compact_str/src/features/mod.rs
index 732db548..cd9c9ed7 100644
--- a/compact_str/src/features/mod.rs
+++ b/compact_str/src/features/mod.rs
@@ -4,6 +4,8 @@
 mod arbitrary;
 #[cfg(feature = "bytes")]
 mod bytes;
+#[cfg(feature = "markup")]
+mod markup;
 #[cfg(feature = "proptest")]
 mod proptest;
 #[cfg(feature = "quickcheck")]
diff --git a/compact_str/src/features/quickcheck.rs b/compact_str/src/features/quickcheck.rs
index d05682fa..286de025 100644
--- a/compact_str/src/features/quickcheck.rs
+++ b/compact_str/src/features/quickcheck.rs
@@ -7,6 +7,7 @@ use quickcheck::{
 
 use crate::CompactString;
 
+#[cfg_attr(docsrs, doc(cfg(feature = "quickcheck")))]
 impl Arbitrary for CompactString {
     fn arbitrary(g: &mut Gen) -> CompactString {
         let max = g.size();
diff --git a/compact_str/src/features/serde.rs b/compact_str/src/features/serde.rs
index 29a1a019..1fdf370f 100644
--- a/compact_str/src/features/serde.rs
+++ b/compact_str/src/features/serde.rs
@@ -61,12 +61,14 @@ fn compact_string<'de: 'a, 'a, D: Deserializer<'de>>(
     deserializer.deserialize_str(CompactStringVisitor)
 }
 
+#[cfg_attr(docsrs, doc(cfg(feature = "serde")))]
 impl serde::Serialize for CompactString {
     fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
         self.as_str().serialize(serializer)
     }
 }
 
+#[cfg_attr(docsrs, doc(cfg(feature = "serde")))]
 impl<'de> serde::Deserialize<'de> for CompactString {
     fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
         compact_string(deserializer)
diff --git a/compact_str/src/lib.rs b/compact_str/src/lib.rs
index 68de63ae..fc6da69d 100644
--- a/compact_str/src/lib.rs
+++ b/compact_str/src/lib.rs
@@ -1,17 +1,5 @@
-//! [`CompactString`] is a compact string type that stores itself on the stack if possible,
-//! otherwise known as a "small string optimization".
-//!
-//! ### Memory Layout
-//! Normally strings are stored on the heap, since they're dynamically sized. In Rust a [`String`]
-//! consists of three things:
-//! 1. A `usize` denoting the length of the string
-//! 2. A pointer to a location on the heap where the string is stored
-//! 3. A `usize` denoting the capacity of the string
-//!
-//! On 64-bit architectures this results in 24 bytes being stored on the stack (12 bytes for 32-bit
-//! architectures). For small strings, e.g. <= 24 characters, instead of storing a pointer, length,
-//! and capacity on the stack, you store the string itself! This avoids the need to heap allocate
-//! which reduces the amount of memory used, and improves performance.
+#![doc = include_str!("../README.md")]
+#![cfg_attr(docsrs, feature(doc_cfg))]
 
 use core::borrow::{
     Borrow,
@@ -26,6 +14,7 @@ use core::hash::{
 use core::iter::FromIterator;
 use core::ops::{
     Add,
+    AddAssign,
     Bound,
     Deref,
     DerefMut,
@@ -439,9 +428,8 @@ impl CompactString {
     ///
     /// assert_eq!("foobar", s);
     /// ```
-    #[inline]
     pub fn push(&mut self, ch: char) {
-        self.repr.push(ch)
+        self.push_str(ch.encode_utf8(&mut [0; 4]));
     }
 
     /// Removes the last character from the [`CompactString`] and returns it.
@@ -1067,6 +1055,12 @@ impl Add<&str> for CompactString {
     }
 }
 
+impl AddAssign<&str> for CompactString {
+    fn add_assign(&mut self, rhs: &str) {
+        self.push_str(rhs);
+    }
+}
+
 /// An iterator over the exacted data by [`CompactString::drain()`].
 #[must_use = "iterators are lazy and do nothing unless consumed"]
 pub struct Drain<'a> {
diff --git a/compact_str/src/repr/mod.rs b/compact_str/src/repr/mod.rs
index e44da9af..4da47dc3 100644
--- a/compact_str/src/repr/mod.rs
+++ b/compact_str/src/repr/mod.rs
@@ -201,23 +201,6 @@ impl Repr {
         self.cast_mut().into_mut_slice()
     }
 
-    #[inline]
-    pub fn push(&mut self, ch: char) {
-        let len = self.len();
-        let char_len = ch.len_utf8();
-
-        // Reserve at least enough space for our char, possibly causing a heap allocation
-        self.reserve(char_len);
-
-        // Get a mutable reference to the underlying memory buffer
-        let slice = unsafe { self.as_mut_slice() };
-
-        // Write our character into the underlying buffer
-        ch.encode_utf8(&mut slice[len..]);
-        // Incrament our length
-        unsafe { self.set_len(len + char_len) };
-    }
-
     #[inline]
     pub fn pop(&mut self) -> Option<char> {
         let ch = self.as_str().chars().rev().next()?;
@@ -648,16 +631,6 @@ mod tests {
         assert_eq!(repr.as_str(), long_word);
     }
 
-    #[test]
-    fn test_push() {
-        let example = "hello world";
-        let mut repr = Repr::new(example);
-        repr.push('!');
-
-        assert_eq!(repr.as_str(), "hello world!");
-        assert_eq!(repr.len(), 12);
-    }
-
     #[test]
     fn test_pop() {
         let example = "hello";
@@ -791,7 +764,7 @@ mod tests {
         assert_eq!(repr.capacity(), 11);
         assert!(repr.is_heap_allocated());
 
-        repr.push('!');
+        repr.push_str("!");
 
         // Once we push a character we'll need to resize, it's at this point we'll inline the string
         // since we need to drop the original buffer anyways
@@ -812,7 +785,7 @@ mod tests {
         assert_eq!(repr.capacity(), 11);
         assert!(repr.is_heap_allocated());
 
-        repr.push('!');
+        repr.push_str("!");
 
         // Once we push a character we'll need to resize, it's at this point we'll inline the string
         // since we need to drop the original buffer anyways


Test patch:
diff --git a/compact_str/src/features/proptest.rs b/compact_str/src/features/proptest.rs
index 346694aa..e0b5bcf5 100644
--- a/compact_str/src/features/proptest.rs
+++ b/compact_str/src/features/proptest.rs
@@ -13,6 +13,7 @@ use proptest::string::StringParam;
 
 use crate::CompactString;
 
+#[cfg_attr(docsrs, doc(cfg(feature = "proptest")))]
 impl Arbitrary for CompactString {
     type Parameters = StringParam;
     type Strategy = MapInto<StrategyFor<String>, Self>;
diff --git a/compact_str/src/tests.rs b/compact_str/src/tests.rs
index dbf1c7bd..3e1af2fc 100644
--- a/compact_str/src/tests.rs
+++ b/compact_str/src/tests.rs
@@ -424,6 +424,13 @@ fn test_plus_operator() {
     assert_eq!(String::from("a") + &"b", "ab");
 }
 
+#[test]
+fn test_plus_equals_operator() {
+    let mut m = CompactString::from("a");
+    m += "b";
+    assert_eq!(m, "ab");
+}
+
 #[test]
 fn test_u8_to_compact_string() {
     let vals = [u8::MIN, 1, 42, u8::MAX - 2, u8::MAX - 1, u8::MAX];