dyer-cli 0.6.0

dyer-cli is a great tool created to guide you use dyer fast and at ease, helps you build a robust crawler, data processor, netwrok program fast and correctly.
use std::io::{BufRead, BufReader, Read, Write};
use std::str::FromStr;

#[derive(std::fmt::Debug)]
pub enum LogLevel {
    Error,
    Warn,
    Info,
    Debug,
    Trace,
}

impl FromStr for LogLevel {
    type Err = ();
    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s.to_lowercase().as_str() {
            "--error" => Ok(Self::Error),
            "--warn" => Ok(Self::Warn),
            "--info" => Ok(Self::Info),
            "--debug" => Ok(Self::Debug),
            "--trace" => Ok(Self::Trace),
            _ => Err(()),
        }
    }
}

pub fn get_package_name() -> String {
    let pat1 = regex::Regex::new(r"^\s*path\s*=.*?src/bin/(?P<pkg_name>[\w|-]+)\.rs").unwrap();
    let file = std::fs::File::open("./Cargo.toml").expect("Cargo.toml file cannot be found!");
    let reader = BufReader::new(file);
    for line in reader.lines() {
        let text = line.unwrap();
        if pat1.is_match(&text) {
            let name = pat1
                .captures(&text)
                .unwrap()
                .name("pkg_name")
                .unwrap()
                .as_str()
                .replace("-", "_");
            return name.into();
        }
    }
    panic!("the target file cannot be found, put `path = 'src/bin/<+target+>.rs'` in your Cargo.toml file!");
}

pub(crate) fn get_file_path<'a>(index: &'a str, name: String) -> String {
    match index {
        "readme" => name + "/README.md",
        "config" => name + "/dyer.cfg",
        "cargo" => name + "/Cargo.toml",
        "affix" => name + "/src/affix.rs",
        "entity" => name + "/src/entity.rs",
        "parser" => name + "/src/parser.rs",
        "actor" => name + "/src/actor.rs",
        "middleware" => name + "/src/middleware.rs",
        "pipeline" => name + "/src/pipeline.rs",
        _ => {
            println!("Invalid name: {}", index);
            panic!()
        }
    }
}

pub(crate) fn get_file_intro(index: &str) -> &str {
    match index {
        "readme" => {
            r#"<!-- 
-This is a markdown file generated by dyer-cli
- Instructions of the project specified here 
--!>"#
        }
        "affix" => {
            r#"use dyer::*;

#[dyer::affix]
pub struct Aff {} 

#[dyer::async_trait]
impl Affixor for Aff {
    async fn init(&mut self) { }
    async fn invoke(&mut self) -> Option<Request> {
        None
    }
    async fn after_invoke(&mut self) {}
    async fn before_parse(&mut self, _: Option<&mut Result<Response, MetaResponse>>) {}
    async fn parse(&mut self, _: Option<Result<Response, MetaResponse>>) -> Option<Affix> {
        None
    }
    async fn after_parse(&mut self) {}
    async fn close(&mut self) {}
}
"#
        }
        "entity" => {
            r#"use serde::{Deserialize, Serialize};

// the Entity to be used
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct Item1 {
    pub field1: String,
    pub field2: i32,
}

/* serve as a container for all entities, and generic parameter of dyer::App
 * attribute #[dyer::entity(entities)] mark the enum and use it as container to all data to be collected
 */
#[dyer::entity(entities)]
#[derive(Serialize, Debug, Clone)]
pub enum Entities {
    Item1(Item1),
}
"#
        }
        "middleware" => {
            r#"use crate::entity::*;
use dyer::*;

/* attribute #[dyer::middleware(attr)] mark the method and use it as that in `MiddleWare`
 * attr could be :
 *    handle_entity/handle_req/handle_task/handle_affix
 *    /handle_res/handle_err/handle_yerr
 */
#[dyer::middleware(handle_entity)]
pub async fn handle_entities(_items: &mut Vec<Entities>, _app: &mut App<Entities>) {}
"#
        }
        "parser" => {
            r#"use crate::entity::*;
use dyer::*;

/* note that call this function to parse via specifying task.parser:
 *     let task = Task::builder();
 *         ...
 *         .parser(parse_func)
 *         .body(Body::empty(), "actor_marker")
 *         .unwrap();
 * that means function `parse_func` is called to parse the Response.
 * attribute #[dyer::parser] mark the method and use it extract entities from `Response` whose
 * parser is  parse_func
 */
#[dyer::parser]
pub fn parse_func(_res: Response) -> Parsed<Entities> {
    Parsed::new()
}"#
        }
        "pipeline" => {
            r#"use dyer::*;
use crate::entity::*;

 /*
 * something to do before sending entities to pipeline
 * the return type inside `Option` requires complete path(starts with `std` or crate in `Cargo.toml`)
 * attribute #[dyer::pipeline(attr)] mark the method and use it as that in `PipeLine` 
 * attr could be:
 *    initializer/disposer/process_entity/process_yerr
 */
#[dyer::pipeline(initializer)]
async fn func_name(_app: &mut App<Entities>) -> Option<std::fs::File> 
{
    None
}
"#
        }
        "actor" => {
            r#"pub mod affix;
pub mod entity; 
pub mod middleware;
pub mod pipeline;
pub mod parser; 

use affix::*;
use entity::*;
use parser::*;
use dyer::*;

// attribute #[dyer::actor] mark the struct and use it as a type implemented trait `Actor`
#[dyer::actor]
pub struct MyActor {
    pub start_uri: String,
}

#[dyer::async_trait]
impl Actor<Entities, Aff> for MyActor {
    // create an instance 
    async fn new() -> Self {
        MyActor{
            start_uri: "https://example.com/some/path/to/site".into()
        }
    }

    // preparation before opening actor
    async fn open_actor(&mut self, _app: &mut App<Entities>) {}

    /* 
     * `Task` to be executed when starting `dyer`. Note that this function must reproduce a
     * non-empty vector, if not, the whole program will be left at blank.
     */
    async fn entry_task(&mut self) -> Result<Vec<Task>, Box<dyn std::error::Error>> {
        let task = Task::get( &self.start_uri ) 
            .parser(parse_func)
            // here the marker `MyActor` is the same as 
            // the type implemented trait `Acotr` 
            // change it if you like as long as it is unique
            .body(Body::empty(), "MyActor")
            .unwrap();
        Ok(vec![task])
    }

    /* the generator of `Affix`
     * `dyer` consume the returned `Request`, generate a `Response` fed to the closure
     * to generate a `Affix`
     */
    async fn entry_affix(&mut self) -> Option<Aff> {
        None
    }

    // preparation before closing actor
    async fn close_actor(&mut self, _app: &mut App<Entities>) {}
}"#
        }
        "cargo" => {
            r#"[package]
name = "<+name+>"
version = "0.1.0"
authors = ["your name"]
edition = "2018"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[lib]
name = "<+name+>"
path = "src/actor.rs"

[[bin]]
name = "<+name+>"
path = "src/bin/<+name+>.rs"

[dependencies]
dyer = { version = "3.3", features = [] }
serde = { version = "1.0", features = ["derive"] }
tokio = { version = "1.20", features = ["rt-multi-thread"]}
simple_logger = "1.11" "#
        }
        "config" => {
            r#"## ArgApp
skip: true,
spawn_task_max: 100,
buf_task: 10000,
round_entity: 50,
data_dir: data/
nap: 15.0,
join_gap: 7.0,

## ArgAffix
arg_affix.is_on: false,
arg_affix.affix_min: 0,
arg_affix.affix_max: 0,

## ArgRate
rate.cycle: 600.0,
rate.load: 99.0,
rate.rate_low: 0.333,
rate.err: 0,
rate.interval: 30.0,
"#
        }
        _ => "",
    }
}

pub(crate) fn run_command(cmd: &str, options: Vec<&str>) {
    let stdout = std::process::Command::new(cmd)
        .args(options)
        .stdout(std::process::Stdio::piped())
        .spawn()
        .unwrap()
        .stdout
        .ok_or_else(|| {
            std::io::Error::new(
                std::io::ErrorKind::Other,
                "Could not capture standard output.",
            )
        })
        .unwrap();

    let reader = BufReader::new(stdout);

    reader
        .lines()
        .filter_map(|line| line.ok())
        .filter(|line| line.find("src\\main.rs").is_none())
        .filter(|line| line.find("src/main.rs").is_none())
        .for_each(|line| println!("{}", line));
}

pub(crate) fn change_log_level(level: &str) {
    let mut file = std::fs::OpenOptions::new()
        .read(true)
        .open("src/bin/main.rs")
        .unwrap();
    let mut buf = String::new();
    file.read_to_string(&mut buf).unwrap();
    drop(file);
    let ll = level.strip_prefix("--").unwrap();
    let l = &("log::LevelFilter::".to_string() + &to_camelcase(ll));
    let buf = buf.replace("log::LevelFilter::Error", l);
    let buf = buf.replace("log::LevelFilter::Warn", l);
    let buf = buf.replace("log::LevelFilter::Info", l);
    let buf = buf.replace("log::LevelFilter::Debug", l);
    let buf = buf.replace("log::LevelFilter::Trace", l);
    let buf = buf.replace("log::LevelFilter::Off", l);
    let mut file = std::fs::OpenOptions::new()
        .truncate(true)
        .write(true)
        .open("src/bin/main.rs")
        .unwrap();
    file.write_all(buf.as_bytes()).unwrap();
}

pub(crate) fn to_camelcase(s: &str) -> String {
    let mut r = String::with_capacity(s.len());
    let mut ch = s[..].chars();
    let e = ch.next().unwrap().to_uppercase();
    r.push(e.to_string().chars().next().unwrap());
    while let Some(t) = ch.next() {
        r.push(t);
    }
    r
}