Skip to main content

golem_schema/schema/render/
json_value.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//! JSON value renderer: convert a `(SchemaGraph, SchemaType, SchemaValue)`
16//! to and from a `serde_json::Value`.
17//!
18//! Rich scalars route through [`crate::schema::canonical`]; the renderer
19//! never re-implements their text/JSON forms.
20
21use crate::schema::canonical;
22use crate::schema::graph::SchemaGraph;
23use crate::schema::host_managed::HostManagedKind;
24use crate::schema::metadata::TypeId;
25use crate::schema::render::error::RenderError;
26use crate::schema::render::walker::{SchemaWalker, WalkerError, resolve_ref, walk};
27use crate::schema::schema_type::{
28    DiscriminatorRule, ResultSpec, SchemaType, UnionBranch, UnionSpec, VariantCaseType,
29};
30use crate::schema::schema_value::{
31    ResultValuePayload, SchemaValue, UnionValuePayload, VariantValuePayload,
32};
33use serde_json::{Map, Number, Value};
34use std::collections::HashSet;
35
36/// Render a value tree to a `serde_json::Value`.
37pub fn to_json_value(
38    graph: &SchemaGraph,
39    ty: &SchemaType,
40    value: &SchemaValue,
41) -> Result<Value, RenderError> {
42    let mut renderer = ToJsonRenderer {
43        path: PathStack::new(),
44        redact: false,
45    };
46    drive(walk(&mut renderer, graph, ty, value))
47}
48
49/// Render a value tree to a `serde_json::Value`, redacting every
50/// host-managed capability node (see [`HostManagedKind`]).
51///
52/// Each `Secret` / `QuotaToken` value — including those nested inside
53/// records, variants, lists, maps, options, results and unions — is
54/// replaced with the JSON string `"<redacted: kind>"`. Non-capability
55/// material is encoded exactly as in [`to_json_value`].
56///
57/// Use this on external-facing surfaces (MCP tool/resource responses,
58/// custom HTTP API responses) that would otherwise forward raw
59/// capability references to clients. Internal, trusted callers that
60/// need the lossless round-trippable form — registry storage,
61/// cross-worker RPC, oplog/grpc, config storage — should keep using
62/// [`to_json_value`]. Unlike [`to_json_value`], the output of this
63/// function is **not** guaranteed to round-trip through
64/// [`from_json_value`]; the redacted placeholder is a one-way sink.
65pub fn to_json_value_redacted(
66    graph: &SchemaGraph,
67    ty: &SchemaType,
68    value: &SchemaValue,
69) -> Result<Value, RenderError> {
70    let mut renderer = ToJsonRenderer {
71        path: PathStack::new(),
72        redact: true,
73    };
74    drive(walk(&mut renderer, graph, ty, value))
75}
76
77/// Decode a `serde_json::Value` into a value tree typed by `ty` in `graph`.
78pub fn from_json_value(
79    graph: &SchemaGraph,
80    ty: &SchemaType,
81    json: &Value,
82) -> Result<SchemaValue, RenderError> {
83    from_json_value_with_policy(graph, ty, json, JsonDecodePolicy::Trusted)
84}
85
86/// Decode externally supplied JSON without allowing it to construct
87/// host-managed capabilities. Rejection happens when the selected value path
88/// reaches a capability leaf, so absent options and unselected sum-type arms
89/// remain valid.
90pub fn from_untrusted_json_value(
91    graph: &SchemaGraph,
92    ty: &SchemaType,
93    json: &Value,
94) -> Result<SchemaValue, RenderError> {
95    from_json_value_with_policy(graph, ty, json, JsonDecodePolicy::Untrusted)
96}
97
98#[derive(Clone, Copy, Debug, PartialEq, Eq)]
99enum JsonDecodePolicy {
100    Trusted,
101    Untrusted,
102}
103
104fn from_json_value_with_policy(
105    graph: &SchemaGraph,
106    ty: &SchemaType,
107    json: &Value,
108    policy: JsonDecodePolicy,
109) -> Result<SchemaValue, RenderError> {
110    let mut path = PathStack::new();
111    let mut visited: HashSet<TypeId> = HashSet::new();
112    from_json_inner(graph, ty, json, &mut path, &mut visited, policy)
113}
114
115// --------------------------------------------------------------------- to
116
117struct ToJsonRenderer {
118    path: PathStack,
119    redact: bool,
120}
121
122impl SchemaWalker for ToJsonRenderer {
123    type Output = Value;
124    type Error = RenderError;
125
126    fn walk(
127        &mut self,
128        graph: &SchemaGraph,
129        ty: &SchemaType,
130        value: &SchemaValue,
131    ) -> Result<Value, RenderError> {
132        encode(self, graph, ty, value)
133    }
134}
135
136fn drive<T>(res: Result<T, WalkerError<RenderError>>) -> Result<T, RenderError> {
137    match res {
138        Ok(v) => Ok(v),
139        Err(WalkerError::Walker(e)) => Err(e),
140        Err(WalkerError::RefCycle(id)) => Err(RenderError::ValueMismatch {
141            path: String::new(),
142            reason: format!("reference cycle through `{id}`"),
143        }),
144        Err(WalkerError::DanglingRef(id)) => Err(RenderError::ValueMismatch {
145            path: String::new(),
146            reason: format!("dangling reference `{id}`"),
147        }),
148    }
149}
150
151fn encode(
152    r: &mut ToJsonRenderer,
153    graph: &SchemaGraph,
154    ty: &SchemaType,
155    value: &SchemaValue,
156) -> Result<Value, RenderError> {
157    if r.redact
158        && let (Some(type_kind), Some(value_kind)) = (
159            HostManagedKind::from_type(ty),
160            HostManagedKind::from_value(value),
161        )
162        && type_kind == value_kind
163    {
164        return Ok(Value::String(type_kind.redacted_placeholder().to_string()));
165    }
166
167    match (ty, value) {
168        (SchemaType::Ref { .. }, _) => unreachable!("walker resolves refs"),
169
170        (SchemaType::Bool { .. }, SchemaValue::Bool(b)) => Ok(Value::Bool(*b)),
171        (SchemaType::S8 { .. }, SchemaValue::S8(i)) => Ok(json_number_i64(*i as i64)),
172        (SchemaType::S16 { .. }, SchemaValue::S16(i)) => Ok(json_number_i64(*i as i64)),
173        (SchemaType::S32 { .. }, SchemaValue::S32(i)) => Ok(json_number_i64(*i as i64)),
174        (SchemaType::S64 { .. }, SchemaValue::S64(i)) => Ok(Value::String(i.to_string())),
175        (SchemaType::U8 { .. }, SchemaValue::U8(u)) => Ok(json_number_u64(*u as u64)),
176        (SchemaType::U16 { .. }, SchemaValue::U16(u)) => Ok(json_number_u64(*u as u64)),
177        (SchemaType::U32 { .. }, SchemaValue::U32(u)) => Ok(json_number_u64(*u as u64)),
178        (SchemaType::U64 { .. }, SchemaValue::U64(u)) => Ok(Value::String(u.to_string())),
179        (SchemaType::F32 { .. }, SchemaValue::F32(f)) => json_number_f64(*f as f64, &r.path),
180        (SchemaType::F64 { .. }, SchemaValue::F64(f)) => json_number_f64(*f, &r.path),
181        (SchemaType::Char { .. }, SchemaValue::Char(c)) => Ok(Value::String(c.to_string())),
182        (SchemaType::String { .. }, SchemaValue::String(s)) => Ok(Value::String(s.clone())),
183
184        (SchemaType::Text { .. }, SchemaValue::Text(p)) => Ok(canonical::text::to_json(p)),
185        (SchemaType::Binary { .. }, SchemaValue::Binary(p)) => {
186            canonical::binary::to_json(p).map_err(RenderError::from)
187        }
188        (SchemaType::Path { .. }, SchemaValue::Path { path }) => {
189            canonical::path::to_json(path).map_err(RenderError::from)
190        }
191        (SchemaType::Url { .. }, SchemaValue::Url { url }) => {
192            canonical::url::to_json(url).map_err(RenderError::from)
193        }
194        (SchemaType::Uuid { .. }, SchemaValue::Uuid(value)) => Ok(canonical::uuid::to_json(value)),
195        (SchemaType::Datetime { .. }, SchemaValue::Datetime { value }) => {
196            canonical::datetime::to_json(value).map_err(RenderError::from)
197        }
198        (SchemaType::Duration { .. }, SchemaValue::Duration(p)) => {
199            Ok(canonical::duration::to_json(p))
200        }
201        (SchemaType::Quantity { .. }, SchemaValue::Quantity(q)) => {
202            Ok(canonical::quantity::to_json(q))
203        }
204        #[cfg(not(all(feature = "guest", not(feature = "host"))))]
205        (SchemaType::Secret { .. }, SchemaValue::Secret(p)) => {
206            canonical::secret::to_json(p).map_err(RenderError::from)
207        }
208        #[cfg(not(all(feature = "guest", not(feature = "host"))))]
209        (SchemaType::QuotaToken { .. }, SchemaValue::QuotaToken(p)) => {
210            canonical::quota_token::to_json(p).map_err(RenderError::from)
211        }
212        #[cfg(not(all(feature = "guest", not(feature = "host"))))]
213        (SchemaType::PermissionCard { .. }, SchemaValue::PermissionCard(p)) => {
214            canonical::permission_card::to_json(p).map_err(RenderError::from)
215        }
216        #[cfg(all(feature = "guest", not(feature = "host")))]
217        (
218            SchemaType::Secret { .. }
219            | SchemaType::QuotaToken { .. }
220            | SchemaType::PermissionCard { .. },
221            SchemaValue::Secret(_) | SchemaValue::QuotaToken(_) | SchemaValue::PermissionCard(_),
222        ) => Err(RenderError::Unsupported(
223            "opaque host-managed capabilities cannot be rendered by a guest",
224        )),
225
226        (SchemaType::Record { fields, .. }, SchemaValue::Record { fields: vs }) => {
227            if fields.len() != vs.len() {
228                return Err(r.mismatch(format!(
229                    "record arity: expected {}, found {}",
230                    fields.len(),
231                    vs.len()
232                )));
233            }
234            let mut obj = Map::new();
235            for (field, value) in fields.iter().zip(vs.iter()) {
236                r.path.push(PathSegment::Field(field.name.clone()));
237                let rendered = drive(walk(r, graph, &field.body, value))?;
238                r.path.pop();
239                obj.insert(field.name.clone(), rendered);
240            }
241            Ok(Value::Object(obj))
242        }
243
244        (SchemaType::Variant { cases, .. }, SchemaValue::Variant(vp)) => {
245            let case_index = vp.case as usize;
246            if case_index >= cases.len() {
247                return Err(r.mismatch(format!(
248                    "variant case index {} out of range (count {})",
249                    vp.case,
250                    cases.len()
251                )));
252            }
253            let case: &VariantCaseType = &cases[case_index];
254            match (&case.payload, &vp.payload) {
255                (None, None) => Ok(Value::String(case.name.clone())),
256                (Some(case_ty), Some(payload)) => {
257                    r.path.push(PathSegment::Variant(case.name.clone()));
258                    let rendered = drive(walk(r, graph, case_ty, payload))?;
259                    r.path.pop();
260                    let mut obj = Map::new();
261                    obj.insert(case.name.clone(), rendered);
262                    Ok(Value::Object(obj))
263                }
264                _ => Err(r.mismatch(format!(
265                    "variant payload presence mismatch on case `{}`",
266                    case.name
267                ))),
268            }
269        }
270
271        (SchemaType::Enum { cases, .. }, SchemaValue::Enum { case }) => {
272            let idx = *case as usize;
273            if idx >= cases.len() {
274                return Err(r.mismatch(format!(
275                    "enum case index {} out of range (count {})",
276                    case,
277                    cases.len()
278                )));
279            }
280            Ok(Value::String(cases[idx].clone()))
281        }
282
283        (SchemaType::Flags { flags, .. }, SchemaValue::Flags { bits }) => {
284            if flags.len() != bits.len() {
285                return Err(r.mismatch(format!(
286                    "flags arity: expected {}, found {}",
287                    flags.len(),
288                    bits.len()
289                )));
290            }
291            let selected: Vec<Value> = flags
292                .iter()
293                .zip(bits.iter())
294                .filter(|(_, on)| **on)
295                .map(|(name, _)| Value::String(name.clone()))
296                .collect();
297            Ok(Value::Array(selected))
298        }
299
300        (SchemaType::Tuple { elements, .. }, SchemaValue::Tuple { elements: vs }) => {
301            if elements.len() != vs.len() {
302                return Err(r.mismatch(format!(
303                    "tuple arity: expected {}, found {}",
304                    elements.len(),
305                    vs.len()
306                )));
307            }
308            let mut out = Vec::with_capacity(vs.len());
309            for (i, (et, ev)) in elements.iter().zip(vs.iter()).enumerate() {
310                r.path.push(PathSegment::Index(i));
311                let rendered = drive(walk(r, graph, et, ev))?;
312                r.path.pop();
313                out.push(rendered);
314            }
315            Ok(Value::Array(out))
316        }
317
318        (SchemaType::List { element, .. }, SchemaValue::List { elements }) => {
319            let mut out = Vec::with_capacity(elements.len());
320            for (i, ev) in elements.iter().enumerate() {
321                r.path.push(PathSegment::Index(i));
322                let rendered = drive(walk(r, graph, element, ev))?;
323                r.path.pop();
324                out.push(rendered);
325            }
326            Ok(Value::Array(out))
327        }
328
329        (
330            SchemaType::FixedList {
331                element, length, ..
332            },
333            SchemaValue::FixedList { elements },
334        ) => {
335            if elements.len() as u32 != *length {
336                return Err(r.mismatch(format!(
337                    "fixed list length: expected {}, found {}",
338                    length,
339                    elements.len()
340                )));
341            }
342            let mut out = Vec::with_capacity(elements.len());
343            for (i, ev) in elements.iter().enumerate() {
344                r.path.push(PathSegment::Index(i));
345                let rendered = drive(walk(r, graph, element, ev))?;
346                r.path.pop();
347                out.push(rendered);
348            }
349            Ok(Value::Array(out))
350        }
351
352        (SchemaType::Map { key, value, .. }, SchemaValue::Map { entries }) => {
353            let mut out = Vec::with_capacity(entries.len());
354            for (i, (k, v)) in entries.iter().enumerate() {
355                r.path.push(PathSegment::MapKey(i));
356                let rk = drive(walk(r, graph, key, k))?;
357                r.path.pop();
358                r.path.push(PathSegment::MapValue(i));
359                let rv = drive(walk(r, graph, value, v))?;
360                r.path.pop();
361                out.push(Value::Array(vec![rk, rv]));
362            }
363            Ok(Value::Array(out))
364        }
365
366        (SchemaType::Option { inner, .. }, SchemaValue::Option { inner: v }) => match v {
367            None => Ok(Value::Null),
368            Some(inner_value) => {
369                r.path.push(PathSegment::OptionInner);
370                let rendered = drive(walk(r, graph, inner, inner_value))?;
371                r.path.pop();
372                Ok(rendered)
373            }
374        },
375
376        (SchemaType::Result { spec, .. }, SchemaValue::Result(payload)) => {
377            encode_result(r, graph, spec, payload)
378        }
379
380        (SchemaType::Union { spec, .. }, SchemaValue::Union(payload)) => {
381            encode_union(r, graph, spec, payload)
382        }
383
384        (SchemaType::Future { .. }, _) | (SchemaType::Stream { .. }, _) => Err(
385            RenderError::Unsupported("future/stream values have no JSON representation"),
386        ),
387
388        (ty, value) => Err(r.mismatch(format!(
389            "shape mismatch: expected {}, found {}",
390            type_name(ty),
391            value_name(value)
392        ))),
393    }
394}
395
396fn encode_result(
397    r: &mut ToJsonRenderer,
398    graph: &SchemaGraph,
399    spec: &ResultSpec,
400    payload: &ResultValuePayload,
401) -> Result<Value, RenderError> {
402    let mut obj = Map::new();
403    match payload {
404        ResultValuePayload::Ok {
405            value: payload_value,
406        } => {
407            let v = match (spec.ok.as_deref(), payload_value.as_deref()) {
408                (None, None) => Value::Null,
409                (Some(ok_ty), Some(inner)) => {
410                    r.path.push(PathSegment::Ok);
411                    let v = drive(walk(r, graph, ok_ty, inner))?;
412                    r.path.pop();
413                    v
414                }
415                _ => {
416                    return Err(r.mismatch("result ok payload presence mismatch".to_string()));
417                }
418            };
419            obj.insert("ok".to_string(), v);
420        }
421        ResultValuePayload::Err {
422            value: payload_value,
423        } => {
424            let v = match (spec.err.as_deref(), payload_value.as_deref()) {
425                (None, None) => Value::Null,
426                (Some(err_ty), Some(inner)) => {
427                    r.path.push(PathSegment::Err);
428                    let v = drive(walk(r, graph, err_ty, inner))?;
429                    r.path.pop();
430                    v
431                }
432                _ => {
433                    return Err(r.mismatch("result err payload presence mismatch".to_string()));
434                }
435            };
436            obj.insert("err".to_string(), v);
437        }
438    }
439    Ok(Value::Object(obj))
440}
441
442fn encode_union(
443    r: &mut ToJsonRenderer,
444    graph: &SchemaGraph,
445    spec: &UnionSpec,
446    payload: &UnionValuePayload,
447) -> Result<Value, RenderError> {
448    let branch = find_branch(spec, &payload.tag)
449        .ok_or_else(|| r.mismatch(format!("unknown union branch tag `{}`", payload.tag)))?;
450    // Encode the body first; the on-wire union form is the body's JSON.
451    r.path.push(PathSegment::Union(payload.tag.clone()));
452    let rendered = drive(walk(r, graph, &branch.body, &payload.body))?;
453    r.path.pop();
454    // Sanity check: the produced JSON should match the branch's
455    // discriminator rule. Validation should have caught a tag/body
456    // disagreement at construction time; this is the runtime safety net.
457    if !rule_matches(&branch.discriminator, &rendered)? {
458        return Err(RenderError::UnionTagMismatch {
459            tag: payload.tag.clone(),
460            reason: format!(
461                "encoded body does not satisfy discriminator {}",
462                rule_label(&branch.discriminator)
463            ),
464        });
465    }
466    Ok(rendered)
467}
468
469fn find_branch<'a>(spec: &'a UnionSpec, tag: &str) -> Option<&'a UnionBranch> {
470    spec.branches.iter().find(|b| b.tag == tag)
471}
472
473impl ToJsonRenderer {
474    fn mismatch(&self, reason: String) -> RenderError {
475        RenderError::ValueMismatch {
476            path: self.path.render(),
477            reason,
478        }
479    }
480}
481
482// --------------------------------------------------------------------- from
483
484fn from_json_inner(
485    graph: &SchemaGraph,
486    ty: &SchemaType,
487    json: &Value,
488    path: &mut PathStack,
489    visited: &mut HashSet<TypeId>,
490    policy: JsonDecodePolicy,
491) -> Result<SchemaValue, RenderError> {
492    // Route through the shared ref-resolution helper so the decoder uses
493    // the same cycle protection as the walker-based encoder.
494    let res = resolve_ref::<_, SchemaValue, RenderError>(graph, ty, visited, |graph, body| {
495        match from_json_body(graph, body, json, path, &mut HashSet::new(), policy) {
496            Ok(v) => Ok(v),
497            Err(e) => Err(WalkerError::Walker(e)),
498        }
499    });
500    drive_with_path(res, path)
501}
502
503fn drive_with_path(
504    res: Result<SchemaValue, WalkerError<RenderError>>,
505    path: &PathStack,
506) -> Result<SchemaValue, RenderError> {
507    match res {
508        Ok(v) => Ok(v),
509        Err(WalkerError::Walker(e)) => Err(e),
510        Err(WalkerError::RefCycle(id)) => Err(RenderError::ValueMismatch {
511            path: path.render(),
512            reason: format!("reference cycle through `{id}`"),
513        }),
514        Err(WalkerError::DanglingRef(id)) => Err(RenderError::ValueMismatch {
515            path: path.render(),
516            reason: format!("dangling reference `{id}`"),
517        }),
518    }
519}
520
521fn from_json_body(
522    graph: &SchemaGraph,
523    ty: &SchemaType,
524    json: &Value,
525    path: &mut PathStack,
526    _local_visited: &mut HashSet<TypeId>,
527    policy: JsonDecodePolicy,
528) -> Result<SchemaValue, RenderError> {
529    // The `visited` HashSet for nested references is provided by the caller
530    // through `resolve_ref`. For sub-recursions we re-enter `from_json_inner`
531    // with a fresh set per sibling traversal because ref protection is
532    // scoped to the active stack of references, not the entire walk.
533    let mut visited: HashSet<TypeId> = HashSet::new();
534    if policy == JsonDecodePolicy::Untrusted
535        && let Some(kind) = HostManagedKind::from_type(ty)
536    {
537        return Err(mismatch(
538            path,
539            format!(
540                "host-managed capability `{}` cannot be constructed from external JSON",
541                kind.kind_name()
542            ),
543        ));
544    }
545
546    match ty {
547        SchemaType::Ref { .. } => unreachable!("ref resolved by resolve_ref"),
548        SchemaType::Bool { .. } => match json.as_bool() {
549            Some(b) => Ok(SchemaValue::Bool(b)),
550            None => Err(mismatch(path, "expected JSON boolean".to_string())),
551        },
552        SchemaType::S8 { .. } => {
553            check_int_range::<i8>(json, path)?;
554            Ok(SchemaValue::S8(json_i64(json, path)? as i8))
555        }
556        SchemaType::S16 { .. } => {
557            check_int_range::<i16>(json, path)?;
558            Ok(SchemaValue::S16(json_i64(json, path)? as i16))
559        }
560        SchemaType::S32 { .. } => {
561            check_int_range::<i32>(json, path)?;
562            Ok(SchemaValue::S32(json_i64(json, path)? as i32))
563        }
564        SchemaType::S64 { .. } => Ok(SchemaValue::S64(json_i64_string(json, path)?)),
565        SchemaType::U8 { .. } => {
566            check_int_range::<u8>(json, path)?;
567            Ok(SchemaValue::U8(json_u64(json, path)? as u8))
568        }
569        SchemaType::U16 { .. } => {
570            check_int_range::<u16>(json, path)?;
571            Ok(SchemaValue::U16(json_u64(json, path)? as u16))
572        }
573        SchemaType::U32 { .. } => {
574            check_int_range::<u32>(json, path)?;
575            Ok(SchemaValue::U32(json_u64(json, path)? as u32))
576        }
577        SchemaType::U64 { .. } => Ok(SchemaValue::U64(json_u64_string(json, path)?)),
578        SchemaType::F32 { .. } => {
579            let value = json_f64(json, path)?;
580            check_f32_in_range(value, path)?;
581            Ok(SchemaValue::F32(value as f32))
582        }
583        SchemaType::F64 { .. } => {
584            // `serde_json::Number` only stores finite floats, but assert
585            // it here as a defensive insurance so round-tripping never
586            // emits a non-finite number.
587            let value = json_f64(json, path)?;
588            if !value.is_finite() {
589                return Err(mismatch(path, "f64 must be finite".to_string()));
590            }
591            Ok(SchemaValue::F64(value))
592        }
593        SchemaType::Char { .. } => match json.as_str() {
594            Some(s) => {
595                let mut chars = s.chars();
596                match (chars.next(), chars.next()) {
597                    (Some(c), None) => Ok(SchemaValue::Char(c)),
598                    _ => Err(mismatch(
599                        path,
600                        "expected JSON string with exactly one char".to_string(),
601                    )),
602                }
603            }
604            None => Err(mismatch(path, "expected JSON string".to_string())),
605        },
606        SchemaType::String { .. } => match json.as_str() {
607            Some(s) => Ok(SchemaValue::String(s.to_string())),
608            None => Err(mismatch(path, "expected JSON string".to_string())),
609        },
610
611        SchemaType::Text { .. } => {
612            let p = canonical::text::from_json(json)?;
613            Ok(SchemaValue::Text(p))
614        }
615        SchemaType::Binary { .. } => {
616            let p = canonical::binary::from_json(json)?;
617            Ok(SchemaValue::Binary(p))
618        }
619        SchemaType::Path { .. } => {
620            let s = canonical::path::from_json(json)?;
621            Ok(SchemaValue::Path { path: s })
622        }
623        SchemaType::Url { .. } => {
624            let s = canonical::url::from_json(json)?;
625            Ok(SchemaValue::Url { url: s })
626        }
627        SchemaType::Uuid { .. } => {
628            let value = canonical::uuid::from_json(json)?;
629            Ok(SchemaValue::Uuid(value))
630        }
631        SchemaType::Datetime { .. } => {
632            let dt = canonical::datetime::from_json(json)?;
633            Ok(SchemaValue::Datetime { value: dt })
634        }
635        SchemaType::Duration { .. } => {
636            let p = canonical::duration::from_json(json)?;
637            Ok(SchemaValue::Duration(p))
638        }
639        SchemaType::Quantity { .. } => {
640            let q = canonical::quantity::from_json(json)?;
641            Ok(SchemaValue::Quantity(q))
642        }
643        #[cfg(not(all(feature = "guest", not(feature = "host"))))]
644        SchemaType::Secret { .. } => {
645            let p = canonical::secret::from_json(json)?;
646            Ok(SchemaValue::Secret(p))
647        }
648        #[cfg(not(all(feature = "guest", not(feature = "host"))))]
649        SchemaType::QuotaToken { .. } => {
650            let p = canonical::quota_token::from_json(json)?;
651            Ok(SchemaValue::QuotaToken(p))
652        }
653        #[cfg(not(all(feature = "guest", not(feature = "host"))))]
654        SchemaType::PermissionCard { .. } => {
655            let p = canonical::permission_card::from_json(json)?;
656            Ok(SchemaValue::PermissionCard(p))
657        }
658        #[cfg(all(feature = "guest", not(feature = "host")))]
659        SchemaType::Secret { .. }
660        | SchemaType::QuotaToken { .. }
661        | SchemaType::PermissionCard { .. } => Err(RenderError::Unsupported(
662            "opaque host-managed capabilities cannot be constructed by a guest",
663        )),
664
665        SchemaType::Record { fields, .. } => {
666            let obj = json
667                .as_object()
668                .ok_or_else(|| mismatch(path, "expected JSON object for record".to_string()))?;
669            // Strict boundary check: every JSON field must be declared by
670            // the schema record.
671            let known: HashSet<&str> = fields.iter().map(|f| f.name.as_str()).collect();
672            for key in obj.keys() {
673                if !known.contains(key.as_str()) {
674                    return Err(RenderError::UnexpectedField {
675                        record: "anonymous".to_string(),
676                        field: key.clone(),
677                    });
678                }
679            }
680            let mut out = Vec::with_capacity(fields.len());
681            for field in fields.iter() {
682                match obj.get(&field.name) {
683                    Some(value) => {
684                        path.push(PathSegment::Field(field.name.clone()));
685                        let v =
686                            from_json_inner(graph, &field.body, value, path, &mut visited, policy)?;
687                        path.pop();
688                        out.push(v);
689                    }
690                    None if resolves_to_option(graph, &field.body) => {
691                        out.push(SchemaValue::Option { inner: None });
692                    }
693                    None => {
694                        return Err(mismatch(
695                            path,
696                            format!("missing record field `{}`", field.name),
697                        ));
698                    }
699                }
700            }
701            Ok(SchemaValue::Record { fields: out })
702        }
703
704        SchemaType::Variant { cases, .. } => match json {
705            Value::String(name) => {
706                let (idx, case) = find_variant_case(cases, name)
707                    .ok_or_else(|| mismatch(path, format!("unknown variant case `{name}`")))?;
708                if case.payload.is_some() {
709                    return Err(mismatch(
710                        path,
711                        format!("variant case `{name}` requires a payload"),
712                    ));
713                }
714                Ok(SchemaValue::Variant(VariantValuePayload {
715                    case: idx as u32,
716                    payload: None,
717                }))
718            }
719            Value::Object(obj) if obj.len() == 1 => {
720                let (name, payload_json) = obj.iter().next().unwrap();
721                let (idx, case) = find_variant_case(cases, name)
722                    .ok_or_else(|| mismatch(path, format!("unknown variant case `{name}`")))?;
723                let payload_ty = case.payload.as_ref().ok_or_else(|| {
724                    mismatch(
725                        path,
726                        format!("variant case `{name}` does not accept a payload"),
727                    )
728                })?;
729                path.push(PathSegment::Variant(name.clone()));
730                let inner =
731                    from_json_inner(graph, payload_ty, payload_json, path, &mut visited, policy)?;
732                path.pop();
733                Ok(SchemaValue::Variant(VariantValuePayload {
734                    case: idx as u32,
735                    payload: Some(Box::new(inner)),
736                }))
737            }
738            _ => Err(mismatch(
739                path,
740                "expected JSON string or single-entry object for variant".to_string(),
741            )),
742        },
743
744        SchemaType::Enum { cases, .. } => {
745            let name = json
746                .as_str()
747                .ok_or_else(|| mismatch(path, "expected JSON string for enum".to_string()))?;
748            let idx = cases
749                .iter()
750                .position(|c| c == name)
751                .ok_or_else(|| mismatch(path, format!("unknown enum case `{name}`")))?;
752            Ok(SchemaValue::Enum { case: idx as u32 })
753        }
754
755        SchemaType::Flags { flags, .. } => {
756            let arr = json
757                .as_array()
758                .ok_or_else(|| mismatch(path, "expected JSON array for flags".to_string()))?;
759            let mut selected: HashSet<&str> = HashSet::new();
760            for item in arr {
761                let name = item.as_str().ok_or_else(|| {
762                    mismatch(path, "expected JSON string in flags array".to_string())
763                })?;
764                if !flags.iter().any(|f| f == name) {
765                    return Err(mismatch(path, format!("unknown flag `{name}`")));
766                }
767                if !selected.insert(name) {
768                    return Err(RenderError::DuplicateFlag {
769                        flag: name.to_string(),
770                    });
771                }
772            }
773            let bits: Vec<bool> = flags
774                .iter()
775                .map(|f| selected.contains(f.as_str()))
776                .collect();
777            Ok(SchemaValue::Flags { bits })
778        }
779
780        SchemaType::Tuple { elements, .. } => {
781            let arr = json
782                .as_array()
783                .ok_or_else(|| mismatch(path, "expected JSON array for tuple".to_string()))?;
784            if arr.len() != elements.len() {
785                return Err(mismatch(
786                    path,
787                    format!(
788                        "tuple arity: expected {}, found {}",
789                        elements.len(),
790                        arr.len()
791                    ),
792                ));
793            }
794            let mut out = Vec::with_capacity(arr.len());
795            for (i, (et, ev)) in elements.iter().zip(arr.iter()).enumerate() {
796                path.push(PathSegment::Index(i));
797                let v = from_json_inner(graph, et, ev, path, &mut visited, policy)?;
798                path.pop();
799                out.push(v);
800            }
801            Ok(SchemaValue::Tuple { elements: out })
802        }
803
804        SchemaType::List { element, .. } => {
805            let arr = json
806                .as_array()
807                .ok_or_else(|| mismatch(path, "expected JSON array for list".to_string()))?;
808            let mut out = Vec::with_capacity(arr.len());
809            for (i, ev) in arr.iter().enumerate() {
810                path.push(PathSegment::Index(i));
811                let v = from_json_inner(graph, element, ev, path, &mut visited, policy)?;
812                path.pop();
813                out.push(v);
814            }
815            Ok(SchemaValue::List { elements: out })
816        }
817
818        SchemaType::FixedList {
819            element, length, ..
820        } => {
821            let arr = json
822                .as_array()
823                .ok_or_else(|| mismatch(path, "expected JSON array for fixed list".to_string()))?;
824            if arr.len() as u32 != *length {
825                return Err(mismatch(
826                    path,
827                    format!(
828                        "fixed list length: expected {}, found {}",
829                        length,
830                        arr.len()
831                    ),
832                ));
833            }
834            let mut out = Vec::with_capacity(arr.len());
835            for (i, ev) in arr.iter().enumerate() {
836                path.push(PathSegment::Index(i));
837                let v = from_json_inner(graph, element, ev, path, &mut visited, policy)?;
838                path.pop();
839                out.push(v);
840            }
841            Ok(SchemaValue::FixedList { elements: out })
842        }
843
844        SchemaType::Map { key, value, .. } => {
845            let arr = json
846                .as_array()
847                .ok_or_else(|| mismatch(path, "expected JSON array for map".to_string()))?;
848            let mut out: Vec<(SchemaValue, SchemaValue)> = Vec::with_capacity(arr.len());
849            for (i, item) in arr.iter().enumerate() {
850                let pair = item.as_array().ok_or_else(|| {
851                    mismatch(path, "expected `[k, v]` array for map entry".to_string())
852                })?;
853                if pair.len() != 2 {
854                    return Err(mismatch(
855                        path,
856                        format!(
857                            "map entry must have exactly 2 elements, found {}",
858                            pair.len()
859                        ),
860                    ));
861                }
862                path.push(PathSegment::MapKey(i));
863                let k = from_json_inner(graph, key, &pair[0], path, &mut visited, policy)?;
864                path.pop();
865                path.push(PathSegment::MapValue(i));
866                let v = from_json_inner(graph, value, &pair[1], path, &mut visited, policy)?;
867                path.pop();
868                out.push((k, v));
869            }
870            Ok(SchemaValue::Map { entries: out })
871        }
872
873        SchemaType::Option { inner, .. } => match json {
874            Value::Null => Ok(SchemaValue::Option { inner: None }),
875            other => {
876                path.push(PathSegment::OptionInner);
877                let v = from_json_inner(graph, inner, other, path, &mut visited, policy)?;
878                path.pop();
879                Ok(SchemaValue::Option {
880                    inner: Some(Box::new(v)),
881                })
882            }
883        },
884
885        SchemaType::Result { spec, .. } => {
886            decode_result(graph, spec, json, path, &mut visited, policy)
887        }
888
889        SchemaType::Union { spec, .. } => decode_union(graph, spec, json, path, policy),
890
891        SchemaType::Future { .. } | SchemaType::Stream { .. } => Err(RenderError::Unsupported(
892            "future/stream values have no JSON representation",
893        )),
894    }
895}
896
897fn resolves_to_option(graph: &SchemaGraph, ty: &SchemaType) -> bool {
898    let mut current = ty;
899    let mut visited = HashSet::new();
900    loop {
901        match current {
902            SchemaType::Option { .. } => return true,
903            SchemaType::Ref { id, .. } if visited.insert(id.clone()) => match graph.lookup(id) {
904                Some(definition) => current = &definition.body,
905                None => return false,
906            },
907            _ => return false,
908        }
909    }
910}
911
912fn decode_result(
913    graph: &SchemaGraph,
914    spec: &ResultSpec,
915    json: &Value,
916    path: &mut PathStack,
917    visited: &mut HashSet<TypeId>,
918    policy: JsonDecodePolicy,
919) -> Result<SchemaValue, RenderError> {
920    let obj = json
921        .as_object()
922        .ok_or_else(|| mismatch(path, "expected JSON object for result".to_string()))?;
923    if obj.len() != 1 {
924        return Err(mismatch(
925            path,
926            "result object must have exactly one key `ok` or `err`".to_string(),
927        ));
928    }
929    let (key, payload_json) = obj.iter().next().unwrap();
930    match key.as_str() {
931        "ok" => {
932            let value = match (spec.ok.as_deref(), payload_json) {
933                (None, Value::Null) => None,
934                (Some(ok_ty), other) => {
935                    path.push(PathSegment::Ok);
936                    let v = from_json_inner(graph, ok_ty, other, path, visited, policy)?;
937                    path.pop();
938                    Some(Box::new(v))
939                }
940                _ => {
941                    return Err(mismatch(
942                        path,
943                        "result ok payload presence mismatch".to_string(),
944                    ));
945                }
946            };
947            Ok(SchemaValue::Result(ResultValuePayload::Ok { value }))
948        }
949        "err" => {
950            let value = match (spec.err.as_deref(), payload_json) {
951                (None, Value::Null) => None,
952                (Some(err_ty), other) => {
953                    path.push(PathSegment::Err);
954                    let v = from_json_inner(graph, err_ty, other, path, visited, policy)?;
955                    path.pop();
956                    Some(Box::new(v))
957                }
958                _ => {
959                    return Err(mismatch(
960                        path,
961                        "result err payload presence mismatch".to_string(),
962                    ));
963                }
964            };
965            Ok(SchemaValue::Result(ResultValuePayload::Err { value }))
966        }
967        other => Err(mismatch(path, format!("unexpected result key `{other}`"))),
968    }
969}
970
971fn decode_union(
972    graph: &SchemaGraph,
973    spec: &UnionSpec,
974    json: &Value,
975    path: &mut PathStack,
976    policy: JsonDecodePolicy,
977) -> Result<SchemaValue, RenderError> {
978    // First: find every branch whose discriminator rule matches the
979    // incoming JSON value. Validation rules out multi-match at construction
980    // time; a runtime safety net catches the case where the value is bad.
981    let mut matched: Vec<&UnionBranch> = Vec::new();
982    for branch in spec.branches.iter() {
983        if rule_matches(&branch.discriminator, json)? {
984            matched.push(branch);
985        }
986    }
987    match matched.as_slice() {
988        [] => Err(RenderError::UnionNoMatch),
989        [branch] => {
990            // Then: decode the body against the matched branch.
991            path.push(PathSegment::Union(branch.tag.clone()));
992            let body =
993                from_json_inner(graph, &branch.body, json, path, &mut HashSet::new(), policy)?;
994            path.pop();
995            Ok(SchemaValue::Union(UnionValuePayload {
996                tag: branch.tag.clone(),
997                body: Box::new(body),
998            }))
999        }
1000        multiple => Err(RenderError::UnionAmbiguous {
1001            matched: multiple.iter().map(|b| b.tag.clone()).collect(),
1002        }),
1003    }
1004}
1005
1006// ----------------------------------------------------------- discriminators
1007
1008/// Whether a [`DiscriminatorRule`] matches a raw JSON value.
1009fn rule_matches(rule: &DiscriminatorRule, json: &Value) -> Result<bool, RenderError> {
1010    Ok(match rule {
1011        DiscriminatorRule::Prefix { prefix } => json
1012            .as_str()
1013            .map(|s| s.starts_with(prefix.as_str()))
1014            .unwrap_or(false),
1015        DiscriminatorRule::Suffix { suffix } => json
1016            .as_str()
1017            .map(|s| s.ends_with(suffix.as_str()))
1018            .unwrap_or(false),
1019        DiscriminatorRule::Contains { substring } => json
1020            .as_str()
1021            .map(|s| s.contains(substring.as_str()))
1022            .unwrap_or(false),
1023        #[cfg(not(feature = "regex"))]
1024        DiscriminatorRule::Regex { .. } => return Err(RenderError::Unsupported("feature `regex`")),
1025        #[cfg(feature = "regex")]
1026        DiscriminatorRule::Regex { regex } => match (json.as_str(), regex::Regex::new(regex)) {
1027            (Some(s), Ok(re)) => re.is_match(s),
1028            _ => false,
1029        },
1030        DiscriminatorRule::FieldEquals(disc) => json
1031            .as_object()
1032            .and_then(|obj| obj.get(disc.field_name.as_str()))
1033            .map(|v| match &disc.literal {
1034                None => true,
1035                Some(lit) => v.as_str().map(|s| s == lit.as_str()).unwrap_or(false),
1036            })
1037            .unwrap_or(false),
1038        DiscriminatorRule::FieldAbsent { field_name } => json
1039            .as_object()
1040            .map(|obj| !obj.contains_key(field_name.as_str()))
1041            .unwrap_or(false),
1042    })
1043}
1044
1045fn rule_label(rule: &DiscriminatorRule) -> String {
1046    match rule {
1047        DiscriminatorRule::Prefix { prefix } => format!("prefix `{prefix}`"),
1048        DiscriminatorRule::Suffix { suffix } => format!("suffix `{suffix}`"),
1049        DiscriminatorRule::Contains { substring } => format!("contains `{substring}`"),
1050        DiscriminatorRule::Regex { regex } => format!("regex `{regex}`"),
1051        DiscriminatorRule::FieldEquals(disc) => match &disc.literal {
1052            None => format!("field `{}` present", disc.field_name),
1053            Some(lit) => format!("field `{}` == `{lit}`", disc.field_name),
1054        },
1055        DiscriminatorRule::FieldAbsent { field_name } => format!("field `{field_name}` absent"),
1056    }
1057}
1058
1059// -------------------------------------------------------------- small bits
1060
1061fn find_variant_case<'a>(
1062    cases: &'a [VariantCaseType],
1063    name: &str,
1064) -> Option<(usize, &'a VariantCaseType)> {
1065    cases.iter().enumerate().find(|(_, c)| c.name == name)
1066}
1067
1068fn json_i64(json: &Value, path: &mut PathStack) -> Result<i64, RenderError> {
1069    json.as_i64()
1070        .ok_or_else(|| mismatch(path, "expected JSON integer".to_string()))
1071}
1072
1073fn json_u64(json: &Value, path: &mut PathStack) -> Result<u64, RenderError> {
1074    json.as_u64()
1075        .ok_or_else(|| mismatch(path, "expected JSON non-negative integer".to_string()))
1076}
1077
1078fn json_i64_string(json: &Value, path: &mut PathStack) -> Result<i64, RenderError> {
1079    let value = json
1080        .as_str()
1081        .ok_or_else(|| mismatch(path, "expected canonical signed integer string".to_string()))?;
1082    if !is_canonical_signed_integer(value) {
1083        return Err(mismatch(
1084            path,
1085            "expected canonical signed integer string".to_string(),
1086        ));
1087    }
1088    value
1089        .parse()
1090        .map_err(|_| mismatch(path, "signed 64-bit integer out of range".to_string()))
1091}
1092
1093fn json_u64_string(json: &Value, path: &mut PathStack) -> Result<u64, RenderError> {
1094    let value = json.as_str().ok_or_else(|| {
1095        mismatch(
1096            path,
1097            "expected canonical unsigned integer string".to_string(),
1098        )
1099    })?;
1100    if value != "0" && !canonical_nonzero_digits(value) {
1101        return Err(mismatch(
1102            path,
1103            "expected canonical unsigned integer string".to_string(),
1104        ));
1105    }
1106    value
1107        .parse()
1108        .map_err(|_| mismatch(path, "unsigned 64-bit integer out of range".to_string()))
1109}
1110
1111fn is_canonical_signed_integer(value: &str) -> bool {
1112    value == "0"
1113        || value
1114            .strip_prefix('-')
1115            .is_some_and(canonical_nonzero_digits)
1116        || canonical_nonzero_digits(value)
1117}
1118
1119fn canonical_nonzero_digits(value: &str) -> bool {
1120    value
1121        .as_bytes()
1122        .first()
1123        .is_some_and(|digit| matches!(digit, b'1'..=b'9'))
1124        && value.bytes().all(|digit| digit.is_ascii_digit())
1125}
1126
1127fn json_f64(json: &Value, path: &mut PathStack) -> Result<f64, RenderError> {
1128    json.as_f64()
1129        .ok_or_else(|| mismatch(path, "expected JSON number".to_string()))
1130}
1131
1132fn json_number_i64(value: i64) -> Value {
1133    Value::Number(Number::from(value))
1134}
1135
1136fn json_number_u64(value: u64) -> Value {
1137    Value::Number(Number::from(value))
1138}
1139
1140fn json_number_f64(value: f64, path: &PathStack) -> Result<Value, RenderError> {
1141    Number::from_f64(value)
1142        .map(Value::Number)
1143        .ok_or_else(|| RenderError::Json(format!("non-finite float at {}", path.render())))
1144}
1145
1146/// Reject f64 inputs whose magnitude exceeds the f32 range, where `value as
1147/// f32` would silently saturate to `±inf`. NaN is rejected because the JSON
1148/// number type cannot represent it on round-trip.
1149fn check_f32_in_range(value: f64, path: &mut PathStack) -> Result<(), RenderError> {
1150    if !value.is_finite() {
1151        return Err(mismatch(path, "f32 must be finite".to_string()));
1152    }
1153    if value.abs() > f32::MAX as f64 {
1154        return Err(mismatch(path, "f32 out of range".to_string()));
1155    }
1156    Ok(())
1157}
1158
1159fn check_int_range<T: TryFrom<i128>>(
1160    json: &Value,
1161    path: &mut PathStack,
1162) -> Result<(), RenderError> {
1163    let n = json
1164        .as_i64()
1165        .map(i128::from)
1166        .or_else(|| json.as_u64().map(i128::from))
1167        .ok_or_else(|| mismatch(path, "expected JSON integer".to_string()))?;
1168    T::try_from(n)
1169        .map(|_| ())
1170        .map_err(|_| mismatch(path, "integer out of range".to_string()))
1171}
1172
1173fn mismatch(path: &PathStack, reason: String) -> RenderError {
1174    RenderError::ValueMismatch {
1175        path: path.render(),
1176        reason,
1177    }
1178}
1179
1180fn type_name(ty: &SchemaType) -> &'static str {
1181    match ty {
1182        SchemaType::Ref { .. } => "ref",
1183        SchemaType::Bool { .. } => "bool",
1184        SchemaType::S8 { .. } => "s8",
1185        SchemaType::S16 { .. } => "s16",
1186        SchemaType::S32 { .. } => "s32",
1187        SchemaType::S64 { .. } => "s64",
1188        SchemaType::U8 { .. } => "u8",
1189        SchemaType::U16 { .. } => "u16",
1190        SchemaType::U32 { .. } => "u32",
1191        SchemaType::U64 { .. } => "u64",
1192        SchemaType::F32 { .. } => "f32",
1193        SchemaType::F64 { .. } => "f64",
1194        SchemaType::Char { .. } => "char",
1195        SchemaType::String { .. } => "string",
1196        SchemaType::Record { .. } => "record",
1197        SchemaType::Variant { .. } => "variant",
1198        SchemaType::Enum { .. } => "enum",
1199        SchemaType::Flags { .. } => "flags",
1200        SchemaType::Tuple { .. } => "tuple",
1201        SchemaType::List { .. } => "list",
1202        SchemaType::FixedList { .. } => "fixed-list",
1203        SchemaType::Map { .. } => "map",
1204        SchemaType::Option { .. } => "option",
1205        SchemaType::Result { .. } => "result",
1206        SchemaType::Text { .. } => "text",
1207        SchemaType::Binary { .. } => "binary",
1208        SchemaType::Path { .. } => "path",
1209        SchemaType::Url { .. } => "url",
1210        SchemaType::Uuid { .. } => "uuid",
1211        SchemaType::Datetime { .. } => "datetime",
1212        SchemaType::Duration { .. } => "duration",
1213        SchemaType::Quantity { .. } => "quantity",
1214        SchemaType::Union { .. } => "union",
1215        SchemaType::Secret { .. } => "secret",
1216        SchemaType::QuotaToken { .. } => "quota-token",
1217        SchemaType::PermissionCard { .. } => "permission-card",
1218        SchemaType::Future { .. } => "future",
1219        SchemaType::Stream { .. } => "stream",
1220    }
1221}
1222
1223fn value_name(value: &SchemaValue) -> &'static str {
1224    match value {
1225        SchemaValue::Bool(_) => "bool",
1226        SchemaValue::S8(_) => "s8",
1227        SchemaValue::S16(_) => "s16",
1228        SchemaValue::S32(_) => "s32",
1229        SchemaValue::S64(_) => "s64",
1230        SchemaValue::U8(_) => "u8",
1231        SchemaValue::U16(_) => "u16",
1232        SchemaValue::U32(_) => "u32",
1233        SchemaValue::U64(_) => "u64",
1234        SchemaValue::F32(_) => "f32",
1235        SchemaValue::F64(_) => "f64",
1236        SchemaValue::Char(_) => "char",
1237        SchemaValue::String(_) => "string",
1238        SchemaValue::Record { .. } => "record",
1239        SchemaValue::Variant(_) => "variant",
1240        SchemaValue::Enum { .. } => "enum",
1241        SchemaValue::Flags { .. } => "flags",
1242        SchemaValue::Tuple { .. } => "tuple",
1243        SchemaValue::List { .. } => "list",
1244        SchemaValue::FixedList { .. } => "fixed-list",
1245        SchemaValue::Map { .. } => "map",
1246        SchemaValue::Option { .. } => "option",
1247        SchemaValue::Result(_) => "result",
1248        SchemaValue::Text(_) => "text",
1249        SchemaValue::Binary(_) => "binary",
1250        SchemaValue::Path { .. } => "path",
1251        SchemaValue::Url { .. } => "url",
1252        SchemaValue::Uuid(_) => "uuid",
1253        SchemaValue::Datetime { .. } => "datetime",
1254        SchemaValue::Duration(_) => "duration",
1255        SchemaValue::Quantity(_) => "quantity",
1256        SchemaValue::Union(_) => "union",
1257        SchemaValue::Secret(_) => "secret",
1258        SchemaValue::QuotaToken(_) => "quota-token",
1259        SchemaValue::Stream(_) => "stream",
1260        SchemaValue::PermissionCard(_) => "permission-card",
1261    }
1262}
1263
1264// ---------------------------------------------------------------- path stack
1265
1266#[derive(Clone, Debug)]
1267enum PathSegment {
1268    Field(String),
1269    Variant(String),
1270    Index(usize),
1271    OptionInner,
1272    Ok,
1273    Err,
1274    Union(String),
1275    MapKey(usize),
1276    MapValue(usize),
1277}
1278
1279struct PathStack {
1280    segments: Vec<PathSegment>,
1281}
1282
1283impl PathStack {
1284    fn new() -> Self {
1285        Self {
1286            segments: Vec::new(),
1287        }
1288    }
1289
1290    fn push(&mut self, segment: PathSegment) {
1291        self.segments.push(segment);
1292    }
1293
1294    fn pop(&mut self) {
1295        self.segments.pop();
1296    }
1297
1298    fn render(&self) -> String {
1299        let mut s = String::new();
1300        for seg in &self.segments {
1301            match seg {
1302                PathSegment::Field(n) => {
1303                    s.push('.');
1304                    s.push_str(n);
1305                }
1306                PathSegment::Variant(n) => {
1307                    s.push('.');
1308                    s.push_str(n);
1309                }
1310                PathSegment::Index(i) => {
1311                    s.push('[');
1312                    s.push_str(&i.to_string());
1313                    s.push(']');
1314                }
1315                PathSegment::OptionInner => s.push_str(".some"),
1316                PathSegment::Ok => s.push_str(".ok"),
1317                PathSegment::Err => s.push_str(".err"),
1318                PathSegment::Union(n) => {
1319                    s.push('.');
1320                    s.push_str(n);
1321                }
1322                PathSegment::MapKey(i) => {
1323                    s.push_str(".key[");
1324                    s.push_str(&i.to_string());
1325                    s.push(']');
1326                }
1327                PathSegment::MapValue(i) => {
1328                    s.push_str(".value[");
1329                    s.push_str(&i.to_string());
1330                    s.push(']');
1331                }
1332            }
1333        }
1334        if s.is_empty() { "$".to_string() } else { s }
1335    }
1336}