use crate::schema::graph::{SchemaGraph, SchemaTypeDef, TypedSchemaValue};
use crate::schema::metadata::{MetadataEnvelope, TypeId};
use crate::schema::schema_type::{SchemaType, VariantCaseType};
use crate::schema::schema_value::{
BinaryValuePayload, DurationValuePayload, SchemaValue, TextValuePayload, VariantValuePayload,
};
use crate::schema::validation::{SchemaError, validate_graph};
use std::collections::{BTreeMap, HashMap, HashSet};
use std::fmt::{self, Display, Formatter};
#[cfg(feature = "bigdecimal")]
mod bigdecimal;
#[cfg(feature = "bit_vec")]
mod bit_vec;
#[cfg(feature = "bytes")]
mod bytes;
#[cfg(feature = "chrono")]
mod chrono;
#[cfg(feature = "mac_address")]
mod mac_address;
#[cfg(feature = "nonempty_collections")]
mod nonempty_collections;
#[cfg(feature = "num_bigint")]
mod num_bigint;
#[cfg(feature = "rust_decimal")]
mod rust_decimal;
#[cfg(feature = "serde_json_types")]
mod serde_json_types;
#[cfg(feature = "url")]
mod url;
mod quantity;
pub use quantity::{Quantity, QuantityUnit};
pub trait IntoSchema {
fn type_id() -> TypeId;
fn register_in(builder: &mut SchemaBuilder) -> SchemaType;
fn to_value(&self) -> SchemaValue;
}
pub trait FromSchema: Sized {
fn from_value(value: &SchemaValue) -> Result<Self, FromSchemaError>;
}
pub fn try_into_schema_graph<T: IntoSchema + ?Sized>() -> Result<SchemaGraph, SchemaError> {
let mut builder = SchemaBuilder::new();
let root = T::register_in(&mut builder);
let graph = SchemaGraph {
defs: builder.finish(),
root,
};
match validate_graph(&graph) {
Ok(()) => Ok(graph),
Err(mut errors) => Err(errors.remove(0)),
}
}
pub fn try_into_typed_schema_value<T: IntoSchema + ?Sized>(
value: &T,
) -> Result<TypedSchemaValue, SchemaError> {
let graph = try_into_schema_graph::<T>()?;
Ok(TypedSchemaValue::new(graph, value.to_value()))
}
pub trait IntoTypedSchemaValue: IntoSchema {
#[allow(clippy::wrong_self_convention)]
fn into_typed_schema_value(&self) -> Result<TypedSchemaValue, SchemaError>;
}
impl<T: IntoSchema + ?Sized> IntoTypedSchemaValue for T {
fn into_typed_schema_value(&self) -> Result<TypedSchemaValue, SchemaError> {
try_into_typed_schema_value(self)
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum MergeError {
ConflictingDefinitions {
id: TypeId,
first_name: Option<String>,
second_name: Option<String>,
},
}
impl Display for MergeError {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self {
MergeError::ConflictingDefinitions {
id,
first_name,
second_name,
} => write!(
f,
"conflicting definitions for TypeId `{id}` while merging agent graphs \
(first name = {first_name:?}, second name = {second_name:?})"
),
}
}
}
impl std::error::Error for MergeError {}
pub fn merge_agent_graphs(
graphs: impl IntoIterator<Item = SchemaGraph>,
) -> Result<SchemaGraph, MergeError> {
let mut merged: Vec<SchemaTypeDef> = Vec::new();
for graph in graphs {
for def in graph.defs {
if let Some(existing) = merged.iter().find(|d| d.id == def.id) {
if existing.body != def.body {
return Err(MergeError::ConflictingDefinitions {
id: def.id,
first_name: existing.name.clone(),
second_name: def.name,
});
}
} else {
merged.push(def);
}
}
}
Ok(SchemaGraph {
defs: merged,
root: SchemaGraph::empty().root,
})
}
#[derive(Default)]
pub struct SchemaBuilder {
defs: Vec<SchemaTypeDef>,
reserved: HashSet<TypeId>,
}
impl SchemaBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn is_registered(&self, id: &TypeId) -> bool {
self.reserved.contains(id) || self.defs.iter().any(|d| &d.id == id)
}
pub fn reserve(&mut self, id: TypeId) {
self.reserved.insert(id);
}
pub fn commit(
&mut self,
id: TypeId,
name: Option<String>,
metadata: MetadataEnvelope,
body: SchemaType,
) {
self.reserved.remove(&id);
let body = body.with_metadata(metadata);
if let Some(existing) = self.defs.iter_mut().find(|d| d.id == id) {
*existing = SchemaTypeDef { id, name, body };
} else {
self.defs.push(SchemaTypeDef { id, name, body });
}
}
pub fn finish(self) -> Vec<SchemaTypeDef> {
self.defs
}
pub fn into_graph(self, root: SchemaType) -> SchemaGraph {
SchemaGraph {
defs: self.finish(),
root,
}
}
}
pub fn normalize_type_path(raw: &str) -> String {
raw.replace("::", ".")
}
pub fn type_id_with_args(base: &str, args: &[TypeId]) -> TypeId {
if args.is_empty() {
TypeId::new(base.to_string())
} else {
let mut s = String::with_capacity(base.len() + 16);
s.push_str(base);
s.push('<');
for (i, a) in args.iter().enumerate() {
if i > 0 {
s.push_str(", ");
}
s.push_str(a.as_str());
}
s.push('>');
TypeId::new(s)
}
}
pub fn default_type_id_from(raw: &str) -> TypeId {
TypeId::new(normalize_type_path(raw))
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum FromSchemaError {
ShapeMismatch {
expected: String,
actual: String,
context: String,
},
OutOfRange {
index: u32,
len: u32,
context: String,
},
UnknownUnionTag(String),
Custom(String),
}
pub type DecodeError = FromSchemaError;
impl FromSchemaError {
pub fn shape_mismatch(
expected: impl Into<String>,
actual: impl Into<String>,
context: impl Into<String>,
) -> Self {
Self::ShapeMismatch {
expected: expected.into(),
actual: actual.into(),
context: context.into(),
}
}
pub fn out_of_range(index: u32, len: u32, context: impl Into<String>) -> Self {
Self::OutOfRange {
index,
len,
context: context.into(),
}
}
pub fn custom(msg: impl Into<String>) -> Self {
Self::Custom(msg.into())
}
}
impl Display for FromSchemaError {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self {
FromSchemaError::ShapeMismatch {
expected,
actual,
context,
} => write!(
f,
"shape mismatch in {context}: expected {expected}, got {actual}"
),
FromSchemaError::OutOfRange {
index,
len,
context,
} => write!(f, "index {index} out of range (len {len}) in {context}"),
FromSchemaError::UnknownUnionTag(tag) => write!(f, "unknown union tag `{tag}`"),
FromSchemaError::Custom(msg) => f.write_str(msg),
}
}
}
impl std::error::Error for FromSchemaError {}
pub fn value_kind(v: &SchemaValue) -> &'static str {
match v {
SchemaValue::Bool(_) => "bool",
SchemaValue::S8(_) => "s8",
SchemaValue::S16(_) => "s16",
SchemaValue::S32(_) => "s32",
SchemaValue::S64(_) => "s64",
SchemaValue::U8(_) => "u8",
SchemaValue::U16(_) => "u16",
SchemaValue::U32(_) => "u32",
SchemaValue::U64(_) => "u64",
SchemaValue::F32(_) => "f32",
SchemaValue::F64(_) => "f64",
SchemaValue::Char(_) => "char",
SchemaValue::String(_) => "string",
SchemaValue::Record { .. } => "record",
SchemaValue::Variant(_) => "variant",
SchemaValue::Enum { .. } => "enum",
SchemaValue::Flags { .. } => "flags",
SchemaValue::Tuple { .. } => "tuple",
SchemaValue::List { .. } => "list",
SchemaValue::FixedList { .. } => "fixed-list",
SchemaValue::Map { .. } => "map",
SchemaValue::Option { .. } => "option",
SchemaValue::Result(_) => "result",
SchemaValue::Text(_) => "text",
SchemaValue::Binary(_) => "binary",
SchemaValue::Path { .. } => "path",
SchemaValue::Url { .. } => "url",
SchemaValue::Uuid(_) => "uuid",
SchemaValue::Datetime { .. } => "datetime",
SchemaValue::Duration(_) => "duration",
SchemaValue::Quantity(_) => "quantity",
SchemaValue::Union(_) => "union",
SchemaValue::Secret(_) => "secret",
SchemaValue::QuotaToken(_) => "quota-token",
SchemaValue::Stream(_) => "stream",
SchemaValue::PermissionCard(_) => "permission-card",
}
}
pub fn text_to_value(body: String, language: Option<String>) -> SchemaValue {
SchemaValue::Text(TextValuePayload {
text: body,
language,
})
}
pub fn text_from_value(v: &SchemaValue, context: &'static str) -> Result<String, FromSchemaError> {
match v {
SchemaValue::Text(p) => Ok(p.text.clone()),
other => Err(FromSchemaError::shape_mismatch(
"text",
value_kind(other),
context,
)),
}
}
pub fn binary_to_value(bytes: Vec<u8>, mime_type: Option<String>) -> SchemaValue {
SchemaValue::Binary(BinaryValuePayload { bytes, mime_type })
}
pub fn binary_from_value(
v: &SchemaValue,
context: &'static str,
) -> Result<Vec<u8>, FromSchemaError> {
match v {
SchemaValue::Binary(p) => Ok(p.bytes.clone()),
other => Err(FromSchemaError::shape_mismatch(
"binary",
value_kind(other),
context,
)),
}
}
pub fn path_to_value(path: String) -> SchemaValue {
SchemaValue::Path { path }
}
pub fn path_from_value(v: &SchemaValue, context: &'static str) -> Result<String, FromSchemaError> {
match v {
SchemaValue::Path { path } => Ok(path.clone()),
other => Err(FromSchemaError::shape_mismatch(
"path",
value_kind(other),
context,
)),
}
}
pub fn url_to_value(url: String) -> SchemaValue {
SchemaValue::Url { url }
}
pub fn url_from_value(v: &SchemaValue, context: &'static str) -> Result<String, FromSchemaError> {
match v {
SchemaValue::Url { url } => Ok(url.clone()),
other => Err(FromSchemaError::shape_mismatch(
"url",
value_kind(other),
context,
)),
}
}
macro_rules! impl_primitive {
($ty:ty, $type_id:literal, $schema_ty:expr, $variant:ident) => {
impl IntoSchema for $ty {
fn type_id() -> TypeId {
TypeId::new($type_id)
}
fn register_in(_b: &mut SchemaBuilder) -> SchemaType {
$schema_ty
}
fn to_value(&self) -> SchemaValue {
SchemaValue::$variant(*self)
}
}
impl FromSchema for $ty {
fn from_value(v: &SchemaValue) -> Result<Self, FromSchemaError> {
match v {
SchemaValue::$variant(x) => Ok(*x),
other => Err(FromSchemaError::shape_mismatch(
stringify!($variant),
value_kind(other),
stringify!($ty),
)),
}
}
}
};
}
impl_primitive!(bool, "bool", SchemaType::bool(), Bool);
impl_primitive!(i8, "i8", SchemaType::s8(), S8);
impl_primitive!(i16, "i16", SchemaType::s16(), S16);
impl_primitive!(i32, "i32", SchemaType::s32(), S32);
impl_primitive!(i64, "i64", SchemaType::s64(), S64);
impl_primitive!(u8, "u8", SchemaType::u8(), U8);
impl_primitive!(u16, "u16", SchemaType::u16(), U16);
impl_primitive!(u32, "u32", SchemaType::u32(), U32);
impl_primitive!(u64, "u64", SchemaType::u64(), U64);
impl_primitive!(f32, "f32", SchemaType::f32(), F32);
impl_primitive!(f64, "f64", SchemaType::f64(), F64);
impl_primitive!(char, "char", SchemaType::char(), Char);
impl IntoSchema for String {
fn type_id() -> TypeId {
TypeId::new("alloc.string.String")
}
fn register_in(_b: &mut SchemaBuilder) -> SchemaType {
SchemaType::string()
}
fn to_value(&self) -> SchemaValue {
SchemaValue::String(self.clone())
}
}
impl FromSchema for String {
fn from_value(v: &SchemaValue) -> Result<Self, FromSchemaError> {
match v {
SchemaValue::String(s) => Ok(s.clone()),
other => Err(FromSchemaError::shape_mismatch(
"string",
value_kind(other),
"String",
)),
}
}
}
impl IntoSchema for str {
fn type_id() -> TypeId {
TypeId::new("alloc.string.String")
}
fn register_in(_b: &mut SchemaBuilder) -> SchemaType {
SchemaType::string()
}
fn to_value(&self) -> SchemaValue {
SchemaValue::String(self.to_string())
}
}
impl<T: IntoSchema + ?Sized> IntoSchema for &T {
fn type_id() -> TypeId {
T::type_id()
}
fn register_in(b: &mut SchemaBuilder) -> SchemaType {
T::register_in(b)
}
fn to_value(&self) -> SchemaValue {
(**self).to_value()
}
}
impl<T: IntoSchema + ?Sized> IntoSchema for Box<T> {
fn type_id() -> TypeId {
T::type_id()
}
fn register_in(b: &mut SchemaBuilder) -> SchemaType {
T::register_in(b)
}
fn to_value(&self) -> SchemaValue {
(**self).to_value()
}
}
impl<T: FromSchema> FromSchema for Box<T> {
fn from_value(v: &SchemaValue) -> Result<Self, FromSchemaError> {
Ok(Box::new(T::from_value(v)?))
}
}
impl<T: IntoSchema + ?Sized> IntoSchema for std::rc::Rc<T> {
fn type_id() -> TypeId {
T::type_id()
}
fn register_in(b: &mut SchemaBuilder) -> SchemaType {
T::register_in(b)
}
fn to_value(&self) -> SchemaValue {
(**self).to_value()
}
}
impl<T: FromSchema> FromSchema for std::rc::Rc<T> {
fn from_value(v: &SchemaValue) -> Result<Self, FromSchemaError> {
Ok(std::rc::Rc::new(T::from_value(v)?))
}
}
impl<T: IntoSchema + ?Sized> IntoSchema for std::sync::Arc<T> {
fn type_id() -> TypeId {
T::type_id()
}
fn register_in(b: &mut SchemaBuilder) -> SchemaType {
T::register_in(b)
}
fn to_value(&self) -> SchemaValue {
(**self).to_value()
}
}
impl<T: FromSchema> FromSchema for std::sync::Arc<T> {
fn from_value(v: &SchemaValue) -> Result<Self, FromSchemaError> {
Ok(std::sync::Arc::new(T::from_value(v)?))
}
}
impl<T: IntoSchema> IntoSchema for Option<T> {
fn type_id() -> TypeId {
type_id_with_args("core.option.Option", &[T::type_id()])
}
fn register_in(b: &mut SchemaBuilder) -> SchemaType {
SchemaType::option(T::register_in(b))
}
fn to_value(&self) -> SchemaValue {
SchemaValue::Option {
inner: self.as_ref().map(|x| Box::new(x.to_value())),
}
}
}
impl<T: FromSchema> FromSchema for Option<T> {
fn from_value(v: &SchemaValue) -> Result<Self, FromSchemaError> {
match v {
SchemaValue::Option { inner: Some(x) } => Ok(Some(T::from_value(x)?)),
SchemaValue::Option { inner: None } => Ok(None),
other => Err(FromSchemaError::shape_mismatch(
"option",
value_kind(other),
"Option",
)),
}
}
}
impl<T: IntoSchema> IntoSchema for Vec<T> {
fn type_id() -> TypeId {
type_id_with_args("alloc.vec.Vec", &[T::type_id()])
}
fn register_in(b: &mut SchemaBuilder) -> SchemaType {
SchemaType::list(T::register_in(b))
}
fn to_value(&self) -> SchemaValue {
SchemaValue::List {
elements: self.iter().map(|x| x.to_value()).collect(),
}
}
}
impl<T: FromSchema> FromSchema for Vec<T> {
fn from_value(v: &SchemaValue) -> Result<Self, FromSchemaError> {
match v {
SchemaValue::List { elements } => elements.iter().map(T::from_value).collect(),
other => Err(FromSchemaError::shape_mismatch(
"list",
value_kind(other),
"Vec",
)),
}
}
}
impl<T: IntoSchema> IntoSchema for HashSet<T> {
fn type_id() -> TypeId {
type_id_with_args("std.collections.HashSet", &[T::type_id()])
}
fn register_in(b: &mut SchemaBuilder) -> SchemaType {
SchemaType::list(T::register_in(b))
}
fn to_value(&self) -> SchemaValue {
SchemaValue::List {
elements: self.iter().map(|x| x.to_value()).collect(),
}
}
}
impl<T: FromSchema + Eq + std::hash::Hash> FromSchema for HashSet<T> {
fn from_value(v: &SchemaValue) -> Result<Self, FromSchemaError> {
match v {
SchemaValue::List { elements } => elements.iter().map(T::from_value).collect(),
other => Err(FromSchemaError::shape_mismatch(
"list",
value_kind(other),
"HashSet",
)),
}
}
}
impl<K: IntoSchema, V: IntoSchema> IntoSchema for HashMap<K, V> {
fn type_id() -> TypeId {
type_id_with_args("std.collections.HashMap", &[K::type_id(), V::type_id()])
}
fn register_in(b: &mut SchemaBuilder) -> SchemaType {
SchemaType::map(K::register_in(b), V::register_in(b))
}
fn to_value(&self) -> SchemaValue {
SchemaValue::Map {
entries: self
.iter()
.map(|(k, v)| (k.to_value(), v.to_value()))
.collect(),
}
}
}
impl<K, V> FromSchema for HashMap<K, V>
where
K: FromSchema + std::hash::Hash + Eq,
V: FromSchema,
{
fn from_value(v: &SchemaValue) -> Result<Self, FromSchemaError> {
match v {
SchemaValue::Map { entries } => entries
.iter()
.map(|(k, v)| Ok((K::from_value(k)?, V::from_value(v)?)))
.collect(),
other => Err(FromSchemaError::shape_mismatch(
"map",
value_kind(other),
"HashMap",
)),
}
}
}
impl<K: IntoSchema, V: IntoSchema> IntoSchema for BTreeMap<K, V> {
fn type_id() -> TypeId {
type_id_with_args("alloc.collections.BTreeMap", &[K::type_id(), V::type_id()])
}
fn register_in(b: &mut SchemaBuilder) -> SchemaType {
SchemaType::map(K::register_in(b), V::register_in(b))
}
fn to_value(&self) -> SchemaValue {
SchemaValue::Map {
entries: self
.iter()
.map(|(k, v)| (k.to_value(), v.to_value()))
.collect(),
}
}
}
impl<K, V> FromSchema for BTreeMap<K, V>
where
K: FromSchema + Ord,
V: FromSchema,
{
fn from_value(v: &SchemaValue) -> Result<Self, FromSchemaError> {
match v {
SchemaValue::Map { entries } => entries
.iter()
.map(|(k, v)| Ok((K::from_value(k)?, V::from_value(v)?)))
.collect(),
other => Err(FromSchemaError::shape_mismatch(
"map",
value_kind(other),
"BTreeMap",
)),
}
}
}
macro_rules! impl_tuple {
( $name:literal => $( $idx:tt : $t:ident ),+ ) => {
impl< $( $t : IntoSchema ),+ > IntoSchema for ( $( $t, )+ ) {
fn type_id() -> TypeId {
type_id_with_args($name, &[ $( <$t as IntoSchema>::type_id() ),+ ])
}
fn register_in(b: &mut SchemaBuilder) -> SchemaType {
SchemaType::tuple(vec![ $( <$t as IntoSchema>::register_in(b) ),+ ])
}
fn to_value(&self) -> SchemaValue {
SchemaValue::Tuple {
elements: vec![ $( IntoSchema::to_value(&self.$idx) ),+ ],
}
}
}
impl< $( $t : FromSchema ),+ > FromSchema for ( $( $t, )+ ) {
fn from_value(v: &SchemaValue) -> Result<Self, FromSchemaError> {
match v {
SchemaValue::Tuple { elements } => {
let expected = [ $( stringify!($t) ),+ ].len();
if elements.len() != expected {
return Err(FromSchemaError::custom(format!(
"tuple length mismatch: expected {expected}, got {}",
elements.len()
)));
}
Ok(( $( <$t as FromSchema>::from_value(&elements[$idx])? , )+ ))
}
other => Err(FromSchemaError::shape_mismatch(
"tuple",
value_kind(other),
"tuple",
)),
}
}
}
};
}
impl_tuple!("core.tuple.Tuple1" => 0: T0);
impl_tuple!("core.tuple.Tuple2" => 0: T0, 1: T1);
impl_tuple!("core.tuple.Tuple3" => 0: T0, 1: T1, 2: T2);
impl_tuple!("core.tuple.Tuple4" => 0: T0, 1: T1, 2: T2, 3: T3);
impl_tuple!("core.tuple.Tuple5" => 0: T0, 1: T1, 2: T2, 3: T3, 4: T4);
impl_tuple!("core.tuple.Tuple6" => 0: T0, 1: T1, 2: T2, 3: T3, 4: T4, 5: T5);
impl_tuple!("core.tuple.Tuple7" => 0: T0, 1: T1, 2: T2, 3: T3, 4: T4, 5: T5, 6: T6);
impl_tuple!("core.tuple.Tuple8" => 0: T0, 1: T1, 2: T2, 3: T3, 4: T4, 5: T5, 6: T6, 7: T7);
impl IntoSchema for () {
fn type_id() -> TypeId {
TypeId::new("core.tuple.Unit")
}
fn register_in(_b: &mut SchemaBuilder) -> SchemaType {
SchemaType::tuple(vec![])
}
fn to_value(&self) -> SchemaValue {
SchemaValue::Tuple { elements: vec![] }
}
}
impl FromSchema for () {
fn from_value(v: &SchemaValue) -> Result<Self, FromSchemaError> {
match v {
SchemaValue::Tuple { elements } if elements.is_empty() => Ok(()),
other => Err(FromSchemaError::shape_mismatch(
"unit tuple",
value_kind(other),
"()",
)),
}
}
}
fn is_unit_type<T: IntoSchema>() -> bool {
T::type_id() == <() as IntoSchema>::type_id()
}
impl<T: IntoSchema, E: IntoSchema> IntoSchema for Result<T, E> {
fn type_id() -> TypeId {
type_id_with_args("core.result.Result", &[T::type_id(), E::type_id()])
}
fn register_in(b: &mut SchemaBuilder) -> SchemaType {
let ok = if is_unit_type::<T>() {
None
} else {
Some(Box::new(T::register_in(b)))
};
let err = if is_unit_type::<E>() {
None
} else {
Some(Box::new(E::register_in(b)))
};
SchemaType::result(crate::schema::schema_type::ResultSpec { ok, err })
}
fn to_value(&self) -> SchemaValue {
use crate::schema::schema_value::ResultValuePayload;
match self {
Ok(x) => SchemaValue::Result(ResultValuePayload::Ok {
value: if is_unit_type::<T>() {
None
} else {
Some(Box::new(x.to_value()))
},
}),
Err(e) => SchemaValue::Result(ResultValuePayload::Err {
value: if is_unit_type::<E>() {
None
} else {
Some(Box::new(e.to_value()))
},
}),
}
}
}
impl<T: FromSchema, E: FromSchema> FromSchema for Result<T, E> {
fn from_value(v: &SchemaValue) -> Result<Self, FromSchemaError> {
use crate::schema::schema_value::ResultValuePayload;
let unit = SchemaValue::Tuple { elements: vec![] };
match v {
SchemaValue::Result(ResultValuePayload::Ok { value }) => {
let inner = value.as_deref().unwrap_or(&unit);
Ok(Ok(T::from_value(inner)?))
}
SchemaValue::Result(ResultValuePayload::Err { value }) => {
let inner = value.as_deref().unwrap_or(&unit);
Ok(Err(E::from_value(inner)?))
}
other => Err(FromSchemaError::shape_mismatch(
"result",
value_kind(other),
"Result",
)),
}
}
}
impl IntoSchema for ::chrono::DateTime<::chrono::Utc> {
fn type_id() -> TypeId {
TypeId::new("chrono.DateTime<chrono.Utc>")
}
fn register_in(_b: &mut SchemaBuilder) -> SchemaType {
SchemaType::datetime()
}
fn to_value(&self) -> SchemaValue {
SchemaValue::Datetime { value: *self }
}
}
impl FromSchema for ::chrono::DateTime<::chrono::Utc> {
fn from_value(v: &SchemaValue) -> Result<Self, FromSchemaError> {
match v {
SchemaValue::Datetime { value } => Ok(*value),
other => Err(FromSchemaError::shape_mismatch(
"datetime",
value_kind(other),
"DateTime<Utc>",
)),
}
}
}
impl IntoSchema for std::time::Duration {
fn type_id() -> TypeId {
TypeId::new("core.time.Duration")
}
fn register_in(_b: &mut SchemaBuilder) -> SchemaType {
SchemaType::duration()
}
fn to_value(&self) -> SchemaValue {
let nanos = self.as_nanos();
let clamped: i64 = if nanos > i64::MAX as u128 {
i64::MAX
} else {
nanos as i64
};
SchemaValue::Duration(DurationValuePayload {
nanoseconds: clamped,
})
}
}
impl FromSchema for std::time::Duration {
fn from_value(v: &SchemaValue) -> Result<Self, FromSchemaError> {
match v {
SchemaValue::Duration(p) => {
if p.nanoseconds < 0 {
Err(FromSchemaError::custom(
"std::time::Duration cannot decode a negative duration",
))
} else {
Ok(std::time::Duration::from_nanos(p.nanoseconds as u64))
}
}
other => Err(FromSchemaError::shape_mismatch(
"duration",
value_kind(other),
"Duration",
)),
}
}
}
impl IntoSchema for std::path::PathBuf {
fn type_id() -> TypeId {
TypeId::new("std.path.PathBuf")
}
fn register_in(_b: &mut SchemaBuilder) -> SchemaType {
use crate::schema::schema_type::{PathDirection, PathKind, PathSpec};
SchemaType::path(PathSpec {
direction: PathDirection::InOut,
kind: PathKind::Any,
allowed_mime_types: None,
allowed_extensions: None,
})
}
fn to_value(&self) -> SchemaValue {
match self.to_str() {
Some(s) => path_to_value(s.to_string()),
None => path_to_value(self.to_string_lossy().into_owned()),
}
}
}
impl FromSchema for std::path::PathBuf {
fn from_value(v: &SchemaValue) -> Result<Self, FromSchemaError> {
Ok(std::path::PathBuf::from(path_from_value(v, "PathBuf")?))
}
}
impl IntoSchema for uuid::Uuid {
fn type_id() -> TypeId {
TypeId::new("uuid.Uuid")
}
fn register_in(_builder: &mut SchemaBuilder) -> SchemaType {
SchemaType::uuid()
}
fn to_value(&self) -> SchemaValue {
SchemaValue::Uuid(*self)
}
}
impl FromSchema for uuid::Uuid {
fn from_value(v: &SchemaValue) -> Result<Self, FromSchemaError> {
match v {
SchemaValue::Uuid(value) => Ok(*value),
other => Err(FromSchemaError::shape_mismatch(
"uuid",
value_kind(other),
"Uuid",
)),
}
}
}
impl IntoSchema for usize {
fn type_id() -> TypeId {
TypeId::new("usize")
}
fn register_in(_b: &mut SchemaBuilder) -> SchemaType {
SchemaType::u64()
}
fn to_value(&self) -> SchemaValue {
SchemaValue::U64(*self as u64)
}
}
impl FromSchema for usize {
fn from_value(v: &SchemaValue) -> Result<Self, FromSchemaError> {
match v {
SchemaValue::U64(x) => Ok(*x as usize),
other => Err(FromSchemaError::shape_mismatch(
"u64",
value_kind(other),
"usize",
)),
}
}
}
impl<T: IntoSchema> IntoSchema for std::ops::Bound<T> {
fn type_id() -> TypeId {
type_id_with_args("core.ops.range.Bound", &[T::type_id()])
}
fn register_in(b: &mut SchemaBuilder) -> SchemaType {
let inner = T::register_in(b);
SchemaType::variant(vec![
VariantCaseType {
name: "included".to_string(),
payload: Some(inner.clone()),
metadata: MetadataEnvelope::default(),
},
VariantCaseType {
name: "excluded".to_string(),
payload: Some(inner),
metadata: MetadataEnvelope::default(),
},
VariantCaseType {
name: "unbounded".to_string(),
payload: None,
metadata: MetadataEnvelope::default(),
},
])
}
fn to_value(&self) -> SchemaValue {
match self {
std::ops::Bound::Included(t) => SchemaValue::Variant(VariantValuePayload {
case: 0,
payload: Some(Box::new(t.to_value())),
}),
std::ops::Bound::Excluded(t) => SchemaValue::Variant(VariantValuePayload {
case: 1,
payload: Some(Box::new(t.to_value())),
}),
std::ops::Bound::Unbounded => SchemaValue::Variant(VariantValuePayload {
case: 2,
payload: None,
}),
}
}
}
impl<T: FromSchema> FromSchema for std::ops::Bound<T> {
fn from_value(v: &SchemaValue) -> Result<Self, FromSchemaError> {
match v {
SchemaValue::Variant(VariantValuePayload { case, payload }) => match case {
0 => {
let p = payload.as_ref().ok_or_else(|| {
FromSchemaError::custom("Bound::Included requires a payload")
})?;
Ok(std::ops::Bound::Included(T::from_value(p)?))
}
1 => {
let p = payload.as_ref().ok_or_else(|| {
FromSchemaError::custom("Bound::Excluded requires a payload")
})?;
Ok(std::ops::Bound::Excluded(T::from_value(p)?))
}
2 => Ok(std::ops::Bound::Unbounded),
other => Err(FromSchemaError::out_of_range(*other, 3, "Bound")),
},
other => Err(FromSchemaError::shape_mismatch(
"variant",
value_kind(other),
"Bound",
)),
}
}
}
#[cfg(test)]
mod result_unit_tests {
use super::*;
use crate::schema::schema_value::ResultValuePayload;
use test_r::test;
#[test]
fn ok_unit_encodes_as_none() {
let v: Result<(), String> = Ok(());
assert_eq!(
v.to_value(),
SchemaValue::Result(ResultValuePayload::Ok { value: None })
);
}
#[test]
fn err_unit_encodes_as_none() {
let v: Result<u32, ()> = Err(());
assert_eq!(
v.to_value(),
SchemaValue::Result(ResultValuePayload::Err { value: None })
);
}
#[test]
fn non_unit_branches_still_carry_payload() {
let ok: Result<u32, String> = Ok(7);
assert_eq!(
ok.to_value(),
SchemaValue::Result(ResultValuePayload::Ok {
value: Some(Box::new(7u32.to_value()))
})
);
let err: Result<u32, String> = Err("boom".to_string());
assert_eq!(
err.to_value(),
SchemaValue::Result(ResultValuePayload::Err {
value: Some(Box::new("boom".to_string().to_value()))
})
);
}
#[test]
fn unit_branch_schema_is_none() {
let mut b = SchemaBuilder::new();
let ty = <Result<(), String> as IntoSchema>::register_in(&mut b);
match ty {
SchemaType::Result { spec, .. } => {
assert!(spec.ok.is_none(), "ok branch should be None for unit");
assert!(spec.err.is_some(), "err branch should carry string");
}
other => panic!("expected result schema, got {other:?}"),
}
}
#[test]
fn ok_unit_roundtrips() {
let v: Result<(), String> = Ok(());
let decoded = <Result<(), String> as FromSchema>::from_value(&v.to_value()).unwrap();
assert_eq!(decoded, Ok(()));
}
#[test]
fn err_unit_roundtrips() {
let v: Result<u32, ()> = Err(());
let decoded = <Result<u32, ()> as FromSchema>::from_value(&v.to_value()).unwrap();
assert_eq!(decoded, Err(()));
}
}
#[cfg(test)]
mod merge_tests {
use super::*;
use test_r::test;
fn def(id: &str, body: SchemaType) -> SchemaTypeDef {
SchemaTypeDef {
id: TypeId::new(id),
name: None,
body,
}
}
#[test]
fn empty_input_produces_empty_merged_graph() {
let merged = merge_agent_graphs(std::iter::empty()).unwrap();
assert!(merged.defs.is_empty());
assert_eq!(merged.root, SchemaGraph::empty().root);
}
#[test]
fn defs_are_deduplicated_by_type_id_when_bodies_match() {
let g1 = SchemaGraph {
defs: vec![def("a.Foo", SchemaType::s32())],
root: SchemaType::ref_to(TypeId::new("a.Foo")),
};
let g2 = SchemaGraph {
defs: vec![def("a.Foo", SchemaType::s32())],
root: SchemaType::ref_to(TypeId::new("a.Foo")),
};
let merged = merge_agent_graphs([g1, g2]).unwrap();
assert_eq!(merged.defs.len(), 1);
assert_eq!(merged.defs[0].id, TypeId::new("a.Foo"));
}
#[test]
fn defs_with_same_id_but_divergent_bodies_are_rejected() {
let g1 = SchemaGraph {
defs: vec![def("a.Foo", SchemaType::s32())],
root: SchemaType::ref_to(TypeId::new("a.Foo")),
};
let g2 = SchemaGraph {
defs: vec![def("a.Foo", SchemaType::string())],
root: SchemaType::ref_to(TypeId::new("a.Foo")),
};
let err = merge_agent_graphs([g1, g2]).unwrap_err();
assert!(matches!(
err,
MergeError::ConflictingDefinitions { ref id, .. } if id == &TypeId::new("a.Foo")
));
}
#[test]
fn first_seen_insertion_order_is_preserved() {
let g1 = SchemaGraph {
defs: vec![
def("a.Alpha", SchemaType::bool()),
def("a.Beta", SchemaType::s64()),
],
root: SchemaType::ref_to(TypeId::new("a.Alpha")),
};
let g2 = SchemaGraph {
defs: vec![
def("a.Gamma", SchemaType::u32()),
def("a.Alpha", SchemaType::bool()), ],
root: SchemaType::ref_to(TypeId::new("a.Gamma")),
};
let merged = merge_agent_graphs([g1, g2]).unwrap();
let ids: Vec<_> = merged
.defs
.iter()
.map(|d| d.id.as_str().to_string())
.collect();
assert_eq!(ids, vec!["a.Alpha", "a.Beta", "a.Gamma"]);
}
#[test]
fn duplicate_id_with_same_body_keeps_first_seen_name() {
let g1 = SchemaGraph {
defs: vec![SchemaTypeDef {
id: TypeId::new("a.Foo"),
name: Some("FooFirst".into()),
body: SchemaType::s32(),
}],
root: SchemaType::ref_to(TypeId::new("a.Foo")),
};
let g2 = SchemaGraph {
defs: vec![SchemaTypeDef {
id: TypeId::new("a.Foo"),
name: Some("FooSecond".into()),
body: SchemaType::s32(),
}],
root: SchemaType::ref_to(TypeId::new("a.Foo")),
};
let merged = merge_agent_graphs([g1, g2]).unwrap();
assert_eq!(merged.defs.len(), 1);
assert_eq!(merged.defs[0].name.as_deref(), Some("FooFirst"));
}
#[test]
fn merged_graph_uses_sentinel_root() {
let g = SchemaGraph::anonymous(SchemaType::s32());
let merged = merge_agent_graphs([g]).unwrap();
assert_eq!(merged.root, SchemaGraph::empty().root);
}
}