Forráskód Böngészése

feat: Add request structure and it's deserialization.

Alexandre Leblanc 5 éve
commit
ec24c1e2fe
7 módosított fájl, 1878 hozzáadás és 0 törlés
  1. 1 0
      .gitignore
  2. 1611 0
      Cargo.lock
  3. 18 0
      Cargo.toml
  4. 1 0
      api_insomnia.json
  5. 1 0
      src/broadsign/mod.rs
  6. 160 0
      src/broadsign/real_time_pop_request.rs
  7. 86 0
      src/main.rs

+ 1 - 0
.gitignore

@@ -0,0 +1 @@
+/target

A különbségek nem kerülnek megjelenítésre, a fájl túl nagy
+ 1611 - 0
Cargo.lock


+ 18 - 0
Cargo.toml

@@ -0,0 +1,18 @@
+[package]
+name = "broadsign-rt-pop-server"
+version = "0.1.0"
+authors = ["Alexandre Leblanc <a.leblanc@mrtryhard.info>"]
+edition = "2018"
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[dependencies]
+actix-web = "2"
+actix-rt = "1"
+chrono = { version = "0.4.13",  features= ["serde"] }
+env_logger = "0.7"
+log = "0.4"
+serde = "1.0"
+serde_derive = "1.0"
+serde_json = "1.0"
+serde_repr = "0.1"

A különbségek nem kerülnek megjelenítésre, a fájl túl nagy
+ 1 - 0
api_insomnia.json


+ 1 - 0
src/broadsign/mod.rs

@@ -0,0 +1 @@
+pub mod real_time_pop_request;

+ 160 - 0
src/broadsign/real_time_pop_request.rs

@@ -0,0 +1,160 @@
+extern crate serde_derive;
+
+use chrono::naive::NaiveDateTime;
+use serde::{Deserialize, Serialize};
+
+/* A proof of play (pop) entry from the request. Some fields were aliased
+ * to be more explicit.
+ */
+#[derive(Serialize, Deserialize, std::fmt::Debug)]
+pub struct RealTimePopEntry {
+    pub display_unit_id: u64,
+    pub frame_id: u64,
+    #[serde(rename = "n_screens")]
+    pub active_screens_count: u32,
+    pub ad_copy_id: u64,
+    pub campaign_id: u64,
+    pub schedule_id: u64,
+    pub impressions: u32,
+    pub interactions: u32,
+    // Since the Broadsign Player sends the timestamp in its local time, we cannot safely
+    // deduce a time zone, so we're using NaiveDateTime. If your players are guaranteed to
+    // be on the same timezone as the server, you may use:
+    //
+    //    chrono::DateTime<chrono::Local> (DateTime<Local>)
+    pub end_time: NaiveDateTime,
+    #[serde(rename = "duration")]
+    pub duration_ms: u32,
+    #[serde(rename = "ext1")]
+    pub service_name: String,
+    #[serde(rename = "ext2")]
+    pub service_value: String,
+    // `extra_data` field has been added in 13.2; if you have players from both 13.0, 13.1 and
+    // 13.2, you may want to make it optional, otherwise the parsing _will_ fail.
+    //
+    //    extra_data: Option<serde_json::Value>
+    //
+    // You may also want to strongly type it (create a struct and derive serde).
+    pub extra_data: serde_json::Value,
+}
+
+/* Request sent by Broadsign Player, as defined on the website:
+ * https://docs.broadsign.com/broadsign-control/13-0/real-time-pop-api.html
+ */
+#[derive(Serialize, Deserialize, std::fmt::Debug)]
+pub struct RealTimePopRequest {
+    pub api_key: String,
+    pub player_id: u64,
+    #[serde(rename = "pop")]
+    pub pops: Vec<RealTimePopEntry>,
+}
+
+#[cfg(test)]
+mod protocol_serialization_tests {
+    use super::*;
+    use chrono::NaiveDate;
+
+    fn assert_valid_request_content(deserialized: RealTimePopRequest) {
+        assert_eq!(deserialized.api_key, "some_secure_api_key");
+        assert_eq!(deserialized.player_id, 12345);
+        assert_eq!(deserialized.pops.len(), 2);
+
+        // Validate first pop
+        assert_eq!(deserialized.pops[0].display_unit_id, 4456);
+        assert_eq!(deserialized.pops[0].frame_id, 4457);
+        assert_eq!(deserialized.pops[0].active_screens_count, 1);
+        assert_eq!(deserialized.pops[0].ad_copy_id, 5001);
+        assert_eq!(deserialized.pops[0].campaign_id, 5002);
+        assert_eq!(deserialized.pops[0].schedule_id, 5003);
+        assert_eq!(deserialized.pops[0].impressions, 2);
+        assert_eq!(deserialized.pops[0].interactions, 0);
+        assert_eq!(
+            deserialized.pops[0].end_time,
+            NaiveDate::from_ymd(2016, 5, 31).and_hms_milli(10, 14, 50, 200)
+        );
+        assert_eq!(deserialized.pops[0].duration_ms, 5000);
+        assert_eq!(deserialized.pops[0].service_name, "bmb");
+        assert_eq!(deserialized.pops[0].service_value, "3451");
+        assert_eq!(deserialized.pops[0].extra_data.is_string(), true);
+        assert_eq!(deserialized.pops[0].extra_data, "");
+
+        // Validate second pop
+        assert_eq!(deserialized.pops[1].display_unit_id, 3456);
+        assert_eq!(deserialized.pops[1].frame_id, 3457);
+        assert_eq!(deserialized.pops[1].active_screens_count, 1);
+        assert_eq!(deserialized.pops[1].ad_copy_id, 7001);
+        assert_eq!(deserialized.pops[1].campaign_id, 7002);
+        assert_eq!(deserialized.pops[1].schedule_id, 7003);
+        assert_eq!(deserialized.pops[1].impressions, 4);
+        assert_eq!(deserialized.pops[1].interactions, 1);
+        assert_eq!(
+            deserialized.pops[1].end_time,
+            NaiveDate::from_ymd(2016, 5, 31).and_hms_milli(10, 14, 55, 200)
+        );
+        assert_eq!(deserialized.pops[1].duration_ms, 5000);
+        assert_eq!(deserialized.pops[1].service_name, "");
+        assert_eq!(deserialized.pops[1].service_value, "");
+        assert_eq!(deserialized.pops[1].extra_data.is_string(), true);
+        assert_eq!(deserialized.pops[1].extra_data, "");
+    }
+
+    #[test]
+    fn given_valid_non_verbose_request_deserialization_is_success() {
+        let request = r#"
+		    {
+		        "api_key": "some_secure_api_key",
+		        "player_id": 12345,
+		        "pop": [
+			        [4456, 4457, 1, 5001, 5002, 5003, 2, 0, "2016-05-31T10:14:50.200", 5000, "bmb", "3451", ""],
+			        [3456, 3457, 1, 7001, 7002, 7003, 4, 1, "2016-05-31T10:14:55.200", 5000, "", "", ""]
+		        ]
+            }"#;
+
+        let deserialized = serde_json::from_str::<RealTimePopRequest>(request).unwrap();
+        assert_valid_request_content(deserialized);
+    }
+
+    #[test]
+    fn given_valid_verbose_request_deserialization_is_success() {
+        let request = r#"
+		    {
+		        "api_key": "some_secure_api_key",
+		        "player_id": 12345,
+		        "pop": [
+                    {
+                        "display_unit_id": 4456,
+                        "frame_id": 4457,
+                        "n_screens": 1,
+                        "ad_copy_id": 5001,
+                        "campaign_id": 5002,
+                        "schedule_id": 5003,
+                        "impressions": 2,
+                        "interactions": 0,
+                        "end_time": "2016-05-31T10:14:50.200",
+                        "duration": 5000,
+                        "ext1": "bmb",
+                        "ext2": "3451",
+                        "extra_data": ""
+                    },
+                    {
+                        "display_unit_id": 3456,
+                        "frame_id": 3457,
+                        "n_screens": 1,
+                        "ad_copy_id": 7001,
+                        "campaign_id": 7002,
+                        "schedule_id": 7003,
+                        "impressions": 4,
+                        "interactions": 1,
+                        "end_time": "2016-05-31T10:14:55.200",
+                        "duration": 5000,
+                        "ext1": "",
+                        "ext2": "",
+                        "extra_data": ""
+                    }
+		        ]
+            }"#;
+
+        let deserialized = serde_json::from_str::<RealTimePopRequest>(request).unwrap();
+        assert_valid_request_content(deserialized);
+    }
+}

+ 86 - 0
src/main.rs

@@ -0,0 +1,86 @@
+#[macro_use]
+extern crate log;
+mod broadsign;
+
+use actix_web::{middleware, web, App, HttpResponse, HttpServer};
+use broadsign::real_time_pop_request::RealTimePopRequest;
+
+pub async fn status_get() -> HttpResponse {
+    HttpResponse::Ok().finish()
+}
+
+pub async fn pop_post(pop_data: web::Json<RealTimePopRequest>) -> HttpResponse {
+    let pop_data: RealTimePopRequest = pop_data.into_inner();
+
+    debug!("{:?}", pop_data);
+
+    HttpResponse::Ok().finish()
+}
+
+#[actix_rt::main]
+async fn main() -> std::io::Result<()> {
+    if !std::env::vars().any(|(k, _)| k == "RUST_LOG") {
+        std::env::set_var("RUST_LOG", "info");
+    }
+    env_logger::init();
+
+    info!("=== Starting Real-Time Pop Service ===");
+
+    HttpServer::new(move || {
+        App::new().wrap(middleware::Logger::default()).service(
+            web::scope("")
+                .route("/status", web::get().to(status_get))
+                .route("/pop", web::post().to(pop_post)),
+        )
+    })
+    .bind("0.0.0.0:8080")?
+    .run()
+    .await
+}
+
+#[cfg(test)]
+mod tests_endpoint_status {
+    use super::*;
+    use actix_web::{http, web};
+
+    #[actix_rt::test]
+    async fn given_everything_is_running_status_returns_200_ok() {
+        let resp = status_get().await;
+
+        assert_eq!(resp.status(), http::StatusCode::OK);
+    }
+}
+
+#[cfg(test)]
+mod tests_endpoint_pop {
+    use super::*;
+    use actix_web::{http, web};
+    use broadsign::real_time_pop_request::{RealTimePopEntry, RealTimePopRequest};
+    use serde_json::json;
+
+    #[actix_rt::test]
+    async fn given_a_valid_pop_and_healthy_server_respond_ok() {
+        let resp = pop_post(web::Json(RealTimePopRequest {
+            api_key: "some_secure_api_key".to_owned(),
+            player_id: 123456,
+            pops: vec![RealTimePopEntry {
+                display_unit_id: 123,
+                frame_id: 124,
+                active_screens_count: 2,
+                ad_copy_id: 56467,
+                campaign_id: 61000,
+                schedule_id: 61001,
+                impressions: 675,
+                interactions: 0,
+                end_time: chrono::NaiveDate::from_ymd(2017, 11, 23).and_hms_milli(13, 27, 12, 500),
+                duration_ms: 12996,
+                service_name: "bmb".to_owned(),
+                service_value: "701".to_owned(),
+                extra_data: json!(""),
+            }],
+        }))
+        .await;
+
+        assert_eq!(resp.status(), http::StatusCode::OK);
+    }
+}