crates/monetize-pay/src/shapes.rs
//! ★ **What differs per payment vendor KIND, as data** (A32-15: the core is no vendor's shape).
//!
//! The pay core, the server and the ONE box's site book editor branch on no vendor's name: what a
//! vendor kind's ids say about its mode, and what its two account secrets look like, are this
//! table's rows — one per kind a ONE box may hold an account of. A new kind is a new row (and
//! its signed guest), never a new branch in the core. The rows are the vendors' own published
//! facts (Stripe's `cs_live_` / `cs_test_` ids and `sk_` / `rk_` / `whsec_` secrets).
/// One vendor kind's shape.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Shape {
/// The kind, as an instance names it (`<kind>/<account>`).
pub kind: &'static str,
/// The vendor's word for this kind, for a person ("Stripe").
pub title: &'static str,
/// The marker a LIVE object's id carries (empty: its ids carry none).
pub live_marker: &'static str,
/// The marker a TEST object's id carries.
pub test_marker: &'static str,
/// Play money only: every payment of this kind is test money.
pub always_test: bool,
/// The prefixes an account's secret key may start with (empty: any).
pub key_prefixes: &'static [&'static str],
/// The prefixes an account's webhook secret may start with (empty: any).
pub hook_prefixes: &'static [&'static str],
/// What the secret key is, for a person.
pub key_words: &'static str,
/// What the webhook secret is, for a person.
pub hook_words: &'static str,
}
/// The kinds a ONE box knows the shape of.
pub const SHAPES: &[Shape] = &[
Shape {
kind: "stripe",
title: "Stripe",
live_marker: "_live_",
test_marker: "_test_",
always_test: false,
key_prefixes: &["sk_test_", "sk_live_", "rk_test_", "rk_live_"],
hook_prefixes: &["whsec_"],
key_words: "the account's `sk_…` (or restricted `rk_…`) key",
hook_words: "the endpoint's `whsec_…` signing secret",
},
Shape {
kind: "sandbox",
title: "Sandbox",
live_marker: "",
test_marker: "",
always_test: true,
key_prefixes: &[],
hook_prefixes: &[],
key_words: "its key",
hook_words: "its webhook secret",
},
];
/// The shape of `kind` (an instance's `<kind>/…` or the bare kind), if a ONE box knows it.
pub fn of(kind_or_instance: &str) -> Option<&'static Shape> {
let kind = crate::instance::kind_of(kind_or_instance);
SHAPES.iter().find(|s| s.kind == kind)
}
impl Shape {
/// Pure: **the mode an object's `id` says** — `Some(true)` live, `Some(false)` test, `None`
/// when it carries no marker (never a guess).
pub fn mode_of(&self, id: &str) -> Option<bool> {
if self.always_test {
return Some(false);
}
if !self.live_marker.is_empty() && id.contains(self.live_marker) {
return Some(true);
}
if !self.test_marker.is_empty() && id.contains(self.test_marker) {
return Some(false);
}
None
}
/// Pure: is `secret` a LIVE secret of this kind?
pub fn is_live(&self, secret: &str) -> bool {
!self.always_test && !self.live_marker.is_empty() && secret.contains(self.live_marker)
}
/// Pure: `Ok` when `key` (empty: none given) looks like this kind's secret key.
pub fn key_ok(&self, key: &str) -> bool {
key.is_empty()
|| self.key_prefixes.is_empty()
|| self.key_prefixes.iter().any(|p| key.starts_with(p))
}
/// Pure: `Ok` when `hook` (empty: none given) looks like this kind's webhook secret.
pub fn hook_ok(&self, hook: &str) -> bool {
hook.is_empty()
|| self.hook_prefixes.is_empty()
|| self.hook_prefixes.iter().any(|p| hook.starts_with(p))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_kinds_shape_reads_its_ids_and_its_secrets() {
let s = of("stripe/gunnar").expect("a known kind");
assert_eq!(
(
s.mode_of("cs_live_a1"),
s.mode_of("cs_test_a1"),
s.mode_of("pi_3Nx")
),
(Some(true), Some(false), None)
);
assert!(s.is_live("sk_live_x") && !s.is_live("sk_test_x"));
assert!(s.key_ok("rk_test_1") && !s.key_ok("pk_live_1") && s.key_ok(""));
assert!(s.hook_ok("whsec_1") && !s.hook_ok("sk_test_1"));
assert_eq!(
of("sandbox/play").unwrap().mode_of("sbx-1"),
Some(false),
"play money is never real"
);
assert!(of("invoice").is_none(), "a kind with no row has no shape");
}
}