Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/monetize

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");
    }
}