1use 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
22const MAX_PROJECTION_NODES: usize = 4096;
23const MAX_PROJECTION_DEPTH: usize = 128;
24
25#[derive(
26 Clone,
27 Copy,
28 Debug,
29 Default,
30 PartialEq,
31 Eq,
32 Serialize,
33 Deserialize,
34 golem_schema_derive::IntoSchema,
35 golem_schema_derive::FromSchema,
36)]
37#[cfg_attr(
38 feature = "full",
39 derive(desert_rust::BinaryCodec, golem_schema_derive::PoemSchema)
40)]
41#[serde(rename_all = "kebab-case")]
42pub enum ToolCompatibilityMode {
43 StrictEquality,
44 #[default]
45 StructuralSubtype,
46 Nominal,
47}
48
49#[derive(
50 Clone,
51 Debug,
52 PartialEq,
53 Serialize,
54 Deserialize,
55 golem_schema_derive::IntoSchema,
56 golem_schema_derive::FromSchema,
57)]
58#[cfg_attr(
59 feature = "full",
60 derive(desert_rust::BinaryCodec, golem_schema_derive::PoemSchema)
61)]
62#[cfg_attr(feature = "full", desert(evolution()))]
63pub struct CompiledToolCompatibility {
64 pub mode: ToolCompatibilityMode,
65 pub commands: Vec<CompiledCommandCompatibility>,
66 pub warnings: Vec<ToolCompatibilityWarning>,
67}
68
69#[derive(
70 Clone,
71 Debug,
72 PartialEq,
73 Eq,
74 Serialize,
75 Deserialize,
76 golem_schema_derive::IntoSchema,
77 golem_schema_derive::FromSchema,
78)]
79#[cfg_attr(
80 feature = "full",
81 derive(desert_rust::BinaryCodec, golem_schema_derive::PoemSchema)
82)]
83#[cfg_attr(feature = "full", desert(evolution()))]
84pub struct ToolCompatibilityWarning {
85 pub path: String,
86 pub name: String,
87}
88
89#[derive(
90 Clone,
91 Debug,
92 PartialEq,
93 Serialize,
94 Deserialize,
95 golem_schema_derive::IntoSchema,
96 golem_schema_derive::FromSchema,
97)]
98#[cfg_attr(
99 feature = "full",
100 derive(desert_rust::BinaryCodec, golem_schema_derive::PoemSchema)
101)]
102#[cfg_attr(feature = "full", desert(evolution()))]
103pub struct CompiledCommandCompatibility {
104 pub expected_path: Vec<String>,
105 pub inner_path: Vec<String>,
106 pub input: ProjectionPlan,
107 pub result: Option<ProjectionPlan>,
108 pub errors: Vec<ErrorProjection>,
109 pub forward_unknown_errors: bool,
110}
111
112#[derive(
113 Clone,
114 Debug,
115 PartialEq,
116 Serialize,
117 Deserialize,
118 golem_schema_derive::IntoSchema,
119 golem_schema_derive::FromSchema,
120)]
121#[cfg_attr(
122 feature = "full",
123 derive(desert_rust::BinaryCodec, golem_schema_derive::PoemSchema)
124)]
125#[cfg_attr(feature = "full", desert(evolution()))]
126pub struct ProjectionPlan {
127 pub source_schema: SchemaGraph,
128 pub target_schema: SchemaGraph,
129 pub nodes: Vec<ProjectionNode>,
130 pub root: usize,
131}
132
133#[derive(
134 Clone,
135 Debug,
136 PartialEq,
137 Serialize,
138 Deserialize,
139 golem_schema_derive::IntoSchema,
140 golem_schema_derive::FromSchema,
141)]
142#[cfg_attr(
143 feature = "full",
144 derive(desert_rust::BinaryCodec, golem_schema_derive::PoemSchema)
145)]
146#[cfg_attr(feature = "full", desert(evolution()))]
147#[serde(tag = "kind", rename_all = "kebab-case")]
148pub enum ProjectionNode {
149 Identity,
150 DynamicChecked,
151 Record {
152 fields: Vec<RecordFieldProjection>,
153 discard: Vec<usize>,
154 },
155 Tuple {
156 elements: Vec<usize>,
157 },
158 List {
159 item: usize,
160 },
161 FixedList {
162 item: usize,
163 length: u32,
164 },
165 Map {
166 key: usize,
167 value: usize,
168 },
169 Option {
170 some: usize,
171 },
172 Result {
173 ok: Option<usize>,
174 err: Option<usize>,
175 },
176 Variant {
177 cases: Vec<CaseProjection>,
178 },
179 Enum {
180 cases: Vec<usize>,
182 },
183 Flags {
184 flags: Vec<usize>,
186 },
187 Union {
188 branches: Vec<CaseProjection>,
189 },
190 Stream {
191 item: Option<usize>,
192 },
193 Recursive {
194 node: usize,
195 },
196}
197
198#[derive(
199 Clone,
200 Debug,
201 PartialEq,
202 Serialize,
203 Deserialize,
204 golem_schema_derive::IntoSchema,
205 golem_schema_derive::FromSchema,
206)]
207#[cfg_attr(
208 feature = "full",
209 derive(desert_rust::BinaryCodec, golem_schema_derive::PoemSchema)
210)]
211#[cfg_attr(feature = "full", desert(evolution()))]
212pub struct RecordFieldProjection {
213 pub target_name: String,
214 pub source_index: Option<usize>,
215 pub plan: Option<usize>,
216 pub default: Option<SchemaValue>,
217}
218
219#[derive(
220 Clone,
221 Debug,
222 PartialEq,
223 Serialize,
224 Deserialize,
225 golem_schema_derive::IntoSchema,
226 golem_schema_derive::FromSchema,
227)]
228#[cfg_attr(
229 feature = "full",
230 derive(desert_rust::BinaryCodec, golem_schema_derive::PoemSchema)
231)]
232#[cfg_attr(feature = "full", desert(evolution()))]
233pub struct CaseProjection {
234 pub name: String,
235 pub source_index: usize,
236 pub target_index: usize,
237 pub payload: Option<usize>,
238}
239
240#[derive(
241 Clone,
242 Debug,
243 PartialEq,
244 Serialize,
245 Deserialize,
246 golem_schema_derive::IntoSchema,
247 golem_schema_derive::FromSchema,
248)]
249#[cfg_attr(
250 feature = "full",
251 derive(desert_rust::BinaryCodec, golem_schema_derive::PoemSchema)
252)]
253#[cfg_attr(feature = "full", desert(evolution()))]
254pub struct ErrorProjection {
255 pub name: String,
256 pub inner_index: usize,
257 pub expected_index: Option<usize>,
258 pub payload: Option<ProjectionPlan>,
259}
260
261#[derive(
262 Clone,
263 Debug,
264 PartialEq,
265 Eq,
266 Serialize,
267 Deserialize,
268 golem_schema_derive::IntoSchema,
269 golem_schema_derive::FromSchema,
270)]
271#[cfg_attr(
272 feature = "full",
273 derive(desert_rust::BinaryCodec, golem_schema_derive::PoemSchema)
274)]
275#[cfg_attr(feature = "full", desert(evolution()))]
276pub struct ToolCompatibilityError {
277 pub path: String,
278 pub message: String,
279}
280
281pub trait ProjectionStreamHandler {
286 fn project_stream(
287 &mut self,
288 stream: crate::schema::SchemaValueStream,
289 item_plan: Option<ProjectionPlan>,
290 ) -> Result<crate::schema::SchemaValueStream, String>;
291
292 fn discard_stream(&mut self, stream: crate::schema::SchemaValueStream);
293}
294
295pub fn apply_projection(
297 plan: &ProjectionPlan,
298 value: SchemaValue,
299 streams: &mut impl ProjectionStreamHandler,
300) -> Result<SchemaValue, ToolCompatibilityError> {
301 if let Err(errors) = validate_value(&plan.source_schema, &plan.source_schema.root, &value) {
302 let error = err(
303 "source",
304 format!("value does not match source schema: {errors:?}"),
305 );
306 Evaluator { plan, streams }.discard_value(value);
307 return Err(error);
308 }
309 let mut evaluator = Evaluator { plan, streams };
310 let result = evaluator.apply(
311 plan.root,
312 &plan.source_schema.root,
313 &plan.target_schema.root,
314 value,
315 )?;
316 if let Err(errors) = validate_value(&plan.target_schema, &plan.target_schema.root, &result) {
317 let error = err(
318 "target",
319 format!("projected value does not match target schema: {errors:?}"),
320 );
321 evaluator.discard_value(result);
322 return Err(error);
323 }
324 Ok(result)
325}
326
327struct Evaluator<'a, H> {
328 plan: &'a ProjectionPlan,
329 streams: &'a mut H,
330}
331
332impl<H: ProjectionStreamHandler> Evaluator<'_, H> {
333 fn apply(
334 &mut self,
335 node: usize,
336 st: &SchemaType,
337 tt: &SchemaType,
338 value: SchemaValue,
339 ) -> Result<SchemaValue, ToolCompatibilityError> {
340 let Some(projection) = self.plan.nodes.get(node).cloned() else {
341 self.discard_value(value);
342 return Err(err("plan", format!("invalid projection node {node}")));
343 };
344 let (st, _) = resolve(&self.plan.source_schema, st);
345 let (tt, _) = resolve(&self.plan.target_schema, tt);
346 let bad = || {
347 err(
348 "value",
349 format!("value shape does not match projection node {node}"),
350 )
351 };
352 let shapes_match = match &projection {
353 ProjectionNode::Identity
354 | ProjectionNode::DynamicChecked
355 | ProjectionNode::Recursive { .. } => true,
356 ProjectionNode::Record { .. } => {
357 matches!(
358 (st, tt),
359 (SchemaType::Record { .. }, SchemaType::Record { .. })
360 )
361 }
362 ProjectionNode::Tuple { .. } => {
363 matches!(
364 (st, tt),
365 (SchemaType::Tuple { .. }, SchemaType::Tuple { .. })
366 )
367 }
368 ProjectionNode::List { .. } => {
369 matches!((st, tt), (SchemaType::List { .. }, SchemaType::List { .. }))
370 }
371 ProjectionNode::FixedList { .. } => matches!(
372 (st, tt),
373 (SchemaType::FixedList { .. }, SchemaType::FixedList { .. })
374 ),
375 ProjectionNode::Map { .. } => {
376 matches!((st, tt), (SchemaType::Map { .. }, SchemaType::Map { .. }))
377 }
378 ProjectionNode::Option { .. } => {
379 matches!(
380 (st, tt),
381 (SchemaType::Option { .. }, SchemaType::Option { .. })
382 )
383 }
384 ProjectionNode::Result { .. } => {
385 matches!(
386 (st, tt),
387 (SchemaType::Result { .. }, SchemaType::Result { .. })
388 )
389 }
390 ProjectionNode::Variant { .. } => matches!(
391 (st, tt),
392 (SchemaType::Variant { .. }, SchemaType::Variant { .. })
393 ),
394 ProjectionNode::Enum { .. } => {
395 matches!((st, tt), (SchemaType::Enum { .. }, SchemaType::Enum { .. }))
396 }
397 ProjectionNode::Flags { .. } => {
398 matches!(
399 (st, tt),
400 (SchemaType::Flags { .. }, SchemaType::Flags { .. })
401 )
402 }
403 ProjectionNode::Union { .. } => {
404 matches!(
405 (st, tt),
406 (SchemaType::Union { .. }, SchemaType::Union { .. })
407 )
408 }
409 ProjectionNode::Stream { item } => matches!(
410 (item, st, tt),
411 (
412 Some(_),
413 SchemaType::Stream { inner: Some(_), .. },
414 SchemaType::Stream { inner: Some(_), .. },
415 ) | (None, SchemaType::Stream { .. }, SchemaType::Stream { .. })
416 ),
417 };
418 if !shapes_match {
419 self.discard_value(value);
420 return Err(err("plan", "projection node does not match its schemas"));
421 }
422 match projection {
423 ProjectionNode::Identity => Ok(value),
424 ProjectionNode::DynamicChecked => {
425 if let Err(errors) = validate_value(&self.plan.target_schema, tt, &value) {
426 let error = err(
427 "value",
428 format!("dynamic value does not match target subtree: {errors:?}"),
429 );
430 self.discard_value(value);
431 return Err(error);
432 }
433 Ok(value)
434 }
435 ProjectionNode::Recursive { node } => self.apply(node, st, tt, value),
436 ProjectionNode::Record { fields, discard } => {
437 let (SchemaType::Record { fields: sf, .. }, SchemaType::Record { fields: tf, .. }) =
438 (st, tt)
439 else {
440 self.discard_value(value);
441 return Err(bad());
442 };
443 let SchemaValue::Record { fields: values } = value else {
444 self.discard_value(value);
445 return Err(bad());
446 };
447 let mut values = values.into_iter().map(Some).collect::<Vec<_>>();
448 let mut claimed = HashSet::new();
449 let mut discarded = HashSet::new();
450 if fields.len() != tf.len()
451 || discard.iter().any(|&index| index >= values.len())
452 || discard.iter().any(|&index| !discarded.insert(index))
453 || fields.iter().enumerate().any(|(target_index, field)| {
454 target_index >= tf.len()
455 || match field.source_index {
456 Some(index) => {
457 index >= values.len()
458 || field.plan.is_none()
459 || field.default.is_some()
460 || !claimed.insert(index)
461 }
462 None => field.plan.is_some() || field.default.is_none(),
463 }
464 })
465 || discard.iter().any(|index| claimed.contains(index))
466 || claimed.len() + discarded.len() != values.len()
467 || sf.len() != values.len()
468 {
469 self.discard_remaining(values);
470 return Err(err("plan", "invalid record projection"));
471 }
472 for index in discard {
473 self.discard_value(values[index].take().unwrap());
474 }
475 let mut output = Vec::with_capacity(fields.len());
476 for (target_index, field) in fields.into_iter().enumerate() {
477 let value = match (field.source_index, field.plan, field.default) {
478 (Some(source_index), Some(child), None) => match self.apply(
479 child,
480 &sf[source_index].body,
481 &tf[target_index].body,
482 values[source_index].take().unwrap(),
483 ) {
484 Ok(value) => value,
485 Err(error) => {
486 self.discard_remaining(output);
487 self.discard_remaining(values);
488 return Err(error);
489 }
490 },
491 (None, None, Some(default)) => default,
492 _ => unreachable!("record projection was validated"),
493 };
494 output.push(value);
495 }
496 Ok(SchemaValue::Record { fields: output })
497 }
498 ProjectionNode::Tuple { elements } => {
499 let (
500 SchemaType::Tuple { elements: ss, .. },
501 SchemaType::Tuple { elements: ts, .. },
502 SchemaValue::Tuple { elements: values },
503 ) = (st, tt, value)
504 else {
505 return Err(bad());
506 };
507 Ok(SchemaValue::Tuple {
508 elements: self.sequence(elements, ss, ts, values)?,
509 })
510 }
511 ProjectionNode::List { item } | ProjectionNode::FixedList { item, .. } => {
512 let (ss, ts, values, fixed) = match (st, tt, value) {
513 (
514 SchemaType::List { element: ss, .. },
515 SchemaType::List { element: ts, .. },
516 SchemaValue::List { elements },
517 ) => (ss.as_ref(), ts.as_ref(), elements, false),
518 (
519 SchemaType::FixedList { element: ss, .. },
520 SchemaType::FixedList { element: ts, .. },
521 SchemaValue::FixedList { elements },
522 ) => (ss.as_ref(), ts.as_ref(), elements, true),
523 _ => return Err(bad()),
524 };
525 let output = self.project_sequence(
526 std::iter::repeat_n(item, values.len()).collect(),
527 std::iter::repeat_n(ss, values.len()).collect(),
528 std::iter::repeat_n(ts, values.len()).collect(),
529 values,
530 )?;
531 Ok(if fixed {
532 SchemaValue::FixedList { elements: output }
533 } else {
534 SchemaValue::List { elements: output }
535 })
536 }
537 ProjectionNode::Map { key, value: vp } => {
538 let (
539 SchemaType::Map {
540 key: sk, value: sv, ..
541 },
542 SchemaType::Map {
543 key: tk, value: tv, ..
544 },
545 SchemaValue::Map { entries },
546 ) = (st, tt, value)
547 else {
548 return Err(bad());
549 };
550 let mut remaining = entries.into_iter().map(Some).collect::<Vec<_>>();
551 let mut output = Vec::with_capacity(remaining.len());
552 for i in 0..remaining.len() {
553 let (k, v) = remaining[i].take().unwrap();
554 let projected_key = match self.apply(key, sk, tk, k) {
555 Ok(key) => key,
556 Err(error) => {
557 self.discard_value(v);
558 self.discard_map_entries(output);
559 self.discard_map_entries(remaining.into_iter().flatten());
560 return Err(error);
561 }
562 };
563 match self.apply(vp, sv, tv, v) {
564 Ok(value) => output.push((projected_key, value)),
565 Err(error) => {
566 self.discard_value(projected_key);
567 self.discard_map_entries(output);
568 self.discard_map_entries(remaining.into_iter().flatten());
569 return Err(error);
570 }
571 }
572 }
573 Ok(SchemaValue::Map { entries: output })
574 }
575 ProjectionNode::Option { some } => {
576 let (
577 SchemaType::Option { inner: ss, .. },
578 SchemaType::Option { inner: ts, .. },
579 SchemaValue::Option { inner },
580 ) = (st, tt, value)
581 else {
582 return Err(bad());
583 };
584 Ok(SchemaValue::Option {
585 inner: inner
586 .map(|v| self.apply(some, ss, ts, *v).map(Box::new))
587 .transpose()?,
588 })
589 }
590 ProjectionNode::Result { ok, err: ep } => {
591 let (
592 SchemaType::Result { spec: ss, .. },
593 SchemaType::Result { spec: ts, .. },
594 SchemaValue::Result(payload),
595 ) = (st, tt, value)
596 else {
597 return Err(bad());
598 };
599 let payload = match payload {
600 ResultValuePayload::Ok { value } => ResultValuePayload::Ok {
601 value: self.optional_payload(
602 ok,
603 ss.ok.as_deref(),
604 ts.ok.as_deref(),
605 value,
606 )?,
607 },
608 ResultValuePayload::Err { value } => ResultValuePayload::Err {
609 value: self.optional_payload(
610 ep,
611 ss.err.as_deref(),
612 ts.err.as_deref(),
613 value,
614 )?,
615 },
616 };
617 Ok(SchemaValue::Result(payload))
618 }
619 ProjectionNode::Variant { cases } => {
620 let (
621 SchemaType::Variant { cases: ss, .. },
622 SchemaType::Variant { cases: ts, .. },
623 SchemaValue::Variant(payload),
624 ) = (st, tt, value)
625 else {
626 return Err(bad());
627 };
628 let case = cases
629 .iter()
630 .find(|c| c.source_index == payload.case as usize)
631 .cloned();
632 let Some(case) = case else {
633 self.discard_remaining(payload.payload.map(|value| *value));
634 return Err(bad());
635 };
636 let (Some(source_case), Some(target_case)) =
637 (ss.get(case.source_index), ts.get(case.target_index))
638 else {
639 self.discard_remaining(payload.payload.map(|value| *value));
640 return Err(err("plan", "invalid variant case projection"));
641 };
642 Ok(SchemaValue::Variant(VariantValuePayload {
643 case: case.target_index as u32,
644 payload: self.optional_payload(
645 case.payload,
646 source_case.payload.as_ref(),
647 target_case.payload.as_ref(),
648 payload.payload,
649 )?,
650 }))
651 }
652 ProjectionNode::Union { branches } => {
653 let (
654 SchemaType::Union { spec: ss, .. },
655 SchemaType::Union { spec: ts, .. },
656 SchemaValue::Union(payload),
657 ) = (st, tt, value)
658 else {
659 return Err(bad());
660 };
661 let case = branches.iter().find(|c| {
662 ss.branches
663 .get(c.source_index)
664 .is_some_and(|branch| branch.tag == payload.tag)
665 });
666 let Some(case) = case else {
667 self.discard_value(*payload.body);
668 return Err(bad());
669 };
670 let (Some(source_branch), Some(target_branch)) = (
671 ss.branches.get(case.source_index),
672 ts.branches.get(case.target_index),
673 ) else {
674 self.discard_value(*payload.body);
675 return Err(err("plan", "invalid union branch projection"));
676 };
677 let Some(child) = case.payload else {
678 self.discard_value(*payload.body);
679 return Err(err("plan", "union branch has no payload plan"));
680 };
681 Ok(SchemaValue::Union(UnionValuePayload {
682 tag: target_branch.tag.clone(),
683 body: Box::new(self.apply(
684 child,
685 &source_branch.body,
686 &target_branch.body,
687 *payload.body,
688 )?),
689 }))
690 }
691 ProjectionNode::Enum { cases } => match value {
692 SchemaValue::Enum { case } => Ok(SchemaValue::Enum {
693 case: *cases.get(case as usize).ok_or_else(bad)? as u32,
694 }),
695 _ => Err(bad()),
696 },
697 ProjectionNode::Flags { flags } => match value {
698 SchemaValue::Flags { bits } => {
699 let target_len = match tt {
700 SchemaType::Flags { flags, .. } => flags.len(),
701 _ => return Err(bad()),
702 };
703 let mut out = vec![false; target_len];
704 for (i, bit) in bits.into_iter().enumerate() {
705 if bit {
706 let Some(&target) = flags.get(i) else {
707 return Err(bad());
708 };
709 let Some(target_bit) = out.get_mut(target) else {
710 return Err(err("plan", "invalid target flag index"));
711 };
712 *target_bit = true;
713 }
714 }
715 Ok(SchemaValue::Flags { bits: out })
716 }
717 _ => Err(bad()),
718 },
719 ProjectionNode::Stream { item } => match value {
720 SchemaValue::Stream(stream) => {
721 let item_plan = match (item, st, tt) {
722 (
723 Some(root),
724 SchemaType::Stream {
725 inner: Some(ss), ..
726 },
727 SchemaType::Stream {
728 inner: Some(ts), ..
729 },
730 ) => Some(ProjectionPlan {
731 source_schema: graph_for(&self.plan.source_schema, ss),
732 target_schema: graph_for(&self.plan.target_schema, ts),
733 nodes: self.plan.nodes.clone(),
734 root,
735 }),
736 (None, _, _) => {
737 return Ok(SchemaValue::Stream(stream));
738 }
739 _ => {
740 self.streams.discard_stream(stream);
741 return Err(err(
742 "plan",
743 "stream item projection does not match stream schemas",
744 ));
745 }
746 };
747 self.streams
748 .project_stream(stream, item_plan)
749 .map(SchemaValue::Stream)
750 .map_err(|e| err("stream", e))
751 }
752 _ => Err(bad()),
753 },
754 }
755 }
756
757 fn sequence(
758 &mut self,
759 plans: Vec<usize>,
760 ss: &[SchemaType],
761 ts: &[SchemaType],
762 values: Vec<SchemaValue>,
763 ) -> Result<Vec<SchemaValue>, ToolCompatibilityError> {
764 if plans.len() != values.len() || ss.len() != values.len() || ts.len() != values.len() {
765 self.discard_remaining(values);
766 return Err(err("value", "tuple length does not match projection"));
767 }
768 self.project_sequence(plans, ss.iter().collect(), ts.iter().collect(), values)
769 }
770 fn project_sequence(
771 &mut self,
772 plans: Vec<usize>,
773 ss: Vec<&SchemaType>,
774 ts: Vec<&SchemaType>,
775 values: Vec<SchemaValue>,
776 ) -> Result<Vec<SchemaValue>, ToolCompatibilityError> {
777 let mut remaining = values.into_iter().map(Some).collect::<Vec<_>>();
778 let mut output = Vec::with_capacity(remaining.len());
779 for i in 0..remaining.len() {
780 match self.apply(plans[i], ss[i], ts[i], remaining[i].take().unwrap()) {
781 Ok(value) => output.push(value),
782 Err(error) => {
783 self.discard_remaining(output);
784 self.discard_remaining(remaining);
785 return Err(error);
786 }
787 }
788 }
789 Ok(output)
790 }
791 fn discard_remaining<T: IntoIterator<Item = V>, V: IntoRemainingValue>(&mut self, values: T) {
792 for value in values {
793 if let Some(value) = value.into_remaining_value() {
794 self.discard_value(value);
795 }
796 }
797 }
798 fn discard_map_entries(
799 &mut self,
800 entries: impl IntoIterator<Item = (SchemaValue, SchemaValue)>,
801 ) {
802 for (key, value) in entries {
803 self.discard_value(key);
804 self.discard_value(value);
805 }
806 }
807 fn optional_payload(
808 &mut self,
809 plan: Option<usize>,
810 ss: Option<&SchemaType>,
811 ts: Option<&SchemaType>,
812 value: Option<Box<SchemaValue>>,
813 ) -> Result<Option<Box<SchemaValue>>, ToolCompatibilityError> {
814 match (plan, ss, ts, value) {
815 (None, None, None, None) => Ok(None),
816 (Some(p), Some(s), Some(t), Some(v)) => self.apply(p, s, t, *v).map(Box::new).map(Some),
817 (_, _, _, value) => {
818 self.discard_remaining(value.map(|value| *value));
819 Err(err("value", "payload presence does not match projection"))
820 }
821 }
822 }
823 fn discard_value(&mut self, value: SchemaValue) {
824 match value {
825 SchemaValue::Stream(stream) => self.streams.discard_stream(stream),
826 SchemaValue::Record { fields }
827 | SchemaValue::Tuple { elements: fields }
828 | SchemaValue::List { elements: fields }
829 | SchemaValue::FixedList { elements: fields } => {
830 for v in fields {
831 self.discard_value(v);
832 }
833 }
834 SchemaValue::Map { entries } => {
835 for (k, v) in entries {
836 self.discard_value(k);
837 self.discard_value(v);
838 }
839 }
840 SchemaValue::Variant(v) => {
841 if let Some(value) = v.payload {
842 self.discard_value(*value);
843 }
844 }
845 SchemaValue::Option { inner: Some(value) } => {
846 self.discard_value(*value);
847 }
848 SchemaValue::Result(
849 ResultValuePayload::Ok { value } | ResultValuePayload::Err { value },
850 ) => {
851 if let Some(value) = value {
852 self.discard_value(*value);
853 }
854 }
855 SchemaValue::Union(v) => self.discard_value(*v.body),
856 _ => {}
857 }
858 }
859}
860
861trait IntoRemainingValue {
862 fn into_remaining_value(self) -> Option<SchemaValue>;
863}
864
865impl IntoRemainingValue for SchemaValue {
866 fn into_remaining_value(self) -> Option<SchemaValue> {
867 Some(self)
868 }
869}
870
871impl IntoRemainingValue for Option<SchemaValue> {
872 fn into_remaining_value(self) -> Option<SchemaValue> {
873 self
874 }
875}
876
877pub fn compile_tool_compatibility(
880 expected: &Tool,
881 inner: &Tool,
882 mode: ToolCompatibilityMode,
883) -> Result<CompiledToolCompatibility, Vec<ToolCompatibilityError>> {
884 let mut errors = Vec::new();
885 if let Err(es) = validate_tool(expected) {
886 errors.extend(es.into_iter().map(|e| err("expected", e.to_string())));
887 }
888 if let Err(es) = validate_tool(inner) {
889 errors.extend(es.into_iter().map(|e| err("inner", e.to_string())));
890 }
891 if !errors.is_empty() {
892 return Err(errors);
893 }
894
895 if expected.name() != inner.name() {
896 return Err(vec![err("name", "tool names differ")]);
897 }
898
899 if mode == ToolCompatibilityMode::StrictEquality && !strictly_equal(expected, inner) {
900 return Err(vec![err("tool", "tool descriptors are not strictly equal")]);
901 }
902
903 let expected_commands = command_paths(expected);
904 let inner_commands = command_paths(inner);
905 let mut commands = Vec::new();
906 let mut warnings = Vec::new();
907 for (path, &ei) in &expected_commands {
908 let Some(&ni) = inner_commands.get(path) else {
909 errors.push(err(
910 format_path(path),
911 "inner tool does not implement command",
912 ));
913 continue;
914 };
915 let eb = expected.commands.nodes[ei]
916 .body
917 .as_ref()
918 .expect("body command");
919 let nb = inner.commands.nodes[ni]
920 .body
921 .as_ref()
922 .expect("body command");
923 if mode == ToolCompatibilityMode::StrictEquality && eb.constraints != nb.constraints {
924 errors.push(err(format_path(path), "command constraints differ"));
925 continue;
926 }
927 if mode == ToolCompatibilityMode::StructuralSubtype
928 && !nb.constraints.iter().all(|c| eb.constraints.contains(c))
929 {
930 errors.push(err(
931 format_path(path),
932 "inner command adds a constraint not guaranteed by the expected command",
933 ));
934 continue;
935 }
936 if !stream_specs_equal(&eb.stdin, &nb.stdin) {
937 errors.push(err(
938 format!("{}.stdin", format_path(path)),
939 "standard-input stream contracts differ",
940 ));
941 continue;
942 }
943 if !stream_specs_equal(&eb.stdout, &nb.stdout) {
944 errors.push(err(
945 format!("{}.stdout", format_path(path)),
946 "standard-output stream contracts differ",
947 ));
948 continue;
949 }
950 if !stream_specs_equal(&eb.stderr, &nb.stderr) {
951 errors.push(err(
952 format!("{}.stderr", format_path(path)),
953 "standard-error stream contracts differ",
954 ));
955 continue;
956 }
957 let input = compile_inputs(
958 expected,
959 ei,
960 inner,
961 ni,
962 mode,
963 path,
964 &mut errors,
965 &mut warnings,
966 );
967 let result = compile_results(expected, eb, inner, nb, mode, path, &mut errors);
968 let mapped_errors = compile_errors(expected, eb, inner, nb, mode, path, &mut errors);
969 if let Some(input) = input {
970 commands.push(CompiledCommandCompatibility {
971 expected_path: path.clone(),
972 inner_path: path.clone(),
973 input,
974 result,
975 errors: mapped_errors,
976 forward_unknown_errors: false,
977 });
978 }
979 }
980 if mode == ToolCompatibilityMode::StrictEquality {
981 for path in inner_commands.keys() {
982 if !expected_commands.contains_key(path) {
983 errors.push(err(
984 format_path(path),
985 "inner tool has an additional command",
986 ));
987 }
988 }
989 }
990 if errors.is_empty() {
991 Ok(CompiledToolCompatibility {
992 mode,
993 commands,
994 warnings,
995 })
996 } else {
997 Err(errors)
998 }
999}
1000
1001fn stream_specs_equal(a: &Option<super::StreamSpec>, b: &Option<super::StreamSpec>) -> bool {
1002 match (a, b) {
1003 (None, None) => true,
1004 (Some(a), Some(b)) => a.mime == b.mime && a.required == b.required,
1005 _ => false,
1006 }
1007}
1008
1009fn compile_inputs(
1010 expected: &Tool,
1011 ei: usize,
1012 inner: &Tool,
1013 ni: usize,
1014 mode: ToolCompatibilityMode,
1015 path: &[String],
1016 errors: &mut Vec<ToolCompatibilityError>,
1017 warnings: &mut Vec<ToolCompatibilityWarning>,
1018) -> Option<ProjectionPlan> {
1019 let ef = expected.canonical_input_fields(ei);
1020 let nf = inner.canonical_input_fields(ni);
1021 let defaults = surface_defaults(inner, ni);
1022 let egraph = match expected.canonical_input_record_schema(ei) {
1023 Ok(graph) => graph,
1024 Err(error) => {
1025 errors.push(err(
1026 format_path(path),
1027 format!("failed to build expected canonical input: {error}"),
1028 ));
1029 return None;
1030 }
1031 };
1032 let ngraph = match inner.canonical_input_record_schema(ni) {
1033 Ok(graph) => graph,
1034 Err(error) => {
1035 errors.push(err(
1036 format_path(path),
1037 format!("failed to build inner canonical input: {error}"),
1038 ));
1039 return None;
1040 }
1041 };
1042 if mode == ToolCompatibilityMode::StrictEquality
1043 && ef.iter().map(|f| &f.name).ne(nf.iter().map(|f| &f.name))
1044 {
1045 errors.push(err(format_path(path), "canonical input fields differ"));
1046 return None;
1047 }
1048 let mut compiler = Compiler::new(&egraph, &ngraph, mode);
1049 let mut fields = Vec::new();
1050 let mut retained = HashSet::new();
1051 for (target, default) in nf.iter().zip(defaults) {
1052 if let Some((source_index, source)) =
1053 ef.iter().enumerate().find(|(_, f)| f.name == target.name)
1054 {
1055 retained.insert(source_index);
1056 let plan = compiler.compile(
1057 &source.type_,
1058 &target.type_,
1059 &format!("{}.input.{}", format_path(path), target.name),
1060 errors,
1061 )?;
1062 fields.push(RecordFieldProjection {
1063 target_name: target.name.clone(),
1064 source_index: Some(source_index),
1065 plan: Some(plan),
1066 default: None,
1067 });
1068 } else if let Some(default) = default.or_else(|| option_none(&target.type_, &ngraph)) {
1069 fields.push(RecordFieldProjection {
1070 target_name: target.name.clone(),
1071 source_index: None,
1072 plan: None,
1073 default: Some(default),
1074 });
1075 } else {
1076 errors.push(err(
1077 format!("{}.input.{}", format_path(path), target.name),
1078 "inner-only input has no representable default",
1079 ));
1080 return None;
1081 }
1082 }
1083 let discard: Vec<_> = (0..ef.len()).filter(|i| !retained.contains(i)).collect();
1084 warnings.extend(discard.iter().map(|&index| ToolCompatibilityWarning {
1085 path: format_path(path),
1086 name: ef[index].name.clone(),
1087 }));
1088 let root = compiler.push(
1089 ProjectionNode::Record { fields, discard },
1090 &format!("{}.input", format_path(path)),
1091 errors,
1092 )?;
1093 Some(compiler.finish(egraph.clone(), ngraph.clone(), root))
1094}
1095
1096fn compile_results(
1097 expected: &Tool,
1098 eb: &CommandBody,
1099 inner: &Tool,
1100 nb: &CommandBody,
1101 mode: ToolCompatibilityMode,
1102 path: &[String],
1103 errors: &mut Vec<ToolCompatibilityError>,
1104) -> Option<ProjectionPlan> {
1105 match (&nb.result, &eb.result) {
1106 (None, None) => None,
1107 (Some(n), Some(e)) => compile_plan(
1108 &inner.schema,
1109 &n.type_,
1110 &expected.schema,
1111 &e.type_,
1112 mode,
1113 &format!("{}.result", format_path(path)),
1114 errors,
1115 ),
1116 _ => {
1117 errors.push(err(
1118 format!("{}.result", format_path(path)),
1119 "result presence differs",
1120 ));
1121 None
1122 }
1123 }
1124}
1125
1126fn compile_errors(
1127 expected: &Tool,
1128 eb: &CommandBody,
1129 inner: &Tool,
1130 nb: &CommandBody,
1131 mode: ToolCompatibilityMode,
1132 path: &[String],
1133 errors: &mut Vec<ToolCompatibilityError>,
1134) -> Vec<ErrorProjection> {
1135 if mode == ToolCompatibilityMode::StrictEquality
1136 && eb
1137 .errors
1138 .iter()
1139 .map(|e| &e.name)
1140 .ne(nb.errors.iter().map(|e| &e.name))
1141 {
1142 errors.push(err(
1143 format!("{}.errors", format_path(path)),
1144 "declared error cases differ",
1145 ));
1146 return Vec::new();
1147 }
1148 if matches!(
1149 mode,
1150 ToolCompatibilityMode::StructuralSubtype | ToolCompatibilityMode::Nominal
1151 ) {
1152 for inner_error in &nb.errors {
1153 if !eb
1154 .errors
1155 .iter()
1156 .any(|expected_error| expected_error.name == inner_error.name)
1157 {
1158 errors.push(err(
1159 format!("{}.error.{}", format_path(path), inner_error.name),
1160 "inner tool declares an error case absent from the expected tool",
1161 ));
1162 }
1163 }
1164 }
1165 nb.errors
1166 .iter()
1167 .enumerate()
1168 .map(|(i, n)| {
1169 let found = eb.errors.iter().enumerate().find(|(_, e)| e.name == n.name);
1170 let (expected_index, payload) = match found {
1171 Some((j, e)) => {
1172 if mode != ToolCompatibilityMode::Nominal
1173 && (n.kind != e.kind || n.exit_code != e.exit_code)
1174 {
1175 errors.push(err(
1176 format!("{}.error.{}", format_path(path), n.name),
1177 "error kind or exit code differs",
1178 ));
1179 }
1180 (
1181 Some(j),
1182 compile_error_payload(inner, n, expected, e, mode, path, errors),
1183 )
1184 }
1185 None => (None, None),
1186 };
1187 ErrorProjection {
1188 name: n.name.clone(),
1189 inner_index: i,
1190 expected_index,
1191 payload,
1192 }
1193 })
1194 .collect()
1195}
1196
1197fn compile_error_payload(
1198 source: &Tool,
1199 s: &ErrorCase,
1200 target: &Tool,
1201 t: &ErrorCase,
1202 mode: ToolCompatibilityMode,
1203 path: &[String],
1204 errors: &mut Vec<ToolCompatibilityError>,
1205) -> Option<ProjectionPlan> {
1206 match (&s.payload, &t.payload) {
1207 (None, None) => None,
1208 (Some(a), Some(b)) => compile_plan(
1209 &source.schema,
1210 a,
1211 &target.schema,
1212 b,
1213 mode,
1214 &format!("{}.error.{}", format_path(path), s.name),
1215 errors,
1216 ),
1217 _ => {
1218 errors.push(err(
1219 format!("{}.error.{}", format_path(path), s.name),
1220 "known error payload presence differs",
1221 ));
1222 None
1223 }
1224 }
1225}
1226
1227fn compile_plan(
1228 sg: &SchemaGraph,
1229 s: &SchemaType,
1230 tg: &SchemaGraph,
1231 t: &SchemaType,
1232 mode: ToolCompatibilityMode,
1233 path: &str,
1234 errors: &mut Vec<ToolCompatibilityError>,
1235) -> Option<ProjectionPlan> {
1236 if mode == ToolCompatibilityMode::Nominal {
1237 return Some(dynamic_plan(graph_for(sg, s), graph_for(tg, t)));
1238 }
1239 let mut c = Compiler::new(sg, tg, mode);
1240 let root = c.compile(s, t, path, errors)?;
1241 Some(c.finish(graph_for(sg, s), graph_for(tg, t), root))
1242}
1243
1244struct Compiler<'a> {
1245 sg: &'a SchemaGraph,
1246 tg: &'a SchemaGraph,
1247 mode: ToolCompatibilityMode,
1248 nodes: Vec<ProjectionNode>,
1249 active: HashMap<(TypeId, TypeId), usize>,
1250 completed: HashMap<(TypeId, TypeId), usize>,
1251}
1252impl<'a> Compiler<'a> {
1253 fn new(sg: &'a SchemaGraph, tg: &'a SchemaGraph, mode: ToolCompatibilityMode) -> Self {
1254 Self {
1255 sg,
1256 tg,
1257 mode,
1258 nodes: Vec::new(),
1259 active: HashMap::new(),
1260 completed: HashMap::new(),
1261 }
1262 }
1263 fn push(
1264 &mut self,
1265 n: ProjectionNode,
1266 path: &str,
1267 errors: &mut Vec<ToolCompatibilityError>,
1268 ) -> Option<usize> {
1269 if self.nodes.len() >= MAX_PROJECTION_NODES {
1270 errors.push(err(path, "projection plan exceeds node limit"));
1271 return None;
1272 }
1273 let i = self.nodes.len();
1274 self.nodes.push(n);
1275 Some(i)
1276 }
1277 fn finish(
1278 self,
1279 source_schema: SchemaGraph,
1280 target_schema: SchemaGraph,
1281 root: usize,
1282 ) -> ProjectionPlan {
1283 ProjectionPlan {
1284 source_schema,
1285 target_schema,
1286 nodes: self.nodes,
1287 root,
1288 }
1289 }
1290 fn compile(
1291 &mut self,
1292 source: &SchemaType,
1293 target: &SchemaType,
1294 path: &str,
1295 errors: &mut Vec<ToolCompatibilityError>,
1296 ) -> Option<usize> {
1297 self.compile_at(source, target, path, errors, 0)
1298 }
1299 fn compile_at(
1300 &mut self,
1301 source: &SchemaType,
1302 target: &SchemaType,
1303 path: &str,
1304 errors: &mut Vec<ToolCompatibilityError>,
1305 depth: usize,
1306 ) -> Option<usize> {
1307 if depth >= MAX_PROJECTION_DEPTH {
1308 errors.push(err(path, "projection plan exceeds depth limit"));
1309 return None;
1310 }
1311 if self.mode == ToolCompatibilityMode::Nominal {
1312 return self.push(ProjectionNode::DynamicChecked, path, errors);
1313 }
1314 if self.mode == ToolCompatibilityMode::StrictEquality
1315 && is_equivalent_cross_graph(self.sg, source, self.tg, target)
1316 {
1317 return self.push(ProjectionNode::Identity, path, errors);
1318 }
1319 let (s, sid) = resolve(self.sg, source);
1320 let (t, tid) = resolve(self.tg, target);
1321 if let (Some(a), Some(b)) = (sid, tid) {
1322 let pair = (a.clone(), b.clone());
1323 if let Some(&node) = self.completed.get(&pair) {
1324 return Some(node);
1325 }
1326 if let Some(&node) = self.active.get(&(a.clone(), b.clone())) {
1327 return self.push(ProjectionNode::Recursive { node }, path, errors);
1328 }
1329 let placeholder = self.push(ProjectionNode::Identity, path, errors)?;
1330 self.active.insert(pair.clone(), placeholder);
1331 let body = self.compile_resolved(s, t, path, errors, depth + 1)?;
1332 self.nodes[placeholder] = ProjectionNode::Recursive { node: body };
1333 self.active.remove(&pair);
1334 self.completed.insert(pair, placeholder);
1335 return Some(placeholder);
1336 }
1337 self.compile_resolved(s, t, path, errors, depth + 1)
1338 }
1339 fn compile_resolved(
1340 &mut self,
1341 s: &SchemaType,
1342 t: &SchemaType,
1343 path: &str,
1344 errors: &mut Vec<ToolCompatibilityError>,
1345 depth: usize,
1346 ) -> Option<usize> {
1347 if self.mode == ToolCompatibilityMode::StrictEquality {
1348 errors.push(err(path, "types are not strictly equal"));
1349 return None;
1350 }
1351 let node = match (s, t) {
1352 (SchemaType::Record { fields: a, .. }, SchemaType::Record { fields: b, .. }) => {
1353 let mut fields = Vec::new();
1354 let mut used = HashSet::new();
1355 for bf in b {
1356 let Some((i, af)) = a.iter().enumerate().find(|(_, f)| f.name == bf.name)
1357 else {
1358 errors.push(err(
1359 format!("{path}.{}", bf.name),
1360 "source record lacks target field",
1361 ));
1362 return None;
1363 };
1364 used.insert(i);
1365 let p = self.compile_at(
1366 &af.body,
1367 &bf.body,
1368 &format!("{path}.{}", bf.name),
1369 errors,
1370 depth,
1371 )?;
1372 fields.push(RecordFieldProjection {
1373 target_name: bf.name.clone(),
1374 source_index: Some(i),
1375 plan: Some(p),
1376 default: None,
1377 });
1378 }
1379 ProjectionNode::Record {
1380 fields,
1381 discard: (0..a.len()).filter(|i| !used.contains(i)).collect(),
1382 }
1383 }
1384 (SchemaType::Tuple { elements: a, .. }, SchemaType::Tuple { elements: b, .. })
1385 if a.len() == b.len() =>
1386 {
1387 ProjectionNode::Tuple {
1388 elements: a
1389 .iter()
1390 .zip(b)
1391 .enumerate()
1392 .map(|(i, (x, y))| {
1393 self.compile_at(x, y, &format!("{path}[{i}]"), errors, depth)
1394 })
1395 .collect::<Option<_>>()?,
1396 }
1397 }
1398 (SchemaType::List { element: a, .. }, SchemaType::List { element: b, .. }) => {
1399 ProjectionNode::List {
1400 item: self.compile_at(a, b, &format!("{path}[]"), errors, depth)?,
1401 }
1402 }
1403 (
1404 SchemaType::FixedList {
1405 element: a,
1406 length: x,
1407 ..
1408 },
1409 SchemaType::FixedList {
1410 element: b,
1411 length: y,
1412 ..
1413 },
1414 ) if x == y => ProjectionNode::FixedList {
1415 item: self.compile_at(a, b, &format!("{path}[]"), errors, depth)?,
1416 length: *x,
1417 },
1418 (
1419 SchemaType::Map {
1420 key: ak, value: av, ..
1421 },
1422 SchemaType::Map {
1423 key: bk, value: bv, ..
1424 },
1425 ) => ProjectionNode::Map {
1426 key: if is_equivalent_cross_graph(self.sg, ak, self.tg, bk) {
1430 self.push(ProjectionNode::Identity, &format!("{path}.key"), errors)?
1431 } else {
1432 errors.push(err(
1433 format!("{path}.key"),
1434 "map key types are not equivalent",
1435 ));
1436 return None;
1437 },
1438 value: self.compile_at(av, bv, &format!("{path}.value"), errors, depth)?,
1439 },
1440 (SchemaType::Option { inner: a, .. }, SchemaType::Option { inner: b, .. }) => {
1441 ProjectionNode::Option {
1442 some: self.compile_at(a, b, &format!("{path}.some"), errors, depth)?,
1443 }
1444 }
1445 (SchemaType::Result { spec: a, .. }, SchemaType::Result { spec: b, .. }) => {
1446 ProjectionNode::Result {
1447 ok: pair(
1448 self,
1449 a.ok.as_deref(),
1450 b.ok.as_deref(),
1451 &format!("{path}.ok"),
1452 errors,
1453 depth,
1454 )?,
1455 err: pair(
1456 self,
1457 a.err.as_deref(),
1458 b.err.as_deref(),
1459 &format!("{path}.err"),
1460 errors,
1461 depth,
1462 )?,
1463 }
1464 }
1465 (SchemaType::Variant { cases: a, .. }, SchemaType::Variant { cases: b, .. }) => {
1466 ProjectionNode::Variant {
1467 cases: cases(self, a, b, path, errors, depth)?,
1468 }
1469 }
1470 (SchemaType::Enum { cases: a, .. }, SchemaType::Enum { cases: b, .. })
1471 if a.iter().all(|n| b.contains(n)) =>
1472 {
1473 ProjectionNode::Enum {
1474 cases: a
1475 .iter()
1476 .map(|n| b.iter().position(|x| x == n).unwrap())
1477 .collect(),
1478 }
1479 }
1480 (SchemaType::Flags { flags: a, .. }, SchemaType::Flags { flags: b, .. })
1481 if a.iter().all(|n| b.contains(n)) =>
1482 {
1483 ProjectionNode::Flags {
1484 flags: a
1485 .iter()
1486 .map(|n| b.iter().position(|x| x == n).unwrap())
1487 .collect(),
1488 }
1489 }
1490 (SchemaType::Union { spec: a, .. }, SchemaType::Union { spec: b, .. }) => {
1491 let ac: Vec<_> = a
1492 .branches
1493 .iter()
1494 .map(|x| crate::schema::schema_type::VariantCaseType {
1495 name: x.tag.clone(),
1496 payload: Some(x.body.clone()),
1497 metadata: Default::default(),
1498 })
1499 .collect();
1500 let bc: Vec<_> = b
1501 .branches
1502 .iter()
1503 .map(|x| crate::schema::schema_type::VariantCaseType {
1504 name: x.tag.clone(),
1505 payload: Some(x.body.clone()),
1506 metadata: Default::default(),
1507 })
1508 .collect();
1509 ProjectionNode::Union {
1510 branches: cases(self, &ac, &bc, path, errors, depth)?,
1511 }
1512 }
1513 (SchemaType::Stream { inner: a, .. }, SchemaType::Stream { inner: b, .. }) => {
1514 if matches!((a, b), (None, None))
1515 || matches!((a, b), (Some(a), Some(b)) if is_equivalent_cross_graph(self.sg, a, self.tg, b))
1516 {
1517 ProjectionNode::Identity
1518 } else {
1519 ProjectionNode::Stream {
1520 item: pair(
1521 self,
1522 a.as_deref(),
1523 b.as_deref(),
1524 &format!("{path}.item"),
1525 errors,
1526 depth,
1527 )?,
1528 }
1529 }
1530 }
1531 (SchemaType::Future { .. }, SchemaType::Future { .. }) => {
1532 errors.push(err(
1533 path,
1534 "future projection is unsupported at tool invocation boundaries",
1535 ));
1536 return None;
1537 }
1538 (SchemaType::Secret { spec: a, .. }, SchemaType::Secret { spec: b, .. }) if a == b => {
1539 ProjectionNode::Identity
1540 }
1541 (SchemaType::QuotaToken { spec: a, .. }, SchemaType::QuotaToken { spec: b, .. })
1542 if a == b =>
1543 {
1544 ProjectionNode::Identity
1545 }
1546 (
1547 SchemaType::PermissionCard { spec: a, .. },
1548 SchemaType::PermissionCard { spec: b, .. },
1549 ) if a == b => ProjectionNode::Identity,
1550 _ if scalar_assignable(s, t) => ProjectionNode::Identity,
1551 _ => {
1552 errors.push(err(
1553 path,
1554 "source type is not substitutable for target type",
1555 ));
1556 return None;
1557 }
1558 };
1559 self.push(node, path, errors)
1560 }
1561}
1562
1563fn pair(
1564 c: &mut Compiler<'_>,
1565 a: Option<&SchemaType>,
1566 b: Option<&SchemaType>,
1567 path: &str,
1568 e: &mut Vec<ToolCompatibilityError>,
1569 depth: usize,
1570) -> Option<Option<usize>> {
1571 match (a, b) {
1572 (None, None) => Some(None),
1573 (Some(x), Some(y)) => c.compile_at(x, y, path, e, depth).map(Some),
1574 _ => {
1575 e.push(err(path, "payload presence differs"));
1576 None
1577 }
1578 }
1579}
1580fn cases(
1581 c: &mut Compiler<'_>,
1582 a: &[crate::schema::schema_type::VariantCaseType],
1583 b: &[crate::schema::schema_type::VariantCaseType],
1584 path: &str,
1585 e: &mut Vec<ToolCompatibilityError>,
1586 depth: usize,
1587) -> Option<Vec<CaseProjection>> {
1588 a.iter()
1589 .enumerate()
1590 .map(|(i, x)| {
1591 let Some((j, y)) = b.iter().enumerate().find(|(_, z)| z.name == x.name) else {
1592 e.push(err(
1593 format!("{path}.{}", x.name),
1594 "target lacks source case",
1595 ));
1596 return None;
1597 };
1598 let payload = pair(
1599 c,
1600 x.payload.as_ref(),
1601 y.payload.as_ref(),
1602 &format!("{path}.{}", x.name),
1603 e,
1604 depth,
1605 )?;
1606 Some(CaseProjection {
1607 name: x.name.clone(),
1608 source_index: i,
1609 target_index: j,
1610 payload,
1611 })
1612 })
1613 .collect()
1614}
1615fn scalar_assignable(s: &SchemaType, t: &SchemaType) -> bool {
1616 let g = SchemaGraph::anonymous(s.clone());
1617 is_assignable(&g, s, t)
1618}
1619
1620fn strictly_equal(expected: &Tool, inner: &Tool) -> bool {
1621 canonical_tool(expected) == canonical_tool(inner)
1622}
1623
1624fn canonical_tool(tool: &Tool) -> Option<serde_json::Value> {
1625 fn discover(
1626 ty: &SchemaType,
1627 defs: &HashMap<TypeId, SchemaTypeDef>,
1628 ids: &mut HashMap<TypeId, TypeId>,
1629 order: &mut Vec<TypeId>,
1630 ) {
1631 walk_refs(ty, &mut |id| {
1632 if !ids.contains_key(id) {
1633 let canonical = TypeId::new(format!("$def{}", ids.len()));
1634 ids.insert(id.clone(), canonical);
1635 order.push(id.clone());
1636 if let Some(def) = defs.get(id) {
1637 discover(&def.body, defs, ids, order);
1638 }
1639 }
1640 });
1641 }
1642
1643 let mut result = tool.clone();
1644 erase_ignored_tool_documentation(&mut result);
1645 let defs: HashMap<_, _> = result
1646 .schema
1647 .defs
1648 .iter()
1649 .map(|def| (def.id.clone(), def.clone()))
1650 .collect();
1651 let mut ids = HashMap::new();
1652 let mut order = Vec::new();
1653 for root in tool_schema_roots(&result) {
1654 discover(root, &defs, &mut ids, &mut order);
1655 }
1656 discover(&result.schema.root, &defs, &mut ids, &mut order);
1657
1658 while ids.len() < defs.len() {
1662 let mut candidates = Vec::new();
1663 for id in defs.keys().filter(|id| !ids.contains_key(*id)) {
1664 let mut local_ids = ids.clone();
1665 let mut local_order = order.clone();
1666 discover(
1667 &SchemaType::ref_to(id.clone()),
1668 &defs,
1669 &mut local_ids,
1670 &mut local_order,
1671 );
1672 let mut bodies = Vec::new();
1673 for original in &local_order[order.len()..] {
1674 let mut def = defs.get(original)?.clone();
1675 def.id = local_ids.get(original)?.clone();
1676 rewrite_refs(&mut def.body, &local_ids);
1677 bodies.push(def);
1678 }
1679 candidates.push((serde_json::to_string(&bodies).ok()?, local_ids, local_order));
1680 }
1681 let (_, next_ids, next_order) = candidates.into_iter().min_by(|a, b| a.0.cmp(&b.0))?;
1682 ids = next_ids;
1683 order = next_order;
1684 }
1685
1686 for root in tool_schema_roots_mut(&mut result) {
1687 rewrite_refs(root, &ids);
1688 }
1689 rewrite_refs(&mut result.schema.root, &ids);
1690 result.schema.defs = order
1691 .into_iter()
1692 .map(|original| {
1693 let mut def = defs.get(&original)?.clone();
1694 def.id = ids.get(&original)?.clone();
1695 rewrite_refs(&mut def.body, &ids);
1696 Some(def)
1697 })
1698 .collect::<Option<Vec<_>>>()?;
1699 serde_json::to_value(result).ok()
1700}
1701
1702fn erase_ignored_tool_documentation(tool: &mut Tool) {
1703 tool.version.clear();
1704 for node in &mut tool.commands.nodes {
1705 node.doc = Default::default();
1706 for option in &mut node.globals.options {
1707 option.doc = Default::default();
1708 }
1709 for flag in &mut node.globals.flags {
1710 flag.doc = Default::default();
1711 }
1712 if let Some(body) = &mut node.body {
1713 for positional in &mut body.positionals.fixed {
1714 positional.doc = Default::default();
1715 }
1716 if let Some(tail) = &mut body.positionals.tail {
1717 tail.doc = Default::default();
1718 }
1719 for option in &mut body.options {
1720 option.doc = Default::default();
1721 }
1722 for flag in &mut body.flags {
1723 flag.doc = Default::default();
1724 }
1725 if let Some(stdin) = &mut body.stdin {
1726 stdin.doc = Default::default();
1727 }
1728 if let Some(stdout) = &mut body.stdout {
1729 stdout.doc = Default::default();
1730 }
1731 if let Some(stderr) = &mut body.stderr {
1732 stderr.doc = Default::default();
1733 }
1734 if let Some(result) = &mut body.result {
1735 result.doc = Default::default();
1736 for formatter in &mut result.formatters {
1737 formatter.doc = Default::default();
1738 }
1739 }
1740 for error in &mut body.errors {
1741 error.doc = Default::default();
1742 }
1743 }
1744 }
1745 for root in tool_schema_roots_mut(tool) {
1746 erase_schema_documentation(root);
1747 }
1748 erase_schema_documentation(&mut tool.schema.root);
1749 for def in &mut tool.schema.defs {
1750 erase_schema_documentation(&mut def.body);
1751 }
1752}
1753
1754fn erase_schema_documentation(ty: &mut SchemaType) {
1755 ty.metadata_mut().doc = None;
1756 match ty {
1757 SchemaType::Record { fields, .. } => fields.iter_mut().for_each(|field| {
1758 field.metadata.doc = None;
1759 erase_schema_documentation(&mut field.body);
1760 }),
1761 SchemaType::Variant { cases, .. } => cases.iter_mut().for_each(|case| {
1762 case.metadata.doc = None;
1763 if let Some(payload) = &mut case.payload {
1764 erase_schema_documentation(payload);
1765 }
1766 }),
1767 SchemaType::Tuple { elements, .. } => {
1768 elements.iter_mut().for_each(erase_schema_documentation)
1769 }
1770 SchemaType::List { element, .. } | SchemaType::FixedList { element, .. } => {
1771 erase_schema_documentation(element)
1772 }
1773 SchemaType::Map { key, value, .. } => {
1774 erase_schema_documentation(key);
1775 erase_schema_documentation(value);
1776 }
1777 SchemaType::Option { inner, .. }
1778 | SchemaType::Secret {
1779 spec: crate::schema::schema_type::SecretSpec { inner, .. },
1780 ..
1781 } => erase_schema_documentation(inner),
1782 SchemaType::Result { spec, .. } => {
1783 if let Some(ok) = &mut spec.ok {
1784 erase_schema_documentation(ok);
1785 }
1786 if let Some(err) = &mut spec.err {
1787 erase_schema_documentation(err);
1788 }
1789 }
1790 SchemaType::Union { spec, .. } => spec.branches.iter_mut().for_each(|branch| {
1791 branch.metadata.doc = None;
1792 erase_schema_documentation(&mut branch.body);
1793 }),
1794 SchemaType::Future { inner, .. } | SchemaType::Stream { inner, .. } => {
1795 if let Some(inner) = inner {
1796 erase_schema_documentation(inner);
1797 }
1798 }
1799 _ => {}
1800 }
1801}
1802
1803fn walk_refs(ty: &SchemaType, visit: &mut impl FnMut(&TypeId)) {
1804 match ty {
1805 SchemaType::Ref { id, .. } => visit(id),
1806 SchemaType::Record { fields, .. } => fields.iter().for_each(|f| walk_refs(&f.body, visit)),
1807 SchemaType::Variant { cases, .. } => cases
1808 .iter()
1809 .filter_map(|c| c.payload.as_ref())
1810 .for_each(|t| walk_refs(t, visit)),
1811 SchemaType::Tuple { elements, .. } => elements.iter().for_each(|t| walk_refs(t, visit)),
1812 SchemaType::List { element, .. } | SchemaType::FixedList { element, .. } => {
1813 walk_refs(element, visit)
1814 }
1815 SchemaType::Map { key, value, .. } => {
1816 walk_refs(key, visit);
1817 walk_refs(value, visit);
1818 }
1819 SchemaType::Option { inner, .. }
1820 | SchemaType::Secret {
1821 spec: crate::schema::schema_type::SecretSpec { inner, .. },
1822 ..
1823 } => walk_refs(inner, visit),
1824 SchemaType::Result { spec, .. } => {
1825 if let Some(t) = &spec.ok {
1826 walk_refs(t, visit)
1827 }
1828 if let Some(t) = &spec.err {
1829 walk_refs(t, visit)
1830 }
1831 }
1832 SchemaType::Union { spec, .. } => {
1833 spec.branches.iter().for_each(|b| walk_refs(&b.body, visit))
1834 }
1835 SchemaType::Future { inner, .. } | SchemaType::Stream { inner, .. } => {
1836 if let Some(t) = inner {
1837 walk_refs(t, visit)
1838 }
1839 }
1840 _ => {}
1841 }
1842}
1843
1844fn rewrite_refs(ty: &mut SchemaType, ids: &HashMap<TypeId, TypeId>) {
1845 match ty {
1846 SchemaType::Ref { id, .. } => {
1847 if let Some(new) = ids.get(id) {
1848 *id = new.clone();
1849 }
1850 }
1851 SchemaType::Record { fields, .. } => fields
1852 .iter_mut()
1853 .for_each(|f| rewrite_refs(&mut f.body, ids)),
1854 SchemaType::Variant { cases, .. } => cases
1855 .iter_mut()
1856 .filter_map(|c| c.payload.as_mut())
1857 .for_each(|t| rewrite_refs(t, ids)),
1858 SchemaType::Tuple { elements, .. } => {
1859 elements.iter_mut().for_each(|t| rewrite_refs(t, ids))
1860 }
1861 SchemaType::List { element, .. } | SchemaType::FixedList { element, .. } => {
1862 rewrite_refs(element, ids)
1863 }
1864 SchemaType::Map { key, value, .. } => {
1865 rewrite_refs(key, ids);
1866 rewrite_refs(value, ids);
1867 }
1868 SchemaType::Option { inner, .. }
1869 | SchemaType::Secret {
1870 spec: crate::schema::schema_type::SecretSpec { inner, .. },
1871 ..
1872 } => rewrite_refs(inner, ids),
1873 SchemaType::Result { spec, .. } => {
1874 if let Some(t) = &mut spec.ok {
1875 rewrite_refs(t, ids)
1876 }
1877 if let Some(t) = &mut spec.err {
1878 rewrite_refs(t, ids)
1879 }
1880 }
1881 SchemaType::Union { spec, .. } => spec
1882 .branches
1883 .iter_mut()
1884 .for_each(|b| rewrite_refs(&mut b.body, ids)),
1885 SchemaType::Future { inner, .. } | SchemaType::Stream { inner, .. } => {
1886 if let Some(t) = inner {
1887 rewrite_refs(t, ids)
1888 }
1889 }
1890 _ => {}
1891 }
1892}
1893
1894fn tool_schema_roots(tool: &Tool) -> Vec<&SchemaType> {
1895 let mut roots = Vec::new();
1896 for node in &tool.commands.nodes {
1897 collect_command_roots(&node.globals, node.body.as_ref(), &mut roots);
1898 }
1899 roots
1900}
1901
1902fn tool_schema_roots_mut(tool: &mut Tool) -> Vec<&mut SchemaType> {
1903 let mut roots = Vec::new();
1904 for node in &mut tool.commands.nodes {
1905 collect_command_roots_mut(&mut node.globals, node.body.as_mut(), &mut roots);
1906 }
1907 roots
1908}
1909
1910fn collect_command_roots<'a>(
1911 globals: &'a super::Globals,
1912 body: Option<&'a CommandBody>,
1913 roots: &mut Vec<&'a SchemaType>,
1914) {
1915 for option in &globals.options {
1916 roots.push(option_type(&option.shape));
1917 }
1918 if let Some(body) = body {
1919 roots.extend(body.positionals.fixed.iter().map(|p| &p.type_));
1920 if let Some(tail) = &body.positionals.tail {
1921 roots.push(&tail.item_type);
1922 }
1923 roots.extend(body.options.iter().map(|o| option_type(&o.shape)));
1924 if let Some(result) = &body.result {
1925 roots.push(&result.type_);
1926 }
1927 roots.extend(body.errors.iter().filter_map(|e| e.payload.as_ref()));
1928 }
1929}
1930
1931fn collect_command_roots_mut<'a>(
1932 globals: &'a mut super::Globals,
1933 body: Option<&'a mut CommandBody>,
1934 roots: &mut Vec<&'a mut SchemaType>,
1935) {
1936 for option in &mut globals.options {
1937 roots.push(option_type_mut(&mut option.shape));
1938 }
1939 if let Some(body) = body {
1940 roots.extend(body.positionals.fixed.iter_mut().map(|p| &mut p.type_));
1941 if let Some(tail) = &mut body.positionals.tail {
1942 roots.push(&mut tail.item_type);
1943 }
1944 roots.extend(
1945 body.options
1946 .iter_mut()
1947 .map(|o| option_type_mut(&mut o.shape)),
1948 );
1949 if let Some(result) = &mut body.result {
1950 roots.push(&mut result.type_);
1951 }
1952 roots.extend(body.errors.iter_mut().filter_map(|e| e.payload.as_mut()));
1953 }
1954}
1955
1956fn option_type(shape: &OptionShape) -> &SchemaType {
1957 match shape {
1958 OptionShape::Scalar(ty) | OptionShape::OptionalScalar(ty) => ty,
1959 OptionShape::RepeatableList(shape) => &shape.item_type,
1960 OptionShape::RepeatableMap(shape) => &shape.map_type,
1961 }
1962}
1963
1964fn option_type_mut(shape: &mut OptionShape) -> &mut SchemaType {
1965 match shape {
1966 OptionShape::Scalar(ty) | OptionShape::OptionalScalar(ty) => ty,
1967 OptionShape::RepeatableList(shape) => &mut shape.item_type,
1968 OptionShape::RepeatableMap(shape) => &mut shape.map_type,
1969 }
1970}
1971
1972fn resolve<'a>(g: &'a SchemaGraph, t: &'a SchemaType) -> (&'a SchemaType, Option<TypeId>) {
1973 let mut x = t;
1974 let mut id = None;
1975 let mut seen = HashSet::new();
1976 while let SchemaType::Ref { id: i, .. } = x {
1977 if !seen.insert(i.clone()) {
1978 break;
1979 }
1980 id = Some(i.clone());
1981 match g.defs.iter().find(|def| def.id == *i) {
1982 Some(def) => x = &def.body,
1983 None => break,
1984 }
1985 }
1986 (x, id)
1987}
1988fn graph_for(g: &SchemaGraph, t: &SchemaType) -> SchemaGraph {
1989 SchemaGraph {
1990 defs: crate::schema::graph::reachable_defs(g, t),
1991 root: t.clone(),
1992 }
1993}
1994fn dynamic_plan(source_schema: SchemaGraph, target_schema: SchemaGraph) -> ProjectionPlan {
1995 ProjectionPlan {
1996 source_schema,
1997 target_schema,
1998 nodes: vec![ProjectionNode::DynamicChecked],
1999 root: 0,
2000 }
2001}
2002fn option_none(t: &SchemaType, g: &SchemaGraph) -> Option<SchemaValue> {
2003 matches!(resolve(g, t).0, SchemaType::Option { .. })
2004 .then(|| SchemaValue::Option { inner: None })
2005}
2006fn surface_defaults(tool: &Tool, index: usize) -> Vec<Option<SchemaValue>> {
2007 tool.canonical_input_surfaces(index)
2008 .into_iter()
2009 .map(|s| match s {
2010 CanonicalSurfaceRef::GlobalOption { node, index } => {
2011 let option = &tool.commands.nodes[node].globals.options[index];
2012 option
2013 .default
2014 .clone()
2015 .or_else(|| collection_default(&option.shape))
2016 }
2017 CanonicalSurfaceRef::GlobalFlag { node, index } => Some(flag_default(
2018 tool.commands.nodes[node].globals.flags[index].shape,
2019 )),
2020 CanonicalSurfaceRef::BodyPositional { index: i } => tool.commands.nodes[index]
2021 .body
2022 .as_ref()
2023 .unwrap()
2024 .positionals
2025 .fixed[i]
2026 .default
2027 .clone(),
2028 CanonicalSurfaceRef::BodyOption { index: i } => {
2029 let option = &tool.commands.nodes[index].body.as_ref().unwrap().options[i];
2030 option
2031 .default
2032 .clone()
2033 .or_else(|| collection_default(&option.shape))
2034 }
2035 CanonicalSurfaceRef::BodyFlag { index: i } => Some(flag_default(
2036 tool.commands.nodes[index].body.as_ref().unwrap().flags[i].shape,
2037 )),
2038 CanonicalSurfaceRef::BodyTail => tool.commands.nodes[index]
2039 .body
2040 .as_ref()
2041 .unwrap()
2042 .positionals
2043 .tail
2044 .as_ref()
2045 .filter(|tail| tail.min == 0)
2046 .map(|_| SchemaValue::List { elements: vec![] }),
2047 })
2048 .collect()
2049}
2050fn collection_default(shape: &OptionShape) -> Option<SchemaValue> {
2051 match shape {
2052 OptionShape::RepeatableList(_) => Some(SchemaValue::List { elements: vec![] }),
2053 OptionShape::RepeatableMap(_) => Some(SchemaValue::Map { entries: vec![] }),
2054 OptionShape::Scalar(_) | OptionShape::OptionalScalar(_) => None,
2055 }
2056}
2057fn flag_default(shape: FlagShape) -> SchemaValue {
2058 match shape {
2059 FlagShape::BoolFlag(shape) => SchemaValue::Bool(shape.default),
2060 FlagShape::CountFlag(_) => SchemaValue::U32(0),
2061 }
2062}
2063fn command_paths(tool: &Tool) -> BTreeMap<Vec<String>, usize> {
2064 fn walk(t: &Tool, i: usize, p: &mut Vec<String>, r: &mut BTreeMap<Vec<String>, usize>) {
2065 let n = &t.commands.nodes[i];
2066 if n.body.is_some() {
2067 r.insert(p.clone(), i);
2068 }
2069 for c in &n.subcommands {
2070 let j = c.as_usize().unwrap();
2071 p.push(t.commands.nodes[j].name.clone());
2072 walk(t, j, p, r);
2073 p.pop();
2074 }
2075 }
2076 let mut r = BTreeMap::new();
2077 if !tool.commands.nodes.is_empty() {
2078 walk(tool, 0, &mut Vec::new(), &mut r)
2079 }
2080 r
2081}
2082fn format_path(p: &[String]) -> String {
2083 if p.is_empty() {
2084 "root".into()
2085 } else {
2086 p.join(".")
2087 }
2088}
2089fn err(path: impl Into<String>, message: impl Into<String>) -> ToolCompatibilityError {
2090 ToolCompatibilityError {
2091 path: path.into(),
2092 message: message.into(),
2093 }
2094}
2095
2096#[cfg(test)]
2097mod tests;