1use 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
16pub const VALIDATION_ERROR: &str = "VALIDATION_ERROR";
18pub const COMMAND_NOT_FOUND: &str = "COMMAND_NOT_FOUND";
20pub const EXECUTION_ERROR: &str = "EXECUTION_ERROR";
22pub const CHANNEL_ERROR: &str = "CHANNEL_ERROR";
24pub const INTERNAL_ERROR: &str = "INTERNAL_ERROR";
26pub const WEBSOCKET_ERROR: &str = "WEBSOCKET_ERROR";
28pub const HANDLER_NOT_FOUND: &str = "HANDLER_NOT_FOUND";
30pub const UPLOAD_HANDLER_NOT_FOUND: &str = "UPLOAD_HANDLER_NOT_FOUND";
32pub const UPLOAD_VALIDATION_ERROR: &str = "UPLOAD_VALIDATION_ERROR";
34pub const STATIC_HANDLER_NOT_FOUND: &str = "STATIC_HANDLER_NOT_FOUND";
36
37pub 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#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
53pub struct JsonResultEnvelope<T> {
54 pub result: T,
56}
57
58impl<T> JsonResultEnvelope<T> {
59 pub fn new(result: T) -> Self {
61 Self { result }
62 }
63}
64
65#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
67pub struct JsonErrorEnvelope {
68 pub code: String,
70 pub message: String,
72 #[serde(skip_serializing_if = "Option::is_none")]
74 pub details: Option<Value>,
75}
76
77impl JsonErrorEnvelope {
78 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 pub fn with_details(mut self, details: Value) -> Self {
89 self.details = Some(details);
90 self
91 }
92}
93
94#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
96pub struct JsonErrorResponseEnvelope {
97 pub error: JsonErrorEnvelope,
99}
100
101impl JsonErrorResponseEnvelope {
102 pub fn new(error: JsonErrorEnvelope) -> Self {
104 Self { error }
105 }
106}
107
108#[derive(Debug, Clone, PartialEq, Error)]
110#[error("{code}: {message}")]
111pub struct ZynkError {
112 pub code: &'static str,
114 pub message: String,
116 pub details: Option<Value>,
118}
119
120impl ZynkError {
121 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 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 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
149pub trait Handler: Send + Sync + 'static {
151 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
166pub struct HandlerKey(pub &'static str);
167
168#[derive(Debug, Clone, PartialEq)]
170pub enum StaticValue {
171 Null,
173 Bool(bool),
175 I64(i64),
177 U64(u64),
179 F64(f64),
181 Str(&'static str),
183}
184
185impl StaticValue {
186 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#[derive(Debug, Clone, PartialEq)]
203pub struct TypeRefStatic {
204 pub kind: TypeKind,
206 pub name: Option<&'static str>,
208 pub inner: &'static [TypeRefStatic],
210 pub optional: bool,
212 pub nullable: bool,
214 pub value: Option<StaticValue>,
216}
217
218impl TypeRefStatic {
219 pub const fn optional(mut self) -> Self {
221 self.optional = true;
222 self
223 }
224
225 pub const fn nullable(mut self) -> Self {
227 self.nullable = true;
228 self
229 }
230
231 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 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 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 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 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 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 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 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 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#[derive(Debug, Clone, PartialEq)]
348pub struct ParamMeta {
349 pub source_name: &'static str,
351 pub wire_name: &'static str,
353 pub ty: TypeRefStatic,
355 pub required: bool,
357 pub default: Option<StaticValue>,
359}
360
361#[derive(Debug, Clone, PartialEq)]
363pub struct EndpointMeta {
364 pub name: &'static str,
366 pub kind: EndpointKind,
368 pub module: Option<&'static str>,
370 pub doc: Option<&'static str>,
372 pub params: &'static [ParamMeta],
374 pub returns: TypeRefStatic,
376 pub channel_item: Option<TypeRefStatic>,
378 pub file_param: Option<&'static str>,
380 pub multi_file: bool,
382 pub max_size: Option<u64>,
384 pub allowed_types: &'static [&'static str],
386 pub server_events: &'static [ParamMeta],
388 pub client_events: &'static [ParamMeta],
390 pub handler_key: Option<HandlerKey>,
392}
393
394inventory::collect!(EndpointMeta);
395
396#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
398#[serde(rename_all = "camelCase")]
399pub struct SseFrame {
400 pub event: String,
402 pub data: Value,
404}
405
406impl SseFrame {
407 pub fn new(event: impl Into<String>, data: Value) -> Self {
409 Self {
410 event: event.into(),
411 data,
412 }
413 }
414
415 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#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
455pub struct WsMessage {
456 pub event: String,
458 pub data: Value,
460}
461
462impl WsMessage {
463 pub fn new(event: impl Into<String>, data: Value) -> Self {
465 Self {
466 event: event.into(),
467 data,
468 }
469 }
470
471 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}