use serde::{Deserialize, Serialize};
use serde_json::Value;
use thiserror::Error;
use zynk_schema::{TypeKind, TypeRef};
pub use inventory;
pub use zynk_schema::{self, EndpointKind};
pub const VALIDATION_ERROR: &str = "VALIDATION_ERROR";
pub const COMMAND_NOT_FOUND: &str = "COMMAND_NOT_FOUND";
pub const EXECUTION_ERROR: &str = "EXECUTION_ERROR";
pub const CHANNEL_ERROR: &str = "CHANNEL_ERROR";
pub const INTERNAL_ERROR: &str = "INTERNAL_ERROR";
pub const WEBSOCKET_ERROR: &str = "WEBSOCKET_ERROR";
pub const HANDLER_NOT_FOUND: &str = "HANDLER_NOT_FOUND";
pub const UPLOAD_HANDLER_NOT_FOUND: &str = "UPLOAD_HANDLER_NOT_FOUND";
pub const UPLOAD_VALIDATION_ERROR: &str = "UPLOAD_VALIDATION_ERROR";
pub const STATIC_HANDLER_NOT_FOUND: &str = "STATIC_HANDLER_NOT_FOUND";
pub const ERROR_CODES: [&str; 10] = [
VALIDATION_ERROR,
COMMAND_NOT_FOUND,
EXECUTION_ERROR,
CHANNEL_ERROR,
INTERNAL_ERROR,
WEBSOCKET_ERROR,
HANDLER_NOT_FOUND,
UPLOAD_HANDLER_NOT_FOUND,
UPLOAD_VALIDATION_ERROR,
STATIC_HANDLER_NOT_FOUND,
];
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct JsonResultEnvelope<T> {
pub result: T,
}
impl<T> JsonResultEnvelope<T> {
pub fn new(result: T) -> Self {
Self { result }
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct JsonErrorEnvelope {
pub code: String,
pub message: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub details: Option<Value>,
}
impl JsonErrorEnvelope {
pub fn new(code: impl Into<String>, message: impl Into<String>) -> Self {
Self {
code: code.into(),
message: message.into(),
details: None,
}
}
pub fn with_details(mut self, details: Value) -> Self {
self.details = Some(details);
self
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct JsonErrorResponseEnvelope {
pub error: JsonErrorEnvelope,
}
impl JsonErrorResponseEnvelope {
pub fn new(error: JsonErrorEnvelope) -> Self {
Self { error }
}
}
#[derive(Debug, Clone, PartialEq, Error)]
#[error("{code}: {message}")]
pub struct ZynkError {
pub code: &'static str,
pub message: String,
pub details: Option<Value>,
}
impl ZynkError {
pub fn new(code: &'static str, message: impl Into<String>) -> Self {
Self {
code,
message: message.into(),
details: None,
}
}
pub fn with_details(code: &'static str, message: impl Into<String>, details: Value) -> Self {
Self {
code,
message: message.into(),
details: Some(details),
}
}
pub fn into_envelope(self) -> JsonErrorEnvelope {
JsonErrorEnvelope {
code: self.code.to_string(),
message: self.message,
details: self.details,
}
}
}
pub trait Handler: Send + Sync + 'static {
fn call(&self, payload: Value) -> Result<Value, ZynkError>;
}
impl<F> Handler for F
where
F: Fn(Value) -> Result<Value, ZynkError> + Send + Sync + 'static,
{
fn call(&self, payload: Value) -> Result<Value, ZynkError> {
self(payload)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct HandlerKey(pub &'static str);
#[derive(Debug, Clone, PartialEq)]
pub enum StaticValue {
Null,
Bool(bool),
I64(i64),
U64(u64),
F64(f64),
Str(&'static str),
}
impl StaticValue {
pub fn to_json(&self) -> Value {
match self {
Self::Null => Value::Null,
Self::Bool(value) => Value::Bool(*value),
Self::I64(value) => Value::Number((*value).into()),
Self::U64(value) => Value::Number((*value).into()),
Self::F64(value) => serde_json::Number::from_f64(*value)
.map(Value::Number)
.unwrap_or(Value::Null),
Self::Str(value) => Value::String((*value).to_string()),
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct TypeRefStatic {
pub kind: TypeKind,
pub name: Option<&'static str>,
pub inner: &'static [TypeRefStatic],
pub optional: bool,
pub nullable: bool,
pub value: Option<StaticValue>,
}
impl TypeRefStatic {
pub const fn optional(mut self) -> Self {
self.optional = true;
self
}
pub const fn nullable(mut self) -> Self {
self.nullable = true;
self
}
pub fn to_schema_type_ref(&self) -> TypeRef {
TypeRef {
kind: self.kind.clone(),
name: self.name.map(str::to_string),
inner: self
.inner
.iter()
.map(TypeRefStatic::to_schema_type_ref)
.collect(),
optional: self.optional,
nullable: self.nullable,
value: self.value.as_ref().map(StaticValue::to_json),
}
}
pub const fn primitive(name: &'static str) -> Self {
Self {
kind: TypeKind::Primitive,
name: Some(name),
inner: &[],
optional: false,
nullable: false,
value: None,
}
}
pub const fn model(name: &'static str) -> Self {
Self {
kind: TypeKind::Model,
name: Some(name),
inner: &[],
optional: false,
nullable: false,
value: None,
}
}
pub const fn enum_ref(name: &'static str) -> Self {
Self {
kind: TypeKind::Enum,
name: Some(name),
inner: &[],
optional: false,
nullable: false,
value: None,
}
}
pub const fn array(item: &'static [TypeRefStatic]) -> Self {
Self {
kind: TypeKind::Array,
name: None,
inner: item,
optional: false,
nullable: false,
value: None,
}
}
pub const fn union(members: &'static [TypeRefStatic]) -> Self {
Self {
kind: TypeKind::Union,
name: None,
inner: members,
optional: false,
nullable: false,
value: None,
}
}
pub const fn literal(value: StaticValue) -> Self {
Self {
kind: TypeKind::Literal,
name: None,
inner: &[],
optional: false,
nullable: false,
value: Some(value),
}
}
pub const fn any() -> Self {
Self {
kind: TypeKind::Any,
name: None,
inner: &[],
optional: false,
nullable: false,
value: None,
}
}
pub const fn void() -> Self {
Self {
kind: TypeKind::Void,
name: None,
inner: &[],
optional: false,
nullable: false,
value: None,
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct ParamMeta {
pub source_name: &'static str,
pub wire_name: &'static str,
pub ty: TypeRefStatic,
pub required: bool,
pub default: Option<StaticValue>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct EndpointMeta {
pub name: &'static str,
pub kind: EndpointKind,
pub module: Option<&'static str>,
pub doc: Option<&'static str>,
pub params: &'static [ParamMeta],
pub returns: TypeRefStatic,
pub channel_item: Option<TypeRefStatic>,
pub file_param: Option<&'static str>,
pub multi_file: bool,
pub max_size: Option<u64>,
pub allowed_types: &'static [&'static str],
pub server_events: &'static [ParamMeta],
pub client_events: &'static [ParamMeta],
pub handler_key: Option<HandlerKey>,
}
inventory::collect!(EndpointMeta);
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SseFrame {
pub event: String,
pub data: Value,
}
impl SseFrame {
pub fn new(event: impl Into<String>, data: Value) -> Self {
Self {
event: event.into(),
data,
}
}
pub fn encode(&self) -> String {
format!(
"event: {}\ndata: {}\n\n",
self.event,
python_json_dumps(&self.data)
)
}
}
fn python_json_dumps(value: &Value) -> String {
match value {
Value::Array(items) => {
let inner = items
.iter()
.map(python_json_dumps)
.collect::<Vec<_>>()
.join(", ");
format!("[{inner}]")
}
Value::Object(object) => {
let inner = object
.iter()
.map(|(key, value)| {
format!(
"{}: {}",
python_json_dumps(&Value::String(key.clone())),
python_json_dumps(value)
)
})
.collect::<Vec<_>>()
.join(", ");
format!("{{{inner}}}")
}
_ => serde_json::to_string(value).expect("JSON value serialization cannot fail"),
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct WsMessage {
pub event: String,
pub data: Value,
}
impl WsMessage {
pub fn new(event: impl Into<String>, data: Value) -> Self {
Self {
event: event.into(),
data,
}
}
pub fn from_json(json: &str) -> serde_json::Result<Self> {
let mut parsed: Value = serde_json::from_str(json)?;
let event = parsed
.get("event")
.and_then(Value::as_str)
.unwrap_or("message")
.to_string();
let data = parsed
.as_object_mut()
.and_then(|object| object.remove("data"))
.unwrap_or_else(|| serde_json::json!({}));
Ok(Self { event, data })
}
}
#[cfg(test)]
mod tests {
use serde_json::json;
use zynk_schema::TypeKind;
use super::{
EndpointMeta, Handler, HandlerKey, JsonErrorEnvelope, JsonErrorResponseEnvelope,
JsonResultEnvelope, ParamMeta, SseFrame, StaticValue, TypeRefStatic, WsMessage,
CHANNEL_ERROR, COMMAND_NOT_FOUND, ERROR_CODES, EXECUTION_ERROR, HANDLER_NOT_FOUND,
INTERNAL_ERROR, STATIC_HANDLER_NOT_FOUND, UPLOAD_HANDLER_NOT_FOUND,
UPLOAD_VALIDATION_ERROR, VALIDATION_ERROR, WEBSOCKET_ERROR,
};
#[test]
fn error_code_constants_match_python_literals_verbatim() {
assert_eq!(VALIDATION_ERROR, "VALIDATION_ERROR");
assert_eq!(COMMAND_NOT_FOUND, "COMMAND_NOT_FOUND");
assert_eq!(EXECUTION_ERROR, "EXECUTION_ERROR");
assert_eq!(CHANNEL_ERROR, "CHANNEL_ERROR");
assert_eq!(INTERNAL_ERROR, "INTERNAL_ERROR");
assert_eq!(WEBSOCKET_ERROR, "WEBSOCKET_ERROR");
assert_eq!(HANDLER_NOT_FOUND, "HANDLER_NOT_FOUND");
assert_eq!(UPLOAD_HANDLER_NOT_FOUND, "UPLOAD_HANDLER_NOT_FOUND");
assert_eq!(UPLOAD_VALIDATION_ERROR, "UPLOAD_VALIDATION_ERROR");
assert_eq!(STATIC_HANDLER_NOT_FOUND, "STATIC_HANDLER_NOT_FOUND");
assert_eq!(ERROR_CODES.len(), 10);
assert_eq!(
ERROR_CODES,
[
"VALIDATION_ERROR",
"COMMAND_NOT_FOUND",
"EXECUTION_ERROR",
"CHANNEL_ERROR",
"INTERNAL_ERROR",
"WEBSOCKET_ERROR",
"HANDLER_NOT_FOUND",
"UPLOAD_HANDLER_NOT_FOUND",
"UPLOAD_VALIDATION_ERROR",
"STATIC_HANDLER_NOT_FOUND",
]
);
}
#[test]
fn json_success_envelope_serializes_to_python_wire_shape() {
let envelope = JsonResultEnvelope::new(json!({"id": 1, "name": "ada"}));
let encoded = serde_json::to_string(&envelope).expect("serialize result envelope");
assert_eq!(encoded, r#"{"result":{"id":1,"name":"ada"}}"#);
}
#[test]
fn json_error_envelope_omits_absent_details_like_python() {
let envelope = JsonErrorEnvelope::new(VALIDATION_ERROR, "bad input");
let encoded = serde_json::to_string(&envelope).expect("serialize error envelope");
assert_eq!(
encoded,
r#"{"code":"VALIDATION_ERROR","message":"bad input"}"#
);
}
#[test]
fn json_error_envelope_includes_details_when_present() {
let envelope = JsonErrorEnvelope::new(COMMAND_NOT_FOUND, "missing")
.with_details(json!({"command": "missing"}));
let encoded = serde_json::to_string(&envelope).expect("serialize error envelope");
assert_eq!(
encoded,
r#"{"code":"COMMAND_NOT_FOUND","message":"missing","details":{"command":"missing"}}"#
);
}
#[test]
fn json_outer_error_response_envelope_serializes_when_needed() {
let envelope =
JsonErrorResponseEnvelope::new(JsonErrorEnvelope::new(VALIDATION_ERROR, "bad input"));
let encoded = serde_json::to_string(&envelope).expect("serialize wrapped error envelope");
assert_eq!(
encoded,
r#"{"error":{"code":"VALIDATION_ERROR","message":"bad input"}}"#
);
}
#[test]
fn sse_frame_encodes_event_and_json_data_lines() {
let frame = SseFrame::new("message", json!({"x": 1}));
assert_eq!(frame.encode(), "event: message\ndata: {\"x\": 1}\n\n");
}
#[test]
fn sse_frame_serializes_to_structure_fields() {
let frame = SseFrame::new("close", json!({"channelId": "abc"}));
let encoded = serde_json::to_string(&frame).expect("serialize sse frame");
assert_eq!(encoded, r#"{"event":"close","data":{"channelId":"abc"}}"#);
}
#[test]
fn sse_frame_json_encodes_string_payloads() {
let frame = SseFrame::new("message", json!("hello"));
assert_eq!(frame.encode(), "event: message\ndata: \"hello\"\n\n");
}
#[test]
fn websocket_message_serializes_to_wire_shape() {
let message = WsMessage::new("chat_message", json!({"body": "hi"}));
let encoded = serde_json::to_string(&message).expect("serialize websocket message");
assert_eq!(encoded, r#"{"event":"chat_message","data":{"body":"hi"}}"#);
}
#[test]
fn websocket_message_from_json_defaults_missing_fields_like_python() {
assert_eq!(
WsMessage::from_json(r#"{"data":{"x":1}}"#).expect("parse missing event"),
WsMessage::new("message", json!({"x": 1}))
);
assert_eq!(
WsMessage::from_json(r#"{"event":"join"}"#).expect("parse missing data"),
WsMessage::new("join", json!({}))
);
assert_eq!(
WsMessage::from_json(r#"{}"#).expect("parse empty object"),
WsMessage::new("message", json!({}))
);
}
#[test]
fn handler_trait_accepts_type_erased_json_invocation() {
let handler = |payload: serde_json::Value| {
Ok(json!({
"echo": payload,
}))
};
let result = Handler::call(&handler, json!({"name": "Ada"})).expect("handler succeeds");
assert_eq!(result, json!({"echo": {"name": "Ada"}}));
}
#[test]
fn endpoint_meta_carries_route_registration_fields() {
static PARAMS: &[ParamMeta] = &[ParamMeta {
source_name: "display_name",
wire_name: "displayName",
ty: TypeRefStatic::primitive("string"),
required: true,
default: None,
}];
static RETURNS: TypeRefStatic = TypeRefStatic::model("User");
static CHANNEL_ITEM: TypeRefStatic = TypeRefStatic::model("User");
static SERVER_EVENTS: &[ParamMeta] = &[ParamMeta {
source_name: "user_updated",
wire_name: "userUpdated",
ty: TypeRefStatic::model("User"),
required: true,
default: None,
}];
static CLIENT_EVENTS: &[ParamMeta] = &[ParamMeta {
source_name: "subscribe_user",
wire_name: "subscribeUser",
ty: TypeRefStatic::primitive("number"),
required: true,
default: None,
}];
let endpoint = EndpointMeta {
name: "get_user",
kind: zynk_schema::EndpointKind::Upload,
module: Some("users"),
doc: Some("Fetches a user"),
params: PARAMS,
returns: RETURNS.clone(),
channel_item: Some(CHANNEL_ITEM.clone()),
file_param: Some("avatar"),
multi_file: false,
max_size: Some(1_048_576),
allowed_types: &["image/png", "image/jpeg"],
server_events: SERVER_EVENTS,
client_events: CLIENT_EVENTS,
handler_key: Some(HandlerKey("users::get_user")),
};
assert_eq!(endpoint.name, "get_user");
assert_eq!(endpoint.params[0].source_name, "display_name");
assert_eq!(endpoint.params[0].wire_name, "displayName");
assert_eq!(endpoint.params[0].ty.kind, TypeKind::Primitive);
assert_eq!(endpoint.returns.kind, TypeKind::Model);
assert_eq!(endpoint.file_param, Some("avatar"));
assert_eq!(endpoint.allowed_types, ["image/png", "image/jpeg"]);
assert_eq!(endpoint.server_events[0].wire_name, "userUpdated");
assert_eq!(endpoint.client_events[0].source_name, "subscribe_user");
assert_eq!(endpoint.handler_key, Some(HandlerKey("users::get_user")));
}
#[test]
fn type_ref_static_converts_to_schema_type_ref() {
static INNER: &[TypeRefStatic] = &[
TypeRefStatic::primitive("string"),
TypeRefStatic {
kind: TypeKind::Literal,
name: None,
inner: &[],
optional: false,
nullable: false,
value: Some(StaticValue::Str("admin")),
},
];
let static_ref = TypeRefStatic {
kind: TypeKind::Union,
name: None,
inner: INNER,
optional: true,
nullable: true,
value: None,
};
let schema_ref = static_ref.to_schema_type_ref();
assert_eq!(schema_ref.kind, TypeKind::Union);
assert!(schema_ref.optional);
assert!(schema_ref.nullable);
assert_eq!(schema_ref.inner.len(), 2);
assert_eq!(schema_ref.inner[1].value, Some(json!("admin")));
}
#[test]
fn zynk_error_converts_into_json_error_envelope() {
let error = super::ZynkError::with_details(
CHANNEL_ERROR,
"channel closed",
json!({"channel_id": "abc"}),
);
let envelope = error.into_envelope();
assert_eq!(envelope.code, CHANNEL_ERROR);
assert_eq!(envelope.message, "channel closed");
assert_eq!(envelope.details, Some(json!({"channel_id": "abc"})));
}
}