1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
use chrono::{DateTime, Timelike, Utc};
use chrono::serde::ts_seconds;
use serde::{Deserialize, Deserializer, Serialize, Serializer};

#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
pub struct Visit {

    /// A hierarchical path to the scene visited.
    #[serde(rename = "action_name", serialize_with = "serialize_scene_path", deserialize_with = "deserialize_action_name")]
    pub scene_path: Vec<String>,

    /// The campaign ID this data point is for.
    pub campaign_id: String,

    /// Number of times this data point has arisen between `first` and `last`. OPTIONAL, defaults to 1.
    pub times: u32,

    /// The first time this data point has arisen. OPTIONAL, defaults to now.
    #[serde(rename = "period_start", with = "ts_seconds")]
    pub first: DateTime<Utc>,

    /// The last time this data point has arisen. OPTIONAL, defaults to now.
    #[serde(rename = "period_end", with = "ts_seconds")]
    pub last: DateTime<Utc>,
}

fn serialize_scene_path<S>(scene_path: &Vec<String>, serializer: S) -> Result<S::Ok, S::Error> where S: Serializer {
    serializer.serialize_str(scene_path.join("/").as_str())
}

fn deserialize_action_name<'de, D>(deserializer: D) -> Result<Vec<String>, D::Error> where D: Deserializer<'de> {
    let action_name = String::deserialize(deserializer)?;

    Ok(action_name.split('/').map(|item| item.to_owned()).collect())
}

impl Visit {

    /// Returns a visit with the given scene_path and campaign_id and sane defaults for the other
    /// properties.
    ///
    /// # Arguments
    /// * scene_path: A hierarchical path to the scene visited.
    /// * campaign_id: The campaign ID this data point is for.
    ///
    /// # Defaults:
    /// * times: 1.
    /// * first: now.
    /// * last: now.
    pub fn new(scene_path: Vec<String>, campaign_id: String) -> Visit {
        let now = Utc::now().with_nanosecond(0).unwrap();

        Visit { scene_path, campaign_id, times: 1, first: now.clone(), last: now }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use chrono::TimeZone;

    #[test]
    fn new_visit() {
        let visit = Visit {
            scene_path: vec!["foo".to_string(), "bar".to_string()],
            campaign_id: "test".to_string(),
            times: 100,
            first: Utc.timestamp(0, 0),
            last: Utc.timestamp(0, 0) };

        let json = serde_json::to_string(&visit).unwrap();

        assert_eq!(json,
                   "{\"action_name\":\"foo/bar\",\"campaign_id\":\"test\",\"times\":100,\"period_start\":0,\"period_end\":0}");

        let visit2: Visit = serde_json::from_str(json.as_str()).unwrap();

        assert_eq!(visit2, visit);
    }

    #[test]
    fn new_visit_with_defaults() {
        let visit = Visit::new(
            vec!["foo".to_string(), "bar".to_string()],
            "test".to_string());

        let json = serde_json::to_string(&visit).unwrap();
        let now = Utc::now().timestamp();

        assert_eq!(json,
                   format!("{{\"action_name\":\"foo/bar\",\"campaign_id\":\"test\",\"times\":1,\"period_start\":{now},\"period_end\":{now}}}", now=now));

        let visit2: Visit = serde_json::from_str(json.as_str()).unwrap();

        assert_eq!(visit2, visit);
    }
}