Skip to main content

freedom_models/
gateway.rs

1//! # Gateway Licenses
2//!
3//! Contains models for interacting with Freedom Gateway licensing endpoints.
4
5use semver::Version;
6use strum::{AsRefStr, EnumString};
7use time::OffsetDateTime;
8
9/// Data model matching return type of `/api/gateway/latestVersion` endpoint
10#[cfg_attr(
11    feature = "serde",
12    derive(serde::Serialize, serde::Deserialize),
13    serde(rename_all = "camelCase")
14)]
15#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
16#[cfg_attr(not(feature = "unstable"), non_exhaustive)]
17pub struct LatestVersion {
18    /// The AWS ECR repository for Freedom Gateway
19    pub repository: String,
20    /// The semantic version of the latest tag of Freedom Gateway
21    pub latest_tag: Version,
22    #[cfg_attr(feature = "serde", serde(with = "time::serde::iso8601"))]
23    pub pushed_at: OffsetDateTime,
24    pub digest: String,
25}
26
27/// Response body returned when regenerating a license key.
28#[cfg_attr(
29    feature = "serde",
30    derive(serde::Serialize, serde::Deserialize),
31    serde(rename_all = "camelCase")
32)]
33#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
34#[cfg_attr(not(feature = "unstable"), non_exhaustive)]
35pub struct RegenerateResponse {
36    pub account_id: u64,
37    pub license_id: u32,
38    #[cfg_attr(feature = "serde", serde(with = "time::serde::iso8601"))]
39    pub expires_at: OffsetDateTime,
40    pub license_key: String,
41}
42
43/// Response body returned from a license verification request.
44#[cfg_attr(
45    feature = "serde",
46    derive(serde::Serialize, serde::Deserialize),
47    serde(rename_all = "camelCase")
48)]
49#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
50#[cfg_attr(not(feature = "unstable"), non_exhaustive)]
51pub struct VerifyResponse {
52    pub valid: bool,
53    pub license_id: Option<u32>,
54    #[cfg_attr(
55        feature = "serde",
56        serde(default, with = "time::serde::iso8601::option")
57    )]
58    pub expires_at: Option<OffsetDateTime>,
59    pub reason: Option<String>,
60}
61
62/// Response body for viewing all licenses associated with an account.
63///
64/// This is a wrapper type over a list of [`ViewOne`] items, corresponding to each license record
65/// for the account.
66#[cfg_attr(
67    feature = "serde",
68    derive(serde::Serialize, serde::Deserialize),
69    serde(rename_all = "camelCase")
70)]
71#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
72#[cfg_attr(not(feature = "unstable"), non_exhaustive)]
73pub struct View(pub Vec<ViewOne>);
74
75/// Representation of a single license associated with an account.
76///
77/// Used in license listing and detail-view responses.
78#[cfg_attr(
79    feature = "serde",
80    derive(serde::Serialize, serde::Deserialize),
81    serde(rename_all = "camelCase")
82)]
83#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
84#[cfg_attr(not(feature = "unstable"), non_exhaustive)]
85pub struct ViewOne {
86    pub id: u32,
87    pub account_id: u64,
88    pub status: Status,
89    #[cfg_attr(feature = "serde", serde(with = "time::serde::iso8601"))]
90    pub expires_at: OffsetDateTime,
91    #[cfg_attr(
92        feature = "serde",
93        serde(default, with = "time::serde::iso8601::option")
94    )]
95    pub last_used_at: Option<OffsetDateTime>,
96    #[cfg_attr(feature = "serde", serde(with = "time::serde::iso8601"))]
97    pub created: OffsetDateTime,
98    #[cfg_attr(feature = "serde", serde(with = "time::serde::iso8601"))]
99    pub modified: OffsetDateTime,
100    pub key_version: u32,
101}
102
103/// The current status of a license.
104///
105/// Additional variants may be added in the future, so consumers should
106/// handle this enum non-exhaustively.
107#[cfg_attr(
108    feature = "serde",
109    derive(serde::Serialize, serde::Deserialize),
110    serde(rename_all = "SCREAMING_SNAKE_CASE")
111)]
112#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, AsRefStr, EnumString)]
113#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
114#[cfg_attr(not(feature = "unstable"), non_exhaustive)]
115pub enum Status {
116    /// License is active and can be used.
117    Active,
118    /// License is inactive and should not be used.
119    Inactive,
120}
121
122#[cfg(all(test, feature = "serde"))]
123mod tests {
124    use time::macros::datetime;
125
126    use super::*;
127
128    #[test]
129    fn view_all() {
130        let json = r#"[
131    {
132        "id": 1,
133        "accountId": 1,
134        "status": "ACTIVE",
135        "expiresAt": "2025-12-11T00:00:00Z",
136        "lastUsedAt": "2025-12-09T23:15:10.520830Z",
137        "created": "2025-12-09T23:14:22.482359Z",
138        "modified": "2025-12-09T23:15:10.522040Z",
139        "keyVersion": 1
140    },
141    {
142        "id": 2,
143        "accountId": 1,
144        "status": "ACTIVE",
145        "expiresAt": "2025-12-11T00:00:00Z",
146        "created": "2025-12-10T17:31:59.709112Z",
147        "modified": "2025-12-10T17:31:59.709112Z",
148        "keyVersion": 1
149    }
150]"#;
151        let view: View = serde_json::from_str(json).unwrap();
152        let should_be = View(vec![
153            ViewOne {
154                id: 1,
155                account_id: 1,
156                status: Status::Active,
157                expires_at: datetime!(2025 - 12 - 11 00:00:00).assume_utc(),
158                last_used_at: Some(datetime!(2025 - 12 - 09 23:15:10.520_830).assume_utc()),
159                created: datetime!(2025 - 12 - 09 23:14:22.482_359).assume_utc(),
160                modified: datetime!(2025 - 12 - 09 23:15:10.522_040).assume_utc(),
161                key_version: 1,
162            },
163            ViewOne {
164                id: 2,
165                account_id: 1,
166                status: Status::Active,
167                expires_at: datetime!(2025 - 12 - 11 00:00:00).assume_utc(),
168                last_used_at: None,
169                created: datetime!(2025 - 12 - 10 17:31:59.709_112).assume_utc(),
170                modified: datetime!(2025 - 12 - 10 17:31:59.709_112).assume_utc(),
171                key_version: 1,
172            },
173        ]);
174        assert_eq!(view, should_be);
175    }
176
177    #[test]
178    fn view_one() {
179        let json = r#"
180    {
181        "id": 1,
182        "accountId": 1,
183        "status": "ACTIVE",
184        "expiresAt": "2025-12-11T00:00:00Z",
185        "lastUsedAt": "2025-12-09T23:15:10.520830Z",
186        "created": "2025-12-09T23:14:22.482359Z",
187        "modified": "2025-12-09T23:15:10.522040Z",
188        "keyVersion": 1
189    }"#;
190        let view: ViewOne = serde_json::from_str(json).unwrap();
191        let should_be = ViewOne {
192            id: 1,
193            account_id: 1,
194            status: Status::Active,
195            expires_at: datetime!(2025 - 12 - 11 00:00:00).assume_utc(),
196            last_used_at: Some(datetime!(2025 - 12 - 09 23:15:10.520_830).assume_utc()),
197            created: datetime!(2025 - 12 - 09 23:14:22.482_359).assume_utc(),
198            modified: datetime!(2025 - 12 - 09 23:15:10.522_040).assume_utc(),
199            key_version: 1,
200        };
201        assert_eq!(view, should_be);
202    }
203
204    #[test]
205    fn view_one_missing_last_used() {
206        let json = r#"
207    {
208        "id": 1,
209        "accountId": 1,
210        "status": "ACTIVE",
211        "expiresAt": "2025-12-11T00:00:00Z",
212        "created": "2025-12-09T23:14:22.482359Z",
213        "modified": "2025-12-09T23:15:10.522040Z",
214        "keyVersion": 1
215    }"#;
216        let view: ViewOne = serde_json::from_str(json).unwrap();
217        let should_be = ViewOne {
218            id: 1,
219            account_id: 1,
220            status: Status::Active,
221            expires_at: datetime!(2025 - 12 - 11 00:00:00).assume_utc(),
222            last_used_at: None,
223            created: datetime!(2025 - 12 - 09 23:14:22.482_359).assume_utc(),
224            modified: datetime!(2025 - 12 - 09 23:15:10.522_040).assume_utc(),
225            key_version: 1,
226        };
227        assert_eq!(view, should_be);
228    }
229
230    #[test]
231    fn regenerate_response() {
232        let json = r#"{
233    "licenseId": 1,
234    "accountId": 1,
235    "expiresAt": "2025-12-11T00:00:00Z",
236    "licenseKey": "foobar"
237}"#;
238        let regenerate: RegenerateResponse = serde_json::from_str(json).unwrap();
239        let should_be = RegenerateResponse {
240            account_id: 1,
241            license_id: 1,
242            expires_at: datetime!(2025 - 12 - 11 00:00:00).assume_utc(),
243            license_key: String::from("foobar"),
244        };
245        assert_eq!(regenerate, should_be);
246    }
247
248    #[test]
249    fn verify_response_valid() {
250        let json = r#"{
251    "valid": true,
252    "licenseId": 1,
253    "expiresAt": "2025-12-11T00:00:00Z"
254}"#;
255        let verify: VerifyResponse = serde_json::from_str(json).unwrap();
256        let should_be = VerifyResponse {
257            valid: true,
258            license_id: Some(1),
259            expires_at: Some(datetime!(2025 - 12 - 11 00:00:00).assume_utc()),
260            reason: None,
261        };
262        assert_eq!(verify, should_be);
263    }
264
265    #[test]
266    fn verify_response_invalid() {
267        let json = r#"{
268    "valid": false,
269    "reason": "INVALID"
270}"#;
271        let verify: VerifyResponse = serde_json::from_str(json).unwrap();
272        let should_be = VerifyResponse {
273            valid: false,
274            license_id: None,
275            expires_at: None,
276            reason: Some(String::from("INVALID")),
277        };
278        assert_eq!(verify, should_be);
279    }
280}