nemo_relay/api/event.rs
1// SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2// SPDX-License-Identifier: Apache-2.0
3//
4// Licensed under the Apache License, Version 2.0 (the "License");
5// you may not use this file except in compliance with the License.
6// You may obtain a copy of the License at
7//
8// http://www.apache.org/licenses/LICENSE-2.0
9//
10// Unless required by applicable law or agreed to in writing, software
11// distributed under the License is distributed on an "AS IS" BASIS,
12// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13// See the License for the specific language governing permissions and
14// limitations under the License.
15
16//! Event types for Agent Trajectory Observability Format (ATOF) runtime events.
17
18use std::collections::BTreeMap;
19use std::sync::Arc;
20
21use chrono::{DateTime, Utc};
22use serde::{Deserialize, Serialize};
23use typed_builder::TypedBuilder;
24use uuid::Uuid;
25
26use crate::api::llm::LlmAttributes;
27use crate::api::scope::{HandleAttributes, ScopeAttributes, ScopeType};
28use crate::api::tool::ToolAttributes;
29use crate::codec::request::AnnotatedLlmRequest;
30use crate::codec::response::AnnotatedLlmResponse;
31use crate::json::Json;
32
33/// Agent Trajectory Observability Format (ATOF) protocol version emitted by this runtime.
34pub const ATOF_VERSION: &str = "0.1";
35
36/// Identifier for the schema that describes an event's opaque `data` payload.
37#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TypedBuilder)]
38#[builder(field_defaults(setter(into)))]
39pub struct DataSchema {
40 /// Schema name.
41 pub name: String,
42 /// Schema version.
43 pub version: String,
44}
45
46/// Semantic category carried by ATOF `category`.
47///
48/// This is intentionally string-backed so consumers can preserve category
49/// values from newer producers without failing deserialization.
50#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
51#[serde(transparent)]
52pub struct EventCategory(String);
53
54impl EventCategory {
55 /// Top-level agent or workflow scope.
56 ///
57 /// # Returns
58 /// An [`EventCategory`] with the wire value `agent`.
59 pub fn agent() -> Self {
60 Self("agent".into())
61 }
62
63 /// Generic function or application step.
64 ///
65 /// # Returns
66 /// An [`EventCategory`] with the wire value `function`.
67 pub fn function() -> Self {
68 Self("function".into())
69 }
70
71 /// LLM call.
72 ///
73 /// # Returns
74 /// An [`EventCategory`] with the wire value `llm`.
75 pub fn llm() -> Self {
76 Self("llm".into())
77 }
78
79 /// Tool invocation.
80 ///
81 /// # Returns
82 /// An [`EventCategory`] with the wire value `tool`.
83 pub fn tool() -> Self {
84 Self("tool".into())
85 }
86
87 /// Retrieval step.
88 ///
89 /// # Returns
90 /// An [`EventCategory`] with the wire value `retriever`.
91 pub fn retriever() -> Self {
92 Self("retriever".into())
93 }
94
95 /// Embedding-generation step.
96 ///
97 /// # Returns
98 /// An [`EventCategory`] with the wire value `embedder`.
99 pub fn embedder() -> Self {
100 Self("embedder".into())
101 }
102
103 /// Result reranking step.
104 ///
105 /// # Returns
106 /// An [`EventCategory`] with the wire value `reranker`.
107 pub fn reranker() -> Self {
108 Self("reranker".into())
109 }
110
111 /// Guardrail or validation step.
112 ///
113 /// # Returns
114 /// An [`EventCategory`] with the wire value `guardrail`.
115 pub fn guardrail() -> Self {
116 Self("guardrail".into())
117 }
118
119 /// Evaluation or scoring step.
120 ///
121 /// # Returns
122 /// An [`EventCategory`] with the wire value `evaluator`.
123 pub fn evaluator() -> Self {
124 Self("evaluator".into())
125 }
126
127 /// Vendor-defined custom category.
128 ///
129 /// # Returns
130 /// An [`EventCategory`] with the wire value `custom`.
131 pub fn custom() -> Self {
132 Self("custom".into())
133 }
134
135 /// Unknown or unclassified work.
136 ///
137 /// # Returns
138 /// An [`EventCategory`] with the wire value `unknown`.
139 pub fn unknown() -> Self {
140 Self("unknown".into())
141 }
142
143 /// Create a category from an arbitrary producer-provided string.
144 ///
145 /// # Parameters
146 /// - `value`: Wire category value to preserve.
147 ///
148 /// # Returns
149 /// An [`EventCategory`] containing `value`.
150 pub fn new(value: impl Into<String>) -> Self {
151 Self(value.into())
152 }
153
154 /// Return the string form serialized on the wire.
155 ///
156 /// # Returns
157 /// The category value as a string slice.
158 pub fn as_str(&self) -> &str {
159 self.0.as_str()
160 }
161
162 /// Convert this category to the closest legacy scope type for internal
163 /// adapters that still need span-kind classification.
164 ///
165 /// # Returns
166 /// The closest matching [`ScopeType`], or [`ScopeType::Unknown`] when the
167 /// category has no legacy equivalent.
168 pub fn to_scope_type(&self) -> ScopeType {
169 match self.as_str() {
170 "agent" => ScopeType::Agent,
171 "function" => ScopeType::Function,
172 "tool" => ScopeType::Tool,
173 "llm" => ScopeType::Llm,
174 "retriever" => ScopeType::Retriever,
175 "embedder" => ScopeType::Embedder,
176 "reranker" => ScopeType::Reranker,
177 "guardrail" => ScopeType::Guardrail,
178 "evaluator" => ScopeType::Evaluator,
179 "custom" => ScopeType::Custom,
180 _ => ScopeType::Unknown,
181 }
182 }
183}
184
185impl From<ScopeType> for EventCategory {
186 fn from(value: ScopeType) -> Self {
187 match value {
188 ScopeType::Agent => Self::agent(),
189 ScopeType::Function => Self::function(),
190 ScopeType::Tool => Self::tool(),
191 ScopeType::Llm => Self::llm(),
192 ScopeType::Retriever => Self::retriever(),
193 ScopeType::Embedder => Self::embedder(),
194 ScopeType::Reranker => Self::reranker(),
195 ScopeType::Guardrail => Self::guardrail(),
196 ScopeType::Evaluator => Self::evaluator(),
197 ScopeType::Custom => Self::custom(),
198 ScopeType::Unknown => Self::unknown(),
199 }
200 }
201}
202
203impl From<&EventCategory> for ScopeType {
204 fn from(value: &EventCategory) -> Self {
205 value.to_scope_type()
206 }
207}
208
209/// Agent Trajectory Observability Format (ATOF) lifecycle phase for a scope event.
210#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
211#[serde(rename_all = "lowercase")]
212pub enum ScopeCategory {
213 /// Scope was entered.
214 Start,
215 /// Scope was exited.
216 End,
217}
218
219/// Category-specific profile data.
220///
221/// Unknown wire keys are preserved in `extra`. LLM annotations are serialized
222/// under `category_profile` when a codec captures them.
223#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, TypedBuilder)]
224#[builder(field_defaults(setter(into, strip_option(ignore_invalid, fallback_suffix = "_opt"))))]
225pub struct CategoryProfile {
226 /// Normalized model identifier for LLM events.
227 #[builder(default)]
228 #[serde(default, skip_serializing_if = "Option::is_none")]
229 pub model_name: Option<String>,
230
231 /// LLM-provider correlation ID for Tool events.
232 #[builder(default)]
233 #[serde(default, skip_serializing_if = "Option::is_none")]
234 pub tool_call_id: Option<String>,
235
236 /// Vendor subtype required when `category == "custom"`.
237 #[builder(default)]
238 #[serde(default, skip_serializing_if = "Option::is_none")]
239 pub subtype: Option<String>,
240
241 /// Unknown category-profile keys preserved from newer producers.
242 #[builder(default)]
243 #[serde(flatten)]
244 pub extra: BTreeMap<String, Json>,
245
246 /// Normalized request annotation for LLM start events.
247 #[builder(default)]
248 #[serde(default, skip_serializing_if = "Option::is_none")]
249 pub annotated_request: Option<Arc<AnnotatedLlmRequest>>,
250
251 /// Normalized response annotation for LLM end events.
252 #[builder(default)]
253 #[serde(default, skip_serializing_if = "Option::is_none")]
254 pub annotated_response: Option<Arc<AnnotatedLlmResponse>>,
255}
256
257impl CategoryProfile {
258 /// Return true when the profile has no wire-serialized fields.
259 ///
260 /// # Returns
261 /// `true` when no profile fields would be serialized on the wire.
262 pub fn is_wire_empty(&self) -> bool {
263 self.model_name.is_none()
264 && self.tool_call_id.is_none()
265 && self.subtype.is_none()
266 && self.annotated_request.is_none()
267 && self.annotated_response.is_none()
268 && self.extra.is_empty()
269 }
270}
271
272/// Shared event metadata carried by every ATOF event.
273#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TypedBuilder)]
274#[builder(field_defaults(setter(into, strip_option(ignore_invalid, fallback_suffix = "_opt"))))]
275pub struct BaseEvent {
276 /// ATOF protocol version.
277 #[builder(default = ATOF_VERSION.to_string())]
278 pub atof_version: String,
279 /// UUID of the parent scope, if any.
280 #[builder(default)]
281 pub parent_uuid: Option<Uuid>,
282 /// Unique identifier for the event or span.
283 #[builder(default = Uuid::now_v7())]
284 pub uuid: Uuid,
285 /// Event timestamp in UTC.
286 #[builder(default = Utc::now())]
287 #[serde(with = "timestamp")]
288 pub timestamp: DateTime<Utc>,
289 /// Human-readable event name.
290 pub name: String,
291 /// Application-defined payload.
292 #[builder(default)]
293 pub data: Option<Json>,
294 /// Optional schema identifier for `data`.
295 #[builder(default)]
296 pub data_schema: Option<DataSchema>,
297 /// Optional tracing/correlation metadata.
298 #[builder(default)]
299 pub metadata: Option<Json>,
300}
301
302/// ATOF scope lifecycle event.
303#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TypedBuilder)]
304#[builder(field_defaults(setter(into, strip_option(ignore_invalid, fallback_suffix = "_opt"))))]
305pub struct ScopeEvent {
306 /// Shared ATOF envelope.
307 #[serde(flatten)]
308 #[builder(setter(skip), default = BaseEvent::builder().name("").build())]
309 pub base: BaseEvent,
310 /// Scope lifecycle phase.
311 pub scope_category: ScopeCategory,
312 /// Canonical lowercase behavioral flags.
313 #[builder(default)]
314 pub attributes: Vec<String>,
315 /// Semantic category of work.
316 pub category: EventCategory,
317 /// Category-specific typed fields.
318 #[builder(default)]
319 pub category_profile: Option<CategoryProfile>,
320}
321
322impl ScopeEvent {
323 /// Construct a scope event from a base envelope and ATOF-specific fields.
324 ///
325 /// # Parameters
326 /// - `base`: Shared ATOF event envelope.
327 /// - `scope_category`: Lifecycle phase for the scope event.
328 /// - `attributes`: Scope attributes to canonicalize and attach.
329 /// - `category`: Semantic event category.
330 /// - `category_profile`: Optional category-specific profile data.
331 ///
332 /// # Returns
333 /// A [`ScopeEvent`] containing the provided fields.
334 pub fn new(
335 base: BaseEvent,
336 scope_category: ScopeCategory,
337 attributes: Vec<String>,
338 category: EventCategory,
339 category_profile: Option<CategoryProfile>,
340 ) -> Self {
341 Self {
342 base,
343 scope_category,
344 attributes: canonicalize_attributes(attributes),
345 category,
346 category_profile,
347 }
348 }
349}
350
351/// ATOF point-in-time mark event.
352#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TypedBuilder)]
353#[builder(field_defaults(setter(into, strip_option(ignore_invalid, fallback_suffix = "_opt"))))]
354pub struct MarkEvent {
355 /// Shared ATOF envelope.
356 #[serde(flatten)]
357 #[builder(setter(skip), default = BaseEvent::builder().name("").build())]
358 pub base: BaseEvent,
359 /// Optional semantic category for the checkpoint.
360 #[builder(default)]
361 pub category: Option<EventCategory>,
362 /// Optional category-specific typed fields.
363 #[builder(default)]
364 pub category_profile: Option<CategoryProfile>,
365}
366
367impl MarkEvent {
368 /// Construct a mark event from a base envelope and optional category data.
369 ///
370 /// # Parameters
371 /// - `base`: Shared ATOF event envelope.
372 /// - `category`: Optional semantic event category.
373 /// - `category_profile`: Optional category-specific profile data.
374 ///
375 /// # Returns
376 /// A [`MarkEvent`] containing the provided fields.
377 pub fn new(
378 base: BaseEvent,
379 category: Option<EventCategory>,
380 category_profile: Option<CategoryProfile>,
381 ) -> Self {
382 Self {
383 base,
384 category,
385 category_profile,
386 }
387 }
388}
389
390/// Tagged union covering the two ATOF event kinds emitted by the runtime.
391#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
392#[serde(tag = "kind", rename_all = "lowercase")]
393pub enum Event {
394 /// Scope lifecycle event.
395 Scope(ScopeEvent),
396 /// Point-in-time checkpoint event.
397 Mark(MarkEvent),
398}
399
400impl Event {
401 /// Return the ATOF event kind.
402 ///
403 /// # Returns
404 /// `"scope"` for [`Event::Scope`] and `"mark"` for [`Event::Mark`].
405 pub fn kind(&self) -> &'static str {
406 match self {
407 Self::Scope(_) => "scope",
408 Self::Mark(_) => "mark",
409 }
410 }
411
412 /// Try to return this event as the canonical JSON object delivered by
413 /// language bindings to subscriber callbacks and ATOF exporters.
414 pub fn try_to_json_value(&self) -> serde_json::Result<Json> {
415 serde_json::to_value(self)
416 }
417
418 /// Return this event as the canonical JSON object delivered by language
419 /// bindings to subscriber callbacks.
420 pub fn to_json_value(&self) -> Json {
421 self.try_to_json_value()
422 .expect("serializing an ATOF event to JSON should not fail")
423 }
424
425 /// Return this event as canonical JSON.
426 pub fn to_json_string(&self) -> serde_json::Result<String> {
427 serde_json::to_string(&self.try_to_json_value()?)
428 }
429
430 /// Return the lifecycle phase for scope events.
431 ///
432 /// # Returns
433 /// `Some` lifecycle phase for scope events, otherwise `None`.
434 pub fn scope_category(&self) -> Option<ScopeCategory> {
435 match self {
436 Self::Scope(event) => Some(event.scope_category),
437 Self::Mark(_) => None,
438 }
439 }
440
441 /// Return the semantic category if present.
442 ///
443 /// # Returns
444 /// `Some` category for scope events and categorized mark events, otherwise
445 /// `None`.
446 pub fn category(&self) -> Option<&EventCategory> {
447 match self {
448 Self::Scope(event) => Some(&event.category),
449 Self::Mark(event) => event.category.as_ref(),
450 }
451 }
452
453 /// Return the category-specific profile if present.
454 ///
455 /// # Returns
456 /// `Some` profile when category-specific fields are present.
457 pub fn category_profile(&self) -> Option<&CategoryProfile> {
458 match self {
459 Self::Scope(event) => event.category_profile.as_ref(),
460 Self::Mark(event) => event.category_profile.as_ref(),
461 }
462 }
463
464 /// Return the mutable category-specific profile if present.
465 ///
466 /// # Returns
467 /// `Some` mutable profile when category-specific fields are present.
468 pub fn category_profile_mut(&mut self) -> Option<&mut CategoryProfile> {
469 match self {
470 Self::Scope(event) => event.category_profile.as_mut(),
471 Self::Mark(event) => event.category_profile.as_mut(),
472 }
473 }
474
475 /// Return the parent scope UUID, if the event is nested under a scope.
476 ///
477 /// # Returns
478 /// `Some` parent UUID when the event has a parent scope, otherwise `None`.
479 pub fn parent_uuid(&self) -> Option<Uuid> {
480 self.base().parent_uuid
481 }
482
483 /// Return the unique event or span UUID.
484 ///
485 /// # Returns
486 /// The event UUID.
487 pub fn uuid(&self) -> Uuid {
488 self.base().uuid
489 }
490
491 /// Return the event timestamp.
492 ///
493 /// # Returns
494 /// The UTC event timestamp.
495 pub fn timestamp(&self) -> &DateTime<Utc> {
496 &self.base().timestamp
497 }
498
499 /// Return the human-readable event name.
500 ///
501 /// # Returns
502 /// The event name.
503 pub fn name(&self) -> &str {
504 self.base().name.as_str()
505 }
506
507 /// Return the optional application payload attached to the event.
508 ///
509 /// # Returns
510 /// `Some` payload when event data is present, otherwise `None`.
511 pub fn data(&self) -> Option<&Json> {
512 self.base().data.as_ref()
513 }
514
515 /// Return the optional data schema.
516 ///
517 /// # Returns
518 /// `Some` schema when the event payload declares one, otherwise `None`.
519 pub fn data_schema(&self) -> Option<&DataSchema> {
520 self.base().data_schema.as_ref()
521 }
522
523 /// Return the optional metadata attached to the event.
524 ///
525 /// # Returns
526 /// `Some` metadata when present, otherwise `None`.
527 pub fn metadata(&self) -> Option<&Json> {
528 self.base().metadata.as_ref()
529 }
530
531 /// Return attributes for scope events.
532 ///
533 /// # Returns
534 /// `Some` attributes for scope events, otherwise `None`.
535 pub fn attributes(&self) -> Option<&[String]> {
536 match self {
537 Self::Scope(event) => Some(event.attributes.as_slice()),
538 Self::Mark(_) => None,
539 }
540 }
541
542 /// Return the semantic scope category for scope events.
543 ///
544 /// # Returns
545 /// `Some` legacy [`ScopeType`] when the event has a category.
546 pub fn scope_type(&self) -> Option<ScopeType> {
547 self.category().map(EventCategory::to_scope_type)
548 }
549
550 /// Return the semantic input payload for start events.
551 ///
552 /// # Returns
553 /// `Some` payload for scope-start events with data, otherwise `None`.
554 pub fn input(&self) -> Option<&Json> {
555 match self {
556 Self::Scope(event) if event.scope_category == ScopeCategory::Start => {
557 event.base.data.as_ref()
558 }
559 _ => None,
560 }
561 }
562
563 /// Return the semantic output payload for end events.
564 ///
565 /// # Returns
566 /// `Some` payload for scope-end events with data, otherwise `None`.
567 pub fn output(&self) -> Option<&Json> {
568 match self {
569 Self::Scope(event) if event.scope_category == ScopeCategory::End => {
570 event.base.data.as_ref()
571 }
572 _ => None,
573 }
574 }
575
576 /// Return the normalized model name for LLM events.
577 ///
578 /// # Returns
579 /// `Some` model name when the event profile includes one.
580 pub fn model_name(&self) -> Option<&str> {
581 self.category_profile()
582 .and_then(|profile| profile.model_name.as_deref())
583 }
584
585 /// Return the provider-specific tool-call correlation identifier.
586 ///
587 /// # Returns
588 /// `Some` tool call identifier when the event profile includes one.
589 pub fn tool_call_id(&self) -> Option<&str> {
590 self.category_profile()
591 .and_then(|profile| profile.tool_call_id.as_deref())
592 }
593
594 /// Return the runtime-only annotated LLM request.
595 ///
596 /// # Returns
597 /// `Some` annotated request when the event profile includes one.
598 pub fn annotated_request(&self) -> Option<&Arc<AnnotatedLlmRequest>> {
599 self.category_profile()
600 .and_then(|profile| profile.annotated_request.as_ref())
601 }
602
603 /// Return the runtime-only annotated LLM response.
604 ///
605 /// # Returns
606 /// `Some` annotated response when the event profile includes one.
607 pub fn annotated_response(&self) -> Option<&Arc<AnnotatedLlmResponse>> {
608 self.category_profile()
609 .and_then(|profile| profile.annotated_response.as_ref())
610 }
611
612 /// Return true for scope-start events.
613 ///
614 /// # Returns
615 /// `true` when the event is a scope-start event.
616 pub fn is_scope_start(&self) -> bool {
617 matches!(
618 self,
619 Self::Scope(ScopeEvent {
620 scope_category: ScopeCategory::Start,
621 ..
622 })
623 )
624 }
625
626 /// Return true for scope-end events.
627 ///
628 /// # Returns
629 /// `true` when the event is a scope-end event.
630 pub fn is_scope_end(&self) -> bool {
631 matches!(
632 self,
633 Self::Scope(ScopeEvent {
634 scope_category: ScopeCategory::End,
635 ..
636 })
637 )
638 }
639
640 fn base(&self) -> &BaseEvent {
641 match self {
642 Self::Scope(event) => &event.base,
643 Self::Mark(event) => &event.base,
644 }
645 }
646}
647
648/// Convert handle bitflags into ATOF attributes.
649///
650/// # Parameters
651/// - `attributes`: Handle-specific attribute bitflags.
652///
653/// # Returns
654/// Canonical lowercase ATOF attribute strings for the provided bitflags.
655pub fn attributes_from_handle(attributes: HandleAttributes) -> Vec<String> {
656 match attributes {
657 HandleAttributes::Scope(attributes) => scope_attributes_to_strings(attributes),
658 HandleAttributes::Tool(attributes) => tool_attributes_to_strings(attributes),
659 HandleAttributes::Llm(attributes) => llm_attributes_to_strings(attributes),
660 }
661}
662
663/// Convert scope bitflags into ATOF attributes.
664///
665/// # Parameters
666/// - `attributes`: Scope attribute bitflags.
667///
668/// # Returns
669/// Canonical lowercase ATOF attribute strings for the provided bitflags.
670pub fn scope_attributes_to_strings(attributes: ScopeAttributes) -> Vec<String> {
671 let mut values = Vec::new();
672 if attributes.contains(ScopeAttributes::PARALLEL) {
673 values.push("parallel".to_string());
674 }
675 if attributes.contains(ScopeAttributes::RELOCATABLE) {
676 values.push("relocatable".to_string());
677 }
678 values
679}
680
681/// Convert tool bitflags into ATOF attributes.
682///
683/// # Parameters
684/// - `attributes`: Tool attribute bitflags.
685///
686/// # Returns
687/// Canonical lowercase ATOF attribute strings for the provided bitflags.
688pub fn tool_attributes_to_strings(attributes: ToolAttributes) -> Vec<String> {
689 let mut values = Vec::new();
690 if attributes.contains(ToolAttributes::REMOTE) {
691 values.push("remote".to_string());
692 }
693 values
694}
695
696/// Convert LLM bitflags into ATOF attributes.
697///
698/// # Parameters
699/// - `attributes`: LLM attribute bitflags.
700///
701/// # Returns
702/// Canonical lowercase ATOF attribute strings for the provided bitflags.
703pub fn llm_attributes_to_strings(attributes: LlmAttributes) -> Vec<String> {
704 let mut values = Vec::new();
705 if attributes.contains(LlmAttributes::STATEFUL) {
706 values.push("stateful".to_string());
707 }
708 if attributes.contains(LlmAttributes::STREAMING) {
709 values.push("streaming".to_string());
710 }
711 values
712}
713
714fn canonicalize_attributes(mut attributes: Vec<String>) -> Vec<String> {
715 attributes.sort();
716 attributes.dedup();
717 attributes
718}
719
720mod timestamp {
721 use chrono::{DateTime, Utc};
722 use serde::{
723 Deserializer, Serializer,
724 de::{self, Visitor},
725 };
726 use std::fmt;
727
728 /// Serialize a UTC timestamp as RFC 3339.
729 ///
730 /// # Parameters
731 /// - `value`: Timestamp to serialize.
732 /// - `serializer`: Serde serializer receiving the string value.
733 ///
734 /// # Returns
735 /// The serializer's success value.
736 ///
737 /// # Errors
738 /// Returns any error produced by the serializer.
739 pub fn serialize<S>(value: &DateTime<Utc>, serializer: S) -> Result<S::Ok, S::Error>
740 where
741 S: Serializer,
742 {
743 serializer.serialize_str(&value.to_rfc3339())
744 }
745
746 /// Deserialize a UTC timestamp from an RFC 3339 string.
747 ///
748 /// # Parameters
749 /// - `deserializer`: Serde deserializer providing the timestamp value.
750 ///
751 /// # Returns
752 /// Parsed UTC timestamp.
753 ///
754 /// # Errors
755 /// Returns a serde error when the input is not a valid RFC 3339 timestamp.
756 pub fn deserialize<'de, D>(deserializer: D) -> Result<DateTime<Utc>, D::Error>
757 where
758 D: Deserializer<'de>,
759 {
760 deserializer.deserialize_any(TimestampVisitor)
761 }
762
763 struct TimestampVisitor;
764
765 impl<'de> Visitor<'de> for TimestampVisitor {
766 type Value = DateTime<Utc>;
767
768 fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
769 formatter.write_str("an RFC 3339 timestamp string or epoch microseconds integer")
770 }
771
772 fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
773 where
774 E: de::Error,
775 {
776 DateTime::parse_from_rfc3339(value)
777 .map(|timestamp| timestamp.with_timezone(&Utc))
778 .map_err(E::custom)
779 }
780
781 fn visit_i64<E>(self, value: i64) -> Result<Self::Value, E>
782 where
783 E: de::Error,
784 {
785 DateTime::<Utc>::from_timestamp_micros(value)
786 .ok_or_else(|| E::custom("epoch microseconds value is out of range"))
787 }
788
789 fn visit_u64<E>(self, value: u64) -> Result<Self::Value, E>
790 where
791 E: de::Error,
792 {
793 let value = i64::try_from(value)
794 .map_err(|_| E::custom("epoch microseconds value is out of range"))?;
795 self.visit_i64(value)
796 }
797 }
798}