use regex::Regex;
use serde_json::Value;
use super::matches::string::string_matches_schema;
const MAX_GENERATED_STRING_LENGTH: u64 = 1024;
pub(super) fn fallback_string_example(
object: &serde_json::Map<String, Value>,
accepts: impl Fn(&str) -> bool,
) -> Option<String> {
let min_length = object.get("minLength").and_then(Value::as_u64).unwrap_or(0);
let max_length = object.get("maxLength").and_then(Value::as_u64);
if max_length.is_some_and(|max_length| min_length > max_length)
|| min_length > MAX_GENERATED_STRING_LENGTH
{
return None;
}
string_candidate_seeds(object).into_iter().find_map(|seed| {
let candidate = fit_string_candidate(seed, min_length, max_length)?;
(string_matches_schema(&candidate, object) && accepts(&candidate)).then_some(candidate)
})
}
fn string_candidate_seeds(object: &serde_json::Map<String, Value>) -> Vec<String> {
let mut seeds = Vec::from([
"example".to_owned(),
"x".to_owned(),
String::new(),
"<secret>".to_owned(),
]);
if let Some(pattern) = object.get("pattern").and_then(Value::as_str)
&& let Some(prefix) = literal_regex_prefix(pattern)
{
seeds.insert(0, prefix);
}
seeds
}
fn fit_string_candidate(
mut candidate: String,
min_length: u64,
max_length: Option<u64>,
) -> Option<String> {
let min_length = usize::try_from(min_length).ok()?;
let max_length = max_length.map(usize::try_from).transpose().ok()?;
let current_length = candidate.chars().count();
if current_length < min_length {
candidate.extend(std::iter::repeat_n('x', min_length - current_length));
}
if let Some(max_length) = max_length
&& candidate.chars().count() > max_length
{
candidate = candidate.chars().take(max_length).collect();
}
Some(candidate)
}
fn literal_regex_prefix(pattern: &str) -> Option<String> {
let pattern = pattern.strip_prefix('^')?;
let mut prefix = String::new();
let mut escaped = false;
for ch in pattern.chars() {
if escaped {
prefix.push(ch);
escaped = false;
continue;
}
match ch {
'\\' => escaped = true,
ch if ch.is_ascii_alphanumeric() || matches!(ch, '-' | '_' | '.' | '/') => {
prefix.push(ch);
}
_ => break,
}
}
(!prefix.is_empty() && Regex::new(pattern).is_ok_and(|regex| regex.is_match(&prefix)))
.then_some(prefix)
}