#![deny(unsafe_code)] #![deny(missing_docs)] #![deny(rustdoc::broken_intra_doc_links)] #![warn(clippy::pedantic)] #![warn(clippy::nursery)] #![allow(clippy::module_name_repetitions)] #![cfg_attr(docsrs, feature(doc_cfg))] //! # `TypeID` Prefix //! //! This crate provides a type-safe implementation of the `TypePrefix` section of the //! [TypeID Specification](https://github.com/jetpack-io/typeid). //! //! The main type provided by this crate is [`TypeIdPrefix`], which represents a valid //! `TypeID` prefix. This type ensures that all instances conform to the `TypeID` specification: //! //! - Maximum length of 63 characters //! - Contains only lowercase ASCII letters and underscores //! - Does not start or end with an underscore //! - Starts and ends with a lowercase letter //! //! ## Features //! //! - **Type-safe**: Ensures that `TypeID` prefixes conform to the specification. //! - **Validation**: Provides robust validation for `TypeID` prefixes. //! - **Sanitization**: Offers methods to clean and sanitize input strings into valid `TypeID` prefixes. //! - **Zero-cost abstractions**: Designed to have minimal runtime overhead. //! - **Optional tracing**: Integrates with the `tracing` crate for logging (optional feature). //! //! ## Usage //! //! ```rust //! use typeid_prefix::prelude::*; //! use std::convert::TryFrom; //! //! // Create a TypeIdPrefix from a valid string //! let prefix = TypeIdPrefix::try_from("user").unwrap(); //! assert_eq!(prefix.as_str(), "user"); //! //! // Attempt to create from an invalid string //! let result = TypeIdPrefix::try_from("Invalid_Prefix"); //! assert!(result.is_err()); //! //! // Sanitize an invalid string //! let sanitized = "Invalid_Prefix123".create_prefix_sanitized(); //! assert_eq!(sanitized.as_str(), "invalid_prefix"); //! ``` //! //! ## Optional Tracing //! //! When the `instrument` feature is enabled, the crate will log validation errors //! using the `tracing` crate. #[cfg(feature = "instrument")] use tracing; pub use type_id_prefix::TypeIdPrefix; pub use crate::error::ValidationError; mod error; mod traits; mod type_id_prefix; pub mod prelude { //! A prelude for the `TypeID` prefix crate. //! //! This module contains the most commonly used items from the crate. //! //! # Usage //! //! ``` //! use typeid_prefix::prelude::*; //! ``` pub use crate::{TypeIdPrefix, ValidationError}; pub use crate::traits::{PrefixFactory, Validate}; } #[cfg(test)] mod tests { use std::convert::TryFrom; use crate::prelude::*; use super::*; #[test] fn test_type_id_spaces_sanitize() { assert_eq!( "Invalid String with Spaces!!__".create_prefix_sanitized().as_str(), "invalidstringwithspaces" ); } #[test] fn test_type_id_truncation() { assert_eq!( "A_valid_string_that_is_way_too_long_and_should_be_truncated_to_63_chars".create_prefix_sanitized().as_str(), "a_valid_string_that_is_way_too_long_and_should_be_truncated_to" ); } #[test] fn test_type_id_underscores_sanitize() { assert_eq!( "_underscores__everywhere__".create_prefix_sanitized().as_str(), "underscores__everywhere" ); } #[test] fn test_typeid_prefix_non_ascii() { assert!(TypeIdPrefix::try_from("🌀").is_err()); let sanitized_input = "🌀".create_prefix_sanitized(); assert!(sanitized_input.as_str().is_empty(), "Prefix was not empty: {sanitized_input}"); } #[test] fn test_typeid_prefix_empty() { assert!(TypeIdPrefix::try_from("").is_ok()); } #[test] fn test_typeid_prefix_single_char() { assert!(TypeIdPrefix::try_from("a").is_ok()); } #[test] fn test_typeid_prefix_valid_string() { assert!(TypeIdPrefix::try_from("valid_string").is_ok()); } #[test] fn test_typeid_prefix_with_underscores() { assert!(TypeIdPrefix::try_from("valid_string_with_underscores").is_ok()); } #[test] fn test_typeid_prefix_exceeds_max_length() { let input = "a_valid_string_with_underscores_and_length_of_63_characters_____"; assert_eq!( TypeIdPrefix::try_from(input).unwrap_err(), ValidationError::ExceedsMaxLength ); assert_eq!( input.create_prefix_sanitized().as_str(), "a_valid_string_with_underscores_and_length_of__characters" ); } #[test] fn test_typeid_prefix_invalid_characters() { assert_eq!( TypeIdPrefix::try_from("InvalidString").unwrap_err(), ValidationError::InvalidStartCharacter ); assert_eq!("InvalidString".create_prefix_sanitized().as_str(), "invalidstring"); } #[test] fn test_typeid_prefix_starts_with_underscore() { assert_eq!( TypeIdPrefix::try_from("_invalid").unwrap_err(), ValidationError::StartsWithUnderscore ); assert_eq!("_invalid".create_prefix_sanitized().as_str(), "invalid"); } #[test] fn test_typeid_prefix_ends_with_underscore() { assert_eq!( TypeIdPrefix::try_from("invalid_").unwrap_err(), ValidationError::EndsWithUnderscore ); assert_eq!("invalid_".create_prefix_sanitized().as_str(), "invalid"); } #[test] fn test_typeid_prefix_invalid_characters_with_spaces() { assert_eq!( TypeIdPrefix::try_from("invalid string with spaces").unwrap_err(), ValidationError::ContainsInvalidCharacters ); assert_eq!("invalid string with spaces".create_prefix_sanitized().as_str(), "invalidstringwithspaces"); } #[test] fn test_typeid_prefix_max_length() { let input = "a".repeat(63); assert!(TypeIdPrefix::try_from(input.as_str()).is_ok()); } #[test] fn test_typeid_prefix_ordering() { let prefix1 = TypeIdPrefix::try_from("alpha").unwrap(); let prefix2 = TypeIdPrefix::try_from("beta").unwrap(); let prefix3 = TypeIdPrefix::try_from("gamma").unwrap(); assert!(prefix1 < prefix2, "Expected 'alpha' to be less than 'beta'"); assert!(prefix2 < prefix3, "Expected 'beta' to be less than 'gamma'"); assert!(prefix1 < prefix3, "Expected 'alpha' to be less than 'gamma'"); } #[test] fn test_typeid_prefix_max_length_exceeded() { let input = "a".repeat(64); assert_eq!( TypeIdPrefix::try_from(input.as_str()).unwrap_err(), ValidationError::ExceedsMaxLength ); assert_eq!(input.create_prefix_sanitized().as_str(), "a".repeat(63)); } #[test] fn test_typeid_prefix_contains_uppercase() { assert_eq!( TypeIdPrefix::try_from("InvalidString").unwrap_err(), ValidationError::InvalidStartCharacter ); assert_eq!("InvalidString".create_prefix_sanitized().as_str(), "invalidstring"); } #[test] fn test_typeid_prefix_non_alphanumeric() { assert_eq!( TypeIdPrefix::try_from("invalid_string!").unwrap_err(), ValidationError::InvalidEndCharacter ); assert_eq!("invalid_string!".create_prefix_sanitized().as_str(), "invalid_string"); } #[test] fn test_typeid_prefix_numeric_start() { assert_eq!( TypeIdPrefix::try_from("1invalid").unwrap_err(), ValidationError::InvalidStartCharacter ); assert_eq!("1invalid".create_prefix_sanitized().as_str(), "invalid"); } #[test] fn test_typeid_prefix_numeric_end() { assert_eq!( TypeIdPrefix::try_from("invalid1").unwrap_err(), ValidationError::InvalidEndCharacter ); assert_eq!("invalid1".create_prefix_sanitized().as_str(), "invalid"); } }