use std::cmp::Ordering;
use openapiv3::Schema;
use openapiv3::SchemaKind;
use openapiv3::Type;
use crate::error::Error;
use crate::error::Result;
use crate::ir::Bound;
use crate::ir::Constraints;
use crate::ir::Field;
use crate::ir::RustType;
use crate::lower::schema::X_RUST_TYPE;
fn fold_bound(bound: Option<i64>, exclusive: bool, step: i64) -> (Option<i64>, bool) {
let Some(value) = bound else {
return (None, false);
};
if !exclusive {
return (Some(value), false);
}
return match value.checked_add(step) {
Some(moved) => (Some(moved), false),
None => (Some(value), true),
};
}
pub(crate) fn inclusive_minimum(it: &openapiv3::IntegerType) -> Option<i64> {
return fold_bound(it.minimum, it.exclusive_minimum, 1).0;
}
pub(crate) fn constraints_of(schema: &Schema) -> Option<Constraints> {
let mut found = Constraints::default();
match &schema.schema_kind {
SchemaKind::Type(Type::String(st)) => {
found.pattern = st.pattern.clone();
found.min_length = st.min_length;
found.max_length = st.max_length;
}
SchemaKind::Type(Type::Integer(it)) => {
let (minimum, exclusive_minimum) = fold_bound(it.minimum, it.exclusive_minimum, 1);
let (maximum, exclusive_maximum) = fold_bound(it.maximum, it.exclusive_maximum, -1);
found.minimum = minimum.map(Bound::Int);
found.maximum = maximum.map(Bound::Int);
found.exclusive_minimum = exclusive_minimum;
found.exclusive_maximum = exclusive_maximum;
found.folded_minimum = it.exclusive_minimum && !exclusive_minimum;
found.folded_maximum = it.exclusive_maximum && !exclusive_maximum;
found.multiple_of = it.multiple_of.map(Bound::Int);
}
SchemaKind::Type(Type::Number(nt)) => {
found.minimum = nt.minimum.map(Bound::Float);
found.maximum = nt.maximum.map(Bound::Float);
found.exclusive_minimum = nt.exclusive_minimum;
found.exclusive_maximum = nt.exclusive_maximum;
found.multiple_of = nt.multiple_of.map(Bound::Float);
}
SchemaKind::Type(Type::Array(at)) => {
found.min_items = at.min_items;
found.max_items = at.max_items;
found.unique_items = at.unique_items;
}
SchemaKind::Type(Type::Object(ot)) => {
found.min_properties = ot.min_properties;
found.max_properties = ot.max_properties;
}
SchemaKind::Type(Type::Boolean(_))
| SchemaKind::OneOf { .. }
| SchemaKind::AnyOf { .. }
| SchemaKind::AllOf { .. }
| SchemaKind::Not { .. }
| SchemaKind::Any(_) => {}
}
if found.minimum.is_none() {
found.exclusive_minimum = false;
found.folded_minimum = false;
}
if found.maximum.is_none() {
found.exclusive_maximum = false;
found.folded_maximum = false;
}
if found.is_empty() {
return None;
}
return Some(found);
}
pub(crate) fn constraints_through_ref(target: &Schema) -> Option<Constraints> {
let mut found = constraints_of(target)?;
if target.schema_data.extensions.contains_key(X_RUST_TYPE) {
return None;
}
let checked_as = match &target.schema_kind {
SchemaKind::Type(Type::String(st)) if st.enumeration.is_empty() => {
crate::lower::schema::string_format_type(&st.format)
}
SchemaKind::Type(Type::Integer(it)) if it.enumeration.is_empty() => crate::lower::schema::integer_type(it),
SchemaKind::Type(Type::Number(nt)) if nt.enumeration.is_empty() => RustType::F64,
_ => return None,
};
found.checked_as = Some(checked_as);
return Some(found);
}
fn integer_limits(ty: &RustType) -> Option<(i64, Option<i64>)> {
return match *ty {
RustType::I32 => Some((i64::from(i32::MIN), Some(i64::from(i32::MAX)))),
RustType::U32 => Some((0, Some(i64::from(u32::MAX)))),
RustType::I64 => Some((i64::MIN, Some(i64::MAX))),
RustType::U64 => Some((0, None)),
_ => None,
};
}
fn int_bound(bound: Option<Bound>) -> Option<i64> {
return match bound {
Some(Bound::Int(value)) => Some(value),
Some(Bound::Float(_)) | None => None,
};
}
fn crossed_bounds_reason(constraints: &Constraints) -> Option<String> {
let low = constraints.minimum?;
let high = constraints.maximum?;
let order = match (low, high) {
(Bound::Int(low), Bound::Int(high)) => low.cmp(&high),
(Bound::Float(low), Bound::Float(high)) => low.partial_cmp(&high)?,
_ => return None,
};
let low_text = bound_text(low);
let high_text = bound_text(high);
if order == Ordering::Greater {
return Some(format!(
"the bounds accept no value: they allow `{low_text}` to `{high_text}`"
));
}
if order == Ordering::Equal && (constraints.exclusive_minimum || constraints.exclusive_maximum) {
return Some(format!(
"the bounds accept no value: they meet at `{low_text}`, which an `exclusive` flag then leaves out"
));
}
return None;
}
fn empty_range_reason(constraints: &Constraints, checked: &RustType) -> Option<String> {
if let Some(reason) = crossed_bounds_reason(constraints) {
return Some(reason);
}
let (type_low, type_high) = integer_limits(checked)?;
let minimum = int_bound(constraints.minimum);
let maximum = int_bound(constraints.maximum);
let label = checked.label();
let stops_above = |written: i64| {
return Some(format!(
"the bounds accept no value: nothing lies above `{written}`, where `{label}` stops"
));
};
let starts_below = |written: i64| {
return Some(format!(
"the bounds accept no value: nothing lies below `{written}`, where `{label}` starts"
));
};
if let Some(low) = minimum
&& let Some(top) = type_high
{
if constraints.folded_minimum && low > top {
return stops_above(low - 1);
}
if constraints.exclusive_minimum && low >= top {
return stops_above(low);
}
}
if let Some(high) = maximum {
if constraints.folded_maximum && high < type_low {
return starts_below(high + 1);
}
if constraints.exclusive_maximum && high <= type_low {
return starts_below(high);
}
}
return None;
}
pub(crate) fn check_constraints(field: &Field) -> Result<()> {
let Some(constraints) = &field.constraints else {
return Ok(());
};
let checked = match &constraints.checked_as {
Some(ty) => ty,
None => field.ty.innermost(),
};
let name = field.name.logical();
let mut diagnostics = crate::lower::validate::Diagnostics::new();
if let Some(step) = constraints.multiple_of {
let positive = match step {
Bound::Int(value) => value > 0,
Bound::Float(value) => value > 0.0_f64,
};
if !positive {
diagnostics.push(Error::UnsupportedSchema {
path: name.to_owned(),
reason: format!("the `multipleOf` value `{}` is not above zero", bound_text(step)),
});
}
}
match empty_range_reason(constraints, checked) {
Some(reason) => diagnostics.push(Error::UnsupportedSchema {
path: name.to_owned(),
reason,
}),
None => check_width(constraints, checked, name, &mut diagnostics),
}
check_reach(constraints, checked, name, &mut diagnostics);
return diagnostics.into_result();
}
fn check_width(
constraints: &Constraints,
checked: &RustType,
name: &str,
diagnostics: &mut crate::lower::validate::Diagnostics,
) {
if !matches!(*checked, RustType::I32 | RustType::U32 | RustType::U64) {
return;
}
for (keyword, bound) in [
("minimum", constraints.minimum),
("maximum", constraints.maximum),
("multipleOf", constraints.multiple_of),
] {
let Some(Bound::Int(value)) = bound else {
continue;
};
if keyword == "multipleOf" && value <= 0 {
continue;
}
let fits = match *checked {
RustType::I32 => i32::try_from(value).is_ok(),
RustType::U32 => u32::try_from(value).is_ok(),
RustType::U64 => u64::try_from(value).is_ok(),
_ => true,
};
if !fits {
diagnostics.push(Error::UnsupportedSchema {
path: name.to_owned(),
reason: format!("the `{keyword}` value `{value}` does not fit `{}`", checked.label()),
});
}
}
}
fn check_reach(
constraints: &Constraints,
checked: &RustType,
name: &str,
diagnostics: &mut crate::lower::validate::Diagnostics,
) {
let mut unreachable = |keyword: &str| {
diagnostics.push(Error::UnsupportedSchema {
path: name.to_owned(),
reason: format!("the `{keyword}` rule does not reach the type this field holds"),
});
};
if !matches!(*checked, RustType::String) {
for (keyword, present) in [
("pattern", constraints.pattern.is_some()),
("minLength", constraints.min_length.is_some_and(|min| return min > 0)),
("maxLength", constraints.max_length.is_some()),
] {
if present {
unreachable(keyword);
}
}
}
if !matches!(*checked, RustType::Map(_)) {
for (keyword, present) in [
(
"minProperties",
constraints.min_properties.is_some_and(|min| return min > 0),
),
("maxProperties", constraints.max_properties.is_some()),
] {
if present {
unreachable(keyword);
}
}
}
let comparable = matches!(*checked, RustType::Vec(ref element) if element.is_scalar());
if constraints.unique_items && !comparable {
unreachable("uniqueItems");
}
}
fn bound_text(bound: Bound) -> String {
return match bound {
Bound::Int(value) => format!("{value}"),
Bound::Float(value) => format!("{value}"),
};
}