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::declare_joined_str;
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//! declare_joined_str!(ALL_JOINED, 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_str_join::declare_joined_str!(FLAGS, ["--help", "--version", "--verbose"], "|");
129/// const_str_join::declare_joined_str!(HELP, ["flags:", FLAGS], " ");
130/// assert_eq!(HELP, "flags: --help|--version|--verbose");
131/// ```
132
133#[macro_export]
134macro_rules! declare_joined_str {
135    ($name:ident, $array:expr, $sep:expr) => {
136        const $name: &'static str = 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        declare_joined_str!(FOO, ARRAY_OF_STRINGS, ":");
161        const MORE_PARTS: [&'static str; 3] = ["<", FOO, ">"];
162        declare_joined_str!(MORE, 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}