1pub mod cli;
59pub mod client;
60pub mod config;
61pub mod contract;
63mod encoding;
64pub mod error;
65pub mod generated;
66pub mod transport;
67
68pub use client::{
70 Configured, FerriskeySdk, FerriskeySdkBuilder, OperationCall, OperationInput,
71 OperationInputBuilder, SdkExt, TagClient, Unconfigured,
72};
73pub use config::{AuthStrategy, AuthStrategyExt, BaseUrlSet, SdkConfig, SdkConfigBuilder};
74pub use encoding::DecodedResponse;
75pub use error::{SdkError, TransportError};
76pub use generated::{OPERATION_COUNT, OPERATION_DESCRIPTORS, PATH_COUNT, SCHEMA_COUNT, TAG_NAMES};
77pub use tower;
79pub use transport::{
80 HpxTransport, MethodSet, PathSet, SdkRequest, SdkRequestBuilder, SdkResponse, Transport,
81 TransportExt,
82};
83
84#[derive(Clone, Debug, Eq, PartialEq)]
91pub struct CartItem {
92 pub name: String,
94 pub price_cents: u32,
96 pub quantity: u32,
98}
99
100impl CartItem {
101 #[must_use]
103 pub fn new(name: impl Into<String>, price_cents: u32, quantity: u32) -> Self {
104 Self { name: name.into(), price_cents, quantity }
105 }
106
107 #[must_use]
109 pub const fn total_cents(&self) -> u32 {
110 self.price_cents * self.quantity
111 }
112}
113
114#[derive(Clone, Debug, Default, Eq, PartialEq)]
121pub struct Cart {
122 pub items: Vec<CartItem>,
124}
125
126impl Cart {
127 #[must_use]
129 pub const fn new() -> Self {
130 Self { items: Vec::new() }
131 }
132
133 pub fn add_item(&mut self, item: CartItem) {
135 self.items.push(item);
136 }
137
138 #[must_use]
140 pub fn total_cents(&self) -> u32 {
141 self.items.iter().map(CartItem::total_cents).sum()
142 }
143
144 #[must_use]
146 pub const fn is_empty(&self) -> bool {
147 self.items.is_empty()
148 }
149}
150
151#[derive(Clone, Debug, Eq, PartialEq)]
153pub struct Order {
154 pub items: Vec<CartItem>,
156 pub total_cents: u32,
158}
159
160impl Order {
161 #[must_use]
163 pub fn from_items(items: Vec<CartItem>) -> Self {
164 let total_cents = items.iter().map(CartItem::total_cents).sum();
165 Self { items, total_cents }
166 }
167}
168
169#[derive(Clone, Debug, Eq, PartialEq)]
171pub struct CheckoutResult {
172 pub order: Order,
174 pub cart: Cart,
176}
177
178#[must_use]
180pub fn greeting(name: &str) -> String {
181 format!("Hello, {name}!")
182}
183
184#[must_use]
191pub fn checkout_cart(items: &[CartItem]) -> CheckoutResult {
192 CheckoutResult { order: Order::from_items(items.to_vec()), cart: Cart::new() }
193}
194
195pub mod prelude {
202 pub use crate::{
203 AuthStrategy,
205 AuthStrategyExt,
206 Cart,
207 CartItem,
208 CheckoutResult,
209 FerriskeySdk,
211 HpxTransport,
213 OperationInput,
214 OperationInputBuilder,
215 Order,
216 SdkConfig,
217 SdkError,
219 SdkRequest,
220 SdkResponse,
221 Transport,
222 TransportError,
223 TransportExt,
224 checkout_cart,
226 greeting,
227 };
228}
229
230#[cfg(test)]
231mod tests {
232 use std::{
233 collections::BTreeMap,
234 future::Future,
235 pin::Pin,
236 sync::{Arc, Mutex},
237 };
238
239 use proptest::prelude::*;
240 use serde::Deserialize;
241 use tower::Service;
242
243 use crate::{
244 AuthStrategy, CartItem, FerriskeySdk, SdkConfig, SdkError, SdkRequest, SdkResponse,
245 TransportError, checkout_cart, greeting,
246 };
247
248 #[derive(Clone, Debug)]
249 struct MockTransport {
250 captured_requests: Arc<Mutex<Vec<SdkRequest>>>,
251 response: SdkResponse,
252 }
253
254 impl MockTransport {
255 fn new(response: SdkResponse) -> Self {
256 Self { captured_requests: Arc::new(Mutex::new(Vec::new())), response }
257 }
258
259 fn captured_requests(&self) -> Vec<SdkRequest> {
260 self.captured_requests
261 .lock()
262 .expect("captured requests mutex should not be poisoned")
263 .clone()
264 }
265 }
266
267 impl Service<SdkRequest> for MockTransport {
270 type Response = SdkResponse;
271 type Error = TransportError;
272 type Future = Pin<Box<dyn Future<Output = Result<SdkResponse, TransportError>> + Send>>;
273
274 fn poll_ready(
275 &mut self,
276 _cx: &mut std::task::Context<'_>,
277 ) -> std::task::Poll<Result<(), Self::Error>> {
278 std::task::Poll::Ready(Ok(()))
279 }
280
281 fn call(&mut self, request: SdkRequest) -> Self::Future {
282 let captured_requests = Arc::clone(&self.captured_requests);
283 let response = self.response.clone();
284
285 Box::pin(async move {
286 captured_requests
287 .lock()
288 .expect("captured requests mutex should not be poisoned")
289 .push(request);
290 Ok(response)
291 })
292 }
293 }
294
295 fn successful_response(body: impl Into<Vec<u8>>) -> SdkResponse {
296 SdkResponse { body: body.into(), headers: BTreeMap::new(), status: 200 }
297 }
298
299 fn cart_item_strategy() -> impl Strategy<Value = CartItem> {
300 ("[A-Za-z][A-Za-z0-9 ]{0,15}", 0_u16..10_000, 0_u16..100).prop_map(
301 |(name, price_cents, quantity)| {
302 CartItem::new(name, u32::from(price_cents), u32::from(quantity))
303 },
304 )
305 }
306
307 #[test]
308 fn greeting_builds_message() {
309 assert_eq!(greeting("Rust"), "Hello, Rust!");
310 }
311
312 #[test]
313 fn checkout_cart_creates_an_order_and_clears_the_cart() {
314 let result = checkout_cart(&[CartItem::new("Tea", 450, 2), CartItem::new("Cake", 350, 1)]);
315
316 assert_eq!(result.order.total_cents, 1250);
317 assert!(result.cart.items.is_empty());
318 }
319
320 #[test]
321 fn cart_item_total_calculation() {
322 let item = CartItem::new("Widget", 1000, 3);
323 assert_eq!(item.total_cents(), 3000);
324 }
325
326 #[tokio::test]
327 async fn transport_and_auth_core_injects_bearer_header() {
328 let transport = MockTransport::new(successful_response(br#"{"ok":true}"#.to_vec()));
329 let sdk = FerriskeySdk::new(
330 SdkConfig::new(
331 "https://api.ferriskey.test",
332 AuthStrategy::Bearer("secret-token".to_string()),
333 ),
334 transport.clone(),
335 );
336 let mut request = SdkRequest::new("GET", "/realms/test");
337 request.requires_auth = true;
338
339 let response = sdk.execute(request).await.expect("request should succeed");
340 let captured_requests = transport.captured_requests();
341
342 assert_eq!(response.status, 200);
343 assert_eq!(captured_requests.len(), 1);
344 assert_eq!(
345 captured_requests[0].headers.get("authorization"),
346 Some(&"Bearer secret-token".to_string()),
347 );
348 assert_eq!(captured_requests[0].path, "https://api.ferriskey.test/realms/test");
349 }
350
351 #[tokio::test]
352 async fn transport_and_auth_core_rejects_missing_auth() {
353 let transport = MockTransport::new(successful_response(Vec::new()));
354 let sdk = FerriskeySdk::new(
355 SdkConfig::new("https://api.ferriskey.test", AuthStrategy::None),
356 transport.clone(),
357 );
358 let mut request = SdkRequest::new("GET", "/realms/secured");
359 request.requires_auth = true;
360
361 let error = sdk.execute(request).await.expect_err("missing auth should fail");
362
363 assert!(matches!(error, SdkError::MissingAuth));
364 assert!(transport.captured_requests().is_empty());
365 }
366
367 #[derive(Debug, Deserialize, PartialEq)]
368 struct ExamplePayload {
369 ok: bool,
370 }
371
372 #[tokio::test]
373 async fn transport_and_auth_core_reports_status_body_mismatch() {
374 let transport = MockTransport::new(successful_response(br#"not-json"#.to_vec()));
375 let sdk = FerriskeySdk::new(
376 SdkConfig::new("https://api.ferriskey.test", AuthStrategy::None),
377 transport,
378 );
379 let request = SdkRequest::new("GET", "/realms/test");
380
381 let error = sdk
382 .execute_json::<ExamplePayload>(request, 200)
383 .await
384 .expect_err("invalid JSON should fail decoding");
385
386 assert!(matches!(error, SdkError::Decode(_)));
387 }
388
389 proptest! {
390 #[test]
391 fn checkout_cart_preserves_generated_items(
392 items in proptest::collection::vec(cart_item_strategy(), 0..16),
393 ) {
394 let result = checkout_cart(&items);
395
396 prop_assert_eq!(result.order.items, items);
397 prop_assert!(result.cart.items.is_empty());
398 }
399
400 #[test]
401 fn checkout_cart_total_matches_generated_line_items(
402 items in proptest::collection::vec(cart_item_strategy(), 0..16),
403 ) {
404 let expected_total: u32 = items.iter().map(CartItem::total_cents).sum();
405 let result = checkout_cart(&items);
406
407 prop_assert_eq!(result.order.total_cents, expected_total);
408 }
409 }
410}