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