Skip to main content

wip_interface/
lib.rs

1//! Public authoring API for Web Interface Protocol (WIP) interfaces.
2//!
3//! Add `wip-interface` to an application and import it under the short name
4//! used by the attribute examples:
5//!
6//! ```
7//! use wip_interface as wip;
8//!
9//! #[derive(Debug, PartialEq, wip::WipType)]
10//! struct Greeting {
11//!     /// Text shown to the recipient.
12//!     message: String,
13//! }
14//!
15//! #[wip::interface]
16//! pub trait Greeter {
17//!     /// Builds a greeting.
18//!     ///
19//!     /// # Returns
20//!     /// The generated greeting.
21//!     fn greet(&self, /// Person to greet.
22//!         name: String) -> wip::OperationResult<Greeting>;
23//! }
24//! ```
25//!
26//! The attribute and derive are re-exported by this facade; applications do
27//! not depend on the implementation-only `wip-interface-macros` crate.
28//!
29//! # Rust to protocol type mapping
30//!
31//! | Rust type | Protocol type/value |
32//! | --- | --- |
33//! | `()` | `Unit` |
34//! | `bool` | `Boolean` |
35//! | signed and unsigned integer primitives | JavaScript-lossless `Integer` |
36//! | `f32`, `f64` | finite `Number` |
37//! | `String` | `String` |
38//! | [`Bytes`] | `Bytes` |
39//! | [`JsonValue`] | schema-neutral `Json` value tree |
40//! | [`Entry`] | canonical absolute Worldspace path (`Entry`) |
41//! | `Vec<T>` | `List<T>` |
42//! | a named-field struct deriving [`WipType`] | named record |
43//! | an all-unit enum deriving [`WipType`] | named enum |
44//! | an enum mixing unit and single-payload cases deriving [`WipType`] | named union |
45//!
46//! `Option<T>` is supported only directly as a record field or operation
47//! parameter. It means omission, not a null value. A domain failure belongs in
48//! an application return enum/union and is returned as `Ok(value)`.
49//! [`HostError`] is reserved for host/runtime failure and requires an explicit
50//! [`HostErrorKind`] plus [`EffectGuarantee`]. The required [`OperationResult`]
51//! convention therefore keeps domain failures separate from runtime failures and
52//! makes outcome uncertainty visible to Host dispatch.
53//!
54//! # Migration to canonical values
55//!
56//! Earlier releases exposed `Entry<K, V>` and semantic `Value::Json`,
57//! `Value::Entry`, `Value::Enum`, and `Value::Union` variants. Those forms are
58//! intentionally gone: [`Entry`] is now one validated absolute Worldspace path,
59//! while JSON, enum, and union meaning comes only from the descriptor. Codecs
60//! exchange only raw [`Value`] strings, records, lists, and primitives. Unit
61//! operation results now cross the boundary as [`Value::Unit`] rather than an
62//! absent value.
63//!
64//! # Supported interface syntax
65//!
66//! An interface is a public, non-generic trait whose operations use exactly
67//! `&self`, simple named owned parameters, and an explicit
68//! `OperationResult<T>`. `WipType` derives support named-field structs,
69//! all-unit enums as protocol enums, and enums made from any mix of unit and
70//! single-payload cases as protocol unions.
71//!
72//! The macros reject trait inheritance and associated items; const, async,
73//! unsafe, extern, variadic, generic, default-bodied, or non-`&self` methods;
74//! borrowed values and custom lifetimes; tuple, unit, or generic structs;
75//! unsupported named-field or multi-field enum variants; explicit discriminants; Rust unions;
76//! and `Option` anywhere except a direct DTO field or operation parameter.
77//! Unsupported inputs produce source-located compile errors rather than
78//! generating a partial interface.
79
80#![deny(missing_docs)]
81
82use std::collections::BTreeMap;
83use std::error::Error;
84use std::fmt::{self, Display, Formatter};
85
86pub use wip_interface_macros::{WipType, interface};
87pub use wip_protocol;
88pub use wip_protocol::{
89    Documentation, EnumCase, FieldDeclaration, InterfaceDescriptor, MAX_SAFE_INTEGER,
90    MIN_SAFE_INTEGER, OperationDeclaration, ParameterDeclaration, ReturnDeclaration,
91    TypeDeclaration, TypeExpr, UnionCase, ValidationError, Value,
92};
93
94/// Converts one Rust application type to and from its protocol representation.
95///
96/// Implementations must describe the same shape accepted by [`WipType::decode`]
97/// and produced by [`WipType::encode`]. Prefer deriving this trait for named
98/// DTOs. Its declaration-collection method exists for generated interface code
99/// and normally should not be called by applications.
100pub trait WipType: Sized {
101    /// Returns the protocol type expression for this Rust type.
102    fn type_expr() -> TypeExpr;
103
104    /// Adds named declarations reachable from this type to `declarations`.
105    ///
106    /// Implementations must be cycle-safe and preserve declaration order.
107    fn collect_declarations(declarations: &mut Vec<TypeDeclaration>);
108
109    /// Encodes this value into the transport-independent protocol value model.
110    fn encode(self) -> Result<Value, CodecError>;
111
112    /// Decodes a previously validated protocol value.
113    fn decode(value: Value) -> Result<Self, CodecError>;
114}
115
116/// Static metadata implemented by every companion generated by
117/// [`interface`].
118pub trait InterfaceDefinition {
119    /// Builds the interface descriptor solely from its trait declaration.
120    fn descriptor() -> InterfaceDescriptor;
121}
122
123/// Typed dispatch implemented by every generated interface companion.
124///
125/// `T` must implement the Rust interface trait associated with this definition.
126/// Host runtimes use this bound when an interface is explicitly exposed, so an
127/// object that does not implement that interface is rejected at compile time.
128pub trait InterfaceImplementation<T>: InterfaceDefinition {
129    /// Validates, decodes, and dispatches one operation to `implementation`,
130    /// then encodes and validates its result.
131    fn dispatch(
132        implementation: &T,
133        operation: &str,
134        arguments: BTreeMap<String, Value>,
135    ) -> Result<Value, DispatchError>;
136}
137
138/// Result convention required for generated interface operations.
139///
140/// `Ok(T)` is an application result, including domain-failure union cases.
141/// `Err(HostError)` means the host or runtime could not perform the operation.
142pub type OperationResult<T> = Result<T, HostError>;
143
144/// Whether an operation failure proves that no effects were committed.
145///
146/// This guarantee is consumed by Host dispatch when choosing a protocol failure:
147/// only [`NotCommitted`](Self::NotCommitted) failures may become `Internal` or
148/// `ResourceLimitExceeded`; [`Unknown`](Self::Unknown) failures must become
149/// `OperationOutcomeUnknown`.
150#[derive(Debug, Clone, Copy, PartialEq, Eq)]
151pub enum EffectGuarantee {
152    /// The operation was not invoked, or every effect was rolled back.
153    NotCommitted,
154    /// The operation may have committed effects.
155    Unknown,
156}
157
158/// Host/runtime failure category used when effects are known not to be committed.
159#[derive(Debug, Clone, Copy, PartialEq, Eq)]
160pub enum HostErrorKind {
161    /// The Host or backend encountered an internal failure.
162    Internal,
163    /// A cancellation, deadline, or bounded resource limit was exceeded.
164    ResourceLimitExceeded,
165}
166
167/// A typed host/runtime failure, kept separate from the operation's application
168/// return type.
169///
170/// The effect guarantee is mandatory. Host runtimes must ignore the diagnostic
171/// when producing a wire response; it is bounded here for local observability but
172/// can still contain backend-private information.
173#[derive(Debug, Clone, PartialEq, Eq)]
174pub struct HostError {
175    kind: HostErrorKind,
176    effect_guarantee: EffectGuarantee,
177    message: String,
178}
179
180impl HostError {
181    /// Creates an internal Host failure with an explicit effect guarantee.
182    #[must_use]
183    pub fn internal(message: impl Into<String>, effect_guarantee: EffectGuarantee) -> Self {
184        Self::new(HostErrorKind::Internal, effect_guarantee, message)
185    }
186
187    /// Creates a resource-limit Host failure with an explicit effect guarantee.
188    #[must_use]
189    pub fn resource_limit(message: impl Into<String>, effect_guarantee: EffectGuarantee) -> Self {
190        Self::new(
191            HostErrorKind::ResourceLimitExceeded,
192            effect_guarantee,
193            message,
194        )
195    }
196
197    fn new(
198        kind: HostErrorKind,
199        effect_guarantee: EffectGuarantee,
200        message: impl Into<String>,
201    ) -> Self {
202        Self {
203            kind,
204            effect_guarantee,
205            message: bounded_diagnostic(message.into()),
206        }
207    }
208
209    /// Returns the typed Host failure category.
210    #[must_use]
211    pub fn kind(&self) -> HostErrorKind {
212        self.kind
213    }
214
215    /// Returns the operation-effect guarantee attached by the implementation.
216    #[must_use]
217    pub fn effect_guarantee(&self) -> EffectGuarantee {
218        self.effect_guarantee
219    }
220
221    /// Returns the bounded local diagnostic message.
222    ///
223    /// Host runtimes must not place this value in protocol responses because it
224    /// can contain backend paths, credentials, or other private details.
225    #[must_use]
226    pub fn message(&self) -> &str {
227        &self.message
228    }
229}
230
231const MAX_DIAGNOSTIC_BYTES: usize = 512;
232
233fn bounded_diagnostic(mut message: String) -> String {
234    if message.len() <= MAX_DIAGNOSTIC_BYTES {
235        return message;
236    }
237    let mut end = MAX_DIAGNOSTIC_BYTES;
238    while !message.is_char_boundary(end) {
239        end -= 1;
240    }
241    message.truncate(end);
242    message
243}
244
245impl Display for HostError {
246    fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
247        formatter.write_str(&self.message)
248    }
249}
250
251impl Error for HostError {}
252
253/// A failure while converting between a Rust value and a protocol [`Value`].
254#[derive(Debug, Clone, PartialEq, Eq)]
255pub struct CodecError {
256    path: Vec<String>,
257    message: String,
258}
259
260impl CodecError {
261    /// Creates a codec failure at the current value.
262    #[must_use]
263    pub fn new(message: impl Into<String>) -> Self {
264        Self {
265            path: Vec::new(),
266            message: message.into(),
267        }
268    }
269
270    /// Creates a structural-kind mismatch diagnostic.
271    #[must_use]
272    pub fn expected(expected: &str, actual: &Value) -> Self {
273        Self::new(format!("expected {expected}, found {:?}", actual.kind()))
274    }
275
276    /// Prepends a field, variant, or collection position to the error path.
277    #[must_use]
278    pub fn at(mut self, segment: impl Into<String>) -> Self {
279        self.path.insert(0, segment.into());
280        self
281    }
282
283    /// Returns the path from the decoded root to the failing value.
284    #[must_use]
285    pub fn path(&self) -> &[String] {
286        &self.path
287    }
288
289    /// Returns the human-readable reason for the failure.
290    #[must_use]
291    pub fn message(&self) -> &str {
292        &self.message
293    }
294}
295
296impl Display for CodecError {
297    fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
298        if self.path.is_empty() {
299            write!(formatter, "WIP codec error: {}", self.message)
300        } else {
301            write!(
302                formatter,
303                "WIP codec error at {}: {}",
304                self.path.join("."),
305                self.message
306            )
307        }
308    }
309}
310
311impl Error for CodecError {}
312
313/// A failure produced by macro-generated typed operation dispatch.
314#[derive(Debug)]
315pub enum DispatchError {
316    /// The requested operation is not declared by this interface.
317    UnknownOperation(String),
318    /// Protocol validation rejected the named arguments.
319    InvalidArguments(wip_protocol::ValidationError),
320    /// A validated value could not be decoded to its Rust type.
321    Decode(CodecError),
322    /// The implementation reported a host/runtime failure.
323    Host(HostError),
324    /// The implementation panicked after invocation began.
325    ///
326    /// Panic payloads are deliberately discarded because they can contain
327    /// backend-private diagnostics.
328    Panic,
329    /// The Rust result could not be encoded.
330    Encode(CodecError),
331    /// The encoded result did not match the generated declaration.
332    InvalidResult(wip_protocol::ValidationError),
333}
334
335/// Whether generated dispatch failed before or after typed implementation
336/// invocation began.
337#[derive(Debug, Clone, Copy, PartialEq, Eq)]
338pub enum DispatchPhase {
339    /// Typed implementation code was not invoked.
340    BeforeInvocation,
341    /// Typed implementation invocation began, so effects require an explicit
342    /// non-commit or rollback guarantee.
343    AfterInvocation,
344}
345
346impl DispatchError {
347    /// Returns the execution phase in which this dispatch failure occurred.
348    #[must_use]
349    pub fn phase(&self) -> DispatchPhase {
350        match self {
351            Self::UnknownOperation(_) | Self::InvalidArguments(_) | Self::Decode(_) => {
352                DispatchPhase::BeforeInvocation
353            }
354            Self::Host(_) | Self::Panic | Self::Encode(_) | Self::InvalidResult(_) => {
355                DispatchPhase::AfterInvocation
356            }
357        }
358    }
359
360    /// Returns a post-invocation effect guarantee when the implementation
361    /// supplied one.
362    ///
363    /// Panic, encode, and result-validation failures default to `Unknown` until
364    /// a provider confirms rollback. Pre-invocation failures return `None`.
365    #[must_use]
366    pub fn effect_guarantee(&self) -> Option<EffectGuarantee> {
367        match self {
368            Self::UnknownOperation(_) | Self::InvalidArguments(_) | Self::Decode(_) => None,
369            Self::Host(error) => Some(error.effect_guarantee()),
370            Self::Panic | Self::Encode(_) | Self::InvalidResult(_) => {
371                Some(EffectGuarantee::Unknown)
372            }
373        }
374    }
375}
376
377impl Display for DispatchError {
378    fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
379        match self {
380            Self::UnknownOperation(operation) => {
381                write!(formatter, "unknown operation `{operation}`")
382            }
383            Self::InvalidArguments(error) => write!(formatter, "invalid arguments: {error}"),
384            Self::Decode(error) => write!(formatter, "argument decoding failed: {error}"),
385            Self::Host(error) => write!(formatter, "host operation failed: {error}"),
386            Self::Panic => formatter.write_str("operation implementation panicked"),
387            Self::Encode(error) => write!(formatter, "result encoding failed: {error}"),
388            Self::InvalidResult(error) => write!(formatter, "invalid operation result: {error}"),
389        }
390    }
391}
392
393impl Error for DispatchError {
394    fn source(&self) -> Option<&(dyn Error + 'static)> {
395        match self {
396            Self::UnknownOperation(_) | Self::Panic => None,
397            Self::InvalidArguments(error) | Self::InvalidResult(error) => Some(error),
398            Self::Decode(error) | Self::Encode(error) => Some(error),
399            Self::Host(error) => Some(error),
400        }
401    }
402}
403
404/// An explicitly byte-oriented sequence.
405///
406/// Use this wrapper when the protocol type must be `Bytes`; `Vec<T>` always
407/// denotes a protocol list, including `Vec<u8>`.
408#[derive(Debug, Clone, PartialEq, Eq, Default)]
409pub struct Bytes(pub Vec<u8>);
410
411impl From<Vec<u8>> for Bytes {
412    fn from(value: Vec<u8>) -> Self {
413        Self(value)
414    }
415}
416
417impl From<Bytes> for Vec<u8> {
418    fn from(value: Bytes) -> Self {
419        value.0
420    }
421}
422
423/// A canonical absolute path to an entry in the current Worldspace.
424///
425/// This replaces the pre-canonical `Entry<K, V>` key/value pair. Construct an
426/// entry from a path such as `/articles/1`; application data associated with
427/// that entry belongs in an ordinary record alongside this value.
428#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
429pub struct Entry(String);
430
431impl Entry {
432    /// Validates and creates an entry path.
433    pub fn new(path: impl Into<String>) -> Result<Self, CodecError> {
434        let path = path.into();
435        validate_entry_path(&path)?;
436        Ok(Self(path))
437    }
438
439    /// Returns the canonical absolute Worldspace path.
440    #[must_use]
441    pub fn as_str(&self) -> &str {
442        &self.0
443    }
444
445    /// Consumes the entry and returns its path.
446    #[must_use]
447    pub fn into_string(self) -> String {
448        self.0
449    }
450}
451
452impl TryFrom<String> for Entry {
453    type Error = CodecError;
454
455    fn try_from(path: String) -> Result<Self, Self::Error> {
456        Self::new(path)
457    }
458}
459
460impl std::str::FromStr for Entry {
461    type Err = CodecError;
462
463    fn from_str(path: &str) -> Result<Self, Self::Err> {
464        Self::new(path)
465    }
466}
467
468/// One member in a [`JsonValue::Object`].
469#[derive(Debug, Clone, PartialEq)]
470pub struct JsonMember {
471    /// Member name.
472    pub name: String,
473    /// Member value.
474    pub value: JsonValue,
475}
476
477/// JSON data represented independently of any application schema.
478///
479/// Its codec uses the ordinary schema-neutral [`Value`] variants. JSON null is
480/// [`Value::Unit`], arrays are [`Value::List`], and objects are
481/// [`Value::Record`]. Protocol bytes have no JSON representation and decoding
482/// a tree containing [`Value::Bytes`] is rejected.
483#[derive(Debug, Clone, PartialEq)]
484pub enum JsonValue {
485    /// JSON null.
486    Null,
487    /// A JSON boolean.
488    Boolean(bool),
489    /// An interoperable integral JSON number.
490    Integer(i64),
491    /// A finite non-integral JSON number.
492    Number(f64),
493    /// A JSON string.
494    String(String),
495    /// A JSON array.
496    Array(Vec<JsonValue>),
497    /// A JSON object. Duplicate member names are rejected while encoding.
498    Object(Vec<JsonMember>),
499}
500
501impl WipType for () {
502    fn type_expr() -> TypeExpr {
503        TypeExpr::Unit
504    }
505
506    fn collect_declarations(_: &mut Vec<TypeDeclaration>) {}
507
508    fn encode(self) -> Result<Value, CodecError> {
509        Ok(Value::Unit)
510    }
511
512    fn decode(value: Value) -> Result<Self, CodecError> {
513        match value {
514            Value::Unit => Ok(()),
515            other => Err(CodecError::expected("unit", &other)),
516        }
517    }
518}
519
520impl WipType for bool {
521    fn type_expr() -> TypeExpr {
522        TypeExpr::Boolean
523    }
524
525    fn collect_declarations(_: &mut Vec<TypeDeclaration>) {}
526
527    fn encode(self) -> Result<Value, CodecError> {
528        Ok(Value::Boolean(self))
529    }
530
531    fn decode(value: Value) -> Result<Self, CodecError> {
532        match value {
533            Value::Boolean(value) => Ok(value),
534            other => Err(CodecError::expected("boolean", &other)),
535        }
536    }
537}
538
539macro_rules! integer_codec {
540    ($($ty:ty),+ $(,)?) => {
541        $(
542            impl WipType for $ty {
543                fn type_expr() -> TypeExpr {
544                    TypeExpr::Integer
545                }
546
547                fn collect_declarations(_: &mut Vec<TypeDeclaration>) {}
548
549                fn encode(self) -> Result<Value, CodecError> {
550                    let value = i64::try_from(self).map_err(|_| {
551                        CodecError::new("integer is outside the protocol i64 range")
552                    })?;
553                    ensure_safe_integer(value)?;
554                    Ok(Value::Integer(value))
555                }
556
557                fn decode(value: Value) -> Result<Self, CodecError> {
558                    let Value::Integer(value) = value else {
559                        return Err(CodecError::expected("integer", &value));
560                    };
561                    ensure_safe_integer(value)?;
562                    Self::try_from(value).map_err(|_| {
563                        CodecError::new(concat!("integer does not fit Rust `", stringify!($ty), "`"))
564                    })
565                }
566            }
567        )+
568    };
569}
570
571integer_codec!(
572    i8, i16, i32, i64, i128, isize, u8, u16, u32, u64, u128, usize
573);
574
575fn ensure_safe_integer(value: i64) -> Result<(), CodecError> {
576    if (MIN_SAFE_INTEGER..=MAX_SAFE_INTEGER).contains(&value) {
577        Ok(())
578    } else {
579        Err(CodecError::new(format!(
580            "integer {value} is outside the JavaScript-lossless protocol range"
581        )))
582    }
583}
584
585impl WipType for f64 {
586    fn type_expr() -> TypeExpr {
587        TypeExpr::Number
588    }
589
590    fn collect_declarations(_: &mut Vec<TypeDeclaration>) {}
591
592    fn encode(self) -> Result<Value, CodecError> {
593        ensure_finite(self)?;
594        Ok(Value::Number(self))
595    }
596
597    fn decode(value: Value) -> Result<Self, CodecError> {
598        match value {
599            Value::Number(value) => {
600                ensure_finite(value)?;
601                Ok(value)
602            }
603            other => Err(CodecError::expected("number", &other)),
604        }
605    }
606}
607
608impl WipType for f32 {
609    fn type_expr() -> TypeExpr {
610        TypeExpr::Number
611    }
612
613    fn collect_declarations(_: &mut Vec<TypeDeclaration>) {}
614
615    fn encode(self) -> Result<Value, CodecError> {
616        let value = f64::from(self);
617        ensure_finite(value)?;
618        Ok(Value::Number(value))
619    }
620
621    fn decode(value: Value) -> Result<Self, CodecError> {
622        let value = f64::decode(value)?;
623        let narrowed = value as Self;
624        if narrowed.is_finite() {
625            Ok(narrowed)
626        } else {
627            Err(CodecError::new("number does not fit finite Rust `f32`"))
628        }
629    }
630}
631
632fn ensure_finite(value: f64) -> Result<(), CodecError> {
633    if value.is_finite() {
634        Ok(())
635    } else {
636        Err(CodecError::new("protocol numbers must be finite"))
637    }
638}
639
640impl WipType for String {
641    fn type_expr() -> TypeExpr {
642        TypeExpr::String
643    }
644
645    fn collect_declarations(_: &mut Vec<TypeDeclaration>) {}
646
647    fn encode(self) -> Result<Value, CodecError> {
648        Ok(Value::String(self))
649    }
650
651    fn decode(value: Value) -> Result<Self, CodecError> {
652        match value {
653            Value::String(value) => Ok(value),
654            other => Err(CodecError::expected("string", &other)),
655        }
656    }
657}
658
659impl WipType for Bytes {
660    fn type_expr() -> TypeExpr {
661        TypeExpr::Bytes
662    }
663
664    fn collect_declarations(_: &mut Vec<TypeDeclaration>) {}
665
666    fn encode(self) -> Result<Value, CodecError> {
667        Ok(Value::Bytes(self.0))
668    }
669
670    fn decode(value: Value) -> Result<Self, CodecError> {
671        match value {
672            Value::Bytes(value) => Ok(Self(value)),
673            other => Err(CodecError::expected("bytes", &other)),
674        }
675    }
676}
677
678impl WipType for JsonValue {
679    fn type_expr() -> TypeExpr {
680        TypeExpr::Json
681    }
682
683    fn collect_declarations(_: &mut Vec<TypeDeclaration>) {}
684
685    fn encode(self) -> Result<Value, CodecError> {
686        encode_json(self)
687    }
688
689    fn decode(value: Value) -> Result<Self, CodecError> {
690        decode_json(value)
691    }
692}
693
694fn encode_json(value: JsonValue) -> Result<Value, CodecError> {
695    match value {
696        JsonValue::Null => Ok(Value::Unit),
697        JsonValue::Boolean(value) => Ok(Value::Boolean(value)),
698        JsonValue::Integer(value) => {
699            ensure_safe_integer(value)?;
700            Ok(Value::Integer(value))
701        }
702        JsonValue::Number(value) => {
703            ensure_finite(value)?;
704            Ok(Value::Number(value))
705        }
706        JsonValue::String(value) => Ok(Value::String(value)),
707        JsonValue::Array(values) => values
708            .into_iter()
709            .enumerate()
710            .map(|(index, value)| encode_json(value).map_err(|error| error.at(index.to_string())))
711            .collect::<Result<Vec<_>, _>>()
712            .map(Value::List),
713        JsonValue::Object(members) => {
714            let mut fields = BTreeMap::new();
715            for member in members {
716                let name = member.name;
717                let value = encode_json(member.value).map_err(|error| error.at(name.clone()))?;
718                if fields.insert(name.clone(), value).is_some() {
719                    return Err(CodecError::new(format!("duplicate JSON member `{name}`")).at(name));
720                }
721            }
722            Ok(Value::Record(fields))
723        }
724    }
725}
726
727fn decode_json(value: Value) -> Result<JsonValue, CodecError> {
728    match value {
729        Value::Unit => Ok(JsonValue::Null),
730        Value::Boolean(value) => Ok(JsonValue::Boolean(value)),
731        Value::Integer(value) => {
732            ensure_safe_integer(value)?;
733            Ok(JsonValue::Integer(value))
734        }
735        Value::Number(value) => {
736            ensure_finite(value)?;
737            Ok(JsonValue::Number(value))
738        }
739        Value::String(value) => Ok(JsonValue::String(value)),
740        Value::Bytes(_) => Err(CodecError::new("bytes are not valid inside JSON")),
741        Value::List(values) => values
742            .into_iter()
743            .enumerate()
744            .map(|(index, value)| decode_json(value).map_err(|error| error.at(index.to_string())))
745            .collect::<Result<Vec<_>, _>>()
746            .map(JsonValue::Array),
747        Value::Record(fields) => fields
748            .into_iter()
749            .map(|(name, value)| {
750                decode_json(value)
751                    .map(|value| JsonMember {
752                        name: name.clone(),
753                        value,
754                    })
755                    .map_err(|error| error.at(name))
756            })
757            .collect::<Result<Vec<_>, _>>()
758            .map(JsonValue::Object),
759    }
760}
761
762impl<T: WipType> WipType for Vec<T> {
763    fn type_expr() -> TypeExpr {
764        TypeExpr::List {
765            items: Box::new(T::type_expr()),
766        }
767    }
768
769    fn collect_declarations(declarations: &mut Vec<TypeDeclaration>) {
770        T::collect_declarations(declarations);
771    }
772
773    fn encode(self) -> Result<Value, CodecError> {
774        self.into_iter()
775            .enumerate()
776            .map(|(index, value)| value.encode().map_err(|error| error.at(index.to_string())))
777            .collect::<Result<Vec<_>, _>>()
778            .map(Value::List)
779    }
780
781    fn decode(value: Value) -> Result<Self, CodecError> {
782        let Value::List(values) = value else {
783            return Err(CodecError::expected("list", &value));
784        };
785        values
786            .into_iter()
787            .enumerate()
788            .map(|(index, value)| T::decode(value).map_err(|error| error.at(index.to_string())))
789            .collect()
790    }
791}
792
793impl WipType for Entry {
794    fn type_expr() -> TypeExpr {
795        TypeExpr::Entry
796    }
797
798    fn collect_declarations(_: &mut Vec<TypeDeclaration>) {}
799
800    fn encode(self) -> Result<Value, CodecError> {
801        validate_entry_path(&self.0)?;
802        Ok(Value::String(self.0))
803    }
804
805    fn decode(value: Value) -> Result<Self, CodecError> {
806        let Value::String(path) = value else {
807            return Err(CodecError::expected("entry path string", &value));
808        };
809        Self::new(path)
810    }
811}
812
813fn validate_entry_path(path: &str) -> Result<(), CodecError> {
814    wip_protocol::validate_path(path)
815        .map_err(|error| CodecError::new(format!("invalid canonical Worldspace path: {error}")))
816}
817
818/// Implementation details used by code emitted from the facade's macros.
819///
820/// These APIs are public only because macro expansions execute in the
821/// application crate. They are excluded from normal generated documentation
822/// and are not intended for direct use.
823#[doc(hidden)]
824pub mod __private {
825    use std::collections::BTreeMap;
826
827    use wip_protocol::{InterfaceDescriptor, TypeDeclaration, ValidationError, Value};
828
829    use crate::{CodecError, Documentation, WipType};
830
831    /// Builds optional descriptor documentation from generated string literals.
832    pub fn documentation(summary: &str, details: Option<&str>) -> Option<Documentation> {
833        if summary.is_empty() && details.is_none() {
834            None
835        } else {
836            Some(Documentation {
837                summary: summary.into(),
838                details: details.map(str::to_owned),
839            })
840        }
841    }
842
843    /// Registers one declaration and reports whether its dependencies should
844    /// be traversed. Identical declarations are deduplicated; a conflicting
845    /// declaration with the same name is retained for protocol validation.
846    pub fn register_declaration(
847        declarations: &mut Vec<TypeDeclaration>,
848        declaration: TypeDeclaration,
849    ) -> bool {
850        if declarations.iter().any(|existing| existing == &declaration) {
851            false
852        } else {
853            declarations.push(declaration);
854            true
855        }
856    }
857
858    /// Validates named arguments against one generated operation declaration.
859    pub fn validate_arguments(
860        descriptor: &InterfaceDescriptor,
861        operation: &str,
862        arguments: &BTreeMap<String, Value>,
863    ) -> Result<(), ValidationError> {
864        descriptor.validate_arguments(operation, arguments)
865    }
866
867    /// Builds a codec error for a generated structural mismatch.
868    pub fn decode_type_mismatch(expected: &str, actual: Value) -> CodecError {
869        CodecError::expected(expected, &actual)
870    }
871
872    /// Builds a codec error for an unknown generated enum or union variant.
873    pub fn decode_unknown_variant(type_name: &str, variant: String) -> CodecError {
874        CodecError::new(format!(
875            "unknown variant `{variant}` for WIP type `{type_name}`"
876        ))
877        .at(variant)
878    }
879
880    /// Decoder for a protocol record or operation's named argument collection.
881    pub struct RecordDecoder {
882        fields: BTreeMap<String, Value>,
883    }
884
885    impl RecordDecoder {
886        /// Starts decoding record fields or named operation arguments.
887        pub fn new(fields: BTreeMap<String, Value>) -> Self {
888            Self { fields }
889        }
890
891        /// Decodes a required field.
892        pub fn required<T: WipType>(&mut self, name: &str) -> Result<T, CodecError> {
893            let value = self.take(name).ok_or_else(|| {
894                CodecError::new(format!("missing required field `{name}`")).at(name)
895            })?;
896            T::decode(value).map_err(|error| error.at(name))
897        }
898
899        /// Decodes an optional field, where absence maps to `None`.
900        pub fn optional<T: WipType>(&mut self, name: &str) -> Result<Option<T>, CodecError> {
901            self.take(name)
902                .map(T::decode)
903                .transpose()
904                .map_err(|error| error.at(name))
905        }
906
907        /// Rejects any fields that were not consumed by generated code.
908        pub fn finish(self) -> Result<(), CodecError> {
909            if let Some(name) = self.fields.keys().next() {
910                Err(CodecError::new(format!("unknown field `{name}`")).at(name.clone()))
911            } else {
912                Ok(())
913            }
914        }
915
916        fn take(&mut self, name: &str) -> Option<Value> {
917            self.fields.remove(name)
918        }
919    }
920}