Skip to main content

thirtyfour_testing_library_ext/options/
common.rs

1use regex;
2use serde::{Serialize, Serializer};
3use serde_json::Value;
4
5/// Common trait for all testing-library option types.
6///
7/// This trait provides standard methods for serialization and construction
8/// that are shared across all option types.
9pub trait TestingLibraryOptions: Serialize + Default {
10    /// Create a new instance with default values.
11    fn new() -> Self
12    where
13        Self: Sized,
14    {
15        Self::default()
16    }
17
18    /// Serialize this options struct to a JSON string.
19    fn to_json_string(&self) -> Result<String, serde_json::Error> {
20        let json = serde_json::to_string(self)?;
21        // Post-process to convert marked raw JavaScript values
22        Ok(process_raw_javascript_markers(&json))
23    }
24
25    /// Serialize this options struct to a JSON value.
26    fn to_json_value(&self) -> Result<Value, serde_json::Error> {
27        serde_json::to_value(self)
28    }
29}
30
31/// A wrapper type that indicates a value should be serialized as raw JavaScript
32#[derive(Debug, Clone)]
33pub struct RawJavaScript(pub String);
34
35impl Serialize for RawJavaScript {
36    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
37    where
38        S: Serializer,
39    {
40        // Mark this as a special regex value that needs post-processing
41        // We'll use a special marker that we can detect and replace later
42        let marked_value = format!("__RAW_JS__{}", self.0);
43        marked_value.serialize(serializer)
44    }
45}
46
47/// Represents text matching options for Testing Library queries
48/// Supports both string and regex patterns like the JavaScript Testing Library
49/// Exact vs substring behavior is controlled by the `exact` option on queries
50#[derive(Debug, Clone)]
51pub enum TextMatch {
52    /// String match (exact vs substring controlled by query options)
53    String(String),
54    /// Regular expression match
55    Regex(String),
56}
57
58impl Serialize for TextMatch {
59    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
60    where
61        S: Serializer,
62    {
63        match self {
64            TextMatch::String(s) => s.serialize(serializer),
65            TextMatch::Regex(pattern) => {
66                // Use RawJavaScript wrapper to indicate this should be raw JS
67                RawJavaScript(pattern.clone()).serialize(serializer)
68            }
69        }
70    }
71}
72
73impl TextMatch {
74    /// Validate that the regex pattern is properly formatted
75    pub fn validate_regex(&self) -> Result<(), String> {
76        match self {
77            TextMatch::Regex(pattern) => {
78                // Check if it looks like a regex literal
79                if !pattern.starts_with('/') {
80                    return Err(
81                        "Regex pattern must start with '/' (e.g., '/pattern/' or '/pattern/i')"
82                            .to_string(),
83                    );
84                }
85
86                // Find the last '/' to separate pattern from flags
87                let last_slash = pattern.rfind('/');
88                if last_slash.is_none() || last_slash.unwrap() == 0 {
89                    return Err("Regex pattern must contain at least one '/' after the pattern (e.g., '/pattern/')".to_string());
90                }
91
92                let last_slash_pos = last_slash.unwrap();
93                let inner_pattern = &pattern[1..last_slash_pos];
94
95                // Validate the regex pattern (ignore flags for now)
96                regex::Regex::new(inner_pattern)
97                    .map_err(|e| format!("Invalid regex pattern: {e}"))?;
98
99                Ok(())
100            }
101            _ => Ok(()),
102        }
103    }
104
105    /// Get the text value for string matches or pattern for regex matches
106    pub fn text_value(&self) -> &str {
107        match self {
108            TextMatch::String(text) => text,
109            TextMatch::Regex(pattern) => pattern,
110        }
111    }
112
113    /// Check if this is a string match
114    pub fn is_string(&self) -> bool {
115        matches!(self, TextMatch::String(_))
116    }
117
118    /// Check if this is a regex match
119    pub fn is_regex(&self) -> bool {
120        matches!(self, TextMatch::Regex(_))
121    }
122}
123
124impl From<&str> for TextMatch {
125    fn from(text: &str) -> Self {
126        if text.starts_with('/') && text.len() > 2 {
127            if let Some(last_slash) = text.rfind('/') {
128                if last_slash > 0 {
129                    return Self::Regex(text.to_string());
130                }
131            }
132        }
133        Self::String(text.to_string())
134    }
135}
136
137impl From<String> for TextMatch {
138    fn from(text: String) -> Self {
139        TextMatch::from(text.as_str())
140    }
141}
142
143/// Convert marked raw JavaScript values to actual raw JavaScript
144pub fn process_raw_javascript_markers(json: &str) -> String {
145    // Replace "__RAW_JS__/pattern/" with /pattern/ (remove quotes and marker)
146    use regex::Regex;
147    let re = Regex::new(r#""__RAW_JS__([^"]+)""#).unwrap();
148    re.replace_all(json, "$1").to_string()
149}