Skip to main content

toolkit_contract/ir/
contract.rs

1use serde::{Deserialize, Serialize};
2
3/// Intermediate representation of a complete contract.
4///
5/// Deliberately NOT `#[non_exhaustive]`: `#[toolkit::contract]` emits a
6/// struct-literal `ContractIr { .. }` directly into the SDK crate's generated
7/// `contract_ir()` function (see `toolkit-contract-macros/src/codegen.rs`).
8/// `#[non_exhaustive]` on a struct blocks ALL external struct-literal
9/// construction (even with every field listed), which would break every
10/// downstream SDK crate at macro-expansion time. The same applies to
11/// [`MethodIr`], [`InputShape`], [`FieldIr`] below, and to
12/// [`super::binding::HttpBindingIr`] / [`super::binding::HttpMethodBindingIr`].
13/// Additive evolution here must go through a constructor fn, not new pub
14/// fields, or accept a semver-breaking release for the IR structs.
15#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct ContractIr {
17    /// Contract name, usually the SDK trait name.
18    pub name: String,
19    /// Gear that provides this contract.
20    pub gear: String,
21    /// API version.
22    pub version: String,
23    /// Methods exposed by this contract.
24    pub methods: Vec<MethodIr>,
25}
26
27/// Intermediate representation of a single contract method.
28#[derive(Debug, Clone, Serialize, Deserialize)]
29pub struct MethodIr {
30    /// Method name.
31    pub name: String,
32    /// Whether this method is unary or streaming.
33    pub kind: MethodKind,
34    /// Input parameters.
35    pub input: InputShape,
36    /// Output type reference.
37    pub output: TypeRef,
38    /// Error type reference, if the method is fallible.
39    pub error: Option<TypeRef>,
40    /// Idempotency classification for retry decisions.
41    pub idempotency: Idempotency,
42    /// `true` when the trait declares a default body — peers MAY omit
43    /// this method (carried as `x-optional` extension in `OpenAPI`).
44    #[serde(default)]
45    pub optional: bool,
46}
47
48/// Whether a method returns a single value or a stream.
49///
50/// `#[non_exhaustive]`: this enum evolves (new method shapes). Only bare
51/// variants are ever emitted by codegen, so external construction of existing
52/// variants remains unaffected — only exhaustive `match` without a wildcard is
53/// blocked, which is the intended forward-compat guarantee.
54#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
55#[non_exhaustive]
56pub enum MethodKind {
57    /// Request -> Response.
58    Unary,
59    /// Request -> Stream of responses.
60    ServerStreaming,
61}
62
63/// Shape of a method's input parameters.
64#[derive(Debug, Clone, Serialize, Deserialize)]
65pub struct InputShape {
66    /// Ordered list of input fields.
67    pub fields: Vec<FieldIr>,
68}
69
70/// A single field in an input shape.
71#[derive(Debug, Clone, Serialize, Deserialize)]
72pub struct FieldIr {
73    /// Field name.
74    pub name: String,
75    /// Field type.
76    pub ty: TypeRef,
77    /// Whether this field is optional.
78    pub optional: bool,
79    /// Semantic role of the field. `#[serde(default)]` keeps deserialization
80    /// backward-compatible with IR persisted before this field existed.
81    #[serde(default)]
82    pub role: FieldRole,
83}
84
85/// Semantic role of a contract input field. Most fields are `Wire` (sent
86/// across the transport boundary). `SecurityContext` is server-injected and
87/// must NOT appear in proto wire schemas or in `OpenAPI` request bodies.
88#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
89#[non_exhaustive]
90pub enum FieldRole {
91    #[default]
92    Wire,
93    SecurityContext,
94}
95
96/// Reference to a type used in method signatures.
97#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
98#[non_exhaustive]
99pub enum TypeRef {
100    /// A primitive scalar type.
101    Primitive(PrimitiveType),
102    /// A named domain type.
103    Named(String),
104    /// An optional wrapper.
105    Optional(Box<TypeRef>),
106    /// A list/vector.
107    List(Box<TypeRef>),
108    /// A key-value map.
109    Map(Box<TypeRef>, Box<TypeRef>),
110}
111
112/// Primitive scalar types supported in contracts.
113#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
114#[non_exhaustive]
115pub enum PrimitiveType {
116    /// UTF-8 string.
117    String,
118    /// 32-bit signed integer.
119    I32,
120    /// 64-bit signed integer.
121    I64,
122    /// 64-bit unsigned integer.
123    U64,
124    /// 64-bit floating point.
125    F64,
126    /// Boolean.
127    Bool,
128    /// UUID.
129    Uuid,
130    /// Raw bytes.
131    Bytes,
132}
133
134/// Idempotency classification for retry policy decisions.
135#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
136#[non_exhaustive]
137pub enum Idempotency {
138    /// Safe read operation — always retriable.
139    SafeRead,
140    /// Idempotent write — retriable.
141    IdempotentWrite,
142    /// Non-idempotent write — not retriable without explicit strategy.
143    NonIdempotentWrite,
144}
145
146pub type ServiceIr = ContractIr;
147
148#[cfg(test)]
149mod tests {
150    use super::*;
151
152    #[test]
153    fn field_role_default_is_wire() {
154        assert_eq!(FieldRole::default(), FieldRole::Wire);
155    }
156
157    #[test]
158    fn field_ir_deserializes_without_role_defaults_to_wire() {
159        let json = r#"{
160            "name": "amount",
161            "ty": { "Primitive": "I64" },
162            "optional": false
163        }"#;
164        let f: FieldIr = serde_json::from_str(json).expect("deserialize FieldIr");
165        assert_eq!(f.role, FieldRole::Wire);
166        assert_eq!(f.name, "amount");
167    }
168
169    #[test]
170    fn field_ir_deserializes_with_explicit_role() {
171        let json = r#"{
172            "name": "ctx",
173            "ty": { "Named": "SecurityContext" },
174            "optional": false,
175            "role": "SecurityContext"
176        }"#;
177        let f: FieldIr = serde_json::from_str(json).expect("deserialize FieldIr");
178        assert_eq!(f.role, FieldRole::SecurityContext);
179    }
180}