const-str-proc-macro-0.7.0/.cargo_vcs_info.json0000644000000001620000000000100150330ustar { "git": { "sha1": "7a49d855603767d8b48a087adfb0e4b56bebf0bb" }, "path_in_vcs": "const-str-proc-macro" }const-str-proc-macro-0.7.0/Cargo.lock0000644000000063320000000000100130130ustar # This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 3 [[package]] name = "aho-corasick" version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" dependencies = [ "memchr", ] [[package]] name = "bytes" version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" [[package]] name = "const-str-proc-macro" version = "0.7.0" dependencies = [ "heck", "http", "proc-macro2", "quote", "regex", "syn", ] [[package]] name = "fnv" version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "heck" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "http" version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" dependencies = [ "bytes", "fnv", "itoa", ] [[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.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" [[package]] name = "proc-macro2" version = "1.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" dependencies = [ "unicode-ident", ] [[package]] name = "quote" version = "1.0.40" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" dependencies = [ "proc-macro2", ] [[package]] name = "regex" version = "1.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "23d7fd106d8c02486a8d64e778353d1cffe08ce79ac2e82f540c86d0facf6912" dependencies = [ "aho-corasick", "memchr", "regex-automata", "regex-syntax", ] [[package]] name = "regex-automata" version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6b9458fa0bfeeac22b5ca447c63aaf45f28439a709ccd244698632f9aa6394d6" dependencies = [ "aho-corasick", "memchr", "regex-syntax", ] [[package]] name = "regex-syntax" version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001" [[package]] name = "syn" version = "2.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[package]] name = "unicode-ident" version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" const-str-proc-macro-0.7.0/Cargo.toml0000644000000024620000000000100130360ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2021" rust-version = "1.65.0" name = "const-str-proc-macro" version = "0.7.0" authors = ["Nugine "] build = false autolib = false autobins = false autoexamples = false autotests = false autobenches = false description = "compile-time string operations" readme = "README.md" keywords = [ "string", "const", "proc-macro", ] categories = [ "text-processing", "no-std", ] license = "MIT" repository = "https://github.com/Nugine/const-str" [lib] name = "const_str_proc_macro" path = "src/lib.rs" proc-macro = true [dependencies.heck] version = "0.5.0" optional = true [dependencies.http] version = "1.0.0" optional = true [dependencies.proc-macro2] version = "1.0.47" [dependencies.quote] version = "1.0.21" [dependencies.regex] version = "1.7.0" optional = true [dependencies.syn] version = "2.0.2" const-str-proc-macro-0.7.0/Cargo.toml.orig000064400000000000000000000012731046102023000165160ustar 00000000000000[package] name = "const-str-proc-macro" version = "0.7.0" authors = ["Nugine "] edition = "2021" description = "compile-time string operations" license = "MIT" repository = "https://github.com/Nugine/const-str" keywords = ["string", "const", "proc-macro"] categories = ["text-processing", "no-std"] readme = "../README.md" rust-version = "1.65.0" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [lib] proc-macro = true [dependencies] syn = "2.0.2" quote = "1.0.21" regex = { version = "1.7.0", optional = true } http = { version = "1.0.0", optional = true } proc-macro2 = "1.0.47" heck = { version = "0.5.0", optional = true } const-str-proc-macro-0.7.0/LICENSE000064400000000000000000000020541046102023000146320ustar 00000000000000MIT License Copyright (c) 2020-2025 Nugine 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: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. const-str-proc-macro-0.7.0/README.md000064400000000000000000000014131046102023000151020ustar 00000000000000# const-str [![Crates.io][crates-badge]][crates-url] [![MIT licensed][mit-badge]][mit-url] [![Docs][docs-badge]][docs-url] [![Downloads]][downloads] [crates-badge]: https://img.shields.io/crates/v/const-str.svg [crates-url]: https://crates.io/crates/const-str [mit-badge]: https://img.shields.io/badge/license-MIT-blue.svg [mit-url]: ./LICENSE [docs-badge]: https://docs.rs/const-str/badge.svg [docs-url]: https://docs.rs/const-str/ [downloads]: https://img.shields.io/crates/d/const-str Compile-time string operations Documentation: ## Contributing + [Development Guide](./CONTRIBUTING.md) ## Sponsor If my open-source work has been helpful to you, please [sponsor me](https://github.com/Nugine#sponsor). Every little bit helps. Thank you! const-str-proc-macro-0.7.0/src/case.rs000064400000000000000000000060711046102023000157000ustar 00000000000000use proc_macro::TokenStream; use quote::ToTokens; use syn::parse::{Parse, ParseStream}; use syn::{Ident, LitStr, Token}; enum Case { Lower, Upper, #[cfg(feature = "heck")] LowerCamel, #[cfg(feature = "heck")] UpperCamel, #[cfg(feature = "heck")] Title, #[cfg(feature = "heck")] Train, #[cfg(feature = "heck")] Snake, #[cfg(feature = "heck")] Kebab, #[cfg(feature = "heck")] ShoutySnake, #[cfg(feature = "heck")] ShoutyKebab, } impl Case { fn convert(&self, s: &str) -> String { #[cfg(feature = "heck")] use heck::{ ToKebabCase, // ToLowerCamelCase, // ToShoutyKebabCase, // ToShoutySnakeCase, // ToSnakeCase, // ToTitleCase, // ToTrainCase, // ToUpperCamelCase, // }; match self { Case::Lower => s.to_lowercase(), Case::Upper => s.to_uppercase(), #[cfg(feature = "heck")] Case::LowerCamel => s.to_lower_camel_case(), #[cfg(feature = "heck")] Case::UpperCamel => s.to_upper_camel_case(), #[cfg(feature = "heck")] Case::Title => s.to_title_case(), #[cfg(feature = "heck")] Case::Train => s.to_train_case(), #[cfg(feature = "heck")] Case::Snake => s.to_snake_case(), #[cfg(feature = "heck")] Case::Kebab => s.to_kebab_case(), #[cfg(feature = "heck")] Case::ShoutySnake => s.to_shouty_snake_case(), #[cfg(feature = "heck")] Case::ShoutyKebab => s.to_shouty_kebab_case(), } } } pub struct ConvertCase { case: Case, src: LitStr, } impl Parse for ConvertCase { fn parse(input: ParseStream) -> syn::Result { let case = input.parse::()?.to_string(); let case = match case.as_str() { "lower" => Case::Lower, "upper" => Case::Upper, #[cfg(feature = "heck")] "lower_camel" => Case::LowerCamel, #[cfg(feature = "heck")] "upper_camel" => Case::UpperCamel, #[cfg(feature = "heck")] "title" => Case::Title, #[cfg(feature = "heck")] "train" => Case::Train, #[cfg(feature = "heck")] "snake" => Case::Snake, #[cfg(feature = "heck")] "kebab" => Case::Kebab, #[cfg(feature = "heck")] "shouty_snake" => Case::ShoutySnake, #[cfg(feature = "heck")] "shouty_kebab" => Case::ShoutyKebab, _ => return Err(input.error("unsupported case")), }; input.parse::()?; let src = input.parse::()?; Ok(Self { case, src }) } } impl ConvertCase { pub fn eval(&self) -> TokenStream { let src = self.src.value(); let dst = self.case.convert(&src); let dst_token = LitStr::new(&dst, self.src.span()); dst_token.into_token_stream().into() } } const-str-proc-macro-0.7.0/src/fmt.rs000064400000000000000000000260511046102023000155530ustar 00000000000000use std::collections::HashMap; use std::{fmt, mem}; use proc_macro::TokenStream; use proc_macro2::TokenStream as TokenStream2; use quote::quote; use syn::parse::{Parse, ParseStream}; use syn::{Expr, Ident, LitStr, Token}; #[derive(Debug)] struct FmtPart { pub literal: Option, pub pos: Option, pub name: Option, pub method: Option, pub spec: FmtSpec, } #[derive(Debug)] struct FmtSpec { pub alternate: bool, } #[derive(Debug)] enum FmtMethod { Debug, Display, LowerHex, UpperHex, Binary, } impl FmtSpec { fn empty() -> Self { Self { alternate: false } } fn alternate() -> Self { Self { alternate: true } } } impl FmtPart { #[cfg(test)] fn literal_str(&self) -> Option<&str> { self.literal.as_deref() } #[cfg(test)] fn named_ident(&self) -> Option<&Ident> { self.name.as_ref() } fn from_literal(lit: String) -> Self { Self { literal: Some(lit), pos: None, name: None, method: None, spec: FmtSpec::empty(), } } fn from_positional(pos: usize, method: FmtMethod, spec: FmtSpec) -> Self { Self { literal: None, pos: Some(pos), name: None, method: Some(method), spec, } } fn from_named(name: Ident, method: FmtMethod, spec: FmtSpec) -> Self { Self { literal: None, pos: None, name: Some(name), method: Some(method), spec, } } } #[derive(Debug)] struct ParseError { _priv: (), } impl ParseError { fn new() -> Self { Self { _priv: () } } } impl fmt::Display for ParseError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str("unsupported format string") } } fn parse_fmt_string(s: &str) -> Result, ParseError> { let mut ans = Vec::new(); let mut iter = s.chars(); let mut literal: String = String::new(); let mut fmt_spec: String = String::new(); let mut pos_iter = 0; loop { match iter.next() { None => { if !literal.is_empty() { ans.push(FmtPart::from_literal(mem::take(&mut literal))); } break; } Some('{') => match iter.next() { None => { return Err(ParseError::new()); } Some('{') => { literal.push('{'); continue; } Some(mut ch) => { if !literal.is_empty() { ans.push(FmtPart::from_literal(mem::take(&mut literal))); } while ch != '}' { fmt_spec.push(ch); match iter.next() { Some(c) => ch = c, None => return Err(ParseError::new()), } } ans.push(parse_fmt_spec(&fmt_spec, &mut pos_iter)?); fmt_spec.clear(); } }, Some('}') => match iter.next() { Some('}') => { literal.push('}'); continue; } _ => return Err(ParseError::new()), }, Some(ch) => literal.push(ch), } } Ok(ans) } fn parse_fmt_spec(s: &str, pos_iter: &mut usize) -> Result { let pieces = s.split(':').collect::>(); if pieces.len() > 2 { return Err(ParseError::new()); } let (method, spec) = match pieces.get(1).copied() { Some("?") => (FmtMethod::Debug, FmtSpec::empty()), Some("#?") => (FmtMethod::Debug, FmtSpec::alternate()), Some("") | None => (FmtMethod::Display, FmtSpec::empty()), Some("x") => (FmtMethod::LowerHex, FmtSpec::empty()), Some("#x") => (FmtMethod::LowerHex, FmtSpec::alternate()), Some("X") => (FmtMethod::UpperHex, FmtSpec::empty()), Some("#X") => (FmtMethod::UpperHex, FmtSpec::alternate()), Some("b") => (FmtMethod::Binary, FmtSpec::empty()), Some("#b") => (FmtMethod::Binary, FmtSpec::alternate()), _ => return Err(ParseError::new()), }; let argument = pieces[0]; if argument.is_empty() { let pos = *pos_iter; *pos_iter += 1; return Ok(FmtPart::from_positional(pos, method, spec)); } if let Ok(pos) = argument.parse::() { return Ok(FmtPart::from_positional(pos, method, spec)); } if let Ok(name) = syn::parse_str::(argument) { return Ok(FmtPart::from_named(name, method, spec)); } Err(ParseError::new()) } #[test] fn test_parse_fmt() { { let s = ""; assert!(parse_fmt_string(s).unwrap().is_empty()); } { let s = "{}"; let parts = parse_fmt_string(s).unwrap(); assert_eq!(parts.len(), 1); assert!(matches!(parts[0].pos, Some(0))) } { let s = "{1} {} {0} {}"; let parts = parse_fmt_string(s).unwrap(); assert_eq!(parts.len(), 7); assert!(matches!(parts[0].pos, Some(1))); assert!(matches!(parts[1].literal_str().unwrap(), " ")); assert!(matches!(parts[2].pos, Some(0))); assert!(matches!(parts[3].literal_str().unwrap(), " ")); assert!(matches!(parts[4].pos, Some(0))); assert!(matches!(parts[5].literal_str().unwrap(), " ")); assert!(matches!(parts[6].pos, Some(1))); } { let s = "{argument}"; let parts = parse_fmt_string(s).unwrap(); assert_eq!(parts.len(), 1); assert_eq!(parts[0].named_ident().unwrap(), "argument"); } { let s = "{name} {}"; let parts = parse_fmt_string(s).unwrap(); assert_eq!(parts.len(), 3); assert_eq!(parts[0].named_ident().unwrap(), "name"); assert!(matches!(parts[1].literal_str().unwrap(), " ")); assert!(matches!(parts[2].pos, Some(0))); } { let s = "{a} {c} {b}"; let parts = parse_fmt_string(s).unwrap(); assert_eq!(parts.len(), 5); assert_eq!(parts[0].named_ident().unwrap(), "a"); assert!(matches!(parts[1].literal_str().unwrap(), " ")); assert_eq!(parts[2].named_ident().unwrap(), "c"); assert!(matches!(parts[3].literal_str().unwrap(), " ")); assert_eq!(parts[4].named_ident().unwrap(), "b"); } { let s = "{{}}"; let parts = parse_fmt_string(s).unwrap(); assert_eq!(parts.len(), 1); assert_eq!(parts[0].literal_str().unwrap(), "{}"); } } pub struct ConstFormat { fmt_string: LitStr, positional_args: Vec, named_args: HashMap, } impl Parse for ConstFormat { fn parse(input: ParseStream) -> syn::Result { let fmt_string = input.parse::()?; let mut comma = input.parse::>()?; let mut positional_args = Vec::new(); let mut named_args = HashMap::new(); if input.is_empty() { return Ok(ConstFormat { fmt_string, positional_args, named_args, }); } while !input.is_empty() && !input.peek2(Token![=]) { if comma.is_none() { return Err(input.error("expected comma")); } let arg = input.parse::()?; comma = input.parse::>()?; positional_args.push(arg); } if input.is_empty() { return Ok(ConstFormat { fmt_string, positional_args, named_args, }); } while input.peek2(Token![=]) { if comma.is_none() { return Err(input.error("expected comma")); } let name = input.parse::()?; let _ = input.parse::()?; let kwarg = input.parse::()?; comma = input.parse::>()?; let prev = named_args.insert(name, kwarg); if prev.is_some() { return Err(input.error("duplicate argument")); } } if input.is_empty() { Ok(ConstFormat { fmt_string, positional_args, named_args, }) } else { Err(input.error("unexpected tokens")) } } } impl ConstFormat { fn fmt_method(method: &FmtMethod) -> proc_macro2::TokenStream { match method { FmtMethod::Debug => quote! { __fmt_debug }, FmtMethod::Display => quote! { __fmt_display }, FmtMethod::LowerHex => quote! { __fmt_lowerhex }, FmtMethod::UpperHex => quote! { __fmt_upperhex }, FmtMethod::Binary => quote! { __fmt_binary }, } } fn fmt_spec(part: &FmtPart) -> proc_macro2::TokenStream { let alternate = part.spec.alternate; quote! {{ FmtSpec { alternate: #alternate } }} } pub fn eval(&self) -> TokenStream { let parts = match parse_fmt_string(&self.fmt_string.value()) { Ok(p) => p, Err(err) => return proc_error!(self.fmt_string, err.to_string()), }; let mut eval_parts: Vec = Vec::new(); for p in parts { match self.convert_part(p) { Ok(tt) => eval_parts.push(tt), Err(err) => return err, } } let tt = quote! { { &[ #(#eval_parts)* ] } }; tt.into() } fn convert_part(&self, p: FmtPart) -> Result { if let Some(ref s) = p.literal { return Ok(quote! { { #s }, }); } if let Some(pos) = p.pos { let method = p.method.as_ref().unwrap(); match self.positional_args.get(pos) { None => { return Err(proc_error!( self.fmt_string, std::format!( "invalid reference to positional argument {pos} (no arguments were given)" ) )) } Some(arg) => { let method_ident = Self::fmt_method(method); let spec = Self::fmt_spec(&p); return Ok(quote! { { #method_ident!(#arg, #spec) }, }); } } } if let Some(ref name) = p.name { let method_ident = Self::fmt_method(p.method.as_ref().unwrap()); let spec = Self::fmt_spec(&p); return Ok(match self.named_args.get(name) { None => quote! { { #method_ident!(#name, #spec) }, }, Some(kwarg) => quote! { { #method_ident!(#kwarg, #spec) }, }, }); } unreachable!() } } const-str-proc-macro-0.7.0/src/lib.rs000064400000000000000000000047461046102023000155420ustar 00000000000000//! const-str proc macros #![forbid(unsafe_code)] #![deny(missing_docs, clippy::all, clippy::cargo)] #![allow( clippy::missing_docs_in_private_items, clippy::missing_inline_in_public_items, clippy::implicit_return )] #[allow(unused_macros)] macro_rules! proc_error { ($token:expr, $msg: expr) => { TokenStream::from(syn::Error::new($token.span(), $msg).to_compile_error()) }; } mod case; mod fmt; #[cfg(feature = "regex")] mod regex; use proc_macro::TokenStream; use quote::ToTokens; use syn::parse::Parse; use syn::spanned::Spanned; use syn::{parse_macro_input, LitStr}; #[allow(dead_code)] fn direct_convert(input: TokenStream, f: F) -> TokenStream where T: Parse + Spanned, E: ToString, F: FnOnce(&T) -> Result, { let src_token: T = parse_macro_input!(input as T); let s = match f(&src_token) { Ok(s) => s, Err(e) => return proc_error!(src_token, e.to_string()), }; let dst_token = LitStr::new(&s, src_token.span()); dst_token.into_token_stream().into() } #[doc(hidden)] #[proc_macro] pub fn format_parts(input: TokenStream) -> TokenStream { use crate::fmt::ConstFormat; let m = parse_macro_input!(input as ConstFormat); m.eval() } /// Converts a string literal to a specified case. #[proc_macro] pub fn convert_case(input: TokenStream) -> TokenStream { use crate::case::ConvertCase; let m = parse_macro_input!(input as ConvertCase); m.eval() } // ----------------------------------------------------------------------------- /// Returns a compile-time verified header name string literal. #[cfg(feature = "http")] #[proc_macro] pub fn verified_header_name(input: TokenStream) -> TokenStream { use http::header::HeaderName; direct_convert(input, |s: &LitStr| { let s = s.value(); HeaderName::from_lowercase(s.as_bytes()).map(|_| s) }) } // ----------------------------------------------------------------------------- /// Returns a compile-time verified regex string literal. #[cfg(feature = "regex")] #[proc_macro] pub fn verified_regex(input: TokenStream) -> TokenStream { direct_convert(input, |s: &LitStr| { let s = s.value(); ::regex::Regex::new(&s).map(|_| s) }) } /// Asserts that the string literal matches the pattern. #[cfg(feature = "regex")] #[proc_macro] pub fn regex_assert_match(input: TokenStream) -> TokenStream { use crate::regex::RegexAssertMatch; let m = parse_macro_input!(input as RegexAssertMatch); m.eval() } const-str-proc-macro-0.7.0/src/regex.rs000064400000000000000000000015711046102023000160770ustar 00000000000000use proc_macro::TokenStream; use syn::parse::{Parse, ParseStream}; use syn::{LitStr, Token}; pub struct RegexAssertMatch { re: LitStr, text: LitStr, } impl Parse for RegexAssertMatch { fn parse(input: ParseStream) -> syn::Result { let re = input.parse::()?; let _ = input.parse::()?; let text = input.parse::()?; Ok(Self { re, text }) } } impl RegexAssertMatch { pub fn eval(&self) -> TokenStream { use regex::Regex; let re: Regex = match Regex::new(&self.re.value()) { Ok(re) => re, Err(e) => return proc_error!(self.re, e.to_string()), }; let text = self.text.value(); if !re.is_match(&text) { return proc_error!(self.text, "the string literal does not match the pattern"); } TokenStream::new() } }