use std::cmp::Ordering;
use std::fmt;
use std::hash::{Hash, Hasher};
use std::marker::PhantomData;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum IdSource {
Provider,
Derived(&'static [&'static str]),
}
pub trait IdKind {
const NAMES: &'static str;
}
pub mod kind {
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct Payment;
impl super::IdKind for Payment {
const NAMES: &'static str = "payment";
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct Instrument;
impl super::IdKind for Instrument {
const NAMES: &'static str = "saved instrument";
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct Refund;
impl super::IdKind for Refund {
const NAMES: &'static str = "refund";
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct Event;
impl super::IdKind for Event {
const NAMES: &'static str = "webhook event";
}
}
pub struct Id<K: IdKind> {
key: Box<str>,
source: IdSource,
kind: PhantomData<K>,
}
impl<K: IdKind> Id<K> {
pub fn issued(value: impl Into<Box<str>>) -> Self {
Self {
key: value.into(),
source: IdSource::Provider,
kind: PhantomData,
}
}
pub fn derived(value: impl Into<Box<str>>, from: &'static [&'static str]) -> Self {
Self {
key: value.into(),
source: IdSource::Derived(from),
kind: PhantomData,
}
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.key
}
#[must_use]
pub const fn source(&self) -> IdSource {
self.source
}
}
pub type PaymentId = Id<kind::Payment>;
pub type RefundId = Id<kind::Refund>;
pub type EventId = Id<kind::Event>;
pub type InstrumentId = Id<kind::Instrument>;
impl<K: IdKind> fmt::Debug for Id<K> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Id")
.field("names", &K::NAMES)
.field("key", &self.key)
.field("source", &self.source)
.finish()
}
}
impl<K: IdKind> fmt::Display for Id<K> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.key)
}
}
impl<K: IdKind> Clone for Id<K> {
fn clone(&self) -> Self {
Self {
key: self.key.clone(),
source: self.source,
kind: PhantomData,
}
}
}
impl<K: IdKind> PartialEq for Id<K> {
fn eq(&self, other: &Self) -> bool {
self.key == other.key && self.source == other.source
}
}
impl<K: IdKind> Eq for Id<K> {}
impl<K: IdKind> Hash for Id<K> {
fn hash<H: Hasher>(&self, state: &mut H) {
self.key.hash(state);
self.source.hash(state);
}
}
impl<K: IdKind> PartialOrd for Id<K> {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl<K: IdKind> Ord for Id<K> {
fn cmp(&self, other: &Self) -> Ordering {
self.key
.cmp(&other.key)
.then_with(|| self.source.cmp(&other.source))
}
}
#[cfg(test)]
mod tests {
use super::PaymentId;
#[test]
fn an_identifier_we_composed_is_not_one_the_provider_issued() {
let issued = PaymentId::issued("ord-1");
let composed = PaymentId::derived("ord-1", &["merchant_oid"]);
assert_eq!(issued.as_str(), composed.as_str());
assert_ne!(issued, composed);
assert_ne!(issued.source(), composed.source());
}
#[test]
fn debug_says_what_the_identifier_names() {
let shown = format!("{:?}", PaymentId::issued("pi_1"));
assert!(shown.contains("payment"), "{shown}");
assert!(shown.contains("pi_1"), "{shown}");
}
}