use serde::Serialize;
use serde_json::Value;
use std::fmt;
use std::ops::Deref;
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
pub struct Event(Value);
impl Event {
pub fn as_value(&self) -> &Value {
&self.0
}
pub fn into_value(self) -> Value {
self.0
}
}
impl From<Event> for Value {
fn from(event: Event) -> Self {
event.0
}
}
impl Deref for Event {
type Target = Value;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl fmt::Display for Event {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum BuildError {
ReservedField(String),
NonObjectField(String),
EmptyRequiredField(String),
}
impl fmt::Display for BuildError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::ReservedField(msg) => write!(f, "reserved field: {msg}"),
Self::NonObjectField(msg) => write!(f, "non-object field: {msg}"),
Self::EmptyRequiredField(msg) => write!(f, "empty required field: {msg}"),
}
}
}
impl std::error::Error for BuildError {}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum LogLevel {
Debug,
Info,
Warn,
Error,
}
impl LogLevel {
pub(crate) fn as_str(&self) -> &'static str {
match self {
Self::Debug => "debug",
Self::Info => "info",
Self::Warn => "warn",
Self::Error => "error",
}
}
}
pub struct ResultBuilder {
payload: Value,
trace: Option<Value>,
}
impl ResultBuilder {
pub fn trace(mut self, trace: Value) -> Self {
self.trace = Some(trace);
self
}
pub fn build(self) -> Result<Event, BuildError> {
let trace = self
.trace
.unwrap_or_else(|| Value::Object(serde_json::Map::new()));
let mut obj = serde_json::Map::new();
obj.insert("kind".to_string(), Value::String("result".to_string()));
obj.insert("result".to_string(), self.payload);
obj.insert("trace".to_string(), trace);
Ok(Event(Value::Object(obj)))
}
}
pub fn json_result(payload: Value) -> ResultBuilder {
ResultBuilder {
payload,
trace: None,
}
}
pub struct ErrorBuilder {
code: String,
message: String,
retryable: bool,
hint: Option<String>,
fields: serde_json::Map<String, Value>,
trace: Option<Value>,
build_error: Option<BuildError>,
}
impl ErrorBuilder {
pub fn retryable(mut self) -> Self {
self.retryable = true;
self
}
pub fn retryable_if(mut self, should_retry: bool) -> Self {
self.retryable = should_retry;
self
}
pub fn hint(mut self, hint: &str) -> Self {
self.hint = Some(hint.to_string());
self
}
pub fn hint_if_some(mut self, hint: Option<&str>) -> Self {
if let Some(h) = hint {
self.hint = Some(h.to_string());
}
self
}
pub fn field(mut self, name: &str, value: Value) -> Self {
if self.build_error.is_none() {
match name {
"code" | "message" | "hint" | "retryable" => {
self.build_error = Some(BuildError::ReservedField(format!(
"cannot write reserved field {name:?} to error payload"
)));
}
_ => {
self.fields.insert(name.to_string(), value);
}
}
}
self
}
pub fn fields(mut self, fields: Value) -> Self {
if self.build_error.is_none() {
match fields {
Value::Object(map) => {
for (k, v) in map {
match k.as_str() {
"code" | "message" | "hint" | "retryable" => {
self.build_error = Some(BuildError::ReservedField(format!(
"cannot write reserved field {k:?} to error payload"
)));
return self;
}
_ => {
self.fields.insert(k, v);
}
}
}
}
_ => {
self.build_error = Some(BuildError::NonObjectField(
"fields() argument must be a JSON object".to_string(),
));
}
}
}
self
}
pub fn extend<T: Serialize>(mut self, value: T) -> Self {
if self.build_error.is_none() {
match serde_json::to_value(&value) {
Ok(Value::Object(map)) => {
for (k, v) in map {
match k.as_str() {
"code" | "message" | "hint" | "retryable" => {
self.build_error = Some(BuildError::ReservedField(format!(
"cannot write reserved field {k:?} to error payload"
)));
return self;
}
_ => {
self.fields.insert(k, v);
}
}
}
}
Ok(_) => {
self.build_error = Some(BuildError::NonObjectField(
"extend() argument must serialize to a JSON object".to_string(),
));
}
Err(_) => {
self.build_error = Some(BuildError::NonObjectField(
"extend() argument serialization failed".to_string(),
));
}
}
}
self
}
pub fn trace(mut self, trace: Value) -> Self {
self.trace = Some(trace);
self
}
pub fn build(self) -> Result<Event, BuildError> {
if let Some(err) = self.build_error {
return Err(err);
}
let mut error_obj = self.fields;
error_obj.insert("code".to_string(), Value::String(self.code));
error_obj.insert("message".to_string(), Value::String(self.message));
error_obj.insert("retryable".to_string(), Value::Bool(self.retryable));
if let Some(h) = self.hint {
error_obj.insert("hint".to_string(), Value::String(h));
}
let trace = self
.trace
.unwrap_or_else(|| Value::Object(serde_json::Map::new()));
let mut obj = serde_json::Map::new();
obj.insert("kind".to_string(), Value::String("error".to_string()));
obj.insert("error".to_string(), Value::Object(error_obj));
obj.insert("trace".to_string(), trace);
Ok(Event(Value::Object(obj)))
}
}
#[allow(clippy::panic)]
pub fn json_error(code: &str, message: &str) -> ErrorBuilder {
if code.is_empty() {
panic!("json_error: code must not be empty");
}
if message.is_empty() {
panic!("json_error: message must not be empty");
}
ErrorBuilder {
code: code.to_string(),
message: message.to_string(),
retryable: false,
hint: None,
fields: serde_json::Map::new(),
trace: None,
build_error: None,
}
}
pub struct ProgressBuilder {
payload: Value,
trace: Option<Value>,
}
impl ProgressBuilder {
pub fn trace(mut self, trace: Value) -> Self {
self.trace = Some(trace);
self
}
pub fn build(self) -> Result<Event, BuildError> {
let trace = self
.trace
.unwrap_or_else(|| Value::Object(serde_json::Map::new()));
let mut obj = serde_json::Map::new();
obj.insert("kind".to_string(), Value::String("progress".to_string()));
obj.insert("progress".to_string(), self.payload);
obj.insert("trace".to_string(), trace);
Ok(Event(Value::Object(obj)))
}
}
pub fn json_progress(payload: Value) -> ProgressBuilder {
ProgressBuilder {
payload,
trace: None,
}
}
pub struct LogBuilder {
payload: Value,
trace: Option<Value>,
}
impl LogBuilder {
pub fn trace(mut self, trace: Value) -> Self {
self.trace = Some(trace);
self
}
pub fn build(self) -> Result<Event, BuildError> {
let trace = self
.trace
.unwrap_or_else(|| Value::Object(serde_json::Map::new()));
let mut obj = serde_json::Map::new();
obj.insert("kind".to_string(), Value::String("log".to_string()));
obj.insert("log".to_string(), self.payload);
obj.insert("trace".to_string(), trace);
Ok(Event(Value::Object(obj)))
}
}
pub fn json_log(payload: Value) -> LogBuilder {
LogBuilder {
payload,
trace: None,
}
}
#[allow(clippy::panic, clippy::expect_used)]
pub fn build_cli_error(message: &str, hint: Option<&str>) -> Event {
if message.is_empty() {
panic!("build_cli_error: message must not be empty");
}
json_error("cli_error", message)
.hint_if_some(hint)
.build()
.expect("build_cli_error: builder returned error unexpectedly")
}
pub fn validate_protocol_event(event: &Value, strict: bool) -> Result<(), String> {
validate_protocol_event_base(event)?;
if strict {
validate_protocol_event_strict_payload(event)?;
}
Ok(())
}
fn validate_protocol_event_base(event: &Value) -> Result<(), String> {
let Some(obj) = event.as_object() else {
return Err("event must be a JSON object".to_string());
};
let Some(kind) = obj.get("kind").and_then(Value::as_str) else {
return Err("event.kind must be one of result, error, progress, log".to_string());
};
if !matches!(kind, "result" | "error" | "progress" | "log") {
return Err(format!("unsupported event kind {kind:?}"));
}
if !obj.contains_key(kind) {
return Err(format!("event payload field {kind:?} is required"));
}
for key in obj.keys() {
if key != "kind" && key != kind && key != "trace" {
return Err(format!("unexpected top-level field {key:?}"));
}
}
if let Some(trace) = obj.get("trace")
&& !trace.is_object()
{
return Err("event.trace must be a JSON object when present".to_string());
}
if kind == "error" {
validate_error_payload(obj.get("error"))?;
}
Ok(())
}
fn validate_error_payload(error: Option<&Value>) -> Result<(), String> {
let Some(error) = error.and_then(Value::as_object) else {
return Err("event.error must be a JSON object".to_string());
};
match error.get("code").and_then(Value::as_str) {
Some(code) if !code.is_empty() => {}
_ => return Err("event.error.code must be a non-empty string".to_string()),
}
match error.get("message").and_then(Value::as_str) {
Some(message) if !message.is_empty() => {}
_ => return Err("event.error.message must be a non-empty string".to_string()),
}
if error.get("hint").is_some_and(|hint| !hint.is_string()) {
return Err("event.error.hint must be a string when present".to_string());
}
Ok(())
}
pub fn validate_protocol_stream(events: &[Value], strict: bool) -> Result<(), String> {
let mut terminal_kind: Option<&str> = None;
for (idx, event) in events.iter().enumerate() {
validate_protocol_event(event, strict).map_err(|err| format!("event {idx}: {err}"))?;
let Some(kind) = event.get("kind").and_then(Value::as_str) else {
return Err(format!("event {idx}: missing kind"));
};
match kind {
"log" | "progress" => {
if terminal_kind.is_some() {
return Err(format!("event {idx}: non-terminal event after terminal"));
}
}
"result" | "error" => {
if terminal_kind.is_some() {
return Err(format!("event {idx}: duplicate terminal event"));
}
terminal_kind = Some(kind);
}
_ => return Err(format!("event {idx}: unsupported event kind {kind:?}")),
}
}
if terminal_kind.is_none() {
return Err("event stream must contain exactly one terminal result or error".to_string());
}
Ok(())
}
fn validate_protocol_event_strict_payload(event: &Value) -> Result<(), String> {
if !event.get("trace").is_some_and(Value::is_object) {
return Err("event.trace is required by the strict profile".to_string());
}
match event.get("kind").and_then(Value::as_str) {
Some("error") => validate_strict_error_payload(event.get("error")),
_ => Ok(()),
}
}
fn validate_strict_error_payload(error: Option<&Value>) -> Result<(), String> {
let Some(error) = error.and_then(Value::as_object) else {
return Err("event.error must be a JSON object in the strict profile".to_string());
};
require_non_empty_string(error, "code", "event.error")?;
require_non_empty_string(error, "message", "event.error")?;
if error.get("retryable").and_then(Value::as_bool).is_none() {
return Err("event.error.retryable must be a boolean in the strict profile".to_string());
}
if error.contains_key("hint") && !error.get("hint").is_some_and(Value::is_string) {
return Err("event.error.hint must be a string when present".to_string());
}
Ok(())
}
fn require_non_empty_string(
payload: &serde_json::Map<String, Value>,
field: &str,
path: &str,
) -> Result<(), String> {
if payload
.get(field)
.and_then(Value::as_str)
.is_some_and(|value| !value.is_empty())
{
return Ok(());
}
Err(format!(
"{path}.{field} must be a non-empty string in the strict profile"
))
}
#[derive(Clone, Debug, PartialEq)]
pub enum DecodedEvent {
Result(DecodedResult),
Error(DecodedError),
Progress(DecodedProgress),
Log(DecodedLog),
}
#[derive(Clone, Debug, PartialEq)]
pub struct DecodedResult {
pub result: Value,
pub trace: Option<Value>,
}
#[derive(Clone, Debug, PartialEq)]
pub struct DecodedError {
pub code: String,
pub message: String,
pub retryable: bool,
pub hint: Option<String>,
pub fields: serde_json::Map<String, Value>,
pub trace: Option<Value>,
}
#[derive(Clone, Debug, PartialEq)]
pub struct DecodedProgress {
pub progress: Value,
pub trace: Option<Value>,
}
#[derive(Clone, Debug, PartialEq)]
pub struct DecodedLog {
pub log: Value,
pub trace: Option<Value>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum EventDecodeError {
InvalidJson(String),
InvalidEvent(String),
}
impl fmt::Display for EventDecodeError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::InvalidJson(err) => write!(f, "invalid JSON: {err}"),
Self::InvalidEvent(err) => write!(f, "invalid protocol event: {err}"),
}
}
}
impl std::error::Error for EventDecodeError {}
pub fn decode_protocol_event(text: &str) -> Result<DecodedEvent, EventDecodeError> {
let value: Value =
serde_json::from_str(text).map_err(|err| EventDecodeError::InvalidJson(err.to_string()))?;
validate_protocol_event(&value, true).map_err(EventDecodeError::InvalidEvent)?;
let malformed = || {
EventDecodeError::InvalidEvent(
"event passed strict validation but has an unexpected shape".to_string(),
)
};
let obj = value.as_object().ok_or_else(malformed)?;
let trace = obj.get("trace").cloned();
match obj.get("kind").and_then(Value::as_str) {
Some("result") => Ok(DecodedEvent::Result(DecodedResult {
result: obj.get("result").cloned().unwrap_or(Value::Null),
trace,
})),
Some("error") => {
let mut fields = obj
.get("error")
.and_then(Value::as_object)
.ok_or_else(malformed)?
.clone();
let code = fields
.remove("code")
.and_then(|v| v.as_str().map(str::to_string))
.ok_or_else(malformed)?;
let message = fields
.remove("message")
.and_then(|v| v.as_str().map(str::to_string))
.ok_or_else(malformed)?;
let retryable = fields
.remove("retryable")
.and_then(|v| v.as_bool())
.ok_or_else(malformed)?;
let hint = fields
.remove("hint")
.and_then(|v| v.as_str().map(str::to_string));
Ok(DecodedEvent::Error(DecodedError {
code,
message,
retryable,
hint,
fields,
trace,
}))
}
Some("progress") => Ok(DecodedEvent::Progress(DecodedProgress {
progress: obj.get("progress").cloned().unwrap_or(Value::Null),
trace,
})),
Some("log") => Ok(DecodedEvent::Log(DecodedLog {
log: obj.get("log").cloned().unwrap_or(Value::Null),
trace,
})),
_ => Err(malformed()),
}
}