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 pub(crate) fn as_str(&self) -> &'static str {
77 match self {
78 Self::Debug => "debug",
79 Self::Info => "info",
80 Self::Warn => "warn",
81 Self::Error => "error",
82 }
83 }
84}
85
86pub struct ResultBuilder {
92 payload: Value,
93 trace: Option<Value>,
94}
95
96impl ResultBuilder {
97 pub fn trace(mut self, trace: Value) -> Self {
99 self.trace = Some(trace);
100 self
101 }
102
103 pub fn build(self) -> Event {
105 let trace = self
106 .trace
107 .unwrap_or_else(|| Value::Object(serde_json::Map::new()));
108 let mut obj = serde_json::Map::new();
109 obj.insert("kind".to_string(), Value::String("result".to_string()));
110 obj.insert("result".to_string(), self.payload);
111 obj.insert("trace".to_string(), trace);
112 Event(Value::Object(obj))
113 }
114}
115
116pub fn json_result(payload: Value) -> ResultBuilder {
118 ResultBuilder {
119 payload,
120 trace: None,
121 }
122}
123
124pub struct ErrorBuilder {
130 code: String,
131 message: String,
132 retryable: bool,
133 hint: Option<String>,
134 fields: serde_json::Map<String, Value>,
135 trace: Option<Value>,
136 build_error: Option<BuildError>,
137}
138
139impl ErrorBuilder {
140 pub fn retryable(mut self) -> Self {
142 self.retryable = true;
143 self
144 }
145
146 pub fn retryable_if(mut self, should_retry: bool) -> Self {
148 self.retryable = should_retry;
149 self
150 }
151
152 pub fn hint(mut self, hint: &str) -> Self {
154 self.hint = Some(hint.to_string());
155 self
156 }
157
158 pub fn hint_if_some(mut self, hint: Option<&str>) -> Self {
160 if let Some(h) = hint {
161 self.hint = Some(h.to_string());
162 }
163 self
164 }
165
166 pub fn field(mut self, name: &str, value: Value) -> Self {
168 if self.build_error.is_none() {
169 match name {
170 "code" | "message" | "hint" | "retryable" => {
171 self.build_error = Some(BuildError::ReservedField(format!(
172 "cannot write reserved field {name:?} to error payload"
173 )));
174 }
175 _ => {
176 self.fields.insert(name.to_string(), value);
177 }
178 }
179 }
180 self
181 }
182
183 pub fn fields(mut self, fields: Value) -> Self {
185 if self.build_error.is_none() {
186 match fields {
187 Value::Object(map) => {
188 for (k, v) in map {
189 match k.as_str() {
190 "code" | "message" | "hint" | "retryable" => {
191 self.build_error = Some(BuildError::ReservedField(format!(
192 "cannot write reserved field {k:?} to error payload"
193 )));
194 return self;
195 }
196 _ => {
197 self.fields.insert(k, v);
198 }
199 }
200 }
201 }
202 _ => {
203 self.build_error = Some(BuildError::NonObjectField(
204 "fields() argument must be a JSON object".to_string(),
205 ));
206 }
207 }
208 }
209 self
210 }
211
212 pub fn extend<T: Serialize>(mut self, value: T) -> Self {
214 if self.build_error.is_none() {
215 match serde_json::to_value(&value) {
216 Ok(Value::Object(map)) => {
217 for (k, v) in map {
218 match k.as_str() {
219 "code" | "message" | "hint" | "retryable" => {
220 self.build_error = Some(BuildError::ReservedField(format!(
221 "cannot write reserved field {k:?} to error payload"
222 )));
223 return self;
224 }
225 _ => {
226 self.fields.insert(k, v);
227 }
228 }
229 }
230 }
231 Ok(_) => {
232 self.build_error = Some(BuildError::NonObjectField(
233 "extend() argument must serialize to a JSON object".to_string(),
234 ));
235 }
236 Err(_) => {
237 self.build_error = Some(BuildError::NonObjectField(
238 "extend() argument serialization failed".to_string(),
239 ));
240 }
241 }
242 }
243 self
244 }
245
246 pub fn trace(mut self, trace: Value) -> Self {
248 self.trace = Some(trace);
249 self
250 }
251
252 pub fn build(self) -> Result<Event, BuildError> {
254 if let Some(err) = self.build_error {
255 return Err(err);
256 }
257
258 let mut error_obj = self.fields;
259 error_obj.insert("code".to_string(), Value::String(self.code));
260 error_obj.insert("message".to_string(), Value::String(self.message));
261 error_obj.insert("retryable".to_string(), Value::Bool(self.retryable));
262 if let Some(h) = self.hint {
263 error_obj.insert("hint".to_string(), Value::String(h));
264 }
265
266 let trace = self
267 .trace
268 .unwrap_or_else(|| Value::Object(serde_json::Map::new()));
269 let mut obj = serde_json::Map::new();
270 obj.insert("kind".to_string(), Value::String("error".to_string()));
271 obj.insert("error".to_string(), Value::Object(error_obj));
272 obj.insert("trace".to_string(), trace);
273
274 Ok(Event(Value::Object(obj)))
275 }
276}
277
278pub fn json_error(code: &str, message: &str) -> ErrorBuilder {
284 let build_error = if code.is_empty() {
285 Some(BuildError::EmptyRequiredField(
286 "error code must not be empty".to_string(),
287 ))
288 } else if message.is_empty() {
289 Some(BuildError::EmptyRequiredField(
290 "error message must not be empty".to_string(),
291 ))
292 } else {
293 None
294 };
295 ErrorBuilder {
296 code: code.to_string(),
297 message: message.to_string(),
298 retryable: false,
299 hint: None,
300 fields: serde_json::Map::new(),
301 trace: None,
302 build_error,
303 }
304}
305
306pub struct ProgressBuilder {
312 payload: Value,
313 trace: Option<Value>,
314}
315
316impl ProgressBuilder {
317 pub fn trace(mut self, trace: Value) -> Self {
319 self.trace = Some(trace);
320 self
321 }
322
323 pub fn build(self) -> Event {
325 let trace = self
326 .trace
327 .unwrap_or_else(|| Value::Object(serde_json::Map::new()));
328 let mut obj = serde_json::Map::new();
329 obj.insert("kind".to_string(), Value::String("progress".to_string()));
330 obj.insert("progress".to_string(), self.payload);
331 obj.insert("trace".to_string(), trace);
332
333 Event(Value::Object(obj))
334 }
335}
336
337pub fn json_progress(payload: Value) -> ProgressBuilder {
340 ProgressBuilder {
341 payload,
342 trace: None,
343 }
344}
345
346pub struct LogBuilder {
352 payload: Value,
353 trace: Option<Value>,
354}
355
356impl LogBuilder {
357 pub fn trace(mut self, trace: Value) -> Self {
359 self.trace = Some(trace);
360 self
361 }
362
363 pub fn build(self) -> Event {
365 let trace = self
366 .trace
367 .unwrap_or_else(|| Value::Object(serde_json::Map::new()));
368 let mut obj = serde_json::Map::new();
369 obj.insert("kind".to_string(), Value::String("log".to_string()));
370 obj.insert("log".to_string(), self.payload);
371 obj.insert("trace".to_string(), trace);
372
373 Event(Value::Object(obj))
374 }
375}
376
377pub fn json_log(payload: Value) -> LogBuilder {
380 LogBuilder {
381 payload,
382 trace: None,
383 }
384}
385
386pub fn build_cli_error(message: &str, hint: Option<&str>) -> Event {
396 let message = if message.is_empty() {
397 "unspecified error"
398 } else {
399 message
400 };
401 match json_error("cli_error", message).hint_if_some(hint).build() {
402 Ok(event) => event,
403 Err(_) => {
406 let mut error_obj = serde_json::Map::new();
407 error_obj.insert("code".to_string(), Value::String("cli_error".to_string()));
408 error_obj.insert(
409 "message".to_string(),
410 Value::String("unspecified error".to_string()),
411 );
412 error_obj.insert("retryable".to_string(), Value::Bool(false));
413 let mut obj = serde_json::Map::new();
414 obj.insert("kind".to_string(), Value::String("error".to_string()));
415 obj.insert("error".to_string(), Value::Object(error_obj));
416 obj.insert("trace".to_string(), Value::Object(serde_json::Map::new()));
417 Event(Value::Object(obj))
418 }
419 }
420}
421
422#[derive(Clone, Debug, PartialEq, Eq)]
438pub struct ProtocolViolation {
439 pub rule: &'static str,
440 pub pointer: String,
441 pub message: String,
442}
443
444impl std::fmt::Display for ProtocolViolation {
445 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
446 write!(f, "{}", self.message)
447 }
448}
449
450impl std::error::Error for ProtocolViolation {}
451
452fn violation(
453 rule: &'static str,
454 pointer: impl Into<String>,
455 message: impl Into<String>,
456) -> ProtocolViolation {
457 ProtocolViolation {
458 rule,
459 pointer: pointer.into(),
460 message: message.into(),
461 }
462}
463
464pub fn validate_protocol_event(event: &Value, strict: bool) -> Result<(), ProtocolViolation> {
472 validate_protocol_event_base(event)?;
473 if strict {
474 validate_protocol_event_strict_payload(event)?;
475 }
476 Ok(())
477}
478
479fn validate_protocol_event_base(event: &Value) -> Result<(), ProtocolViolation> {
480 let Some(obj) = event.as_object() else {
481 return Err(violation(
482 "event_not_object",
483 "",
484 "event must be a JSON object",
485 ));
486 };
487 let Some(kind) = obj.get("kind").and_then(Value::as_str) else {
488 return Err(violation(
489 "kind_invalid",
490 "/kind",
491 "event.kind must be one of result, error, progress, log",
492 ));
493 };
494 if !matches!(kind, "result" | "error" | "progress" | "log") {
495 return Err(violation(
496 "kind_unsupported",
497 "/kind",
498 format!("unsupported event kind {kind:?}"),
499 ));
500 }
501 if !obj.contains_key(kind) {
502 return Err(violation(
503 "payload_missing",
504 format!("/{kind}"),
505 format!("event payload field {kind:?} is required"),
506 ));
507 }
508 for key in obj.keys() {
509 if key != "kind" && key != kind && key != "trace" {
510 return Err(violation(
511 "unexpected_field",
512 format!("/{key}"),
513 format!("unexpected top-level field {key:?}"),
514 ));
515 }
516 }
517 if let Some(trace) = obj.get("trace")
518 && !trace.is_object()
519 {
520 return Err(violation(
521 "trace_not_object",
522 "/trace",
523 "event.trace must be a JSON object when present",
524 ));
525 }
526 if kind == "error" {
527 validate_error_payload(obj.get("error"))?;
528 }
529 Ok(())
530}
531
532fn validate_error_payload(error: Option<&Value>) -> Result<(), ProtocolViolation> {
533 let Some(error) = error.and_then(Value::as_object) else {
534 return Err(violation(
535 "error_not_object",
536 "/error",
537 "event.error must be a JSON object",
538 ));
539 };
540 match error.get("code").and_then(Value::as_str) {
541 Some(code) if !code.is_empty() => {}
542 _ => {
543 return Err(violation(
544 "error_code_invalid",
545 "/error/code",
546 "event.error.code must be a non-empty string",
547 ));
548 }
549 }
550 match error.get("message").and_then(Value::as_str) {
551 Some(message) if !message.is_empty() => {}
552 _ => {
553 return Err(violation(
554 "error_message_invalid",
555 "/error/message",
556 "event.error.message must be a non-empty string",
557 ));
558 }
559 }
560 if error.get("hint").is_some_and(|hint| !hint.is_string()) {
561 return Err(violation(
562 "error_hint_invalid",
563 "/error/hint",
564 "event.error.hint must be a string when present",
565 ));
566 }
567 Ok(())
568}
569
570pub fn validate_protocol_stream(
579 events: &[Value],
580 strict: bool,
581) -> Result<(), Vec<ProtocolViolation>> {
582 let mut violations = Vec::new();
583 let mut terminal_seen = false;
584 for (idx, event) in events.iter().enumerate() {
585 if let Err(v) = validate_protocol_event(event, strict) {
586 violations.push(ProtocolViolation {
587 rule: v.rule,
588 pointer: format!("/{idx}{}", v.pointer),
589 message: v.message,
590 });
591 }
592 match event.get("kind").and_then(Value::as_str) {
593 Some("log") | Some("progress") => {
594 if terminal_seen {
595 violations.push(violation(
596 "stream_non_terminal_after_terminal",
597 format!("/{idx}"),
598 "non-terminal event after terminal",
599 ));
600 }
601 }
602 Some("result") | Some("error") => {
603 if terminal_seen {
604 violations.push(violation(
605 "stream_duplicate_terminal",
606 format!("/{idx}"),
607 "duplicate terminal event",
608 ));
609 } else {
610 terminal_seen = true;
611 }
612 }
613 _ => {} }
615 }
616 if !terminal_seen {
617 violations.push(violation(
618 "stream_missing_terminal",
619 String::new(),
620 "event stream must contain exactly one terminal result or error",
621 ));
622 }
623 if violations.is_empty() {
624 Ok(())
625 } else {
626 Err(violations)
627 }
628}
629
630fn validate_protocol_event_strict_payload(event: &Value) -> Result<(), ProtocolViolation> {
634 if !event.get("trace").is_some_and(Value::is_object) {
635 return Err(violation(
636 "trace_required",
637 "/trace",
638 "event.trace is required by the strict profile",
639 ));
640 }
641 match event.get("kind").and_then(Value::as_str) {
642 Some("error") => validate_strict_error_payload(event.get("error")),
643 _ => Ok(()),
644 }
645}
646
647fn validate_strict_error_payload(error: Option<&Value>) -> Result<(), ProtocolViolation> {
648 let Some(error) = error.and_then(Value::as_object) else {
649 return Err(violation(
650 "error_not_object",
651 "/error",
652 "event.error must be a JSON object in the strict profile",
653 ));
654 };
655 require_non_empty_string(
656 error,
657 "code",
658 "error_code_invalid",
659 "/error/code",
660 "event.error",
661 )?;
662 require_non_empty_string(
663 error,
664 "message",
665 "error_message_invalid",
666 "/error/message",
667 "event.error",
668 )?;
669 if error.get("retryable").and_then(Value::as_bool).is_none() {
670 return Err(violation(
671 "error_retryable_invalid",
672 "/error/retryable",
673 "event.error.retryable must be a boolean in the strict profile",
674 ));
675 }
676 if error.contains_key("hint") && !error.get("hint").is_some_and(Value::is_string) {
677 return Err(violation(
678 "error_hint_invalid",
679 "/error/hint",
680 "event.error.hint must be a string when present",
681 ));
682 }
683 Ok(())
684}
685
686fn require_non_empty_string(
687 payload: &serde_json::Map<String, Value>,
688 field: &str,
689 rule: &'static str,
690 pointer: &'static str,
691 path: &str,
692) -> Result<(), ProtocolViolation> {
693 if payload
694 .get(field)
695 .and_then(Value::as_str)
696 .is_some_and(|value| !value.is_empty())
697 {
698 return Ok(());
699 }
700 Err(violation(
701 rule,
702 pointer,
703 format!("{path}.{field} must be a non-empty string in the strict profile"),
704 ))
705}
706
707#[derive(Clone, Debug, PartialEq)]
713pub enum DecodedEvent {
714 Result(DecodedResult),
715 Error(DecodedError),
716 Progress(DecodedProgress),
717 Log(DecodedLog),
718}
719
720#[derive(Clone, Debug, PartialEq)]
722pub struct DecodedResult {
723 pub result: Value,
725 pub trace: Option<Value>,
727}
728
729#[derive(Clone, Debug, PartialEq)]
731pub struct DecodedError {
732 pub code: String,
733 pub message: String,
734 pub retryable: bool,
735 pub hint: Option<String>,
736 pub fields: serde_json::Map<String, Value>,
738 pub trace: Option<Value>,
740}
741
742#[derive(Clone, Debug, PartialEq)]
744pub struct DecodedProgress {
745 pub progress: Value,
747 pub trace: Option<Value>,
749}
750
751#[derive(Clone, Debug, PartialEq)]
753pub struct DecodedLog {
754 pub log: Value,
756 pub trace: Option<Value>,
758}
759
760#[derive(Clone, Debug, PartialEq, Eq)]
762pub enum EventDecodeError {
763 InvalidJson(String),
765 InvalidEvent(String),
767}
768
769impl fmt::Display for EventDecodeError {
770 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
771 match self {
772 Self::InvalidJson(err) => write!(f, "invalid JSON: {err}"),
773 Self::InvalidEvent(err) => write!(f, "invalid protocol event: {err}"),
774 }
775 }
776}
777
778impl std::error::Error for EventDecodeError {}
779
780pub fn decode_protocol_event(text: &str) -> Result<DecodedEvent, EventDecodeError> {
784 let value: Value =
785 serde_json::from_str(text).map_err(|err| EventDecodeError::InvalidJson(err.to_string()))?;
786 validate_protocol_event(&value, true)
787 .map_err(|v| EventDecodeError::InvalidEvent(v.to_string()))?;
788
789 let malformed = || {
792 EventDecodeError::InvalidEvent(
793 "event passed strict validation but has an unexpected shape".to_string(),
794 )
795 };
796 let obj = value.as_object().ok_or_else(malformed)?;
797 let trace = obj.get("trace").cloned();
798 match obj.get("kind").and_then(Value::as_str) {
799 Some("result") => Ok(DecodedEvent::Result(DecodedResult {
800 result: obj.get("result").cloned().unwrap_or(Value::Null),
801 trace,
802 })),
803 Some("error") => {
804 let mut fields = obj
805 .get("error")
806 .and_then(Value::as_object)
807 .ok_or_else(malformed)?
808 .clone();
809 let code = fields
810 .remove("code")
811 .and_then(|v| v.as_str().map(str::to_string))
812 .ok_or_else(malformed)?;
813 let message = fields
814 .remove("message")
815 .and_then(|v| v.as_str().map(str::to_string))
816 .ok_or_else(malformed)?;
817 let retryable = fields
818 .remove("retryable")
819 .and_then(|v| v.as_bool())
820 .ok_or_else(malformed)?;
821 let hint = fields
822 .remove("hint")
823 .and_then(|v| v.as_str().map(str::to_string));
824 Ok(DecodedEvent::Error(DecodedError {
825 code,
826 message,
827 retryable,
828 hint,
829 fields,
830 trace,
831 }))
832 }
833 Some("progress") => Ok(DecodedEvent::Progress(DecodedProgress {
834 progress: obj.get("progress").cloned().unwrap_or(Value::Null),
835 trace,
836 })),
837 Some("log") => Ok(DecodedEvent::Log(DecodedLog {
838 log: obj.get("log").cloned().unwrap_or(Value::Null),
839 trace,
840 })),
841 _ => Err(malformed()),
842 }
843}