const OCTET: &str = "(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])";
const H16: &str = "[0-9a-fA-F]{1,4}";
pub const STRING_FORMATS: [&str; 7] = [
"uuid",
"ipv4",
"ipv6",
"hostname",
"email",
"uri",
"uri-reference",
];
pub const SUPPORTED_FORMATS: [&str; 11] = [
"uuid",
"ipv4",
"ipv6",
"hostname",
"email",
"uri",
"uri-reference",
"date-time",
"date",
"time",
"duration",
];
pub const TEMPORAL_FORMATS: [&str; 4] = ["date-time", "date", "time", "duration"];
pub(crate) fn accepted_set_is_contained_by(narrower: &str, wider: &str) -> bool {
matches!(
(narrower, wider),
("uri", "uri-reference")
| ("hostname", "uri-reference")
| ("uuid", "hostname")
| ("ipv4", "hostname")
| ("date", "hostname")
| ("duration", "uri-reference")
| ("uuid", "uri-reference")
| ("ipv4", "uri-reference")
| ("date", "uri-reference")
)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TemporalKind {
DateTime,
Date,
Time,
Duration,
}
impl TemporalKind {
pub fn from_name(name: &str) -> Option<Self> {
match name {
"date-time" => Some(Self::DateTime),
"date" => Some(Self::Date),
"time" => Some(Self::Time),
"duration" => Some(Self::Duration),
_ => None,
}
}
pub fn name(self) -> &'static str {
match self {
Self::DateTime => "date-time",
Self::Date => "date",
Self::Time => "time",
Self::Duration => "duration",
}
}
pub fn pattern(self) -> &'static str {
materialized_pattern(self)
}
}
pub fn materialized_pattern(kind: TemporalKind) -> &'static str {
match kind {
TemporalKind::Date => "^[0-9]{4}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$",
TemporalKind::Time => {
"^([01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9](\\.[0-9]+)?([Zz]|[+-]((0[0-9]|1[0-7]):[0-5][0-9]|18:00))?$"
}
TemporalKind::DateTime => {
"^[0-9]{4}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])[Tt]([01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9](\\.[0-9]+)?([Zz]|[+-]((0[0-9]|1[0-7]):[0-5][0-9]|18:00))$"
}
TemporalKind::Duration => {
"^PT(?:[0-9]+H(?:[0-9]+M(?:[0-9]+S)?)?|[0-9]+M(?:[0-9]+S)?|[0-9]+S)$"
}
}
}
pub fn is_leap_year(year: i64) -> bool {
(year % 4 == 0 && year % 100 != 0) || year % 400 == 0
}
pub fn days_in_month(year: i64, month: u32) -> Option<u32> {
Some(match month {
1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
4 | 6 | 9 | 11 => 30,
2 => {
if is_leap_year(year) {
29
} else {
28
}
}
_ => return None,
})
}
pub const MAX_DURATION_NANOS: i128 = i64::MAX as i128;
fn valid_calendar_prefix(value: &str) -> bool {
let bytes = value.as_bytes();
if bytes.len() < 10 {
return false;
}
let (Ok(year), Ok(month), Ok(day)) = (
value[0..4].parse::<i64>(),
value[5..7].parse::<u32>(),
value[8..10].parse::<u32>(),
) else {
return false;
};
if year < 1 {
return false;
}
match days_in_month(year, month) {
Some(max) => (1..=max).contains(&day),
None => false,
}
}
pub fn duration_total_nanos(value: &str) -> Option<i128> {
let body = value.strip_prefix("PT")?;
let mut total_seconds: i128 = 0;
let mut number = String::new();
for ch in body.chars() {
if ch.is_ascii_digit() {
number.push(ch);
continue;
}
let magnitude: i128 = number.parse().ok()?;
number.clear();
let unit_seconds: i128 = match ch {
'H' => 3600,
'M' => 60,
'S' => 1,
_ => return None,
};
total_seconds = total_seconds.checked_add(magnitude.checked_mul(unit_seconds)?)?;
if total_seconds > MAX_DURATION_NANOS {
return None;
}
}
let total_nanos = total_seconds.checked_mul(1_000_000_000)?;
if total_nanos > MAX_DURATION_NANOS {
return None;
}
Some(total_nanos)
}
pub fn canonicalize(kind: TemporalKind, value: &str) -> Option<String> {
if !is_valid_materialized(kind, value) {
return None;
}
Some(match kind {
TemporalKind::Date => value.to_string(),
TemporalKind::Time | TemporalKind::DateTime => canonicalize_clock(value),
TemporalKind::Duration => canonicalize_duration(value)?,
})
}
pub fn canonicalize_for_format(format: &str, value: &str) -> Option<String> {
TemporalKind::from_name(format).and_then(|kind| canonicalize(kind, value))
}
fn canonicalize_clock(value: &str) -> String {
let (body, offset) =
if let Some(body) = value.strip_suffix('Z').or_else(|| value.strip_suffix('z')) {
(body, "Z")
} else if value.len() >= 6 && value.is_char_boundary(value.len() - 6) {
let tail = &value[value.len() - 6..];
let bytes = tail.as_bytes();
if (bytes[0] == b'+' || bytes[0] == b'-') && bytes[3] == b':' {
let folded = if &tail[1..] == "00:00" { "Z" } else { tail };
(&value[..value.len() - 6], folded)
} else {
(value, "")
}
} else {
(value, "")
};
let body = match body.split_once('.') {
Some((whole, fraction)) => {
let trimmed = fraction.trim_end_matches('0');
if trimmed.is_empty() {
whole.to_string()
} else {
format!("{whole}.{trimmed}")
}
}
None => body.to_string(),
};
format!("{}{offset}", body.replace('t', "T"))
}
pub fn canonicalize_duration(value: &str) -> Option<String> {
let total_nanos = duration_total_nanos(value)?;
let total_seconds = (total_nanos / 1_000_000_000) as i128;
Some(format_duration_seconds(total_seconds))
}
pub fn format_duration_seconds(total_seconds: i128) -> String {
if total_seconds == 0 {
return "PT0S".to_string();
}
let hours = total_seconds / 3600;
let minutes = (total_seconds % 3600) / 60;
let seconds = total_seconds % 60;
let mut out = String::from("PT");
if hours != 0 {
out.push_str(&format!("{hours}H"));
}
if minutes != 0 {
out.push_str(&format!("{minutes}M"));
}
if seconds != 0 {
out.push_str(&format!("{seconds}S"));
}
out
}
pub fn is_valid_materialized(kind: TemporalKind, value: &str) -> bool {
let matched = regex::Regex::new(materialized_pattern(kind))
.expect("pinned materialized pattern compiles")
.is_match(value);
if !matched {
return false;
}
match kind {
TemporalKind::Date | TemporalKind::DateTime => valid_calendar_prefix(value),
TemporalKind::Time => true,
TemporalKind::Duration => duration_total_nanos(value).is_some(),
}
}
pub const DEFERRED_FORMATS: [&str; 8] = [
"idn-email",
"idn-hostname",
"iri",
"iri-reference",
"uri-template",
"json-pointer",
"relative-json-pointer",
"regex",
];
#[derive(Debug, Clone)]
pub struct FormatCheck {
pub name: &'static str,
pub pattern: String,
pub max_code_points: Option<usize>,
}
pub enum FormatClass {
Supported(FormatCheck),
Temporal(TemporalKind),
Deferred,
Unknown,
}
fn ipv6_body() -> String {
let v4 = format!("({OCTET}\\.{OCTET}\\.{OCTET}\\.{OCTET})");
let ls32 = format!("({H16}:{H16}|{v4})");
format!(
"({H16}:){{6}}{ls32}|\
::({H16}:){{5}}{ls32}|\
({H16})?::({H16}:){{4}}{ls32}|\
(({H16}:){{0,1}}{H16})?::({H16}:){{3}}{ls32}|\
(({H16}:){{0,2}}{H16})?::({H16}:){{2}}{ls32}|\
(({H16}:){{0,3}}{H16})?::({H16}:){ls32}|\
(({H16}:){{0,4}}{H16})?::{ls32}|\
(({H16}:){{0,5}}{H16})?::{H16}|\
(({H16}:){{0,6}}{H16})?::"
)
}
fn uri_body(reference: bool) -> &'static str {
if reference {
include_str!("../../specs/json-schema/corpora/format_uri/pinned_body_uriref.body")
} else {
include_str!("../../specs/json-schema/corpora/format_uri/pinned_body.body")
}
}
pub fn check_for(name: &str) -> Option<FormatCheck> {
let anchored = |body: String| format!("^(?:{body})$");
match name {
"uuid" => Some(FormatCheck {
name: "uuid",
pattern:
"^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$"
.to_string(),
max_code_points: None,
}),
"ipv4" => Some(FormatCheck {
name: "ipv4",
pattern: format!("^{OCTET}\\.{OCTET}\\.{OCTET}\\.{OCTET}$"),
max_code_points: None,
}),
"ipv6" => Some(FormatCheck {
name: "ipv6",
pattern: anchored(ipv6_body()),
max_code_points: None,
}),
"hostname" => Some(FormatCheck {
name: "hostname",
pattern: "^[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?(?:\\.[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?)*$"
.to_string(),
max_code_points: Some(253),
}),
"email" => Some(FormatCheck {
name: "email",
pattern: "^[a-zA-Z0-9!#$%&'*+/=?^_`{|}~-]+(?:\\.[a-zA-Z0-9!#$%&'*+/=?^_`{|}~-]+)*@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+$"
.to_string(),
max_code_points: Some(254),
}),
"uri" => Some(FormatCheck {
name: "uri",
pattern: anchored(uri_body(false).trim().to_string()),
max_code_points: None,
}),
"uri-reference" => Some(FormatCheck {
name: "uri-reference",
pattern: anchored(uri_body(true).trim().to_string()),
max_code_points: None,
}),
_ => None,
}
}
pub fn classify(name: &str) -> FormatClass {
if let Some(check) = check_for(name) {
FormatClass::Supported(check)
} else if let Some(kind) = TemporalKind::from_name(name) {
FormatClass::Temporal(kind)
} else if DEFERRED_FORMATS.contains(&name) {
FormatClass::Deferred
} else {
FormatClass::Unknown
}
}
pub fn is_valid(name: &str, value: &str) -> bool {
if let Some(kind) = TemporalKind::from_name(name) {
return is_valid_materialized(kind, value);
}
let Some(check) = check_for(name) else {
return true;
};
if let Some(max) = check.max_code_points
&& value.chars().count() > max
{
return false;
}
regex::Regex::new(&check.pattern)
.expect("pinned format pattern compiles")
.is_match(value)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn containment_table_is_directional() {
assert!(accepted_set_is_contained_by("uuid", "hostname"));
assert!(accepted_set_is_contained_by("uuid", "uri-reference"));
assert!(!accepted_set_is_contained_by("hostname", "uuid"));
assert!(!accepted_set_is_contained_by("email", "uri-reference"));
}
#[test]
fn pinned_patterns_pass_the_pattern_gate() {
for name in STRING_FORMATS {
let check = check_for(name).expect("supported");
crate::json_schema::pattern::gate_and_normalize(&check.pattern).unwrap_or_else(
|error| panic!("{name} pinned pattern rejected by gate: {error:?}"),
);
}
for name in TEMPORAL_FORMATS {
let kind = TemporalKind::from_name(name).expect("temporal");
crate::json_schema::pattern::gate_and_normalize(kind.pattern()).unwrap_or_else(
|error| panic!("{name} pinned pattern rejected by gate: {error:?}"),
);
}
}
#[test]
fn supported_formats_names_every_accepted_format() {
for name in STRING_FORMATS {
assert!(SUPPORTED_FORMATS.contains(&name), "{name}");
}
for name in TEMPORAL_FORMATS {
assert!(SUPPORTED_FORMATS.contains(&name), "{name}");
}
for name in SUPPORTED_FORMATS {
assert!(
!matches!(classify(name), FormatClass::Unknown | FormatClass::Deferred),
"{name} is advertised but not accepted"
);
}
}
#[test]
fn uuid_accepts_canonical_rejects_malformed() {
assert!(is_valid("uuid", "de305d54-75b4-431b-adb2-eb6b9e546013"));
assert!(!is_valid("uuid", "not-a-uuid"));
assert!(!is_valid("uuid", "de305d54-75b4-431b-adb2-eb6b9e54601")); }
#[test]
fn ipv4_rejects_out_of_range_and_leading_zero() {
assert!(is_valid("ipv4", "192.168.0.1"));
assert!(!is_valid("ipv4", "256.0.0.1"));
assert!(!is_valid("ipv4", "01.2.3.4"));
}
#[test]
fn ipv6_rejects_double_colon() {
assert!(is_valid("ipv6", "2001:db8::1"));
assert!(is_valid("ipv6", "::1"));
assert!(is_valid("ipv6", "::ffff:192.0.2.1"));
assert!(!is_valid("ipv6", "1::2::3"));
assert!(!is_valid("ipv6", "zzzz"));
}
#[test]
fn email_rejects_single_label_domain_and_enforces_length() {
assert!(is_valid("email", "user@example.com"));
assert!(!is_valid("email", "user@localhost"));
let huge = format!("{}@example.com", "a".repeat(300));
assert!(!is_valid("email", &huge));
}
#[test]
fn hostname_enforces_length_and_rejects_trailing_dot() {
assert!(is_valid("hostname", "example.com"));
assert!(!is_valid("hostname", "example.com."));
let long_label = "a".repeat(64);
assert!(!is_valid("hostname", &long_label));
let over = (0..64).map(|_| "abc").collect::<Vec<_>>().join(".");
assert!(over.chars().count() > 253);
assert!(!is_valid("hostname", &over));
}
#[test]
fn uri_splices_ipv6_and_requires_scheme() {
assert!(is_valid("uri", "https://example.com/path?q=1#frag"));
assert!(is_valid("uri", "http://[2001:db8::1]/"));
assert!(is_valid("uri", "http://[::1]"));
assert!(!is_valid("uri", "http://[1::2::3]"));
assert!(!is_valid("uri", "//example.com/no-scheme"));
assert!(!is_valid("uri", "http://example.com/%2"));
}
#[test]
fn uri_reference_allows_relative() {
assert!(is_valid("uri-reference", "/relative/path"));
assert!(is_valid("uri-reference", "https://example.com"));
assert!(!is_valid("uri-reference", "http://[1::2::3]"));
}
#[test]
fn classify_partitions_names() {
assert!(matches!(classify("uuid"), FormatClass::Supported(_)));
assert!(matches!(
classify("date-time"),
FormatClass::Temporal(TemporalKind::DateTime)
));
assert!(matches!(
classify("duration"),
FormatClass::Temporal(TemporalKind::Duration)
));
assert!(matches!(classify("iri"), FormatClass::Deferred));
assert!(matches!(classify("phone"), FormatClass::Unknown));
assert!(matches!(classify("datetime"), FormatClass::Unknown));
}
#[test]
fn materialized_temporal_matches_the_conformance_corpus() {
let corpus: serde_json::Value = serde_json::from_str(include_str!(
"../../specs/json-schema/corpora/format_conformance/corpus.json"
))
.expect("corpus parses");
for pair in corpus["pairs"].as_array().expect("pairs") {
let format = pair["format"].as_str().unwrap_or_default();
let Some(kind) = TemporalKind::from_name(format) else {
continue;
};
let value = pair["value"].as_str().expect("value string");
let expect = pair["expect_valid"].as_bool().expect("expect_valid");
assert_eq!(
is_valid_materialized(kind, value),
expect,
"materialized {format} {value:?}"
);
}
}
#[test]
fn materialized_duration_time_only_and_canonicalizes() {
assert!(is_valid_materialized(TemporalKind::Duration, "PT1H30M15S"));
assert!(is_valid_materialized(TemporalKind::Duration, "PT0S"));
assert!(is_valid_materialized(TemporalKind::Duration, "PT90M"));
assert!(!is_valid_materialized(TemporalKind::Duration, "P1Y"));
assert!(!is_valid_materialized(TemporalKind::Duration, "P4W"));
assert!(!is_valid_materialized(TemporalKind::Duration, "P1D"));
assert!(!is_valid_materialized(TemporalKind::Duration, "P1YT1H"));
assert_eq!(canonicalize_duration("PT90M").as_deref(), Some("PT1H30M"));
assert_eq!(canonicalize_duration("PT3600S").as_deref(), Some("PT1H"));
assert_eq!(
canonicalize_duration("PT1H30M15S").as_deref(),
Some("PT1H30M15S")
);
assert_eq!(canonicalize_duration("PT0S").as_deref(), Some("PT0S"));
assert!(!is_valid_materialized(
TemporalKind::Duration,
"PT999999999999H"
));
}
#[test]
fn canonicalize_covers_every_temporal_kind() {
use TemporalKind::*;
let cases = [
(Date, "2021-06-15", "2021-06-15"),
(DateTime, "2021-06-15t12:30:45z", "2021-06-15T12:30:45Z"),
(Time, "12:30:45z", "12:30:45Z"),
(
DateTime,
"2021-06-15T12:30:45+00:00",
"2021-06-15T12:30:45Z",
),
(
DateTime,
"2021-06-15T12:30:45-00:00",
"2021-06-15T12:30:45Z",
),
(
DateTime,
"2021-06-15T12:30:45-05:00",
"2021-06-15T12:30:45-05:00",
),
(Time, "12:30:45+00:00", "12:30:45Z"),
(Time, "12:30:45-00:00", "12:30:45Z"),
(Time, "12:30:45+02:00", "12:30:45+02:00"),
(
DateTime,
"2021-06-15T12:30:45.120Z",
"2021-06-15T12:30:45.12Z",
),
(DateTime, "2021-06-15T12:30:45.000Z", "2021-06-15T12:30:45Z"),
(
DateTime,
"2021-06-15t12:30:45.5-03:00",
"2021-06-15T12:30:45.5-03:00",
),
(Time, "12:30:45.250", "12:30:45.25"),
(Time, "12:30:45.000-00:00", "12:30:45Z"),
(Time, "12:30:45", "12:30:45"),
(Duration, "PT90M", "PT1H30M"),
(Duration, "PT3600S", "PT1H"),
(Duration, "PT0S", "PT0S"),
];
for (kind, input, expected) in cases {
assert_eq!(
canonicalize(kind, input).as_deref(),
Some(expected),
"canonicalize({kind:?}, {input:?})"
);
assert!(is_valid_materialized(kind, expected), "{expected:?}");
assert_eq!(canonicalize(kind, expected).as_deref(), Some(expected));
}
assert_eq!(canonicalize(TemporalKind::Date, "2021-02-30"), None);
assert_eq!(canonicalize(TemporalKind::DateTime, "2021-06-15"), None);
assert_eq!(canonicalize(TemporalKind::Duration, "P1D"), None);
assert_eq!(
canonicalize_for_format("date-time", "2021-06-15t12:30:45z").as_deref(),
Some("2021-06-15T12:30:45Z")
);
assert_eq!(canonicalize_for_format("uuid", "not-a-uuid"), None);
}
fn clock_row_is_valid(row: &serde_json::Value) -> bool {
row["expect_valid"].as_bool().unwrap_or(true)
}
#[test]
fn canonicalize_is_idempotent_over_the_clock_corpus() {
let corpus: serde_json::Value = serde_json::from_str(include_str!(
"../../specs/json-schema/corpora/format_materialize_clock/corpus.json"
))
.expect("corpus parses");
for (key, kind) in [
("date-time", TemporalKind::DateTime),
("date", TemporalKind::Date),
("time", TemporalKind::Time),
] {
for row in corpus[key].as_array().expect("rows") {
let wire = row["wire"].as_str().expect("wire");
let Some(canonical) = canonicalize(kind, wire) else {
assert!(
!clock_row_is_valid(row),
"{key} {wire:?} is declared valid but does not canonicalize"
);
continue;
};
assert_eq!(
canonicalize(kind, &canonical).as_deref(),
Some(canonical.as_str()),
"{key} {wire:?} is not a canonicalization fixed point"
);
assert!(!canonical.contains('t') && !canonical.contains('z'));
assert!(!canonical.ends_with("+00:00") && !canonical.ends_with("-00:00"));
}
}
}
#[test]
fn materialized_fraction_is_accepted_at_every_width() {
for kind in [TemporalKind::DateTime, TemporalKind::Time] {
let render = |fraction: &str| match kind {
TemporalKind::DateTime => format!("2021-01-15T12:30:45{fraction}Z"),
_ => format!("12:30:45{fraction}Z"),
};
assert!(is_valid_materialized(kind, &render("")), "{kind:?} bare");
for digits in [1, 2, 3, 6, 7, 9, 10, 12, 20] {
let fraction = format!(".{}", "1".repeat(digits));
assert!(
is_valid_materialized(kind, &render(&fraction)),
"{kind:?} {digits} fractional digits must be accepted \
(the contract is truncate-per-target, not reject)"
);
}
assert!(!is_valid_materialized(kind, &render(".")));
}
assert!(is_valid_materialized(
TemporalKind::DateTime,
"2021-01-15T12:30:45.123456789012Z"
));
}
#[test]
fn materialized_calendar_starts_at_year_one() {
assert!(is_valid_materialized(TemporalKind::Date, "0001-01-01"));
assert!(!is_valid_materialized(TemporalKind::Date, "0000-01-01"));
assert!(is_valid_materialized(
TemporalKind::DateTime,
"0001-01-01T00:00:00Z"
));
assert!(!is_valid_materialized(
TemporalKind::DateTime,
"0000-01-01T00:00:00Z"
));
}
#[test]
fn materialized_offsets_are_limited_to_eighteen_hours() {
for kind in [TemporalKind::DateTime, TemporalKind::Time] {
let render = |offset: &str| match kind {
TemporalKind::DateTime => format!("2021-01-15T12:30:45{offset}"),
_ => format!("12:30:45{offset}"),
};
for offset in ["+18:00", "-18:00"] {
assert!(
is_valid_materialized(kind, &render(offset)),
"{kind:?} must accept {offset}"
);
}
for offset in ["+18:01", "-18:01", "+23:59", "-23:59"] {
assert!(
!is_valid_materialized(kind, &render(offset)),
"{kind:?} must reject {offset}"
);
}
}
}
#[test]
fn materialized_clock_roundtrip_values_are_valid() {
let corpus: serde_json::Value = serde_json::from_str(include_str!(
"../../specs/json-schema/corpora/format_materialize_clock/corpus.json"
))
.expect("corpus parses");
for (key, kind) in [
("date-time", TemporalKind::DateTime),
("date", TemporalKind::Date),
("time", TemporalKind::Time),
] {
for row in corpus[key].as_array().expect("rows") {
let wire = row["wire"].as_str().expect("wire");
let expect = clock_row_is_valid(row);
assert_eq!(
is_valid_materialized(kind, wire),
expect,
"clock {key} {wire:?}"
);
}
}
}
#[test]
fn conformance_corpora_agree() {
let corpus: serde_json::Value = serde_json::from_str(include_str!(
"../../specs/json-schema/corpora/format_conformance/corpus.json"
))
.expect("corpus parses");
for pair in corpus["pairs"].as_array().expect("pairs") {
let format = pair["format"].as_str().unwrap_or_default();
if !STRING_FORMATS.contains(&format) {
continue;
}
let value = pair["value"].as_str().expect("value string");
let expect = pair["expect_valid"].as_bool().expect("expect_valid");
assert_eq!(
is_valid(format, value),
expect,
"format_conformance {format} {value:?}"
);
}
}
#[test]
fn hostname_corpus_agrees() {
let corpus: serde_json::Value = serde_json::from_str(include_str!(
"../../specs/json-schema/corpora/format_hostname/corpus.json"
))
.expect("corpus parses");
for case in corpus["cases"].as_array().expect("cases") {
let instance = case["instance"].as_str().expect("instance string");
let expect = case["valid"].as_bool().expect("valid");
assert_eq!(
is_valid("hostname", instance),
expect,
"hostname {instance:?}"
);
}
}
#[test]
fn uri_corpus_all_uri() {
let corpus: serde_json::Value = serde_json::from_str(include_str!(
"../../specs/json-schema/corpora/format_uri/corpus.json"
))
.expect("corpus parses");
for pair in corpus["pairs"].as_array().expect("pairs") {
let value = pair["value"].as_str().expect("value string");
let expect = pair["expect"].as_bool().expect("expect");
assert_eq!(is_valid("uri", value), expect, "uri {value:?}");
}
}
#[test]
fn email_corpus_agrees() {
let corpus: serde_json::Value = serde_json::from_str(include_str!(
"../../specs/json-schema/corpora/format_email/corpus.json"
))
.expect("corpus parses");
for pair in corpus["pairs"].as_array().expect("pairs") {
let instance = pair["instance"].as_str().expect("instance string");
let expect = pair["expect_valid"].as_bool().expect("expect_valid");
assert_eq!(is_valid("email", instance), expect, "email {instance:?}");
}
}
}