Skip to main content

keygen_rs/
api_version.rs

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    /// Resolve an observed response contract to the newest contract this SDK
27    /// can safely request. Newer minor versions remain compatible through
28    /// Keygen's version pinning, while a different major version does not.
29    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}