Skip to main content

cloud_sdk_testkit/
metadata.rs

1//! Provider-neutral response metadata fixtures.
2
3use cloud_sdk::rate_limit::RateLimit;
4
5/// Fixture metadata validation error.
6#[derive(Clone, Copy, Debug, Eq, PartialEq)]
7pub enum FixtureMetadataError {
8    /// Pagination page and page-size values must be nonzero.
9    PaginationZero,
10    /// Current page must not exceed the last page.
11    PageAfterLast,
12    /// Last page does not match total entries and page size.
13    InvalidLastPage,
14    /// Remaining requests must not exceed the rate limit.
15    RemainingExceedsLimit,
16    /// Rate limits must be nonzero.
17    RateLimitZero,
18    /// Action progress must be in `0..=100`.
19    InvalidProgress,
20}
21
22impl_static_error!(FixtureMetadataError,
23    Self::PaginationZero => "fixture pagination value must be nonzero",
24    Self::PageAfterLast => "fixture page exceeds the last page",
25    Self::InvalidLastPage => "fixture last-page metadata is invalid",
26    Self::RemainingExceedsLimit => "fixture remaining requests exceed the rate limit",
27    Self::RateLimitZero => "fixture rate limit must be nonzero",
28    Self::InvalidProgress => "fixture action progress exceeds 100",
29);
30
31/// Pagination metadata for a deterministic response.
32#[derive(Clone, Copy, Debug, Eq, PartialEq)]
33pub struct PaginationFixture {
34    page: u64,
35    per_page: u64,
36    total_entries: u64,
37    last_page: u64,
38}
39
40impl PaginationFixture {
41    /// Creates coherent pagination metadata.
42    pub const fn new(
43        page: u64,
44        per_page: u64,
45        total_entries: u64,
46        last_page: u64,
47    ) -> Result<Self, FixtureMetadataError> {
48        if page == 0 || per_page == 0 || last_page == 0 {
49            return Err(FixtureMetadataError::PaginationZero);
50        }
51        if page > last_page {
52            return Err(FixtureMetadataError::PageAfterLast);
53        }
54        let mut expected_last = match total_entries.checked_div(per_page) {
55            Some(value) => value,
56            None => return Err(FixtureMetadataError::PaginationZero),
57        };
58        let remainder = match total_entries.checked_rem(per_page) {
59            Some(value) => value,
60            None => return Err(FixtureMetadataError::PaginationZero),
61        };
62        if remainder != 0 {
63            expected_last = match expected_last.checked_add(1) {
64                Some(value) => value,
65                None => return Err(FixtureMetadataError::InvalidLastPage),
66            };
67        }
68        if expected_last == 0 {
69            expected_last = 1;
70        }
71        if last_page != expected_last {
72            return Err(FixtureMetadataError::InvalidLastPage);
73        }
74        Ok(Self {
75            page,
76            per_page,
77            total_entries,
78            last_page,
79        })
80    }
81
82    /// Returns the current page.
83    #[must_use]
84    pub const fn page(self) -> u64 {
85        self.page
86    }
87
88    /// Returns the requested page size.
89    #[must_use]
90    pub const fn per_page(self) -> u64 {
91        self.per_page
92    }
93
94    /// Returns the total entry count.
95    #[must_use]
96    pub const fn total_entries(self) -> u64 {
97        self.total_entries
98    }
99
100    /// Returns the last page number.
101    #[must_use]
102    pub const fn last_page(self) -> u64 {
103        self.last_page
104    }
105}
106
107/// Provider-neutral action lifecycle fixture.
108#[derive(Clone, Copy, Debug, Eq, PartialEq)]
109pub enum ActionState {
110    /// Action remains in progress.
111    Running,
112    /// Action completed successfully.
113    Success,
114    /// Action completed with an error.
115    Error,
116}
117
118/// Action metadata for polling tests.
119#[derive(Clone, Copy, Debug, Eq, PartialEq)]
120pub struct ActionFixture {
121    state: ActionState,
122    progress: u8,
123}
124
125impl ActionFixture {
126    /// Creates bounded action metadata.
127    pub const fn new(state: ActionState, progress: u8) -> Result<Self, FixtureMetadataError> {
128        if progress > 100 {
129            return Err(FixtureMetadataError::InvalidProgress);
130        }
131        Ok(Self { state, progress })
132    }
133
134    /// Returns the lifecycle state.
135    #[must_use]
136    pub const fn state(self) -> ActionState {
137        self.state
138    }
139
140    /// Returns progress in `0..=100`.
141    #[must_use]
142    pub const fn progress(self) -> u8 {
143        self.progress
144    }
145}
146
147/// Rate-limit metadata fixture.
148#[derive(Clone, Copy, Debug, Eq, PartialEq)]
149pub struct RateLimitFixture {
150    limit: u64,
151    remaining: u64,
152    reset_at: u64,
153}
154
155impl RateLimitFixture {
156    /// Creates coherent rate-limit metadata.
157    pub const fn new(
158        limit: u64,
159        remaining: u64,
160        reset_at: u64,
161    ) -> Result<Self, FixtureMetadataError> {
162        if limit == 0 {
163            return Err(FixtureMetadataError::RateLimitZero);
164        }
165        if remaining > limit {
166            return Err(FixtureMetadataError::RemainingExceedsLimit);
167        }
168        Ok(Self {
169            limit,
170            remaining,
171            reset_at,
172        })
173    }
174
175    /// Returns the request limit.
176    #[must_use]
177    pub const fn limit(self) -> u64 {
178        self.limit
179    }
180
181    /// Returns the remaining request count.
182    #[must_use]
183    pub const fn remaining(self) -> u64 {
184        self.remaining
185    }
186
187    /// Returns the provider-specific reset timestamp.
188    #[must_use]
189    pub const fn reset_at(self) -> u64 {
190        self.reset_at
191    }
192
193    pub(crate) const fn into_rate_limit(self) -> Result<RateLimit, FixtureMetadataError> {
194        match RateLimit::new(self.limit, self.remaining, self.reset_at) {
195            Ok(value) => Ok(value),
196            Err(_) => Err(FixtureMetadataError::RemainingExceedsLimit),
197        }
198    }
199}