1use std::sync::Arc;
23
24#[cfg(feature = "arrow")]
25use arrow::{
26 datatypes::{DataType as ArrowDataType, Field, Schema},
27 record_batch::RecordBatch,
28};
29use dashmap::{DashMap, mapref::entry::Entry};
30use nautilus_core::Params;
31#[cfg(feature = "python")]
32use pyo3::types::PyAnyMethods;
33
34use crate::data::{CustomData, CustomDataTrait, Data, DataType};
35
36pub type JsonDeserializer =
37 Box<dyn Fn(serde_json::Value) -> Result<Arc<dyn CustomDataTrait>, anyhow::Error> + Send + Sync>;
38#[cfg(feature = "arrow")]
39pub type ArrowEncoder =
40 Box<dyn Fn(&[Arc<dyn CustomDataTrait>]) -> Result<RecordBatch, anyhow::Error> + Send + Sync>;
41#[cfg(feature = "arrow")]
42pub type ArrowDecoder = Box<
43 dyn Fn(
44 &std::collections::HashMap<String, String>,
45 RecordBatch,
46 ) -> Result<Vec<Data>, anyhow::Error>
47 + Send
48 + Sync,
49>;
50
51#[cfg(feature = "arrow")]
60pub fn validate_custom_arrow_schema(
61 type_name: &str,
62 schema: &Schema,
63 allow_binary: bool,
64) -> anyhow::Result<()> {
65 for field in schema.fields() {
66 validate_custom_arrow_field(type_name, field, allow_binary)?;
67 }
68 Ok(())
69}
70
71#[cfg(feature = "arrow")]
72fn validate_custom_arrow_field(
73 type_name: &str,
74 field: &Field,
75 allow_binary: bool,
76) -> anyhow::Result<()> {
77 match field.data_type() {
78 ArrowDataType::Binary if allow_binary => Ok(()),
79 ArrowDataType::Binary
80 | ArrowDataType::LargeBinary
81 | ArrowDataType::BinaryView
82 | ArrowDataType::FixedSizeBinary(_) => anyhow::bail!(
83 "custom write schema `{type_name}` contains opaque byte field `{}`: {}",
84 field.name(),
85 field.data_type(),
86 ),
87 ArrowDataType::List(child)
88 | ArrowDataType::LargeList(child)
89 | ArrowDataType::ListView(child)
90 | ArrowDataType::LargeListView(child)
91 | ArrowDataType::FixedSizeList(child, _)
92 | ArrowDataType::Map(child, _) => {
93 validate_custom_arrow_field(type_name, child, allow_binary)
94 }
95 ArrowDataType::Struct(children) => {
96 for child in children {
97 validate_custom_arrow_field(type_name, child, allow_binary)?;
98 }
99 Ok(())
100 }
101 ArrowDataType::Dictionary(_, value) => {
102 let child = Field::new(field.name(), value.as_ref().clone(), field.is_nullable());
103 validate_custom_arrow_field(type_name, &child, allow_binary)
104 }
105 _ => Ok(()),
106 }
107}
108
109struct Registries {
110 json: DashMap<String, JsonDeserializer>,
111 #[cfg(feature = "arrow")]
112 arrow: DashMap<String, (Arc<Schema>, ArrowEncoder, ArrowDecoder)>,
113}
114
115fn registries() -> &'static Registries {
116 static REGISTRIES: std::sync::OnceLock<Registries> = std::sync::OnceLock::new();
117 REGISTRIES.get_or_init(|| Registries {
118 json: DashMap::new(),
119 #[cfg(feature = "arrow")]
120 arrow: DashMap::new(),
121 })
122}
123
124pub fn register_json_deserializer(
130 type_name: &str,
131 deserializer: JsonDeserializer,
132) -> Result<(), anyhow::Error> {
133 let reg = registries();
134 match reg.json.entry(type_name.to_string()) {
135 Entry::Occupied(_) => {
136 anyhow::bail!("Custom data type \"{type_name}\" is already registered for JSON");
137 }
138 Entry::Vacant(v) => {
139 v.insert(deserializer);
140 Ok(())
141 }
142 }
143}
144
145pub fn ensure_json_deserializer_registered(
152 type_name: &str,
153 deserializer: JsonDeserializer,
154) -> Result<(), anyhow::Error> {
155 let reg = registries();
156 reg.json
157 .entry(type_name.to_string())
158 .or_insert_with(|| deserializer);
159 Ok(())
160}
161
162fn parse_data_type_from_value(value: &serde_json::Value) -> Option<DataType> {
164 let obj = value.get("data_type")?.as_object()?;
165 let type_name = obj.get("type_name")?.as_str()?;
166 let metadata = obj.get("metadata").and_then(|m| {
167 if m.is_null() {
168 None
169 } else {
170 let p: Params = serde_json::from_value(m.clone()).ok()?;
171 if p.is_empty() { None } else { Some(p) }
172 }
173 });
174 let identifier = obj
175 .get("identifier")
176 .and_then(|v| v.as_str())
177 .map(String::from);
178 Some(DataType::new(type_name, metadata, identifier))
179}
180
181fn parse_envelope_payload(value: &serde_json::Value) -> Result<serde_json::Value, anyhow::Error> {
185 let payload = value
186 .get("payload")
187 .cloned()
188 .ok_or_else(|| anyhow::anyhow!("CustomData JSON missing 'payload' field"))?;
189 Ok(payload)
190}
191
192pub fn deserialize_custom_from_json(
198 type_name: &str,
199 value: &serde_json::Value,
200) -> Result<Option<Data>, anyhow::Error> {
201 let reg = registries();
202 let deserializer_ref = match reg.json.get(type_name) {
203 Some(d) => d,
204 None => return Ok(None),
205 };
206 let data_type = parse_data_type_from_value(value);
207 let payload = parse_envelope_payload(value)?;
208 let arc = deserializer_ref.value()(payload)?;
209 let custom = match data_type {
210 Some(dt) => CustomData::new(arc, dt),
211 None => CustomData::from_arc(arc),
212 };
213 Ok(Some(Data::Custom(custom)))
214}
215
216#[cfg(feature = "arrow")]
221pub fn register_arrow(
222 type_name: &str,
223 schema: Arc<Schema>,
224 encoder: ArrowEncoder,
225 decoder: ArrowDecoder,
226) -> Result<(), anyhow::Error> {
227 let reg = registries();
228 match reg.arrow.entry(type_name.to_string()) {
229 Entry::Occupied(_) => {
230 anyhow::bail!("Custom data type \"{type_name}\" is already registered for Arrow");
231 }
232 Entry::Vacant(v) => {
233 v.insert((schema, encoder, decoder));
234 Ok(())
235 }
236 }
237}
238
239#[cfg(feature = "arrow")]
246pub fn ensure_arrow_registered(
247 type_name: &str,
248 schema: Arc<Schema>,
249 encoder: ArrowEncoder,
250 decoder: ArrowDecoder,
251) -> Result<(), anyhow::Error> {
252 let reg = registries();
253 reg.arrow
254 .entry(type_name.to_string())
255 .or_insert_with(|| (schema, encoder, decoder));
256 Ok(())
257}
258
259#[must_use]
261#[cfg(feature = "arrow")]
262pub fn get_arrow_schema(type_name: &str) -> Option<Arc<Schema>> {
263 let reg = registries();
264 reg.arrow
265 .get(type_name)
266 .map(|entry| Arc::clone(&entry.value().0))
267}
268
269#[cfg(feature = "arrow")]
274pub fn encode_custom_to_arrow(
275 type_name: &str,
276 items: &[Arc<dyn CustomDataTrait>],
277) -> Result<Option<RecordBatch>, anyhow::Error> {
278 let reg = registries();
279 let entry = match reg.arrow.get(type_name) {
280 Some(e) => e,
281 None => return Ok(None),
282 };
283 let encoder = &entry.value().1;
284 encoder(items).map(Some)
285}
286
287#[expect(
292 clippy::implicit_hasher,
293 reason = "callers always use the default hasher"
294)]
295#[cfg(feature = "arrow")]
296pub fn decode_custom_from_arrow(
297 type_name: &str,
298 metadata: &std::collections::HashMap<String, String>,
299 record_batch: RecordBatch,
300) -> Result<Option<Vec<Data>>, anyhow::Error> {
301 let reg = registries();
302 let entry = match reg.arrow.get(type_name) {
303 Some(e) => e,
304 None => return Ok(None),
305 };
306 let decoder = &entry.value().2;
307 decoder(metadata, record_batch).map(Some)
308}
309
310#[cfg(feature = "python")]
311pub type PyExtractor = Box<
312 dyn for<'a> Fn(&pyo3::Bound<'a, pyo3::PyAny>) -> Option<Arc<dyn CustomDataTrait>> + Send + Sync,
313>;
314
315#[cfg(feature = "python")]
316fn py_extractors() -> &'static DashMap<String, PyExtractor> {
317 static PY_EXTRACTORS: std::sync::OnceLock<DashMap<String, PyExtractor>> =
318 std::sync::OnceLock::new();
319 PY_EXTRACTORS.get_or_init(DashMap::new)
320}
321
322#[cfg(feature = "python")]
328pub fn register_py_extractor(type_name: &str, extractor: PyExtractor) -> Result<(), anyhow::Error> {
329 let reg = py_extractors();
330 match reg.entry(type_name.to_string()) {
331 Entry::Occupied(_) => {
332 anyhow::bail!(
333 "Custom data type \"{type_name}\" is already registered for Python extraction"
334 );
335 }
336 Entry::Vacant(v) => {
337 v.insert(extractor);
338 Ok(())
339 }
340 }
341}
342
343#[cfg(feature = "python")]
350pub fn ensure_py_extractor_registered(
351 type_name: &str,
352 extractor: PyExtractor,
353) -> Result<(), anyhow::Error> {
354 let reg = py_extractors();
355 reg.entry(type_name.to_string())
356 .or_insert_with(|| extractor);
357 Ok(())
358}
359
360#[cfg(feature = "python")]
363#[must_use]
364pub fn try_extract_from_py(
365 type_name: &str,
366 obj: &pyo3::Bound<'_, pyo3::PyAny>,
367) -> Option<Arc<dyn CustomDataTrait>> {
368 let reg = py_extractors();
369 let entry = reg.get(type_name)?;
370 let extractor = entry.value();
371 extractor(obj)
372}
373
374#[cfg(feature = "python")]
375type RustExtractorFactory = Box<dyn Fn() -> PyExtractor + Send + Sync>;
376
377#[cfg(feature = "python")]
378fn rust_extractor_factories() -> &'static DashMap<String, RustExtractorFactory> {
379 static RUST_EXTRACTOR_FACTORIES: std::sync::OnceLock<DashMap<String, RustExtractorFactory>> =
380 std::sync::OnceLock::new();
381 RUST_EXTRACTOR_FACTORIES.get_or_init(DashMap::new)
382}
383
384#[cfg(feature = "python")]
392pub fn register_rust_extractor_factory(
393 type_name: &str,
394 factory: RustExtractorFactory,
395) -> Result<(), anyhow::Error> {
396 let reg = rust_extractor_factories();
397 match reg.entry(type_name.to_string()) {
398 Entry::Occupied(_) => {
399 anyhow::bail!("Rust extractor factory for \"{type_name}\" is already registered");
400 }
401 Entry::Vacant(v) => {
402 v.insert(factory);
403 Ok(())
404 }
405 }
406}
407
408#[cfg(feature = "python")]
415pub fn ensure_rust_extractor_factory_registered(
416 type_name: &str,
417 factory: RustExtractorFactory,
418) -> Result<(), anyhow::Error> {
419 let reg = rust_extractor_factories();
420 reg.entry(type_name.to_string()).or_insert_with(|| factory);
421 Ok(())
422}
423
424#[cfg(feature = "python")]
431pub fn register_rust_extractor<T>() -> Result<(), anyhow::Error>
432where
433 T: CustomDataTrait + for<'a, 'py> pyo3::FromPyObject<'a, 'py> + Send + Sync + 'static,
434{
435 let type_name = T::type_name_static();
436 let factory: RustExtractorFactory = Box::new(|| {
437 Box::new(|obj: &pyo3::Bound<'_, pyo3::PyAny>| {
438 obj.extract::<T>()
439 .ok()
440 .map(|x| Arc::new(x) as Arc<dyn CustomDataTrait>)
441 })
442 });
443 register_rust_extractor_factory(type_name, factory)
444}
445
446#[cfg(feature = "python")]
453pub fn ensure_rust_extractor_registered<T>() -> Result<(), anyhow::Error>
454where
455 T: CustomDataTrait + for<'a, 'py> pyo3::FromPyObject<'a, 'py> + Send + Sync + 'static,
456{
457 let type_name = T::type_name_static();
458 let factory: RustExtractorFactory = Box::new(|| {
459 Box::new(|obj: &pyo3::Bound<'_, pyo3::PyAny>| {
460 obj.extract::<T>()
461 .ok()
462 .map(|x| Arc::new(x) as Arc<dyn CustomDataTrait>)
463 })
464 });
465 ensure_rust_extractor_factory_registered(type_name, factory)
466}
467
468#[cfg(feature = "python")]
470#[must_use]
471pub fn get_rust_extractor(type_name: &str) -> Option<PyExtractor> {
472 let reg = rust_extractor_factories();
473 let factory_ref = reg.get(type_name)?;
474 Some(factory_ref.value()())
475}
476
477#[cfg(test)]
478mod tests {
479 use std::hash::{DefaultHasher, Hash, Hasher};
480
481 use nautilus_core::UnixNanos;
482 use rstest::rstest;
483 use serde::{Deserialize, Serialize};
484
485 use super::*;
486 use crate::data::{CustomData, custom::register_custom_data_json};
487
488 #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
489 struct TestRegCustomData {
490 ts_init: UnixNanos,
491 }
492
493 #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
494 #[serde(deny_unknown_fields)]
495 struct StrictRegCustomData {
496 ts_init: UnixNanos,
497 }
498
499 impl crate::data::HasTsInit for TestRegCustomData {
500 fn ts_init(&self) -> UnixNanos {
501 self.ts_init
502 }
503 }
504
505 impl crate::data::custom::CustomDataTrait for TestRegCustomData {
506 fn type_name(&self) -> &'static str {
507 "TestRegCustomData"
508 }
509 fn type_name_static() -> &'static str {
510 "TestRegCustomData"
511 }
512 fn as_any(&self) -> &dyn std::any::Any {
513 self
514 }
515 fn ts_event(&self) -> nautilus_core::UnixNanos {
516 self.ts_init
517 }
518 fn to_json(&self) -> anyhow::Result<String> {
519 Ok(serde_json::to_string(self)?)
520 }
521 fn clone_arc(&self) -> Arc<dyn crate::data::CustomDataTrait> {
522 Arc::new(self.clone())
523 }
524 fn eq_arc(&self, other: &dyn crate::data::CustomDataTrait) -> bool {
525 other.as_any().downcast_ref::<Self>() == Some(self)
526 }
527 fn from_json(
528 value: serde_json::Value,
529 ) -> anyhow::Result<Arc<dyn crate::data::CustomDataTrait>> {
530 let t: Self = serde_json::from_value(value)?;
531 Ok(Arc::new(t))
532 }
533 }
534
535 impl crate::data::HasTsInit for StrictRegCustomData {
536 fn ts_init(&self) -> UnixNanos {
537 self.ts_init
538 }
539 }
540
541 impl crate::data::custom::CustomDataTrait for StrictRegCustomData {
542 fn type_name(&self) -> &'static str {
543 "StrictRegCustomData"
544 }
545 fn type_name_static() -> &'static str {
546 "StrictRegCustomData"
547 }
548 fn as_any(&self) -> &dyn std::any::Any {
549 self
550 }
551 fn ts_event(&self) -> nautilus_core::UnixNanos {
552 self.ts_init
553 }
554 fn to_json(&self) -> anyhow::Result<String> {
555 Ok(serde_json::to_string(self)?)
556 }
557 fn clone_arc(&self) -> Arc<dyn crate::data::CustomDataTrait> {
558 Arc::new(self.clone())
559 }
560 fn eq_arc(&self, other: &dyn crate::data::CustomDataTrait) -> bool {
561 other.as_any().downcast_ref::<Self>() == Some(self)
562 }
563 fn from_json(
564 value: serde_json::Value,
565 ) -> anyhow::Result<Arc<dyn crate::data::CustomDataTrait>> {
566 let t: Self = serde_json::from_value(value)?;
567 Ok(Arc::new(t))
568 }
569 }
570
571 #[rstest]
572 fn json_registry_roundtrip() {
573 let _ = register_custom_data_json::<TestRegCustomData>();
574
575 let data = Data::Custom(CustomData::from_arc(Arc::new(TestRegCustomData {
576 ts_init: UnixNanos::from(100),
577 })));
578
579 let json = serde_json::to_string(&data).unwrap();
580 let back: Data = serde_json::from_str(&json).unwrap();
581
582 match (&data, &back) {
583 (Data::Custom(a), Data::Custom(b)) => {
584 assert_eq!(a.data.type_name(), b.data.type_name());
585 assert_eq!(a.data.ts_init(), b.data.ts_init());
586 }
587 _ => panic!("expected Custom variant"),
588 }
589 }
590
591 #[rstest]
592 fn json_registry_roundtrip_with_deny_unknown_fields() {
593 let _ = register_custom_data_json::<StrictRegCustomData>();
594
595 let data = Data::Custom(CustomData::from_arc(Arc::new(StrictRegCustomData {
596 ts_init: UnixNanos::from(200),
597 })));
598
599 let json = serde_json::to_string(&data).unwrap();
600 let back: Data = serde_json::from_str(&json).unwrap();
601
602 match (&data, &back) {
603 (Data::Custom(a), Data::Custom(b)) => {
604 assert_eq!(a.data.type_name(), b.data.type_name());
605 assert_eq!(a.data.ts_init(), b.data.ts_init());
606 }
607 _ => panic!("expected Custom variant"),
608 }
609 }
610
611 #[rstest]
612 fn data_type_registry_result_hashes_like_equal_values_from_all_routes() {
613 fn hash_data_type(data_type: &DataType) -> u64 {
614 let mut hasher = DefaultHasher::new();
615 data_type.hash(&mut hasher);
616 hasher.finish()
617 }
618
619 let metadata = serde_json::json!({"key": "value"});
620 let constructed = DataType::new(
621 "ExampleType",
622 Some(serde_json::from_value(metadata.clone()).unwrap()),
623 Some("catalog/path".to_string()),
624 );
625 let persistence_json = serde_json::json!({
626 "type_name": constructed.type_name(),
627 "metadata": metadata,
628 "identifier": constructed.identifier(),
629 });
630 let persisted = DataType::from_persistence_json(&persistence_json.to_string()).unwrap();
631 let registry_envelope = serde_json::json!({"data_type": persistence_json});
632 let registered = parse_data_type_from_value(®istry_envelope).unwrap();
633 let deserialization_payload = serde_json::json!({
634 "type_name": constructed.type_name(),
635 "metadata": constructed.metadata(),
636 "topic": constructed.topic(),
637 "hash": constructed.precomputed_hash() ^ u64::MAX,
638 "identifier": constructed.identifier(),
639 });
640 let deserialized: DataType = serde_json::from_value(deserialization_payload).unwrap();
641
642 for data_type in [&deserialized, &persisted, ®istered] {
646 assert_eq!(data_type, &constructed);
647 assert_eq!(hash_data_type(data_type), hash_data_type(&constructed));
648 assert_eq!(data_type.type_name(), constructed.type_name());
649 assert_eq!(data_type.metadata(), constructed.metadata());
650 assert_eq!(data_type.identifier(), constructed.identifier());
651 assert_eq!(data_type.topic(), constructed.topic());
652 }
653 }
654
655 #[rstest]
656 fn ensure_json_deserializer_registered_is_idempotent() {
657 let deserializer: JsonDeserializer = Box::new(|value| {
658 let t: TestRegCustomData = serde_json::from_value(value)?;
659 Ok(Arc::new(t) as Arc<dyn crate::data::CustomDataTrait>)
660 });
661 let r1 = ensure_json_deserializer_registered("IdempotentTestJson", deserializer);
662 assert!(r1.is_ok(), "first registration should succeed");
663 let deserializer2: JsonDeserializer = Box::new(|value| {
664 let t: TestRegCustomData = serde_json::from_value(value)?;
665 Ok(Arc::new(t) as Arc<dyn crate::data::CustomDataTrait>)
666 });
667 let r2 = ensure_json_deserializer_registered("IdempotentTestJson", deserializer2);
668 assert!(
669 r2.is_ok(),
670 "second registration with same type_name should succeed (idempotent)"
671 );
672 }
673
674 #[rstest]
675 fn register_json_deserializer_fails_on_duplicate() {
676 let deserializer: JsonDeserializer = Box::new(|value| {
677 let t: TestRegCustomData = serde_json::from_value(value)?;
678 Ok(Arc::new(t) as Arc<dyn crate::data::CustomDataTrait>)
679 });
680 let r1 = register_json_deserializer("StrictDuplicateTestJson", deserializer);
681 assert!(r1.is_ok());
682 let deserializer2: JsonDeserializer = Box::new(|value| {
683 let t: TestRegCustomData = serde_json::from_value(value)?;
684 Ok(Arc::new(t) as Arc<dyn crate::data::CustomDataTrait>)
685 });
686 let r2 = register_json_deserializer("StrictDuplicateTestJson", deserializer2);
687 assert!(r2.is_err());
688 let err_msg = r2.unwrap_err().to_string();
689 assert!(
690 err_msg.contains("already registered"),
691 "expected 'already registered' in error, found: {err_msg}"
692 );
693 }
694
695 #[rstest]
696 #[cfg(feature = "arrow")]
697 fn ensure_arrow_registered_is_idempotent() {
698 let schema = Arc::new(arrow::datatypes::Schema::empty());
699 let encoder: ArrowEncoder = Box::new(|_| {
700 Ok(arrow::record_batch::RecordBatch::new_empty(Arc::new(
701 arrow::datatypes::Schema::empty(),
702 )))
703 });
704 let decoder: ArrowDecoder = Box::new(|_, _| Ok(Vec::new()));
705
706 let r1 = ensure_arrow_registered("IdempotentTestArrow", schema, encoder, decoder);
707 assert!(r1.is_ok(), "first Arrow registration should succeed");
708
709 let schema2 = Arc::new(arrow::datatypes::Schema::empty());
710 let encoder2: ArrowEncoder = Box::new(|_| {
711 Ok(arrow::record_batch::RecordBatch::new_empty(Arc::new(
712 arrow::datatypes::Schema::empty(),
713 )))
714 });
715 let decoder2: ArrowDecoder = Box::new(|_, _| Ok(Vec::new()));
716
717 let r2 = ensure_arrow_registered("IdempotentTestArrow", schema2, encoder2, decoder2);
718 assert!(
719 r2.is_ok(),
720 "second Arrow registration with same type_name should be idempotent"
721 );
722 }
723
724 #[rstest]
725 #[cfg(feature = "arrow")]
726 fn python_custom_arrow_schema_rejects_opaque_bytes() {
727 let schema = Schema::new(vec![Field::new("payload", ArrowDataType::Binary, false)]);
728
729 let error = validate_custom_arrow_schema("PythonPayload", &schema, false).unwrap_err();
730
731 assert_eq!(
732 error.to_string(),
733 "custom write schema `PythonPayload` contains opaque byte field `payload`: Binary",
734 );
735 }
736}