Skip to main content

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}