Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 14 additions & 7 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ name: Rust Build

on:
push:
branches: [ "main" ]
branches: ["main"]
pull_request:
branches: [ "main" ]
branches: ["main"]

env:
CARGO_TERM_COLOR: always
Expand All @@ -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
11 changes: 7 additions & 4 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[package]
name = "inline-str"
version = "0.4.0"
edition = "2021"
version = "0.5.0"
edition = "2024"
authors = ["Adam Gutglick <adamgsal@gmail.com>"]
description = "Efficent and immutable string type, backed by inline-array"
license = "Apache-2.0 OR MIT"
Expand All @@ -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"]
2 changes: 1 addition & 1 deletion LICENSE-MIT
Original file line number Diff line number Diff line change
@@ -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:

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -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.

Expand Down
177 changes: 154 additions & 23 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -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)
Expand All @@ -37,7 +91,7 @@ impl std::fmt::Debug for InlineStr {

impl std::hash::Hash for InlineStr {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
let as_str: &str = &*self;
let as_str = &**self;
as_str.hash(state);
}
}
Expand Down Expand Up @@ -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<Ordering> {
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<String> for InlineStr {
fn eq(&self, other: &String) -> bool {
(**self).eq(other)
self.as_str() == other
}
}

impl PartialEq<InlineStr> 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<InlineStr> 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<Cow<'_, str>> for InlineStr {
fn eq(&self, other: &Cow<'_, str>) -> bool {
(**self).eq(other)
self.as_str() == other
}
}

impl PartialEq<InlineStr> for Cow<'_, str> {
fn eq(&self, other: &InlineStr) -> bool {
other.eq(self)
self.as_ref() == other.as_str()
}
}

impl PartialEq<InlineStr> 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<str> for InlineStr {
fn as_ref(&self) -> &str {
self
}
}

impl AsRef<Path> 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<OsStr> for InlineStr {
fn as_ref(&self) -> &OsStr {
self.as_str().as_ref()
}
}

impl Borrow<str> 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::*;

Expand Down Expand Up @@ -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::<HashMap<InlineStr, i32>>();

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");
}
}
36 changes: 36 additions & 0 deletions src/serde.rs
Original file line number Diff line number Diff line change
@@ -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<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(self.as_str())
}
}

impl<'de> Deserialize<'de> for InlineStr {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
Ok(InlineStr::from(s))
}
}