1use crate::schema::metadata::{MetadataEnvelope, TypeId};
16use crate::schema::schema_type::SchemaType;
17use crate::schema::schema_value::SchemaValue;
18use golem_schema_derive::{FromSchema, IntoSchema};
19use serde::{Deserialize, Serialize};
20use std::collections::{HashMap, HashSet};
21
22#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, IntoSchema, FromSchema)]
47#[cfg_attr(feature = "full", derive(desert_rust::BinaryCodec))]
48#[cfg_attr(feature = "full", desert(evolution()))]
49#[serde(rename_all = "camelCase")]
50#[schema(named = "schema-graph")]
51#[cfg_attr(feature = "full", derive(golem_schema_derive::PoemSchema))]
52pub struct SchemaGraph {
53 #[serde(default, skip_serializing_if = "Vec::is_empty")]
58 pub defs: Vec<SchemaTypeDef>,
59 pub root: SchemaType,
62}
63
64impl SchemaGraph {
65 pub fn anonymous(root: SchemaType) -> Self {
67 Self {
68 defs: Vec::new(),
69 root,
70 }
71 }
72
73 pub fn empty() -> Self {
82 Self {
83 defs: Vec::new(),
84 root: SchemaType::Record {
85 fields: Vec::new(),
86 metadata: MetadataEnvelope::default(),
87 },
88 }
89 }
90
91 pub fn lookup(&self, id: &TypeId) -> Option<&SchemaTypeDef> {
93 self.defs.iter().find(|d| &d.id == id)
94 }
95
96 pub fn contains_stream_type(&self) -> bool {
98 let mut pending = vec![&self.root];
99 let mut visited = HashSet::new();
100 while let Some(ty) = pending.pop() {
101 match ty {
102 SchemaType::Stream { .. } => return true,
103 SchemaType::Ref { id, .. } => {
104 if visited.insert(id)
105 && let Some(def) = self.lookup(id)
106 {
107 pending.push(&def.body);
108 }
109 }
110 SchemaType::Record { fields, .. } => {
111 pending.extend(fields.iter().map(|field| &field.body));
112 }
113 SchemaType::Variant { cases, .. } => {
114 pending.extend(cases.iter().filter_map(|case| case.payload.as_ref()));
115 }
116 SchemaType::Tuple { elements, .. } => pending.extend(elements),
117 SchemaType::List { element, .. } | SchemaType::FixedList { element, .. } => {
118 pending.push(element);
119 }
120 SchemaType::Map { key, value, .. } => {
121 pending.extend([key.as_ref(), value.as_ref()])
122 }
123 SchemaType::Option { inner, .. } => pending.push(inner),
124 SchemaType::Result { spec, .. } => {
125 pending.extend(spec.ok.as_deref());
126 pending.extend(spec.err.as_deref());
127 }
128 SchemaType::Union { spec, .. } => {
129 pending.extend(spec.branches.iter().map(|branch| &branch.body));
130 }
131 SchemaType::Future { inner, .. } => pending.extend(inner.as_deref()),
132 SchemaType::Secret { spec, .. } => pending.push(&spec.inner),
133 SchemaType::Bool { .. }
134 | SchemaType::S8 { .. }
135 | SchemaType::S16 { .. }
136 | SchemaType::S32 { .. }
137 | SchemaType::S64 { .. }
138 | SchemaType::U8 { .. }
139 | SchemaType::U16 { .. }
140 | SchemaType::U32 { .. }
141 | SchemaType::U64 { .. }
142 | SchemaType::F32 { .. }
143 | SchemaType::F64 { .. }
144 | SchemaType::Char { .. }
145 | SchemaType::String { .. }
146 | SchemaType::Enum { .. }
147 | SchemaType::Flags { .. }
148 | SchemaType::Text { .. }
149 | SchemaType::Binary { .. }
150 | SchemaType::Path { .. }
151 | SchemaType::Url { .. }
152 | SchemaType::Uuid { .. }
153 | SchemaType::Datetime { .. }
154 | SchemaType::Duration { .. }
155 | SchemaType::Quantity { .. }
156 | SchemaType::QuotaToken { .. }
157 | SchemaType::PermissionCard { .. } => {}
158 }
159 }
160 false
161 }
162
163 pub fn resolve_ref<'a>(
168 &'a self,
169 ty: &'a SchemaType,
170 ) -> Result<&'a SchemaType, RefResolutionError> {
171 let mut visiting: Vec<TypeId> = Vec::new();
172 let mut current = ty;
173 loop {
174 match current {
175 SchemaType::Ref { id, .. } => {
176 if visiting.iter().any(|x| x == id) {
177 return Err(RefResolutionError::RecursiveRef(id.clone()));
178 }
179 let def = self
180 .lookup(id)
181 .ok_or_else(|| RefResolutionError::DanglingRef(id.clone()))?;
182 visiting.push(id.clone());
183 current = &def.body;
184 }
185 other => return Ok(other),
186 }
187 }
188 }
189}
190
191pub struct GraphIndex<'a> {
206 defs: &'a [SchemaTypeDef],
207 index: Option<HashMap<&'a str, &'a SchemaTypeDef>>,
208}
209
210impl<'a> GraphIndex<'a> {
211 const INDEX_THRESHOLD: usize = 8;
214
215 pub fn new(graph: &'a SchemaGraph) -> Self {
217 let defs = graph.defs.as_slice();
218 let index = (defs.len() > Self::INDEX_THRESHOLD).then(|| {
219 let mut map = HashMap::with_capacity(defs.len());
220 for def in defs {
221 map.entry(def.id.as_str()).or_insert(def);
224 }
225 map
226 });
227 Self { defs, index }
228 }
229
230 pub fn defs_len(&self) -> usize {
232 self.defs.len()
233 }
234
235 pub fn lookup(&self, id: &TypeId) -> Option<&'a SchemaTypeDef> {
238 match &self.index {
239 Some(map) => map.get(id.as_str()).copied(),
240 None => self.defs.iter().find(|d| &d.id == id),
241 }
242 }
243}
244
245#[derive(Debug, Clone, PartialEq, Eq)]
248pub enum RefResolutionError {
249 RecursiveRef(TypeId),
251 DanglingRef(TypeId),
253}
254
255impl std::fmt::Display for RefResolutionError {
256 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
257 match self {
258 Self::RecursiveRef(id) => {
259 write!(f, "schema graph contains a recursive reference: {id}")
260 }
261 Self::DanglingRef(id) => write!(f, "schema graph contains dangling reference: {id}"),
262 }
263 }
264}
265
266impl std::error::Error for RefResolutionError {}
267
268#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, IntoSchema, FromSchema)]
274#[cfg_attr(feature = "full", derive(desert_rust::BinaryCodec))]
275#[cfg_attr(feature = "full", desert(evolution()))]
276#[serde(rename_all = "camelCase")]
277#[schema(named = "schema-type-def")]
278#[cfg_attr(feature = "full", derive(golem_schema_derive::PoemSchema))]
279pub struct SchemaTypeDef {
280 pub id: TypeId,
282 #[serde(default, skip_serializing_if = "Option::is_none")]
284 pub name: Option<String>,
285 pub body: SchemaType,
288}
289
290#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, IntoSchema, FromSchema)]
296#[cfg_attr(feature = "full", derive(desert_rust::BinaryCodec))]
297#[cfg_attr(feature = "full", desert(evolution()))]
298#[serde(rename_all = "camelCase")]
299#[schema(named = "typed-schema-value")]
300#[cfg_attr(feature = "full", derive(golem_schema_derive::PoemSchema))]
301pub struct TypedSchemaValue {
302 graph: SchemaGraph,
303 value: SchemaValue,
304}
305
306impl TypedSchemaValue {
307 pub fn new(graph: SchemaGraph, value: SchemaValue) -> Self {
308 Self { graph, value }
309 }
310
311 pub fn graph(&self) -> &SchemaGraph {
312 &self.graph
313 }
314
315 pub fn value(&self) -> &SchemaValue {
316 &self.value
317 }
318
319 pub fn root_type(&self) -> &SchemaType {
320 &self.graph.root
321 }
322
323 pub fn into_parts(self) -> (SchemaGraph, SchemaValue) {
324 (self.graph, self.value)
325 }
326}
327
328pub fn reachable_defs<'a>(graph: &'a SchemaGraph, root: &'a SchemaType) -> Vec<SchemaTypeDef> {
342 let mut stack: Vec<&'a TypeId> = Vec::new();
345 collect_refs(root, &mut stack);
346 if stack.is_empty() {
347 return Vec::new();
348 }
349
350 let index = GraphIndex::new(graph);
351 let mut reachable: HashSet<&'a str> = HashSet::new();
352 while let Some(id) = stack.pop() {
353 if !reachable.insert(id.as_str()) {
354 continue;
355 }
356 if let Some(def) = index.lookup(id) {
357 collect_refs(&def.body, &mut stack);
358 }
359 }
360
361 graph
365 .defs
366 .iter()
367 .filter(|d| reachable.remove(d.id.as_str()))
368 .cloned()
369 .collect()
370}
371
372fn collect_refs<'a>(ty: &'a SchemaType, out: &mut Vec<&'a TypeId>) {
378 match ty {
379 SchemaType::Ref { id, .. } => out.push(id),
380 SchemaType::Record { fields, .. } => {
381 for field in fields {
382 collect_refs(&field.body, out);
383 }
384 }
385 SchemaType::Variant { cases, .. } => {
386 for case in cases {
387 if let Some(payload) = &case.payload {
388 collect_refs(payload, out);
389 }
390 }
391 }
392 SchemaType::Tuple { elements, .. } => {
393 for element in elements {
394 collect_refs(element, out);
395 }
396 }
397 SchemaType::List { element, .. } | SchemaType::FixedList { element, .. } => {
398 collect_refs(element, out);
399 }
400 SchemaType::Map { key, value, .. } => {
401 collect_refs(key, out);
402 collect_refs(value, out);
403 }
404 SchemaType::Option { inner, .. } => collect_refs(inner, out),
405 SchemaType::Result { spec, .. } => {
406 if let Some(ok) = &spec.ok {
407 collect_refs(ok, out);
408 }
409 if let Some(err) = &spec.err {
410 collect_refs(err, out);
411 }
412 }
413 SchemaType::Union { spec, .. } => {
414 for branch in &spec.branches {
415 collect_refs(&branch.body, out);
416 }
417 }
418 SchemaType::Future { inner, .. } | SchemaType::Stream { inner, .. } => {
419 if let Some(inner) = inner {
420 collect_refs(inner, out);
421 }
422 }
423 SchemaType::Secret { spec, .. } => collect_refs(&spec.inner, out),
424 SchemaType::Bool { .. }
428 | SchemaType::S8 { .. }
429 | SchemaType::S16 { .. }
430 | SchemaType::S32 { .. }
431 | SchemaType::S64 { .. }
432 | SchemaType::U8 { .. }
433 | SchemaType::U16 { .. }
434 | SchemaType::U32 { .. }
435 | SchemaType::U64 { .. }
436 | SchemaType::F32 { .. }
437 | SchemaType::F64 { .. }
438 | SchemaType::Char { .. }
439 | SchemaType::String { .. }
440 | SchemaType::Enum { .. }
441 | SchemaType::Flags { .. }
442 | SchemaType::Text { .. }
443 | SchemaType::Binary { .. }
444 | SchemaType::Path { .. }
445 | SchemaType::Url { .. }
446 | SchemaType::Uuid { .. }
447 | SchemaType::Datetime { .. }
448 | SchemaType::Duration { .. }
449 | SchemaType::Quantity { .. }
450 | SchemaType::QuotaToken { .. }
451 | SchemaType::PermissionCard { .. } => {}
452 }
453}
454
455#[cfg(test)]
456mod tests {
457 use super::*;
458 use test_r::test;
459
460 #[test]
461 fn stream_detection_follows_reachable_recursive_definitions() {
462 let recursive = TypeId::new("recursive");
463 let output = TypeId::new("output");
464 let mut graph = SchemaGraph {
465 root: SchemaType::ref_to(recursive.clone()),
466 defs: vec![
467 SchemaTypeDef {
468 id: recursive.clone(),
469 name: None,
470 body: SchemaType::tuple(vec![SchemaType::ref_to(recursive)]),
471 },
472 SchemaTypeDef {
473 id: output.clone(),
474 name: None,
475 body: SchemaType::option(SchemaType::stream(Some(SchemaType::u32()))),
476 },
477 ],
478 };
479 assert!(
480 !graph.contains_stream_type(),
481 "unreachable streams do not count"
482 );
483 let SchemaType::Tuple { elements, .. } = &mut graph.defs[0].body else {
484 unreachable!()
485 };
486 elements.push(SchemaType::ref_to(output));
487 assert!(
488 graph.contains_stream_type(),
489 "a cycle must not hide a sibling stream"
490 );
491 assert!(SchemaGraph::anonymous(SchemaType::stream(None)).contains_stream_type());
492 }
493
494 fn def(id: &str, name: Option<&str>) -> SchemaTypeDef {
495 SchemaTypeDef {
496 id: TypeId::new(id),
497 name: name.map(|n| n.to_string()),
498 body: SchemaType::bool(),
499 }
500 }
501
502 fn assert_index_matches_linear(graph: &SchemaGraph) {
506 let index = GraphIndex::new(graph);
507 let mut ids: Vec<TypeId> = graph.defs.iter().map(|d| d.id.clone()).collect();
509 ids.push(TypeId::new("definitely-absent-id"));
510 for id in &ids {
511 assert_eq!(
512 index.lookup(id),
513 graph.lookup(id),
514 "GraphIndex and SchemaGraph disagree on id {id:?}"
515 );
516 }
517 }
518
519 #[test]
520 fn graph_index_matches_linear_small_with_duplicates() {
521 let graph = SchemaGraph {
524 defs: vec![
525 def("a", Some("first-a")),
526 def("b", Some("b")),
527 def("a", Some("second-a")),
528 ],
529 root: SchemaType::bool(),
530 };
531 let index = GraphIndex::new(&graph);
532 assert!(
533 index.index.is_none(),
534 "small graph must use linear fallback"
535 );
536 assert_eq!(
537 index
538 .lookup(&TypeId::new("a"))
539 .and_then(|d| d.name.as_deref()),
540 Some("first-a"),
541 "first definition must win on duplicate ids"
542 );
543 assert_index_matches_linear(&graph);
544 }
545
546 #[test]
547 fn graph_index_matches_linear_wide_with_duplicates() {
548 let mut defs: Vec<SchemaTypeDef> = (0..20)
550 .map(|i| def(&format!("t{i:02}"), Some(&format!("name-{i:02}"))))
551 .collect();
552 defs.push(def("t05", Some("duplicate-of-t05")));
553 let graph = SchemaGraph {
554 defs,
555 root: SchemaType::bool(),
556 };
557 let index = GraphIndex::new(&graph);
558 assert!(index.index.is_some(), "wide graph must build an index");
559 assert_eq!(
560 index
561 .lookup(&TypeId::new("t05"))
562 .and_then(|d| d.name.as_deref()),
563 Some("name-05"),
564 "first definition must win on duplicate ids in the indexed branch"
565 );
566 assert_index_matches_linear(&graph);
567 }
568}