use std::{cmp::Ordering, hash::Hash};
use anyhow::Context;
use json_canon::to_string as to_canonical_json;
use oas3::spec::ObjectSchema;
use serde_json::{Number, Value};
#[derive(Debug, Clone, Eq)]
pub struct CanonicalSchema(String);
impl CanonicalSchema {
pub fn from_schema(schema: &ObjectSchema) -> anyhow::Result<Self> {
let mut value = serde_json::to_value(schema).context("Failed to serialize schema for canonicalization")?;
normalize_schema_semantics(&mut value);
let canonical_json = to_canonical_json(&value).context("Failed to create canonical JSON string")?;
Ok(CanonicalSchema(canonical_json))
}
}
impl PartialEq for CanonicalSchema {
fn eq(&self, other: &Self) -> bool {
self.0 == other.0
}
}
impl PartialOrd for CanonicalSchema {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for CanonicalSchema {
fn cmp(&self, other: &Self) -> Ordering {
self.0.cmp(&other.0)
}
}
impl Hash for CanonicalSchema {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
let hash = blake3::hash(self.0.as_bytes());
hash.as_bytes().hash(state);
}
}
fn normalize_schema_semantics(value: &mut Value) {
match value {
Value::Object(map) => {
if let Some(Value::Array(arr)) = map.get_mut("required") {
sort_string_array_in_place(arr);
}
if let Some(Value::Array(arr)) = map.get_mut("type") {
sort_string_array_in_place(arr);
}
for value in map.values_mut() {
normalize_schema_semantics(value);
}
}
Value::Array(arr) => {
for item in arr {
normalize_schema_semantics(item);
}
}
Value::Number(n) => {
if let Some(clamped) = clamp_number_to_safe_range(n) {
*value = Value::Number(clamped);
}
}
_ => {}
}
}
const MAX_SAFE_INTEGER: i64 = 9_007_199_254_740_991;
const MIN_SAFE_INTEGER: i64 = -9_007_199_254_740_991;
#[allow(clippy::cast_precision_loss)]
fn clamp_number_to_safe_range(n: &serde_json::Number) -> Option<serde_json::Number> {
if let Some(i) = n.as_i64() {
return match i {
i if i > MAX_SAFE_INTEGER => Some(MAX_SAFE_INTEGER.into()),
i if i < MIN_SAFE_INTEGER => Some(MIN_SAFE_INTEGER.into()),
_ => None,
};
}
if let Some(u) = n.as_u64() {
return (u > MAX_SAFE_INTEGER as u64).then_some(MAX_SAFE_INTEGER.into());
}
if let Some(f) = n.as_f64() {
let (min_f, max_f) = (MIN_SAFE_INTEGER as f64, MAX_SAFE_INTEGER as f64);
if f < min_f || f > max_f {
return Number::from_f64(f.clamp(min_f, max_f));
}
}
None
}
fn sort_string_array_in_place(arr: &mut Vec<Value>) {
let mut strings: Vec<String> = arr.iter().filter_map(|v| v.as_str().map(String::from)).collect();
if strings.len() == arr.len() {
strings.sort_unstable();
*arr = strings.into_iter().map(Value::String).collect();
}
}