use serde::{Deserialize, Deserializer, Serialize};
#[derive(Debug, Clone, Default, PartialEq, Eq, Hash, Serialize)]
#[serde(transparent)]
pub struct FlexibleString(pub String);
impl FlexibleString {
pub fn as_str(&self) -> &str {
&self.0
}
pub fn into_inner(self) -> String {
self.0
}
}
impl std::ops::Deref for FlexibleString {
type Target = str;
fn deref(&self) -> &str {
&self.0
}
}
impl AsRef<str> for FlexibleString {
fn as_ref(&self) -> &str {
&self.0
}
}
impl std::fmt::Display for FlexibleString {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
impl From<String> for FlexibleString {
fn from(s: String) -> Self {
Self(s)
}
}
impl From<&str> for FlexibleString {
fn from(s: &str) -> Self {
Self(s.to_owned())
}
}
impl From<FlexibleString> for String {
fn from(s: FlexibleString) -> Self {
s.0
}
}
impl PartialEq<str> for FlexibleString {
fn eq(&self, other: &str) -> bool {
self.0 == other
}
}
impl PartialEq<&str> for FlexibleString {
fn eq(&self, other: &&str) -> bool {
self.0 == *other
}
}
impl PartialEq<String> for FlexibleString {
fn eq(&self, other: &String) -> bool {
&self.0 == other
}
}
impl<'de> Deserialize<'de> for FlexibleString {
fn deserialize<D>(de: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
struct V;
impl<'de> serde::de::Visitor<'de> for V {
type Value = FlexibleString;
fn expecting(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("a string or a number")
}
fn visit_str<E: serde::de::Error>(self, v: &str) -> Result<FlexibleString, E> {
Ok(FlexibleString(v.to_owned()))
}
fn visit_string<E: serde::de::Error>(self, v: String) -> Result<FlexibleString, E> {
Ok(FlexibleString(v))
}
fn visit_i64<E: serde::de::Error>(self, v: i64) -> Result<FlexibleString, E> {
Ok(FlexibleString(v.to_string()))
}
fn visit_u64<E: serde::de::Error>(self, v: u64) -> Result<FlexibleString, E> {
Ok(FlexibleString(v.to_string()))
}
fn visit_i128<E: serde::de::Error>(self, v: i128) -> Result<FlexibleString, E> {
Ok(FlexibleString(v.to_string()))
}
fn visit_u128<E: serde::de::Error>(self, v: u128) -> Result<FlexibleString, E> {
Ok(FlexibleString(v.to_string()))
}
fn visit_f64<E: serde::de::Error>(self, v: f64) -> Result<FlexibleString, E> {
Ok(FlexibleString((v as i64).to_string()))
}
}
de.deserialize_any(V)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn deserialize_from_string() {
let v: FlexibleString = serde_json::from_str(r#""04/27/2026 10:00:00 UTC""#).unwrap();
assert_eq!(&*v, "04/27/2026 10:00:00 UTC");
}
#[test]
fn deserialize_from_unix_millis_integer() {
let v: FlexibleString = serde_json::from_str("1777382685086").unwrap();
assert_eq!(&*v, "1777382685086");
}
#[test]
fn deserialize_from_negative_integer() {
let v: FlexibleString = serde_json::from_str("-1").unwrap();
assert_eq!(&*v, "-1");
}
#[test]
fn deserialize_from_zero() {
let v: FlexibleString = serde_json::from_str("0").unwrap();
assert_eq!(&*v, "0");
}
#[test]
fn deserialize_from_float_truncates() {
let v: FlexibleString = serde_json::from_str("123.456").unwrap();
assert_eq!(&*v, "123");
}
#[test]
fn deserialize_inside_struct() {
#[derive(Deserialize)]
struct Snap {
timestamp: Option<FlexibleString>,
}
let s: Snap = serde_json::from_str(r#"{"timestamp": 1777382685086}"#).unwrap();
assert_eq!(s.timestamp.as_deref(), Some("1777382685086"));
let s: Snap = serde_json::from_str(r#"{"timestamp": "anything"}"#).unwrap();
assert_eq!(s.timestamp.as_deref(), Some("anything"));
let s: Snap = serde_json::from_str(r#"{"timestamp": null}"#).unwrap();
assert!(s.timestamp.is_none());
}
#[test]
fn serialize_round_trips_as_string() {
let v = FlexibleString::from("1777382685086");
let json = serde_json::to_string(&v).unwrap();
assert_eq!(json, r#""1777382685086""#);
}
#[test]
fn equality_with_str_and_string() {
let v = FlexibleString::from("hello");
assert_eq!(v, *"hello");
assert_eq!(v, "hello");
assert_eq!(v, String::from("hello"));
}
#[test]
fn deref_and_display() {
let v = FlexibleString::from("abc");
let lower: &str = &v;
assert_eq!(lower, "abc");
assert_eq!(format!("{v}"), "abc");
}
#[test]
fn into_string_and_back() {
let s: String = FlexibleString::from("xyz").into();
assert_eq!(s, "xyz");
let v: FlexibleString = s.into();
assert_eq!(&*v, "xyz");
}
}