summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKjetil Orbekk <kjetil.orbekk@gmail.com>2020-02-07 21:27:11 -0500
committerKjetil Orbekk <kjetil.orbekk@gmail.com>2020-02-07 21:27:11 -0500
commit06dbcc08dd75cdcb790e3f41180114cbcddb8bc8 (patch)
tree6bb36c1b1cb439cb5010136c5504b30823701de4
parent8d4d29a3c25063d71c561f30ccc1c31ab85d2bc7 (diff)
Oh dear
-rw-r--r--src/error.rs2
-rw-r--r--src/template.rs203
2 files changed, 198 insertions, 7 deletions
diff --git a/src/error.rs b/src/error.rs
index 166005d..c58083e 100644
--- a/src/error.rs
+++ b/src/error.rs
@@ -81,6 +81,7 @@ pub enum Error {
CommunicationError(reqwest::Error),
ParseError(serde_json::error::Error),
ParseTimeError(chrono::format::ParseError),
+ TemplateError(String, Value),
StravaApiError(StravaApiError),
UnexpectedJson(Value),
AlreadyExists,
@@ -96,6 +97,7 @@ impl fmt::Display for Error {
Error::CommunicationError(ref e) => e.fmt(f),
Error::ParseError(ref e) => e.fmt(f),
Error::ParseTimeError(ref e) => e.fmt(f),
+ Error::TemplateError(ref s, _) => f.write_str(&s),
Error::UnexpectedJson(_) => f.write_str("UnexpectedJson"),
Error::StravaApiError(ref e) => e.fmt(f),
Error::AlreadyExists => f.write_str("AlreadyExists"),
diff --git a/src/template.rs b/src/template.rs
index 21defa3..d469394 100644
--- a/src/template.rs
+++ b/src/template.rs
@@ -1,31 +1,34 @@
+use crate::error::Error;
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: Some("Date".to_string()),
+ display_name: "Date".to_string(),
field: FieldSpec::App(
Function::DisplayUnit(Unit::Timestamp, "date".to_string()),
vec![FieldSpec::Field("start_time".to_string())],
),
},
Column {
- display_name: Some("Time".to_string()),
+ display_name: "Time".to_string(),
field: FieldSpec::App(
Function::DisplayUnit(Unit::Seconds, "".to_string()),
vec![FieldSpec::Field("moving_time".to_string())],
),
},
Column {
- display_name: Some("Distance".to_string()),
+ display_name: "Distance".to_string(),
field: FieldSpec::App(
Function::DisplayUnit(Unit::Meters, "miles".to_string()),
vec![FieldSpec::Field("distance".to_string())],
),
},
Column {
- display_name: Some("Pace".to_string()),
+ display_name: "Pace".to_string(),
field: FieldSpec::App(
Function::DisplayUnit(Unit::Pace, "".to_string()),
vec![FieldSpec::App(
@@ -40,7 +43,7 @@ pub fn running_template() -> TemplateSpec {
])
}
-#[derive(Debug, Serialize, Deserialize, Clone)]
+#[derive(Debug, Serialize, Deserialize, Copy, Clone)]
pub enum Unit {
Timestamp, // As Rfc3339
Meters,
@@ -51,7 +54,6 @@ pub enum Unit {
#[derive(Debug, Serialize, Deserialize, Clone)]
pub enum Function {
- TimestampToDate,
DisplayUnit(Unit, String),
Div,
}
@@ -64,7 +66,7 @@ pub enum FieldSpec {
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Column {
- display_name: Option<String>,
+ display_name: String,
field: FieldSpec,
}
@@ -78,3 +80,190 @@ 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<f64, Error> {
+ d.to_string()
+ .parse::<f64>()
+ .map_err(|_| Error::TemplateError("convert to f64".to_string(), d.clone()))
+ }
+
+ fn format_pace(d: &Json) -> Result<Json, Error> {
+ let pace = really_f64(d)?;
+ let seconds_per_mile = (pace * METERS_PER_MILE).round() as i64;
+ let mut hours = "".to_string();
+ if seconds_per_mile > 3600 {
+ hours = format!("{}", seconds_per_mile / 3600);
+ }
+ Ok(json!(format!(
+ "{}{:02}:{:02}",
+ hours,
+ seconds_per_mile / 60,
+ seconds_per_mile % 60
+ )))
+ }
+
+ fn format_distance(d: &Json, opts: &str) -> Result<Json, Error> {
+ Ok(json!(match opts {
+ "miles" => format!("{:.1}", really_f64(d)? / METERS_PER_MILE),
+ _ => format!("{} m", really_f64(d)?)
+ }))
+ }
+
+ fn display_unit(u: Unit, opts: &str, params: &Vec<FieldSpec>, d: &Json) -> Result<Json, Error> {
+ if params.len() != 1 {
+ Err(Error::TemplateError(
+ format!("DisplayUnit expects 1 parameter, got {:?}", params),
+ d.clone(),
+ ))?;
+ }
+ let d = eval(&params[0], &d)?;
+ Ok(match u {
+ Unit::Timestamp => json!(format!("as_timestamp({:?})", d)),
+ Unit::Meters => format_distance(&d, &opts)?,
+ Unit::Seconds => json!(format!("as_duration({:?})", d)),
+ Unit::Speed => json!(format!("as_speed({:?})", d)),
+ Unit::Pace => format_pace(&d)?,
+ })
+ }
+
+ fn div(params: &Vec<FieldSpec>, d: &Json) -> Result<Json, Error> {
+ if params.len() != 2 {
+ Err(Error::TemplateError(
+ format!("DisplayUnit expects 1 parameter, got {:?}", params),
+ d.clone(),
+ ))?;
+ }
+ let p1 = really_f64(&eval(&params[0], &d)?)?;
+ let p2 = really_f64(&eval(&params[1], &d)?)?;
+ Ok(json!(p1 / p2))
+ }
+
+ fn app(f: &Function, params: &Vec<FieldSpec>, d: &Json) -> Result<Json, Error> {
+ 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<Json, Error> {
+ 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": 0.260976});
+
+ let app1 = |f| {
+ println!("{:?}", f);
+ eval(
+ &FieldSpec::App(f, vec![FieldSpec::Field("x".to_string())]),
+ &d,
+ )
+ .unwrap()
+ };
+ assert_eq!(
+ json!("07:00"),
+ app1(Function::DisplayUnit(Unit::Pace, "".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)]
+ pub struct DisplayTable {
+ pub headings: Vec<String>,
+ pub rows: Vec<Vec<Json>>,
+ }
+
+ pub fn apply(columns: &Vec<Column>, d: &Json) -> Result<DisplayTable, Error> {
+ let rows = d.as_array()
+ .ok_or_else(|| Error::TemplateError("expected array".to_string(), d.clone()))?;
+
+ Ok(DisplayTable {
+ headings: columns.iter().map(|c| c.display_name.clone()).collect(),
+ rows: rows.iter().map(|d| {
+ columns.iter().map(|c| {
+ function::eval(&c.field, d)
+ }).collect::<Result<Vec<_>, _>>()
+ }).collect::<Result<Vec<_>, _>>()?,
+ })
+ }
+}
+
+pub fn apply(t: &TemplateSpec, d: &Json) -> Result<Json, Error> {
+ Ok(match t {
+ &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, vec!("Name", "Distance"));
+ assert_eq!(table.rows, vec!(vec!(json!("Nick"), json!("2.0"))));
+ }
+}