Skip to main content

golem_schema/schema/
schema_type.rs

1// Copyright 2024-2026 Golem Cloud
2//
3// Licensed under the Golem Source License v1.1 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://license.golem.cloud/LICENSE
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use crate::schema::metadata::{MetadataEnvelope, TypeId};
16use golem_schema_derive::{FromSchema, IntoSchema};
17use serde::{Deserialize, Serialize};
18
19/// One schema type in the recursive in-memory representation.
20///
21/// Every variant carries a [`MetadataEnvelope`] so docs / aliases / examples /
22/// deprecation / role can attach to any node, not only to registered defs or
23/// named positions. Use [`SchemaType::metadata`] / [`SchemaType::metadata_mut`]
24/// for ergonomic access regardless of variant.
25///
26/// Recursive positions hold owned, boxed children (rather than indices) so
27/// consumers can walk and pattern-match the schema directly without chasing
28/// indices into a flat node list. The WIT-shaped flat form is reconstructed
29/// on demand by [`super::wit`].
30///
31/// Closed sum types come in two shapes that differ by how the decoder learns
32/// which branch a value belongs to:
33///
34/// - [`SchemaType::Variant`] is a **carried-tag** sum: the value explicitly
35///   carries its case (via [`super::VariantValuePayload::case`]). Zero-
36///   inference decoding. Natural mapping for language-level algebraic data
37///   types.
38/// - [`SchemaType::Union`] is an **inferred-tag** sum: the value does not
39///   carry its tag. Each branch declares a [`DiscriminatorRule`] and the
40///   decoder picks the branch whose rule matches the raw value.
41#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, IntoSchema, FromSchema)]
42#[cfg_attr(feature = "full", derive(desert_rust::BinaryCodec))]
43#[cfg_attr(feature = "full", desert(evolution()))]
44#[serde(tag = "kind", content = "value", rename_all = "kebab-case")]
45#[schema(named = "schema-type")]
46#[cfg_attr(feature = "full", derive(golem_schema_derive::PoemSchema))]
47pub enum SchemaType {
48    /// Reference to a named definition in the enclosing
49    /// [`super::SchemaGraph`].
50    Ref {
51        id: TypeId,
52        #[serde(default, skip_serializing_if = "MetadataEnvelope::is_empty")]
53        metadata: MetadataEnvelope,
54    },
55
56    // Primitives
57    Bool {
58        #[serde(default, skip_serializing_if = "MetadataEnvelope::is_empty")]
59        metadata: MetadataEnvelope,
60    },
61    S8 {
62        #[serde(
63            default,
64            skip_serializing_if = "Option::is_none",
65            deserialize_with = "deserialize_normalized_numeric_restrictions"
66        )]
67        restrictions: Option<NumericRestrictions>,
68        #[serde(default, skip_serializing_if = "MetadataEnvelope::is_empty")]
69        metadata: MetadataEnvelope,
70    },
71    S16 {
72        #[serde(
73            default,
74            skip_serializing_if = "Option::is_none",
75            deserialize_with = "deserialize_normalized_numeric_restrictions"
76        )]
77        restrictions: Option<NumericRestrictions>,
78        #[serde(default, skip_serializing_if = "MetadataEnvelope::is_empty")]
79        metadata: MetadataEnvelope,
80    },
81    S32 {
82        #[serde(
83            default,
84            skip_serializing_if = "Option::is_none",
85            deserialize_with = "deserialize_normalized_numeric_restrictions"
86        )]
87        restrictions: Option<NumericRestrictions>,
88        #[serde(default, skip_serializing_if = "MetadataEnvelope::is_empty")]
89        metadata: MetadataEnvelope,
90    },
91    S64 {
92        #[serde(
93            default,
94            skip_serializing_if = "Option::is_none",
95            deserialize_with = "deserialize_normalized_numeric_restrictions"
96        )]
97        restrictions: Option<NumericRestrictions>,
98        #[serde(default, skip_serializing_if = "MetadataEnvelope::is_empty")]
99        metadata: MetadataEnvelope,
100    },
101    U8 {
102        #[serde(
103            default,
104            skip_serializing_if = "Option::is_none",
105            deserialize_with = "deserialize_normalized_numeric_restrictions"
106        )]
107        restrictions: Option<NumericRestrictions>,
108        #[serde(default, skip_serializing_if = "MetadataEnvelope::is_empty")]
109        metadata: MetadataEnvelope,
110    },
111    U16 {
112        #[serde(
113            default,
114            skip_serializing_if = "Option::is_none",
115            deserialize_with = "deserialize_normalized_numeric_restrictions"
116        )]
117        restrictions: Option<NumericRestrictions>,
118        #[serde(default, skip_serializing_if = "MetadataEnvelope::is_empty")]
119        metadata: MetadataEnvelope,
120    },
121    U32 {
122        #[serde(
123            default,
124            skip_serializing_if = "Option::is_none",
125            deserialize_with = "deserialize_normalized_numeric_restrictions"
126        )]
127        restrictions: Option<NumericRestrictions>,
128        #[serde(default, skip_serializing_if = "MetadataEnvelope::is_empty")]
129        metadata: MetadataEnvelope,
130    },
131    U64 {
132        #[serde(
133            default,
134            skip_serializing_if = "Option::is_none",
135            deserialize_with = "deserialize_normalized_numeric_restrictions"
136        )]
137        restrictions: Option<NumericRestrictions>,
138        #[serde(default, skip_serializing_if = "MetadataEnvelope::is_empty")]
139        metadata: MetadataEnvelope,
140    },
141    F32 {
142        #[serde(
143            default,
144            skip_serializing_if = "Option::is_none",
145            deserialize_with = "deserialize_normalized_numeric_restrictions"
146        )]
147        restrictions: Option<NumericRestrictions>,
148        #[serde(default, skip_serializing_if = "MetadataEnvelope::is_empty")]
149        metadata: MetadataEnvelope,
150    },
151    F64 {
152        #[serde(
153            default,
154            skip_serializing_if = "Option::is_none",
155            deserialize_with = "deserialize_normalized_numeric_restrictions"
156        )]
157        restrictions: Option<NumericRestrictions>,
158        #[serde(default, skip_serializing_if = "MetadataEnvelope::is_empty")]
159        metadata: MetadataEnvelope,
160    },
161    Char {
162        #[serde(default, skip_serializing_if = "MetadataEnvelope::is_empty")]
163        metadata: MetadataEnvelope,
164    },
165    String {
166        #[serde(default, skip_serializing_if = "MetadataEnvelope::is_empty")]
167        metadata: MetadataEnvelope,
168    },
169
170    // Structural composites
171    Record {
172        fields: Vec<NamedFieldType>,
173        #[serde(default, skip_serializing_if = "MetadataEnvelope::is_empty")]
174        metadata: MetadataEnvelope,
175    },
176    Variant {
177        cases: Vec<VariantCaseType>,
178        #[serde(default, skip_serializing_if = "MetadataEnvelope::is_empty")]
179        metadata: MetadataEnvelope,
180    },
181    Enum {
182        cases: Vec<String>,
183        #[serde(default, skip_serializing_if = "MetadataEnvelope::is_empty")]
184        metadata: MetadataEnvelope,
185    },
186    Flags {
187        flags: Vec<String>,
188        #[serde(default, skip_serializing_if = "MetadataEnvelope::is_empty")]
189        metadata: MetadataEnvelope,
190    },
191    Tuple {
192        elements: Vec<SchemaType>,
193        #[serde(default, skip_serializing_if = "MetadataEnvelope::is_empty")]
194        metadata: MetadataEnvelope,
195    },
196    List {
197        element: Box<SchemaType>,
198        #[serde(default, skip_serializing_if = "MetadataEnvelope::is_empty")]
199        metadata: MetadataEnvelope,
200    },
201    FixedList {
202        element: Box<SchemaType>,
203        length: u32,
204        #[serde(default, skip_serializing_if = "MetadataEnvelope::is_empty")]
205        metadata: MetadataEnvelope,
206    },
207    Map {
208        key: Box<SchemaType>,
209        value: Box<SchemaType>,
210        #[serde(default, skip_serializing_if = "MetadataEnvelope::is_empty")]
211        metadata: MetadataEnvelope,
212    },
213    Option {
214        inner: Box<SchemaType>,
215        #[serde(default, skip_serializing_if = "MetadataEnvelope::is_empty")]
216        metadata: MetadataEnvelope,
217    },
218    Result {
219        spec: ResultSpec,
220        #[serde(default, skip_serializing_if = "MetadataEnvelope::is_empty")]
221        metadata: MetadataEnvelope,
222    },
223
224    // Rich semantic types
225    Text {
226        restrictions: TextRestrictions,
227        #[serde(default, skip_serializing_if = "MetadataEnvelope::is_empty")]
228        metadata: MetadataEnvelope,
229    },
230    Binary {
231        restrictions: BinaryRestrictions,
232        #[serde(default, skip_serializing_if = "MetadataEnvelope::is_empty")]
233        metadata: MetadataEnvelope,
234    },
235    Path {
236        spec: PathSpec,
237        #[serde(default, skip_serializing_if = "MetadataEnvelope::is_empty")]
238        metadata: MetadataEnvelope,
239    },
240    Url {
241        restrictions: UrlRestrictions,
242        #[serde(default, skip_serializing_if = "MetadataEnvelope::is_empty")]
243        metadata: MetadataEnvelope,
244    },
245    Uuid {
246        #[serde(default, skip_serializing_if = "MetadataEnvelope::is_empty")]
247        metadata: MetadataEnvelope,
248    },
249    Datetime {
250        #[serde(default, skip_serializing_if = "MetadataEnvelope::is_empty")]
251        metadata: MetadataEnvelope,
252    },
253    Duration {
254        #[serde(default, skip_serializing_if = "MetadataEnvelope::is_empty")]
255        metadata: MetadataEnvelope,
256    },
257    Quantity {
258        spec: QuantitySpec,
259        #[serde(default, skip_serializing_if = "MetadataEnvelope::is_empty")]
260        metadata: MetadataEnvelope,
261    },
262
263    // Discriminated union (closed, inferred-tag)
264    Union {
265        spec: UnionSpec,
266        #[serde(default, skip_serializing_if = "MetadataEnvelope::is_empty")]
267        metadata: MetadataEnvelope,
268    },
269
270    // Capability nodes
271    Secret {
272        spec: SecretSpec,
273        #[serde(default, skip_serializing_if = "MetadataEnvelope::is_empty")]
274        metadata: MetadataEnvelope,
275    },
276    QuotaToken {
277        spec: QuotaTokenSpec,
278        #[serde(default, skip_serializing_if = "MetadataEnvelope::is_empty")]
279        metadata: MetadataEnvelope,
280    },
281    PermissionCard {
282        spec: PermissionCardSpec,
283        #[serde(default, skip_serializing_if = "MetadataEnvelope::is_empty")]
284        metadata: MetadataEnvelope,
285    },
286
287    // WASI P3 stubs (parseable only; no semantics yet).
288    Future {
289        inner: Option<Box<SchemaType>>,
290        #[serde(default, skip_serializing_if = "MetadataEnvelope::is_empty")]
291        metadata: MetadataEnvelope,
292    },
293    Stream {
294        inner: Option<Box<SchemaType>>,
295        #[serde(default, skip_serializing_if = "MetadataEnvelope::is_empty")]
296        metadata: MetadataEnvelope,
297    },
298}
299
300impl SchemaType {
301    /// Per-node metadata envelope (docs / aliases / examples / deprecation /
302    /// role). Always returns a reference; empty envelopes are the default.
303    pub fn metadata(&self) -> &MetadataEnvelope {
304        match self {
305            SchemaType::Ref { metadata, .. }
306            | SchemaType::Bool { metadata }
307            | SchemaType::S8 { metadata, .. }
308            | SchemaType::S16 { metadata, .. }
309            | SchemaType::S32 { metadata, .. }
310            | SchemaType::S64 { metadata, .. }
311            | SchemaType::U8 { metadata, .. }
312            | SchemaType::U16 { metadata, .. }
313            | SchemaType::U32 { metadata, .. }
314            | SchemaType::U64 { metadata, .. }
315            | SchemaType::F32 { metadata, .. }
316            | SchemaType::F64 { metadata, .. }
317            | SchemaType::Char { metadata }
318            | SchemaType::String { metadata }
319            | SchemaType::Record { metadata, .. }
320            | SchemaType::Variant { metadata, .. }
321            | SchemaType::Enum { metadata, .. }
322            | SchemaType::Flags { metadata, .. }
323            | SchemaType::Tuple { metadata, .. }
324            | SchemaType::List { metadata, .. }
325            | SchemaType::FixedList { metadata, .. }
326            | SchemaType::Map { metadata, .. }
327            | SchemaType::Option { metadata, .. }
328            | SchemaType::Result { metadata, .. }
329            | SchemaType::Text { metadata, .. }
330            | SchemaType::Binary { metadata, .. }
331            | SchemaType::Path { metadata, .. }
332            | SchemaType::Url { metadata, .. }
333            | SchemaType::Uuid { metadata }
334            | SchemaType::Datetime { metadata }
335            | SchemaType::Duration { metadata }
336            | SchemaType::Quantity { metadata, .. }
337            | SchemaType::Union { metadata, .. }
338            | SchemaType::Secret { metadata, .. }
339            | SchemaType::QuotaToken { metadata, .. }
340            | SchemaType::PermissionCard { metadata, .. }
341            | SchemaType::Future { metadata, .. }
342            | SchemaType::Stream { metadata, .. } => metadata,
343        }
344    }
345
346    /// Mutable access to the per-node metadata envelope.
347    pub fn metadata_mut(&mut self) -> &mut MetadataEnvelope {
348        match self {
349            SchemaType::Ref { metadata, .. }
350            | SchemaType::Bool { metadata }
351            | SchemaType::S8 { metadata, .. }
352            | SchemaType::S16 { metadata, .. }
353            | SchemaType::S32 { metadata, .. }
354            | SchemaType::S64 { metadata, .. }
355            | SchemaType::U8 { metadata, .. }
356            | SchemaType::U16 { metadata, .. }
357            | SchemaType::U32 { metadata, .. }
358            | SchemaType::U64 { metadata, .. }
359            | SchemaType::F32 { metadata, .. }
360            | SchemaType::F64 { metadata, .. }
361            | SchemaType::Char { metadata }
362            | SchemaType::String { metadata }
363            | SchemaType::Record { metadata, .. }
364            | SchemaType::Variant { metadata, .. }
365            | SchemaType::Enum { metadata, .. }
366            | SchemaType::Flags { metadata, .. }
367            | SchemaType::Tuple { metadata, .. }
368            | SchemaType::List { metadata, .. }
369            | SchemaType::FixedList { metadata, .. }
370            | SchemaType::Map { metadata, .. }
371            | SchemaType::Option { metadata, .. }
372            | SchemaType::Result { metadata, .. }
373            | SchemaType::Text { metadata, .. }
374            | SchemaType::Binary { metadata, .. }
375            | SchemaType::Path { metadata, .. }
376            | SchemaType::Url { metadata, .. }
377            | SchemaType::Uuid { metadata }
378            | SchemaType::Datetime { metadata }
379            | SchemaType::Duration { metadata }
380            | SchemaType::Quantity { metadata, .. }
381            | SchemaType::Union { metadata, .. }
382            | SchemaType::Secret { metadata, .. }
383            | SchemaType::QuotaToken { metadata, .. }
384            | SchemaType::PermissionCard { metadata, .. }
385            | SchemaType::Future { metadata, .. }
386            | SchemaType::Stream { metadata, .. } => metadata,
387        }
388    }
389
390    /// Replace this node's metadata envelope, returning `self` for chaining.
391    pub fn with_metadata(mut self, metadata: MetadataEnvelope) -> Self {
392        *self.metadata_mut() = metadata;
393        self
394    }
395
396    /// The numeric representation of this node, if it is a numeric type.
397    pub fn numeric_repr(&self) -> Option<NumericRepr> {
398        match self {
399            SchemaType::S8 { .. } => Some(NumericRepr::S8),
400            SchemaType::S16 { .. } => Some(NumericRepr::S16),
401            SchemaType::S32 { .. } => Some(NumericRepr::S32),
402            SchemaType::S64 { .. } => Some(NumericRepr::S64),
403            SchemaType::U8 { .. } => Some(NumericRepr::U8),
404            SchemaType::U16 { .. } => Some(NumericRepr::U16),
405            SchemaType::U32 { .. } => Some(NumericRepr::U32),
406            SchemaType::U64 { .. } => Some(NumericRepr::U64),
407            SchemaType::F32 { .. } => Some(NumericRepr::F32),
408            SchemaType::F64 { .. } => Some(NumericRepr::F64),
409            _ => None,
410        }
411    }
412
413    /// The numeric restrictions of this node, if it is a numeric type with
414    /// restrictions set.
415    pub fn numeric_restrictions(&self) -> Option<&NumericRestrictions> {
416        match self {
417            SchemaType::S8 { restrictions, .. }
418            | SchemaType::S16 { restrictions, .. }
419            | SchemaType::S32 { restrictions, .. }
420            | SchemaType::S64 { restrictions, .. }
421            | SchemaType::U8 { restrictions, .. }
422            | SchemaType::U16 { restrictions, .. }
423            | SchemaType::U32 { restrictions, .. }
424            | SchemaType::U64 { restrictions, .. }
425            | SchemaType::F32 { restrictions, .. }
426            | SchemaType::F64 { restrictions, .. } => restrictions.as_ref(),
427            _ => None,
428        }
429    }
430
431    // --- Ergonomic constructors ------------------------------------------
432    //
433    // These default the metadata envelope to empty; callers that need
434    // metadata can chain `.with_metadata(env)`.
435
436    pub fn ref_to(id: TypeId) -> Self {
437        Self::Ref {
438            id,
439            metadata: MetadataEnvelope::default(),
440        }
441    }
442    pub fn bool() -> Self {
443        Self::Bool {
444            metadata: MetadataEnvelope::default(),
445        }
446    }
447    pub fn s8() -> Self {
448        Self::S8 {
449            restrictions: None,
450            metadata: MetadataEnvelope::default(),
451        }
452    }
453    pub fn s16() -> Self {
454        Self::S16 {
455            restrictions: None,
456            metadata: MetadataEnvelope::default(),
457        }
458    }
459    pub fn s32() -> Self {
460        Self::S32 {
461            restrictions: None,
462            metadata: MetadataEnvelope::default(),
463        }
464    }
465    pub fn s64() -> Self {
466        Self::S64 {
467            restrictions: None,
468            metadata: MetadataEnvelope::default(),
469        }
470    }
471    pub fn u8() -> Self {
472        Self::U8 {
473            restrictions: None,
474            metadata: MetadataEnvelope::default(),
475        }
476    }
477    pub fn u16() -> Self {
478        Self::U16 {
479            restrictions: None,
480            metadata: MetadataEnvelope::default(),
481        }
482    }
483    pub fn u32() -> Self {
484        Self::U32 {
485            restrictions: None,
486            metadata: MetadataEnvelope::default(),
487        }
488    }
489    pub fn u64() -> Self {
490        Self::U64 {
491            restrictions: None,
492            metadata: MetadataEnvelope::default(),
493        }
494    }
495    pub fn f32() -> Self {
496        Self::F32 {
497            restrictions: None,
498            metadata: MetadataEnvelope::default(),
499        }
500    }
501    pub fn f64() -> Self {
502        Self::F64 {
503            restrictions: None,
504            metadata: MetadataEnvelope::default(),
505        }
506    }
507    pub fn char() -> Self {
508        Self::Char {
509            metadata: MetadataEnvelope::default(),
510        }
511    }
512    pub fn string() -> Self {
513        Self::String {
514            metadata: MetadataEnvelope::default(),
515        }
516    }
517    pub fn uuid() -> Self {
518        Self::Uuid {
519            metadata: MetadataEnvelope::default(),
520        }
521    }
522    pub fn datetime() -> Self {
523        Self::Datetime {
524            metadata: MetadataEnvelope::default(),
525        }
526    }
527    pub fn duration() -> Self {
528        Self::Duration {
529            metadata: MetadataEnvelope::default(),
530        }
531    }
532    pub fn record(fields: Vec<NamedFieldType>) -> Self {
533        Self::Record {
534            fields,
535            metadata: MetadataEnvelope::default(),
536        }
537    }
538    /// Convenience: build a record whose fields have no metadata.
539    pub fn record_from_fields<N>(fields: impl IntoIterator<Item = (N, SchemaType)>) -> Self
540    where
541        N: Into<String>,
542    {
543        Self::record(
544            fields
545                .into_iter()
546                .map(|(name, body)| NamedFieldType {
547                    name: name.into(),
548                    body,
549                    metadata: MetadataEnvelope::default(),
550                })
551                .collect(),
552        )
553    }
554    pub fn variant(cases: Vec<VariantCaseType>) -> Self {
555        Self::Variant {
556            cases,
557            metadata: MetadataEnvelope::default(),
558        }
559    }
560    pub fn r#enum(cases: Vec<String>) -> Self {
561        Self::Enum {
562            cases,
563            metadata: MetadataEnvelope::default(),
564        }
565    }
566    pub fn flags(flags: Vec<String>) -> Self {
567        Self::Flags {
568            flags,
569            metadata: MetadataEnvelope::default(),
570        }
571    }
572    pub fn tuple(elements: Vec<SchemaType>) -> Self {
573        Self::Tuple {
574            elements,
575            metadata: MetadataEnvelope::default(),
576        }
577    }
578    pub fn list(element: SchemaType) -> Self {
579        Self::List {
580            element: Box::new(element),
581            metadata: MetadataEnvelope::default(),
582        }
583    }
584    pub fn fixed_list(element: SchemaType, length: u32) -> Self {
585        Self::FixedList {
586            element: Box::new(element),
587            length,
588            metadata: MetadataEnvelope::default(),
589        }
590    }
591    pub fn map(key: SchemaType, value: SchemaType) -> Self {
592        Self::Map {
593            key: Box::new(key),
594            value: Box::new(value),
595            metadata: MetadataEnvelope::default(),
596        }
597    }
598    pub fn option(inner: SchemaType) -> Self {
599        Self::Option {
600            inner: Box::new(inner),
601            metadata: MetadataEnvelope::default(),
602        }
603    }
604    pub fn result(spec: ResultSpec) -> Self {
605        Self::Result {
606            spec,
607            metadata: MetadataEnvelope::default(),
608        }
609    }
610    pub fn text(restrictions: TextRestrictions) -> Self {
611        Self::Text {
612            restrictions,
613            metadata: MetadataEnvelope::default(),
614        }
615    }
616    pub fn binary(restrictions: BinaryRestrictions) -> Self {
617        Self::Binary {
618            restrictions,
619            metadata: MetadataEnvelope::default(),
620        }
621    }
622    pub fn path(spec: PathSpec) -> Self {
623        Self::Path {
624            spec,
625            metadata: MetadataEnvelope::default(),
626        }
627    }
628    pub fn url(restrictions: UrlRestrictions) -> Self {
629        Self::Url {
630            restrictions,
631            metadata: MetadataEnvelope::default(),
632        }
633    }
634    pub fn quantity(spec: QuantitySpec) -> Self {
635        Self::Quantity {
636            spec,
637            metadata: MetadataEnvelope::default(),
638        }
639    }
640    pub fn union(spec: UnionSpec) -> Self {
641        Self::Union {
642            spec,
643            metadata: MetadataEnvelope::default(),
644        }
645    }
646    pub fn secret(spec: SecretSpec) -> Self {
647        Self::Secret {
648            spec,
649            metadata: MetadataEnvelope::default(),
650        }
651    }
652    pub fn quota_token(spec: QuotaTokenSpec) -> Self {
653        Self::QuotaToken {
654            spec,
655            metadata: MetadataEnvelope::default(),
656        }
657    }
658    pub fn permission_card(spec: PermissionCardSpec) -> Self {
659        Self::PermissionCard {
660            spec,
661            metadata: MetadataEnvelope::default(),
662        }
663    }
664    pub fn future(inner: Option<SchemaType>) -> Self {
665        Self::Future {
666            inner: inner.map(Box::new),
667            metadata: MetadataEnvelope::default(),
668        }
669    }
670    pub fn stream(inner: Option<SchemaType>) -> Self {
671        Self::Stream {
672            inner: inner.map(Box::new),
673            metadata: MetadataEnvelope::default(),
674        }
675    }
676}
677
678#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, IntoSchema, FromSchema)]
679#[cfg_attr(feature = "full", derive(desert_rust::BinaryCodec))]
680#[cfg_attr(feature = "full", desert(evolution()))]
681#[serde(rename_all = "camelCase")]
682#[cfg_attr(feature = "full", derive(golem_schema_derive::PoemSchema))]
683pub struct NamedFieldType {
684    pub name: String,
685    pub body: SchemaType,
686    #[serde(default, skip_serializing_if = "MetadataEnvelope::is_empty")]
687    pub metadata: MetadataEnvelope,
688}
689
690#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, IntoSchema, FromSchema)]
691#[cfg_attr(feature = "full", derive(desert_rust::BinaryCodec))]
692#[cfg_attr(feature = "full", desert(evolution()))]
693#[serde(rename_all = "camelCase")]
694#[cfg_attr(feature = "full", derive(golem_schema_derive::PoemSchema))]
695pub struct VariantCaseType {
696    pub name: String,
697    #[serde(default, skip_serializing_if = "Option::is_none")]
698    pub payload: Option<SchemaType>,
699    #[serde(default, skip_serializing_if = "MetadataEnvelope::is_empty")]
700    pub metadata: MetadataEnvelope,
701}
702
703#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, IntoSchema, FromSchema)]
704#[cfg_attr(feature = "full", derive(desert_rust::BinaryCodec))]
705#[cfg_attr(feature = "full", desert(evolution()))]
706#[serde(rename_all = "camelCase")]
707#[cfg_attr(feature = "full", derive(golem_schema_derive::PoemSchema))]
708pub struct ResultSpec {
709    #[serde(default, skip_serializing_if = "Option::is_none")]
710    pub ok: Option<Box<SchemaType>>,
711    #[serde(default, skip_serializing_if = "Option::is_none")]
712    pub err: Option<Box<SchemaType>>,
713}
714
715// --- Numeric ---
716
717/// A numeric bound usable across every numeric representation.
718///
719/// `SchemaType` derives `Eq`, and an `i64` mantissa cannot represent
720/// `u64::MAX`, so the bound is a three-family sum. Float bounds store canonical
721/// IEEE-754 `f64` bits (`NaN`/`inf` rejected at construction, `-0.0` normalized
722/// to `+0.0`) so the type stays `Eq`; comparisons always decode the bits back to
723/// `f64` and compare numerically, never by bit order.
724#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, IntoSchema, FromSchema)]
725#[cfg_attr(feature = "full", derive(desert_rust::BinaryCodec))]
726#[cfg_attr(feature = "full", desert(evolution()))]
727#[serde(tag = "kind", content = "value", rename_all = "kebab-case")]
728#[cfg_attr(feature = "full", derive(golem_schema_derive::PoemSchema))]
729pub enum NumericBound {
730    Signed(i64),
731    Unsigned(u64),
732    FloatBits(u64),
733}
734
735/// Error returned when constructing a numeric bound from an invalid value.
736#[derive(Clone, Copy, Debug, PartialEq, Eq)]
737pub enum NumericBoundError {
738    /// The float value was `NaN` or infinite.
739    NonFinite,
740}
741
742impl std::fmt::Display for NumericBoundError {
743    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
744        match self {
745            NumericBoundError::NonFinite => write!(f, "numeric float bound must be finite"),
746        }
747    }
748}
749
750impl std::error::Error for NumericBoundError {}
751
752impl NumericBound {
753    /// Construct a float bound from an `f64`, rejecting `NaN`/`inf` and
754    /// normalizing `-0.0` to `+0.0`.
755    pub fn float(value: f64) -> Result<Self, NumericBoundError> {
756        if !value.is_finite() {
757            return Err(NumericBoundError::NonFinite);
758        }
759        // Normalize -0.0 to +0.0 so canonical bits are stable for `Eq`.
760        let normalized = value + 0.0;
761        Ok(NumericBound::FloatBits(normalized.to_bits()))
762    }
763
764    /// The float value of a `FloatBits` bound (decoded from canonical bits).
765    pub fn as_f64(&self) -> Option<f64> {
766        match self {
767            NumericBound::FloatBits(bits) => Some(f64::from_bits(*bits)),
768            _ => None,
769        }
770    }
771}
772
773/// Inline numeric refinement carried by the numeric `SchemaType` variants.
774///
775/// `None` on the variant means "unconstrained" — the common, hot-path case, a
776/// single tag rather than three carried inner `Option`s. The empty restriction
777/// set is never stored as `Some`: smart constructors and decoders collapse it to
778/// `None` via [`NumericRestrictions::normalize`], and well-formedness rejects a
779/// stored `Some(empty)`.
780#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, IntoSchema, FromSchema)]
781#[cfg_attr(feature = "full", derive(desert_rust::BinaryCodec))]
782#[cfg_attr(feature = "full", desert(evolution()))]
783#[serde(rename_all = "camelCase")]
784#[cfg_attr(feature = "full", derive(golem_schema_derive::PoemSchema))]
785pub struct NumericRestrictions {
786    #[serde(default, skip_serializing_if = "Option::is_none")]
787    pub min: Option<NumericBound>,
788    #[serde(default, skip_serializing_if = "Option::is_none")]
789    pub max: Option<NumericBound>,
790    /// Free-form unit annotation. Schema/help metadata only: numeric
791    /// `SchemaValue`s carry no unit, so value validation never checks it;
792    /// equivalence/subtyping require the normalized unit to match exactly.
793    #[serde(default, skip_serializing_if = "Option::is_none")]
794    pub unit: Option<String>,
795}
796
797impl NumericRestrictions {
798    /// True when there is nothing to constrain (no bounds and no non-empty unit).
799    pub fn is_empty(&self) -> bool {
800        self.min.is_none()
801            && self.max.is_none()
802            && self.unit.as_ref().map(|u| u.is_empty()).unwrap_or(true)
803    }
804
805    /// Collapse the empty restriction set to `None`, dropping an empty `unit`
806    /// and canonicalizing float bounds (so a decoded `-0.0` becomes `+0.0`).
807    /// This enforces the canonicalization invariants that `Some(empty)` is never
808    /// constructed and that two restriction sets which differ only by float zero
809    /// sign compare equal (`SchemaType` derives `Eq` and feeds byte-equivalence).
810    pub fn normalize(mut self) -> Option<Self> {
811        self.min = self.min.map(canonicalize_bound);
812        self.max = self.max.map(canonicalize_bound);
813        if self.unit.as_ref().map(|u| u.is_empty()).unwrap_or(false) {
814            self.unit = None;
815        }
816        if self.is_empty() { None } else { Some(self) }
817    }
818
819    /// Repr-aware well-formedness check, shared by every validation entry point
820    /// (well-formedness, subtyping, value validation, macro refinement) so no
821    /// path assumes a prior well-formedness pass.
822    ///
823    /// Rejects: a stored empty set (`Some(empty)` must never exist), a bound
824    /// whose family does not match the repr, an integer repr carrying a float
825    /// bound, a bound that does not fit the repr's range, an `F32` bound that
826    /// does not round-trip through `f32`, and `min > max` (compared numerically).
827    pub fn validate_for_repr(&self, repr: NumericRepr) -> Result<(), NumericRestrictionError> {
828        if self.is_empty() {
829            return Err(NumericRestrictionError::EmptyStored);
830        }
831        if let Some(min) = self.min {
832            check_bound_repr(min, repr)?;
833        }
834        if let Some(max) = self.max {
835            check_bound_repr(max, repr)?;
836        }
837        if let (Some(min), Some(max)) = (self.min, self.max) {
838            match numeric_bound_cmp(min, max) {
839                Some(std::cmp::Ordering::Greater) => {
840                    return Err(NumericRestrictionError::MinGreaterThanMax);
841                }
842                Some(_) => {}
843                None => return Err(NumericRestrictionError::FamilyMismatch),
844            }
845        }
846        Ok(())
847    }
848}
849
850/// The concrete numeric representation of a numeric `SchemaType` variant.
851#[derive(Clone, Copy, Debug, PartialEq, Eq)]
852pub enum NumericRepr {
853    S8,
854    S16,
855    S32,
856    S64,
857    U8,
858    U16,
859    U32,
860    U64,
861    F32,
862    F64,
863}
864
865impl NumericRepr {
866    /// True for the integer reprs (`S*`/`U*`); false for `F32`/`F64`.
867    pub fn is_integer(self) -> bool {
868        !matches!(self, NumericRepr::F32 | NumericRepr::F64)
869    }
870
871    fn family(self) -> NumericFamily {
872        match self {
873            NumericRepr::S8 | NumericRepr::S16 | NumericRepr::S32 | NumericRepr::S64 => {
874                NumericFamily::Signed
875            }
876            NumericRepr::U8 | NumericRepr::U16 | NumericRepr::U32 | NumericRepr::U64 => {
877                NumericFamily::Unsigned
878            }
879            NumericRepr::F32 | NumericRepr::F64 => NumericFamily::Float,
880        }
881    }
882
883    fn signed_range(self) -> Option<(i64, i64)> {
884        match self {
885            NumericRepr::S8 => Some((i8::MIN as i64, i8::MAX as i64)),
886            NumericRepr::S16 => Some((i16::MIN as i64, i16::MAX as i64)),
887            NumericRepr::S32 => Some((i32::MIN as i64, i32::MAX as i64)),
888            NumericRepr::S64 => Some((i64::MIN, i64::MAX)),
889            _ => None,
890        }
891    }
892
893    fn unsigned_max(self) -> Option<u64> {
894        match self {
895            NumericRepr::U8 => Some(u8::MAX as u64),
896            NumericRepr::U16 => Some(u16::MAX as u64),
897            NumericRepr::U32 => Some(u32::MAX as u64),
898            NumericRepr::U64 => Some(u64::MAX),
899            _ => None,
900        }
901    }
902}
903
904#[derive(Clone, Copy, Debug, PartialEq, Eq)]
905enum NumericFamily {
906    Signed,
907    Unsigned,
908    Float,
909}
910
911impl NumericBound {
912    fn family(self) -> NumericFamily {
913        match self {
914            NumericBound::Signed(_) => NumericFamily::Signed,
915            NumericBound::Unsigned(_) => NumericFamily::Unsigned,
916            NumericBound::FloatBits(_) => NumericFamily::Float,
917        }
918    }
919}
920
921/// Canonicalize a numeric bound so equal values have equal bits. Currently this
922/// only affects float bounds: a `-0.0` (which compares equal to `+0.0`
923/// numerically but has different bits) is rewritten to canonical `+0.0` so the
924/// `Eq`/byte-equivalence invariants hold for decoded bounds. `NaN`/`inf` bits
925/// are left untouched (well-formedness rejects them).
926fn canonicalize_bound(b: NumericBound) -> NumericBound {
927    match b {
928        NumericBound::FloatBits(bits) => {
929            if f64::from_bits(bits) == 0.0 {
930                NumericBound::FloatBits(0)
931            } else {
932                b
933            }
934        }
935        other => other,
936    }
937}
938
939/// serde helper: deserialize an `Option<NumericRestrictions>` field on a numeric
940/// `SchemaType` variant, applying the same empty→`None` and float canonicalization
941/// normalization the WIT/protobuf decoders use, so JSON is a faithful decode
942/// boundary (`restrictions: {}` or `unit: ""` collapse to `None`).
943fn deserialize_normalized_numeric_restrictions<'de, D>(
944    deserializer: D,
945) -> Result<Option<NumericRestrictions>, D::Error>
946where
947    D: serde::Deserializer<'de>,
948{
949    let raw = Option::<NumericRestrictions>::deserialize(deserializer)?;
950    Ok(raw.and_then(NumericRestrictions::normalize))
951}
952
953/// Numeric comparison of two bounds of the same family. Returns `None` when the
954/// families differ (which callers treat as a family mismatch / "not a subtype").
955pub fn numeric_bound_cmp(a: NumericBound, b: NumericBound) -> Option<std::cmp::Ordering> {
956    match (a, b) {
957        (NumericBound::Signed(x), NumericBound::Signed(y)) => Some(x.cmp(&y)),
958        (NumericBound::Unsigned(x), NumericBound::Unsigned(y)) => Some(x.cmp(&y)),
959        (NumericBound::FloatBits(x), NumericBound::FloatBits(y)) => {
960            // Both are finite by construction, so partial_cmp is total here.
961            f64::from_bits(x).partial_cmp(&f64::from_bits(y))
962        }
963        _ => None,
964    }
965}
966
967fn check_bound_repr(bound: NumericBound, repr: NumericRepr) -> Result<(), NumericRestrictionError> {
968    if bound.family() != repr.family() {
969        return Err(NumericRestrictionError::FamilyMismatch);
970    }
971    match bound {
972        NumericBound::Signed(v) => {
973            let (lo, hi) = repr.signed_range().expect("signed family => signed range");
974            if v < lo || v > hi {
975                return Err(NumericRestrictionError::BoundOutOfRange);
976            }
977        }
978        NumericBound::Unsigned(v) => {
979            let hi = repr
980                .unsigned_max()
981                .expect("unsigned family => unsigned max");
982            if v > hi {
983                return Err(NumericRestrictionError::BoundOutOfRange);
984            }
985        }
986        NumericBound::FloatBits(bits) => {
987            let v = f64::from_bits(bits);
988            if !v.is_finite() {
989                return Err(NumericRestrictionError::NonFiniteFloat);
990            }
991            if repr == NumericRepr::F32 && (v as f32) as f64 != v {
992                return Err(NumericRestrictionError::FloatNotRoundTrippable);
993            }
994        }
995    }
996    Ok(())
997}
998
999/// Reasons a numeric restriction set is not well-formed for a given repr.
1000#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1001pub enum NumericRestrictionError {
1002    /// An empty restriction set was stored as `Some` (must be `None`).
1003    EmptyStored,
1004    /// A bound's family does not match the repr (e.g. signed bound on `U32`).
1005    FamilyMismatch,
1006    /// A bound value does not fit the repr's range.
1007    BoundOutOfRange,
1008    /// A float bound was `NaN`/infinite.
1009    NonFiniteFloat,
1010    /// An `F32` bound does not round-trip through `f32`.
1011    FloatNotRoundTrippable,
1012    /// `min` is numerically greater than `max`.
1013    MinGreaterThanMax,
1014}
1015
1016impl std::fmt::Display for NumericRestrictionError {
1017    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1018        let msg = match self {
1019            NumericRestrictionError::EmptyStored => {
1020                "numeric restriction set is empty (must be None)"
1021            }
1022            NumericRestrictionError::FamilyMismatch => {
1023                "numeric bound family does not match the numeric type"
1024            }
1025            NumericRestrictionError::BoundOutOfRange => {
1026                "numeric bound does not fit the numeric type's range"
1027            }
1028            NumericRestrictionError::NonFiniteFloat => "numeric float bound must be finite",
1029            NumericRestrictionError::FloatNotRoundTrippable => {
1030                "f32 numeric bound does not round-trip through f32"
1031            }
1032            NumericRestrictionError::MinGreaterThanMax => {
1033                "numeric min bound is greater than max bound"
1034            }
1035        };
1036        write!(f, "{msg}")
1037    }
1038}
1039
1040// --- Text / Binary ---
1041
1042#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, IntoSchema, FromSchema)]
1043#[cfg_attr(feature = "full", derive(desert_rust::BinaryCodec))]
1044#[cfg_attr(feature = "full", desert(evolution()))]
1045#[serde(rename_all = "camelCase")]
1046#[cfg_attr(feature = "full", derive(golem_schema_derive::PoemSchema))]
1047pub struct TextRestrictions {
1048    /// Optional set of allowed BCP-47 language codes. `None` = unrestricted.
1049    #[serde(default, skip_serializing_if = "Option::is_none")]
1050    pub languages: Option<Vec<String>>,
1051    #[serde(default, skip_serializing_if = "Option::is_none")]
1052    pub min_length: Option<u32>,
1053    #[serde(default, skip_serializing_if = "Option::is_none")]
1054    pub max_length: Option<u32>,
1055    #[serde(default, skip_serializing_if = "Option::is_none")]
1056    pub regex: Option<String>,
1057}
1058
1059#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, IntoSchema, FromSchema)]
1060#[cfg_attr(feature = "full", derive(desert_rust::BinaryCodec))]
1061#[cfg_attr(feature = "full", desert(evolution()))]
1062#[serde(rename_all = "camelCase")]
1063#[cfg_attr(feature = "full", derive(golem_schema_derive::PoemSchema))]
1064pub struct BinaryRestrictions {
1065    /// Optional set of allowed MIME types. `None` = unrestricted.
1066    #[serde(default, skip_serializing_if = "Option::is_none")]
1067    pub mime_types: Option<Vec<String>>,
1068    #[serde(default, skip_serializing_if = "Option::is_none")]
1069    pub min_bytes: Option<u32>,
1070    #[serde(default, skip_serializing_if = "Option::is_none")]
1071    pub max_bytes: Option<u32>,
1072}
1073
1074// --- Path ---
1075
1076#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize, IntoSchema, FromSchema)]
1077#[cfg_attr(feature = "full", derive(desert_rust::BinaryCodec))]
1078#[cfg_attr(feature = "full", desert(evolution()))]
1079#[serde(rename_all = "kebab-case")]
1080#[cfg_attr(feature = "full", derive(golem_schema_derive::PoemSchema))]
1081pub enum PathDirection {
1082    Input,
1083    Output,
1084    InOut,
1085}
1086
1087#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize, IntoSchema, FromSchema)]
1088#[cfg_attr(feature = "full", derive(desert_rust::BinaryCodec))]
1089#[cfg_attr(feature = "full", desert(evolution()))]
1090#[serde(rename_all = "kebab-case")]
1091#[cfg_attr(feature = "full", derive(golem_schema_derive::PoemSchema))]
1092pub enum PathKind {
1093    File,
1094    Directory,
1095    Any,
1096}
1097
1098#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, IntoSchema, FromSchema)]
1099#[cfg_attr(feature = "full", derive(desert_rust::BinaryCodec))]
1100#[cfg_attr(feature = "full", desert(evolution()))]
1101#[serde(rename_all = "camelCase")]
1102#[cfg_attr(feature = "full", derive(golem_schema_derive::PoemSchema))]
1103pub struct PathSpec {
1104    pub direction: PathDirection,
1105    pub kind: PathKind,
1106    /// MIME types allowed for content referenced by this path.
1107    ///
1108    /// Not enforced by value validation because [`crate::schema::schema_value::SchemaValue::Path`]
1109    /// carries only a path string. Enforce this only in protocol/adaptation
1110    /// layers that have MIME metadata, such as HTTP `Content-Type` or a
1111    /// content-typed envelope. The validator must not read the filesystem
1112    /// or sniff content.
1113    #[serde(default, skip_serializing_if = "Option::is_none")]
1114    pub allowed_mime_types: Option<Vec<String>>,
1115    #[serde(default, skip_serializing_if = "Option::is_none")]
1116    pub allowed_extensions: Option<Vec<String>>,
1117}
1118
1119// --- URL ---
1120
1121#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, IntoSchema, FromSchema)]
1122#[cfg_attr(feature = "full", derive(desert_rust::BinaryCodec))]
1123#[cfg_attr(feature = "full", desert(evolution()))]
1124#[serde(rename_all = "camelCase")]
1125#[cfg_attr(feature = "full", derive(golem_schema_derive::PoemSchema))]
1126pub struct UrlRestrictions {
1127    #[serde(default, skip_serializing_if = "Option::is_none")]
1128    pub allowed_schemes: Option<Vec<String>>,
1129    #[serde(default, skip_serializing_if = "Option::is_none")]
1130    pub allowed_hosts: Option<Vec<String>>,
1131}
1132
1133// --- Quantity ---
1134
1135/// Fixed-point decimal value with unit: numeric value = `mantissa * 10^(-scale)`.
1136/// `unit` is a free-form string at the value level; the schema's
1137/// [`QuantitySpec`] constrains the accepted set.
1138#[derive(
1139    Clone,
1140    Debug,
1141    PartialEq,
1142    Eq,
1143    Serialize,
1144    Deserialize,
1145    golem_schema_derive::IntoSchema,
1146    golem_schema_derive::FromSchema,
1147)]
1148#[cfg_attr(feature = "full", derive(desert_rust::BinaryCodec))]
1149#[cfg_attr(feature = "full", desert(evolution()))]
1150#[serde(rename_all = "camelCase")]
1151#[cfg_attr(feature = "full", derive(golem_schema_derive::PoemSchema))]
1152pub struct QuantityValue {
1153    pub mantissa: i64,
1154    pub scale: i32,
1155    pub unit: String,
1156}
1157
1158#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, IntoSchema, FromSchema)]
1159#[cfg_attr(feature = "full", derive(desert_rust::BinaryCodec))]
1160#[cfg_attr(feature = "full", desert(evolution()))]
1161#[serde(rename_all = "camelCase")]
1162#[cfg_attr(feature = "full", derive(golem_schema_derive::PoemSchema))]
1163pub struct QuantitySpec {
1164    /// Canonical base unit (e.g., `"kg"`, `"m"`, `"s"`, `"B"`).
1165    pub base_unit: String,
1166    /// Suffixes accepted on input and rendered on output.
1167    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1168    pub allowed_suffixes: Vec<String>,
1169    /// Optional inclusive range, expressed in canonical fixed-point form.
1170    #[serde(default, skip_serializing_if = "Option::is_none")]
1171    pub min: Option<QuantityValue>,
1172    #[serde(default, skip_serializing_if = "Option::is_none")]
1173    pub max: Option<QuantityValue>,
1174}
1175
1176// --- Discriminated union ---
1177
1178#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, IntoSchema, FromSchema)]
1179#[cfg_attr(feature = "full", derive(desert_rust::BinaryCodec))]
1180#[cfg_attr(feature = "full", desert(evolution()))]
1181#[serde(rename_all = "camelCase")]
1182#[cfg_attr(feature = "full", derive(golem_schema_derive::PoemSchema))]
1183/// An inferred-tag sum governed by the canonical portable pair-classification
1184/// contract.
1185///
1186/// Under that contract, all guaranteed non-regex overlaps and byte-identical
1187/// regex text are rejected. Other regex-containing string-rule pairs are
1188/// accepted as indeterminate because SDK regex dialects differ; runtime
1189/// decoding still rejects a value that matches multiple branches.
1190pub struct UnionSpec {
1191    pub branches: Vec<UnionBranch>,
1192}
1193
1194#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, IntoSchema, FromSchema)]
1195#[cfg_attr(feature = "full", derive(desert_rust::BinaryCodec))]
1196#[cfg_attr(feature = "full", desert(evolution()))]
1197#[serde(rename_all = "camelCase")]
1198#[cfg_attr(feature = "full", derive(golem_schema_derive::PoemSchema))]
1199pub struct UnionBranch {
1200    /// Logical branch name carried in
1201    /// [`super::UnionValuePayload::tag`] after the decoder resolves the
1202    /// branch; used by renderers, codegen, docs.
1203    pub tag: String,
1204    /// Branch body type. Must be compatible with the discriminator rule (see
1205    /// schema-construction validation rules in the design).
1206    pub body: SchemaType,
1207    /// Rule the decoder uses to pick this branch from a raw value.
1208    pub discriminator: DiscriminatorRule,
1209    #[serde(default, skip_serializing_if = "MetadataEnvelope::is_empty")]
1210    pub metadata: MetadataEnvelope,
1211}
1212
1213/// How the decoder identifies that a value belongs to a given union branch.
1214#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, IntoSchema, FromSchema)]
1215#[cfg_attr(feature = "full", derive(desert_rust::BinaryCodec))]
1216#[cfg_attr(feature = "full", desert(evolution()))]
1217#[serde(tag = "rule", content = "value", rename_all = "kebab-case")]
1218#[cfg_attr(feature = "full", derive(golem_schema_derive::PoemSchema))]
1219pub enum DiscriminatorRule {
1220    /// String value starts with this prefix.
1221    Prefix { prefix: String },
1222    /// String value ends with this suffix.
1223    Suffix { suffix: String },
1224    /// String value contains this substring.
1225    Contains { substring: String },
1226    /// String value has a match for this regex. Include anchors in the pattern
1227    /// when whole-string matching is required.
1228    Regex { regex: String },
1229    /// Record-shaped value where the named field is present, and — if
1230    /// `literal` is set — has the given literal string value.
1231    FieldEquals(FieldDiscriminator),
1232    /// Record-shaped value where the named field is absent.
1233    FieldAbsent {
1234        #[serde(rename = "fieldName")]
1235        field_name: String,
1236    },
1237}
1238
1239#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, IntoSchema, FromSchema)]
1240#[cfg_attr(feature = "full", derive(desert_rust::BinaryCodec))]
1241#[cfg_attr(feature = "full", desert(evolution()))]
1242#[serde(rename_all = "camelCase")]
1243#[cfg_attr(feature = "full", derive(golem_schema_derive::PoemSchema))]
1244pub struct FieldDiscriminator {
1245    pub field_name: String,
1246    #[serde(default, skip_serializing_if = "Option::is_none")]
1247    pub literal: Option<String>,
1248}
1249
1250// --- Capability nodes ---
1251
1252#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, IntoSchema, FromSchema)]
1253#[cfg_attr(feature = "full", derive(desert_rust::BinaryCodec))]
1254#[cfg_attr(feature = "full", desert(evolution()))]
1255#[serde(rename_all = "camelCase")]
1256#[cfg_attr(feature = "full", derive(golem_schema_derive::PoemSchema))]
1257pub struct SecretSpec {
1258    /// Revealed payload type carried by this secret handle.
1259    #[serde(default = "default_secret_inner")]
1260    pub inner: Box<SchemaType>,
1261    /// Optional categorisation (e.g., `"api-key"`, `"oauth-token"`).
1262    #[serde(default, skip_serializing_if = "Option::is_none")]
1263    pub category: Option<String>,
1264}
1265
1266fn default_secret_inner() -> Box<SchemaType> {
1267    Box::new(SchemaType::string())
1268}
1269
1270impl Default for SecretSpec {
1271    fn default() -> Self {
1272        Self {
1273            inner: default_secret_inner(),
1274            category: None,
1275        }
1276    }
1277}
1278
1279#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, IntoSchema, FromSchema)]
1280#[cfg_attr(feature = "full", derive(desert_rust::BinaryCodec))]
1281#[cfg_attr(feature = "full", desert(evolution()))]
1282#[serde(rename_all = "camelCase")]
1283#[cfg_attr(feature = "full", derive(golem_schema_derive::PoemSchema))]
1284pub struct QuotaTokenSpec {
1285    /// Resource name this token covers (declared in the agent manifest).
1286    /// `None` = any resource permitted.
1287    #[serde(default, skip_serializing_if = "Option::is_none")]
1288    pub resource_name: Option<String>,
1289}
1290
1291/// Spec for a [`SchemaType::PermissionCard`] capability node.
1292///
1293/// Mirrors the WIT `permission-card-spec { polymorphic: bool }` record: a
1294/// permission-card is an opaque, unforgeable handle whose only static trait
1295/// visible to schema consumers is whether it is polymorphic (can spawn scoped
1296/// child cards at runtime).
1297#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, IntoSchema, FromSchema)]
1298#[cfg_attr(feature = "full", derive(desert_rust::BinaryCodec))]
1299#[cfg_attr(feature = "full", desert(evolution()))]
1300#[serde(rename_all = "camelCase")]
1301#[cfg_attr(feature = "full", derive(golem_schema_derive::PoemSchema))]
1302pub struct PermissionCardSpec {
1303    /// Whether cards of this type may spawn scoped child cards at runtime.
1304    pub polymorphic: bool,
1305}
1306
1307#[cfg(test)]
1308mod tests {
1309    use super::*;
1310    use test_r::test;
1311
1312    #[test]
1313    fn record_from_fields_builds_fields_without_metadata() {
1314        assert_eq!(
1315            SchemaType::record_from_fields([("name", SchemaType::string())]),
1316            SchemaType::record(vec![NamedFieldType {
1317                name: "name".into(),
1318                body: SchemaType::string(),
1319                metadata: MetadataEnvelope::default(),
1320            }])
1321        );
1322    }
1323
1324    #[test]
1325    fn secret_spec_defaults_inner_to_string_when_absent() {
1326        let decoded: SecretSpec = serde_json::from_value(serde_json::json!({
1327            "category": "api-key"
1328        }))
1329        .expect("SecretSpec JSON without inner should deserialize");
1330
1331        assert_eq!(decoded.inner.as_ref(), &SchemaType::string());
1332        assert_eq!(decoded.category.as_deref(), Some("api-key"));
1333    }
1334
1335    #[test]
1336    fn normalize_drops_empty_to_none() {
1337        assert_eq!(NumericRestrictions::default().normalize(), None);
1338        assert_eq!(
1339            NumericRestrictions {
1340                min: None,
1341                max: None,
1342                unit: Some(String::new()),
1343            }
1344            .normalize(),
1345            None
1346        );
1347    }
1348
1349    #[test]
1350    fn normalize_canonicalizes_negative_zero_float_bound() {
1351        let neg_zero = NumericBound::FloatBits((-0.0f64).to_bits());
1352        let r = NumericRestrictions {
1353            min: Some(neg_zero),
1354            max: None,
1355            unit: None,
1356        }
1357        .normalize()
1358        .expect("non-empty restriction");
1359        assert_eq!(r.min, Some(NumericBound::FloatBits(0)));
1360        // Canonical +0.0 has bits 0, so it now equals a freshly-constructed +0.0.
1361        assert_eq!(r.min, Some(NumericBound::float(0.0).unwrap()));
1362    }
1363
1364    #[test]
1365    fn serde_empty_restrictions_object_deserializes_to_none() {
1366        let json = serde_json::json!({ "kind": "u32", "value": { "restrictions": {} } });
1367        let ty: SchemaType = serde_json::from_value(json).expect("deserialize");
1368        assert_eq!(ty.numeric_restrictions(), None);
1369    }
1370
1371    #[test]
1372    fn serde_empty_unit_deserializes_to_none() {
1373        let json =
1374            serde_json::json!({ "kind": "u32", "value": { "restrictions": { "unit": "" } } });
1375        let ty: SchemaType = serde_json::from_value(json).expect("deserialize");
1376        assert_eq!(ty.numeric_restrictions(), None);
1377    }
1378
1379    #[test]
1380    fn serde_empty_unit_with_bound_drops_only_the_unit() {
1381        // An empty `unit` is non-canonical and must normalize to `None`, but a
1382        // present bound keeps the restriction set alive (it is not empty).
1383        let json = serde_json::json!({
1384            "kind": "u32",
1385            "value": {
1386                "restrictions": {
1387                    "min": { "kind": "unsigned", "value": 1 },
1388                    "unit": ""
1389                }
1390            }
1391        });
1392        let ty: SchemaType = serde_json::from_value(json).expect("deserialize");
1393        assert_eq!(
1394            ty.numeric_restrictions(),
1395            Some(&NumericRestrictions {
1396                min: Some(NumericBound::Unsigned(1)),
1397                max: None,
1398                unit: None,
1399            })
1400        );
1401    }
1402
1403    #[test]
1404    fn serde_canonicalizes_negative_zero_float_bound() {
1405        let neg_zero_bits = (-0.0f64).to_bits();
1406        let json = serde_json::json!({
1407            "kind": "f64",
1408            "value": { "restrictions": { "min": { "kind": "float-bits", "value": neg_zero_bits } } }
1409        });
1410        let ty: SchemaType = serde_json::from_value(json).expect("deserialize");
1411        assert_eq!(
1412            ty.numeric_restrictions().and_then(|r| r.min),
1413            Some(NumericBound::FloatBits(0))
1414        );
1415    }
1416
1417    #[test]
1418    fn serde_present_restrictions_round_trip() {
1419        let original = SchemaType::U32 {
1420            restrictions: NumericRestrictions {
1421                min: Some(NumericBound::Unsigned(1)),
1422                max: Some(NumericBound::Unsigned(100)),
1423                unit: Some("items".to_string()),
1424            }
1425            .normalize(),
1426            metadata: MetadataEnvelope::default(),
1427        };
1428        let json = serde_json::to_value(&original).expect("serialize");
1429        let back: SchemaType = serde_json::from_value(json).expect("deserialize");
1430        assert_eq!(original, back);
1431    }
1432}