use super::schema::Direction;
use crate::error::Error;
const MAX_REGEX_LEN: usize = 200;
fn is_terminal_char(c: char) -> bool {
c.is_ascii_alphanumeric()
|| matches!(
c,
'_' | '$'
| '.'
| '['
| ']'
| ':'
| '?'
| '/'
| '`'
| '^'
| '('
| ')'
| '-'
| '+'
| '\\'
| '*'
| '|'
)
}
pub(super) fn validate_key(key: &str, direction: &Direction) -> Result<(), Error> {
if key.is_empty() {
return Err(Error::Suture("key must not be empty".into()));
}
if !key.starts_with('`') {
match direction {
Direction::Request => validate_struct_terminal(key, "request key")?,
Direction::Response => validate_json_terminal(key, "response key")?,
}
}
validate_charset(key)?;
validate_backticks(key)?;
validate_brackets(key)?;
Ok(())
}
pub(super) fn validate_terminal(s: &str, direction: &Direction) -> Result<(), Error> {
if s.is_empty() {
return Err(Error::Suture("terminal must not be empty".into()));
}
match direction {
Direction::Request => validate_json_terminal(s, "request value")?,
Direction::Response => validate_struct_terminal(s, "response value")?,
}
validate_charset(s)?;
if contains_backtick(s) {
return Err(Error::Suture(format!(
"regex is not allowed on the write side, got: '{s}'"
)));
}
validate_brackets(s)?;
Ok(())
}
pub(super) fn validate_constant(val: &serde_json::Value) -> Result<(), Error> {
match val {
serde_json::Value::String(_)
| serde_json::Value::Number(_)
| serde_json::Value::Bool(_)
| serde_json::Value::Null => Ok(()),
_ => Err(Error::Suture(
"constant value must be a scalar (string, number, boolean, or null)".into(),
)),
}
}
fn validate_json_terminal(s: &str, ctx: &str) -> Result<(), Error> {
if !s.starts_with('/') {
return Err(Error::Suture(format!(
"{ctx} must start with '/', got: '{s}'"
)));
}
if s.len() > 1 && s.ends_with('/') {
return Err(Error::Suture(format!(
"{ctx} must not end with '/', got: '{s}'"
)));
}
if s.contains("//") {
return Err(Error::Suture(format!(
"{ctx} must not contain consecutive '/', got: '{s}'"
)));
}
Ok(())
}
fn validate_struct_terminal(s: &str, ctx: &str) -> Result<(), Error> {
if !s.starts_with(|c: char| c.is_ascii_alphabetic()) {
return Err(Error::Suture(format!(
"{ctx} must start with a letter, got: '{s}'"
)));
}
if s.ends_with('.') {
return Err(Error::Suture(format!(
"{ctx} must not end with '.', got: '{s}'"
)));
}
if s.contains("..") {
return Err(Error::Suture(format!(
"{ctx} must not contain consecutive dots '..', got: '{s}'"
)));
}
Ok(())
}
fn validate_charset(s: &str) -> Result<(), Error> {
let mut in_backtick = false;
for (i, c) in s.char_indices() {
if c == '`' {
in_backtick = !in_backtick;
continue;
}
if !in_backtick && !is_terminal_char(c) {
return Err(Error::Suture(format!(
"invalid character '{c}' at position {i} in terminal: '{s}'"
)));
}
}
Ok(())
}
fn contains_backtick(s: &str) -> bool {
s.contains('`')
}
fn validate_backticks(s: &str) -> Result<(), Error> {
let bytes = s.as_bytes();
let mut i = 0;
while i < bytes.len() {
if bytes[i] == b'`' {
let open = i;
i += 1;
let pattern_start = i;
while i < bytes.len() && bytes[i] != b'`' {
i += 1;
}
if i >= bytes.len() {
return Err(Error::Suture(format!(
"unmatched backtick at position {open} in terminal: '{s}'"
)));
}
let pattern = &s[pattern_start..i];
i += 1;
if pattern.is_empty() {
return Err(Error::Suture(format!(
"empty regex pattern not allowed in terminal: '{s}'"
)));
}
if pattern.len() > MAX_REGEX_LEN {
return Err(Error::Suture(format!(
"regex pattern exceeds max length ({MAX_REGEX_LEN} chars) in terminal: '{s}'"
)));
}
validate_no_capturing_groups(pattern, s)?;
if let Err(e) = regex::Regex::new(&format!("^{pattern}$")) {
return Err(Error::Suture(format!(
"invalid regex `{pattern}` in terminal '{s}': {e}"
)));
}
continue;
}
i += 1;
}
Ok(())
}
fn validate_no_capturing_groups(pattern: &str, full: &str) -> Result<(), Error> {
let bytes = pattern.as_bytes();
let mut i = 0;
while i < bytes.len() {
if bytes[i] == b'\\' {
i += 2;
continue;
}
if bytes[i] == b'(' {
if i + 1 < bytes.len() && bytes[i + 1] == b'?' {
i += 2;
continue;
}
return Err(Error::Suture(format!(
"capturing groups not allowed in regex, use (?:...) instead, in terminal: '{full}'"
)));
}
i += 1;
}
Ok(())
}
fn validate_brackets(s: &str) -> Result<(), Error> {
let bytes = s.as_bytes();
let mut i = 0;
while i < bytes.len() {
if bytes[i] == b'`' {
i += 1;
while i < bytes.len() && bytes[i] != b'`' {
i += 1;
}
if i < bytes.len() {
i += 1; }
continue;
}
if bytes[i] == b'[' {
i += 1;
let bracket_start = i;
while i < bytes.len() && bytes[i] != b']' {
if bytes[i] == b'[' {
return Err(Error::Suture(format!(
"nested brackets not allowed in terminal: '{s}'"
)));
}
i += 1;
}
if i >= bytes.len() {
return Err(Error::Suture(format!("unclosed '[' in terminal: '{s}'")));
}
let inner = &s[bracket_start..i];
i += 1;
validate_bracket_inner(inner, s)?;
if i < bytes.len() && bytes[i] == b'[' {
return Err(Error::Suture(format!(
"consecutive brackets not allowed in terminal: '{s}'"
)));
}
continue;
}
if bytes[i] == b']' {
return Err(Error::Suture(format!(
"unexpected ']' without matching '[' in terminal: '{s}'"
)));
}
i += 1;
}
Ok(())
}
fn validate_bracket_inner(inner: &str, full: &str) -> Result<(), Error> {
if inner.is_empty() {
return Err(Error::Suture(format!(
"empty brackets '[]' not allowed in terminal: '{full}'"
)));
}
if inner.contains(|c: char| c.is_ascii_whitespace()) {
return Err(Error::Suture(format!(
"whitespace not allowed inside brackets '[{inner}]' in terminal: '{full}'"
)));
}
let parts: Vec<&str> = inner.split(':').collect();
match parts.len() {
1 => {
let idx = validate_int_part(parts[0], full, "index")?;
if idx == i64::MAX {
return Err(Error::Suture(format!(
"index too large in bracket expression '[{inner}]' in terminal: '{full}'"
)));
}
}
2 => {
if !parts[0].is_empty() {
validate_int_part(parts[0], full, "start")?;
}
if !parts[1].is_empty() {
validate_int_part(parts[1], full, "end")?;
}
}
3 => {
if !parts[0].is_empty() {
validate_int_part(parts[0], full, "start")?;
}
if !parts[1].is_empty() {
validate_int_part(parts[1], full, "end")?;
}
if !parts[2].is_empty() {
let step = validate_int_part(parts[2], full, "step")?;
if step == 0 {
return Err(Error::Suture(format!(
"step cannot be 0 in bracket expression '[{inner}]' in terminal: '{full}'"
)));
}
}
}
_ => {
return Err(Error::Suture(format!(
"too many ':' in bracket expression '[{inner}]' in terminal: '{full}'"
)));
}
}
Ok(())
}
fn validate_int_part(part: &str, full: &str, label: &str) -> Result<i64, Error> {
part.parse::<i64>().map_err(|_| {
Error::Suture(format!(
"invalid {label} '{part}' in bracket expression, expected integer, in terminal: '{full}'"
))
})
}