diff --git a/Cargo.lock b/Cargo.lock index dde756c..c51139e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -381,6 +381,19 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chrono" +version = "0.4.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73" +dependencies = [ + "libc", + "num-integer", + "num-traits 0.2.14", + "time 0.1.44", + "winapi", +] + [[package]] name = "combine" version = "3.8.1" @@ -545,6 +558,18 @@ dependencies = [ "synstructure", ] +[[package]] +name = "filetime" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "975ccf83d8d9d0d84682850a38c8169027be83368805971cc4f238c2b245bc98" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "winapi", +] + [[package]] name = "firestorm" version = "0.4.6" @@ -764,6 +789,15 @@ dependencies = [ "libc", ] +[[package]] +name = "home" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2456aef2e6b6a9784192ae780c0f15bc57df0e918585282325e8c8ac27737654" +dependencies = [ + "winapi", +] + [[package]] name = "http" version = "0.2.5" @@ -886,6 +920,12 @@ version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" +[[package]] +name = "itoap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9028f49264629065d057f340a86acb84867925865f73bbf8d47b4d149a7e88b8" + [[package]] name = "jobserver" version = "0.1.24" @@ -943,6 +983,7 @@ dependencies = [ "actix-rt", "actix-web", "actix-web-codegen 0.5.0-beta.4", + "chrono", "config", "derive_more", "graphql_client", @@ -951,6 +992,7 @@ dependencies = [ "num_cpus", "pretty_env_logger", "reqwest", + "sailfish", "serde 1.0.130", "serde_json", "url", @@ -1086,6 +1128,16 @@ dependencies = [ "winapi", ] +[[package]] +name = "num-integer" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db" +dependencies = [ + "autocfg", + "num-traits 0.2.14", +] + [[package]] name = "num-traits" version = "0.1.43" @@ -1276,9 +1328,9 @@ checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5" [[package]] name = "proc-macro2" -version = "1.0.32" +version = "1.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba508cc11742c0dc5c1659771673afbab7a0efab23aa17e854cbab0837ed0b43" +checksum = "a152013215dca273577e18d2bf00fa862b89b24169fb78c4c95aeb07992c9cec" dependencies = [ "unicode-xid", ] @@ -1444,6 +1496,43 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e" +[[package]] +name = "sailfish" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "816920a08514d9741242b3efe70c16c350ed548bc4a5ba03426e56faf9d45f77" +dependencies = [ + "itoap", + "ryu", + "sailfish-macros", + "version_check", +] + +[[package]] +name = "sailfish-compiler" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4276e7b848bde8e7813d534f014bc35ce5acd2b9e2b6b075727113fcf478ba63" +dependencies = [ + "filetime", + "home", + "memchr", + "proc-macro2", + "quote", + "syn", + "yaml-rust", +] + +[[package]] +name = "sailfish-macros" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bba2458ef07ae12c9aed2edb866c3db2f9c21cf19a2c3f2613b2982bc1a4a46" +dependencies = [ + "proc-macro2", + "sailfish-compiler", +] + [[package]] name = "schannel" version = "0.1.19" @@ -1693,9 +1782,9 @@ checksum = "213701ba3370744dcd1a12960caa4843b3d68b4d1c0a5d575e0d65b2ee9d16c0" [[package]] name = "syn" -version = "1.0.81" +version = "1.0.80" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2afee18b8beb5a596ecb4a2dce128c719b4ba399d34126b9e4396e3f9860966" +checksum = "d010a1623fbd906d51d650a9916aaefc05ffa0e4053ff7fe601167f3e715d194" dependencies = [ "proc-macro2", "quote", @@ -1737,6 +1826,17 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "time" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255" +dependencies = [ + "libc", + "wasi", + "winapi", +] + [[package]] name = "time" version = "0.2.27" @@ -1973,9 +2073,9 @@ dependencies = [ [[package]] name = "wasi" -version = "0.10.2+wasi-snapshot-preview1" +version = "0.10.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" +checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" [[package]] name = "wasm-bindgen" diff --git a/Cargo.toml b/Cargo.toml index e45fae5..5cfbb28 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,7 +31,10 @@ lazy_static = "1.4" url = "2.2" derive_more = "0.99" +sailfish = "0.3.2" num_cpus = "1.13" reqwest = { version = "0.11.6", features = ["json"] } graphql_client = { version = "0.10.0", features = ["reqwest"]} + +chrono = "0.4.19" diff --git a/sailfish.yml b/sailfish.yml new file mode 100644 index 0000000..5f64209 --- /dev/null +++ b/sailfish.yml @@ -0,0 +1 @@ +delimiter: "." diff --git a/src/proxy.rs b/src/proxy.rs index c4fb294..88aa413 100644 --- a/src/proxy.rs +++ b/src/proxy.rs @@ -14,11 +14,13 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ +use std::ops::{Bound, RangeBounds}; + use actix_web::{web, HttpResponse, Responder}; use graphql_client::{reqwest::post_graphql, GraphQLQuery}; -use serde::{Deserialize, Serialize}; +use sailfish::TemplateOnce; -use crate::SETTINGS; +use crate::AppData; pub mod routes { pub struct Proxy { @@ -41,6 +43,58 @@ pub mod routes { } } +// credits @carlomilanesi: +// https://users.rust-lang.org/t/how-to-get-a-substring-of-a-string/1351/11 +trait StringUtils { + fn substring(&self, start: usize, len: usize) -> &str; + fn slice(&self, range: impl RangeBounds) -> &str; +} + +impl StringUtils for str { + fn substring(&self, start: usize, len: usize) -> &str { + let mut char_pos = 0; + let mut byte_start = 0; + let mut it = self.chars(); + loop { + if char_pos == start { + break; + } + if let Some(c) = it.next() { + char_pos += 1; + byte_start += c.len_utf8(); + } else { + break; + } + } + char_pos = 0; + let mut byte_end = byte_start; + loop { + if char_pos == len { + break; + } + if let Some(c) = it.next() { + char_pos += 1; + byte_end += c.len_utf8(); + } else { + break; + } + } + &self[byte_start..byte_end] + } + fn slice(&self, range: impl RangeBounds) -> &str { + let start = match range.start_bound() { + Bound::Included(bound) | Bound::Excluded(bound) => *bound, + Bound::Unbounded => 0, + }; + let len = match range.end_bound() { + Bound::Included(bound) => *bound + 1, + Bound::Excluded(bound) => *bound, + Bound::Unbounded => self.len(), + } - start; + self.substring(start, len) + } +} + #[derive(GraphQLQuery)] #[graphql( schema_path = "schemas/schema.graphql", @@ -49,44 +103,39 @@ pub mod routes { )] struct GetPost; +#[derive(TemplateOnce)] +#[template(path = "index.html")] +pub struct Post { + pub data: get_post::GetPostPost, + pub id: String, +} + #[my_codegen::get(path = "crate::V1_API_ROUTES.proxy.page")] -async fn page(path: web::Path<(String, String)>) -> impl Responder { +async fn page(path: web::Path<(String, String)>, data: AppData) -> impl Responder { let post_id = path.1.split("-").last(); if post_id.is_none() { - return HttpResponse::BadRequest(); + return HttpResponse::BadRequest().finish(); } let id = post_id.unwrap().to_string(); - let vars = get_post::Variables { id }; + let vars = get_post::Variables { id: id.clone() }; const URL: &str = "https://medium.com/_/graphql"; - let client = reqwest::Client::new(); - let res = post_graphql::(&client, URL, vars) + let res = post_graphql::(&data.client, URL, vars) .await .unwrap(); - println!("{:?}", res); - let response_data: get_post::ResponseData = res.data.expect("missing response data"); - for p in response_data - .post - .unwrap() - .content - .unwrap() - .body_model - .unwrap() - .paragraphs - .unwrap() - .iter() - { - println!("paragraph content: {:?}", p.as_ref().unwrap()); - } - // .bodyModel - // .paragraphs - // .iter(); - // println!("{:?}", response_data); + let page = Post { + id, + data: response_data.post.unwrap(), + } + .render_once() + .unwrap(); HttpResponse::Ok() + .content_type("text/html; charset=utf-8") + .body(page) } pub fn services(cfg: &mut web::ServiceConfig) { @@ -97,42 +146,22 @@ pub fn services(cfg: &mut web::ServiceConfig) { mod tests { use actix_web::{http::StatusCode, test, App}; - use crate::services; - use crate::*; + use crate::{services, Data}; - use super::*; + #[actix_rt::test] + async fn deploy_update_works() { + let data = Data::new(); + let app = test::init_service(App::new().app_data(data.clone()).configure(services)).await; + let urls = vec![ + "/@ftrain/big-data-small-effort-b62607a43a8c", + "/geekculture/rest-api-best-practices-decouple-long-running-tasks-from-http-request-processing-9fab2921ace8", + "/illumination/5-bugs-that-turned-into-features-e9a0e972a4e7", + ]; - // #[actix_rt::test] - // async fn deploy_update_works() { - // let app = test::init_service(App::new().configure(services)).await; - // - // let page = page.unwrap(); - // - // let mut payload = ProxyEvent { - // secret: page.secret.clone(), - // branch: page.branch.clone(), - // }; - // - // let resp = test::call_service( - // &app, - // test::TestRequest::post() - // .uri(V1_API_ROUTES.deploy.update) - // .set_json(&payload) - // .to_request(), - // ) - // .await; - // assert_eq!(resp.status(), StatusCode::OK); - // - // payload.secret = page.branch.clone(); - // - // let resp = test::call_service( - // &app, - // test::TestRequest::post() - // .uri(V1_API_ROUTES.deploy.update) - // .set_json(&payload) - // .to_request(), - // ) - // .await; - // assert_eq!(resp.status(), StatusCode::NOT_FOUND); - // } + for uri in urls.iter() { + let resp = + test::call_service(&app, test::TestRequest::get().uri(uri).to_request()).await; + assert_eq!(resp.status(), StatusCode::OK); + } + } } diff --git a/templates/img.html b/templates/img.html new file mode 100644 index 0000000..cd6e987 --- /dev/null +++ b/templates/img.html @@ -0,0 +1,9 @@ +<. let metadata = p.metadata.as_ref().unwrap(); .> + +<.= p.text .> diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..ffdff6c --- /dev/null +++ b/templates/index.html @@ -0,0 +1,37 @@ + + + + + + <.= data.title .> + + +
+

<.= data.title .>

+ <. use chrono::{TimeZone, Utc}; .> + <. let dt = Utc.timestamp_millis(data.created_at); .> +

+ + <.= data.creator.name .> + on <.= dt.format("%b %e, %Y").to_string() .> +

+
+ <. let paragraphs = data.content.body_model.paragraphs; .> + <. for (pindex, p) in paragraphs.iter().enumerate() {.> + <. if pindex == 1 && p.type_ == "H3" {.> + <. continue; .> + <.}.> + <. if p.type_ == "IMG" {.> + <. include!("./img.html"); .> + <.} else if p.type_ == "P" {.> + <. include!("./p.html"); .> + <.}.> + <.}.> +
+
+ + + diff --git a/templates/main.css b/templates/main.css new file mode 100644 index 0000000..da59c78 --- /dev/null +++ b/templates/main.css @@ -0,0 +1,27 @@ +* { + margin: 0; + padding: 0; +} + +body { + width: 100%; + display: flex; + flex-direction: column; +} + +main { + width: 60%; + margin: auto; + display: flex; + flex-direction: column; +} + +p { + margin: 20px 0; +} + +img { + margin: auto; + max-width: 80%; + /*! display: block; */ +} diff --git a/templates/matcher.rs b/templates/matcher.rs new file mode 100644 index 0000000..5f8842a --- /dev/null +++ b/templates/matcher.rs @@ -0,0 +1,15 @@ +match p.type_.as_str() { + "IMG" => { + include!("./img.html"); + }, + _ => unimplemented!(), + +} + +//<. match p.type_ { .> +// <. "IMG" => { .> +// <. include!("./img.html") .> +// <. }, .> +// <. _ => log::error("Unable to find paragraph render class. Post ID: {}. Paragraph item {:?}", id, p), +// <. } .> +// diff --git a/templates/p.html b/templates/p.html new file mode 100644 index 0000000..44e8f41 --- /dev/null +++ b/templates/p.html @@ -0,0 +1,44 @@ +

+<. if p.markups.is_empty() {.> +<.= p.text .> +<.} else {.> + <. let mut cur: usize = 0; .> + <. for markup in &p.markups {.> + <.= &p.text.substring(cur, (markup.start -1) as usize) .> + <. cur = (markup.end + 1) as usize; .> + <. let text = &p.text.substring(markup.start as usize, markup.end as usize); .> + <. if markup.type_ == "A" {.> + <. if let Some(anchor_type) = &markup.anchor_type {.> + <. if anchor_type == "LINK" {.> + <.= text .> + <.} else if anchor_type == "USER" {.> + + <.= text .> + + <.} else {.> + <. log::error!("unknown markup.anchor_type: {:?} post id {}", anchor_type, id); .> + <.= text .> + <.}.> + <.}.> + <.} else if markup.type_ == "EM" {.> + <.= text .> + <.} else if markup.type_ == "STRONG" {.> + <.= text .> + <.} else if markup.type_ == "CODE" {.> + <.= text .> + <.} else {.> + <. log::error!("unknown markup.type_: {:?} post id {}", markup.type_, id); .> + <.= text .> + <.}.> + + <. if cur < p.text.len() {.> + <.= p.text.slice(cur..) .> + <.}.> + <.}.> + +<.}.> +

+