lightstreamer-rs 0.3.1

A Rust client for Lightstreamer, designed to facilitate real-time communication with Lightstreamer servers.
Documentation
use std::sync::Arc;
use tokio::sync::Notify;
#[cfg(windows)]
use tracing::error;
use tracing::info;

/// Clean the message from newlines and carriage returns.
///
/// Only the **message type prefix** (the portion before the first comma) is
/// lowercased so that downstream `match` arms can use lowercase literals
/// (`"u"`, `"conok"`, …). The rest of the message — including field values,
/// session identifiers and protocol tokens — is preserved verbatim so that
/// casing‑sensitive data (e.g. `DEAL`, `^P`, `^T`) is not corrupted.
///
/// Content inside curly braces `{}` is always preserved as‑is.
pub fn clean_message(text: &str) -> String {
    let chars_to_replace = ['\n', '\r'];
    let stripped = text.replace(chars_to_replace, "");

    // Find the first comma that is outside curly braces.
    let mut in_brackets: usize = 0;
    let mut first_comma: Option<usize> = None;
    for (i, c) in stripped.char_indices() {
        match c {
            '{' => in_brackets += 1,
            '}' => in_brackets = in_brackets.saturating_sub(1),
            ',' if in_brackets == 0 => {
                first_comma = Some(i);
                break;
            }
            _ => {}
        }
    }

    match first_comma {
        Some(pos) => {
            // Lowercase only the message‑type prefix.
            let mut result = String::with_capacity(stripped.len());
            result.push_str(&stripped[..pos].to_lowercase());
            result.push_str(&stripped[pos..]);
            result
        }
        None => {
            // No comma found — the whole string is the prefix (e.g. "PROBE").
            stripped.to_lowercase()
        }
    }
}

/// Parses a comma-separated string input into a vector of string slices (`Vec<&str>`).
///
/// This function supports skipping commas inside nested curly braces `{}`. It correctly handles
/// nested structures, ensuring that commas within curly braces are not treated as delimiters.
///
/// # Parameters
/// - `input`: A string slice (`&str`) containing comma-separated values, potentially with nested curly braces.
///
/// # Returns
/// A `Vec<&str>` containing trimmed substrings split by commas outside of curly braces.
///
/// # Behavior
/// - Commas outside of curly braces `{}` are treated as delimiters.
/// - Commas inside curly braces are ignored for splitting purposes.
/// - Leading and trailing whitespace around substrings are trimmed.
/// - Empty substrings (those consisting solely of whitespace) are ignored.
///
/// # Caveats
/// - The function requires matched curly braces `{}`. If the input contains unmatched curly braces,
///   the function may produce unexpected results.
///
/// # Panics
/// This function does not explicitly panic, but improper manipulation of indices or unmatched
/// braces could lead to unintended behavior.
///
/// # Errors in Current Code:
/// - There is a bug in the code where `arguments.push(*slice)` is used. The dereference operator
///   (`*`) is invalid for string slices. It should be `arguments.push(slice)`.
/// - Recommend fixing this bug by removing the dereference operator for proper functionality.
pub fn parse_arguments(input: &str) -> Vec<&str> {
    let mut arguments = Vec::new();
    let mut start = 0;
    let mut in_brackets = 0; // Tracks nesting level for curly braces

    for (i, c) in input.char_indices() {
        match c {
            '{' => in_brackets += 1,
            '}' => in_brackets -= 1,
            ',' if in_brackets == 0 => {
                // Outside of brackets, treat comma as a delimiter
                let slice = &input[start..i].trim();
                if !slice.is_empty() {
                    arguments.push(*slice); // Dereference slice here
                }
                start = i + 1;
            }
            _ => {}
        }
    }

    // Push the final argument if it's not empty
    if start < input.len() {
        let slice = &input[start..].trim();
        if !slice.is_empty() {
            arguments.push(*slice); // Dereference slice here
        }
    }

    arguments
}

/// Sets up cross-platform signal handling for graceful shutdown.
/// On Unix systems, handles SIGINT and SIGTERM signals.
/// On Windows, handles Ctrl+C signal.
///
/// # Arguments
///
/// * `shutdown_signal` - `Arc<Notify>` to signal shutdown to other parts of the application.
///
/// # Panics
///
/// The function panics if it fails to create the signal handlers.
///
pub async fn setup_signal_hook(shutdown_signal: Arc<Notify>) {
    #[cfg(unix)]
    {
        use tokio::signal::unix::{SignalKind, signal};
        let mut sigint = signal(SignalKind::interrupt()).expect("Failed to create SIGINT handler");
        let mut sigterm =
            signal(SignalKind::terminate()).expect("Failed to create SIGTERM handler");

        tokio::spawn(async move {
            tokio::select! {
                _ = sigint.recv() => {
                    info!("Received SIGINT signal");
                    shutdown_signal.notify_one();
                }
                _ = sigterm.recv() => {
                    info!("Received SIGTERM signal");
                    shutdown_signal.notify_one();
                }
            }
        });
    }

    #[cfg(windows)]
    {
        use tokio::signal;
        tokio::spawn(async move {
            match signal::ctrl_c().await {
                Ok(()) => {
                    info!("Received Ctrl+C signal");
                    shutdown_signal.notify_one();
                }
                Err(err) => {
                    error!("Failed to listen for Ctrl+C: {}", err);
                }
            }
        });
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    mod clean_message_tests {
        use super::*;

        #[test]
        fn test_clean_message_basic() {
            // No comma → entire string is the prefix → lowercased
            let text = "Hello\nWorld";
            let result = clean_message(text);
            assert_eq!(result, "helloworld");
        }

        #[test]
        fn test_clean_message_with_partial_braces() {
            // No comma → entire string is the prefix → lowercased
            let text = "{partial brace content} followed by text";
            let result = clean_message(text);
            assert_eq!(result, "{partial brace content} followed by text");
        }

        #[test]
        fn test_clean_message_with_ending_brace() {
            // No comma → entire string is the prefix → lowercased
            let text = "text followed by {partial brace content}";
            let result = clean_message(text);
            assert_eq!(result, "text followed by {partial brace content}");
        }

        #[test]
        fn test_clean_message_with_carriage_return() {
            // No comma → entire string is the prefix → lowercased
            let text = "Hello\r\nWorld";
            let result = clean_message(text);
            assert_eq!(result, "helloworld");
        }

        #[test]
        fn test_clean_message_lowercase_conversion() {
            // No comma → entire string is the prefix → lowercased
            let text = "Hello WORLD";
            let result = clean_message(text);
            assert_eq!(result, "hello world");
        }

        #[test]
        fn test_clean_message_empty_string() {
            let text = "";
            let result = clean_message(text);
            assert_eq!(result, "");
        }

        #[test]
        fn test_clean_message_preserve_braces_content() {
            // No comma → entire string is the prefix → lowercased (braces don't matter
            // when the whole message is a single token).
            let text = "Message with {Preserved\nContent} and not preserved\nContent";
            let result = clean_message(text);
            assert_eq!(
                result,
                "message with {preservedcontent} and not preservedcontent"
            );
        }

        #[test]
        fn test_clean_message_nested_braces() {
            // No comma → entire string is the prefix → lowercased
            let text = "Message with {Outer{Inner\nContent}Outer} and regular\nContent";
            let result = clean_message(text);
            assert_eq!(
                result,
                "message with {outer{innercontent}outer} and regularcontent"
            );
        }

        #[test]
        fn test_clean_message_unbalanced_braces() {
            // No comma → entire string is the prefix → lowercased
            let text = "Message with {Unbalanced and regular\nContent";
            let result = clean_message(text);
            assert_eq!(result, "message with {unbalanced and regularcontent");
        }

        #[test]
        fn test_clean_message_protocol_example() {
            // Only the prefix is lowercased; the session ID preserves casing.
            let text = "CONOK,S8f4aec42c3c14ad0,50000,5000,*\r\n";
            let result = clean_message(text);
            assert_eq!(result, "conok,S8f4aec42c3c14ad0,50000,5000,*");

            let text = "PROBE\r\n";
            let result = clean_message(text);
            assert_eq!(result, "probe");
        }

        #[test]
        fn test_clean_message_update_preserves_field_values() {
            // Update messages: only "U" → "u", field values are preserved.
            let text = "U,1,1,DEAL|1.32|#|^P\r\n";
            let result = clean_message(text);
            assert_eq!(result, "u,1,1,DEAL|1.32|#|^P");
        }

        #[test]
        fn test_clean_message_update_preserves_caret_commands() {
            let text = "U,1,1,^5|CLOSED|-0.5\r\n";
            let result = clean_message(text);
            assert_eq!(result, "u,1,1,^5|CLOSED|-0.5");
        }

        #[test]
        fn test_clean_message_reqerr_preserves_details() {
            let text = "REQERR,21,InvalidField\r\n";
            let result = clean_message(text);
            assert_eq!(result, "reqerr,21,InvalidField");
        }
    }

    mod parse_arguments_tests {
        use super::*;

        #[test]
        fn test_parse_arguments_basic() {
            let input = "arg1,arg2,arg3";
            let result = parse_arguments(input);
            assert_eq!(result, vec!["arg1", "arg2", "arg3"]);
        }

        #[test]
        fn test_parse_arguments_empty_string() {
            let input = "";
            let result = parse_arguments(input);
            assert_eq!(result, Vec::<&str>::new());
        }

        #[test]
        fn test_parse_arguments_single_argument() {
            let input = "arg1";
            let result = parse_arguments(input);
            assert_eq!(result, vec!["arg1"]);
        }

        #[test]
        fn test_parse_arguments_with_whitespace() {
            let input = " arg1 , arg2 , arg3 ";
            let result = parse_arguments(input);
            assert_eq!(result, vec!["arg1", "arg2", "arg3"]);
        }

        #[test]
        fn test_parse_arguments_empty_arguments() {
            let input = "arg1,,arg3";
            let result = parse_arguments(input);
            assert_eq!(result, vec!["arg1", "arg3"]);
        }

        #[test]
        fn test_parse_arguments_with_braces() {
            let input = "arg1,{inner1,inner2},arg3";
            let result = parse_arguments(input);
            assert_eq!(result, vec!["arg1", "{inner1,inner2}", "arg3"]);
        }

        #[test]
        fn test_parse_arguments_nested_braces() {
            let input = "arg1,{outer{inner1,inner2}outer},arg3";
            let result = parse_arguments(input);
            assert_eq!(result, vec!["arg1", "{outer{inner1,inner2}outer}", "arg3"]);
        }

        #[test]
        fn test_parse_arguments_unbalanced_braces() {
            let input = "arg1,{unbalanced,arg3";
            let result = parse_arguments(input);
            // Even with unbalanced braces, we expect it to treat everything inside as one argument
            assert_eq!(result, vec!["arg1", "{unbalanced,arg3"]);
        }

        #[test]
        fn test_parse_arguments_protocol_examples() {
            // TLCP protocol message example arguments
            let input = "CONOK,S8f4aec42c3c14ad0,50000,5000,*";
            let result = parse_arguments(input);
            assert_eq!(
                result,
                vec!["CONOK", "S8f4aec42c3c14ad0", "50000", "5000", "*"]
            );

            let input = "u,1,1,a|b|c";
            let result = parse_arguments(input);
            assert_eq!(result, vec!["u", "1", "1", "a|b|c"]);
        }
    }
}