///|
/// Numeric bounds accept both integer arguments and Double arguments.
pub(open) trait NumericBound {
fn to_bound(Self) -> Double
}
///|
pub impl NumericBound for Int32 with fn to_bound(self) {
self.to_double()
}
///|
pub impl NumericBound for Int64 with fn to_bound(self) {
self.to_double()
}
///|
pub impl NumericBound for Double with fn to_bound(self) {
self
}
///|
fn is_safe_integer(value : Double) -> Bool {
!value.is_nan() &&
!value.is_inf() &&
value == value.floor() &&
value.abs() <= 9007199254740991.0
}
///|
fn decimal_parts(value : Double) -> (@bigint.BigInt, Int32) {
let chars = value.abs().to_string().to_array()
let digits = StringBuilder()
let mut fraction = 0
let mut after_dot = false
let mut i = 0
while i < chars.length() && chars[i] != 'e' && chars[i] != 'E' {
if chars[i] == '.' {
after_dot = true
} else {
digits.write_char(chars[i])
if after_dot {
fraction = fraction + 1
}
}
i = i + 1
}
let mut exponent = 0
let mut negative = false
if i < chars.length() {
i = i + 1
if chars[i] == '-' {
negative = true
i = i + 1
} else if chars[i] == '+' {
i = i + 1
}
while i < chars.length() {
exponent = exponent * 10 + chars[i].to_int() - 48
i = i + 1
}
}
(
@bigint.BigInt::from_string(digits.to_string()),
(if negative { -exponent } else { exponent }) - fraction,
)
}
///|
fn is_decimal_multiple(value : Double, divisor : Double) -> Bool {
if value.is_nan() || value.is_inf() {
return false
}
if is_safe_integer(value) && is_safe_integer(divisor) {
return value % divisor == 0.0
}
let (left, left_scale) = decimal_parts(value)
let (right, right_scale) = decimal_parts(divisor)
let scale = if left_scale < right_scale { left_scale } else { right_scale }
let ten = @bigint.BigInt::from_int(10)
let numerator = left * ten.pow(@bigint.BigInt::from_int(left_scale - scale))
let denominator = right *
ten.pow(@bigint.BigInt::from_int(right_scale - scale))
(numerator % denominator).is_zero()
}
///|
/// Create a schema that validates JSON numbers.
pub fn number(
required_error~ : String = "",
invalid_type_error~ : String = "",
) -> Schema {
{
id: new_schema_id(),
schema_type: NumberType,
rules: [],
description: "",
required_error,
invalid_type_error,
name: "",
brand: "",
}
}
///|
/// Require the number to be an integer (no fractional part).
pub fn Schema::int(self : Schema, msg~ : String = "") -> Schema {
guard_no_user_transform(self)
match inner_type(self.schema_type) {
NumberType => ()
_ => abort("int() is only valid for number schemas")
}
let message = if msg.is_empty() { "Value must be an integer" } else { msg }
append_builtin_rule(
self,
fn(json) {
match json {
Number(v, ..) => is_safe_integer(v)
_ => false
}
},
message,
IssueCode::InvalidFormat("integer"),
Json::object({
"type": Json::string("integer"),
"minimum": Json::number(-9007199254740991.0),
"maximum": Json::number(9007199254740991.0),
}),
explicit_message=!msg.is_empty(),
)
}
///|
/// Require the number to be positive (> 0).
pub fn Schema::positive(self : Schema, msg~ : String = "") -> Schema {
guard_no_user_transform(self)
match inner_type(self.schema_type) {
NumberType => ()
_ => abort("positive() is only valid for number schemas")
}
let message = if msg.is_empty() { "Value must be positive" } else { msg }
append_builtin_rule(
self,
fn(json) {
match json {
Number(v, ..) => v > 0.0
_ => false
}
},
message,
IssueCode::TooSmall("number", 0.0, false),
Json::object({ "exclusiveMinimum": Json::number(0.0) }),
explicit_message=!msg.is_empty(),
)
}
///|
/// Require the number to be negative (< 0).
pub fn Schema::negative(self : Schema, msg~ : String = "") -> Schema {
guard_no_user_transform(self)
match inner_type(self.schema_type) {
NumberType => ()
_ => abort("negative() is only valid for number schemas")
}
let message = if msg.is_empty() { "Value must be negative" } else { msg }
append_builtin_rule(
self,
fn(json) {
match json {
Number(v, ..) => v < 0.0
_ => false
}
},
message,
IssueCode::TooBig("number", 0.0, false),
Json::object({ "exclusiveMaximum": Json::number(0.0) }),
explicit_message=!msg.is_empty(),
)
}
///|
/// Require the number to be a multiple of `n`.
pub fn[N : NumericBound] Schema::multipleOf(
self : Schema,
n : N,
msg~ : String = "",
) -> Schema {
let n = n.to_bound()
if n.is_nan() || n.is_inf() || n <= 0.0 {
abort("multipleOf() requires a positive finite divisor")
}
guard_no_user_transform(self)
match inner_type(self.schema_type) {
NumberType => ()
_ => abort("multipleOf() is only valid for number schemas")
}
let message = if msg.is_empty() {
"Value must be a multiple of \{n}"
} else {
msg
}
append_builtin_rule(
self,
fn(json) {
match json {
Number(v, ..) => is_decimal_multiple(v, n)
_ => false
}
},
message,
IssueCode::NotMultipleOf(n),
Json::object({ "multipleOf": Json::number(n) }),
explicit_message=!msg.is_empty(),
)
}
///|
/// Require the number to be finite (reject Infinity and NaN).
pub fn Schema::finite(self : Schema, msg~ : String = "") -> Schema {
guard_no_user_transform(self)
match inner_type(self.schema_type) {
NumberType => ()
_ => abort("finite() is only valid for number schemas")
}
let message = if msg.is_empty() {
"Value must be a finite number"
} else {
msg
}
append_builtin_rule(
self,
fn(json) {
match json {
Number(v, ..) => !v.is_nan() && !v.is_inf()
_ => false
}
},
message,
IssueCode::Custom,
Json::null(),
constraint=Some(Finite),
explicit_message=!msg.is_empty(),
)
}
///|
/// Require the number to be a safe integer (within ±2^53-1).
pub fn Schema::safe(self : Schema, msg~ : String = "") -> Schema {
guard_no_user_transform(self)
match inner_type(self.schema_type) {
NumberType => ()
_ => abort("safe() is only valid for number schemas")
}
let message = if msg.is_empty() {
"Value must be a safe integer"
} else {
msg
}
let min_safe = -9007199254740991.0
let max_safe = 9007199254740991.0
append_builtin_rule(
self,
fn(json) {
match json {
Number(v, ..) => is_safe_integer(v) && v >= min_safe && v <= max_safe
_ => false
}
},
message,
IssueCode::Custom,
Json::object({
"type": Json::string("integer"),
"minimum": Json::number(min_safe),
"maximum": Json::number(max_safe),
}),
explicit_message=!msg.is_empty(),
constraint=Some(SafeInteger),
)
}
///|
/// Require a number to be strictly greater than a finite bound.
pub fn[N : NumericBound] Schema::gt(
self : Schema,
bound : N,
msg~ : String = "",
) -> Schema {
let bound = bound.to_bound()
guard_numeric_bound(self, bound)
append_builtin_rule(
self,
fn(value) {
match value {
Number(n, ..) => n > bound
_ => false
}
},
if msg.is_empty() {
"Value must be > \{bound}"
} else {
msg
},
TooSmall("number", bound, false),
Json::object({ "exclusiveMinimum": Json::number(bound) }),
explicit_message=!msg.is_empty(),
)
}
///|
/// Require a number to be strictly less than a finite bound.
pub fn[N : NumericBound] Schema::lt(
self : Schema,
bound : N,
msg~ : String = "",
) -> Schema {
let bound = bound.to_bound()
guard_numeric_bound(self, bound)
append_builtin_rule(
self,
fn(value) {
match value {
Number(n, ..) => n < bound
_ => false
}
},
if msg.is_empty() {
"Value must be < \{bound}"
} else {
msg
},
TooBig("number", bound, false),
Json::object({ "exclusiveMaximum": Json::number(bound) }),
explicit_message=!msg.is_empty(),
)
}
///|
fn guard_numeric_bound(schema : Schema, bound : Double) -> Unit {
guard_no_user_transform(schema)
if bound.is_nan() || bound.is_inf() {
abort("numeric bounds must be finite")
}
guard inner_type(schema.schema_type) is NumberType else {
abort("numeric bounds require a number schema")
}
}