Split md_parser

This commit is contained in:
Joel Wachsler 2022-07-14 11:16:03 +00:00
parent 9b192a92f2
commit 85875abd2b
17 changed files with 382 additions and 368 deletions

View File

@ -7,10 +7,16 @@ keywords = ["qbittorrent"]
repository = "https://github.com/JoelWachsler/qbittorrent-web-api" repository = "https://github.com/JoelWachsler/qbittorrent-web-api"
description = "Generated web api for qBittorrent" description = "Generated web api for qBittorrent"
exclude = ["*.txt", "tests"] exclude = ["*.txt", "tests"]
# we use trybuild instead
autotests = false
[lib] [lib]
proc-macro = true proc-macro = true
[[test]]
name = "tests"
path = "tests/tests.rs"
[dependencies] [dependencies]
syn = { version = "1.0.98", features = ["extra-traits"]} syn = { version = "1.0.98", features = ["extra-traits"]}
quote = "1.0.20" quote = "1.0.20"

View File

@ -0,0 +1,127 @@
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum MdContent {
Text(String),
Asterix(String),
Table(Table),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Table {
pub header: TableRow,
pub split: String,
pub rows: Vec<TableRow>,
}
impl Table {
fn raw(&self) -> String {
let mut output = vec![self.header.raw.clone(), self.split.clone()];
for row in self.rows.clone() {
output.push(row.raw);
}
output.join("\n")
}
}
#[derive(Debug, Clone)]
pub struct Header {
pub level: i32,
pub content: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TableRow {
raw: String,
pub columns: Vec<String>,
}
impl MdContent {
pub fn inner_value_as_string(&self) -> String {
match self {
MdContent::Text(text) => text.into(),
MdContent::Asterix(text) => text.into(),
MdContent::Table(table) => table.raw(),
}
}
}
#[derive(Debug)]
pub enum MdToken {
Header(Header),
Content(MdContent),
}
impl MdToken {
fn parse_token(line: &str) -> MdToken {
if line.starts_with('#') {
let mut level = 0;
for char in line.chars() {
if char != '#' {
break;
}
level += 1;
}
MdToken::Header(Header {
level,
content: line.trim_matches('#').trim().to_string(),
})
} else if line.starts_with('*') {
MdToken::Content(MdContent::Asterix(
line.trim_matches('*').trim().to_string(),
))
} else {
MdToken::Content(MdContent::Text(line.to_string()))
}
}
pub fn from(content: &str) -> Vec<MdToken> {
let mut output = Vec::new();
let mut iter = content.lines();
while let Some(line) = iter.next() {
// assume this is a table
if line.contains('|') {
let to_columns = |column_line: &str| {
column_line
.replace('`', "")
.split('|')
.map(|s| s.trim().to_string())
.collect()
};
let table_header = TableRow {
raw: line.into(),
columns: to_columns(line),
};
let table_split = iter.next().unwrap();
let mut table_rows = Vec::new();
while let Some(row_line) = iter.next() {
if !row_line.contains('|') {
// we've reached the end of the table, let's go back one step
iter.next_back();
break;
}
let table_row = TableRow {
raw: row_line.into(),
columns: to_columns(row_line),
};
table_rows.push(table_row);
}
output.push(MdToken::Content(MdContent::Table(Table {
header: table_header,
split: table_split.to_string(),
rows: table_rows,
})));
} else {
output.push(MdToken::parse_token(line));
}
}
output
}
}

View File

@ -1,316 +1,7 @@
use std::{cell::RefCell, rc::Rc}; mod md_token;
mod token_tree;
mod token_tree_factory;
#[derive(Debug, Clone, PartialEq, Eq)] pub use md_token::*;
pub enum MdContent { pub use token_tree::TokenTree;
Text(String), pub use token_tree_factory::TokenTreeFactory;
Asterix(String),
Table(Table),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Table {
pub header: TableRow,
pub split: String,
pub rows: Vec<TableRow>,
}
impl Table {
fn raw(&self) -> String {
let mut output = vec![self.header.raw.clone(), self.split.clone()];
for row in self.rows.clone() {
output.push(row.raw);
}
output.join("\n")
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TableRow {
raw: String,
pub columns: Vec<String>,
}
impl MdContent {
pub fn inner_value_as_string(&self) -> String {
match self {
MdContent::Text(text) => text.into(),
MdContent::Asterix(text) => text.into(),
MdContent::Table(table) => table.raw(),
}
}
}
#[derive(Debug, Clone)]
pub struct Header {
level: i32,
content: String,
}
/// These are the only relevant tokens we need for the api generation.
#[derive(Debug)]
pub enum MdToken {
Header(Header),
Content(MdContent),
}
impl MdToken {
fn parse_token(line: &str) -> MdToken {
if line.starts_with('#') {
let mut level = 0;
for char in line.chars() {
if char != '#' {
break;
}
level += 1;
}
MdToken::Header(Header {
level,
content: line.trim_matches('#').trim().to_string(),
})
} else if line.starts_with('*') {
MdToken::Content(MdContent::Asterix(
line.trim_matches('*').trim().to_string(),
))
} else {
MdToken::Content(MdContent::Text(line.to_string()))
}
}
fn from(content: &str) -> Vec<MdToken> {
let mut output = Vec::new();
let mut iter = content.lines();
while let Some(line) = iter.next() {
// assume this is a table
if line.contains('|') {
let to_columns = |column_line: &str| {
column_line
.replace('`', "")
.split('|')
.map(|s| s.trim().to_string())
.collect()
};
let table_header = TableRow {
raw: line.into(),
columns: to_columns(line),
};
let table_split = iter.next().unwrap();
let mut table_rows = Vec::new();
while let Some(row_line) = iter.next() {
if !row_line.contains('|') {
// we've reached the end of the table, let's go back one step
iter.next_back();
break;
}
let table_row = TableRow {
raw: row_line.into(),
columns: to_columns(row_line),
};
table_rows.push(table_row);
}
output.push(MdToken::Content(MdContent::Table(Table {
header: table_header,
split: table_split.to_string(),
rows: table_rows,
})));
} else {
output.push(MdToken::parse_token(line));
}
}
output
}
}
#[derive(Debug)]
pub struct TokenTree {
pub title: Option<String>,
pub content: Vec<MdContent>,
pub children: Vec<TokenTree>,
}
impl From<Rc<TokenTreeFactory>> for TokenTree {
fn from(builder: Rc<TokenTreeFactory>) -> Self {
let children = builder
.children
.clone()
.into_inner()
.into_iter()
.map(|child| child.into())
.collect::<Vec<TokenTree>>();
let content = builder.content.clone().into_inner();
TokenTree {
title: builder.title.clone(),
content,
children,
}
}
}
#[derive(Debug, Default)]
pub struct TokenTreeFactory {
title: Option<String>,
content: RefCell<Vec<MdContent>>,
children: RefCell<Vec<Rc<TokenTreeFactory>>>,
level: i32,
}
impl TokenTreeFactory {
fn new(title: &str, level: i32) -> Self {
Self {
title: if title.is_empty() {
None
} else {
Some(title.to_string())
},
level,
..Default::default()
}
}
fn add_content(&self, content: MdContent) {
self.content.borrow_mut().push(content);
}
fn append(&self, child: &Rc<TokenTreeFactory>) {
self.children.borrow_mut().push(child.clone());
}
pub fn create(content: &str) -> TokenTree {
let tokens = MdToken::from(content);
let mut stack = Vec::new();
let root = Rc::new(TokenTreeFactory::default());
stack.push(root.clone());
for token in tokens {
match token {
MdToken::Header(Header { level, content }) => {
let new_header = Rc::new(TokenTreeFactory::new(&content, level));
// go back until we're at the same or lower level.
while let Some(current) = stack.pop() {
if current.level < level {
current.append(&new_header);
stack.push(current);
break;
}
}
stack.push(new_header.clone());
}
MdToken::Content(content) => {
let current = stack.pop().unwrap();
current.add_content(content);
stack.push(current);
}
}
}
root.into()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn should_remove_surrounding_asterix() {
// given
let input = r#"
# A
**B**
"#
.trim_matches('\n')
.trim();
// when
let tree = TokenTreeFactory::create(input);
// then
println!("{:#?}", tree);
let first = tree.children.first().unwrap();
let content = first.content.first().unwrap();
assert_eq!(*content, MdContent::Asterix("B".into()));
}
#[test]
fn should_remove_surrounding_hash() {
// given
let input = r#"
# A #
"#
.trim_matches('\n')
.trim();
// when
let tree = TokenTreeFactory::create(input);
// then
println!("{:#?}", tree);
assert_eq!(tree.children.first().unwrap().title, Some("A".into()));
}
#[test]
fn single_level() {
// given
let input = r#"
# A
Foo
"#
.trim_matches('\n')
.trim();
// when
let tree = TokenTreeFactory::create(input);
// then
println!("{:#?}", tree);
assert_eq!(tree.title, None);
let first_child = tree.children.first().unwrap();
assert_eq!(first_child.title, Some("A".into()));
}
#[test]
fn complex() {
// given
let input = r#"
# A
Foo
## B
# C
## D
Bar
"#
.trim_matches('\n')
.trim();
// when
let tree = TokenTreeFactory::create(input);
// then
println!("{:#?}", tree);
assert_eq!(tree.title, None);
assert_eq!(tree.children.len(), 2);
let first = tree.children.get(0).unwrap();
assert_eq!(first.title, Some("A".into()));
assert_eq!(first.children.len(), 1);
assert_eq!(first.children.first().unwrap().title, Some("B".into()));
let second = tree.children.get(1).unwrap();
assert_eq!(second.title, Some("C".into()));
assert_eq!(second.children.len(), 1);
assert_eq!(second.children.first().unwrap().title, Some("D".into()));
}
}

View File

@ -0,0 +1,30 @@
use std::rc::Rc;
use super::{md_token::MdContent, token_tree_factory::TokenTreeFactory};
#[derive(Debug)]
pub struct TokenTree {
pub title: Option<String>,
pub content: Vec<MdContent>,
pub children: Vec<TokenTree>,
}
impl From<Rc<TokenTreeFactory>> for TokenTree {
fn from(builder: Rc<TokenTreeFactory>) -> Self {
let children = builder
.children
.clone()
.into_inner()
.into_iter()
.map(|child| child.into())
.collect::<Vec<TokenTree>>();
let content = builder.content.clone().into_inner();
TokenTree {
title: builder.title.clone(),
content,
children,
}
}
}

View File

@ -0,0 +1,166 @@
use std::{cell::RefCell, rc::Rc};
use super::{
md_token::{Header, MdContent, MdToken},
token_tree::TokenTree,
};
#[derive(Debug, Default)]
pub struct TokenTreeFactory {
pub title: Option<String>,
pub content: RefCell<Vec<MdContent>>,
pub children: RefCell<Vec<Rc<TokenTreeFactory>>>,
pub level: i32,
}
impl TokenTreeFactory {
fn new(title: &str, level: i32) -> Self {
Self {
title: if title.is_empty() {
None
} else {
Some(title.to_string())
},
level,
..Default::default()
}
}
fn add_content(&self, content: MdContent) {
self.content.borrow_mut().push(content);
}
fn append(&self, child: &Rc<TokenTreeFactory>) {
self.children.borrow_mut().push(child.clone());
}
pub fn create(content: &str) -> TokenTree {
let tokens = MdToken::from(content);
let mut stack = Vec::new();
let root = Rc::new(TokenTreeFactory::default());
stack.push(root.clone());
for token in tokens {
match token {
MdToken::Header(Header { level, content }) => {
let new_header = Rc::new(TokenTreeFactory::new(&content, level));
// go back until we're at the same or lower level.
while let Some(current) = stack.pop() {
if current.level < level {
current.append(&new_header);
stack.push(current);
break;
}
}
stack.push(new_header.clone());
}
MdToken::Content(content) => {
let current = stack.pop().unwrap();
current.add_content(content);
stack.push(current);
}
}
}
root.into()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn should_remove_surrounding_asterix() {
// given
let input = r#"
# A
**B**
"#
.trim_matches('\n')
.trim();
// when
let tree = TokenTreeFactory::create(input);
// then
println!("{:#?}", tree);
let first = tree.children.first().unwrap();
let content = first.content.first().unwrap();
assert_eq!(*content, MdContent::Asterix("B".into()));
}
#[test]
fn should_remove_surrounding_hash() {
// given
let input = r#"
# A #
"#
.trim_matches('\n')
.trim();
// when
let tree = TokenTreeFactory::create(input);
// then
println!("{:#?}", tree);
assert_eq!(tree.children.first().unwrap().title, Some("A".into()));
}
#[test]
fn single_level() {
// given
let input = r#"
# A
Foo
"#
.trim_matches('\n')
.trim();
// when
let tree = TokenTreeFactory::create(input);
// then
println!("{:#?}", tree);
assert_eq!(tree.title, None);
let first_child = tree.children.first().unwrap();
assert_eq!(first_child.title, Some("A".into()));
}
#[test]
fn complex() {
// given
let input = r#"
# A
Foo
## B
# C
## D
Bar
"#
.trim_matches('\n')
.trim();
// when
let tree = TokenTreeFactory::create(input);
// then
println!("{:#?}", tree);
assert_eq!(tree.title, None);
assert_eq!(tree.children.len(), 2);
let first = tree.children.get(0).unwrap();
assert_eq!(first.title, Some("A".into()));
assert_eq!(first.children.len(), 1);
assert_eq!(first.children.first().unwrap().title, Some("B".into()));
let second = tree.children.get(1).unwrap();
assert_eq!(second.title, Some("C".into()));
assert_eq!(second.children.len(), 1);
assert_eq!(second.children.first().unwrap().title, Some("D".into()));
}
}

View File

@ -1,9 +1,8 @@
use anyhow::Result; mod common;
use qbittorrent_web_api_gen::QBittorrentApiGen;
const USERNAME: &str = "admin"; use anyhow::Result;
const PASSWORD: &str = "adminadmin"; use common::*;
const BASE_URL: &str = "http://localhost:8080"; use qbittorrent_web_api_gen::QBittorrentApiGen;
#[derive(QBittorrentApiGen)] #[derive(QBittorrentApiGen)]
struct Api {} struct Api {}

View File

@ -1,9 +1,8 @@
use anyhow::Result; mod common;
use qbittorrent_web_api_gen::QBittorrentApiGen;
const USERNAME: &str = "admin"; use anyhow::Result;
const PASSWORD: &str = "adminadmin"; use common::*;
const BASE_URL: &str = "http://localhost:8080"; use qbittorrent_web_api_gen::QBittorrentApiGen;
#[derive(QBittorrentApiGen)] #[derive(QBittorrentApiGen)]
struct Api {} struct Api {}

View File

@ -1,9 +1,8 @@
use anyhow::Result; mod common;
use qbittorrent_web_api_gen::QBittorrentApiGen;
const USERNAME: &str = "admin"; use anyhow::Result;
const PASSWORD: &str = "adminadmin"; use common::*;
const BASE_URL: &str = "http://localhost:8080"; use qbittorrent_web_api_gen::QBittorrentApiGen;
#[derive(QBittorrentApiGen)] #[derive(QBittorrentApiGen)]
struct Foo {} struct Foo {}

View File

@ -0,0 +1,3 @@
pub const USERNAME: &str = "admin";
pub const PASSWORD: &str = "adminadmin";
pub const BASE_URL: &str = "http://localhost:8080";

View File

@ -1,9 +1,8 @@
use anyhow::Result; mod common;
use qbittorrent_web_api_gen::QBittorrentApiGen;
const USERNAME: &str = "admin"; use anyhow::Result;
const PASSWORD: &str = "adminadmin"; use common::*;
const BASE_URL: &str = "http://localhost:8080"; use qbittorrent_web_api_gen::QBittorrentApiGen;
#[derive(QBittorrentApiGen)] #[derive(QBittorrentApiGen)]
struct Api {} struct Api {}

View File

@ -1,9 +1,8 @@
use anyhow::Result; mod common;
use qbittorrent_web_api_gen::QBittorrentApiGen;
const USERNAME: &str = "admin"; use anyhow::Result;
const PASSWORD: &str = "adminadmin"; use common::*;
const BASE_URL: &str = "http://localhost:8080"; use qbittorrent_web_api_gen::QBittorrentApiGen;
#[derive(QBittorrentApiGen)] #[derive(QBittorrentApiGen)]
struct Api {} struct Api {}

View File

@ -1,9 +1,8 @@
use anyhow::Result; mod common;
use qbittorrent_web_api_gen::QBittorrentApiGen;
const USERNAME: &str = "admin"; use anyhow::Result;
const PASSWORD: &str = "adminadmin"; use common::*;
const BASE_URL: &str = "http://localhost:8080"; use qbittorrent_web_api_gen::QBittorrentApiGen;
#[derive(QBittorrentApiGen)] #[derive(QBittorrentApiGen)]
struct Api {} struct Api {}

View File

@ -1,9 +1,8 @@
use anyhow::Result; mod common;
use qbittorrent_web_api_gen::QBittorrentApiGen;
const USERNAME: &str = "admin"; use anyhow::Result;
const PASSWORD: &str = "adminadmin"; use common::*;
const BASE_URL: &str = "http://localhost:8080"; use qbittorrent_web_api_gen::QBittorrentApiGen;
#[derive(QBittorrentApiGen)] #[derive(QBittorrentApiGen)]
struct Api {} struct Api {}

View File

@ -1,10 +1,9 @@
use anyhow::Result; mod common;
use qbittorrent_web_api_gen::QBittorrentApiGen;
use tokio::time::{sleep, Duration};
const USERNAME: &str = "admin"; use anyhow::Result;
const PASSWORD: &str = "adminadmin"; use common::*;
const BASE_URL: &str = "http://localhost:8080"; use qbittorrent_web_api_gen::QBittorrentApiGen;
use tokio::time::*;
#[derive(QBittorrentApiGen)] #[derive(QBittorrentApiGen)]
struct Api {} struct Api {}

View File

@ -1,10 +1,9 @@
use anyhow::Result; mod common;
use qbittorrent_web_api_gen::QBittorrentApiGen;
use tokio::time::{sleep, Duration};
const USERNAME: &str = "admin"; use anyhow::Result;
const PASSWORD: &str = "adminadmin"; use common::*;
const BASE_URL: &str = "http://localhost:8080"; use qbittorrent_web_api_gen::QBittorrentApiGen;
use tokio::time::*;
#[derive(QBittorrentApiGen)] #[derive(QBittorrentApiGen)]
struct Api {} struct Api {}

View File

@ -1,6 +1,7 @@
#[test] #[test]
fn tests() { fn tests() {
let t = trybuild::TestCases::new(); let t = trybuild::TestCases::new();
// --- Auth --- // --- Auth ---
t.pass("tests/login.rs"); t.pass("tests/login.rs");
t.pass("tests/logout.rs"); t.pass("tests/logout.rs");
@ -19,5 +20,5 @@ fn tests() {
t.pass("tests/add_torrent.rs"); t.pass("tests/add_torrent.rs");
t.pass("tests/another_struct_name.rs"); t.pass("tests/another_struct_name.rs");
t.pass("tests/access_impl_types.rs"); t.pass("tests/access_impl_types.rs");
t.pass("tests/search_types.rs"); // t.pass("tests/search_types.rs");
} }

View File

@ -1,9 +1,8 @@
use anyhow::Result; mod common;
use qbittorrent_web_api_gen::QBittorrentApiGen;
const USERNAME: &str = "admin"; use anyhow::Result;
const PASSWORD: &str = "adminadmin"; use common::*;
const BASE_URL: &str = "http://localhost:8080"; use qbittorrent_web_api_gen::QBittorrentApiGen;
#[derive(QBittorrentApiGen)] #[derive(QBittorrentApiGen)]
struct Api {} struct Api {}