pub mod cli;
pub mod client;
pub mod config;
pub mod contract;
mod encoding;
pub mod error;
pub mod generated;
pub mod transport;
pub use client::{
Configured, FerriskeySdk, FerriskeySdkBuilder, OperationCall, OperationInput,
OperationInputBuilder, SdkExt, TagClient, Unconfigured,
};
pub use config::{AuthStrategy, AuthStrategyExt, BaseUrlSet, SdkConfig, SdkConfigBuilder};
pub use encoding::DecodedResponse;
pub use error::{SdkError, TransportError};
pub use generated::{OPERATION_COUNT, OPERATION_DESCRIPTORS, PATH_COUNT, SCHEMA_COUNT, TAG_NAMES};
pub use tower;
pub use transport::{
HpxTransport, MethodSet, PathSet, SdkRequest, SdkRequestBuilder, SdkResponse, Transport,
TransportExt,
};
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct CartItem {
pub name: String,
pub price_cents: u32,
pub quantity: u32,
}
impl CartItem {
#[must_use]
pub fn new(name: impl Into<String>, price_cents: u32, quantity: u32) -> Self {
Self { name: name.into(), price_cents, quantity }
}
#[must_use]
pub const fn total_cents(&self) -> u32 {
self.price_cents * self.quantity
}
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct Cart {
pub items: Vec<CartItem>,
}
impl Cart {
#[must_use]
pub const fn new() -> Self {
Self { items: Vec::new() }
}
pub fn add_item(&mut self, item: CartItem) {
self.items.push(item);
}
#[must_use]
pub fn total_cents(&self) -> u32 {
self.items.iter().map(CartItem::total_cents).sum()
}
#[must_use]
pub const fn is_empty(&self) -> bool {
self.items.is_empty()
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Order {
pub items: Vec<CartItem>,
pub total_cents: u32,
}
impl Order {
#[must_use]
pub fn from_items(items: Vec<CartItem>) -> Self {
let total_cents = items.iter().map(CartItem::total_cents).sum();
Self { items, total_cents }
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct CheckoutResult {
pub order: Order,
pub cart: Cart,
}
#[must_use]
pub fn greeting(name: &str) -> String {
format!("Hello, {name}!")
}
#[must_use]
pub fn checkout_cart(items: &[CartItem]) -> CheckoutResult {
CheckoutResult { order: Order::from_items(items.to_vec()), cart: Cart::new() }
}
pub mod prelude {
pub use crate::{
AuthStrategy,
AuthStrategyExt,
Cart,
CartItem,
CheckoutResult,
FerriskeySdk,
HpxTransport,
OperationInput,
OperationInputBuilder,
Order,
SdkConfig,
SdkError,
SdkRequest,
SdkResponse,
Transport,
TransportError,
TransportExt,
checkout_cart,
greeting,
};
}
#[cfg(test)]
mod tests {
use std::{
collections::BTreeMap,
future::Future,
pin::Pin,
sync::{Arc, Mutex},
};
use proptest::prelude::*;
use serde::Deserialize;
use tower::Service;
use crate::{
AuthStrategy, CartItem, FerriskeySdk, SdkConfig, SdkError, SdkRequest, SdkResponse,
TransportError, checkout_cart, greeting,
};
#[derive(Clone, Debug)]
struct MockTransport {
captured_requests: Arc<Mutex<Vec<SdkRequest>>>,
response: SdkResponse,
}
impl MockTransport {
fn new(response: SdkResponse) -> Self {
Self { captured_requests: Arc::new(Mutex::new(Vec::new())), response }
}
fn captured_requests(&self) -> Vec<SdkRequest> {
self.captured_requests
.lock()
.expect("captured requests mutex should not be poisoned")
.clone()
}
}
impl Service<SdkRequest> for MockTransport {
type Response = SdkResponse;
type Error = TransportError;
type Future = Pin<Box<dyn Future<Output = Result<SdkResponse, TransportError>> + Send>>;
fn poll_ready(
&mut self,
_cx: &mut std::task::Context<'_>,
) -> std::task::Poll<Result<(), Self::Error>> {
std::task::Poll::Ready(Ok(()))
}
fn call(&mut self, request: SdkRequest) -> Self::Future {
let captured_requests = Arc::clone(&self.captured_requests);
let response = self.response.clone();
Box::pin(async move {
captured_requests
.lock()
.expect("captured requests mutex should not be poisoned")
.push(request);
Ok(response)
})
}
}
fn successful_response(body: impl Into<Vec<u8>>) -> SdkResponse {
SdkResponse { body: body.into(), headers: BTreeMap::new(), status: 200 }
}
fn cart_item_strategy() -> impl Strategy<Value = CartItem> {
("[A-Za-z][A-Za-z0-9 ]{0,15}", 0_u16..10_000, 0_u16..100).prop_map(
|(name, price_cents, quantity)| {
CartItem::new(name, u32::from(price_cents), u32::from(quantity))
},
)
}
#[test]
fn greeting_builds_message() {
assert_eq!(greeting("Rust"), "Hello, Rust!");
}
#[test]
fn checkout_cart_creates_an_order_and_clears_the_cart() {
let result = checkout_cart(&[CartItem::new("Tea", 450, 2), CartItem::new("Cake", 350, 1)]);
assert_eq!(result.order.total_cents, 1250);
assert!(result.cart.items.is_empty());
}
#[test]
fn cart_item_total_calculation() {
let item = CartItem::new("Widget", 1000, 3);
assert_eq!(item.total_cents(), 3000);
}
#[tokio::test]
async fn transport_and_auth_core_injects_bearer_header() {
let transport = MockTransport::new(successful_response(br#"{"ok":true}"#.to_vec()));
let sdk = FerriskeySdk::new(
SdkConfig::new(
"https://api.ferriskey.test",
AuthStrategy::Bearer("secret-token".to_string()),
),
transport.clone(),
);
let mut request = SdkRequest::new("GET", "/realms/test");
request.requires_auth = true;
let response = sdk.execute(request).await.expect("request should succeed");
let captured_requests = transport.captured_requests();
assert_eq!(response.status, 200);
assert_eq!(captured_requests.len(), 1);
assert_eq!(
captured_requests[0].headers.get("authorization"),
Some(&"Bearer secret-token".to_string()),
);
assert_eq!(captured_requests[0].path, "https://api.ferriskey.test/realms/test");
}
#[tokio::test]
async fn transport_and_auth_core_rejects_missing_auth() {
let transport = MockTransport::new(successful_response(Vec::new()));
let sdk = FerriskeySdk::new(
SdkConfig::new("https://api.ferriskey.test", AuthStrategy::None),
transport.clone(),
);
let mut request = SdkRequest::new("GET", "/realms/secured");
request.requires_auth = true;
let error = sdk.execute(request).await.expect_err("missing auth should fail");
assert!(matches!(error, SdkError::MissingAuth));
assert!(transport.captured_requests().is_empty());
}
#[derive(Debug, Deserialize, PartialEq)]
struct ExamplePayload {
ok: bool,
}
#[tokio::test]
async fn transport_and_auth_core_reports_status_body_mismatch() {
let transport = MockTransport::new(successful_response(br#"not-json"#.to_vec()));
let sdk = FerriskeySdk::new(
SdkConfig::new("https://api.ferriskey.test", AuthStrategy::None),
transport,
);
let request = SdkRequest::new("GET", "/realms/test");
let error = sdk
.execute_json::<ExamplePayload>(request, 200)
.await
.expect_err("invalid JSON should fail decoding");
assert!(matches!(error, SdkError::Decode(_)));
}
proptest! {
#[test]
fn checkout_cart_preserves_generated_items(
items in proptest::collection::vec(cart_item_strategy(), 0..16),
) {
let result = checkout_cart(&items);
prop_assert_eq!(result.order.items, items);
prop_assert!(result.cart.items.is_empty());
}
#[test]
fn checkout_cart_total_matches_generated_line_items(
items in proptest::collection::vec(cart_item_strategy(), 0..16),
) {
let expected_total: u32 = items.iter().map(CartItem::total_cents).sum();
let result = checkout_cart(&items);
prop_assert_eq!(result.order.total_cents, expected_total);
}
}
}