crates/mock-scaleway/src/catalog.rs
//! **The product catalog** — what `GET /products/servers` and `GET /products/volumes` answer.
//!
//! The server rows carry Scaleway's own field names and units (`ram` in BYTES, prices in EUR
//! excluding VAT as floats, `hourly_price` and `monthly_price`). The numbers are close to the
//! public list of fr-par-1 in October 2026 and are the MOCK's: nothing reads them as a price
//! anybody pays.
//!
//! Volumes: the real `GET /products/volumes` answers each volume type's display name,
//! capabilities and size constraints, and NO price (block storage is priced in the Block
//! Storage and product catalogs). The mock adds `hourly_price_per_gb` to each row so a guest's
//! `price` has one place to read; that one field is the mock's, and the module says so.
use serde_json::{json, Value};
/// `(commercial_type, ncpus, ram GiB, hourly EUR, monthly EUR, arch)`.
pub const SERVERS: &[(&str, u32, u64, f64, f64)] = &[
("DEV1-S", 2, 2, 0.0088, 6.42),
("DEV1-M", 3, 4, 0.0198, 14.45),
("DEV1-L", 4, 8, 0.042, 30.66),
("DEV1-XL", 4, 12, 0.063, 45.99),
("PRO2-XXS", 2, 8, 0.055, 40.15),
("PRO2-XS", 4, 16, 0.11, 80.3),
("PRO2-S", 8, 32, 0.219, 159.87),
("PRO2-M", 16, 64, 0.438, 319.74),
];
/// `(volume_type, display name, hourly EUR per GB, can resize, min bytes, max bytes)`.
pub const VOLUMES: &[(&str, &str, f64, bool, u64, u64)] = &[
(
"l_ssd",
"Local SSD",
0.0,
false,
1_000_000_000,
600_000_000_000,
),
(
"b_ssd",
"Block SSD",
0.000_11,
true,
1_000_000_000,
10_000_000_000_000,
),
(
"sbs_volume",
"Block Storage (SBS)",
0.000_118,
true,
1_000_000_000,
10_000_000_000_000,
),
];
/// The snapshot price per GB-hour (what a backup costs while it is kept).
pub const SNAPSHOT_HOURLY_PER_GB: f64 = 0.000_046_6;
pub const GB: u64 = 1_000_000_000;
pub const GIB: u64 = 1024 * 1024 * 1024;
pub fn server_row(t: &str) -> Option<(u32, u64)> {
SERVERS.iter().find(|r| r.0 == t).map(|r| (r.1, r.2))
}
pub fn volume_row(t: &str) -> Option<&'static (&'static str, &'static str, f64, bool, u64, u64)> {
VOLUMES.iter().find(|r| r.0 == t)
}
/// `GET /products/servers`.
pub fn servers_json() -> Value {
let mut m = serde_json::Map::new();
for (t, cpus, ram, hourly, monthly) in SERVERS {
m.insert(
(*t).to_string(),
json!({
"alt_names": [],
"arch": "x86_64",
"ncpus": cpus,
"ram": ram * GIB,
"gpu": 0,
"baremetal": false,
"hourly_price": hourly,
"monthly_price": monthly,
"volumes_constraint": { "min_size": 0, "max_size": 0 },
"per_volume_constraint": { "l_ssd": { "min_size": 1_000_000_000u64, "max_size": 600_000_000_000u64 } },
"network": { "sum_internal_bandwidth": 400_000_000u64, "sum_internet_bandwidth": 400_000_000u64, "ipv6_support": true, "interfaces": [] },
"block_bandwidth": 1_073_741_824u64,
"end_of_service": false,
}),
);
}
json!({ "servers": m })
}
/// `GET /products/volumes` (plus the mock's own `hourly_price_per_gb`).
pub fn volumes_json() -> Value {
let mut m = serde_json::Map::new();
for (t, name, hourly, resize, min, max) in VOLUMES {
m.insert(
(*t).to_string(),
json!({
"display_name": name,
"capabilities": { "snapshot": true, "block_snapshot": *t != "l_ssd" },
"constraint": { "min": min, "max": max },
"can_resize": resize,
"hourly_price_per_gb": hourly,
}),
);
}
json!({ "volumes": m, "snapshot_hourly_price_per_gb": SNAPSHOT_HOURLY_PER_GB })
}