1use anyhow::{Result, anyhow};
23use zenkey::schema::{SchemaKind, TypeSchema, WireEncoding};
24use zenoh::Session;
25
26use crate::decode::SchemaStore;
27use crate::registry::SliceSet;
28
29#[derive(Debug, Clone, PartialEq, Eq)]
32pub enum BodySource {
33 Encoded { type_name: String },
35 AsTyped,
39 Raw,
41}
42
43#[derive(Debug, Clone, Copy, PartialEq, Eq)]
45pub enum PrepareMode {
46 Encode,
48 Lenient,
51 Raw,
53}
54
55#[derive(Debug, Clone)]
57pub struct PreparedBody {
58 pub bytes: Vec<u8>,
59 pub encoding: Option<String>,
62 pub source: BodySource,
63 pub note: Option<String>,
66}
67
68impl PreparedBody {
69 fn raw(bytes: Vec<u8>, encoding: Option<String>, note: Option<String>) -> PreparedBody {
70 PreparedBody {
71 bytes,
72 encoding,
73 source: BodySource::Raw,
74 note,
75 }
76 }
77}
78
79pub fn encode_encoding(
90 declared: Option<&str>,
91 registry: Option<&str>,
92 schema: Option<&TypeSchema>,
93) -> Option<String> {
94 if let Some(e) = declared {
95 return Some(e.to_string());
96 }
97 if let Some(e) = registry {
98 return Some(e.to_string());
99 }
100 schema.and_then(|s| match s.kind().as_str() {
101 SchemaKind::JSON_SCHEMA => Some("application/json".to_string()),
102 SchemaKind::PROTOBUF => Some("application/protobuf".to_string()),
103 SchemaKind::CDR => Some("application/cdr".to_string()),
104 _ => None,
107 })
108}
109
110#[allow(clippy::too_many_arguments)]
118pub async fn prepare_request(
119 session: &Session,
120 store: &SchemaStore,
121 producer: &str,
122 type_name: &str,
123 declared_encoding: Option<&str>,
124 registry_encoding: Option<&str>,
125 body: &[u8],
126 mode: PrepareMode,
127) -> Result<PreparedBody> {
128 if mode == PrepareMode::Raw {
129 return Ok(PreparedBody::raw(
130 body.to_vec(),
131 encode_encoding(declared_encoding, registry_encoding, None),
132 Some("raw: bytes sent verbatim, not encoded against the served schema".into()),
133 ));
134 }
135 let schema = store.schema_for(session, producer, type_name).await;
136 let encoding = encode_encoding(declared_encoding, registry_encoding, schema.as_ref());
137 let Some(schema) = schema else {
138 return Ok(PreparedBody {
139 bytes: body.to_vec(),
140 encoding,
141 source: BodySource::AsTyped,
142 note: Some(format!(
143 "{producer} serves no schema for {type_name} — body sent as typed, unchecked \
144 (RFC 08 §7 describe is a SHOULD; \"not served\" is not \"anything goes\")"
145 )),
146 });
147 };
148
149 let lenient = mode == PrepareMode::Lenient;
150 let value: serde_json::Value = match serde_json::from_slice(body) {
151 Ok(v) => v,
152 Err(e) if lenient => {
153 return Ok(PreparedBody {
154 bytes: body.to_vec(),
155 encoding,
156 source: BodySource::AsTyped,
157 note: Some(format!(
158 "body is not JSON, so it could not be encoded as {type_name} ({e}) — \
159 sent as typed"
160 )),
161 });
162 }
163 Err(e) => {
164 return Err(anyhow!(
165 "body is not JSON but {producer} declares schema-validated type {type_name} — {e}"
166 ));
167 }
168 };
169
170 let target = encoding
171 .as_deref()
172 .map(WireEncoding::from_encoding_str)
173 .unwrap_or(WireEncoding::Json);
176 match store.encode(&schema, &value, &target) {
177 Ok(bytes) => Ok(PreparedBody {
178 bytes,
179 encoding,
180 source: BodySource::Encoded {
181 type_name: type_name.to_string(),
182 },
183 note: None,
184 }),
185 Err(e) if lenient => Ok(PreparedBody {
186 bytes: body.to_vec(),
187 encoding,
188 source: BodySource::AsTyped,
189 note: Some(format!(
190 "body rejected by {type_name}'s served schema ({e}) — sent as typed anyway"
191 )),
192 }),
193 Err(e) => Err(anyhow!("body rejected by {type_name}'s served schema: {e}")),
194 }
195}
196
197#[allow(clippy::too_many_arguments)]
203pub async fn prepare_publish(
204 session: &Session,
205 store: &SchemaStore,
206 slices: Option<&SliceSet>,
207 base: &str,
208 wire_key: &str,
209 declared_encoding: Option<&str>,
210 body: &[u8],
211 mode: PrepareMode,
212) -> Result<PreparedBody> {
213 if mode == PrepareMode::Raw {
214 return Ok(PreparedBody::raw(
215 body.to_vec(),
216 declared_encoding.map(str::to_string),
217 Some("raw: bytes sent verbatim, not encoded against the served schema".into()),
218 ));
219 }
220
221 let description = crate::facts::describe_key(base, wire_key, slices);
222 let crate::facts::Registration::Registered(subject) = &description.facts.registration else {
223 return Ok(PreparedBody {
224 bytes: body.to_vec(),
225 encoding: declared_encoding.map(str::to_string),
226 source: BodySource::AsTyped,
227 note: Some(match slices {
228 None => format!(
231 "no registry loaded, so {wire_key} was never classified — body sent as typed"
232 ),
233 Some(_) => format!(
234 "{wire_key} is not a registered subject ({:?}) — body sent as typed",
235 description.facts.registration
236 ),
237 }),
238 });
239 };
240 let Some(producer) = subject_producer(&description) else {
241 return Ok(PreparedBody {
242 bytes: body.to_vec(),
243 encoding: encode_encoding(declared_encoding, subject.encoding.as_deref(), None),
244 source: BodySource::AsTyped,
245 note: Some(format!(
246 "{wire_key} refines to a registered subject with no producer chunk to ask for a \
247 schema — body sent as typed"
248 )),
249 });
250 };
251 if subject.type_name.is_empty() {
252 return Ok(PreparedBody {
253 bytes: body.to_vec(),
254 encoding: encode_encoding(declared_encoding, subject.encoding.as_deref(), None),
255 source: BodySource::AsTyped,
256 note: Some(format!(
257 "{wire_key} is registered but declares no payload type — body sent as typed"
258 )),
259 });
260 }
261
262 prepare_request(
263 session,
264 store,
265 &producer,
266 &subject.type_name,
267 declared_encoding,
268 subject.encoding.as_deref(),
269 body,
270 mode,
271 )
272 .await
273}
274
275pub fn subject_producer(description: &crate::facts::KeyDescription) -> Option<String> {
279 match &description.facts.shape {
280 crate::facts::KeyShape::V1(v) => v.producer.clone(),
281 _ => None,
282 }
283}
284
285#[cfg(test)]
286mod tests {
287 use super::*;
288 use serde_json::json;
289
290 #[test]
291 fn the_encode_ladder_never_sniffs_the_operators_text() {
292 let protobuf = TypeSchema::protobuf("t.Blob", b"\x0a\x00");
293 assert_eq!(
295 encode_encoding(None, None, Some(&protobuf)).as_deref(),
296 Some("application/protobuf")
297 );
298 assert_eq!(
300 encode_encoding(None, Some("application/cbor"), Some(&protobuf)).as_deref(),
301 Some("application/cbor")
302 );
303 assert_eq!(
305 encode_encoding(Some("application/json"), Some("application/cbor"), None).as_deref(),
306 Some("application/json")
307 );
308 let json = TypeSchema::json_schema(json!({"type": "object"}));
311 assert_eq!(
312 encode_encoding(None, None, Some(&json)).as_deref(),
313 Some("application/json")
314 );
315 assert_eq!(encode_encoding(None, None, None), None);
316 }
317}