use anyhow::{Result, anyhow};
use zenkey::schema::{SchemaKind, TypeSchema, WireEncoding};
use zenoh::Session;
use crate::decode::SchemaStore;
use crate::registry::SliceSet;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum BodySource {
Encoded { type_name: String },
AsTyped,
Raw,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PrepareMode {
Encode,
Lenient,
Raw,
}
#[derive(Debug, Clone)]
pub struct PreparedBody {
pub bytes: Vec<u8>,
pub encoding: Option<String>,
pub source: BodySource,
pub note: Option<String>,
}
impl PreparedBody {
fn raw(bytes: Vec<u8>, encoding: Option<String>, note: Option<String>) -> PreparedBody {
PreparedBody {
bytes,
encoding,
source: BodySource::Raw,
note,
}
}
}
pub fn encode_encoding(
declared: Option<&str>,
registry: Option<&str>,
schema: Option<&TypeSchema>,
) -> Option<String> {
if let Some(e) = declared {
return Some(e.to_string());
}
if let Some(e) = registry {
return Some(e.to_string());
}
schema.and_then(|s| match s.kind().as_str() {
SchemaKind::JSON_SCHEMA => Some("application/json".to_string()),
SchemaKind::PROTOBUF => Some("application/protobuf".to_string()),
SchemaKind::CDR => Some("application/cdr".to_string()),
_ => None,
})
}
#[allow(clippy::too_many_arguments)]
pub async fn prepare_request(
session: &Session,
store: &SchemaStore,
producer: &str,
type_name: &str,
declared_encoding: Option<&str>,
registry_encoding: Option<&str>,
body: &[u8],
mode: PrepareMode,
) -> Result<PreparedBody> {
if mode == PrepareMode::Raw {
return Ok(PreparedBody::raw(
body.to_vec(),
encode_encoding(declared_encoding, registry_encoding, None),
Some("raw: bytes sent verbatim, not encoded against the served schema".into()),
));
}
let schema = store.schema_for(session, producer, type_name).await;
let encoding = encode_encoding(declared_encoding, registry_encoding, schema.as_ref());
let Some(schema) = schema else {
return Ok(PreparedBody {
bytes: body.to_vec(),
encoding,
source: BodySource::AsTyped,
note: Some(format!(
"{producer} serves no schema for {type_name} — body sent as typed, unchecked \
(RFC 08 §7 describe is a SHOULD; \"not served\" is not \"anything goes\")"
)),
});
};
let lenient = mode == PrepareMode::Lenient;
let value: serde_json::Value = match serde_json::from_slice(body) {
Ok(v) => v,
Err(e) if lenient => {
return Ok(PreparedBody {
bytes: body.to_vec(),
encoding,
source: BodySource::AsTyped,
note: Some(format!(
"body is not JSON, so it could not be encoded as {type_name} ({e}) — \
sent as typed"
)),
});
}
Err(e) => {
return Err(anyhow!(
"body is not JSON but {producer} declares schema-validated type {type_name} — {e}"
));
}
};
let target = encoding
.as_deref()
.map(WireEncoding::from_encoding_str)
.unwrap_or(WireEncoding::Json);
match store.encode(&schema, &value, &target) {
Ok(bytes) => Ok(PreparedBody {
bytes,
encoding,
source: BodySource::Encoded {
type_name: type_name.to_string(),
},
note: None,
}),
Err(e) if lenient => Ok(PreparedBody {
bytes: body.to_vec(),
encoding,
source: BodySource::AsTyped,
note: Some(format!(
"body rejected by {type_name}'s served schema ({e}) — sent as typed anyway"
)),
}),
Err(e) => Err(anyhow!("body rejected by {type_name}'s served schema: {e}")),
}
}
#[allow(clippy::too_many_arguments)]
pub async fn prepare_publish(
session: &Session,
store: &SchemaStore,
slices: Option<&SliceSet>,
base: &str,
wire_key: &str,
declared_encoding: Option<&str>,
body: &[u8],
mode: PrepareMode,
) -> Result<PreparedBody> {
if mode == PrepareMode::Raw {
return Ok(PreparedBody::raw(
body.to_vec(),
declared_encoding.map(str::to_string),
Some("raw: bytes sent verbatim, not encoded against the served schema".into()),
));
}
let description = crate::facts::describe_key(base, wire_key, slices);
let crate::facts::Registration::Registered(subject) = &description.facts.registration else {
return Ok(PreparedBody {
bytes: body.to_vec(),
encoding: declared_encoding.map(str::to_string),
source: BodySource::AsTyped,
note: Some(match slices {
None => format!(
"no registry loaded, so {wire_key} was never classified — body sent as typed"
),
Some(_) => format!(
"{wire_key} is not a registered subject ({:?}) — body sent as typed",
description.facts.registration
),
}),
});
};
let Some(producer) = subject_producer(&description) else {
return Ok(PreparedBody {
bytes: body.to_vec(),
encoding: encode_encoding(declared_encoding, subject.encoding.as_deref(), None),
source: BodySource::AsTyped,
note: Some(format!(
"{wire_key} refines to a registered subject with no producer chunk to ask for a \
schema — body sent as typed"
)),
});
};
if subject.type_name.is_empty() {
return Ok(PreparedBody {
bytes: body.to_vec(),
encoding: encode_encoding(declared_encoding, subject.encoding.as_deref(), None),
source: BodySource::AsTyped,
note: Some(format!(
"{wire_key} is registered but declares no payload type — body sent as typed"
)),
});
}
prepare_request(
session,
store,
&producer,
&subject.type_name,
declared_encoding,
subject.encoding.as_deref(),
body,
mode,
)
.await
}
pub fn subject_producer(description: &crate::facts::KeyDescription) -> Option<String> {
match &description.facts.shape {
crate::facts::KeyShape::V1(v) => v.producer.clone(),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn the_encode_ladder_never_sniffs_the_operators_text() {
let protobuf = TypeSchema::protobuf("t.Blob", b"\x0a\x00");
assert_eq!(
encode_encoding(None, None, Some(&protobuf)).as_deref(),
Some("application/protobuf")
);
assert_eq!(
encode_encoding(None, Some("application/cbor"), Some(&protobuf)).as_deref(),
Some("application/cbor")
);
assert_eq!(
encode_encoding(Some("application/json"), Some("application/cbor"), None).as_deref(),
Some("application/json")
);
let json = TypeSchema::json_schema(json!({"type": "object"}));
assert_eq!(
encode_encoding(None, None, Some(&json)).as_deref(),
Some("application/json")
);
assert_eq!(encode_encoding(None, None, None), None);
}
}