1use serde::{Deserialize, Deserializer, Serialize, Serializer};
2use std::error::Error as StdError;
3use std::fmt;
4use std::str::FromStr;
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
7pub struct ApiContractVersion {
8 major: u8,
9 minor: u8,
10}
11
12impl ApiContractVersion {
13 pub const V1_7: Self = Self { major: 1, minor: 7 };
14 pub const V1_8: Self = Self { major: 1, minor: 8 };
15 pub const CURRENT: Self = Self::V1_8;
16 pub const MINIMUM_SUPPORTED: Self = Self::V1_7;
17
18 pub const fn as_str(self) -> &'static str {
19 match (self.major, self.minor) {
20 (1, 7) => "1.7",
21 (1, 8) => "1.8",
22 _ => unreachable!(),
23 }
24 }
25
26 pub fn resolve_observed(value: &str) -> Result<Self, ApiVersionParseError> {
30 let mut parts = value.split('.');
31 let parsed = parts
32 .next()
33 .and_then(|major| major.parse::<u16>().ok())
34 .zip(parts.next().and_then(|minor| minor.parse::<u16>().ok()))
35 .filter(|_| parts.next().is_none());
36 let Some((major, minor)) = parsed else {
37 return Err(ApiVersionParseError::new(value));
38 };
39
40 if major != u16::from(Self::CURRENT.major)
41 || minor < u16::from(Self::MINIMUM_SUPPORTED.minor)
42 {
43 return Err(ApiVersionParseError::new(value));
44 }
45
46 if minor == u16::from(Self::V1_7.minor) {
47 Ok(Self::V1_7)
48 } else {
49 Ok(Self::CURRENT)
50 }
51 }
52}
53
54impl Default for ApiContractVersion {
55 fn default() -> Self {
56 Self::CURRENT
57 }
58}
59
60impl fmt::Display for ApiContractVersion {
61 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
62 formatter.write_str(self.as_str())
63 }
64}
65
66impl FromStr for ApiContractVersion {
67 type Err = ApiVersionParseError;
68
69 fn from_str(value: &str) -> Result<Self, Self::Err> {
70 match value {
71 "1.7" => Ok(Self::V1_7),
72 "1.8" => Ok(Self::V1_8),
73 _ => Err(ApiVersionParseError::new(value)),
74 }
75 }
76}
77
78impl TryFrom<&str> for ApiContractVersion {
79 type Error = ApiVersionParseError;
80
81 fn try_from(value: &str) -> Result<Self, Self::Error> {
82 value.parse()
83 }
84}
85
86impl TryFrom<String> for ApiContractVersion {
87 type Error = ApiVersionParseError;
88
89 fn try_from(value: String) -> Result<Self, Self::Error> {
90 value.parse()
91 }
92}
93
94impl Serialize for ApiContractVersion {
95 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
96 where
97 S: Serializer,
98 {
99 serializer.serialize_str(self.as_str())
100 }
101}
102
103impl<'de> Deserialize<'de> for ApiContractVersion {
104 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
105 where
106 D: Deserializer<'de>,
107 {
108 let value = String::deserialize(deserializer)?;
109 value.parse().map_err(serde::de::Error::custom)
110 }
111}
112
113#[derive(Debug, Clone, PartialEq, Eq)]
114pub struct ApiVersionParseError {
115 value: String,
116}
117
118impl ApiVersionParseError {
119 fn new(value: impl Into<String>) -> Self {
120 Self {
121 value: value.into(),
122 }
123 }
124
125 pub fn value(&self) -> &str {
126 &self.value
127 }
128}
129
130impl fmt::Display for ApiVersionParseError {
131 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
132 write!(
133 formatter,
134 "unsupported Keygen API version '{}'; supported versions are 1.7 and 1.8",
135 self.value
136 )
137 }
138}
139
140impl StdError for ApiVersionParseError {}
141
142#[cfg(test)]
143mod tests {
144 use super::*;
145
146 #[test]
147 fn parses_supported_versions() {
148 assert_eq!("1.7".parse(), Ok(ApiContractVersion::V1_7));
149 assert_eq!("1.8".parse(), Ok(ApiContractVersion::V1_8));
150 }
151
152 #[test]
153 fn rejects_legacy_future_and_prefixed_versions() {
154 for value in ["1.6", "1.9", "2.0", "v1.8", "1.8.0"] {
155 assert!(value.parse::<ApiContractVersion>().is_err());
156 }
157 }
158
159 #[test]
160 fn resolves_observed_contracts_to_the_latest_supported_version() {
161 assert_eq!(
162 ApiContractVersion::resolve_observed("1.7"),
163 Ok(ApiContractVersion::V1_7)
164 );
165 assert_eq!(
166 ApiContractVersion::resolve_observed("1.8"),
167 Ok(ApiContractVersion::V1_8)
168 );
169 assert_eq!(
170 ApiContractVersion::resolve_observed("1.9"),
171 Ok(ApiContractVersion::CURRENT)
172 );
173 }
174
175 #[test]
176 fn rejects_observed_contracts_outside_the_supported_major_range() {
177 for value in ["1.6", "2.0", "v1.8", "1.8.0", "unknown"] {
178 assert!(ApiContractVersion::resolve_observed(value).is_err());
179 }
180 }
181
182 #[test]
183 fn serde_uses_the_header_representation() {
184 let encoded = serde_json::to_string(&ApiContractVersion::V1_8).unwrap();
185 let decoded: ApiContractVersion = serde_json::from_str(&encoded).unwrap();
186
187 assert_eq!(encoded, "\"1.8\"");
188 assert_eq!(decoded, ApiContractVersion::V1_8);
189 }
190}