golem_schema/schema/render/walker.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
15//! Dynamic walker skeleton operating over `(SchemaGraph, SchemaType,
16//! SchemaValue)`.
17//!
18//! The walker is the single public traversal API consumed by every renderer
19//! in this module tree. It enforces the rule that a value never travels
20//! without the schema that types it; the walker resolves named
21//! [`crate::schema::SchemaType::Ref`] hops transparently and protects
22//! against reference cycles by tracking the set of currently-active type
23//! identifiers.
24//!
25//! The same ref-resolution / cycle-protection logic is exposed as
26//! [`resolve_ref`] so decoders that consume `serde_json::Value` (and other
27//! non-`SchemaValue` inputs) can reuse it without duplicating the body of
28//! the walker.
29
30use crate::schema::graph::SchemaGraph;
31use crate::schema::metadata::TypeId;
32use crate::schema::schema_type::SchemaType;
33use crate::schema::schema_value::SchemaValue;
34use std::collections::HashSet;
35
36/// Driver-friendly walker over a typed value tree.
37///
38/// Implementations focus on the actual per-shape logic; the top-level
39/// [`walk`] function handles ref resolution and cycle protection before
40/// dispatching back into [`SchemaWalker::walk`] on the resolved body.
41pub trait SchemaWalker {
42 /// The result produced when a value tree is fully consumed.
43 type Output;
44 /// The error produced when the walker rejects a sub-tree.
45 type Error;
46
47 /// Walk one (sub-)value of a given type in the context of `graph`.
48 ///
49 /// Implementations may freely call back into [`walk`] (the free function
50 /// in this module) when they recurse into child nodes; that keeps ref
51 /// resolution centralised.
52 fn walk(
53 &mut self,
54 graph: &SchemaGraph,
55 ty: &SchemaType,
56 value: &SchemaValue,
57 ) -> Result<Self::Output, Self::Error>;
58}
59
60/// Drive a walker over `(graph, ty, value)`.
61///
62/// - [`SchemaType::Ref`] hops are resolved against `graph.defs` before
63/// handing the body to the walker.
64/// - A reference cycle (the same [`TypeId`] re-entered while still on the
65/// stack) is reported via [`WalkerError::RefCycle`] so walkers do not
66/// recurse forever on ill-formed graphs.
67/// - A dangling reference (the named id is not present in `graph.defs`) is
68/// reported via [`WalkerError::DanglingRef`].
69/// - Any error produced by the walker itself is bubbled up unchanged via
70/// [`WalkerError::Walker`].
71pub fn walk<W: SchemaWalker>(
72 walker: &mut W,
73 graph: &SchemaGraph,
74 ty: &SchemaType,
75 value: &SchemaValue,
76) -> Result<W::Output, WalkerError<W::Error>> {
77 let mut visited: HashSet<TypeId> = HashSet::new();
78 walk_inner(walker, graph, ty, value, &mut visited)
79}
80
81fn walk_inner<W: SchemaWalker>(
82 walker: &mut W,
83 graph: &SchemaGraph,
84 ty: &SchemaType,
85 value: &SchemaValue,
86 visited: &mut HashSet<TypeId>,
87) -> Result<W::Output, WalkerError<W::Error>> {
88 resolve_ref(graph, ty, visited, |graph, body| {
89 walker.walk(graph, body, value).map_err(WalkerError::Walker)
90 })
91}
92
93/// Resolve any leading [`SchemaType::Ref`] hops on `ty` (cycle-aware) and
94/// hand the resulting non-ref body to `f`. Errors raised by ref handling
95/// surface as [`WalkerError::RefCycle`] / [`WalkerError::DanglingRef`];
96/// the caller's own error type is wrapped in [`WalkerError::Walker`].
97///
98/// Decoders that consume an input other than `SchemaValue` (for example
99/// `serde_json::Value`) use this directly so the ref-resolution path is
100/// shared with the walker rather than re-implemented.
101pub fn resolve_ref<F, T, E>(
102 graph: &SchemaGraph,
103 ty: &SchemaType,
104 visited: &mut HashSet<TypeId>,
105 f: F,
106) -> Result<T, WalkerError<E>>
107where
108 F: FnOnce(&SchemaGraph, &SchemaType) -> Result<T, WalkerError<E>>,
109{
110 let mut current = ty;
111 let mut entered: Vec<TypeId> = Vec::new();
112 let result = loop {
113 match current {
114 SchemaType::Ref { id, .. } => {
115 if !visited.insert(id.clone()) {
116 break Err(WalkerError::RefCycle(id.clone()));
117 }
118 entered.push(id.clone());
119 match graph.lookup(id) {
120 Some(def) => current = &def.body,
121 None => break Err(WalkerError::DanglingRef(id.clone())),
122 }
123 }
124 other => break f(graph, other),
125 }
126 };
127 for id in entered {
128 visited.remove(&id);
129 }
130 result
131}
132
133/// Errors raised by the [`walk`] driver. Walker-specific errors are wrapped
134/// in [`WalkerError::Walker`].
135#[derive(Clone, Debug, PartialEq, Eq)]
136pub enum WalkerError<E> {
137 /// A ref cycle was detected during traversal.
138 RefCycle(TypeId),
139 /// A named ref pointed at a type id that is not declared in the graph.
140 DanglingRef(TypeId),
141 /// The walker rejected the sub-tree.
142 Walker(E),
143}
144
145impl<E: std::fmt::Display> std::fmt::Display for WalkerError<E> {
146 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
147 match self {
148 WalkerError::RefCycle(id) => write!(f, "reference cycle through `{id}`"),
149 WalkerError::DanglingRef(id) => write!(f, "dangling reference `{id}`"),
150 WalkerError::Walker(inner) => write!(f, "{inner}"),
151 }
152 }
153}
154
155impl<E: std::error::Error + 'static> std::error::Error for WalkerError<E> {
156 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
157 match self {
158 WalkerError::Walker(inner) => Some(inner),
159 _ => None,
160 }
161 }
162}