use chrono::{DateTime, Utc};
use is_empty::IsEmpty;
use serde::{Deserialize, Serialize};
use self::barcode::Barcode;
use self::beacon::Beacon;
use self::location::Location;
use self::nfc::NFC;
use self::semantic_tags::SemanticTags;
use self::visual_appearance::VisualAppearance;
use self::web_service::WebService;
pub mod barcode;
pub mod beacon;
mod date_format;
pub mod fields;
pub mod location;
pub mod nfc;
pub mod semantic_tags;
pub mod visual_appearance;
pub mod web_service;
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct PassConfig {
pub organization_name: String,
pub description: String,
pub pass_type_identifier: String,
pub team_identifier: String,
pub serial_number: String,
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct Pass {
format_version: u32,
#[serde(flatten)]
pub config: PassConfig,
#[serde(default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub grouping_identifier: Option<String>,
#[serde(default)]
#[serde(flatten)]
#[serde(skip_serializing_if = "Option::is_none")]
pub appearance: Option<VisualAppearance>,
#[serde(default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub logo_text: Option<String>,
#[serde(default)]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(with = "date_format")]
pub relevant_date: Option<DateTime<Utc>>,
#[serde(default)]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(with = "date_format")]
pub expiration_date: Option<DateTime<Utc>>,
#[serde(default)]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "appLaunchURL")]
pub app_launch_url: Option<String>,
#[serde(default)]
#[serde(skip_serializing_if = "Vec::is_empty")]
pub associated_store_identifiers: Vec<i32>,
#[serde(default)]
#[serde(flatten)]
#[serde(skip_serializing_if = "Option::is_none")]
pub web_service: Option<WebService>,
#[serde(default)]
#[serde(skip_serializing_if = "_is_false")]
pub sharing_prohibited: bool,
#[serde(default = "_default_true")]
#[serde(skip_serializing_if = "_is_true")]
pub suppress_strip_shine: bool,
#[serde(default)]
#[serde(skip_serializing_if = "_is_false")]
pub voided: bool,
#[serde(default)]
#[serde(skip_serializing_if = "Vec::is_empty")]
pub barcodes: Vec<Barcode>,
#[serde(default)]
#[serde(skip_serializing_if = "Vec::is_empty")]
pub beacons: Vec<Beacon>,
#[serde(default)]
#[serde(skip_serializing_if = "Vec::is_empty")]
pub locations: Vec<Location>,
#[serde(default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub max_distance: Option<u32>,
#[serde(default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub nfc: Option<NFC>,
#[serde(default)]
#[serde(skip_serializing_if = "SemanticTags::is_empty")]
pub semantics: SemanticTags,
#[serde(flatten)]
pub fields: fields::Type,
}
impl Pass {
pub fn make_json(&self) -> Result<String, serde_json::Error> {
let json = serde_json::to_string_pretty(&self)?;
Ok(json)
}
pub fn from_json(data: &str) -> Result<Self, serde_json::Error> {
let pass: Pass = serde_json::from_str(data)?;
Ok(pass)
}
}
pub struct PassBuilder {
pass: Pass,
}
impl PassBuilder {
pub fn new(config: PassConfig) -> Self {
let pass = Pass {
format_version: 1,
config,
grouping_identifier: None,
appearance: None,
logo_text: None,
relevant_date: None,
expiration_date: None,
app_launch_url: None,
associated_store_identifiers: Vec::new(),
web_service: None,
sharing_prohibited: false,
suppress_strip_shine: true,
voided: false,
barcodes: Vec::new(),
beacons: Vec::new(),
locations: Vec::new(),
max_distance: None,
nfc: None,
semantics: Default::default(),
fields: fields::Type::Generic {
pass_fields: fields::Fields {
..Default::default()
},
},
};
Self { pass }
}
pub fn grouping_identifier(mut self, field: String) -> PassBuilder {
self.pass.grouping_identifier = Some(field);
self
}
pub fn appearance(mut self, field: VisualAppearance) -> PassBuilder {
self.pass.appearance = Some(field);
self
}
pub fn logo_text(mut self, field: String) -> PassBuilder {
self.pass.logo_text = Some(field);
self
}
pub fn relevant_date(mut self, field: DateTime<Utc>) -> PassBuilder {
self.pass.relevant_date = Some(field);
self
}
pub fn expiration_date(mut self, field: DateTime<Utc>) -> PassBuilder {
self.pass.expiration_date = Some(field);
self
}
pub fn app_launch_url(mut self, field: String) -> PassBuilder {
self.pass.app_launch_url = Some(field);
self
}
pub fn add_associated_store_identifier(mut self, id: i32) -> PassBuilder {
self.pass.associated_store_identifiers.push(id);
self
}
pub fn web_service(mut self, web_service: WebService) -> PassBuilder {
self.pass.web_service = Some(web_service);
self
}
pub fn set_sharing_prohibited(mut self, field: bool) -> PassBuilder {
self.pass.sharing_prohibited = field;
self
}
pub fn set_suppress_strip_shine(mut self, field: bool) -> PassBuilder {
self.pass.suppress_strip_shine = field;
self
}
pub fn voided(mut self, field: bool) -> PassBuilder {
self.pass.voided = field;
self
}
pub fn add_barcode(mut self, barcode: Barcode) -> PassBuilder {
self.pass.barcodes.push(barcode);
self
}
pub fn add_beacon(mut self, beacon: Beacon) -> PassBuilder {
self.pass.beacons.push(beacon);
self
}
pub fn add_location(mut self, location: Location) -> PassBuilder {
assert!(
self.pass.locations.len() < 10,
"Reached limit for geographic locations (maximum - 10)"
);
self.pass.locations.push(location);
self
}
pub fn max_distance(mut self, field: u32) -> PassBuilder {
self.pass.max_distance = Some(field);
self
}
pub fn nfc(mut self, field: NFC) -> PassBuilder {
self.pass.nfc = Some(field);
self
}
pub fn semantics(mut self, field: SemanticTags) -> PassBuilder {
self.pass.semantics = field;
self
}
pub fn fields(mut self, field: fields::Type) -> PassBuilder {
self.pass.fields = field;
self
}
pub fn build(self) -> Pass {
self.pass
}
}
#[cfg(test)]
mod tests {
use chrono::prelude::*;
use tests::{fields, semantic_tags::SemanticTagLocation, visual_appearance::Color};
use super::*;
#[test]
fn make_minimal_pass() {
let pass = PassBuilder::new(PassConfig {
organization_name: String::from("Apple inc."),
description: String::from("Example pass"),
pass_type_identifier: String::from("com.example.pass"),
team_identifier: String::from("AA00AA0A0A"),
serial_number: String::from("ABCDEFG1234567890"),
})
.build();
let json = pass.make_json().unwrap();
println!("{}", json);
let json_expected = r#"{
"formatVersion": 1,
"organizationName": "Apple inc.",
"description": "Example pass",
"passTypeIdentifier": "com.example.pass",
"teamIdentifier": "AA00AA0A0A",
"serialNumber": "ABCDEFG1234567890",
"generic": {
"auxiliaryFields": [],
"backFields": [],
"headerFields": [],
"primaryFields": [],
"secondaryFields": []
}
}"#;
assert_eq!(json_expected, json);
let pass: Pass = Pass::from_json(json_expected).unwrap();
let json = pass.make_json().unwrap();
assert_eq!(json_expected, json);
}
#[test]
fn make_pass() {
let pass = PassBuilder::new(PassConfig {
organization_name: String::from("Apple inc."),
description: String::from("Example pass"),
pass_type_identifier: String::from("com.example.pass"),
team_identifier: String::from("AA00AA0A0A"),
serial_number: String::from("ABCDEFG1234567890"),
})
.grouping_identifier(String::from("com.example.pass.app"))
.appearance(VisualAppearance {
label_color: None,
foreground_color: Color::new(250, 10, 10),
background_color: Color::white(),
})
.logo_text(String::from("Test pass"))
.relevant_date(Utc.with_ymd_and_hms(2024, 02, 07, 0, 0, 0).unwrap())
.expiration_date(Utc.with_ymd_and_hms(2024, 02, 08, 0, 0, 0).unwrap())
.app_launch_url(String::from("testapp:param?index=1"))
.add_associated_store_identifier(100)
.web_service(WebService {
authentication_token: String::from("abcdefg01234567890abcdefg"),
web_service_url: String::from("https://example.com/passes/"),
})
.set_sharing_prohibited(false)
.set_suppress_strip_shine(false)
.voided(false)
.add_barcode(Barcode {
message: String::from("Hello world!"),
format: barcode::BarcodeFormat::QR,
alt_text: Some(String::from("test by test")),
..Default::default()
})
.add_beacon(Beacon {
proximity_uuid: String::from("e286373b-15b5-4f4e-bf91-e9e64787724a"),
major: Some(2),
minor: Some(150),
relevant_text: Some(String::from("The simple beacon")),
})
.add_location(Location {
latitude: 37.334606,
longitude: -122.009102,
relevant_text: Some(String::from("Apple Park, Cupertino, CA, USA")),
..Default::default()
})
.max_distance(1000)
.nfc(NFC {
encryption_public_key: String::from("ABCDEFG_0011223344556677889900"),
message: String::from("test message"),
..Default::default()
})
.semantics(SemanticTags {
airline_code: String::from("EX123").into(),
departure_location: SemanticTagLocation {
latitude: 43.3948533,
longitude: 132.1451673,
}
.into(),
..Default::default()
})
.fields(
fields::Type::BoardingPass {
pass_fields: fields::Fields {
..Default::default()
},
transit_type: fields::TransitType::Air,
}
.add_header_field(fields::Content::new(
"serial",
"1122",
fields::ContentOptions {
label: String::from("SERIAL").into(),
..Default::default()
},
))
.add_header_field(fields::Content::new(
"number",
"0011223344",
fields::ContentOptions {
label: String::from("NUMBER").into(),
text_alignment: fields::TextAlignment::Right.into(),
..Default::default()
},
))
.add_primary_field(fields::Content::new(
"from",
"UHWW",
fields::ContentOptions {
label: String::from("FROM").into(),
text_alignment: fields::TextAlignment::Left.into(),
..Default::default()
},
))
.add_primary_field(fields::Content::new(
"to",
"RKSI",
fields::ContentOptions {
label: String::from("TO").into(),
text_alignment: fields::TextAlignment::Right.into(),
..Default::default()
},
))
.add_auxiliary_field(fields::Content::new(
"date_departure",
"20.02.2024",
fields::ContentOptions {
label: String::from("Departure date").into(),
..Default::default()
},
)),
)
.build();
let json = pass.make_json().unwrap();
println!("{}", json);
let json_expected = r#"{
"formatVersion": 1,
"organizationName": "Apple inc.",
"description": "Example pass",
"passTypeIdentifier": "com.example.pass",
"teamIdentifier": "AA00AA0A0A",
"serialNumber": "ABCDEFG1234567890",
"groupingIdentifier": "com.example.pass.app",
"foregroundColor": "rgb(250, 10, 10)",
"backgroundColor": "rgb(255, 255, 255)",
"logoText": "Test pass",
"relevantDate": "2024-02-07T00:00:00+00:00",
"expirationDate": "2024-02-08T00:00:00+00:00",
"appLaunchURL": "testapp:param?index=1",
"associatedStoreIdentifiers": [
100
],
"authenticationToken": "abcdefg01234567890abcdefg",
"webServiceURL": "https://example.com/passes/",
"suppressStripShine": false,
"barcodes": [
{
"message": "Hello world!",
"format": "PKBarcodeFormatQR",
"altText": "test by test",
"messageEncoding": "iso-8859-1"
}
],
"beacons": [
{
"proximityUUID": "e286373b-15b5-4f4e-bf91-e9e64787724a",
"major": 2,
"minor": 150,
"relevantText": "The simple beacon"
}
],
"locations": [
{
"latitude": 37.334606,
"longitude": -122.009102,
"relevantText": "Apple Park, Cupertino, CA, USA"
}
],
"maxDistance": 1000,
"nfc": {
"encryptionPublicKey": "ABCDEFG_0011223344556677889900",
"message": "test message",
"requiresAuthentication": false
},
"semantics": {
"airlineCode": "EX123",
"departureLocation": {
"latitude": 43.3948533,
"longitude": 132.1451673
}
},
"boardingPass": {
"auxiliaryFields": [
{
"key": "date_departure",
"value": "20.02.2024",
"label": "Departure date"
}
],
"backFields": [],
"headerFields": [
{
"key": "serial",
"value": "1122",
"label": "SERIAL"
},
{
"key": "number",
"value": "0011223344",
"label": "NUMBER",
"textAlignment": "PKTextAlignmentRight"
}
],
"primaryFields": [
{
"key": "from",
"value": "UHWW",
"label": "FROM",
"textAlignment": "PKTextAlignmentLeft"
},
{
"key": "to",
"value": "RKSI",
"label": "TO",
"textAlignment": "PKTextAlignmentRight"
}
],
"secondaryFields": [],
"transitType": "PKTransitTypeAir"
}
}"#;
assert_eq!(json_expected, json);
let pass: Pass = Pass::from_json(json_expected).unwrap();
let json = pass.make_json().unwrap();
assert_eq!(json_expected, json);
}
}
fn _is_false(b: &bool) -> bool {
!b
}
fn _is_true(b: &bool) -> bool {
*b
}
const fn _default_true() -> bool {
true
}