use std::fmt;
use serde::de::Error as _;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use super::json::Json;
use crate::macros::text_conversions;
use crate::{Error, Result};
#[derive(Clone, PartialEq)]
pub struct DecisionInput(Repr);
#[derive(Clone, PartialEq)]
enum Repr {
Text(String),
Json(Json),
}
impl DecisionInput {
#[must_use]
pub fn text(text: impl Into<String>) -> Self {
Self(Repr::Text(text.into()))
}
pub fn json<T: Serialize + ?Sized>(value: &T) -> Result<Self> {
let text = serde_json::to_string(value)?;
let json: Json = serde_json::from_str(&text)?;
Self::from_json(json).map_err(Error::invalid_request)
}
#[must_use]
pub fn as_text(&self) -> Option<&str> {
match &self.0 {
Repr::Text(text) => Some(text),
Repr::Json(_) => None,
}
}
#[must_use]
pub fn is_json(&self) -> bool {
matches!(self.0, Repr::Json(_))
}
#[must_use]
pub fn to_json_string(&self) -> Option<String> {
match &self.0 {
Repr::Text(_) => None,
Repr::Json(json) => serde_json::to_string(json).ok(),
}
}
#[must_use]
pub fn is_blank(&self) -> bool {
match &self.0 {
Repr::Text(text) => text.trim().is_empty(),
Repr::Json(_) => false,
}
}
fn from_json(json: Json) -> std::result::Result<Self, String> {
match json {
Json::String(text) => Ok(Self::text(text)),
json @ (Json::Object(_) | Json::Array(_)) => Ok(Self(Repr::Json(json))),
other => Err(format!(
"decision input must be text, a JSON object, or a JSON array, found {}",
serde_json::to_string(&other).unwrap_or_default()
)),
}
}
}
text_conversions!(DecisionInput, Self::text);
impl From<serde_json::Value> for DecisionInput {
fn from(value: serde_json::Value) -> Self {
use serde_json::Value;
match value {
Value::String(text) => Self::text(text),
Value::Object(_) | Value::Array(_) => Self(Repr::Json(Json::from(value))),
Value::Null => Self::text(String::new()),
scalar @ (Value::Bool(_) | Value::Number(_)) => Self::text(scalar.to_string()),
}
}
}
impl fmt::Debug for DecisionInput {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match &self.0 {
Repr::Text(text) => f.debug_tuple("Text").field(text).finish(),
Repr::Json(_) => f
.debug_tuple("Json")
.field(&self.to_json_string().unwrap_or_default())
.finish(),
}
}
}
impl Serialize for DecisionInput {
fn serialize<S: Serializer>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error> {
match &self.0 {
Repr::Text(text) => serializer.serialize_str(text),
Repr::Json(json) => json.serialize(serializer),
}
}
}
impl<'de> Deserialize<'de> for DecisionInput {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> std::result::Result<Self, D::Error> {
Self::from_json(Json::deserialize(deserializer)?).map_err(D::Error::custom)
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn json_keeps_serialized_key_order() {
#[derive(Serialize)]
struct Ticket {
zeta: u8,
alpha: u8,
}
let input = DecisionInput::json(&Ticket { zeta: 1, alpha: 2 }).unwrap();
assert!(input.is_json());
assert_eq!(
input.to_json_string().as_deref(),
Some(r#"{"zeta":1,"alpha":2}"#)
);
assert_eq!(
serde_json::to_string(&input).unwrap(),
r#"{"zeta":1,"alpha":2}"#
);
}
#[test]
fn json_strings_become_text_and_scalars_are_rejected() {
assert_eq!(
DecisionInput::json("hello").unwrap(),
DecisionInput::text("hello")
);
for scalar in [json!(1), json!(true), json!(null)] {
let err = DecisionInput::json(&scalar).unwrap_err();
assert!(matches!(err, Error::InvalidRequest { .. }), "{err:?}");
}
}
#[test]
fn from_value_matches_json() {
for value in [
json!({"zeta": 1, "alpha": {"b": [1, 2.5, null], "a": true}}),
json!({}),
json!([{"b": 1, "a": 2}, "x", -3]),
json!([]),
json!("hello"),
] {
assert_eq!(
DecisionInput::from(value.clone()),
DecisionInput::json(&value).unwrap(),
"{value}"
);
}
}
#[test]
fn from_null_is_blank_text() {
let input = DecisionInput::from(json!(null));
assert_eq!(input, DecisionInput::text(""));
assert!(input.is_blank());
}
#[test]
fn from_value_makes_scalars_text() {
assert_eq!(DecisionInput::from(json!(42)), DecisionInput::text("42"));
assert_eq!(
DecisionInput::from(json!(-2.5)),
DecisionInput::text("-2.5")
);
assert_eq!(
DecisionInput::from(json!(true)),
DecisionInput::text("true")
);
assert!(!DecisionInput::from(json!(false)).is_blank());
assert!(!DecisionInput::from(json!(0)).is_blank());
}
#[test]
fn round_trips_through_serde_in_order() {
let text = r#"{"b":[1,-2,2.5,null,true],"a":{"y":"z","x":"w"}}"#;
let input: DecisionInput = serde_json::from_str(text).unwrap();
assert_eq!(serde_json::to_string(&input).unwrap(), text);
assert_eq!(
serde_json::from_str::<DecisionInput>(r#""hi""#).unwrap(),
DecisionInput::text("hi")
);
assert!(serde_json::from_str::<DecisionInput>("42").is_err());
assert_eq!(
serde_json::from_str::<DecisionInput>(r#"{ "a" : 1 }"#).unwrap(),
serde_json::from_str::<DecisionInput>(r#"{"a":1}"#).unwrap()
);
}
#[test]
fn deserializes_through_buffered_serde_paths() {
#[derive(Deserialize)]
#[serde(tag = "kind")]
enum Envelope {
Decide { input: DecisionInput },
}
let Envelope::Decide { input } =
serde_json::from_str(r#"{"kind":"Decide","input":{"z":1,"a":2}}"#).unwrap();
assert_eq!(input.to_json_string().as_deref(), Some(r#"{"z":1,"a":2}"#));
}
#[test]
fn blank_is_whitespace_only_text() {
assert!(DecisionInput::text(" \n").is_blank());
assert!(!DecisionInput::json(&json!({})).unwrap().is_blank());
}
}