crates/monetize-pay/src/factory.rs
//! **The registration seam — how a vendor gets into a server without the
//! server knowing its name.**
//!
//! The trait and the server implementation live in monetize itself; the ones
//! that need a paid account live in the private implementation repository. That
//! is what lets the server and the console be run open source by people sharing
//! their servers for free.
//!
//! So the server's registry does not match on `"klarna"` or `"stripe"`; it is
//! handed a list of [`VendorFactory`] values and looks a configured name up in
//! that list. The open binary registers the open vendors (`invoice`,
//! `sandbox`); the vetra build registers the paid ones on top, from a private
//! crate that depends on this one. A name that is enabled in the config and not
//! in the list is refused at boot, by name, exactly as an uncompiled feature was
//! before — rule 4's mechanism, kept.
//!
//! A factory is a `const`: a name, the environment names the vendor reads (so
//! `--install` can carry them into the unit's 0600 env file), and a plain `fn`
//! that builds the vendor from its `[vendors.<name>]` block. No trait, no
//! generics: three fields and a function pointer are the whole contract, and a
//! private crate's `register` is `list.push(FACTORY)`.
use std::path::Path;
use crate::Payment;
/// **The `[vendors.<name>]` block, minus what the server already read.** The
/// server keeps `enabled` and `checkout_url` for itself; every other key in the
/// block arrives here untyped, and the vendor reads the ones it knows. A vendor
/// that needs `environment = "playground"` writes it down in its own docs and
/// reads it from here, and the open config type never grows a vendor's knob.
#[derive(Clone, Copy, Debug)]
pub struct Settings<'a>(pub &'a serde_json::Value);
impl<'a> Settings<'a> {
/// One key, typed, or `None` when absent. A key that IS there and does not
/// parse is an error naming the key — a typo in a config is not "unset".
pub fn get<T: serde::de::DeserializeOwned>(&self, key: &str) -> Result<Option<T>, String> {
match self.0.get(key) {
None | Some(serde_json::Value::Null) => Ok(None),
Some(v) => serde_json::from_value(v.clone()).map(Some).map_err(|e| format!("`{key}` does not parse: {e}")),
}
}
/// One key that must be there.
pub fn require<T: serde::de::DeserializeOwned>(&self, key: &str) -> Result<T, String> {
self.get(key)?.ok_or_else(|| format!("`{key}` is required and not set"))
}
/// The whole block, for an impl that has its own settings struct.
pub fn parse<T: serde::de::DeserializeOwned>(&self) -> Result<T, String> {
serde_json::from_value(self.0.clone()).map_err(|e| format!("settings do not parse: {e}"))
}
}
/// What a vendor is built from.
pub struct VendorBuild<'a> {
/// The configured INSTANCE id, `<kind>/<instance>` (`stripe/gunnar`, from
/// `[vendors.stripe.gunnar]`); the kind is the factory's own name.
pub name: &'a str,
/// The server's data root, for a vendor that keeps state beside the
/// ledger (the sandbox's charges file).
pub data_dir: &'a Path,
pub settings: Settings<'a>,
}
impl VendorBuild<'_> {
/// `[vendors.<name>] <what>` — the prefix every refusal from a build wears.
pub fn err(&self, what: impl std::fmt::Display) -> String {
format!("{} {what}", crate::instance::block_of(self.name))
}
/// `stripe` of `stripe/gunnar`.
pub fn kind(&self) -> &str {
crate::instance::kind_of(self.name)
}
/// `gunnar` of `stripe/gunnar`; empty for a name with no instance.
pub fn instance(&self) -> &str {
self.name.split_once('/').map_or("", |(_, i)| i)
}
/// One of the factory's `config_envs`, expanded for this instance:
/// `MONETIZE_STRIPE_{INSTANCE}_SECRET_KEY` → `MONETIZE_STRIPE_GUNNAR_SECRET_KEY`.
pub fn env_name(&self, template: &str) -> String {
crate::instance::expand_env(template, self.instance())
}
}
pub type VendorBuilder = fn(&VendorBuild<'_>) -> Result<Box<dyn Payment>, String>;
/// One linkable vendor: its name, its secrets' environment names, its constructor.
#[derive(Clone, Copy)]
pub struct VendorFactory {
pub name: &'static str,
/// Every environment name `build` may read. The install forwards exactly
/// these into the unit's 0600 `EnvironmentFile=`; a vendor that is not in
/// the list cannot put a name into the unit. A name may carry
/// [`crate::instance::INSTANCE_PLACEHOLDER`] (`MONETIZE_STRIPE_{INSTANCE}_SECRET_KEY`):
/// it is expanded once per configured instance of the kind.
pub config_envs: &'static [&'static str],
pub build: VendorBuilder,
}
impl std::fmt::Debug for VendorFactory {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("VendorFactory").field("name", &self.name).field("config_envs", &self.config_envs).finish_non_exhaustive()
}
}
/// **What the SIGNUP SINK is built from.** The same two things a vendor gets —
/// the server's data root and ONE product's `[signups.<product>]` block, its
/// `product` set from the block's name — because a sink that has to talk to a product
/// needs a credential path and a socket exactly as a vendor does.
pub struct SignupBuild<'a> {
/// The registered sink's own name, echoed so an error can say which
/// `[signups.<product>]` and which impl refused.
pub name: &'a str,
pub data_dir: &'a Path,
pub settings: Settings<'a>,
}
impl SignupBuild<'_> {
/// `[signups.<product>] (<impl>) <what>` — the prefix a sink's build refusal wears.
pub fn err(&self, what: impl std::fmt::Display) -> String {
let product = self.settings.0.get("product").and_then(|p| p.as_str()).unwrap_or("?");
format!("[signups.{product}] ({}) {what}", self.name)
}
}
pub type SignupBuilder = fn(&SignupBuild<'_>) -> Result<std::sync::Arc<dyn crate::SignupSink>, String>;
/// **The one linkable signup sink**, in the shape [`VendorFactory`] has, for
/// the reason that one has it: the open server must not name the private impl.
///
/// Unlike vendors there is at most ONE linked kind, so [`crate::signup`]'s seam
/// is a single slot on the server's `Linked` rather than a list, and
/// registering a second sink replaces the first. That is the same "last writer
/// wins" rule [`register`] applies, stated in a place where it cannot be
/// misread as "both run". Each `[signups.<product>]` block with no `guest`
/// builds its OWN sink from it, with `product` set to the block's name; a
/// linked sink that serves one product refuses another's block at start.
#[derive(Clone, Copy)]
pub struct SignupFactory {
pub name: &'static str,
/// Every environment name `build` may read, so `--install` can carry the
/// sink's credential into the unit's 0600 `EnvironmentFile=`. A sink that
/// is not linked cannot put a name into the unit.
pub config_envs: &'static [&'static str],
pub build: SignupBuilder,
}
impl std::fmt::Debug for SignupFactory {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("SignupFactory").field("name", &self.name).field("config_envs", &self.config_envs).finish_non_exhaustive()
}
}
/// **Register a factory, replacing one of the same name.** Last writer wins,
/// deliberately: a build that links a richer implementation of an open name
/// registers after the open set and takes the name over.
pub fn register(list: &mut Vec<VendorFactory>, factory: VendorFactory) {
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 settings_read_typed_keys_and_name_the_key_that_does_not_parse() {
let v = serde_json::json!({ "zone": "se-sto1", "pool_gib": 100, "prices": { "disk_minor_per_gib_month": 23 } });
let s = Settings(&v);
assert_eq!(s.get::<String>("zone").unwrap().as_deref(), Some("se-sto1"));
assert_eq!(s.get::<u64>("pool_gib").unwrap(), Some(100));
assert_eq!(s.get::<u64>("absent").unwrap(), None);
let err = s.get::<u64>("zone").unwrap_err();
assert!(err.starts_with("`zone` does not parse"), "{err}");
let err = s.require::<u64>("absent").unwrap_err();
assert!(err.contains("`absent` is required"), "{err}");
#[derive(serde::Deserialize)]
struct Prices {
disk_minor_per_gib_month: u64,
}
assert_eq!(s.get::<Prices>("prices").unwrap().unwrap().disk_minor_per_gib_month, 23);
}
/// Registration replaces by name and keeps order otherwise — the property
/// the vetra build's "sandbox with measured prices" overlay stands on.
#[test]
fn register_replaces_a_factory_of_the_same_name_and_appends_a_new_one() {
fn never(_: &VendorBuild<'_>) -> Result<Box<dyn Payment>, String> {
Err("never".into())
}
let a = VendorFactory { name: "a", config_envs: &["A_ONE"], build: never };
let a2 = VendorFactory { name: "a", config_envs: &["A_TWO"], build: never };
let b = VendorFactory { name: "b", config_envs: &[], build: never };
let mut list = Vec::new();
register(&mut list, a);
register(&mut list, b);
register(&mut list, a2);
assert_eq!(list.iter().map(|f| f.name).collect::<Vec<_>>(), vec!["a", "b"]);
assert_eq!(list[0].config_envs, &["A_TWO"], "the later registration took the name over");
}
}