use serde_json::Value;
#[cfg(feature = "openapi")]
use std::cmp::Ordering;
use crate::error::{IssueCode, ValidationIssue, VldError, issue_error};
use crate::schema::VldSchema;
use super::{Kind, Num, NumberCheck, ZInt};
#[derive(Clone)]
pub struct ZNumber {
checks: Vec<NumberCheck>,
pub(super) coerce: bool,
custom_type_error: Option<String>,
messages: crate::primitives::Messages,
}
impl ZNumber {
pub fn new() -> Self {
Self {
checks: vec![],
coerce: false,
custom_type_error: None,
messages: None,
}
}
fn push(mut self, kind: Kind) -> Self {
self.checks.push(NumberCheck { kind, msg: None });
self
}
pub fn type_error(mut self, msg: impl Into<String>) -> Self {
self.custom_type_error = Some(msg.into());
self
}
pub fn with_messages<F>(mut self, f: F) -> Self
where
F: Fn(&str) -> Option<String> + Send + Sync + 'static,
{
self.messages = Some(std::sync::Arc::new(f));
self
}
pub(super) fn min_n(self, v: Num) -> Self {
self.push(Kind::Min(v))
}
pub(super) fn max_n(self, v: Num) -> Self {
self.push(Kind::Max(v))
}
pub(super) fn gt_n(self, v: Num) -> Self {
self.push(Kind::Gt(v))
}
pub(super) fn lt_n(self, v: Num) -> Self {
self.push(Kind::Lt(v))
}
pub(super) fn multiple_of_n(self, v: Num) -> Self {
self.push(Kind::MultipleOf(v))
}
pub fn min(self, val: f64) -> Self {
self.min_n(Num::F(val))
}
pub fn max(self, val: f64) -> Self {
self.max_n(Num::F(val))
}
pub fn gt(self, val: f64) -> Self {
self.gt_n(Num::F(val))
}
pub fn gte(self, val: f64) -> Self {
self.min(val)
}
pub fn lt(self, val: f64) -> Self {
self.lt_n(Num::F(val))
}
pub fn lte(self, val: f64) -> Self {
self.max(val)
}
pub fn positive(self) -> Self {
self.push(Kind::Positive)
}
pub fn negative(self) -> Self {
self.push(Kind::Negative)
}
pub fn non_negative(self) -> Self {
self.push(Kind::NonNegative)
}
pub fn non_positive(self) -> Self {
self.push(Kind::NonPositive)
}
pub fn multiple_of(self, val: f64) -> Self {
self.multiple_of_n(Num::F(val))
}
pub fn safe(self) -> Self {
self.push(Kind::Safe)
}
pub fn int(self) -> ZInt {
ZInt::from_number(self)
}
pub fn coerce(mut self) -> Self {
self.coerce = true;
self
}
fn type_err(&self, value: &Value) -> VldError {
crate::primitives::type_error(&self.custom_type_error, "number", value)
}
fn coerce_err(&self, value: &Value) -> VldError {
issue_error(
IssueCode::InvalidType {
expected: "number".to_string(),
received: "string".to_string(),
},
self.custom_type_error.as_ref(),
value,
)
}
pub(super) fn extract_number(&self, value: &Value) -> Result<f64, VldError> {
match value {
Value::Number(n) => n.as_f64().ok_or_else(|| self.type_err(value)),
Value::String(s) if self.coerce => s
.parse::<f64>()
.ok()
.filter(|f| f.is_finite())
.ok_or_else(|| self.coerce_err(value)),
Value::Bool(b) if self.coerce => Ok(if *b { 1.0 } else { 0.0 }),
_ => Err(self.type_err(value)),
}
}
pub(super) fn validate(&self, n: Num, value: &Value) -> Result<(), VldError> {
let mut errors = VldError::new();
for check in &self.checks {
if let Some(code) = check.kind.failure(n) {
let bulk = || self.messages.as_ref().and_then(|f| f(check.kind.key()));
let msg = check.msg.clone().or_else(bulk);
errors
.issues
.push(ValidationIssue::with_override(code, msg.as_ref()).with_received(value));
}
}
if errors.is_empty() {
Ok(())
} else {
Err(errors)
}
}
}
impl Default for ZNumber {
fn default() -> Self {
Self::new()
}
}
impl VldSchema for ZNumber {
type Output = f64;
fn parse_value(&self, value: &Value) -> Result<f64, VldError> {
let n = self.extract_number(value)?;
self.validate(Num::F(n), value)?;
Ok(n)
}
}
#[cfg(feature = "openapi")]
impl crate::openapi::JsonSchema for ZNumber {
fn json_schema(&self) -> serde_json::Value {
self.schema("number", None)
}
}
impl ZNumber {
#[cfg(feature = "openapi")]
pub(super) fn schema(&self, ty: &str, range: Option<(Num, Num)>) -> Value {
let mut schema = serde_json::json!({ "type": ty });
let pick = |a: (Num, bool), b: (Num, bool), want: Ordering| match b.0.cmp(a.0) {
Some(o) if o == want => b,
Some(Ordering::Equal) if b.1 => b,
_ => a,
};
let lower = range
.map(|(lo, _)| (lo, false))
.into_iter()
.chain(self.checks.iter().filter_map(|c| c.kind.lower()))
.reduce(|a, b| pick(a, b, Ordering::Greater));
let upper = range
.map(|(_, hi)| (hi, false))
.into_iter()
.chain(self.checks.iter().filter_map(|c| c.kind.upper()))
.reduce(|a, b| pick(a, b, Ordering::Less));
if let Some((v, excl)) = lower {
let key = if excl { "exclusiveMinimum" } else { "minimum" };
schema[key] = v.json();
}
if let Some((v, excl)) = upper {
let key = if excl { "exclusiveMaximum" } else { "maximum" };
schema[key] = v.json();
}
let mut multiples = self.checks.iter().filter_map(|c| match c.kind {
Kind::MultipleOf(v) if v.as_f64() != 0.0 => Some(v.json()),
_ => None,
});
if let Some(m) = multiples.next() {
schema["multipleOf"] = m;
}
let rest: Vec<Value> = multiples
.map(|m| serde_json::json!({ "multipleOf": m }))
.collect();
if !rest.is_empty() {
schema["allOf"] = Value::Array(rest);
}
schema
}
}
#[cfg(feature = "openapi")]
impl crate::openapi::CollectNestedSchemas for ZNumber {}