Skip to main content

zynk_runtime/
lib.rs

1//! Pure wire-contract types for Zynk server runtimes.
2//!
3//! This crate deliberately contains no HTTP framework integration. Server
4//! bindings such as Axum use these data structures to register endpoints,
5//! dispatch type-erased handlers, and preserve Zynk's JSON/SSE/WebSocket wire
6//! shapes.
7
8use serde::{Deserialize, Serialize};
9use serde_json::Value;
10use thiserror::Error;
11use zynk_schema::{TypeKind, TypeRef};
12
13pub use inventory;
14pub use zynk_schema::{self, EndpointKind};
15
16/// Request validation failed.
17pub const VALIDATION_ERROR: &str = "VALIDATION_ERROR";
18/// Requested RPC/channel command was not registered.
19pub const COMMAND_NOT_FOUND: &str = "COMMAND_NOT_FOUND";
20/// Endpoint handler returned an execution error.
21pub const EXECUTION_ERROR: &str = "EXECUTION_ERROR";
22/// Channel operation failed.
23pub const CHANNEL_ERROR: &str = "CHANNEL_ERROR";
24/// Unexpected internal runtime error.
25pub const INTERNAL_ERROR: &str = "INTERNAL_ERROR";
26/// WebSocket operation failed.
27pub const WEBSOCKET_ERROR: &str = "WEBSOCKET_ERROR";
28/// Requested WebSocket message handler was not registered.
29pub const HANDLER_NOT_FOUND: &str = "HANDLER_NOT_FOUND";
30/// Requested upload handler was not registered.
31pub const UPLOAD_HANDLER_NOT_FOUND: &str = "UPLOAD_HANDLER_NOT_FOUND";
32/// Upload request or file validation failed.
33pub const UPLOAD_VALIDATION_ERROR: &str = "UPLOAD_VALIDATION_ERROR";
34/// Requested static file handler was not registered.
35pub const STATIC_HANDLER_NOT_FOUND: &str = "STATIC_HANDLER_NOT_FOUND";
36
37/// All Python-compatible error code spellings in canonical order.
38pub const ERROR_CODES: [&str; 10] = [
39    VALIDATION_ERROR,
40    COMMAND_NOT_FOUND,
41    EXECUTION_ERROR,
42    CHANNEL_ERROR,
43    INTERNAL_ERROR,
44    WEBSOCKET_ERROR,
45    HANDLER_NOT_FOUND,
46    UPLOAD_HANDLER_NOT_FOUND,
47    UPLOAD_VALIDATION_ERROR,
48    STATIC_HANDLER_NOT_FOUND,
49];
50
51/// Success envelope for JSON endpoint responses: `{ "result": T }`.
52#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
53pub struct JsonResultEnvelope<T> {
54    /// Successful endpoint result payload.
55    pub result: T,
56}
57
58impl<T> JsonResultEnvelope<T> {
59    /// Wrap a successful result in Zynk's JSON envelope.
60    pub fn new(result: T) -> Self {
61        Self { result }
62    }
63}
64
65/// Error envelope payload for JSON endpoint failures.
66#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
67pub struct JsonErrorEnvelope {
68    /// Machine-readable Zynk error code.
69    pub code: String,
70    /// Human-readable error message.
71    pub message: String,
72    /// Optional structured error details, omitted from JSON when absent.
73    #[serde(skip_serializing_if = "Option::is_none")]
74    pub details: Option<Value>,
75}
76
77impl JsonErrorEnvelope {
78    /// Create an error envelope payload.
79    pub fn new(code: impl Into<String>, message: impl Into<String>) -> Self {
80        Self {
81            code: code.into(),
82            message: message.into(),
83            details: None,
84        }
85    }
86
87    /// Attach structured details to an error envelope payload.
88    pub fn with_details(mut self, details: Value) -> Self {
89        self.details = Some(details);
90        self
91    }
92}
93
94/// Outer error response shape for surfaces that wrap errors under `error`.
95#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
96pub struct JsonErrorResponseEnvelope {
97    /// Error payload.
98    pub error: JsonErrorEnvelope,
99}
100
101impl JsonErrorResponseEnvelope {
102    /// Wrap an error payload in the outer error response envelope.
103    pub fn new(error: JsonErrorEnvelope) -> Self {
104        Self { error }
105    }
106}
107
108/// Runtime error type that can be converted into a JSON error envelope.
109#[derive(Debug, Clone, PartialEq, Error)]
110#[error("{code}: {message}")]
111pub struct ZynkError {
112    /// Machine-readable Zynk error code.
113    pub code: &'static str,
114    /// Human-readable error message.
115    pub message: String,
116    /// Optional structured error details.
117    pub details: Option<Value>,
118}
119
120impl ZynkError {
121    /// Create a runtime error without structured details.
122    pub fn new(code: &'static str, message: impl Into<String>) -> Self {
123        Self {
124            code,
125            message: message.into(),
126            details: None,
127        }
128    }
129
130    /// Create a runtime error with structured details.
131    pub fn with_details(code: &'static str, message: impl Into<String>, details: Value) -> Self {
132        Self {
133            code,
134            message: message.into(),
135            details: Some(details),
136        }
137    }
138
139    /// Convert this runtime error into the JSON error envelope payload.
140    pub fn into_envelope(self) -> JsonErrorEnvelope {
141        JsonErrorEnvelope {
142            code: self.code.to_string(),
143            message: self.message,
144            details: self.details,
145        }
146    }
147}
148
149/// Type-erased endpoint handler contract used by server bindings.
150pub trait Handler: Send + Sync + 'static {
151    /// Invoke the handler with a JSON object payload and return a JSON value.
152    fn call(&self, payload: Value) -> Result<Value, ZynkError>;
153}
154
155impl<F> Handler for F
156where
157    F: Fn(Value) -> Result<Value, ZynkError> + Send + Sync + 'static,
158{
159    fn call(&self, payload: Value) -> Result<Value, ZynkError> {
160        self(payload)
161    }
162}
163
164/// Registration key for a type-erased handler stored by a server binding.
165#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
166pub struct HandlerKey(pub &'static str);
167
168/// Static type metadata used by proc macros in link-time endpoint registration.
169#[derive(Debug, Clone, PartialEq)]
170pub enum StaticValue {
171    /// JSON null literal.
172    Null,
173    /// Boolean literal.
174    Bool(bool),
175    /// Signed integer literal.
176    I64(i64),
177    /// Unsigned integer literal.
178    U64(u64),
179    /// Floating-point literal.
180    F64(f64),
181    /// String literal.
182    Str(&'static str),
183}
184
185impl StaticValue {
186    /// Convert static literal metadata into an owned JSON value.
187    pub fn to_json(&self) -> Value {
188        match self {
189            Self::Null => Value::Null,
190            Self::Bool(value) => Value::Bool(*value),
191            Self::I64(value) => Value::Number((*value).into()),
192            Self::U64(value) => Value::Number((*value).into()),
193            Self::F64(value) => serde_json::Number::from_f64(*value)
194                .map(Value::Number)
195                .unwrap_or(Value::Null),
196            Self::Str(value) => Value::String((*value).to_string()),
197        }
198    }
199}
200
201/// Static type metadata used by proc macros in link-time endpoint registration.
202#[derive(Debug, Clone, PartialEq)]
203pub struct TypeRefStatic {
204    /// Language-neutral type category.
205    pub kind: TypeKind,
206    /// Primitive/model/enum name, when applicable.
207    pub name: Option<&'static str>,
208    /// Nested type references for arrays, records, tuples, and unions.
209    pub inner: &'static [TypeRefStatic],
210    /// Whether the value may be omitted.
211    pub optional: bool,
212    /// Whether the value may be null.
213    pub nullable: bool,
214    /// Literal value metadata, when applicable.
215    pub value: Option<StaticValue>,
216}
217
218impl TypeRefStatic {
219    /// Return a copy of this metadata with optionality enabled.
220    pub const fn optional(mut self) -> Self {
221        self.optional = true;
222        self
223    }
224
225    /// Return a copy of this metadata with nullability enabled.
226    pub const fn nullable(mut self) -> Self {
227        self.nullable = true;
228        self
229    }
230
231    /// Convert static metadata into the canonical schema type reference.
232    pub fn to_schema_type_ref(&self) -> TypeRef {
233        TypeRef {
234            kind: self.kind.clone(),
235            name: self.name.map(str::to_string),
236            inner: self
237                .inner
238                .iter()
239                .map(TypeRefStatic::to_schema_type_ref)
240                .collect(),
241            optional: self.optional,
242            nullable: self.nullable,
243            value: self.value.as_ref().map(StaticValue::to_json),
244        }
245    }
246
247    /// Construct static type metadata for a primitive type.
248    pub const fn primitive(name: &'static str) -> Self {
249        Self {
250            kind: TypeKind::Primitive,
251            name: Some(name),
252            inner: &[],
253            optional: false,
254            nullable: false,
255            value: None,
256        }
257    }
258
259    /// Construct static type metadata for a model reference.
260    pub const fn model(name: &'static str) -> Self {
261        Self {
262            kind: TypeKind::Model,
263            name: Some(name),
264            inner: &[],
265            optional: false,
266            nullable: false,
267            value: None,
268        }
269    }
270
271    /// Construct static type metadata for an enum reference.
272    pub const fn enum_ref(name: &'static str) -> Self {
273        Self {
274            kind: TypeKind::Enum,
275            name: Some(name),
276            inner: &[],
277            optional: false,
278            nullable: false,
279            value: None,
280        }
281    }
282
283    /// Construct static type metadata for an array item type.
284    ///
285    /// Pass a one-element slice containing the item type.
286    pub const fn array(item: &'static [TypeRefStatic]) -> Self {
287        Self {
288            kind: TypeKind::Array,
289            name: None,
290            inner: item,
291            optional: false,
292            nullable: false,
293            value: None,
294        }
295    }
296
297    /// Construct static type metadata for a union of member types.
298    pub const fn union(members: &'static [TypeRefStatic]) -> Self {
299        Self {
300            kind: TypeKind::Union,
301            name: None,
302            inner: members,
303            optional: false,
304            nullable: false,
305            value: None,
306        }
307    }
308
309    /// Construct static type metadata for a literal value.
310    pub const fn literal(value: StaticValue) -> Self {
311        Self {
312            kind: TypeKind::Literal,
313            name: None,
314            inner: &[],
315            optional: false,
316            nullable: false,
317            value: Some(value),
318        }
319    }
320
321    /// Construct static type metadata for an unconstrained JSON value.
322    pub const fn any() -> Self {
323        Self {
324            kind: TypeKind::Any,
325            name: None,
326            inner: &[],
327            optional: false,
328            nullable: false,
329            value: None,
330        }
331    }
332
333    /// Construct static type metadata for a void return type.
334    pub const fn void() -> Self {
335        Self {
336            kind: TypeKind::Void,
337            name: None,
338            inner: &[],
339            optional: false,
340            nullable: false,
341            value: None,
342        }
343    }
344}
345
346/// Static parameter metadata used by `EndpointMeta`.
347#[derive(Debug, Clone, PartialEq)]
348pub struct ParamMeta {
349    /// Source-language parameter name.
350    pub source_name: &'static str,
351    /// Wire-format parameter name.
352    pub wire_name: &'static str,
353    /// Static type metadata for this parameter.
354    pub ty: TypeRefStatic,
355    /// Whether this parameter is required.
356    pub required: bool,
357    /// Optional static JSON default value.
358    pub default: Option<StaticValue>,
359}
360
361/// Link-time endpoint registration metadata produced by proc macros.
362#[derive(Debug, Clone, PartialEq)]
363pub struct EndpointMeta {
364    /// Stable endpoint name.
365    pub name: &'static str,
366    /// Endpoint surface kind.
367    pub kind: EndpointKind,
368    /// Optional source module for introspection/schema output.
369    pub module: Option<&'static str>,
370    /// Optional documentation text.
371    pub doc: Option<&'static str>,
372    /// Callable parameter metadata.
373    pub params: &'static [ParamMeta],
374    /// Return type metadata.
375    pub returns: TypeRefStatic,
376    /// Channel item type for channel endpoints.
377    pub channel_item: Option<TypeRefStatic>,
378    /// Upload file parameter name for upload endpoints.
379    pub file_param: Option<&'static str>,
380    /// Whether an upload endpoint accepts multiple files.
381    pub multi_file: bool,
382    /// Maximum upload size in bytes.
383    pub max_size: Option<u64>,
384    /// Allowed upload content types.
385    pub allowed_types: &'static [&'static str],
386    /// WebSocket server event metadata.
387    pub server_events: &'static [ParamMeta],
388    /// WebSocket client event metadata.
389    pub client_events: &'static [ParamMeta],
390    /// Type-erased handler registration key for dispatch tables.
391    pub handler_key: Option<HandlerKey>,
392}
393
394inventory::collect!(EndpointMeta);
395
396/// One Server-Sent Events frame: `event: <name>` / `data: <json>`.
397#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
398#[serde(rename_all = "camelCase")]
399pub struct SseFrame {
400    /// SSE event name.
401    pub event: String,
402    /// JSON payload for the `data:` line.
403    pub data: Value,
404}
405
406impl SseFrame {
407    /// Create an SSE frame.
408    pub fn new(event: impl Into<String>, data: Value) -> Self {
409        Self {
410            event: event.into(),
411            data,
412        }
413    }
414
415    /// Encode this frame in the exact Zynk SSE wire format.
416    pub fn encode(&self) -> String {
417        format!(
418            "event: {}\ndata: {}\n\n",
419            self.event,
420            python_json_dumps(&self.data)
421        )
422    }
423}
424
425fn python_json_dumps(value: &Value) -> String {
426    match value {
427        Value::Array(items) => {
428            let inner = items
429                .iter()
430                .map(python_json_dumps)
431                .collect::<Vec<_>>()
432                .join(", ");
433            format!("[{inner}]")
434        }
435        Value::Object(object) => {
436            let inner = object
437                .iter()
438                .map(|(key, value)| {
439                    format!(
440                        "{}: {}",
441                        python_json_dumps(&Value::String(key.clone())),
442                        python_json_dumps(value)
443                    )
444                })
445                .collect::<Vec<_>>()
446                .join(", ");
447            format!("{{{inner}}}")
448        }
449        _ => serde_json::to_string(value).expect("JSON value serialization cannot fail"),
450    }
451}
452
453/// One WebSocket JSON text-frame message: `{ "event": name, "data": payload }`.
454#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
455pub struct WsMessage {
456    /// Snake-case event name.
457    pub event: String,
458    /// JSON payload for this event.
459    pub data: Value,
460}
461
462impl WsMessage {
463    /// Create a WebSocket message.
464    pub fn new(event: impl Into<String>, data: Value) -> Self {
465        Self {
466            event: event.into(),
467            data,
468        }
469    }
470
471    /// Parse a WebSocket JSON frame, defaulting missing `event` to `message`
472    /// and missing `data` to `{}` to match the Python runtime.
473    pub fn from_json(json: &str) -> serde_json::Result<Self> {
474        let mut parsed: Value = serde_json::from_str(json)?;
475        let event = parsed
476            .get("event")
477            .and_then(Value::as_str)
478            .unwrap_or("message")
479            .to_string();
480        let data = parsed
481            .as_object_mut()
482            .and_then(|object| object.remove("data"))
483            .unwrap_or_else(|| serde_json::json!({}));
484
485        Ok(Self { event, data })
486    }
487}
488
489#[cfg(test)]
490mod tests {
491    use serde_json::json;
492    use zynk_schema::TypeKind;
493
494    use super::{
495        EndpointMeta, Handler, HandlerKey, JsonErrorEnvelope, JsonErrorResponseEnvelope,
496        JsonResultEnvelope, ParamMeta, SseFrame, StaticValue, TypeRefStatic, WsMessage,
497        CHANNEL_ERROR, COMMAND_NOT_FOUND, ERROR_CODES, EXECUTION_ERROR, HANDLER_NOT_FOUND,
498        INTERNAL_ERROR, STATIC_HANDLER_NOT_FOUND, UPLOAD_HANDLER_NOT_FOUND,
499        UPLOAD_VALIDATION_ERROR, VALIDATION_ERROR, WEBSOCKET_ERROR,
500    };
501
502    #[test]
503    fn error_code_constants_match_python_literals_verbatim() {
504        assert_eq!(VALIDATION_ERROR, "VALIDATION_ERROR");
505        assert_eq!(COMMAND_NOT_FOUND, "COMMAND_NOT_FOUND");
506        assert_eq!(EXECUTION_ERROR, "EXECUTION_ERROR");
507        assert_eq!(CHANNEL_ERROR, "CHANNEL_ERROR");
508        assert_eq!(INTERNAL_ERROR, "INTERNAL_ERROR");
509        assert_eq!(WEBSOCKET_ERROR, "WEBSOCKET_ERROR");
510        assert_eq!(HANDLER_NOT_FOUND, "HANDLER_NOT_FOUND");
511        assert_eq!(UPLOAD_HANDLER_NOT_FOUND, "UPLOAD_HANDLER_NOT_FOUND");
512        assert_eq!(UPLOAD_VALIDATION_ERROR, "UPLOAD_VALIDATION_ERROR");
513        assert_eq!(STATIC_HANDLER_NOT_FOUND, "STATIC_HANDLER_NOT_FOUND");
514        assert_eq!(ERROR_CODES.len(), 10);
515        assert_eq!(
516            ERROR_CODES,
517            [
518                "VALIDATION_ERROR",
519                "COMMAND_NOT_FOUND",
520                "EXECUTION_ERROR",
521                "CHANNEL_ERROR",
522                "INTERNAL_ERROR",
523                "WEBSOCKET_ERROR",
524                "HANDLER_NOT_FOUND",
525                "UPLOAD_HANDLER_NOT_FOUND",
526                "UPLOAD_VALIDATION_ERROR",
527                "STATIC_HANDLER_NOT_FOUND",
528            ]
529        );
530    }
531
532    #[test]
533    fn json_success_envelope_serializes_to_python_wire_shape() {
534        let envelope = JsonResultEnvelope::new(json!({"id": 1, "name": "ada"}));
535
536        let encoded = serde_json::to_string(&envelope).expect("serialize result envelope");
537
538        assert_eq!(encoded, r#"{"result":{"id":1,"name":"ada"}}"#);
539    }
540
541    #[test]
542    fn json_error_envelope_omits_absent_details_like_python() {
543        let envelope = JsonErrorEnvelope::new(VALIDATION_ERROR, "bad input");
544
545        let encoded = serde_json::to_string(&envelope).expect("serialize error envelope");
546
547        assert_eq!(
548            encoded,
549            r#"{"code":"VALIDATION_ERROR","message":"bad input"}"#
550        );
551    }
552
553    #[test]
554    fn json_error_envelope_includes_details_when_present() {
555        let envelope = JsonErrorEnvelope::new(COMMAND_NOT_FOUND, "missing")
556            .with_details(json!({"command": "missing"}));
557
558        let encoded = serde_json::to_string(&envelope).expect("serialize error envelope");
559
560        assert_eq!(
561            encoded,
562            r#"{"code":"COMMAND_NOT_FOUND","message":"missing","details":{"command":"missing"}}"#
563        );
564    }
565
566    #[test]
567    fn json_outer_error_response_envelope_serializes_when_needed() {
568        let envelope =
569            JsonErrorResponseEnvelope::new(JsonErrorEnvelope::new(VALIDATION_ERROR, "bad input"));
570
571        let encoded = serde_json::to_string(&envelope).expect("serialize wrapped error envelope");
572
573        assert_eq!(
574            encoded,
575            r#"{"error":{"code":"VALIDATION_ERROR","message":"bad input"}}"#
576        );
577    }
578
579    #[test]
580    fn sse_frame_encodes_event_and_json_data_lines() {
581        let frame = SseFrame::new("message", json!({"x": 1}));
582
583        assert_eq!(frame.encode(), "event: message\ndata: {\"x\": 1}\n\n");
584    }
585
586    #[test]
587    fn sse_frame_serializes_to_structure_fields() {
588        let frame = SseFrame::new("close", json!({"channelId": "abc"}));
589
590        let encoded = serde_json::to_string(&frame).expect("serialize sse frame");
591
592        assert_eq!(encoded, r#"{"event":"close","data":{"channelId":"abc"}}"#);
593    }
594
595    #[test]
596    fn sse_frame_json_encodes_string_payloads() {
597        let frame = SseFrame::new("message", json!("hello"));
598
599        assert_eq!(frame.encode(), "event: message\ndata: \"hello\"\n\n");
600    }
601
602    #[test]
603    fn websocket_message_serializes_to_wire_shape() {
604        let message = WsMessage::new("chat_message", json!({"body": "hi"}));
605
606        let encoded = serde_json::to_string(&message).expect("serialize websocket message");
607
608        assert_eq!(encoded, r#"{"event":"chat_message","data":{"body":"hi"}}"#);
609    }
610
611    #[test]
612    fn websocket_message_from_json_defaults_missing_fields_like_python() {
613        assert_eq!(
614            WsMessage::from_json(r#"{"data":{"x":1}}"#).expect("parse missing event"),
615            WsMessage::new("message", json!({"x": 1}))
616        );
617        assert_eq!(
618            WsMessage::from_json(r#"{"event":"join"}"#).expect("parse missing data"),
619            WsMessage::new("join", json!({}))
620        );
621        assert_eq!(
622            WsMessage::from_json(r#"{}"#).expect("parse empty object"),
623            WsMessage::new("message", json!({}))
624        );
625    }
626
627    #[test]
628    fn handler_trait_accepts_type_erased_json_invocation() {
629        let handler = |payload: serde_json::Value| {
630            Ok(json!({
631                "echo": payload,
632            }))
633        };
634
635        let result = Handler::call(&handler, json!({"name": "Ada"})).expect("handler succeeds");
636
637        assert_eq!(result, json!({"echo": {"name": "Ada"}}));
638    }
639
640    #[test]
641    fn endpoint_meta_carries_route_registration_fields() {
642        static PARAMS: &[ParamMeta] = &[ParamMeta {
643            source_name: "display_name",
644            wire_name: "displayName",
645            ty: TypeRefStatic::primitive("string"),
646            required: true,
647            default: None,
648        }];
649        static RETURNS: TypeRefStatic = TypeRefStatic::model("User");
650        static CHANNEL_ITEM: TypeRefStatic = TypeRefStatic::model("User");
651        static SERVER_EVENTS: &[ParamMeta] = &[ParamMeta {
652            source_name: "user_updated",
653            wire_name: "userUpdated",
654            ty: TypeRefStatic::model("User"),
655            required: true,
656            default: None,
657        }];
658        static CLIENT_EVENTS: &[ParamMeta] = &[ParamMeta {
659            source_name: "subscribe_user",
660            wire_name: "subscribeUser",
661            ty: TypeRefStatic::primitive("number"),
662            required: true,
663            default: None,
664        }];
665
666        let endpoint = EndpointMeta {
667            name: "get_user",
668            kind: zynk_schema::EndpointKind::Upload,
669            module: Some("users"),
670            doc: Some("Fetches a user"),
671            params: PARAMS,
672            returns: RETURNS.clone(),
673            channel_item: Some(CHANNEL_ITEM.clone()),
674            file_param: Some("avatar"),
675            multi_file: false,
676            max_size: Some(1_048_576),
677            allowed_types: &["image/png", "image/jpeg"],
678            server_events: SERVER_EVENTS,
679            client_events: CLIENT_EVENTS,
680            handler_key: Some(HandlerKey("users::get_user")),
681        };
682
683        assert_eq!(endpoint.name, "get_user");
684        assert_eq!(endpoint.params[0].source_name, "display_name");
685        assert_eq!(endpoint.params[0].wire_name, "displayName");
686        assert_eq!(endpoint.params[0].ty.kind, TypeKind::Primitive);
687        assert_eq!(endpoint.returns.kind, TypeKind::Model);
688        assert_eq!(endpoint.file_param, Some("avatar"));
689        assert_eq!(endpoint.allowed_types, ["image/png", "image/jpeg"]);
690        assert_eq!(endpoint.server_events[0].wire_name, "userUpdated");
691        assert_eq!(endpoint.client_events[0].source_name, "subscribe_user");
692        assert_eq!(endpoint.handler_key, Some(HandlerKey("users::get_user")));
693    }
694
695    #[test]
696    fn type_ref_static_converts_to_schema_type_ref() {
697        static INNER: &[TypeRefStatic] = &[
698            TypeRefStatic::primitive("string"),
699            TypeRefStatic {
700                kind: TypeKind::Literal,
701                name: None,
702                inner: &[],
703                optional: false,
704                nullable: false,
705                value: Some(StaticValue::Str("admin")),
706            },
707        ];
708        let static_ref = TypeRefStatic {
709            kind: TypeKind::Union,
710            name: None,
711            inner: INNER,
712            optional: true,
713            nullable: true,
714            value: None,
715        };
716
717        let schema_ref = static_ref.to_schema_type_ref();
718
719        assert_eq!(schema_ref.kind, TypeKind::Union);
720        assert!(schema_ref.optional);
721        assert!(schema_ref.nullable);
722        assert_eq!(schema_ref.inner.len(), 2);
723        assert_eq!(schema_ref.inner[1].value, Some(json!("admin")));
724    }
725
726    #[test]
727    fn zynk_error_converts_into_json_error_envelope() {
728        let error = super::ZynkError::with_details(
729            CHANNEL_ERROR,
730            "channel closed",
731            json!({"channel_id": "abc"}),
732        );
733
734        let envelope = error.into_envelope();
735
736        assert_eq!(envelope.code, CHANNEL_ERROR);
737        assert_eq!(envelope.message, "channel closed");
738        assert_eq!(envelope.details, Some(json!({"channel_id": "abc"})));
739    }
740}