Skip to main content

golem_schema/schema/tool/compatibility/
mod.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
6//! Pure compiler for the directional value projections at a monomorphic
7//! middleware edge. Applying these plans, including disposal and stream relay,
8//! is a runtime responsibility.
9
10use super::canonical::CanonicalSurfaceRef;
11use super::{CommandBody, ErrorCase, FlagShape, OptionShape, Tool, validation::validate_tool};
12use crate::schema::graph::{SchemaGraph, SchemaTypeDef};
13use crate::schema::metadata::TypeId;
14use crate::schema::schema_type::SchemaType;
15use crate::schema::schema_value::SchemaValue;
16use crate::schema::schema_value::{ResultValuePayload, UnionValuePayload, VariantValuePayload};
17use crate::schema::validation::subtyping::{is_assignable, is_equivalent_cross_graph};
18use crate::schema::validation::validate_value;
19use serde::{Deserialize, Serialize};
20use std::collections::{BTreeMap, HashMap, HashSet};
21
22mod compiler;
23mod equality;
24mod evaluator;
25
26pub use compiler::compile_tool_compatibility;
27pub use evaluator::apply_projection;
28
29const MAX_PROJECTION_NODES: usize = 4096;
30const MAX_PROJECTION_DEPTH: usize = 128;
31
32#[derive(
33    Clone,
34    Copy,
35    Debug,
36    Default,
37    PartialEq,
38    Eq,
39    Serialize,
40    Deserialize,
41    golem_schema_derive::IntoSchema,
42    golem_schema_derive::FromSchema,
43)]
44#[cfg_attr(
45    feature = "full",
46    derive(desert_rust::BinaryCodec, golem_schema_derive::PoemSchema)
47)]
48#[serde(rename_all = "kebab-case")]
49pub enum ToolCompatibilityMode {
50    StrictEquality,
51    #[default]
52    StructuralSubtype,
53    Nominal,
54}
55
56#[derive(
57    Clone,
58    Debug,
59    PartialEq,
60    Serialize,
61    Deserialize,
62    golem_schema_derive::IntoSchema,
63    golem_schema_derive::FromSchema,
64)]
65#[cfg_attr(
66    feature = "full",
67    derive(desert_rust::BinaryCodec, golem_schema_derive::PoemSchema)
68)]
69#[cfg_attr(feature = "full", desert(evolution()))]
70pub struct CompiledToolCompatibility {
71    pub mode: ToolCompatibilityMode,
72    pub commands: Vec<CompiledCommandCompatibility>,
73    pub warnings: Vec<ToolCompatibilityWarning>,
74}
75
76#[derive(
77    Clone,
78    Debug,
79    PartialEq,
80    Eq,
81    Serialize,
82    Deserialize,
83    golem_schema_derive::IntoSchema,
84    golem_schema_derive::FromSchema,
85)]
86#[cfg_attr(
87    feature = "full",
88    derive(desert_rust::BinaryCodec, golem_schema_derive::PoemSchema)
89)]
90#[cfg_attr(feature = "full", desert(evolution()))]
91pub struct ToolCompatibilityWarning {
92    pub path: String,
93    pub name: String,
94}
95
96#[derive(
97    Clone,
98    Debug,
99    PartialEq,
100    Serialize,
101    Deserialize,
102    golem_schema_derive::IntoSchema,
103    golem_schema_derive::FromSchema,
104)]
105#[cfg_attr(
106    feature = "full",
107    derive(desert_rust::BinaryCodec, golem_schema_derive::PoemSchema)
108)]
109#[cfg_attr(feature = "full", desert(evolution()))]
110pub struct CompiledCommandCompatibility {
111    pub expected_path: Vec<String>,
112    pub inner_path: Vec<String>,
113    pub input: ProjectionPlan,
114    pub result: Option<ProjectionPlan>,
115    pub errors: Vec<ErrorProjection>,
116    pub forward_unknown_errors: bool,
117}
118
119#[derive(
120    Clone,
121    Debug,
122    PartialEq,
123    Serialize,
124    Deserialize,
125    golem_schema_derive::IntoSchema,
126    golem_schema_derive::FromSchema,
127)]
128#[cfg_attr(
129    feature = "full",
130    derive(desert_rust::BinaryCodec, golem_schema_derive::PoemSchema)
131)]
132#[cfg_attr(feature = "full", desert(evolution()))]
133pub struct ProjectionPlan {
134    pub source_schema: SchemaGraph,
135    pub target_schema: SchemaGraph,
136    pub nodes: Vec<ProjectionNode>,
137    pub root: usize,
138}
139
140#[derive(
141    Clone,
142    Debug,
143    PartialEq,
144    Serialize,
145    Deserialize,
146    golem_schema_derive::IntoSchema,
147    golem_schema_derive::FromSchema,
148)]
149#[cfg_attr(
150    feature = "full",
151    derive(desert_rust::BinaryCodec, golem_schema_derive::PoemSchema)
152)]
153#[cfg_attr(feature = "full", desert(evolution()))]
154#[serde(tag = "kind", rename_all = "kebab-case")]
155pub enum ProjectionNode {
156    Identity,
157    DynamicChecked,
158    Record {
159        fields: Vec<RecordFieldProjection>,
160        discard: Vec<usize>,
161    },
162    Tuple {
163        elements: Vec<usize>,
164    },
165    List {
166        item: usize,
167    },
168    FixedList {
169        item: usize,
170        length: u32,
171    },
172    Map {
173        key: usize,
174        value: usize,
175    },
176    Option {
177        some: usize,
178    },
179    Result {
180        ok: Option<usize>,
181        err: Option<usize>,
182    },
183    Variant {
184        cases: Vec<CaseProjection>,
185    },
186    Enum {
187        /// Target case index for each source case index.
188        cases: Vec<usize>,
189    },
190    Flags {
191        /// Target flag index for each source flag index.
192        flags: Vec<usize>,
193    },
194    Union {
195        branches: Vec<CaseProjection>,
196    },
197    Stream {
198        item: Option<usize>,
199    },
200    Recursive {
201        node: usize,
202    },
203}
204
205#[derive(
206    Clone,
207    Debug,
208    PartialEq,
209    Serialize,
210    Deserialize,
211    golem_schema_derive::IntoSchema,
212    golem_schema_derive::FromSchema,
213)]
214#[cfg_attr(
215    feature = "full",
216    derive(desert_rust::BinaryCodec, golem_schema_derive::PoemSchema)
217)]
218#[cfg_attr(feature = "full", desert(evolution()))]
219pub struct RecordFieldProjection {
220    pub target_name: String,
221    pub source_index: Option<usize>,
222    pub plan: Option<usize>,
223    pub default: Option<SchemaValue>,
224}
225
226#[derive(
227    Clone,
228    Debug,
229    PartialEq,
230    Serialize,
231    Deserialize,
232    golem_schema_derive::IntoSchema,
233    golem_schema_derive::FromSchema,
234)]
235#[cfg_attr(
236    feature = "full",
237    derive(desert_rust::BinaryCodec, golem_schema_derive::PoemSchema)
238)]
239#[cfg_attr(feature = "full", desert(evolution()))]
240pub struct CaseProjection {
241    pub name: String,
242    pub source_index: usize,
243    pub target_index: usize,
244    pub payload: Option<usize>,
245}
246
247#[derive(
248    Clone,
249    Debug,
250    PartialEq,
251    Serialize,
252    Deserialize,
253    golem_schema_derive::IntoSchema,
254    golem_schema_derive::FromSchema,
255)]
256#[cfg_attr(
257    feature = "full",
258    derive(desert_rust::BinaryCodec, golem_schema_derive::PoemSchema)
259)]
260#[cfg_attr(feature = "full", desert(evolution()))]
261pub struct ErrorProjection {
262    pub name: String,
263    pub inner_index: usize,
264    pub expected_index: Option<usize>,
265    pub payload: Option<ProjectionPlan>,
266}
267
268#[derive(
269    Clone,
270    Debug,
271    PartialEq,
272    Eq,
273    Serialize,
274    Deserialize,
275    golem_schema_derive::IntoSchema,
276    golem_schema_derive::FromSchema,
277)]
278#[cfg_attr(
279    feature = "full",
280    derive(desert_rust::BinaryCodec, golem_schema_derive::PoemSchema)
281)]
282#[cfg_attr(feature = "full", desert(evolution()))]
283pub struct ToolCompatibilityError {
284    pub path: String,
285    pub message: String,
286}
287
288/// Runtime-independent hooks needed while applying a projection. Streams are
289/// affine, so the evaluator transfers each retained stream to `project_stream`
290/// and every stream below a discarded value to `discard_stream`. On error,
291/// `project_stream` must dispose the stream it consumed.
292pub trait ProjectionStreamHandler {
293    fn project_stream(
294        &mut self,
295        stream: crate::schema::SchemaValueStream,
296        item_plan: Option<ProjectionPlan>,
297    ) -> Result<crate::schema::SchemaValueStream, String>;
298
299    fn discard_stream(&mut self, stream: crate::schema::SchemaValueStream);
300}
301
302fn resolve<'a>(g: &'a SchemaGraph, t: &'a SchemaType) -> (&'a SchemaType, Option<TypeId>) {
303    let mut x = t;
304    let mut id = None;
305    let mut seen = HashSet::new();
306    while let SchemaType::Ref { id: i, .. } = x {
307        if !seen.insert(i.clone()) {
308            break;
309        }
310        id = Some(i.clone());
311        match g.defs.iter().find(|def| def.id == *i) {
312            Some(def) => x = &def.body,
313            None => break,
314        }
315    }
316    (x, id)
317}
318fn graph_for(g: &SchemaGraph, t: &SchemaType) -> SchemaGraph {
319    SchemaGraph {
320        defs: crate::schema::graph::reachable_defs(g, t),
321        root: t.clone(),
322    }
323}
324fn err(path: impl Into<String>, message: impl Into<String>) -> ToolCompatibilityError {
325    ToolCompatibilityError {
326        path: path.into(),
327        message: message.into(),
328    }
329}
330
331#[cfg(test)]
332mod tests;