finish timeago parser, refactor codegen

This commit is contained in:
ThetaDev 2022-09-05 21:43:43 +02:00
parent 500ea77788
commit 513bf1dc9c
12 changed files with 641 additions and 895 deletions

View file

@ -0,0 +1,102 @@
#![cfg(test)]
use std::{
collections::BTreeMap,
fmt::Debug,
fs::File,
io::{BufReader},
};
use crate::{model::Language, timeago::TimeUnit};
use fancy_regex::Regex;
use once_cell::sync::Lazy;
use serde::Deserialize;
const DICT_PATH: &str = "testfiles/date/dictionary.json";
const TARGET_FILE: &str = "src/dictionary.rs";
type Dictionary = BTreeMap<Language, DictEntry>;
#[derive(Debug, Deserialize)]
struct DictEntry {
#[serde(default)]
equivalent: Vec<Language>,
#[serde(default)]
by_char: bool,
timeago_tokens: BTreeMap<String, String>,
}
fn parse_tu(tu: &str) -> (u8, Option<TimeUnit>) {
static TU_PATTERN: Lazy<Regex> = Lazy::new(|| Regex::new(r"^(\d*)(\w?)$").unwrap());
match TU_PATTERN.captures(tu).unwrap() {
Some(cap) => (
cap.get(1).unwrap().as_str().parse().unwrap_or(1),
match cap.get(2).unwrap().as_str() {
"s" => Some(TimeUnit::Second),
"m" => Some(TimeUnit::Minute),
"h" => Some(TimeUnit::Hour),
"D" => Some(TimeUnit::Day),
"W" => Some(TimeUnit::Week),
"M" => Some(TimeUnit::Month),
"Y" => Some(TimeUnit::Year),
"" => None,
_ => panic!("invalid time unit: {}", tu),
},
),
None => panic!("invalid time unit: {}", tu),
}
}
fn read_dict() -> Dictionary {
let json_file = File::open(DICT_PATH).unwrap();
serde_json::from_reader(BufReader::new(json_file)).unwrap()
}
// #[test]
fn generate_dictionary() {
let dict = read_dict();
let code_head = r#"// This file is automatically generated. DO NOT EDIT.
use crate::{
model::Language,
timeago::{TaToken, TimeUnit},
};
"#;
let mut code_timeago_tokens = r#"#[rustfmt::skip]
pub(crate) fn get_timeago_tokens(lang: Language) -> phf::Map<&'static str, TaToken> {
match lang {
"#
.to_owned();
dict.iter().for_each(|(lang, entry)| {
// Create a map for the language
let mut map = phf_codegen::Map::<&str>::new();
entry.timeago_tokens.iter().for_each(|(txt, tu_str)| {
let (n, unit) = parse_tu(&tu_str);
match unit {
Some(unit) => map.entry(
&txt,
&format!("TaToken {{ n: {}, unit: Some(TimeUnit::{:?}) }}", n, unit),
),
None => map.entry(&txt, &format!("TaToken {{ n: {}, unit: None }}", n)),
};
});
let mut selector = format!("Language::{:?}", lang);
entry.equivalent.iter().for_each(|eq| {
selector += &format!(" | Language::{:?}", eq);
});
let code_map = &map.build().to_string().replace('\n', "\n ");
code_timeago_tokens += &format!("{} => {},\n ", selector, code_map);
});
code_timeago_tokens = code_timeago_tokens.trim_end().to_owned() + "\n }\n}\n";
let code = format!("{}\n{}", code_head, code_timeago_tokens);
std::fs::write(TARGET_FILE, code).unwrap();
}

351
src/codegen/gen_locales.rs Normal file
View file

@ -0,0 +1,351 @@
#![cfg(test)]
use std::collections::BTreeMap;
use std::path::Path;
use reqwest::Method;
use serde::{Deserialize, Serialize};
use serde_with::serde_as;
use serde_with::VecSkipError;
use crate::client::{ClientType, ContextYT, RustyTube};
#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct QLanguageMenu {
context: ContextYT,
}
#[serde_as]
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct LanguageMenu {
#[serde_as(as = "VecSkipError<_>")]
actions: Vec<ActionWrap>,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct ActionWrap {
open_popup_action: OpenPopupAction,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct OpenPopupAction {
popup: Popup,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct Popup {
multi_page_menu_renderer: MultiPageMenuRenderer<MenuSectionRenderer>,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct MultiPageMenuRenderer<T> {
sections: Vec<MenuSectionRendererWrap<T>>,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct MenuSectionRendererWrap<T> {
multi_page_menu_section_renderer: T,
}
#[serde_as]
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct MenuSectionRenderer {
#[serde_as(as = "VecSkipError<_>")]
items: Vec<CompactLinkRendererWrap>,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct CompactLinkRendererWrap {
compact_link_renderer: CompactLinkRenderer,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct CompactLinkRenderer {
icon: Icon,
service_endpoint: ServiceEndpoint<MenuAction>,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Icon {
pub icon_type: String,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct ServiceEndpoint<T> {
signal_service_endpoint: SignalServiceEndpoint<T>,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct SignalServiceEndpoint<T> {
actions: Vec<T>,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct MenuAction {
get_multi_page_menu_action: MultiPageMenuAction,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct MultiPageMenuAction {
menu: Menu,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct Menu {
multi_page_menu_renderer: MultiPageMenuRenderer<ItemSectionRenderer>,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct ItemSectionRenderer {
items: Vec<LanguageItemWrap>,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct LanguageItemWrap {
compact_link_renderer: LanguageItem,
}
#[serde_as]
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct LanguageItem {
#[serde_as(as = "crate::serializer::text::Text")]
title: String,
service_endpoint: ServiceEndpoint<LanguageCountryAction>,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct LanguageCountryAction {
#[serde(alias = "selectCountryCommand")]
select_language_command: LanguageCountryCommand,
}
#[derive(Clone, Debug, Deserialize)]
struct LanguageCountryCommand {
#[serde(alias = "gl")]
hl: String,
}
// #[test_log::test(tokio::test)]
#[allow(dead_code)]
async fn generate_locales() {
let (languages, countries) = get_locales().await;
let code_head = r#"// This file is automatically generated. DO NOT EDIT.
use std::{fmt::Display, str::FromStr};
use serde::{Deserialize, Serialize};
"#;
let code_foot = r#"impl Display for Language {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(
&serde_json::to_string(self).map_or("".to_owned(), |s| s[1..s.len() - 1].to_owned()),
)
}
}
impl Display for Country {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(
&serde_json::to_string(self).map_or("".to_owned(), |s| s[1..s.len() - 1].to_owned()),
)
}
}
impl FromStr for Language {
type Err = serde_json::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
serde_json::from_str(&format!("\"{}\"", s))
}
}
impl FromStr for Country {
type Err = serde_json::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
serde_json::from_str(&format!("\"{}\"", s))
}
}
"#;
let mut code_langs = r#"#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[serde(rename_all = "lowercase")]
pub enum Language {
"#.to_owned();
let mut code_countries = r#"#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[serde(rename_all = "UPPERCASE")]
pub enum Country {
"#.to_owned();
let mut code_lang_array = format!("pub const LANGUAGES: [Language; {}] = [\n", languages.len());
let mut code_country_array = format!("pub const COUNTRIES: [Country; {}] = [\n", countries.len());
let mut code_lang_names = r#"impl Language {
pub fn name(&self) -> &str {
match self {
"#
.to_owned();
let mut code_country_names = r#"impl Country {
pub fn name(&self) -> &str {
match self {
"#
.to_owned();
languages.iter().for_each(|(c, n)| {
let enum_name = c
.split('-')
.map(|c| {
format!(
"{}{}",
c[0..1].to_owned().to_uppercase(),
c[1..].to_owned().to_lowercase()
)
})
.collect::<String>();
// Language enum
code_langs += &format!(" /// {}\n ", n);
if c.contains('-') {
code_langs += &format!("#[serde(rename = \"{}\")]\n ", c);
}
code_langs += &enum_name;
code_langs += ",\n";
// Language array
code_lang_array += &format!(" Language::{},\n", enum_name);
// Language names
code_lang_names += &format!(" Language::{} => \"{}\",\n", enum_name, n);
});
code_langs += "}\n";
countries.iter().for_each(|(c, n)| {
let enum_name = c[0..1].to_owned().to_uppercase() + &c[1..].to_owned().to_lowercase();
// Country enum
code_countries += &format!(" /// {}\n", n);
code_countries += &format!(" {},\n", enum_name);
// Country array
code_country_array += &format!(" Country::{},\n", enum_name);
// Country names
code_country_names += &format!(" Country::{} => \"{}\",\n", enum_name, n);
});
code_countries += "}\n";
code_lang_array += "];\n";
code_country_array += "];\n";
code_lang_names += " }\n }\n}\n";
code_country_names += " }\n }\n}\n";
let code = format!(
"{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}",
code_head,
code_langs,
code_countries,
code_lang_array,
code_country_array,
code_lang_names,
code_country_names,
code_foot,
);
let locale_path = Path::new("src/model/locale.rs");
std::fs::write(locale_path, code).unwrap();
}
async fn get_locales() -> (BTreeMap<String, String>, BTreeMap<String, String>) {
let rt = RustyTube::new();
let client = rt.get_ytclient(ClientType::Desktop);
let context = client.get_context(true).await;
let request_body = QLanguageMenu { context };
let resp = client
.request_builder(Method::POST, "account/account_menu")
.await
.json(&request_body)
.send()
.await
.unwrap()
.error_for_status()
.unwrap();
let language_menu = resp.json::<LanguageMenu>().await.unwrap();
let lm_section = &language_menu.actions[0]
.open_popup_action
.popup
.multi_page_menu_renderer
.sections
.iter()
.find(|s| s.multi_page_menu_section_renderer.items.len() >= 2)
.unwrap();
let lang_section = lm_section
.multi_page_menu_section_renderer
.items
.iter()
.find(|s| s.compact_link_renderer.icon.icon_type == "TRANSLATE")
.unwrap();
let country_section = lm_section
.multi_page_menu_section_renderer
.items
.iter()
.find(|s| s.compact_link_renderer.icon.icon_type == "LANGUAGE")
.unwrap();
let languages = map_language_section(lang_section);
let countries = map_language_section(country_section);
(languages, countries)
}
fn map_language_section(section: &CompactLinkRendererWrap) -> BTreeMap<String, String> {
section
.compact_link_renderer
.service_endpoint
.signal_service_endpoint
.actions[0]
.get_multi_page_menu_action
.menu
.multi_page_menu_renderer
.sections[0]
.multi_page_menu_section_renderer
.items
.iter()
.map(|i| {
(
i.compact_link_renderer
.service_endpoint
.signal_service_endpoint
.actions[0]
.select_language_command
.hl
.to_owned(),
i.compact_link_renderer.title.to_owned(),
)
})
.collect::<BTreeMap<_, _>>()
}

3
src/codegen/mod.rs Normal file
View file

@ -0,0 +1,3 @@
#![cfg(test)]
mod gen_dictionary;
mod gen_locales;