use crate::schema::metadata::{MetadataEnvelope, TypeId};
use golem_schema_derive::{FromSchema, IntoSchema};
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, IntoSchema, FromSchema)]
#[cfg_attr(feature = "full", derive(desert_rust::BinaryCodec))]
#[cfg_attr(feature = "full", desert(evolution()))]
#[serde(tag = "kind", content = "value", rename_all = "kebab-case")]
#[schema(named = "schema-type")]
#[cfg_attr(feature = "full", derive(golem_schema_derive::PoemSchema))]
pub enum SchemaType {
Ref {
id: TypeId,
#[serde(default, skip_serializing_if = "MetadataEnvelope::is_empty")]
metadata: MetadataEnvelope,
},
Bool {
#[serde(default, skip_serializing_if = "MetadataEnvelope::is_empty")]
metadata: MetadataEnvelope,
},
S8 {
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_normalized_numeric_restrictions"
)]
restrictions: Option<NumericRestrictions>,
#[serde(default, skip_serializing_if = "MetadataEnvelope::is_empty")]
metadata: MetadataEnvelope,
},
S16 {
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_normalized_numeric_restrictions"
)]
restrictions: Option<NumericRestrictions>,
#[serde(default, skip_serializing_if = "MetadataEnvelope::is_empty")]
metadata: MetadataEnvelope,
},
S32 {
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_normalized_numeric_restrictions"
)]
restrictions: Option<NumericRestrictions>,
#[serde(default, skip_serializing_if = "MetadataEnvelope::is_empty")]
metadata: MetadataEnvelope,
},
S64 {
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_normalized_numeric_restrictions"
)]
restrictions: Option<NumericRestrictions>,
#[serde(default, skip_serializing_if = "MetadataEnvelope::is_empty")]
metadata: MetadataEnvelope,
},
U8 {
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_normalized_numeric_restrictions"
)]
restrictions: Option<NumericRestrictions>,
#[serde(default, skip_serializing_if = "MetadataEnvelope::is_empty")]
metadata: MetadataEnvelope,
},
U16 {
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_normalized_numeric_restrictions"
)]
restrictions: Option<NumericRestrictions>,
#[serde(default, skip_serializing_if = "MetadataEnvelope::is_empty")]
metadata: MetadataEnvelope,
},
U32 {
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_normalized_numeric_restrictions"
)]
restrictions: Option<NumericRestrictions>,
#[serde(default, skip_serializing_if = "MetadataEnvelope::is_empty")]
metadata: MetadataEnvelope,
},
U64 {
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_normalized_numeric_restrictions"
)]
restrictions: Option<NumericRestrictions>,
#[serde(default, skip_serializing_if = "MetadataEnvelope::is_empty")]
metadata: MetadataEnvelope,
},
F32 {
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_normalized_numeric_restrictions"
)]
restrictions: Option<NumericRestrictions>,
#[serde(default, skip_serializing_if = "MetadataEnvelope::is_empty")]
metadata: MetadataEnvelope,
},
F64 {
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_normalized_numeric_restrictions"
)]
restrictions: Option<NumericRestrictions>,
#[serde(default, skip_serializing_if = "MetadataEnvelope::is_empty")]
metadata: MetadataEnvelope,
},
Char {
#[serde(default, skip_serializing_if = "MetadataEnvelope::is_empty")]
metadata: MetadataEnvelope,
},
String {
#[serde(default, skip_serializing_if = "MetadataEnvelope::is_empty")]
metadata: MetadataEnvelope,
},
Record {
fields: Vec<NamedFieldType>,
#[serde(default, skip_serializing_if = "MetadataEnvelope::is_empty")]
metadata: MetadataEnvelope,
},
Variant {
cases: Vec<VariantCaseType>,
#[serde(default, skip_serializing_if = "MetadataEnvelope::is_empty")]
metadata: MetadataEnvelope,
},
Enum {
cases: Vec<String>,
#[serde(default, skip_serializing_if = "MetadataEnvelope::is_empty")]
metadata: MetadataEnvelope,
},
Flags {
flags: Vec<String>,
#[serde(default, skip_serializing_if = "MetadataEnvelope::is_empty")]
metadata: MetadataEnvelope,
},
Tuple {
elements: Vec<SchemaType>,
#[serde(default, skip_serializing_if = "MetadataEnvelope::is_empty")]
metadata: MetadataEnvelope,
},
List {
element: Box<SchemaType>,
#[serde(default, skip_serializing_if = "MetadataEnvelope::is_empty")]
metadata: MetadataEnvelope,
},
FixedList {
element: Box<SchemaType>,
length: u32,
#[serde(default, skip_serializing_if = "MetadataEnvelope::is_empty")]
metadata: MetadataEnvelope,
},
Map {
key: Box<SchemaType>,
value: Box<SchemaType>,
#[serde(default, skip_serializing_if = "MetadataEnvelope::is_empty")]
metadata: MetadataEnvelope,
},
Option {
inner: Box<SchemaType>,
#[serde(default, skip_serializing_if = "MetadataEnvelope::is_empty")]
metadata: MetadataEnvelope,
},
Result {
spec: ResultSpec,
#[serde(default, skip_serializing_if = "MetadataEnvelope::is_empty")]
metadata: MetadataEnvelope,
},
Text {
restrictions: TextRestrictions,
#[serde(default, skip_serializing_if = "MetadataEnvelope::is_empty")]
metadata: MetadataEnvelope,
},
Binary {
restrictions: BinaryRestrictions,
#[serde(default, skip_serializing_if = "MetadataEnvelope::is_empty")]
metadata: MetadataEnvelope,
},
Path {
spec: PathSpec,
#[serde(default, skip_serializing_if = "MetadataEnvelope::is_empty")]
metadata: MetadataEnvelope,
},
Url {
restrictions: UrlRestrictions,
#[serde(default, skip_serializing_if = "MetadataEnvelope::is_empty")]
metadata: MetadataEnvelope,
},
Uuid {
#[serde(default, skip_serializing_if = "MetadataEnvelope::is_empty")]
metadata: MetadataEnvelope,
},
Datetime {
#[serde(default, skip_serializing_if = "MetadataEnvelope::is_empty")]
metadata: MetadataEnvelope,
},
Duration {
#[serde(default, skip_serializing_if = "MetadataEnvelope::is_empty")]
metadata: MetadataEnvelope,
},
Quantity {
spec: QuantitySpec,
#[serde(default, skip_serializing_if = "MetadataEnvelope::is_empty")]
metadata: MetadataEnvelope,
},
Union {
spec: UnionSpec,
#[serde(default, skip_serializing_if = "MetadataEnvelope::is_empty")]
metadata: MetadataEnvelope,
},
Secret {
spec: SecretSpec,
#[serde(default, skip_serializing_if = "MetadataEnvelope::is_empty")]
metadata: MetadataEnvelope,
},
QuotaToken {
spec: QuotaTokenSpec,
#[serde(default, skip_serializing_if = "MetadataEnvelope::is_empty")]
metadata: MetadataEnvelope,
},
PermissionCard {
spec: PermissionCardSpec,
#[serde(default, skip_serializing_if = "MetadataEnvelope::is_empty")]
metadata: MetadataEnvelope,
},
Future {
inner: Option<Box<SchemaType>>,
#[serde(default, skip_serializing_if = "MetadataEnvelope::is_empty")]
metadata: MetadataEnvelope,
},
Stream {
inner: Option<Box<SchemaType>>,
#[serde(default, skip_serializing_if = "MetadataEnvelope::is_empty")]
metadata: MetadataEnvelope,
},
}
impl SchemaType {
pub fn metadata(&self) -> &MetadataEnvelope {
match self {
SchemaType::Ref { metadata, .. }
| SchemaType::Bool { metadata }
| SchemaType::S8 { metadata, .. }
| SchemaType::S16 { metadata, .. }
| SchemaType::S32 { metadata, .. }
| SchemaType::S64 { metadata, .. }
| SchemaType::U8 { metadata, .. }
| SchemaType::U16 { metadata, .. }
| SchemaType::U32 { metadata, .. }
| SchemaType::U64 { metadata, .. }
| SchemaType::F32 { metadata, .. }
| SchemaType::F64 { metadata, .. }
| SchemaType::Char { metadata }
| SchemaType::String { metadata }
| SchemaType::Record { metadata, .. }
| SchemaType::Variant { metadata, .. }
| SchemaType::Enum { metadata, .. }
| SchemaType::Flags { metadata, .. }
| SchemaType::Tuple { metadata, .. }
| SchemaType::List { metadata, .. }
| SchemaType::FixedList { metadata, .. }
| SchemaType::Map { metadata, .. }
| SchemaType::Option { metadata, .. }
| SchemaType::Result { metadata, .. }
| SchemaType::Text { metadata, .. }
| SchemaType::Binary { metadata, .. }
| SchemaType::Path { metadata, .. }
| SchemaType::Url { metadata, .. }
| SchemaType::Uuid { metadata }
| SchemaType::Datetime { metadata }
| SchemaType::Duration { metadata }
| SchemaType::Quantity { metadata, .. }
| SchemaType::Union { metadata, .. }
| SchemaType::Secret { metadata, .. }
| SchemaType::QuotaToken { metadata, .. }
| SchemaType::PermissionCard { metadata, .. }
| SchemaType::Future { metadata, .. }
| SchemaType::Stream { metadata, .. } => metadata,
}
}
pub fn metadata_mut(&mut self) -> &mut MetadataEnvelope {
match self {
SchemaType::Ref { metadata, .. }
| SchemaType::Bool { metadata }
| SchemaType::S8 { metadata, .. }
| SchemaType::S16 { metadata, .. }
| SchemaType::S32 { metadata, .. }
| SchemaType::S64 { metadata, .. }
| SchemaType::U8 { metadata, .. }
| SchemaType::U16 { metadata, .. }
| SchemaType::U32 { metadata, .. }
| SchemaType::U64 { metadata, .. }
| SchemaType::F32 { metadata, .. }
| SchemaType::F64 { metadata, .. }
| SchemaType::Char { metadata }
| SchemaType::String { metadata }
| SchemaType::Record { metadata, .. }
| SchemaType::Variant { metadata, .. }
| SchemaType::Enum { metadata, .. }
| SchemaType::Flags { metadata, .. }
| SchemaType::Tuple { metadata, .. }
| SchemaType::List { metadata, .. }
| SchemaType::FixedList { metadata, .. }
| SchemaType::Map { metadata, .. }
| SchemaType::Option { metadata, .. }
| SchemaType::Result { metadata, .. }
| SchemaType::Text { metadata, .. }
| SchemaType::Binary { metadata, .. }
| SchemaType::Path { metadata, .. }
| SchemaType::Url { metadata, .. }
| SchemaType::Uuid { metadata }
| SchemaType::Datetime { metadata }
| SchemaType::Duration { metadata }
| SchemaType::Quantity { metadata, .. }
| SchemaType::Union { metadata, .. }
| SchemaType::Secret { metadata, .. }
| SchemaType::QuotaToken { metadata, .. }
| SchemaType::PermissionCard { metadata, .. }
| SchemaType::Future { metadata, .. }
| SchemaType::Stream { metadata, .. } => metadata,
}
}
pub fn with_metadata(mut self, metadata: MetadataEnvelope) -> Self {
*self.metadata_mut() = metadata;
self
}
pub fn numeric_repr(&self) -> Option<NumericRepr> {
match self {
SchemaType::S8 { .. } => Some(NumericRepr::S8),
SchemaType::S16 { .. } => Some(NumericRepr::S16),
SchemaType::S32 { .. } => Some(NumericRepr::S32),
SchemaType::S64 { .. } => Some(NumericRepr::S64),
SchemaType::U8 { .. } => Some(NumericRepr::U8),
SchemaType::U16 { .. } => Some(NumericRepr::U16),
SchemaType::U32 { .. } => Some(NumericRepr::U32),
SchemaType::U64 { .. } => Some(NumericRepr::U64),
SchemaType::F32 { .. } => Some(NumericRepr::F32),
SchemaType::F64 { .. } => Some(NumericRepr::F64),
_ => None,
}
}
pub fn numeric_restrictions(&self) -> Option<&NumericRestrictions> {
match self {
SchemaType::S8 { restrictions, .. }
| SchemaType::S16 { restrictions, .. }
| SchemaType::S32 { restrictions, .. }
| SchemaType::S64 { restrictions, .. }
| SchemaType::U8 { restrictions, .. }
| SchemaType::U16 { restrictions, .. }
| SchemaType::U32 { restrictions, .. }
| SchemaType::U64 { restrictions, .. }
| SchemaType::F32 { restrictions, .. }
| SchemaType::F64 { restrictions, .. } => restrictions.as_ref(),
_ => None,
}
}
pub fn ref_to(id: TypeId) -> Self {
Self::Ref {
id,
metadata: MetadataEnvelope::default(),
}
}
pub fn bool() -> Self {
Self::Bool {
metadata: MetadataEnvelope::default(),
}
}
pub fn s8() -> Self {
Self::S8 {
restrictions: None,
metadata: MetadataEnvelope::default(),
}
}
pub fn s16() -> Self {
Self::S16 {
restrictions: None,
metadata: MetadataEnvelope::default(),
}
}
pub fn s32() -> Self {
Self::S32 {
restrictions: None,
metadata: MetadataEnvelope::default(),
}
}
pub fn s64() -> Self {
Self::S64 {
restrictions: None,
metadata: MetadataEnvelope::default(),
}
}
pub fn u8() -> Self {
Self::U8 {
restrictions: None,
metadata: MetadataEnvelope::default(),
}
}
pub fn u16() -> Self {
Self::U16 {
restrictions: None,
metadata: MetadataEnvelope::default(),
}
}
pub fn u32() -> Self {
Self::U32 {
restrictions: None,
metadata: MetadataEnvelope::default(),
}
}
pub fn u64() -> Self {
Self::U64 {
restrictions: None,
metadata: MetadataEnvelope::default(),
}
}
pub fn f32() -> Self {
Self::F32 {
restrictions: None,
metadata: MetadataEnvelope::default(),
}
}
pub fn f64() -> Self {
Self::F64 {
restrictions: None,
metadata: MetadataEnvelope::default(),
}
}
pub fn char() -> Self {
Self::Char {
metadata: MetadataEnvelope::default(),
}
}
pub fn string() -> Self {
Self::String {
metadata: MetadataEnvelope::default(),
}
}
pub fn uuid() -> Self {
Self::Uuid {
metadata: MetadataEnvelope::default(),
}
}
pub fn datetime() -> Self {
Self::Datetime {
metadata: MetadataEnvelope::default(),
}
}
pub fn duration() -> Self {
Self::Duration {
metadata: MetadataEnvelope::default(),
}
}
pub fn record(fields: Vec<NamedFieldType>) -> Self {
Self::Record {
fields,
metadata: MetadataEnvelope::default(),
}
}
pub fn record_from_fields<N>(fields: impl IntoIterator<Item = (N, SchemaType)>) -> Self
where
N: Into<String>,
{
Self::record(
fields
.into_iter()
.map(|(name, body)| NamedFieldType {
name: name.into(),
body,
metadata: MetadataEnvelope::default(),
})
.collect(),
)
}
pub fn variant(cases: Vec<VariantCaseType>) -> Self {
Self::Variant {
cases,
metadata: MetadataEnvelope::default(),
}
}
pub fn r#enum(cases: Vec<String>) -> Self {
Self::Enum {
cases,
metadata: MetadataEnvelope::default(),
}
}
pub fn flags(flags: Vec<String>) -> Self {
Self::Flags {
flags,
metadata: MetadataEnvelope::default(),
}
}
pub fn tuple(elements: Vec<SchemaType>) -> Self {
Self::Tuple {
elements,
metadata: MetadataEnvelope::default(),
}
}
pub fn list(element: SchemaType) -> Self {
Self::List {
element: Box::new(element),
metadata: MetadataEnvelope::default(),
}
}
pub fn fixed_list(element: SchemaType, length: u32) -> Self {
Self::FixedList {
element: Box::new(element),
length,
metadata: MetadataEnvelope::default(),
}
}
pub fn map(key: SchemaType, value: SchemaType) -> Self {
Self::Map {
key: Box::new(key),
value: Box::new(value),
metadata: MetadataEnvelope::default(),
}
}
pub fn option(inner: SchemaType) -> Self {
Self::Option {
inner: Box::new(inner),
metadata: MetadataEnvelope::default(),
}
}
pub fn result(spec: ResultSpec) -> Self {
Self::Result {
spec,
metadata: MetadataEnvelope::default(),
}
}
pub fn text(restrictions: TextRestrictions) -> Self {
Self::Text {
restrictions,
metadata: MetadataEnvelope::default(),
}
}
pub fn binary(restrictions: BinaryRestrictions) -> Self {
Self::Binary {
restrictions,
metadata: MetadataEnvelope::default(),
}
}
pub fn path(spec: PathSpec) -> Self {
Self::Path {
spec,
metadata: MetadataEnvelope::default(),
}
}
pub fn url(restrictions: UrlRestrictions) -> Self {
Self::Url {
restrictions,
metadata: MetadataEnvelope::default(),
}
}
pub fn quantity(spec: QuantitySpec) -> Self {
Self::Quantity {
spec,
metadata: MetadataEnvelope::default(),
}
}
pub fn union(spec: UnionSpec) -> Self {
Self::Union {
spec,
metadata: MetadataEnvelope::default(),
}
}
pub fn secret(spec: SecretSpec) -> Self {
Self::Secret {
spec,
metadata: MetadataEnvelope::default(),
}
}
pub fn quota_token(spec: QuotaTokenSpec) -> Self {
Self::QuotaToken {
spec,
metadata: MetadataEnvelope::default(),
}
}
pub fn permission_card(spec: PermissionCardSpec) -> Self {
Self::PermissionCard {
spec,
metadata: MetadataEnvelope::default(),
}
}
pub fn future(inner: Option<SchemaType>) -> Self {
Self::Future {
inner: inner.map(Box::new),
metadata: MetadataEnvelope::default(),
}
}
pub fn stream(inner: Option<SchemaType>) -> Self {
Self::Stream {
inner: inner.map(Box::new),
metadata: MetadataEnvelope::default(),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, IntoSchema, FromSchema)]
#[cfg_attr(feature = "full", derive(desert_rust::BinaryCodec))]
#[cfg_attr(feature = "full", desert(evolution()))]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "full", derive(golem_schema_derive::PoemSchema))]
pub struct NamedFieldType {
pub name: String,
pub body: SchemaType,
#[serde(default, skip_serializing_if = "MetadataEnvelope::is_empty")]
pub metadata: MetadataEnvelope,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, IntoSchema, FromSchema)]
#[cfg_attr(feature = "full", derive(desert_rust::BinaryCodec))]
#[cfg_attr(feature = "full", desert(evolution()))]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "full", derive(golem_schema_derive::PoemSchema))]
pub struct VariantCaseType {
pub name: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub payload: Option<SchemaType>,
#[serde(default, skip_serializing_if = "MetadataEnvelope::is_empty")]
pub metadata: MetadataEnvelope,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, IntoSchema, FromSchema)]
#[cfg_attr(feature = "full", derive(desert_rust::BinaryCodec))]
#[cfg_attr(feature = "full", desert(evolution()))]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "full", derive(golem_schema_derive::PoemSchema))]
pub struct ResultSpec {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub ok: Option<Box<SchemaType>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub err: Option<Box<SchemaType>>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, IntoSchema, FromSchema)]
#[cfg_attr(feature = "full", derive(desert_rust::BinaryCodec))]
#[cfg_attr(feature = "full", desert(evolution()))]
#[serde(tag = "kind", content = "value", rename_all = "kebab-case")]
#[cfg_attr(feature = "full", derive(golem_schema_derive::PoemSchema))]
pub enum NumericBound {
Signed(i64),
Unsigned(u64),
FloatBits(u64),
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum NumericBoundError {
NonFinite,
}
impl std::fmt::Display for NumericBoundError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
NumericBoundError::NonFinite => write!(f, "numeric float bound must be finite"),
}
}
}
impl std::error::Error for NumericBoundError {}
impl NumericBound {
pub fn float(value: f64) -> Result<Self, NumericBoundError> {
if !value.is_finite() {
return Err(NumericBoundError::NonFinite);
}
let normalized = value + 0.0;
Ok(NumericBound::FloatBits(normalized.to_bits()))
}
pub fn as_f64(&self) -> Option<f64> {
match self {
NumericBound::FloatBits(bits) => Some(f64::from_bits(*bits)),
_ => None,
}
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, IntoSchema, FromSchema)]
#[cfg_attr(feature = "full", derive(desert_rust::BinaryCodec))]
#[cfg_attr(feature = "full", desert(evolution()))]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "full", derive(golem_schema_derive::PoemSchema))]
pub struct NumericRestrictions {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub min: Option<NumericBound>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub max: Option<NumericBound>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub unit: Option<String>,
}
impl NumericRestrictions {
pub fn is_empty(&self) -> bool {
self.min.is_none()
&& self.max.is_none()
&& self.unit.as_ref().map(|u| u.is_empty()).unwrap_or(true)
}
pub fn normalize(mut self) -> Option<Self> {
self.min = self.min.map(canonicalize_bound);
self.max = self.max.map(canonicalize_bound);
if self.unit.as_ref().map(|u| u.is_empty()).unwrap_or(false) {
self.unit = None;
}
if self.is_empty() { None } else { Some(self) }
}
pub fn validate_for_repr(&self, repr: NumericRepr) -> Result<(), NumericRestrictionError> {
if self.is_empty() {
return Err(NumericRestrictionError::EmptyStored);
}
if let Some(min) = self.min {
check_bound_repr(min, repr)?;
}
if let Some(max) = self.max {
check_bound_repr(max, repr)?;
}
if let (Some(min), Some(max)) = (self.min, self.max) {
match numeric_bound_cmp(min, max) {
Some(std::cmp::Ordering::Greater) => {
return Err(NumericRestrictionError::MinGreaterThanMax);
}
Some(_) => {}
None => return Err(NumericRestrictionError::FamilyMismatch),
}
}
Ok(())
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum NumericRepr {
S8,
S16,
S32,
S64,
U8,
U16,
U32,
U64,
F32,
F64,
}
impl NumericRepr {
pub fn is_integer(self) -> bool {
!matches!(self, NumericRepr::F32 | NumericRepr::F64)
}
fn family(self) -> NumericFamily {
match self {
NumericRepr::S8 | NumericRepr::S16 | NumericRepr::S32 | NumericRepr::S64 => {
NumericFamily::Signed
}
NumericRepr::U8 | NumericRepr::U16 | NumericRepr::U32 | NumericRepr::U64 => {
NumericFamily::Unsigned
}
NumericRepr::F32 | NumericRepr::F64 => NumericFamily::Float,
}
}
fn signed_range(self) -> Option<(i64, i64)> {
match self {
NumericRepr::S8 => Some((i8::MIN as i64, i8::MAX as i64)),
NumericRepr::S16 => Some((i16::MIN as i64, i16::MAX as i64)),
NumericRepr::S32 => Some((i32::MIN as i64, i32::MAX as i64)),
NumericRepr::S64 => Some((i64::MIN, i64::MAX)),
_ => None,
}
}
fn unsigned_max(self) -> Option<u64> {
match self {
NumericRepr::U8 => Some(u8::MAX as u64),
NumericRepr::U16 => Some(u16::MAX as u64),
NumericRepr::U32 => Some(u32::MAX as u64),
NumericRepr::U64 => Some(u64::MAX),
_ => None,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum NumericFamily {
Signed,
Unsigned,
Float,
}
impl NumericBound {
fn family(self) -> NumericFamily {
match self {
NumericBound::Signed(_) => NumericFamily::Signed,
NumericBound::Unsigned(_) => NumericFamily::Unsigned,
NumericBound::FloatBits(_) => NumericFamily::Float,
}
}
}
fn canonicalize_bound(b: NumericBound) -> NumericBound {
match b {
NumericBound::FloatBits(bits) => {
if f64::from_bits(bits) == 0.0 {
NumericBound::FloatBits(0)
} else {
b
}
}
other => other,
}
}
fn deserialize_normalized_numeric_restrictions<'de, D>(
deserializer: D,
) -> Result<Option<NumericRestrictions>, D::Error>
where
D: serde::Deserializer<'de>,
{
let raw = Option::<NumericRestrictions>::deserialize(deserializer)?;
Ok(raw.and_then(NumericRestrictions::normalize))
}
pub fn numeric_bound_cmp(a: NumericBound, b: NumericBound) -> Option<std::cmp::Ordering> {
match (a, b) {
(NumericBound::Signed(x), NumericBound::Signed(y)) => Some(x.cmp(&y)),
(NumericBound::Unsigned(x), NumericBound::Unsigned(y)) => Some(x.cmp(&y)),
(NumericBound::FloatBits(x), NumericBound::FloatBits(y)) => {
f64::from_bits(x).partial_cmp(&f64::from_bits(y))
}
_ => None,
}
}
fn check_bound_repr(bound: NumericBound, repr: NumericRepr) -> Result<(), NumericRestrictionError> {
if bound.family() != repr.family() {
return Err(NumericRestrictionError::FamilyMismatch);
}
match bound {
NumericBound::Signed(v) => {
let (lo, hi) = repr.signed_range().expect("signed family => signed range");
if v < lo || v > hi {
return Err(NumericRestrictionError::BoundOutOfRange);
}
}
NumericBound::Unsigned(v) => {
let hi = repr
.unsigned_max()
.expect("unsigned family => unsigned max");
if v > hi {
return Err(NumericRestrictionError::BoundOutOfRange);
}
}
NumericBound::FloatBits(bits) => {
let v = f64::from_bits(bits);
if !v.is_finite() {
return Err(NumericRestrictionError::NonFiniteFloat);
}
if repr == NumericRepr::F32 && (v as f32) as f64 != v {
return Err(NumericRestrictionError::FloatNotRoundTrippable);
}
}
}
Ok(())
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum NumericRestrictionError {
EmptyStored,
FamilyMismatch,
BoundOutOfRange,
NonFiniteFloat,
FloatNotRoundTrippable,
MinGreaterThanMax,
}
impl std::fmt::Display for NumericRestrictionError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let msg = match self {
NumericRestrictionError::EmptyStored => {
"numeric restriction set is empty (must be None)"
}
NumericRestrictionError::FamilyMismatch => {
"numeric bound family does not match the numeric type"
}
NumericRestrictionError::BoundOutOfRange => {
"numeric bound does not fit the numeric type's range"
}
NumericRestrictionError::NonFiniteFloat => "numeric float bound must be finite",
NumericRestrictionError::FloatNotRoundTrippable => {
"f32 numeric bound does not round-trip through f32"
}
NumericRestrictionError::MinGreaterThanMax => {
"numeric min bound is greater than max bound"
}
};
write!(f, "{msg}")
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, IntoSchema, FromSchema)]
#[cfg_attr(feature = "full", derive(desert_rust::BinaryCodec))]
#[cfg_attr(feature = "full", desert(evolution()))]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "full", derive(golem_schema_derive::PoemSchema))]
pub struct TextRestrictions {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub languages: Option<Vec<String>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub min_length: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub max_length: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub regex: Option<String>,
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, IntoSchema, FromSchema)]
#[cfg_attr(feature = "full", derive(desert_rust::BinaryCodec))]
#[cfg_attr(feature = "full", desert(evolution()))]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "full", derive(golem_schema_derive::PoemSchema))]
pub struct BinaryRestrictions {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub mime_types: Option<Vec<String>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub min_bytes: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub max_bytes: Option<u32>,
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize, IntoSchema, FromSchema)]
#[cfg_attr(feature = "full", derive(desert_rust::BinaryCodec))]
#[cfg_attr(feature = "full", desert(evolution()))]
#[serde(rename_all = "kebab-case")]
#[cfg_attr(feature = "full", derive(golem_schema_derive::PoemSchema))]
pub enum PathDirection {
Input,
Output,
InOut,
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize, IntoSchema, FromSchema)]
#[cfg_attr(feature = "full", derive(desert_rust::BinaryCodec))]
#[cfg_attr(feature = "full", desert(evolution()))]
#[serde(rename_all = "kebab-case")]
#[cfg_attr(feature = "full", derive(golem_schema_derive::PoemSchema))]
pub enum PathKind {
File,
Directory,
Any,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, IntoSchema, FromSchema)]
#[cfg_attr(feature = "full", derive(desert_rust::BinaryCodec))]
#[cfg_attr(feature = "full", desert(evolution()))]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "full", derive(golem_schema_derive::PoemSchema))]
pub struct PathSpec {
pub direction: PathDirection,
pub kind: PathKind,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub allowed_mime_types: Option<Vec<String>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub allowed_extensions: Option<Vec<String>>,
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, IntoSchema, FromSchema)]
#[cfg_attr(feature = "full", derive(desert_rust::BinaryCodec))]
#[cfg_attr(feature = "full", desert(evolution()))]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "full", derive(golem_schema_derive::PoemSchema))]
pub struct UrlRestrictions {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub allowed_schemes: Option<Vec<String>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub allowed_hosts: Option<Vec<String>>,
}
#[derive(
Clone,
Debug,
PartialEq,
Eq,
Serialize,
Deserialize,
golem_schema_derive::IntoSchema,
golem_schema_derive::FromSchema,
)]
#[cfg_attr(feature = "full", derive(desert_rust::BinaryCodec))]
#[cfg_attr(feature = "full", desert(evolution()))]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "full", derive(golem_schema_derive::PoemSchema))]
pub struct QuantityValue {
pub mantissa: i64,
pub scale: i32,
pub unit: String,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, IntoSchema, FromSchema)]
#[cfg_attr(feature = "full", derive(desert_rust::BinaryCodec))]
#[cfg_attr(feature = "full", desert(evolution()))]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "full", derive(golem_schema_derive::PoemSchema))]
pub struct QuantitySpec {
pub base_unit: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub allowed_suffixes: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub min: Option<QuantityValue>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub max: Option<QuantityValue>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, IntoSchema, FromSchema)]
#[cfg_attr(feature = "full", derive(desert_rust::BinaryCodec))]
#[cfg_attr(feature = "full", desert(evolution()))]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "full", derive(golem_schema_derive::PoemSchema))]
pub struct UnionSpec {
pub branches: Vec<UnionBranch>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, IntoSchema, FromSchema)]
#[cfg_attr(feature = "full", derive(desert_rust::BinaryCodec))]
#[cfg_attr(feature = "full", desert(evolution()))]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "full", derive(golem_schema_derive::PoemSchema))]
pub struct UnionBranch {
pub tag: String,
pub body: SchemaType,
pub discriminator: DiscriminatorRule,
#[serde(default, skip_serializing_if = "MetadataEnvelope::is_empty")]
pub metadata: MetadataEnvelope,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, IntoSchema, FromSchema)]
#[cfg_attr(feature = "full", derive(desert_rust::BinaryCodec))]
#[cfg_attr(feature = "full", desert(evolution()))]
#[serde(tag = "rule", content = "value", rename_all = "kebab-case")]
#[cfg_attr(feature = "full", derive(golem_schema_derive::PoemSchema))]
pub enum DiscriminatorRule {
Prefix { prefix: String },
Suffix { suffix: String },
Contains { substring: String },
Regex { regex: String },
FieldEquals(FieldDiscriminator),
FieldAbsent {
#[serde(rename = "fieldName")]
field_name: String,
},
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, IntoSchema, FromSchema)]
#[cfg_attr(feature = "full", derive(desert_rust::BinaryCodec))]
#[cfg_attr(feature = "full", desert(evolution()))]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "full", derive(golem_schema_derive::PoemSchema))]
pub struct FieldDiscriminator {
pub field_name: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub literal: Option<String>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, IntoSchema, FromSchema)]
#[cfg_attr(feature = "full", derive(desert_rust::BinaryCodec))]
#[cfg_attr(feature = "full", desert(evolution()))]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "full", derive(golem_schema_derive::PoemSchema))]
pub struct SecretSpec {
#[serde(default = "default_secret_inner")]
pub inner: Box<SchemaType>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub category: Option<String>,
}
fn default_secret_inner() -> Box<SchemaType> {
Box::new(SchemaType::string())
}
impl Default for SecretSpec {
fn default() -> Self {
Self {
inner: default_secret_inner(),
category: None,
}
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, IntoSchema, FromSchema)]
#[cfg_attr(feature = "full", derive(desert_rust::BinaryCodec))]
#[cfg_attr(feature = "full", desert(evolution()))]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "full", derive(golem_schema_derive::PoemSchema))]
pub struct QuotaTokenSpec {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub resource_name: Option<String>,
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, IntoSchema, FromSchema)]
#[cfg_attr(feature = "full", derive(desert_rust::BinaryCodec))]
#[cfg_attr(feature = "full", desert(evolution()))]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "full", derive(golem_schema_derive::PoemSchema))]
pub struct PermissionCardSpec {
pub polymorphic: bool,
}
#[cfg(test)]
mod tests {
use super::*;
use test_r::test;
#[test]
fn record_from_fields_builds_fields_without_metadata() {
assert_eq!(
SchemaType::record_from_fields([("name", SchemaType::string())]),
SchemaType::record(vec![NamedFieldType {
name: "name".into(),
body: SchemaType::string(),
metadata: MetadataEnvelope::default(),
}])
);
}
#[test]
fn secret_spec_defaults_inner_to_string_when_absent() {
let decoded: SecretSpec = serde_json::from_value(serde_json::json!({
"category": "api-key"
}))
.expect("SecretSpec JSON without inner should deserialize");
assert_eq!(decoded.inner.as_ref(), &SchemaType::string());
assert_eq!(decoded.category.as_deref(), Some("api-key"));
}
#[test]
fn normalize_drops_empty_to_none() {
assert_eq!(NumericRestrictions::default().normalize(), None);
assert_eq!(
NumericRestrictions {
min: None,
max: None,
unit: Some(String::new()),
}
.normalize(),
None
);
}
#[test]
fn normalize_canonicalizes_negative_zero_float_bound() {
let neg_zero = NumericBound::FloatBits((-0.0f64).to_bits());
let r = NumericRestrictions {
min: Some(neg_zero),
max: None,
unit: None,
}
.normalize()
.expect("non-empty restriction");
assert_eq!(r.min, Some(NumericBound::FloatBits(0)));
assert_eq!(r.min, Some(NumericBound::float(0.0).unwrap()));
}
#[test]
fn serde_empty_restrictions_object_deserializes_to_none() {
let json = serde_json::json!({ "kind": "u32", "value": { "restrictions": {} } });
let ty: SchemaType = serde_json::from_value(json).expect("deserialize");
assert_eq!(ty.numeric_restrictions(), None);
}
#[test]
fn serde_empty_unit_deserializes_to_none() {
let json =
serde_json::json!({ "kind": "u32", "value": { "restrictions": { "unit": "" } } });
let ty: SchemaType = serde_json::from_value(json).expect("deserialize");
assert_eq!(ty.numeric_restrictions(), None);
}
#[test]
fn serde_empty_unit_with_bound_drops_only_the_unit() {
let json = serde_json::json!({
"kind": "u32",
"value": {
"restrictions": {
"min": { "kind": "unsigned", "value": 1 },
"unit": ""
}
}
});
let ty: SchemaType = serde_json::from_value(json).expect("deserialize");
assert_eq!(
ty.numeric_restrictions(),
Some(&NumericRestrictions {
min: Some(NumericBound::Unsigned(1)),
max: None,
unit: None,
})
);
}
#[test]
fn serde_canonicalizes_negative_zero_float_bound() {
let neg_zero_bits = (-0.0f64).to_bits();
let json = serde_json::json!({
"kind": "f64",
"value": { "restrictions": { "min": { "kind": "float-bits", "value": neg_zero_bits } } }
});
let ty: SchemaType = serde_json::from_value(json).expect("deserialize");
assert_eq!(
ty.numeric_restrictions().and_then(|r| r.min),
Some(NumericBound::FloatBits(0))
);
}
#[test]
fn serde_present_restrictions_round_trip() {
let original = SchemaType::U32 {
restrictions: NumericRestrictions {
min: Some(NumericBound::Unsigned(1)),
max: Some(NumericBound::Unsigned(100)),
unit: Some("items".to_string()),
}
.normalize(),
metadata: MetadataEnvelope::default(),
};
let json = serde_json::to_value(&original).expect("serialize");
let back: SchemaType = serde_json::from_value(json).expect("deserialize");
assert_eq!(original, back);
}
}