Skip to main content

tauri_plugin_licensemanager/
models.rs

1//! Serializable shapes sent to the webview.
2
3use std::time::UNIX_EPOCH;
4
5use serde::{Deserialize, Serialize};
6
7use crate::core::{
8    LatestAllowedVersion, LicenseError, LicenseErrorCode, LicenseManager, LicenseState,
9    OfflineState,
10};
11
12/// [`OfflineState`] as JSON: `{ mode: "online" }` or
13/// `{ mode: "offline", validUntil: number | null, remainingMs: number }`,
14/// times in ms since the epoch.
15#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
16#[serde(tag = "mode", rename_all = "lowercase")]
17pub enum OfflineStateDto {
18    /// The last validation reached the server.
19    Online,
20    /// The app runs on the stored token.
21    Offline {
22        /// Expiry of the stored token in ms since the epoch, or `null`.
23        #[serde(rename = "validUntil")]
24        valid_until: Option<u64>,
25        /// Time left before `validUntil`, in ms.
26        #[serde(rename = "remainingMs")]
27        remaining_ms: u64,
28    },
29}
30
31impl From<OfflineState> for OfflineStateDto {
32    fn from(state: OfflineState) -> Self {
33        match state {
34            OfflineState::Online => Self::Online,
35            OfflineState::Offline {
36                valid_until,
37                remaining,
38            } => Self::Offline {
39                valid_until: valid_until.map(|t| {
40                    t.duration_since(UNIX_EPOCH)
41                        .map_or(0, |d| u64::try_from(d.as_millis()).unwrap_or(u64::MAX))
42                }),
43                remaining_ms: u64::try_from(remaining.as_millis()).unwrap_or(u64::MAX),
44            },
45        }
46    }
47}
48
49/// `undefined` (field absent) for `NotApplicable`, else `string | null`.
50fn latest_to_dto(latest: &LatestAllowedVersion) -> Option<Option<String>> {
51    latest.as_option().map(|v| v.map(str::to_owned))
52}
53
54/// Everything the webview needs to render the license, in one call. Also the
55/// payload of the `licensemanager://state-changed` event.
56#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
57#[serde(rename_all = "camelCase")]
58pub struct Snapshot {
59    /// The license state.
60    pub state: LicenseState,
61    /// Whether the last validation reached the server.
62    pub offline: OfflineStateDto,
63    /// Absent unless the server refused this version: then the latest
64    /// allowed version, or `null` when none is.
65    #[serde(default, skip_serializing_if = "Option::is_none")]
66    pub latest_allowed_version: Option<Option<String>>,
67}
68
69impl Snapshot {
70    /// The current snapshot of `manager`.
71    pub fn of(manager: &LicenseManager) -> Self {
72        Self {
73            state: manager.state(),
74            offline: manager.offline_state().into(),
75            latest_allowed_version: latest_to_dto(&manager.latest_allowed_version()),
76        }
77    }
78}
79
80/// Return value of `get_latest_allowed_version`: an object, so that
81/// "not applicable" (absent) differs from "none allowed" (`null`).
82#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
83#[serde(rename_all = "camelCase")]
84pub(crate) struct LatestAllowedVersionDto {
85    #[serde(skip_serializing_if = "Option::is_none")]
86    pub latest_allowed_version: Option<Option<String>>,
87}
88
89impl From<&LatestAllowedVersion> for LatestAllowedVersionDto {
90    fn from(latest: &LatestAllowedVersion) -> Self {
91        Self {
92            latest_allowed_version: latest_to_dto(latest),
93        }
94    }
95}
96
97/// A [`LicenseError`] as JSON: `{ code, message, status?, latestAllowedVersion? }`.
98#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
99#[serde(rename_all = "camelCase")]
100pub struct LicenseErrorDto {
101    /// The error code, e.g. `"not_found"`.
102    pub code: LicenseErrorCode,
103    /// Human-readable description.
104    pub message: String,
105    /// HTTP status, when the server answered.
106    #[serde(default, skip_serializing_if = "Option::is_none")]
107    pub status: Option<u16>,
108    /// For `version_not_allowed`: latest allowed version, or `null`.
109    #[serde(default, skip_serializing_if = "Option::is_none")]
110    pub latest_allowed_version: Option<Option<String>>,
111}
112
113impl From<LicenseError> for LicenseErrorDto {
114    fn from(error: LicenseError) -> Self {
115        Self {
116            code: error.code,
117            latest_allowed_version: latest_to_dto(&error.latest_allowed_version),
118            message: error.message,
119            status: error.status,
120        }
121    }
122}
123
124#[cfg(test)]
125mod tests {
126    use std::time::Duration;
127
128    use serde_json::json;
129
130    use super::*;
131
132    #[test]
133    fn serializes_offline_states() {
134        assert_eq!(
135            serde_json::to_value(OfflineStateDto::from(OfflineState::Online)).unwrap(),
136            json!({ "mode": "online" })
137        );
138        assert_eq!(
139            serde_json::to_value(OfflineStateDto::from(OfflineState::Offline {
140                valid_until: Some(UNIX_EPOCH + Duration::from_millis(1_767_225_600_000)),
141                remaining: Duration::from_millis(1234),
142            }))
143            .unwrap(),
144            json!({ "mode": "offline", "validUntil": 1_767_225_600_000u64, "remainingMs": 1234 })
145        );
146        assert_eq!(
147            serde_json::to_value(OfflineStateDto::from(OfflineState::Offline {
148                valid_until: None,
149                remaining: Duration::ZERO,
150            }))
151            .unwrap(),
152            json!({ "mode": "offline", "validUntil": null, "remainingMs": 0 })
153        );
154    }
155
156    #[test]
157    fn serializes_latest_allowed_version_absent_null_or_string() {
158        let dto = |latest: LatestAllowedVersion| {
159            serde_json::to_value(LatestAllowedVersionDto::from(&latest)).unwrap()
160        };
161        assert_eq!(dto(LatestAllowedVersion::NotApplicable), json!({}));
162        assert_eq!(
163            dto(LatestAllowedVersion::NoneAllowed),
164            json!({ "latestAllowedVersion": null })
165        );
166        assert_eq!(
167            dto(LatestAllowedVersion::Version("2.3.1".into())),
168            json!({ "latestAllowedVersion": "2.3.1" })
169        );
170
171        let snapshot = Snapshot {
172            state: LicenseState::VersionNotAllowed,
173            offline: OfflineStateDto::Online,
174            latest_allowed_version: Some(None),
175        };
176        assert_eq!(
177            serde_json::to_value(&snapshot).unwrap(),
178            json!({ "state": "VERSION_NOT_ALLOWED", "offline": { "mode": "online" }, "latestAllowedVersion": null })
179        );
180        let snapshot = Snapshot {
181            latest_allowed_version: None,
182            state: LicenseState::Active,
183            ..snapshot
184        };
185        assert_eq!(
186            serde_json::to_value(&snapshot).unwrap(),
187            json!({ "state": "ACTIVE", "offline": { "mode": "online" } })
188        );
189    }
190
191    #[test]
192    fn serializes_errors() {
193        let error = LicenseError {
194            code: LicenseErrorCode::VersionNotAllowed,
195            message: "nope".into(),
196            status: Some(403),
197            latest_allowed_version: LatestAllowedVersion::Version("2.3.1".into()),
198        };
199        assert_eq!(
200            serde_json::to_value(LicenseErrorDto::from(error)).unwrap(),
201            json!({ "code": "version_not_allowed", "message": "nope", "status": 403, "latestAllowedVersion": "2.3.1" })
202        );
203        let error = LicenseError::new(LicenseErrorCode::NetworkError, "offline");
204        assert_eq!(
205            serde_json::to_value(LicenseErrorDto::from(error)).unwrap(),
206            json!({ "code": "network_error", "message": "offline" })
207        );
208    }
209}