use crate::WalletError;
use chrono::{DateTime, Utc};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PassKind {
EventTicket,
BoardingPass(TransitType),
Generic,
Coupon,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TransitType {
Air,
Boat,
Bus,
Generic,
Train,
}
impl TransitType {
pub(crate) fn as_apple_str(&self) -> &'static str {
match self {
TransitType::Air => "PKTransitTypeAir",
TransitType::Boat => "PKTransitTypeBoat",
TransitType::Bus => "PKTransitTypeBus",
TransitType::Generic => "PKTransitTypeGeneric",
TransitType::Train => "PKTransitTypeTrain",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum FieldAlignment {
Left,
Center,
Right,
Natural,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TextColorMode {
Auto,
Light,
Dark,
}
#[derive(Debug, Clone)]
pub struct Field {
pub key: String,
pub label: String,
pub value: String,
pub alignment: FieldAlignment,
}
#[derive(Debug, Clone)]
pub struct Branding {
pub organization_name: Option<String>,
pub logo_text: Option<String>,
pub background_color: RgbColor,
pub text_color_mode: TextColorMode,
pub logo_png_bytes: Option<Vec<u8>>,
pub icon_png_bytes: Option<Vec<u8>>,
pub hero_png_bytes: Option<Vec<u8>>,
}
#[derive(Debug, Clone)]
pub struct GeoPoint {
pub latitude: f64,
pub longitude: f64,
pub relevant_text: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct RgbColor {
pub r: u8,
pub g: u8,
pub b: u8,
}
impl RgbColor {
pub fn from_hex(s: &str) -> Result<Self, WalletError> {
let hex = s.strip_prefix('#').unwrap_or(s);
if hex.len() != 6 {
return Err(WalletError::InvalidInput(format!(
"rgb hex must be 6 chars (with optional leading '#'): got {s:?}"
)));
}
let parse = |range: std::ops::Range<usize>| -> Result<u8, WalletError> {
u8::from_str_radix(&hex[range], 16)
.map_err(|e| WalletError::InvalidInput(format!("rgb hex parse: {e}")))
};
Ok(RgbColor {
r: parse(0..2)?,
g: parse(2..4)?,
b: parse(4..6)?,
})
}
pub fn css_rgb(&self) -> String {
format!("rgb({},{},{})", self.r, self.g, self.b)
}
}
pub fn auto_foreground(bg: RgbColor) -> RgbColor {
let r = bg.r as f64 / 255.0;
let g = bg.g as f64 / 255.0;
let b = bg.b as f64 / 255.0;
let lum = 0.299 * r + 0.587 * g + 0.114 * b;
if lum < 0.5 {
RgbColor {
r: 255,
g: 255,
b: 255,
}
} else {
RgbColor {
r: 17,
g: 24,
b: 39,
}
}
}
pub trait WalletSubject {
fn pass_kind(&self) -> PassKind;
fn serial(&self) -> String;
fn primary(&self) -> Vec<Field>;
fn secondary(&self) -> Vec<Field>;
fn auxiliary(&self) -> Vec<Field>;
fn back(&self) -> Vec<Field>;
fn barcode_token(&self) -> String;
fn relevant_at(&self) -> Option<DateTime<Utc>>;
fn expires_at(&self) -> Option<DateTime<Utc>>;
fn locations(&self) -> Vec<GeoPoint>;
fn branding(&self) -> Branding;
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rgb_from_hex() {
assert_eq!(
RgbColor::from_hex("#ffffff").unwrap(),
RgbColor {
r: 255,
g: 255,
b: 255
}
);
assert_eq!(
RgbColor::from_hex("000000").unwrap(),
RgbColor { r: 0, g: 0, b: 0 }
);
assert_eq!(
RgbColor::from_hex("#FF8000").unwrap(),
RgbColor {
r: 255,
g: 128,
b: 0
}
);
assert_eq!(
RgbColor::from_hex("#aAbBcC").unwrap(),
RgbColor {
r: 0xaa,
g: 0xbb,
b: 0xcc
}
);
}
#[test]
fn rgb_from_hex_rejects_malformed() {
let err = RgbColor::from_hex("not-a-color").unwrap_err();
assert!(matches!(err, WalletError::InvalidInput(_)));
let err = RgbColor::from_hex("#fff").unwrap_err();
assert!(matches!(err, WalletError::InvalidInput(_)));
let err = RgbColor::from_hex("#fffffff").unwrap_err();
assert!(matches!(err, WalletError::InvalidInput(_)));
let err = RgbColor::from_hex("#zzzzzz").unwrap_err();
assert!(matches!(err, WalletError::InvalidInput(_)));
let err = RgbColor::from_hex("").unwrap_err();
assert!(matches!(err, WalletError::InvalidInput(_)));
}
#[test]
fn auto_foreground_dark_bg_is_white() {
assert_eq!(
auto_foreground(RgbColor { r: 0, g: 0, b: 0 }),
RgbColor {
r: 255,
g: 255,
b: 255
}
);
}
#[test]
fn auto_foreground_light_bg_is_dark_slate() {
assert_eq!(
auto_foreground(RgbColor {
r: 255,
g: 255,
b: 255
}),
RgbColor {
r: 17,
g: 24,
b: 39
}
);
}
#[test]
fn rgb_css_rgb_format() {
assert_eq!(
RgbColor {
r: 17,
g: 24,
b: 39
}
.css_rgb(),
"rgb(17,24,39)"
);
}
}