1use serde::Serialize;
2use serde_json::Value;
3use std::fmt;
4
5#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
16pub struct Event(Value);
17
18impl Event {
19 pub fn as_value(&self) -> &Value {
21 &self.0
22 }
23
24 pub fn into_value(self) -> Value {
26 self.0
27 }
28}
29
30impl From<Event> for Value {
31 fn from(event: Event) -> Self {
32 event.0
33 }
34}
35
36impl fmt::Display for Event {
37 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
38 write!(f, "{}", self.0)
39 }
40}
41
42#[derive(Clone, Debug, PartialEq, Eq)]
49pub enum BuildError {
50 ReservedField(String),
51 NonObjectField(String),
52 EmptyRequiredField(String),
53}
54
55impl fmt::Display for BuildError {
56 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
57 match self {
58 Self::ReservedField(msg) => write!(f, "reserved field: {msg}"),
59 Self::NonObjectField(msg) => write!(f, "non-object field: {msg}"),
60 Self::EmptyRequiredField(msg) => write!(f, "empty required field: {msg}"),
61 }
62 }
63}
64
65impl std::error::Error for BuildError {}
66
67#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
69#[serde(rename_all = "lowercase")]
70pub enum LogLevel {
71 Debug,
72 Info,
73 Warn,
74 Error,
75}
76
77impl LogLevel {
78 #[cfg(any(feature = "cli", feature = "tracing"))]
82 pub(crate) fn as_str(&self) -> &'static str {
83 match self {
84 Self::Debug => "debug",
85 Self::Info => "info",
86 Self::Warn => "warn",
87 Self::Error => "error",
88 }
89 }
90}
91
92pub struct ResultBuilder {
98 payload: Value,
99 trace: Option<Value>,
100}
101
102impl ResultBuilder {
103 pub fn trace(mut self, trace: Value) -> Self {
105 self.trace = Some(trace);
106 self
107 }
108
109 pub fn build(self) -> Event {
111 let trace = self
112 .trace
113 .unwrap_or_else(|| Value::Object(serde_json::Map::new()));
114 let mut obj = serde_json::Map::new();
115 obj.insert("kind".to_string(), Value::String("result".to_string()));
116 obj.insert("result".to_string(), self.payload);
117 obj.insert("trace".to_string(), trace);
118 Event(Value::Object(obj))
119 }
120}
121
122pub fn json_result(payload: Value) -> ResultBuilder {
124 ResultBuilder {
125 payload,
126 trace: None,
127 }
128}
129
130pub struct ErrorBuilder {
136 code: String,
137 message: String,
138 retryable: bool,
139 hint: Option<String>,
140 fields: serde_json::Map<String, Value>,
141 trace: Option<Value>,
142 build_error: Option<BuildError>,
143}
144
145impl ErrorBuilder {
146 pub fn retryable(mut self) -> Self {
148 self.retryable = true;
149 self
150 }
151
152 pub fn retryable_if(mut self, should_retry: bool) -> Self {
154 self.retryable = should_retry;
155 self
156 }
157
158 pub fn hint(mut self, hint: &str) -> Self {
160 if !hint.is_empty() {
161 self.hint = Some(hint.to_string());
162 }
163 self
164 }
165
166 pub fn hint_if_some(mut self, hint: Option<&str>) -> Self {
168 if let Some(h) = hint.filter(|hint| !hint.is_empty()) {
169 self.hint = Some(h.to_string());
170 }
171 self
172 }
173
174 pub fn field(mut self, name: &str, value: Value) -> Self {
176 if self.build_error.is_none() {
177 match name {
178 "code" | "message" | "hint" | "retryable" => {
179 self.build_error = Some(BuildError::ReservedField(format!(
180 "cannot write reserved field {name:?} to error payload"
181 )));
182 }
183 _ => {
184 self.fields.insert(name.to_string(), value);
185 }
186 }
187 }
188 self
189 }
190
191 pub fn fields(mut self, fields: Value) -> Self {
193 if self.build_error.is_none() {
194 match fields {
195 Value::Object(map) => {
196 for (k, v) in map {
197 match k.as_str() {
198 "code" | "message" | "hint" | "retryable" => {
199 self.build_error = Some(BuildError::ReservedField(format!(
200 "cannot write reserved field {k:?} to error payload"
201 )));
202 return self;
203 }
204 _ => {
205 self.fields.insert(k, v);
206 }
207 }
208 }
209 }
210 _ => {
211 self.build_error = Some(BuildError::NonObjectField(
212 "fields() argument must be a JSON object".to_string(),
213 ));
214 }
215 }
216 }
217 self
218 }
219
220 pub fn extend<T: Serialize>(mut self, value: T) -> Self {
222 if self.build_error.is_none() {
223 match serde_json::to_value(&value) {
224 Ok(Value::Object(map)) => {
225 for (k, v) in map {
226 match k.as_str() {
227 "code" | "message" | "hint" | "retryable" => {
228 self.build_error = Some(BuildError::ReservedField(format!(
229 "cannot write reserved field {k:?} to error payload"
230 )));
231 return self;
232 }
233 _ => {
234 self.fields.insert(k, v);
235 }
236 }
237 }
238 }
239 Ok(_) => {
240 self.build_error = Some(BuildError::NonObjectField(
241 "extend() argument must serialize to a JSON object".to_string(),
242 ));
243 }
244 Err(_) => {
245 self.build_error = Some(BuildError::NonObjectField(
246 "extend() argument serialization failed".to_string(),
247 ));
248 }
249 }
250 }
251 self
252 }
253
254 pub fn trace(mut self, trace: Value) -> Self {
256 self.trace = Some(trace);
257 self
258 }
259
260 pub fn build(self) -> Result<Event, BuildError> {
262 if let Some(err) = self.build_error {
263 return Err(err);
264 }
265 if self.trace.as_ref().is_some_and(|trace| !trace.is_object()) {
266 return Err(BuildError::NonObjectField(
267 "trace() argument must be a JSON object".to_string(),
268 ));
269 }
270
271 let mut error_obj = self.fields;
272 error_obj.insert("code".to_string(), Value::String(self.code));
273 error_obj.insert("message".to_string(), Value::String(self.message));
274 error_obj.insert("retryable".to_string(), Value::Bool(self.retryable));
275 if let Some(h) = self.hint {
276 error_obj.insert("hint".to_string(), Value::String(h));
277 }
278
279 let trace = self
280 .trace
281 .unwrap_or_else(|| Value::Object(serde_json::Map::new()));
282 let mut obj = serde_json::Map::new();
283 obj.insert("kind".to_string(), Value::String("error".to_string()));
284 obj.insert("error".to_string(), Value::Object(error_obj));
285 obj.insert("trace".to_string(), trace);
286
287 Ok(Event(Value::Object(obj)))
288 }
289}
290
291pub fn json_error(code: &str, message: &str) -> ErrorBuilder {
297 let build_error = if code.is_empty() {
298 Some(BuildError::EmptyRequiredField(
299 "error code must not be empty".to_string(),
300 ))
301 } else if message.is_empty() {
302 Some(BuildError::EmptyRequiredField(
303 "error message must not be empty".to_string(),
304 ))
305 } else {
306 None
307 };
308 ErrorBuilder {
309 code: code.to_string(),
310 message: message.to_string(),
311 retryable: false,
312 hint: None,
313 fields: serde_json::Map::new(),
314 trace: None,
315 build_error,
316 }
317}
318
319pub struct ProgressBuilder {
325 payload: Value,
326 trace: Option<Value>,
327}
328
329impl ProgressBuilder {
330 pub fn trace(mut self, trace: Value) -> Self {
332 self.trace = Some(trace);
333 self
334 }
335
336 pub fn build(self) -> Event {
338 let trace = self
339 .trace
340 .unwrap_or_else(|| Value::Object(serde_json::Map::new()));
341 let mut obj = serde_json::Map::new();
342 obj.insert("kind".to_string(), Value::String("progress".to_string()));
343 obj.insert("progress".to_string(), self.payload);
344 obj.insert("trace".to_string(), trace);
345
346 Event(Value::Object(obj))
347 }
348}
349
350pub fn json_progress(payload: Value) -> ProgressBuilder {
353 ProgressBuilder {
354 payload,
355 trace: None,
356 }
357}
358
359pub struct LogBuilder {
365 payload: Value,
366 trace: Option<Value>,
367}
368
369impl LogBuilder {
370 pub fn trace(mut self, trace: Value) -> Self {
372 self.trace = Some(trace);
373 self
374 }
375
376 pub fn build(self) -> Event {
378 let trace = self
379 .trace
380 .unwrap_or_else(|| Value::Object(serde_json::Map::new()));
381 let mut obj = serde_json::Map::new();
382 obj.insert("kind".to_string(), Value::String("log".to_string()));
383 obj.insert("log".to_string(), self.payload);
384 obj.insert("trace".to_string(), trace);
385
386 Event(Value::Object(obj))
387 }
388}
389
390pub fn json_log(payload: Value) -> LogBuilder {
393 LogBuilder {
394 payload,
395 trace: None,
396 }
397}
398
399pub fn build_cli_error(message: &str, hint: Option<&str>) -> Event {
409 let message = if message.is_empty() {
410 "unspecified error"
411 } else {
412 message
413 };
414 match json_error("cli_error", message).hint_if_some(hint).build() {
415 Ok(event) => event,
416 Err(_) => {
419 let mut error_obj = serde_json::Map::new();
420 error_obj.insert("code".to_string(), Value::String("cli_error".to_string()));
421 error_obj.insert(
422 "message".to_string(),
423 Value::String("unspecified error".to_string()),
424 );
425 error_obj.insert("retryable".to_string(), Value::Bool(false));
426 let mut obj = serde_json::Map::new();
427 obj.insert("kind".to_string(), Value::String("error".to_string()));
428 obj.insert("error".to_string(), Value::Object(error_obj));
429 obj.insert("trace".to_string(), Value::Object(serde_json::Map::new()));
430 Event(Value::Object(obj))
431 }
432 }
433}
434
435#[derive(Clone, Debug, PartialEq, Eq)]
451pub struct ProtocolViolation {
452 pub rule: &'static str,
453 pub pointer: String,
454 pub message: String,
455}
456
457impl std::fmt::Display for ProtocolViolation {
458 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
459 write!(f, "{}", self.message)
460 }
461}
462
463impl std::error::Error for ProtocolViolation {}
464
465fn violation(
466 rule: &'static str,
467 pointer: impl Into<String>,
468 message: impl Into<String>,
469) -> ProtocolViolation {
470 ProtocolViolation {
471 rule,
472 pointer: pointer.into(),
473 message: message.into(),
474 }
475}
476
477pub fn validate_protocol_event(event: &Value, strict: bool) -> Result<(), ProtocolViolation> {
485 validate_protocol_event_base(event)?;
486 if strict {
487 validate_protocol_event_strict_payload(event)?;
488 }
489 Ok(())
490}
491
492fn validate_protocol_event_base(event: &Value) -> Result<(), ProtocolViolation> {
493 let Some(obj) = event.as_object() else {
494 return Err(violation(
495 "event_not_object",
496 "",
497 "event must be a JSON object",
498 ));
499 };
500 let Some(kind) = obj.get("kind").and_then(Value::as_str) else {
501 return Err(violation(
502 "kind_invalid",
503 "/kind",
504 "event.kind must be one of result, error, progress, log",
505 ));
506 };
507 if !matches!(kind, "result" | "error" | "progress" | "log") {
508 return Err(violation(
509 "kind_unsupported",
510 "/kind",
511 format!("unsupported event kind {kind:?}"),
512 ));
513 }
514 if !obj.contains_key(kind) {
515 return Err(violation(
516 "payload_missing",
517 format!("/{kind}"),
518 format!("event payload field {kind:?} is required"),
519 ));
520 }
521 for key in obj.keys() {
522 if key != "kind" && key != kind && key != "trace" {
523 return Err(violation(
524 "unexpected_field",
525 format!("/{key}"),
526 format!("unexpected top-level field {key:?}"),
527 ));
528 }
529 }
530 if let Some(trace) = obj.get("trace")
531 && !trace.is_object()
532 {
533 return Err(violation(
534 "trace_not_object",
535 "/trace",
536 "event.trace must be a JSON object when present",
537 ));
538 }
539 if kind == "error" {
540 validate_error_payload(obj.get("error"))?;
541 }
542 Ok(())
543}
544
545fn validate_error_payload(error: Option<&Value>) -> Result<(), ProtocolViolation> {
546 let Some(error) = error.and_then(Value::as_object) else {
547 return Err(violation(
548 "error_not_object",
549 "/error",
550 "event.error must be a JSON object",
551 ));
552 };
553 match error.get("code").and_then(Value::as_str) {
554 Some(code) if !code.is_empty() => {}
555 _ => {
556 return Err(violation(
557 "error_code_invalid",
558 "/error/code",
559 "event.error.code must be a non-empty string",
560 ));
561 }
562 }
563 match error.get("message").and_then(Value::as_str) {
564 Some(message) if !message.is_empty() => {}
565 _ => {
566 return Err(violation(
567 "error_message_invalid",
568 "/error/message",
569 "event.error.message must be a non-empty string",
570 ));
571 }
572 }
573 if error.get("hint").is_some_and(|hint| !hint.is_string()) {
574 return Err(violation(
575 "error_hint_invalid",
576 "/error/hint",
577 "event.error.hint must be a string when present",
578 ));
579 }
580 if error
583 .get("retryable")
584 .is_some_and(|retryable| !retryable.is_boolean())
585 {
586 return Err(violation(
587 "error_retryable_invalid",
588 "/error/retryable",
589 "event.error.retryable must be a boolean when present",
590 ));
591 }
592 Ok(())
593}
594
595pub fn validate_protocol_stream(
604 events: &[Value],
605 strict: bool,
606) -> Result<(), Vec<ProtocolViolation>> {
607 let mut violations = Vec::new();
608 let mut terminal_seen = false;
609 for (idx, event) in events.iter().enumerate() {
610 if let Err(v) = validate_protocol_event(event, strict) {
611 violations.push(ProtocolViolation {
612 rule: v.rule,
613 pointer: format!("/{idx}{}", v.pointer),
614 message: v.message,
615 });
616 }
617 match event.get("kind").and_then(Value::as_str) {
618 Some("log") | Some("progress") => {
619 if terminal_seen {
620 violations.push(violation(
621 "stream_non_terminal_after_terminal",
622 format!("/{idx}"),
623 "non-terminal event after terminal",
624 ));
625 }
626 }
627 Some("result") | Some("error") => {
628 if terminal_seen {
629 violations.push(violation(
630 "stream_duplicate_terminal",
631 format!("/{idx}"),
632 "duplicate terminal event",
633 ));
634 } else {
635 terminal_seen = true;
636 }
637 }
638 _ => {} }
640 }
641 if !terminal_seen {
642 violations.push(violation(
643 "stream_missing_terminal",
644 String::new(),
645 "event stream must contain exactly one terminal result or error",
646 ));
647 }
648 if violations.is_empty() {
649 Ok(())
650 } else {
651 Err(violations)
652 }
653}
654
655fn validate_protocol_event_strict_payload(event: &Value) -> Result<(), ProtocolViolation> {
659 if !event.get("trace").is_some_and(Value::is_object) {
660 return Err(violation(
661 "trace_required",
662 "/trace",
663 "event.trace is required by the strict profile",
664 ));
665 }
666 match event.get("kind").and_then(Value::as_str) {
667 Some("error") => validate_strict_error_payload(event.get("error")),
668 _ => Ok(()),
669 }
670}
671
672fn validate_strict_error_payload(error: Option<&Value>) -> Result<(), ProtocolViolation> {
673 let Some(error) = error.and_then(Value::as_object) else {
674 return Err(violation(
675 "error_not_object",
676 "/error",
677 "event.error must be a JSON object in the strict profile",
678 ));
679 };
680 require_non_empty_string(
681 error,
682 "code",
683 "error_code_invalid",
684 "/error/code",
685 "event.error",
686 )?;
687 require_non_empty_string(
688 error,
689 "message",
690 "error_message_invalid",
691 "/error/message",
692 "event.error",
693 )?;
694 if error.get("retryable").and_then(Value::as_bool).is_none() {
695 return Err(violation(
696 "error_retryable_invalid",
697 "/error/retryable",
698 "event.error.retryable must be a boolean in the strict profile",
699 ));
700 }
701 if error.contains_key("hint") && !error.get("hint").is_some_and(Value::is_string) {
702 return Err(violation(
703 "error_hint_invalid",
704 "/error/hint",
705 "event.error.hint must be a string when present",
706 ));
707 }
708 Ok(())
709}
710
711fn require_non_empty_string(
712 payload: &serde_json::Map<String, Value>,
713 field: &str,
714 rule: &'static str,
715 pointer: &'static str,
716 path: &str,
717) -> Result<(), ProtocolViolation> {
718 if payload
719 .get(field)
720 .and_then(Value::as_str)
721 .is_some_and(|value| !value.is_empty())
722 {
723 return Ok(());
724 }
725 Err(violation(
726 rule,
727 pointer,
728 format!("{path}.{field} must be a non-empty string in the strict profile"),
729 ))
730}
731
732#[derive(Clone, Debug, PartialEq)]
738pub enum DecodedEvent {
739 Result(DecodedResult),
740 Error(DecodedError),
741 Progress(DecodedProgress),
742 Log(DecodedLog),
743}
744
745#[derive(Clone, Debug, PartialEq)]
747pub struct DecodedResult {
748 pub result: Value,
750 pub trace: Option<Value>,
752}
753
754#[derive(Clone, Debug, PartialEq)]
756pub struct DecodedError {
757 pub code: String,
758 pub message: String,
759 pub retryable: Option<bool>,
760 pub hint: Option<String>,
761 pub fields: serde_json::Map<String, Value>,
763 pub trace: Option<Value>,
765}
766
767#[derive(Clone, Debug, PartialEq)]
769pub struct DecodedProgress {
770 pub progress: Value,
772 pub trace: Option<Value>,
774}
775
776#[derive(Clone, Debug, PartialEq)]
778pub struct DecodedLog {
779 pub log: Value,
781 pub trace: Option<Value>,
783}
784
785#[derive(Clone, Debug, PartialEq, Eq)]
787pub enum EventDecodeError {
788 InvalidJson(String),
790 InvalidEvent(String),
792}
793
794impl fmt::Display for EventDecodeError {
795 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
796 match self {
797 Self::InvalidJson(err) => write!(f, "invalid JSON: {err}"),
798 Self::InvalidEvent(err) => write!(f, "invalid protocol event: {err}"),
799 }
800 }
801}
802
803impl std::error::Error for EventDecodeError {}
804
805pub fn decode_protocol_event(text: &str) -> Result<DecodedEvent, EventDecodeError> {
815 let value: Value =
816 serde_json::from_str(text).map_err(|err| EventDecodeError::InvalidJson(err.to_string()))?;
817 validate_protocol_event(&value, false)
818 .map_err(|v| EventDecodeError::InvalidEvent(v.to_string()))?;
819
820 let malformed = || {
823 EventDecodeError::InvalidEvent(
824 "event passed base validation but has an unexpected shape".to_string(),
825 )
826 };
827 let obj = value.as_object().ok_or_else(malformed)?;
828 let trace = obj.get("trace").cloned();
829 match obj.get("kind").and_then(Value::as_str) {
830 Some("result") => Ok(DecodedEvent::Result(DecodedResult {
831 result: obj.get("result").cloned().unwrap_or(Value::Null),
832 trace,
833 })),
834 Some("error") => {
835 let mut fields = obj
836 .get("error")
837 .and_then(Value::as_object)
838 .ok_or_else(malformed)?
839 .clone();
840 let code = fields
841 .remove("code")
842 .and_then(|v| v.as_str().map(str::to_string))
843 .ok_or_else(malformed)?;
844 let message = fields
845 .remove("message")
846 .and_then(|v| v.as_str().map(str::to_string))
847 .ok_or_else(malformed)?;
848 let retryable = fields.remove("retryable").and_then(|v| v.as_bool());
849 let hint = fields
850 .remove("hint")
851 .and_then(|v| v.as_str().map(str::to_string));
852 Ok(DecodedEvent::Error(DecodedError {
853 code,
854 message,
855 retryable,
856 hint,
857 fields,
858 trace,
859 }))
860 }
861 Some("progress") => Ok(DecodedEvent::Progress(DecodedProgress {
862 progress: obj.get("progress").cloned().unwrap_or(Value::Null),
863 trace,
864 })),
865 Some("log") => Ok(DecodedEvent::Log(DecodedLog {
866 log: obj.get("log").cloned().unwrap_or(Value::Null),
867 trace,
868 })),
869 _ => Err(malformed()),
870 }
871}