Skip to main content

shopify_sdk/config/
version.rs

1//! Shopify API version definitions.
2//!
3//! This module provides the [`ApiVersion`] enum for specifying which version
4//! of the Shopify API to use.
5
6use crate::error::ConfigError;
7use std::fmt;
8use std::str::FromStr;
9
10/// Shopify API version.
11///
12/// Shopify releases new API versions quarterly (January, April, July, October).
13/// This enum provides variants for known stable versions, plus an `Unstable`
14/// variant for development and a `Custom` variant for future versions.
15///
16/// # Example
17///
18/// ```rust
19/// use shopify_sdk::ApiVersion;
20///
21/// // Use the latest stable version
22/// let version = ApiVersion::latest();
23/// assert!(version.is_stable());
24///
25/// // Parse from string
26/// let version: ApiVersion = "2024-10".parse().unwrap();
27/// assert_eq!(version, ApiVersion::V2024_10);
28///
29/// // Display as string
30/// assert_eq!(format!("{}", ApiVersion::V2024_10), "2024-10");
31/// ```
32#[derive(Clone, Debug, PartialEq, Eq, Hash)]
33pub enum ApiVersion {
34    /// API version 2024-01 (January 2024)
35    V2024_01,
36    /// API version 2024-04 (April 2024)
37    V2024_04,
38    /// API version 2024-07 (July 2024)
39    V2024_07,
40    /// API version 2024-10 (October 2024)
41    V2024_10,
42    /// API version 2025-01 (January 2025)
43    V2025_01,
44    /// API version 2025-04 (April 2025)
45    V2025_04,
46    /// API version 2025-07 (July 2025)
47    V2025_07,
48    /// API version 2025-10 (October 2025)
49    V2025_10,
50    /// API version 2026-01 (January 2026)
51    V2026_01,
52    /// Unstable API version for development and testing.
53    Unstable,
54    /// Custom version string for future or unrecognized versions.
55    Custom(String),
56}
57
58impl ApiVersion {
59    /// Returns the latest stable API version.
60    ///
61    /// This should be updated when new stable versions are released.
62    #[must_use]
63    pub const fn latest() -> Self {
64        Self::V2026_01
65    }
66
67    /// Returns `true` if this is a known stable API version.
68    ///
69    /// Returns `false` for `Unstable` and `Custom` variants.
70    #[must_use]
71    pub const fn is_stable(&self) -> bool {
72        !matches!(self, Self::Unstable | Self::Custom(_))
73    }
74
75    /// Returns all supported stable versions in chronological order.
76    ///
77    /// This includes versions within Shopify's approximately 12-month support window.
78    /// Versions are ordered from oldest to newest.
79    ///
80    /// # Example
81    ///
82    /// ```rust
83    /// use shopify_api::ApiVersion;
84    ///
85    /// let versions = ApiVersion::supported_versions();
86    /// assert!(!versions.is_empty());
87    /// assert!(versions.contains(&ApiVersion::latest()));
88    /// ```
89    #[must_use]
90    pub fn supported_versions() -> Vec<Self> {
91        vec![
92            Self::V2025_04,
93            Self::V2025_07,
94            Self::V2025_10,
95            Self::V2026_01,
96        ]
97    }
98
99    /// Returns the oldest supported API version.
100    ///
101    /// This represents the minimum version within Shopify's support window
102    /// (approximately 12 months). Versions older than this are considered
103    /// deprecated and may stop working at any time.
104    ///
105    /// # Example
106    ///
107    /// ```rust
108    /// use shopify_api::ApiVersion;
109    ///
110    /// let minimum = ApiVersion::minimum_supported();
111    /// assert!(minimum.is_supported());
112    /// ```
113    #[must_use]
114    pub const fn minimum_supported() -> Self {
115        Self::V2025_04
116    }
117
118    /// Returns `true` if this version is within Shopify's support window.
119    ///
120    /// Supported versions include:
121    /// - All stable versions from [`minimum_supported()`] onwards
122    /// - The `Unstable` version (always supported for development)
123    /// - `Custom` versions (assumed supported as they may be newer versions)
124    ///
125    /// # Example
126    ///
127    /// ```rust
128    /// use shopify_api::ApiVersion;
129    ///
130    /// assert!(ApiVersion::V2025_10.is_supported());
131    /// assert!(ApiVersion::Unstable.is_supported());
132    /// assert!(!ApiVersion::V2024_01.is_supported());
133    /// ```
134    #[must_use]
135    pub fn is_supported(&self) -> bool {
136        match self {
137            Self::Unstable => true,
138            Self::Custom(_) => true, // Custom versions are assumed to be newer/valid
139            _ => *self >= Self::minimum_supported(),
140        }
141    }
142
143    /// Returns `true` if this version is past Shopify's support window.
144    ///
145    /// Deprecated versions are older than [`minimum_supported()`] and may
146    /// stop working at any time. You should upgrade to a supported version.
147    ///
148    /// Note: `Unstable` and `Custom` versions are never considered deprecated.
149    ///
150    /// # Example
151    ///
152    /// ```rust
153    /// use shopify_api::ApiVersion;
154    ///
155    /// assert!(ApiVersion::V2024_01.is_deprecated());
156    /// assert!(!ApiVersion::V2025_10.is_deprecated());
157    /// assert!(!ApiVersion::Unstable.is_deprecated());
158    /// ```
159    #[must_use]
160    pub fn is_deprecated(&self) -> bool {
161        match self {
162            Self::Unstable => false,
163            Self::Custom(_) => false,
164            _ => *self < Self::minimum_supported(),
165        }
166    }
167
168    /// Returns a numeric ordering value for version comparison.
169    ///
170    /// This is used internally for implementing `Ord`.
171    const fn ordinal(&self) -> u32 {
172        match self {
173            Self::V2024_01 => 1,
174            Self::V2024_04 => 2,
175            Self::V2024_07 => 3,
176            Self::V2024_10 => 4,
177            Self::V2025_01 => 5,
178            Self::V2025_04 => 6,
179            Self::V2025_07 => 7,
180            Self::V2025_10 => 8,
181            Self::V2026_01 => 9,
182            Self::Unstable => 100, // Always sorts after stable versions
183            Self::Custom(_) => 101, // Custom sorts after unstable
184        }
185    }
186}
187
188impl PartialOrd for ApiVersion {
189    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
190        Some(self.cmp(other))
191    }
192}
193
194impl Ord for ApiVersion {
195    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
196        match (self, other) {
197            // Custom versions compare lexicographically with each other
198            (Self::Custom(a), Self::Custom(b)) => a.cmp(b),
199            // Otherwise use ordinal comparison
200            _ => self.ordinal().cmp(&other.ordinal()),
201        }
202    }
203}
204
205impl fmt::Display for ApiVersion {
206    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
207        let version_str = match self {
208            Self::V2024_01 => "2024-01",
209            Self::V2024_04 => "2024-04",
210            Self::V2024_07 => "2024-07",
211            Self::V2024_10 => "2024-10",
212            Self::V2025_01 => "2025-01",
213            Self::V2025_04 => "2025-04",
214            Self::V2025_07 => "2025-07",
215            Self::V2025_10 => "2025-10",
216            Self::V2026_01 => "2026-01",
217            Self::Unstable => "unstable",
218            Self::Custom(s) => s,
219        };
220        f.write_str(version_str)
221    }
222}
223
224impl FromStr for ApiVersion {
225    type Err = ConfigError;
226
227    fn from_str(s: &str) -> Result<Self, Self::Err> {
228        let s = s.trim().to_lowercase();
229
230        match s.as_str() {
231            "2024-01" => Ok(Self::V2024_01),
232            "2024-04" => Ok(Self::V2024_04),
233            "2024-07" => Ok(Self::V2024_07),
234            "2024-10" => Ok(Self::V2024_10),
235            "2025-01" => Ok(Self::V2025_01),
236            "2025-04" => Ok(Self::V2025_04),
237            "2025-07" => Ok(Self::V2025_07),
238            "2025-10" => Ok(Self::V2025_10),
239            "2026-01" => Ok(Self::V2026_01),
240            "unstable" => Ok(Self::Unstable),
241            _ => {
242                // Check if it matches the version format YYYY-MM
243                if Self::is_valid_version_format(&s) {
244                    Ok(Self::Custom(s))
245                } else {
246                    Err(ConfigError::InvalidApiVersion { version: s })
247                }
248            }
249        }
250    }
251}
252
253impl ApiVersion {
254    fn is_valid_version_format(s: &str) -> bool {
255        // Format: YYYY-MM
256        if s.len() != 7 {
257            return false;
258        }
259
260        let parts: Vec<&str> = s.split('-').collect();
261        if parts.len() != 2 {
262            return false;
263        }
264
265        let year = parts[0];
266        let month = parts[1];
267
268        if year.len() != 4 || month.len() != 2 {
269            return false;
270        }
271
272        // Validate year is numeric
273        if !year.chars().all(|c| c.is_ascii_digit()) {
274            return false;
275        }
276
277        // Validate month is 01, 04, 07, or 10 (Shopify's quarterly releases)
278        matches!(month, "01" | "04" | "07" | "10")
279    }
280}
281
282#[cfg(test)]
283mod tests {
284    use super::*;
285
286    #[test]
287    fn test_api_version_parses_known_versions() {
288        assert_eq!(
289            "2024-01".parse::<ApiVersion>().unwrap(),
290            ApiVersion::V2024_01
291        );
292        assert_eq!(
293            "2024-10".parse::<ApiVersion>().unwrap(),
294            ApiVersion::V2024_10
295        );
296        assert_eq!(
297            "2025-01".parse::<ApiVersion>().unwrap(),
298            ApiVersion::V2025_01
299        );
300        assert_eq!(
301            "unstable".parse::<ApiVersion>().unwrap(),
302            ApiVersion::Unstable
303        );
304    }
305
306    #[test]
307    fn test_api_version_display() {
308        assert_eq!(format!("{}", ApiVersion::V2024_01), "2024-01");
309        assert_eq!(format!("{}", ApiVersion::V2024_10), "2024-10");
310        assert_eq!(format!("{}", ApiVersion::V2026_01), "2026-01");
311        assert_eq!(format!("{}", ApiVersion::Unstable), "unstable");
312        assert_eq!(
313            format!("{}", ApiVersion::Custom("2026-04".to_string())),
314            "2026-04"
315        );
316    }
317
318    #[test]
319    fn test_api_version_is_stable() {
320        assert!(ApiVersion::V2024_01.is_stable());
321        assert!(ApiVersion::V2025_10.is_stable());
322        assert!(ApiVersion::V2026_01.is_stable());
323        assert!(!ApiVersion::Unstable.is_stable());
324        assert!(!ApiVersion::Custom("2026-04".to_string()).is_stable());
325    }
326
327    #[test]
328    fn test_api_version_latest() {
329        let latest = ApiVersion::latest();
330        assert!(latest.is_stable());
331        assert_eq!(latest, ApiVersion::V2026_01);
332    }
333
334    #[test]
335    fn test_api_version_parses_future_versions() {
336        // Future versions should be parsed as Custom
337        let version: ApiVersion = "2026-04".parse().unwrap();
338        assert_eq!(version, ApiVersion::Custom("2026-04".to_string()));
339        assert!(!version.is_stable());
340    }
341
342    #[test]
343    fn test_api_version_rejects_invalid() {
344        assert!("invalid".parse::<ApiVersion>().is_err());
345        assert!("2024".parse::<ApiVersion>().is_err());
346        assert!("2024-1".parse::<ApiVersion>().is_err());
347        assert!("2024-02".parse::<ApiVersion>().is_err()); // February is not a release month
348        assert!("24-01".parse::<ApiVersion>().is_err());
349    }
350
351    #[test]
352    fn test_supported_versions_chronological() {
353        let versions = ApiVersion::supported_versions();
354
355        // Should not be empty
356        assert!(!versions.is_empty());
357
358        // Should contain the latest version
359        assert!(versions.contains(&ApiVersion::latest()));
360
361        // Should be in chronological order
362        for window in versions.windows(2) {
363            assert!(
364                window[0] < window[1],
365                "Versions should be in chronological order"
366            );
367        }
368
369        // All versions should be supported
370        for version in &versions {
371            assert!(version.is_supported(), "{version} should be supported");
372        }
373    }
374
375    #[test]
376    fn test_minimum_supported() {
377        let minimum = ApiVersion::minimum_supported();
378
379        // Minimum should be supported
380        assert!(minimum.is_supported());
381
382        // Minimum should not be deprecated
383        assert!(!minimum.is_deprecated());
384
385        // Versions before minimum should be deprecated
386        assert!(ApiVersion::V2024_01.is_deprecated());
387        assert!(ApiVersion::V2024_04.is_deprecated());
388        assert!(ApiVersion::V2024_07.is_deprecated());
389        assert!(ApiVersion::V2024_10.is_deprecated());
390        assert!(ApiVersion::V2025_01.is_deprecated());
391    }
392
393    #[test]
394    fn test_is_deprecated_for_old_versions() {
395        // Old versions are deprecated
396        assert!(ApiVersion::V2024_01.is_deprecated());
397        assert!(ApiVersion::V2024_04.is_deprecated());
398        assert!(ApiVersion::V2024_07.is_deprecated());
399        assert!(ApiVersion::V2024_10.is_deprecated());
400        assert!(ApiVersion::V2025_01.is_deprecated());
401
402        // Current versions are not deprecated
403        assert!(!ApiVersion::V2025_04.is_deprecated());
404        assert!(!ApiVersion::V2025_07.is_deprecated());
405        assert!(!ApiVersion::V2025_10.is_deprecated());
406        assert!(!ApiVersion::V2026_01.is_deprecated());
407
408        // Unstable and Custom are never deprecated
409        assert!(!ApiVersion::Unstable.is_deprecated());
410        assert!(!ApiVersion::Custom("2026-04".to_string()).is_deprecated());
411    }
412
413    #[test]
414    fn test_is_supported() {
415        // Supported versions
416        assert!(ApiVersion::V2025_04.is_supported());
417        assert!(ApiVersion::V2025_07.is_supported());
418        assert!(ApiVersion::V2025_10.is_supported());
419        assert!(ApiVersion::V2026_01.is_supported());
420        assert!(ApiVersion::Unstable.is_supported());
421        assert!(ApiVersion::Custom("2026-04".to_string()).is_supported());
422
423        // Unsupported versions
424        assert!(!ApiVersion::V2024_01.is_supported());
425        assert!(!ApiVersion::V2024_04.is_supported());
426        assert!(!ApiVersion::V2024_07.is_supported());
427        assert!(!ApiVersion::V2024_10.is_supported());
428        assert!(!ApiVersion::V2025_01.is_supported());
429    }
430
431    #[test]
432    fn test_version_ordering() {
433        // Chronological ordering of stable versions
434        assert!(ApiVersion::V2024_01 < ApiVersion::V2024_04);
435        assert!(ApiVersion::V2024_04 < ApiVersion::V2024_07);
436        assert!(ApiVersion::V2024_07 < ApiVersion::V2024_10);
437        assert!(ApiVersion::V2024_10 < ApiVersion::V2025_01);
438        assert!(ApiVersion::V2025_01 < ApiVersion::V2025_04);
439        assert!(ApiVersion::V2025_04 < ApiVersion::V2025_07);
440        assert!(ApiVersion::V2025_07 < ApiVersion::V2025_10);
441        assert!(ApiVersion::V2025_10 < ApiVersion::V2026_01);
442
443        // Unstable sorts after all stable versions
444        assert!(ApiVersion::V2026_01 < ApiVersion::Unstable);
445
446        // Custom sorts after unstable
447        assert!(ApiVersion::Unstable < ApiVersion::Custom("2026-04".to_string()));
448
449        // Custom versions compare lexicographically
450        assert!(
451            ApiVersion::Custom("2026-04".to_string()) < ApiVersion::Custom("2026-07".to_string())
452        );
453    }
454
455    #[test]
456    fn test_version_equality() {
457        assert_eq!(ApiVersion::V2024_01, ApiVersion::V2024_01);
458        assert_ne!(ApiVersion::V2024_01, ApiVersion::V2024_04);
459        assert_eq!(ApiVersion::V2026_01, ApiVersion::V2026_01);
460        assert_eq!(
461            ApiVersion::Custom("2026-04".to_string()),
462            ApiVersion::Custom("2026-04".to_string())
463        );
464    }
465}