use crate::prelude::*;
#[derive(Clone, Debug, Serialize)]
pub struct Type {
#[serde(skip_serializing_if = "<[_]>::is_empty")]
normalizers: &'static [TypeNormalizer],
#[serde(skip_serializing_if = "<[_]>::is_empty")]
validators: &'static [TypeValidator],
#[serde(skip_serializing_if = "<[_]>::is_empty")]
rules: &'static [SourceRule],
}
impl Type {
#[must_use]
pub const fn new(
normalizers: &'static [TypeNormalizer],
validators: &'static [TypeValidator],
rules: &'static [SourceRule],
) -> Self {
Self {
normalizers,
validators,
rules,
}
}
#[must_use]
pub const fn normalizers(&self) -> &'static [TypeNormalizer] {
self.normalizers
}
#[must_use]
pub const fn validators(&self) -> &'static [TypeValidator] {
self.validators
}
#[must_use]
pub const fn rules(&self) -> &'static [SourceRule] {
self.rules
}
}
impl ValidateNode for Type {}
impl VisitableNode for Type {
fn drive<V: Visitor>(&self, v: &mut V) {
for node in self.normalizers() {
node.accept(v);
}
for node in self.validators() {
node.accept(v);
}
for node in self.rules() {
node.accept(v);
}
}
}
#[derive(Clone, Debug, Serialize)]
pub struct SourceRule {
name: &'static str,
operation: SourceRuleAuthoringOperation,
}
impl SourceRule {
#[must_use]
pub const fn new(name: &'static str, operation: SourceRuleAuthoringOperation) -> Self {
Self { name, operation }
}
#[must_use]
pub const fn name(&self) -> &'static str {
self.name
}
#[must_use]
pub const fn operation(&self) -> &SourceRuleAuthoringOperation {
&self.operation
}
}
impl ValidateNode for SourceRule {
fn validate(&self) -> Result<(), ErrorTree> {
let mut errs = ErrorTree::new();
validate_source_name(
&mut errs,
"rule",
self.name(),
icydb_schema::RuleSourceKey::try_new,
);
if let Err(message) = self.operation().validate_shape() {
err!(errs, "rule '{}': {message}", self.name());
}
errs.result()
}
}
impl VisitableNode for SourceRule {}
#[derive(Clone, Debug, Serialize)]
pub enum SourceRuleAuthoringOperation {
LengthRangeInclusive {
min: RuleNumber,
max: RuleNumber,
},
MultipleOf {
divisor: RuleNumber,
},
NumericMaximumInclusive {
value: RuleNumber,
},
NumericMinimumInclusive {
value: RuleNumber,
},
NumericRangeInclusive {
min: RuleNumber,
max: RuleNumber,
},
}
impl SourceRuleAuthoringOperation {
fn validate_shape(&self) -> Result<(), &'static str> {
match self {
Self::LengthRangeInclusive { min, max } => {
let min = rule_length_bound(min).ok_or(
"length_range_inclusive operands must be nonnegative integers within u64",
)?;
let max = rule_length_bound(max).ok_or(
"length_range_inclusive operands must be nonnegative integers within u64",
)?;
if min > max {
return Err("length_range_inclusive requires min <= max");
}
}
Self::MultipleOf { divisor } => {
if !rule_number_is_valid(divisor) {
return Err("multiple_of divisor must be a valid numeric literal");
}
if rule_number_is_zero(divisor) {
return Err("multiple_of divisor must be nonzero");
}
}
Self::NumericMaximumInclusive { value } | Self::NumericMinimumInclusive { value } => {
if !rule_number_is_valid(value) {
return Err("numeric rule value must be a valid numeric literal");
}
}
Self::NumericRangeInclusive { min, max } => {
if !rule_number_is_valid(min) || !rule_number_is_valid(max) {
return Err("numeric range operands must be valid numeric literals");
}
}
}
Ok(())
}
}
#[derive(Clone, Debug, Serialize)]
pub enum RuleNumber {
Integer(&'static str),
Decimal(&'static str),
Float32(f32),
Float64(f64),
}
fn rule_length_bound(value: &RuleNumber) -> Option<u64> {
match value {
RuleNumber::Integer(value) => value.parse().ok(),
RuleNumber::Decimal(_) | RuleNumber::Float32(_) | RuleNumber::Float64(_) => None,
}
}
fn rule_number_is_zero(value: &RuleNumber) -> bool {
match value {
RuleNumber::Integer(value) => {
value.parse::<i128>().is_ok_and(|value| value == 0)
|| value.parse::<u128>().is_ok_and(|value| value == 0)
}
RuleNumber::Decimal(value) => value
.parse::<icydb_schema::Decimal>()
.is_ok_and(|value| value.is_zero()),
RuleNumber::Float32(value) => *value == 0.0,
RuleNumber::Float64(value) => *value == 0.0,
}
}
fn rule_number_is_valid(value: &RuleNumber) -> bool {
match value {
RuleNumber::Integer(value) => {
value.parse::<i128>().is_ok() || value.parse::<u128>().is_ok()
}
RuleNumber::Decimal(value) => value.parse::<icydb_schema::Decimal>().is_ok(),
RuleNumber::Float32(value) => value.is_finite(),
RuleNumber::Float64(value) => value.is_finite(),
}
}
#[derive(Clone, Debug, Serialize)]
pub struct TypeNormalizer {
path: &'static str,
args: Args,
}
impl TypeNormalizer {
#[must_use]
pub const fn new(path: &'static str, args: Args) -> Self {
Self { path, args }
}
#[must_use]
pub const fn path(&self) -> &'static str {
self.path
}
#[must_use]
pub const fn args(&self) -> &Args {
&self.args
}
}
impl ValidateNode for TypeNormalizer {
fn validate(&self) -> Result<(), ErrorTree> {
let mut errs = ErrorTree::new();
let res = schema_read().check_node_as::<Normalizer>(self.path());
if let Err(e) = res {
errs.add(e.to_string());
}
errs.result()
}
}
impl VisitableNode for TypeNormalizer {}
#[derive(Clone, Debug, Serialize)]
pub struct TypeValidator {
path: &'static str,
args: Args,
}
impl TypeValidator {
#[must_use]
pub const fn new(path: &'static str, args: Args) -> Self {
Self { path, args }
}
#[must_use]
pub const fn path(&self) -> &'static str {
self.path
}
#[must_use]
pub const fn args(&self) -> &Args {
&self.args
}
}
impl ValidateNode for TypeValidator {
fn validate(&self) -> Result<(), ErrorTree> {
let mut errs = ErrorTree::new();
let res = schema_read().check_node_as::<Validator>(self.path());
if let Err(e) = res {
errs.add(e.to_string());
}
errs.result()
}
}
impl VisitableNode for TypeValidator {}
#[cfg(test)]
mod tests {
use super::{RuleNumber, SourceRuleAuthoringOperation};
#[test]
fn directly_constructed_rule_numbers_validate_before_lowering() {
assert_eq!(
SourceRuleAuthoringOperation::MultipleOf {
divisor: RuleNumber::Decimal("not-a-decimal"),
}
.validate_shape(),
Err("multiple_of divisor must be a valid numeric literal"),
);
assert_eq!(
SourceRuleAuthoringOperation::NumericMaximumInclusive {
value: RuleNumber::Float64(f64::NAN),
}
.validate_shape(),
Err("numeric rule value must be a valid numeric literal"),
);
}
}