use crate::error::Error; use crate::models; use chrono::DateTime; use serde::Deserialize; use serde::Serialize; use serde_json::to_value; use serde_json::Value as Json; pub fn running_template() -> TemplateSpec { TemplateSpec::Table(vec![ Column { display_name: "Date".to_string(), field: FieldSpec::App( Function::DisplayUnit(Unit::Timestamp, "date".to_string()), vec![FieldSpec::Field("start_timestamp".to_string())], ), }, Column { display_name: "Time".to_string(), field: FieldSpec::App( Function::DisplayUnit(Unit::Seconds, "".to_string()), vec![FieldSpec::Field("moving_time".to_string())], ), }, Column { display_name: "Distance".to_string(), field: FieldSpec::App( Function::DisplayUnit(Unit::Meters, "miles".to_string()), vec![FieldSpec::Field("distance".to_string())], ), }, Column { display_name: "Pace".to_string(), field: FieldSpec::App( Function::DisplayUnit(Unit::Pace, "".to_string()), vec![FieldSpec::App( Function::Div, vec![ FieldSpec::Field("moving_time".to_string()), FieldSpec::Field("distance".to_string()), ], )], ), }, // Column { // display_name: "Title".to_string(), // field: FieldSpec::Field("name".to_string()), // }, // Column { // display_name: "Description".to_string(), // field: FieldSpec::Field("description".to_string()), // }, ]) } #[derive(Debug, Serialize, Deserialize, Copy, Clone)] pub enum Unit { Timestamp, // As Rfc3339 Meters, Seconds, Speed, // As m/s Pace, // As s/m } #[derive(Debug, Serialize, Deserialize, Clone)] pub enum Function { DisplayUnit(Unit, String), Div, } #[derive(Debug, Serialize, Deserialize, Clone)] pub enum FieldSpec { Field(String), App(Function, Vec), } #[derive(Debug, Serialize, Deserialize, Clone)] pub struct Column { display_name: String, field: FieldSpec, } #[derive(Debug, Serialize, Deserialize, Clone)] pub enum TemplateSpec { Table(Vec), } #[derive(Debug, Serialize, Deserialize, Clone)] pub struct Template { pub entry_type: String, pub spec: TemplateSpec, } mod function { use super::*; const METERS_PER_MILE: f64 = 1609.344; fn really_f64(d: &Json) -> Result { d.to_string() .parse::() .map_err(|_| Error::TemplateError("convert to f64".to_string(), d.clone())) } fn format_duration(d: &Json, _opts: &str) -> Result { let seconds = really_f64(d)? as i64; let mut hours = "".to_string(); if seconds > 3600 { hours = format!("{}:", seconds / 3600); } Ok(json!(format!( "{}{:02}:{:02}", hours, (seconds % 3600) / 60, seconds % 60 ))) } fn format_pace(d: &Json) -> Result { let pace = really_f64(d)?; format_duration(&json!(pace * METERS_PER_MILE), "") } fn format_distance(d: &Json, opts: &str) -> Result { Ok(json!(match opts { "miles" => format!("{:.1}", really_f64(d)? / METERS_PER_MILE), _ => format!("{} m", really_f64(d)?), })) } fn format_timestamp(d: &Json, opts: &str) -> Result { fn format_timestamp(t: DateTime, opts: &str) -> Result { let s = match opts { "date" => t.format("%Y/%m/%d"), _ => t.format("%c"), }; Ok(json!(format!("{}", s))) } let t = d.as_str(); match t { None => Ok(json!(())), Some(t) => format_timestamp( DateTime::parse_from_rfc3339(t)?.with_timezone(&chrono::Utc), opts, ), } } fn display_unit(u: Unit, opts: &str, params: &Vec, d: &Json) -> Result { if params.len() != 1 { Err(Error::TemplateError( format!("DisplayUnit expects 1 parameter, got {:?}", params), d.clone(), ))?; } let d = eval(¶ms[0], &d)?; Ok(match u { Unit::Timestamp => format_timestamp(&d, &opts)?, Unit::Meters => format_distance(&d, &opts)?, Unit::Seconds => format_duration(&d, &opts)?, Unit::Speed => json!(format!("as_speed({:?})", d)), Unit::Pace => format_pace(&d)?, }) } fn div(params: &Vec, d: &Json) -> Result { if params.len() != 2 { Err(Error::TemplateError( format!("DisplayUnit expects 1 parameter, got {:?}", params), d.clone(), ))?; } let p1 = really_f64(&eval(¶ms[0], &d)?)?; let p2 = really_f64(&eval(¶ms[1], &d)?)?; Ok(json!(p1 / p2)) } fn app(f: &Function, params: &Vec, d: &Json) -> Result { match f { Function::DisplayUnit(ref u, ref opts) => display_unit(*u, opts, params, d), Function::Div => div(params, d), } } pub fn eval(s: &FieldSpec, d: &Json) -> Result { Ok(match s { FieldSpec::Field(ref s) => to_value(&d[s])?, FieldSpec::App(ref f, ref params) => app(f, params, d)?, }) } #[cfg(test)] mod tests { use super::*; #[test] fn eval_field() { let d = json!({"x": 2}); assert_eq!( json!(2), eval(&FieldSpec::Field("x".to_string()), &d).unwrap() ); } #[test] fn eval_fn1() { let d = json!({"x": 2}); let app1 = |f| { println!("{:?}", f); eval( &FieldSpec::App(f, vec![FieldSpec::Field("x".to_string())]), &d, ) .unwrap() }; assert_eq!( json!("53:38"), app1(Function::DisplayUnit(Unit::Pace, "".to_string())) ); assert_eq!( json!("00:02"), app1(Function::DisplayUnit(Unit::Seconds, "".to_string())) ); } #[test] fn eval_fn2() { let distance = 2.0 * 1609.344; let time = 60 * 14; let d = json!({"distance": distance, "time": time}); assert_eq!( eval( &FieldSpec::App( Function::Div, vec!( FieldSpec::Field("time".to_string()), FieldSpec::Field("distance".to_string()) ) ), &d ) .unwrap(), json!((time as f64) / distance) ) } } } mod table { use super::*; #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] pub struct TableRow { pub index: i32, pub columns: Vec, } #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] pub struct DisplayTable { pub num_rows: usize, pub num_columns: usize, pub headings: TableRow, pub rows: Vec, } pub fn apply(columns: &Vec, d: &Json) -> Result { let rows = d .as_array() .ok_or_else(|| Error::TemplateError("expected array".to_string(), d.clone()))? .iter() .map(|d| { columns .iter() .map(|c| function::eval(&c.field, d)) .collect::, _>>() }) .collect::, _>>()?; Ok(DisplayTable { num_rows: rows.len(), num_columns: columns.len(), headings: TableRow { index: 1, columns: columns.iter().map(|c| json!(c.display_name)).collect(), }, rows: (2..) .zip(rows.into_iter()) .map(|(i, columns)| TableRow { index: i, columns: columns, }) .collect(), }) } } impl TemplateSpec { pub fn apply(self: &TemplateSpec, entries: Vec) -> Result { info!("Applying template\n {:#?}", self); info!("Sample entry\n {:#?}", entries.first()); let d = Json::Array(entries.into_iter().map(|e| e.payload).collect()); Ok(match self { &TemplateSpec::Table(ref c) => to_value(table::apply(c, &d)?)?, }) } } #[cfg(test)] mod tests { use super::*; #[test] fn table_test() { let d = json!([{ "name": "Nick", "distance": 3218.688 }]); let columns = vec![ Column { display_name: "Name".to_string(), field: FieldSpec::Field("name".to_string()), }, Column { display_name: "Distance".to_string(), field: FieldSpec::App( Function::DisplayUnit(Unit::Meters, "miles".to_string()), vec![FieldSpec::Field("distance".to_string())], ), }, ]; let table = table::apply(&columns, &d).unwrap(); assert_eq!( table.headings.columns, vec!(json!("Name"), json!("Distance")) ); assert_eq!( table.rows, vec!(table::TableRow { index: 2, columns: vec!(json!("Nick"), json!("2.0")) }) ); } }