use core::fmt;
use core::str::FromStr;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum A2aVersion {
V1_0,
Unknown {
major: u16,
minor: u16,
},
}
const fn parts(version: A2aVersion) -> (u16, u16) {
match version {
A2aVersion::V1_0 => (1, 0),
A2aVersion::Unknown { major, minor } => (major, minor),
}
}
impl PartialOrd for A2aVersion {
fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for A2aVersion {
fn cmp(&self, other: &Self) -> core::cmp::Ordering {
parts(*self).cmp(&parts(*other))
}
}
impl A2aVersion {
pub const KNOWN: [Self; 1] = [Self::V1_0];
pub const fn as_str(&self) -> &'static str {
match self {
Self::V1_0 => "1.0",
Self::Unknown { .. } => "unknown",
}
}
pub const fn is_known(&self) -> bool {
matches!(self, Self::V1_0)
}
}
impl fmt::Display for A2aVersion {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::V1_0 => f.write_str("1.0"),
Self::Unknown { major, minor } => write!(f, "{major}.{minor}"),
}
}
}
impl Serialize for A2aVersion {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.collect_str(self)
}
}
impl<'de> Deserialize<'de> for A2aVersion {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let s = String::deserialize(deserializer)?;
Self::from_str(&s).map_err(serde::de::Error::custom)
}
}
impl FromStr for A2aVersion {
type Err = ParseA2aVersionError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let (major, minor) =
s.split_once('.')
.ok_or_else(|| ParseA2aVersionError::MissingMinor {
input: s.to_owned(),
})?;
let major: u16 = major
.parse()
.map_err(|error| ParseA2aVersionError::InvalidMajor {
input: s.to_owned(),
error,
})?;
let minor: u16 = minor
.parse()
.map_err(|error| ParseA2aVersionError::InvalidMinor {
input: s.to_owned(),
error,
})?;
let version = match (major, minor) {
(1, 0) => Self::V1_0,
_ => Self::Unknown { major, minor },
};
Ok(version)
}
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum ParseA2aVersionError {
#[error("A2A version `{input}` is missing the `.Minor` part")]
MissingMinor {
input: String,
},
#[error("A2A version `{input}` has a non-numeric Major: {error}")]
InvalidMajor {
input: String,
#[source]
error: core::num::ParseIntError,
},
#[error("A2A version `{input}` has a non-numeric Minor: {error}")]
InvalidMinor {
input: String,
#[source]
error: core::num::ParseIntError,
},
}
#[cfg(test)]
mod tests {
use core::str::FromStr;
use super::{A2aVersion, ParseA2aVersionError};
#[test]
fn v1_0_as_str_is_the_wire_form() {
assert_eq!(A2aVersion::V1_0.as_str(), "1.0");
}
#[test]
fn unknown_as_str_is_the_literal_unknown() {
let version = A2aVersion::Unknown { major: 2, minor: 5 };
assert_eq!(version.as_str(), "unknown");
}
#[test]
fn is_known_matches_the_variant() {
assert!(A2aVersion::V1_0.is_known());
assert!(!A2aVersion::Unknown { major: 1, minor: 0 }.is_known());
assert!(!A2aVersion::Unknown { major: 2, minor: 0 }.is_known());
}
#[test]
fn display_for_v1_0_is_dot_zero() {
assert_eq!(A2aVersion::V1_0.to_string(), "1.0");
}
#[test]
fn display_for_unknown_is_major_dot_minor() {
let version = A2aVersion::Unknown { major: 2, minor: 5 };
assert_eq!(version.to_string(), "2.5");
}
#[test]
fn from_str_parses_v1_0() -> Result<(), ParseA2aVersionError> {
let version = A2aVersion::from_str("1.0")?;
assert_eq!(version, A2aVersion::V1_0);
Ok(())
}
#[test]
fn from_str_parses_unknown_with_known_fields() -> Result<(), ParseA2aVersionError> {
let version = A2aVersion::from_str("2.5")?;
assert_eq!(version, A2aVersion::Unknown { major: 2, minor: 5 });
Ok(())
}
#[test]
fn from_str_rejects_missing_minor_with_named_variant() -> Result<(), String> {
let result = A2aVersion::from_str("1");
match result {
Err(ParseA2aVersionError::MissingMinor { input }) => {
if input != "1" {
return Err(format!("expected input '1', got {input:?}"));
}
Ok(())
}
other => Err(format!("expected MissingMinor, got {other:?}")),
}
}
#[test]
fn from_str_rejects_non_numeric_major_with_named_variant() -> Result<(), String> {
let result = A2aVersion::from_str("foo.bar");
match result {
Err(ParseA2aVersionError::InvalidMajor { input, .. }) => {
if input != "foo.bar" {
return Err(format!("expected input 'foo.bar', got {input:?}"));
}
Ok(())
}
other => Err(format!("expected InvalidMajor, got {other:?}")),
}
}
#[test]
fn from_str_rejects_non_numeric_minor_with_named_variant() -> Result<(), String> {
let result = A2aVersion::from_str("1.foo");
match result {
Err(ParseA2aVersionError::InvalidMinor { input, .. }) => {
if input != "1.foo" {
return Err(format!("expected input '1.foo', got {input:?}"));
}
Ok(())
}
other => Err(format!("expected InvalidMinor, got {other:?}")),
}
}
#[test]
fn unknown_ordering_is_numeric_not_lexicographic() {
let nine = A2aVersion::Unknown { major: 0, minor: 9 };
let ten = A2aVersion::Unknown {
major: 0,
minor: 10,
};
assert!(nine < ten, "0.9 must sort before 0.10 numerically");
}
#[test]
fn unknown_versions_sort_with_their_known_equivalent() {
let known = A2aVersion::V1_0;
let unknown_equivalent = A2aVersion::Unknown { major: 1, minor: 0 };
assert_eq!(known.cmp(&unknown_equivalent), core::cmp::Ordering::Equal);
assert_eq!(unknown_equivalent.cmp(&known), core::cmp::Ordering::Equal);
}
#[test]
fn known_array_lists_v1_0() {
assert_eq!(A2aVersion::KNOWN, [A2aVersion::V1_0]);
}
}