diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 232cff0..8aaee39 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,9 +2,9 @@ name: Rust Build on: push: - branches: [ "main" ] + branches: ["main"] pull_request: - branches: [ "main" ] + branches: ["main"] env: CARGO_TERM_COLOR: always @@ -13,8 +13,15 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - name: Build - run: cargo build --verbose - - name: Run tests - run: cargo test --verbose + - uses: actions/checkout@v4 + - uses: taiki-e/install-action@cargo-hack + - name: Build + run: cargo hack build --feature-powerset + - name: Run tests + run: cargo hack test --feature-powerset + - name: Lint + run: cargo hack clippy --all-targets --feature-powerset + - name: Fmt + run: cargo fmt --all --check + - name: Build docs + run: RUSTDOCFLAGS="-D warnings" cargo doc --no-deps diff --git a/Cargo.toml b/Cargo.toml index 8599ad7..b926555 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "inline-str" -version = "0.4.0" -edition = "2021" +version = "0.5.0" +edition = "2024" authors = ["Adam Gutglick "] description = "Efficent and immutable string type, backed by inline-array" license = "Apache-2.0 OR MIT" @@ -12,8 +12,11 @@ keywords = ["string", "compact", "stack", "immutable", "database"] categories = ["data-structures", "compression"] [dependencies] -inline-array = "0.1.13" -serde = { version = "1.0", features = ["derive"], optional = true } +inline-array = "0.1.14" +serde = { version = "1", features = ["derive"], optional = true } + +[dev-dependencies] +serde_json = "1" [features] serde = ["inline-array/serde", "dep:serde"] diff --git a/LICENSE-MIT b/LICENSE-MIT index 60facdf..d45d654 100644 --- a/LICENSE-MIT +++ b/LICENSE-MIT @@ -1,4 +1,4 @@ -Copyright 2024 Adam Gutglick +Copyright 2025 Adam Gutglick Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: diff --git a/README.md b/README.md index 73b9226..be8deba 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # inline-str -`inline-str` is a small and cheaply cloned string type, intended for use in cases where you expect to be cloning the same short string many times. +`inline-str` is a small and cheaply cloned string type, intended for use in cases where you expect to be cloning the same somewhat short string many times. It is a thin layer over [`inline-array`](https://github.com/komora-io/inline-array) inspired by [@spacejam's](https://github.com/spacejam) work who suggested I build this crate a while back. diff --git a/src/lib.rs b/src/lib.rs index b119060..79ecc2c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,28 +1,82 @@ -// Copyright 2024 Adam Gutglick - +// Copyright 2025 Adam Gutglick +// // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at - +// // http://www.apache.org/licenses/LICENSE-2.0 - +// // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. +//! A string type that stores strings inline when small. +//! +//! `InlineStr` is a string type built on top of [`inline-array`] that can store small strings +//! directly inline to avoid heap allocation, falling back to heap allocation for larger strings. +//! +//! This crate doesn't do any of the heavy lifting, if you want to better understand how it works +//! its recommended to read through inline-array's docs and source code. +//! +//! # Examples +//! +//! ``` +//! use inline_str::InlineStr; +//! +//! let s = InlineStr::from("hello"); +//! assert_eq!(s, "hello"); +//! ``` +//! +//! # Features +//! +//! - **serde**: Enable serialization/deserialization support with serde +//! +//! [`inline-array`]: https://crates.io/crates/inline-array + +#![deny(clippy::doc_markdown)] +#![deny(missing_docs)] + use core::str; -use std::{borrow::Cow, ops::Deref}; +use std::{ + borrow::{Borrow, Cow}, + cmp::Ordering, + ffi::OsStr, + ops::Deref, + path::Path, +}; + +#[cfg(feature = "serde")] +mod serde; use inline_array::InlineArray; -#[derive(PartialEq, Eq, PartialOrd, Ord, Clone)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +/// Immutable stack-inlinable string type that can be cheaply cloned and shared. +#[derive(PartialEq, Eq, Clone)] pub struct InlineStr { inner: InlineArray, } +impl InlineStr { + /// Extracts a string slice containing the entire `InlineStr`. + pub fn as_str(&self) -> &str { + // Safety: + // InlineStr can only be created from valid UTF8 byte sequences + unsafe { str::from_utf8_unchecked(&self.inner) } + } + + /// Returns the length of the `InlineStr` in **bytes**. + pub fn len(&self) -> usize { + self.inner.len() + } + + /// Returns `true` if this `InlineStr` has a length of 0 (in bytes), otherwise `false`. + pub fn is_empty(&self) -> bool { + self.len() == 0 + } +} + impl std::fmt::Display for InlineStr { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { std::fmt::Display::fmt(&**self, f) @@ -37,7 +91,7 @@ impl std::fmt::Debug for InlineStr { impl std::hash::Hash for InlineStr { fn hash(&self, state: &mut H) { - let as_str: &str = &*self; + let as_str = &**self; as_str.hash(state); } } @@ -66,55 +120,109 @@ impl From<&str> for InlineStr { } } -impl Deref for InlineStr { - type Target = str; +impl PartialOrd for InlineStr { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} - fn deref(&self) -> &Self::Target { - // Safety: - // InlineStr can only be created from valid UTF8 byte sequences - unsafe { str::from_utf8_unchecked(&self.inner) } +impl Ord for InlineStr { + fn cmp(&self, other: &Self) -> Ordering { + self.as_str().cmp(other.as_str()) } } impl PartialEq for InlineStr { fn eq(&self, other: &String) -> bool { - (**self).eq(other) + self.as_str() == other } } impl PartialEq for String { fn eq(&self, other: &InlineStr) -> bool { - other.eq(self) + self.as_str() == other.as_str() } } -impl<'a> PartialEq<&'a str> for InlineStr { - fn eq(&self, other: &&'a str) -> bool { - (&&**self).eq(other) +impl PartialEq<&'_ str> for InlineStr { + fn eq(&self, other: &&str) -> bool { + self.as_str() == *other } } impl PartialEq for &str { fn eq(&self, other: &InlineStr) -> bool { - other.eq(self) + *self == other.as_str() } } +impl PartialEq<&InlineStr> for &str { + fn eq(&self, other: &&InlineStr) -> bool { + self == *other + } +} impl PartialEq> for InlineStr { fn eq(&self, other: &Cow<'_, str>) -> bool { - (**self).eq(other) + self.as_str() == other } } impl PartialEq for Cow<'_, str> { fn eq(&self, other: &InlineStr) -> bool { - other.eq(self) + self.as_ref() == other.as_str() + } +} + +impl PartialEq for &InlineStr { + fn eq(&self, other: &InlineStr) -> bool { + self.as_str() == other.as_str() + } +} + +impl Deref for InlineStr { + type Target = str; + + fn deref(&self) -> &Self::Target { + self.as_str() + } +} + +impl AsRef for InlineStr { + fn as_ref(&self) -> &str { + self + } +} + +impl AsRef for InlineStr { + fn as_ref(&self) -> &Path { + self.as_str().as_ref() + } +} + +impl AsRef<[u8]> for InlineStr { + fn as_ref(&self) -> &[u8] { + self.inner.as_ref() + } +} + +impl AsRef for InlineStr { + fn as_ref(&self) -> &OsStr { + self.as_str().as_ref() + } +} + +impl Borrow for InlineStr { + fn borrow(&self) -> &str { + self.as_ref() } } #[cfg(test)] mod tests { - use std::hash::{BuildHasher, RandomState}; + use std::{ + collections::HashMap, + hash::{BuildHasher, RandomState}, + }; use super::*; @@ -142,4 +250,27 @@ mod tests { assert_eq!(words_hash, words_hash_2); assert_eq!(words_hash, inline_hash); } + + #[test] + fn test_borrow() { + let map = [(InlineStr::from("x"), 5)] + .into_iter() + .collect::>(); + + let v = map.get("x"); + assert_eq!(v, Some(&5)); + } + + #[cfg(feature = "serde")] + #[test] + fn test_serde() { + let s = "hello world"; + let inline_s = InlineStr::from("hello world"); + assert_eq!(s, inline_s); + let serialized_s = serde_json::to_value(s).unwrap(); + let serialized_inline = serde_json::to_value(inline_s.as_str()).unwrap(); + assert_eq!(serialized_s, serialized_inline); + let deserialized: InlineStr = serde_json::from_value(serialized_s).unwrap(); + assert_eq!(deserialized, "hello world"); + } } diff --git a/src/serde.rs b/src/serde.rs new file mode 100644 index 0000000..6d7ac32 --- /dev/null +++ b/src/serde.rs @@ -0,0 +1,36 @@ +// Copyright 2025 Adam Gutglick +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use serde::{Deserialize, Serialize, de::Deserializer, ser::Serializer}; + +use crate::InlineStr; + +impl Serialize for InlineStr { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_str(self.as_str()) + } +} + +impl<'de> Deserialize<'de> for InlineStr { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let s = String::deserialize(deserializer)?; + Ok(InlineStr::from(s)) + } +}