use chrono::{DateTime, NaiveDate, NaiveDateTime, Utc};
use serde_json::Value;
use std::net::IpAddr;
use crate::validate::{is_empty_value, Validate};
fn value_as_string(value: &Value) -> String {
match value {
Value::String(s) => s.clone(),
Value::Number(n) => n.to_string(),
Value::Bool(b) => {
if *b {
"1".to_string()
} else {
String::new()
}
}
Value::Null => String::new(),
_ => String::new(),
}
}
fn value_as_f64(value: &Value) -> Option<f64> {
if let Some(n) = value.as_f64() {
return Some(n);
}
if let Value::String(s) = value {
return s.parse::<f64>().ok();
}
None
}
fn value_loose_equals_str(value: &Value, other: &str) -> bool {
if let Some(v_num) = value_as_f64(value) {
if let Ok(o_num) = other.parse::<f64>() {
return v_num == o_num;
}
}
value_as_string(value) == other
}
fn value_loose_equals(value: &Value, other: &Value) -> bool {
if let (Some(v_num), Some(o_num)) = (value_as_f64(value), value_as_f64(other)) {
return v_num == o_num;
}
value_as_string(value) == value_as_string(other)
}
fn value_loose_compare(value: &Value, other: &Value) -> Option<std::cmp::Ordering> {
if let (Some(v_num), Some(o_num)) = (value_as_f64(value), value_as_f64(other)) {
return v_num.partial_cmp(&o_num);
}
Some(value_as_string(value).cmp(&value_as_string(other)))
}
fn parse_timestamp(value: &Value) -> Option<i64> {
let s = match value {
Value::String(s) => s.as_str(),
Value::Number(n) => {
if let Some(i) = n.as_i64() {
return Some(i);
}
return None;
}
_ => return None,
};
if let Ok(dt) = DateTime::parse_from_rfc3339(s) {
return Some(dt.timestamp());
}
let formats: &[&str] = &[
"%Y-%m-%d %H:%M:%S",
"%Y-%m-%d",
"%Y/%m/%d %H:%M:%S",
"%Y/%m/%d",
"%Y-%m-%dT%H:%M:%S",
"%Y-%m-%dT%H:%M:%SZ",
];
for fmt in formats {
if let Ok(dt) = NaiveDateTime::parse_from_str(s, fmt) {
return Some(dt.and_utc().timestamp());
}
if let Ok(d) = NaiveDate::parse_from_str(s, fmt) {
return d.and_hms_opt(0, 0, 0).map(|t| t.and_utc().timestamp());
}
}
None
}
fn php_date_format_to_chrono(php_format: &str) -> String {
let mut result = String::new();
let mut chars = php_format.chars().peekable();
while let Some(c) = chars.next() {
match c {
'Y' => result.push_str("%Y"),
'y' => result.push_str("%y"),
'm' => result.push_str("%m"),
'n' => result.push_str("%_m"),
'd' => result.push_str("%d"),
'j' => result.push_str("%_d"),
'H' => result.push_str("%H"),
'G' => result.push_str("%_H"),
'i' => result.push_str("%M"),
's' => result.push_str("%S"),
'a' | 'A' => result.push_str("%P"),
'\\' => {
if let Some(next) = chars.next() {
result.push(next);
}
}
_ => result.push(c),
}
}
result
}
pub fn eq(value: &Value, rule: &str, _data: &Value, _field: &str) -> bool {
value_loose_equals_str(value, rule)
}
pub fn egt(value: &Value, rule: &str, data: &Value, _field: &str) -> bool {
let other = Validate::get_data_value(data, rule);
matches!(
value_loose_compare(value, &other),
Some(std::cmp::Ordering::Equal | std::cmp::Ordering::Greater)
)
}
pub fn gt(value: &Value, rule: &str, data: &Value, _field: &str) -> bool {
let other = Validate::get_data_value(data, rule);
matches!(
value_loose_compare(value, &other),
Some(std::cmp::Ordering::Greater)
)
}
pub fn elt(value: &Value, rule: &str, data: &Value, _field: &str) -> bool {
let other = Validate::get_data_value(data, rule);
matches!(
value_loose_compare(value, &other),
Some(std::cmp::Ordering::Equal | std::cmp::Ordering::Less)
)
}
pub fn lt(value: &Value, rule: &str, data: &Value, _field: &str) -> bool {
let other = Validate::get_data_value(data, rule);
matches!(
value_loose_compare(value, &other),
Some(std::cmp::Ordering::Less)
)
}
pub fn confirm(value: &Value, rule: &str, data: &Value, field: &str) -> bool {
let confirm_field = if rule.is_empty() {
if field.contains("_confirm") {
field.split("_confirm").next().unwrap_or("").to_string()
} else {
format!("{}_confirm", field)
}
} else {
rule.to_string()
};
let other = Validate::get_data_value(data, &confirm_field);
value == &other
}
pub fn different(value: &Value, rule: &str, data: &Value, _field: &str) -> bool {
let other = Validate::get_data_value(data, rule);
!value_loose_equals(value, &other)
}
pub fn in_rule(value: &Value, rule: &str, _data: &Value, _field: &str) -> bool {
let items: Vec<&str> = rule.split(',').collect();
for item in items {
let item = item.trim();
if value_loose_equals_str(value, item) {
return true;
}
}
false
}
pub fn not_in(value: &Value, rule: &str, _data: &Value, _field: &str) -> bool {
!in_rule(value, rule, _data, _field)
}
pub fn between(value: &Value, rule: &str, _data: &Value, _field: &str) -> bool {
let parts: Vec<&str> = rule.split(',').collect();
if parts.len() < 2 {
return false;
}
let min = parts[0].trim();
let max = parts[1].trim();
let ge_min = value_loose_compare_str(value, min)
.map(|o| o != std::cmp::Ordering::Less)
.unwrap_or(false);
let le_max = value_loose_compare_str(value, max)
.map(|o| o != std::cmp::Ordering::Greater)
.unwrap_or(false);
ge_min && le_max
}
pub fn not_between(value: &Value, rule: &str, data: &Value, field: &str) -> bool {
!between(value, rule, data, field)
}
fn value_loose_compare_str(value: &Value, other: &str) -> Option<std::cmp::Ordering> {
if let Some(v_num) = value_as_f64(value) {
if let Ok(o_num) = other.parse::<f64>() {
return v_num.partial_cmp(&o_num);
}
}
Some(value_as_string(value).as_str().cmp(other))
}
fn value_length(value: &Value) -> usize {
match value {
Value::Array(a) => a.len(),
Value::Object(o) => o.len(),
Value::String(s) => s.chars().count(),
_ => value_as_string(value).chars().count(),
}
}
pub fn length(value: &Value, rule: &str, _data: &Value, _field: &str) -> bool {
let len = value_length(value);
if let Some(idx) = rule.find(',') {
let min_str = rule[..idx].trim();
let max_str = rule[idx + 1..].trim();
let min: usize = min_str.parse().unwrap_or(0);
let max: usize = max_str.parse().unwrap_or(0);
len >= min && len <= max
} else {
let target: usize = rule.parse().unwrap_or(0);
len == target
}
}
pub fn max(value: &Value, rule: &str, _data: &Value, _field: &str) -> bool {
let len = value_length(value);
let max: usize = rule.parse().unwrap_or(0);
len <= max
}
pub fn min(value: &Value, rule: &str, _data: &Value, _field: &str) -> bool {
let len = value_length(value);
let min: usize = rule.parse().unwrap_or(0);
len >= min
}
pub fn date_format(value: &Value, rule: &str, _data: &Value, _field: &str) -> bool {
let s = match value {
Value::String(s) => s.as_str(),
_ => return false,
};
let chrono_fmt = php_date_format_to_chrono(rule);
if NaiveDateTime::parse_from_str(s, &chrono_fmt).is_ok() {
return true;
}
if NaiveDate::parse_from_str(s, &chrono_fmt).is_ok() {
return true;
}
false
}
pub fn after(value: &Value, rule: &str, _data: &Value, _field: &str) -> bool {
let value_ts = parse_timestamp(value);
let rule_ts = parse_timestamp(&Value::String(rule.to_string()));
match (value_ts, rule_ts) {
(Some(v), Some(r)) => v >= r,
_ => false,
}
}
pub fn before(value: &Value, rule: &str, _data: &Value, _field: &str) -> bool {
let value_ts = parse_timestamp(value);
let rule_ts = parse_timestamp(&Value::String(rule.to_string()));
match (value_ts, rule_ts) {
(Some(v), Some(r)) => v <= r,
_ => false,
}
}
pub fn after_with(value: &Value, rule: &str, data: &Value, _field: &str) -> bool {
let other = Validate::get_data_value(data, rule);
if other.is_null() {
return false;
}
let value_ts = parse_timestamp(value);
let rule_ts = parse_timestamp(&other);
match (value_ts, rule_ts) {
(Some(v), Some(r)) => v >= r,
_ => false,
}
}
pub fn before_with(value: &Value, rule: &str, data: &Value, _field: &str) -> bool {
let other = Validate::get_data_value(data, rule);
if other.is_null() {
return false;
}
let value_ts = parse_timestamp(value);
let rule_ts = parse_timestamp(&other);
match (value_ts, rule_ts) {
(Some(v), Some(r)) => v <= r,
_ => false,
}
}
pub fn expire(_value: &Value, rule: &str, _data: &Value, _field: &str) -> bool {
let parts: Vec<&str> = rule.split(',').collect();
if parts.len() < 2 {
return false;
}
let start_str = parts[0].trim();
let end_str = parts[1].trim();
let start_ts = if let Ok(n) = start_str.parse::<i64>() {
Some(n)
} else {
parse_timestamp(&Value::String(start_str.to_string()))
};
let end_ts = if let Ok(n) = end_str.parse::<i64>() {
Some(n)
} else {
parse_timestamp(&Value::String(end_str.to_string()))
};
match (start_ts, end_ts) {
(Some(s), Some(e)) => {
let now = Utc::now().timestamp();
now >= s && now <= e
}
_ => false,
}
}
pub fn require_if(value: &Value, rule: &str, data: &Value, _field: &str) -> bool {
let parts: Vec<&str> = rule.split(',').collect();
if parts.len() < 2 {
return true;
}
let field_name = parts[0].trim();
let expected_val = parts[1].trim();
let actual = Validate::get_data_value(data, field_name);
if value_loose_equals_str(&actual, expected_val) {
!is_empty_value(value) || matches!(value, Value::String(s) if s == "0")
} else {
true
}
}
pub fn require_with(value: &Value, rule: &str, data: &Value, _field: &str) -> bool {
let other = Validate::get_data_value(data, rule);
if !is_empty_value(&other) {
!is_empty_value(value) || matches!(value, Value::String(s) if s == "0")
} else {
true
}
}
pub fn require_without(value: &Value, rule: &str, data: &Value, _field: &str) -> bool {
let other = Validate::get_data_value(data, rule);
if is_empty_value(&other) {
!is_empty_value(value) || matches!(value, Value::String(s) if s == "0")
} else {
true
}
}
pub fn ip(value: &Value, rule: &str, _data: &Value, _field: &str) -> bool {
let s = match value {
Value::String(s) => s.as_str(),
_ => return false,
};
let parsed: Result<IpAddr, _> = s.parse();
match parsed {
Ok(IpAddr::V4(_)) => rule != "ipv6", Ok(IpAddr::V6(_)) => rule == "ipv6",
Err(_) => false,
}
}
pub fn allow_ip(value: &Value, rule: &str, _data: &Value, _field: &str) -> bool {
let s = match value {
Value::String(s) => s.as_str(),
_ => return false,
};
let allowed: Vec<&str> = rule.split(',').map(|x| x.trim()).collect();
allowed.contains(&s)
}
pub fn deny_ip(value: &Value, rule: &str, _data: &Value, _field: &str) -> bool {
!allow_ip(value, rule, _data, _field)
}
pub fn active_url(value: &Value, _rule: &str, _data: &Value, _field: &str) -> bool {
let s = match value {
Value::String(s) => s.as_str(),
_ => return false,
};
if s.is_empty() {
return false;
}
use std::net::ToSocketAddrs;
let target = format!("{}:80", s);
target.to_socket_addrs().is_ok()
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn test_eq_numeric() {
assert!(eq(&json!(1), "1", &Value::Null, ""));
assert!(eq(&json!("1"), "1", &Value::Null, ""));
assert!(eq(&json!(1.5), "1.5", &Value::Null, ""));
assert!(!eq(&json!(2), "1", &Value::Null, ""));
}
#[test]
fn test_eq_string() {
assert!(eq(&json!("hello"), "hello", &Value::Null, ""));
assert!(!eq(&json!("hello"), "world", &Value::Null, ""));
}
#[test]
fn test_egt_field_comparison() {
let data = json!({"min_val": 10});
assert!(egt(&json!(15), "min_val", &data, ""));
assert!(egt(&json!(10), "min_val", &data, ""));
assert!(!egt(&json!(5), "min_val", &data, ""));
}
#[test]
fn test_gt_field_comparison() {
let data = json!({"min_val": 10});
assert!(gt(&json!(15), "min_val", &data, ""));
assert!(!gt(&json!(10), "min_val", &data, ""));
assert!(!gt(&json!(5), "min_val", &data, ""));
}
#[test]
fn test_elt_field_comparison() {
let data = json!({"max_val": 100});
assert!(elt(&json!(50), "max_val", &data, ""));
assert!(elt(&json!(100), "max_val", &data, ""));
assert!(!elt(&json!(150), "max_val", &data, ""));
}
#[test]
fn test_lt_field_comparison() {
let data = json!({"max_val": 100});
assert!(lt(&json!(50), "max_val", &data, ""));
assert!(!lt(&json!(100), "max_val", &data, ""));
assert!(!lt(&json!(150), "max_val", &data, ""));
}
#[test]
fn test_confirm_explicit_field() {
let data = json!({"password": "abc123", "password_confirm": "abc123"});
assert!(confirm(
&json!("abc123"),
"password_confirm",
&data,
"password"
));
assert!(!confirm(
&json!("wrong"),
"password_confirm",
&data,
"password"
));
}
#[test]
fn test_confirm_auto_field_inference() {
let data = json!({"password": "abc123", "password_confirm": "abc123"});
assert!(confirm(&json!("abc123"), "", &data, "password"));
assert!(!confirm(&json!("wrong"), "", &data, "password"));
}
#[test]
fn test_confirm_auto_field_strips_suffix() {
let data = json!({"password": "abc123"});
assert!(confirm(&json!("abc123"), "", &data, "password_confirm"));
}
#[test]
fn test_different_loose_comparison() {
let data = json!({"other": "abc"});
assert!(different(&json!("xyz"), "other", &data, ""));
assert!(!different(&json!("abc"), "other", &data, ""));
let data2 = json!({"other": "1"});
assert!(!different(&json!(1), "other", &data2, ""));
}
#[test]
fn test_in_rule() {
assert!(in_rule(&json!(1), "1,2,3", &Value::Null, ""));
assert!(in_rule(&json!("1"), "1,2,3", &Value::Null, ""));
assert!(in_rule(
&json!("active"),
"active,inactive",
&Value::Null,
""
));
assert!(!in_rule(&json!(4), "1,2,3", &Value::Null, ""));
assert!(!in_rule(&json!("xyz"), "active,inactive", &Value::Null, ""));
}
#[test]
fn test_not_in() {
assert!(!not_in(&json!(1), "1,2,3", &Value::Null, ""));
assert!(not_in(&json!(4), "1,2,3", &Value::Null, ""));
}
#[test]
fn test_between_numeric() {
assert!(between(&json!(5), "1,10", &Value::Null, ""));
assert!(between(&json!(1), "1,10", &Value::Null, ""));
assert!(between(&json!(10), "1,10", &Value::Null, ""));
assert!(!between(&json!(0), "1,10", &Value::Null, ""));
assert!(!between(&json!(11), "1,10", &Value::Null, ""));
}
#[test]
fn test_between_string_numeric() {
assert!(between(&json!("5"), "1,10", &Value::Null, ""));
}
#[test]
fn test_not_between() {
assert!(!not_between(&json!(5), "1,10", &Value::Null, ""));
assert!(not_between(&json!(11), "1,10", &Value::Null, ""));
}
#[test]
fn test_between_invalid_format() {
assert!(!between(&json!(5), "1", &Value::Null, "")); }
#[test]
fn test_length_exact() {
assert!(length(&json!("abc"), "3", &Value::Null, ""));
assert!(!length(&json!("abc"), "5", &Value::Null, ""));
}
#[test]
fn test_length_range() {
assert!(length(&json!("abc"), "1,5", &Value::Null, ""));
assert!(length(&json!("abcde"), "1,5", &Value::Null, ""));
assert!(!length(&json!("abcdef"), "1,5", &Value::Null, ""));
}
#[test]
fn test_length_unicode() {
assert!(length(&json!("中文"), "2", &Value::Null, ""));
assert!(!length(&json!("中文"), "4", &Value::Null, "")); }
#[test]
fn test_length_array() {
assert!(length(&json!([1, 2, 3]), "3", &Value::Null, ""));
assert!(!length(&json!([1, 2, 3]), "2", &Value::Null, ""));
}
#[test]
fn test_max_length() {
assert!(max(&json!("abc"), "5", &Value::Null, ""));
assert!(max(&json!("abcde"), "5", &Value::Null, ""));
assert!(!max(&json!("abcdef"), "5", &Value::Null, ""));
}
#[test]
fn test_min_length() {
assert!(min(&json!("abc"), "3", &Value::Null, ""));
assert!(!min(&json!("ab"), "3", &Value::Null, ""));
}
#[test]
fn test_date_format_y_m_d() {
assert!(date_format(&json!("2024-01-15"), "Y-m-d", &Value::Null, ""));
assert!(!date_format(
&json!("2024/01/15"),
"Y-m-d",
&Value::Null,
""
));
}
#[test]
fn test_date_format_full() {
assert!(date_format(
&json!("2024-01-15 12:30:45"),
"Y-m-d H:i:s",
&Value::Null,
""
));
}
#[test]
fn test_after_date() {
assert!(after(&json!("2024-01-02"), "2024-01-01", &Value::Null, ""));
assert!(after(&json!("2024-01-01"), "2024-01-01", &Value::Null, ""));
assert!(!after(&json!("2023-12-31"), "2024-01-01", &Value::Null, ""));
}
#[test]
fn test_before_date() {
assert!(before(&json!("2023-12-31"), "2024-01-01", &Value::Null, ""));
assert!(before(&json!("2024-01-01"), "2024-01-01", &Value::Null, ""));
assert!(!before(
&json!("2024-01-02"),
"2024-01-01",
&Value::Null,
""
));
}
#[test]
fn test_after_with_field() {
let data = json!({"start_date": "2024-01-01"});
assert!(after_with(&json!("2024-01-02"), "start_date", &data, ""));
assert!(!after_with(&json!("2023-12-31"), "start_date", &data, ""));
}
#[test]
fn test_before_with_field() {
let data = json!({"end_date": "2024-12-31"});
assert!(before_with(&json!("2024-06-15"), "end_date", &data, ""));
assert!(!before_with(&json!("2025-01-01"), "end_date", &data, ""));
}
#[test]
fn test_after_with_null_field() {
let data = json!({});
assert!(!after_with(&json!("2024-01-02"), "missing", &data, ""));
}
#[test]
fn test_expire_with_timestamps() {
let now = Utc::now().timestamp();
let past_start = now - 7200; let past_end = now - 3600; let rule = format!("{},{}", past_start, past_end);
assert!(!expire(&Value::Null, &rule, &Value::Null, ""));
let future_start = now - 60;
let future_end = now + 60;
let rule = format!("{},{}", future_start, future_end);
assert!(expire(&Value::Null, &rule, &Value::Null, ""));
}
#[test]
fn test_expire_with_date_strings() {
let rule = "2020-01-01,2030-12-31";
assert!(expire(&Value::Null, rule, &Value::Null, ""));
let rule = "2010-01-01,2015-12-31";
assert!(!expire(&Value::Null, rule, &Value::Null, ""));
}
#[test]
fn test_require_if_condition_met() {
let data = json!({"type": "login"});
assert!(require_if(&json!("alice"), "type,login", &data, ""));
assert!(!require_if(&json!(""), "type,login", &data, ""));
assert!(require_if(&json!("0"), "type,login", &data, ""));
}
#[test]
fn test_require_if_condition_not_met() {
let data = json!({"type": "register"});
assert!(require_if(&json!(""), "type,login", &data, ""));
}
#[test]
fn test_require_with_other_has_value() {
let data = json!({"other_field": "some_value"});
assert!(require_with(&json!("value"), "other_field", &data, ""));
assert!(!require_with(&json!(""), "other_field", &data, ""));
}
#[test]
fn test_require_with_other_empty() {
let data = json!({"other_field": ""});
assert!(require_with(&json!(""), "other_field", &data, ""));
let data2 = json!({});
assert!(require_with(&json!(""), "missing", &data2, ""));
}
#[test]
fn test_require_without_other_empty() {
let data = json!({"other_field": ""});
assert!(require_without(&json!("value"), "other_field", &data, ""));
assert!(!require_without(&json!(""), "other_field", &data, ""));
}
#[test]
fn test_require_without_other_has_value() {
let data = json!({"other_field": "some_value"});
assert!(require_without(&json!(""), "other_field", &data, ""));
}
#[test]
fn test_ip_v4() {
assert!(ip(&json!("127.0.0.1"), "ipv4", &Value::Null, ""));
assert!(ip(&json!("192.168.1.1"), "ipv4", &Value::Null, ""));
assert!(ip(&json!("127.0.0.1"), "", &Value::Null, "")); assert!(!ip(&json!("::1"), "ipv4", &Value::Null, ""));
assert!(!ip(&json!("999.999.999.999"), "ipv4", &Value::Null, ""));
}
#[test]
fn test_ip_v6() {
assert!(ip(&json!("::1"), "ipv6", &Value::Null, ""));
assert!(ip(&json!("2001:db8::1"), "ipv6", &Value::Null, ""));
assert!(!ip(&json!("127.0.0.1"), "ipv6", &Value::Null, ""));
}
#[test]
fn test_allow_ip() {
assert!(allow_ip(
&json!("127.0.0.1"),
"127.0.0.1,192.168.1.1",
&Value::Null,
""
));
assert!(!allow_ip(
&json!("10.0.0.1"),
"127.0.0.1,192.168.1.1",
&Value::Null,
""
));
}
#[test]
fn test_deny_ip() {
assert!(!deny_ip(
&json!("127.0.0.1"),
"127.0.0.1,192.168.1.1",
&Value::Null,
""
));
assert!(deny_ip(
&json!("10.0.0.1"),
"127.0.0.1,192.168.1.1",
&Value::Null,
""
));
}
#[test]
fn test_active_url_valid_domain() {
assert!(active_url(&json!("localhost"), "", &Value::Null, ""));
}
#[test]
fn test_active_url_invalid() {
assert!(!active_url(
&json!("not.a.valid.domain.example.invalid"),
"",
&Value::Null,
""
));
assert!(!active_url(&json!(""), "", &Value::Null, ""));
assert!(!active_url(&json!(123), "", &Value::Null, ""));
}
#[test]
fn test_value_loose_equals_str_numeric() {
assert!(value_loose_equals_str(&json!(1), "1"));
assert!(value_loose_equals_str(&json!(1.0), "1"));
assert!(value_loose_equals_str(&json!("1"), "1"));
assert!(!value_loose_equals_str(&json!(2), "1"));
}
#[test]
fn test_value_loose_equals_str_string() {
assert!(value_loose_equals_str(&json!("hello"), "hello"));
assert!(!value_loose_equals_str(&json!("hello"), "world"));
}
#[test]
fn test_value_loose_compare_numeric() {
use std::cmp::Ordering;
assert_eq!(
value_loose_compare(&json!(5), &json!(3)),
Some(Ordering::Greater)
);
assert_eq!(
value_loose_compare(&json!(3), &json!(5)),
Some(Ordering::Less)
);
assert_eq!(
value_loose_compare(&json!(5), &json!(5)),
Some(Ordering::Equal)
);
}
#[test]
fn test_value_length_string() {
assert_eq!(value_length(&json!("abc")), 3);
assert_eq!(value_length(&json!("中文")), 2); }
#[test]
fn test_value_length_array() {
assert_eq!(value_length(&json!([1, 2, 3])), 3);
assert_eq!(value_length(&json!([])), 0);
}
#[test]
fn test_parse_timestamp_iso() {
let ts = parse_timestamp(&json!("2024-01-01 12:00:00"));
assert!(ts.is_some());
}
#[test]
fn test_parse_timestamp_numeric() {
let ts = parse_timestamp(&json!(1700000000));
assert_eq!(ts, Some(1700000000));
}
#[test]
fn test_php_date_format_to_chrono_simple() {
assert_eq!(php_date_format_to_chrono("Y-m-d"), "%Y-%m-%d");
assert_eq!(
php_date_format_to_chrono("Y/m/d H:i:s"),
"%Y/%m/%d %H:%M:%S"
);
}
}