crates/monetize-cloud/src/factory.rs
//! **The registration seam for clouds** — the twin of
//! `monetize_pay::factory`, with two things a vendor's does not have: the
//! spend ceiling the server read from the block (so a declared price table can
//! borrow its currency), and the linked products (so the `server` cloud can
//! measure through the one whose disk it sells).
//!
//! And one flag that decides whether the server demands a ceiling at all:
//! [`CloudFactory::spends`]. A provider that can buy is wrapped in
//! `cap::SpendCap` and refused without a `cap`; one that cannot is wrapped in
//! `cap::CannotSpend`, which holds it to that. The flag is a claim; the wrapper
//! is what makes the claim safe to act on.
use std::path::Path;
use std::sync::Arc;
pub use monetize_pay::factory::Settings;
use monetize_product::Product;
use crate::cap::Ceiling;
use crate::Cloud;
/// What a cloud is built from.
pub struct CloudBuild<'a> {
/// The configured name — the factory's own, echoed so an error can say
/// `[clouds.<name>]`.
pub name: &'a str,
/// The server's data root: the pool ledger, the spend file, the sandbox's
/// iron all live beside the ledger.
pub data_dir: &'a Path,
/// The `[clouds.<name>]` block minus `enabled` and `cap`.
pub settings: Settings<'a>,
/// The ceiling the server will wrap this provider in, if the block declared
/// one. `None` for a provider that cannot spend, or a misconfigured one the
/// server is about to refuse.
pub ceiling: Option<Ceiling>,
/// Every product linked into the server, in config order.
pub products: &'a [Arc<dyn Product>],
}
impl CloudBuild<'_> {
/// `[clouds.<name>] <what>` — the prefix every refusal from a build wears.
pub fn err(&self, what: impl std::fmt::Display) -> String {
format!("[clouds.{}] {what}", self.name)
}
}
pub type CloudBuilder = fn(&CloudBuild<'_>) -> Result<Box<dyn Cloud>, String>;
/// One linkable cloud: its name, its secrets' environment names, whether it
/// can spend, its constructor.
#[derive(Clone, Copy)]
pub struct CloudFactory {
pub name: &'static str,
/// Every environment name `build` may read; forwarded into the unit's 0600
/// env file by the install. Empty for a provider with no account.
pub config_envs: &'static [&'static str],
/// **Can this provider ever buy?** `true` ⇒ the server demands a `cap` and
/// wraps it in `SpendCap`; `false` ⇒ no ceiling, and `CannotSpend` destroys
/// and refuses any `Provisioned` answer it ever gives.
pub spends: bool,
pub build: CloudBuilder,
}
impl std::fmt::Debug for CloudFactory {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("CloudFactory").field("name", &self.name).field("spends", &self.spends).field("config_envs", &self.config_envs).finish_non_exhaustive()
}
}
/// **Register a factory, replacing one of the same name.** Last writer wins:
/// the vetra build registers a `sandbox` that prices off UpCloud's real list
/// over the open `sandbox` that only knows a declared table.
pub fn register(list: &mut Vec<CloudFactory>, factory: CloudFactory) {
match list.iter_mut().find(|f| f.name == factory.name) {
Some(slot) => *slot = factory,
None => list.push(factory),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn register_replaces_by_name_and_the_flag_travels_with_the_replacement() {
fn never(_: &CloudBuild<'_>) -> Result<Box<dyn Cloud>, String> {
Err("never".into())
}
let open = CloudFactory { name: "sandbox", config_envs: &["A"], spends: true, build: never };
let richer = CloudFactory { name: "sandbox", config_envs: &["A", "B"], spends: true, build: never };
let iron = CloudFactory { name: "server", config_envs: &[], spends: false, build: never };
let mut list = Vec::new();
register(&mut list, open);
register(&mut list, iron);
register(&mut list, richer);
assert_eq!(list.iter().map(|f| (f.name, f.spends)).collect::<Vec<_>>(), vec![("sandbox", true), ("server", false)]);
assert_eq!(list[0].config_envs, &["A", "B"]);
let v = serde_json::json!({});
let ctx = CloudBuild { name: "x", data_dir: Path::new("/tmp"), settings: Settings(&v), ceiling: None, products: &[] };
assert_eq!(ctx.err("no"), "[clouds.x] no");
}
}