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}