use chrono::{DateTime, Utc};
use is_empty::IsEmpty;
use serde::{Deserialize, Serialize};
use super::semantic_tags::SemanticTags;
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct Fields {
#[serde(skip_serializing_if = "Option::is_none")]
pub auxiliary_fields: Option<Vec<Content>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub back_fields: Option<Vec<Content>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub header_fields: Option<Vec<Content>>,
pub primary_fields: Vec<Content>,
#[serde(skip_serializing_if = "Option::is_none")]
pub secondary_fields: Option<Vec<Content>>,
}
impl Default for Fields {
fn default() -> Self {
Self {
auxiliary_fields: None,
back_fields: None,
header_fields: None,
primary_fields: Vec::new(),
secondary_fields: None,
}
}
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(untagged)]
pub enum ContentValue {
String(String),
Date(DateTime<Utc>),
Int(i64),
Float(f64),
}
impl From<String> for ContentValue {
fn from(value: String) -> Self {
Self::String(value)
}
}
impl From<&str> for ContentValue {
fn from(value: &str) -> Self {
Self::String(value.to_string())
}
}
impl From<DateTime<Utc>> for ContentValue {
fn from(value: DateTime<Utc>) -> Self {
Self::Date(value)
}
}
impl From<i64> for ContentValue {
fn from(value: i64) -> Self {
Self::Int(value)
}
}
impl From<f64> for ContentValue {
fn from(value: f64) -> Self {
Self::Float(value)
}
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct Content {
pub key: String,
pub value: ContentValue,
#[serde(flatten)]
pub options: ContentOptions,
}
impl Content {
#[must_use]
pub fn new(key: &str, value: &str, options: ContentOptions) -> Self {
Self {
key: String::from(key),
value: ContentValue::from(value),
options,
}
}
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct ContentOptions {
#[serde(default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub attributed_value: Option<String>,
#[serde(default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub change_message: Option<String>,
#[serde(default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub currency_code: Option<String>,
#[serde(default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub data_detector_types: Option<DetectorType>,
#[serde(default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub date_style: Option<DateStyle>,
#[serde(default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub ignores_time_zone: Option<bool>,
#[serde(default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub is_relative: Option<bool>,
#[serde(default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub label: Option<String>,
#[serde(default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub number_style: Option<NumberStyle>,
#[serde(default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub text_alignment: Option<TextAlignment>,
#[serde(skip_serializing_if = "Option::is_none")]
pub time_style: Option<DateStyle>,
#[serde(default)]
#[serde(skip_serializing_if = "SemanticTags::is_empty")]
pub semantics: SemanticTags,
}
impl Default for ContentOptions {
fn default() -> Self {
Self {
attributed_value: None,
change_message: None,
currency_code: None,
data_detector_types: None,
date_style: None,
ignores_time_zone: None,
is_relative: None,
label: None,
number_style: None,
text_alignment: None,
time_style: None,
semantics: SemanticTags::default(),
}
}
}
#[derive(Serialize, Deserialize, Debug)]
pub enum DetectorType {
#[serde(rename = "PKDataDetectorTypePhoneNumber")]
PhoneNumber,
#[serde(rename = "PKDataDetectorTypeLink")]
Link,
#[serde(rename = "PKDataDetectorTypeAddress")]
Address,
#[serde(rename = "PKDataDetectorTypeCalendarEvent")]
CalendarEvent,
}
#[derive(Serialize, Deserialize, Debug)]
pub enum DateStyle {
#[serde(rename = "PKDateStyleNone")]
None,
#[serde(rename = "PKDateStyleShort")]
Short,
#[serde(rename = "PKDateStyleMedium")]
Medium,
#[serde(rename = "PKDateStyleLong")]
Long,
#[serde(rename = "PKDateStyleFull")]
Full,
}
#[derive(Serialize, Deserialize, Debug)]
pub enum NumberStyle {
#[serde(rename = "PKNumberStyleDecimal")]
Decimal,
#[serde(rename = "PKNumberStylePercent")]
Percent,
#[serde(rename = "PKNumberStyleScientific")]
Scientific,
#[serde(rename = "PKNumberStyleSpellOut")]
SpellOut,
}
#[derive(Serialize, Deserialize, Debug)]
pub enum TextAlignment {
#[serde(rename = "PKTextAlignmentLeft")]
Left,
#[serde(rename = "PKTextAlignmentCenter")]
Center,
#[serde(rename = "PKTextAlignmentRight")]
Right,
#[serde(rename = "PKTextAlignmentNatural")]
Natural,
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub enum Type {
BoardingPass {
#[serde(flatten)]
pass_fields: Fields,
#[serde(rename = "transitType")]
transit_type: TransitType,
},
Coupon {
#[serde(flatten)]
pass_fields: Fields,
},
EventTicket {
#[serde(flatten)]
pass_fields: Fields,
},
StoreCard {
#[serde(flatten)]
pass_fields: Fields,
},
Generic {
#[serde(flatten)]
pass_fields: Fields,
},
}
#[derive(Serialize, Deserialize, Debug)]
pub enum TransitType {
#[serde(rename = "PKTransitTypeAir")]
Air,
#[serde(rename = "PKTransitTypeBoat")]
Boat,
#[serde(rename = "PKTransitTypeBus")]
Bus,
#[serde(rename = "PKTransitTypeGeneric")]
Generic,
#[serde(rename = "PKTransitTypeTrain")]
Train,
}
impl Type {
#[must_use]
pub fn add_auxiliary_field(mut self, field: Content) -> Self {
match self {
Self::BoardingPass {
ref mut pass_fields,
transit_type: _,
}
| Self::Coupon {
ref mut pass_fields,
}
| Self::EventTicket {
ref mut pass_fields,
}
| Self::StoreCard {
ref mut pass_fields,
}
| Self::Generic {
ref mut pass_fields,
} => pass_fields
.auxiliary_fields
.get_or_insert_default()
.push(field),
}
self
}
#[must_use]
pub fn add_back_field(mut self, field: Content) -> Self {
match self {
Self::BoardingPass {
ref mut pass_fields,
transit_type: _,
}
| Self::Coupon {
ref mut pass_fields,
}
| Self::EventTicket {
ref mut pass_fields,
}
| Self::StoreCard {
ref mut pass_fields,
}
| Self::Generic {
ref mut pass_fields,
} => pass_fields.back_fields.get_or_insert_default().push(field),
}
self
}
#[must_use]
pub fn add_header_field(mut self, field: Content) -> Self {
match self {
Self::BoardingPass {
ref mut pass_fields,
transit_type: _,
}
| Self::Coupon {
ref mut pass_fields,
}
| Self::EventTicket {
ref mut pass_fields,
}
| Self::StoreCard {
ref mut pass_fields,
}
| Self::Generic {
ref mut pass_fields,
} => pass_fields
.header_fields
.get_or_insert_default()
.push(field),
}
self
}
#[must_use]
pub fn add_primary_field(mut self, field: Content) -> Self {
match self {
Self::BoardingPass {
ref mut pass_fields,
transit_type: _,
}
| Self::Coupon {
ref mut pass_fields,
}
| Self::EventTicket {
ref mut pass_fields,
}
| Self::StoreCard {
ref mut pass_fields,
}
| Self::Generic {
ref mut pass_fields,
} => pass_fields.primary_fields.push(field),
}
self
}
#[must_use]
pub fn add_secondary_field(mut self, field: Content) -> Self {
match self {
Self::BoardingPass {
ref mut pass_fields,
transit_type: _,
}
| Self::Coupon {
ref mut pass_fields,
}
| Self::EventTicket {
ref mut pass_fields,
}
| Self::StoreCard {
ref mut pass_fields,
}
| Self::Generic {
ref mut pass_fields,
} => pass_fields
.secondary_fields
.get_or_insert_default()
.push(field),
}
self
}
}
#[cfg(test)]
mod tests {
use crate::pass::semantic_tags::SemanticTagSeat;
use super::*;
#[test]
fn make_pass() {
let pass = Type::Generic {
pass_fields: Fields {
..Default::default()
},
};
let json = serde_json::to_string_pretty(&pass).unwrap();
println!("{json}");
let json_expected = r#"{
"generic": {
"primaryFields": []
}
}"#;
assert_eq!(json_expected, json);
let pass: Type = serde_json::from_str(json_expected).unwrap();
let json = serde_json::to_string_pretty(&pass).unwrap();
assert_eq!(json_expected, json);
}
#[test]
fn make_boarding_pass() {
let boarding_pass = Type::BoardingPass {
pass_fields: Fields {
..Default::default()
},
transit_type: TransitType::Air,
}
.add_primary_field(Content::new(
"title",
"Airplane Ticket",
ContentOptions::default(),
))
.add_primary_field(Content::new(
"seat",
"12",
ContentOptions {
semantics: SemanticTags {
seats: vec![SemanticTagSeat {
seat_number: String::from("12").into(),
seat_row: String::from("5A").into(),
seat_section: String::from("A").into(),
..Default::default()
}],
..Default::default()
},
label: String::from("Seat").into(),
..Default::default()
},
))
.add_header_field(Content::new("company", "DAL", ContentOptions::default()))
.add_header_field(Content::new(
"company_sub",
"Dodo Air Lines",
ContentOptions::default(),
))
.add_secondary_field(Content::new(
"description",
"Some information here",
ContentOptions::default(),
));
let json = serde_json::to_string_pretty(&boarding_pass).unwrap();
println!("{json}");
let json_expected = r#"{
"boardingPass": {
"headerFields": [
{
"key": "company",
"value": "DAL"
},
{
"key": "company_sub",
"value": "Dodo Air Lines"
}
],
"primaryFields": [
{
"key": "title",
"value": "Airplane Ticket"
},
{
"key": "seat",
"value": "12",
"label": "Seat",
"semantics": {
"seats": [
{
"seatNumber": "12",
"seatRow": "5A",
"seatSection": "A"
}
]
}
}
],
"secondaryFields": [
{
"key": "description",
"value": "Some information here"
}
],
"transitType": "PKTransitTypeAir"
}
}"#;
assert_eq!(json_expected, json);
let boarding_pass: Type = serde_json::from_str(json_expected).unwrap();
let json = serde_json::to_string_pretty(&boarding_pass).unwrap();
assert_eq!(json_expected, json);
}
#[test]
fn make_event_ticket() {
let event_ticket = Type::EventTicket {
pass_fields: Fields {
..Default::default()
},
}
.add_primary_field(Content::new(
"title",
"Super Ticket",
ContentOptions {
label: String::from("NAME").into(),
..Default::default()
},
))
.add_primary_field(Content::new("seat", "12", ContentOptions::default()))
.add_header_field(Content::new(
"event_title",
"KKK",
ContentOptions::default(),
))
.add_header_field(Content::new("some", "123", ContentOptions::default()))
.add_secondary_field(Content::new(
"description",
"Some information here",
ContentOptions::default(),
));
let json = serde_json::to_string_pretty(&event_ticket).unwrap();
println!("{json}");
let json_expected = r#"{
"eventTicket": {
"headerFields": [
{
"key": "event_title",
"value": "KKK"
},
{
"key": "some",
"value": "123"
}
],
"primaryFields": [
{
"key": "title",
"value": "Super Ticket",
"label": "NAME"
},
{
"key": "seat",
"value": "12"
}
],
"secondaryFields": [
{
"key": "description",
"value": "Some information here"
}
]
}
}"#;
assert_eq!(json_expected, json);
let event_ticket: Type = serde_json::from_str(json_expected).unwrap();
let json = serde_json::to_string_pretty(&event_ticket).unwrap();
assert_eq!(json_expected, json);
}
}