diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..0618acf --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,153 @@ +name: CI + +on: + push: + branches: + - main + - master + tags: + - "*" + pull_request: + branches: + - main + - master + workflow_dispatch: + inputs: + release: + type: boolean + description: 'Create a GitHub Release?' + default: false + required: false + publish: + type: boolean + description: 'Publish to crates.io?' + default: false + required: false + version: + type: string + description: 'Version (if $GITHUB_REF is not a tag)' + default: '' + required: false +env: + CARGO_TERM_COLOR: always + +permissions: + contents: write + +jobs: + check: + name: Lint & Test + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + + - name: setup + uses: dtolnay/rust-toolchain@stable + + - name: cache + uses: Swatinem/rust-cache@v2 + + - name: fmt + run: cargo fmt --all --check + + - name: lint + run: cargo clippy --all-targets --all-features --no-deps -- -D warnings + + - name: test + run: cargo test --all-features + + publish: + name: Publish & Release + needs: check + runs-on: ubuntu-latest + if: (startsWith(github.ref, 'refs/tags/') && github.event.inputs.publish != 'false') || (github.event_name == 'workflow_dispatch' && github.event.inputs.publish == 'true') + env: + CARGO_TERM_COLOR: always + VERSION: ${{ github.event.inputs.version }} + outputs: + published: ${{ steps.publish.outcome == 'success' }} + released: ${{ steps.release.outcome == 'success' }} + steps: + - uses: actions/checkout@v5 + + - name: setup + uses: dtolnay/rust-toolchain@stable + + - name: cache + uses: Swatinem/rust-cache@v2 + + - if: | + github.event.inputs.publish != 'false' && ( + github.event.inputs.version != '' || + startsWith(github.ref, 'refs/tags/') + ) + name: publish + id: publish + env: + CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} + run: | + if [ -z "${CARGO_REGISTRY_TOKEN}" ]; then + echo "CARGO_REGISTRY_TOKEN is not set." >&2 + exit 1 + fi + cargo publish --token "${CARGO_REGISTRY_TOKEN}" + + - id: release_exists + uses: actions/github-script@v7 + env: + RELEASE_VERSION: ${{ github.event.inputs.version }} + with: + result-encoding: string + script: | + const ref = context.ref; + const ref_name = ref.replace('refs/tags/', ''); + let tag = process.env.RELEASE_VERSION; + if (ref === ref_name && !tag) { + // indicates the ref is not a tag ref, thus we + // throw if no version was provided. + throw new Error('No version provided for release.'); + } else if (!tag) { + tag = ref_name; + } + + try { + const { owner, repo } = context.repo; + await github.rest.repos.getReleaseByTag({ owner, repo, tag }); + return true; + } catch (error) { + if (error.status === 404) { + return false; + } else { + throw error; + } + } + + - name: release + id: release + if: | + (github.event.inputs.release == 'true' && github.event_name == 'workflow_dispatch') || + ( + startsWith(github.ref, 'refs/tags/') && + steps.release_exists.outputs.result == 'false' + ) + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + RELEASE_VERSION: ${{ github.event.inputs.version }} + run: | + if [ -z "${GITHUB_TOKEN}" ]; then + echo "GITHUB_TOKEN is not set." >&2 + exit 1 + fi + ref=${GITHUB_REF} + ref_name=${ref#refs/tags/} + TAG=${RELEASE_VERSION} + if [ -z "${TAG}" ]; then + TAG=${ref_name} + fi + TITLE="v${TAG#v}" + if [ "${TITLE}" != "v" ]; then + gh release create "${TAG}" -t "${TITLE}" --generate-notes --latest + else + echo "No valid tag found for release." >&2 + exit 1 + fi diff --git a/Cargo.lock b/Cargo.lock index adaf661..622782c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -33,12 +33,25 @@ dependencies = [ "syn", ] +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "memchr" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" + [[package]] name = "moos" -version = "0.1.0" +version = "0.2.0" dependencies = [ "derive_more", "serde", + "serde_json", ] [[package]] @@ -68,6 +81,12 @@ dependencies = [ "semver", ] +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + [[package]] name = "semver" version = "1.0.27" @@ -104,11 +123,24 @@ dependencies = [ "syn", ] +[[package]] +name = "serde_json" +version = "1.0.145" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", + "serde_core", +] + [[package]] name = "syn" -version = "2.0.109" +version = "2.0.111" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f17c7e013e88258aa9543dcbe81aca68a667a9ac37cd69c9fbc07858bfe0e2f" +checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 1caac64..5d52181 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "moos" -version = "0.1.0" +version = "0.2.0" edition = "2024" license = "MIT" authors = ["Nicholas Berlette "] @@ -27,3 +27,6 @@ serde = { version = "1.0", features = [ "rc", "alloc", ], default-features = false, optional = true } + +[dev-dependencies] +serde_json = "1.0" diff --git a/src/cow_str.rs b/src/cow_str.rs index ba4755c..8b5807e 100644 --- a/src/cow_str.rs +++ b/src/cow_str.rs @@ -74,19 +74,29 @@ pub enum CowStr<'i> { impl<'i> CowStr<'i> { #[inline(always)] - pub const fn as_str(&self) -> &str { + pub fn as_str(&self) -> &str { match self { CowStr::Owned(b) => b, - CowStr::Borrowed(b) => *b, - CowStr::Inlined(s) => &*s.deref(), + CowStr::Borrowed(b) => b, + CowStr::Inlined(s) => s.deref(), } } + /// Returns a mutable reference to the string as a slice. + /// + /// # Safety + /// + /// The caller must ensure that the mutable reference does not violate any + /// aliasing rules, i.e., there are no other references to the same data while + /// this mutable reference is in use. This is especially important for the + /// `Borrowed` variant, as modifying the data could lead to undefined behavior + /// if there are other references to the same data. Use with caution and + /// discretion. #[inline(always)] pub unsafe fn as_mut_str(&mut self) -> &mut str { unsafe { match self { - CowStr::Owned(b) => &mut **b, + CowStr::Owned(b) => b, CowStr::Borrowed(b) => transmute_copy(&b.to_owned().as_bytes_mut()), CowStr::Inlined(s) => s.as_mut_str_unchecked(), } @@ -94,7 +104,7 @@ impl<'i> CowStr<'i> { } #[inline(always)] - pub const fn as_bytes(&self) -> &[u8] { + pub fn as_bytes(&self) -> &[u8] { match self { CowStr::Owned(b) => b.as_bytes(), CowStr::Borrowed(b) => b.as_bytes(), @@ -102,6 +112,14 @@ impl<'i> CowStr<'i> { } } + /// Returns a mutable byte slice of the string's contents. + /// + /// # Safety + /// + /// The caller must ensure that the underlying data is not aliased while the + /// mutable byte slice is in use. This is particularly important for the + /// [`CowStr::Borrowed`] variant - modifying the data while there are existing + /// references to it is undefined behavior. Use with caution. #[inline(always)] pub unsafe fn as_bytes_mut(&mut self) -> &mut [u8] { unsafe { @@ -113,11 +131,21 @@ impl<'i> CowStr<'i> { } } + /// Returns the length of the `CowStr` in bytes. + #[inline(always)] + pub fn len(&self) -> usize { + self.as_bytes().len() + } + + /// Returns `true` if the `CowStr` is empty. #[inline(always)] - pub const fn len(&self) -> usize { - self.deref().len() + pub fn is_empty(&self) -> bool { + self.len() == 0 } + /// Converts the `CowStr` into an owned `String`, cloning the data if + /// necessary. + #[deprecated(since = "0.4.0", note = "use `into_string` instead")] #[inline(always)] pub fn into_owned(self) -> String { match self { @@ -166,13 +194,13 @@ impl<'i> Clone for CowStr<'i> { Ok(inline) => CowStr::Inlined(inline), Err(_) => CowStr::Owned(s.clone()), }, - CowStr::Borrowed(s) => CowStr::Borrowed(*s), + CowStr::Borrowed(s) => CowStr::Borrowed(s), CowStr::Inlined(s) => CowStr::Inlined(*s), } } } -impl<'i> const Deref for CowStr<'i> { +impl<'i> Deref for CowStr<'i> { type Target = str; #[inline(always)] @@ -188,7 +216,7 @@ impl<'i> DerefMut for CowStr<'i> { } } -impl<'i> const AsRef for CowStr<'i> { +impl<'i> AsRef for CowStr<'i> { #[inline(always)] fn as_ref(&self) -> &str { self.deref() @@ -202,7 +230,7 @@ impl<'i> AsMut for CowStr<'i> { } } -impl<'i> const Borrow for CowStr<'i> { +impl<'i> Borrow for CowStr<'i> { fn borrow(&self) -> &str { self.deref() } @@ -252,7 +280,7 @@ impl<'i> PartialEq> for str { impl<'i> PartialEq> for &'i str { #[inline(always)] fn eq(&self, other: &CowStr<'_>) -> bool { - self == &*other + other.deref() == *self } } @@ -438,18 +466,6 @@ mod serde_impl { mod tests { use super::*; - #[test] - fn inlinestr_ascii() { - let s: InlineStr = 'i'.into(); - assert_eq!("i", s.deref()); - } - - #[test] - fn inlinestr_unicode() { - let s: InlineStr = '🍔'.into(); - assert_eq!("🍔", s.deref()); - } - #[test] fn cowstr_size() { let size = std::mem::size_of::(); @@ -466,27 +482,6 @@ mod tests { assert_eq!(expected, owned); } - #[test] - fn max_inline_str_len_atleast_four() { - // we need 4 bytes to store a char - assert!(MAX_INLINE_STR_LEN >= 4); - } - - #[test] - #[cfg(target_pointer_width = "64")] - fn inlinestr_fits_twentytwo() { - let s = "0123456789abcdefghijkl"; - let stack_str = InlineStr::try_from(s).unwrap(); - assert_eq!(stack_str, *s); - } - - #[test] - #[cfg(target_pointer_width = "64")] - fn inlinestr_not_fits_twentythree() { - let s = "0123456789abcdefghijklm"; - let _stack_str = InlineStr::try_from(s).unwrap_err(); - } - #[test] #[cfg(target_pointer_width = "64")] fn small_boxed_str_clones_to_stack() { diff --git a/src/inline_str.rs b/src/inline_str.rs index 4876f44..cef4d20 100644 --- a/src/inline_str.rs +++ b/src/inline_str.rs @@ -21,10 +21,6 @@ use core::ops::DerefMut; use core::str; use core::str::FromStr; -use derive_more::with_trait::Constructor; -use derive_more::with_trait::Index; -use derive_more::with_trait::IndexMut; - use crate::CowStr; /// Maximum length of an inline string in bytes. On 64-bit systems this is @@ -57,7 +53,12 @@ pub const MAX_INLINE_STR_LEN: usize = 3 * size_of::() - 2; #[derive(Debug, Clone, Copy)] pub struct StringTooLongError; -#[derive(Debug, Clone, Copy, Constructor, Index, IndexMut)] +#[derive(Debug, Clone, Copy)] +#[cfg_attr(feature = "constructors", derive(derive_more::Constructor))] +#[cfg_attr( + feature = "index", + derive(derive_more::Index, derive_more::IndexMut) +)] /// Represents a short inline string stored on the stack in fixed-size buffers. /// /// Designed to hold very short strings (up to [`MAX_INLINE_STR_LEN`] bytes), @@ -88,13 +89,19 @@ pub struct StringTooLongError; /// # } /// ``` pub struct InlineStr { - #[index] - #[index_mut] - buf: [u8; MAX_INLINE_STR_LEN], - len: u8, + #[cfg_attr(feature = "index", index)] + #[cfg_attr(feature = "index", index_mut)] + pub(crate) buf: [u8; MAX_INLINE_STR_LEN], + pub(crate) len: u8, } impl InlineStr { + /// Creates a new `InlineStr`. + #[cfg(not(feature = "constructors"))] + pub const fn new(buf: [u8; MAX_INLINE_STR_LEN], len: u8) -> Self { + Self { buf, len } + } + /// Returns the length of the string. #[inline] pub const fn len(&self) -> usize { @@ -109,26 +116,25 @@ impl InlineStr { /// Returns a reference to the underlying byte buffer. #[inline] - pub const fn as_bytes(&self) -> &[u8] { - &self - .buf - .get(..self.len as usize) - .expect("InlineStr length should be valid and within bounds") + pub fn as_bytes(&self) -> &[u8] { + &self.buf[..self.len as usize] } /// Returns a mutable reference to the underlying byte buffer. #[inline] - pub const fn as_bytes_mut(&mut self) -> &mut [u8] { - self - .buf - .get_mut(..self.len as usize) - .expect("InlineStr length should be valid and within bounds") + pub fn as_bytes_mut(&mut self) -> &mut [u8] { + &mut self.buf[..self.len as usize] } /// Returns a reference to the string as a slice. + /// + /// # Panics + /// + /// This method panics if the internal byte buffer does not contain valid + /// UTF-8 data. #[inline] - pub const fn as_str(&self) -> &str { - if let Ok(s) = str::from_utf8(&self.as_bytes()) { + pub fn as_str(&self) -> &str { + if let Ok(s) = str::from_utf8(self.as_bytes()) { s } else { panic!("InlineStr should only contain valid UTF-8 data"); @@ -137,21 +143,29 @@ impl InlineStr { /// Returns a mutable reference to the string as a slice. #[inline] - pub const fn as_mut_str(&mut self) -> Result<&mut str, str::Utf8Error> { + pub fn as_mut_str(&mut self) -> Result<&mut str, str::Utf8Error> { str::from_utf8_mut(self.as_bytes_mut()) } /// Returns a reference to the string as a slice, without checking - /// for UTF-8 validity. The caller must ensure the data is valid UTF-8. + /// for UTF-8 validity. + /// + /// # Safety + /// + /// The caller must ensure the data is valid UTF-8. #[inline] - pub const unsafe fn as_str_unchecked(&self) -> &str { + pub unsafe fn as_str_unchecked(&self) -> &str { unsafe { str::from_utf8_unchecked(self.as_bytes()) } } /// Returns a mutable reference to the string as a slice, without checking - /// for UTF-8 validity. The caller must ensure the data is valid UTF-8. + /// for UTF-8 validity. + /// + /// # Safety + /// + /// The caller must ensure the data is valid UTF-8. #[inline] - pub const unsafe fn as_mut_str_unchecked(&mut self) -> &mut str { + pub unsafe fn as_mut_str_unchecked(&mut self) -> &mut str { unsafe { str::from_utf8_unchecked_mut(self.as_bytes_mut()) } } } @@ -187,7 +201,7 @@ impl BorrowMut for InlineStr { } } -impl const Deref for InlineStr { +impl Deref for InlineStr { type Target = str; #[inline(always)] @@ -203,7 +217,7 @@ impl DerefMut for InlineStr { } } -impl const AsRef for InlineStr { +impl AsRef for InlineStr { #[inline(always)] fn as_ref(&self) -> &str { self.deref() @@ -340,7 +354,7 @@ impl<'i> PartialEq for CowStr<'i> { } } -impl<'i> PartialEq for &'i str { +impl PartialEq for &str { #[inline(always)] fn eq(&self, other: &InlineStr) -> bool { *self == other.deref() @@ -387,21 +401,21 @@ impl PartialEq for &&str { } } -impl<'i> PartialEq for &'i mut str { +impl PartialEq for &mut str { #[inline(always)] fn eq(&self, other: &InlineStr) -> bool { &**self == other.deref() } } -impl<'i> PartialEq for &'i mut String { +impl PartialEq for &mut String { #[inline(always)] fn eq(&self, other: &InlineStr) -> bool { self.as_str() == other.deref() } } -impl<'i> PartialEq for &'i mut InlineStr { +impl PartialEq for &mut InlineStr { #[inline(always)] fn eq(&self, other: &InlineStr) -> bool { **self == *other @@ -456,21 +470,21 @@ impl PartialOrd for &&str { } } -impl<'i> PartialOrd for &'i mut str { +impl PartialOrd for &mut str { #[inline(always)] fn partial_cmp(&self, other: &InlineStr) -> Option { - Some((&**self).cmp(other.deref())) + Some((**self).cmp(other.deref())) } } -impl<'i> PartialOrd for &'i mut String { +impl PartialOrd for &mut String { #[inline(always)] fn partial_cmp(&self, other: &InlineStr) -> Option { Some(self.as_str().cmp(other.deref())) } } -impl<'i> PartialOrd for &'i mut InlineStr { +impl PartialOrd for &mut InlineStr { #[inline(always)] fn partial_cmp(&self, other: &InlineStr) -> Option { Some((**self).deref().cmp(other.deref())) @@ -512,7 +526,8 @@ mod tests { #[test] fn max_inline_str_len_is_at_least_4_bytes() { - assert!(MAX_INLINE_STR_LEN >= 4); + let max = MAX_INLINE_STR_LEN; + assert!(max >= 4); } #[test] @@ -549,6 +564,7 @@ mod tests { } #[test] + #[cfg(target_pointer_width = "64")] fn try_inline_str_from_str() { let s = "Hello, world!"; let inline_str = InlineStr::try_from(s); @@ -557,6 +573,27 @@ mod tests { assert_eq!(inline_str.deref(), s); } + #[test] + #[cfg(target_pointer_width = "32")] + fn inline_str_fits_ten() { + let s = "0123456789"; + let stack_str = InlineStr::try_from(s); + assert!(stack_str.is_ok()); + let stack_str = stack_str.unwrap(); + assert_eq!(stack_str.len(), 10); + assert_eq!(stack_str.deref().len(), 10); + assert_eq!(stack_str.deref(), s); + } + + #[test] + #[cfg(target_pointer_width = "32")] + fn inline_str_not_fits_eleven() { + let s = "0123456789a"; + let err = InlineStr::try_from(s); + assert!(err.is_err()); + assert!(matches!(err, Err(StringTooLongError))); + } + #[test] fn try_inline_str_from_long_str() { let s = "This string is too long to fit in an InlineStr"; diff --git a/src/lib.rs b/src/lib.rs index b23fcef..684c5a6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -62,9 +62,6 @@ //! //! > † enabled by default -#![feature(const_convert)] -#![feature(const_index)] -#![feature(const_trait_impl)] #![cfg_attr(not(any(test, feature = "std")), no_std)] extern crate alloc;