const_str_join/lib.rs
1#![no_std]
2
3//! Helper crate to concat static strings during compilation.
4//!
5//! Strings (`&'static str`) that are known during compile-time can't
6//! currently easily concatenated nor joined with a seperator. This
7//! tiny crate aims to help with the situation by providing conviencne
8//! macros that execute the required steps.
9//!
10//! Some of the limitations that have to be worked around are:
11//! * No const iterators, yet.
12//! * Allocating static memory must be done with a fixed and know size.
13//! * Writing into strings must be done "manually" bytewise as there
14//! is no copy implementation.
15//! * Byteslices aren't available in const-context requiring
16//! manually indexing into the buffers.
17//!
18//! All of those aren't show-stoppers but don't exactly enable
19//! idiomatic Rust code and require walking the thin line of whats
20//! possible and what isn't.
21//!
22//! Example usage:
23//! ```rust
24//! use const_str_join::const_join;
25//!
26//! const A: &'static str = "A";
27//! const B: &'static str = "B";
28//! const C: &'static str = "C";
29//! const ALL: [&'static str; 3] = [A, B, C] ;
30//! // declare a new &'static str with the final string
31//! const ALL_JOINED: &'static str = const_join!(ALL, ",");
32//! assert_eq!(ALL_JOINED, "A,B,C");
33//! ```
34
35#[doc(hidden)]
36pub const fn concated_size<const N: usize>(array: [&'static str; N], sep: &'static str) -> usize {
37 let mut n = if N == 0 { 0 } else { N - 1 } * sep.len();
38 let mut i = N;
39 loop {
40 if i <= 0 {
41 break;
42 }
43 i -= 1;
44 n += array[i].len();
45 }
46 n
47}
48
49#[doc(hidden)]
50pub const fn copy_bytes(src: &[u8], dest: &mut [u8], offset: usize) -> usize {
51 let mut i = 0;
52 let mut op = offset;
53 if !src.is_empty() {
54 loop {
55 assert!(dest[op] == b'\0');
56 dest[op] = src[i];
57 op += 1;
58 i += 1;
59 if i >= src.len() {
60 break;
61 }
62 }
63 }
64
65 op
66}
67
68#[doc(hidden)]
69pub const fn join_strings(inputs: &[&str], sep: Option<&str>, mut output: &mut [u8]) -> usize {
70 assert!(output.len() > 0);
71 let mut n = 0;
72 let mut op = 0;
73 loop {
74 op = copy_bytes(&inputs[n].as_bytes(), &mut output, op);
75
76 if n + 1 < inputs.len()
77 && let Some(sep) = sep
78 {
79 let s = sep.as_bytes();
80 op = copy_bytes(s, &mut output, op);
81 }
82
83 n += 1;
84 if n >= inputs.len() {
85 break;
86 }
87 }
88 op
89}
90
91/// Returns a buffer (`[u8; N]`) that contains the joined string as bytes.
92///
93/// Example usage:
94/// ```rust
95/// let s: [u8; _] = const_str_join::joined_array!(["A", "B", "C"], "<>");
96/// assert_eq!(&s, b"A<>B<>C")
97/// ```
98#[macro_export]
99macro_rules! joined_array {
100 ($array:expr, $sep:expr) => {
101 const {
102 const SIZE: usize = $crate::concated_size($array, $sep);
103 $crate::joined_array!($array, $sep, SIZE)
104 }
105 };
106 ($array:expr, $sep:expr, $size:expr) => {
107 const {
108 const ARRAY_LEN: usize = $size;
109
110 let sep = $sep;
111 let sep = if sep.len() > 0 { Some(sep) } else { None };
112 let array = &$array;
113
114 let mut buffer = [0u8; ARRAY_LEN];
115 let next_position = $crate::join_strings(array, sep, &mut buffer);
116
117 // when we are done we should have written to all the bytes, if not then something is off
118 assert!(next_position == ARRAY_LEN);
119 buffer
120 }
121 };
122
123}
124
125/// Declares a new constant value `name` with the joined string of `array` and `sep`.
126/// Example usage:
127/// ```rust
128/// const FLAGS: &'static str = const_str_join::const_join!(["--help", "--version", "--verbose"], "|");
129/// const HELP: &'static str = const_str_join::const_join!(["flags:", FLAGS], " ");
130/// assert_eq!(HELP, "flags: --help|--version|--verbose");
131/// ```
132
133#[macro_export]
134macro_rules! const_join {
135 ($array:expr, $sep:expr) => {
136 const {
137 const SIZE: usize = $crate::concated_size($array, $sep);
138 static STORAGE: [u8; SIZE] = $crate::joined_array!($array, $sep, SIZE);
139 // no unwrap in const :|
140 if let Ok(v) = core::str::from_utf8(&STORAGE) {
141 v
142 } else {
143 panic!("joined array isn't a valid utf8 string");
144 }
145 }
146 };
147}
148
149#[cfg(test)]
150mod tests {
151 use super::*;
152
153 const A: &'static str = "A";
154 const B: &'static str = "B";
155 const C: &'static str = "C";
156 const ARRAY_OF_STRINGS: [&'static str; 3] = [A, B, C];
157
158 #[test]
159 fn nested() {
160 const FOO: &'static str = const_join!(ARRAY_OF_STRINGS, ":");
161 const MORE_PARTS: [&'static str; 3] = ["<", FOO, ">"];
162 const MORE: &'static str = const_join!(MORE_PARTS, "");
163 assert_eq!(FOO, "A:B:C");
164 assert_eq!(MORE, "<A:B:C>");
165 }
166
167 #[test]
168 fn joined_array() {
169 let s = joined_array!(ARRAY_OF_STRINGS, "-");
170 assert_eq!(&s, b"A-B-C");
171 }
172}