added cloude generated tests
parent
0c10ac9748
commit
4706de1b42
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(cargo test *)"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Commands
|
||||
|
||||
```bash
|
||||
cargo build # debug build
|
||||
cargo build --release # release build
|
||||
cargo run -- <args> # run with args (see CLI section)
|
||||
cargo check # fast type/borrow check without linking
|
||||
cargo clippy # lint
|
||||
```
|
||||
|
||||
No tests exist yet in this project.
|
||||
|
||||
## Configuration
|
||||
|
||||
The binary reads environment config from `~/.config/rcc/<env>` (dotenv format). Default env is `local`.
|
||||
|
||||
Required keys: `DB_USERNAME`, `DB_PASSWORD`
|
||||
Optional keys: `DB_LOCATION` (default: localhost), `DB_PORT` (default: 3306), `DB` (default: efulfilment)
|
||||
|
||||
Pass `-e <env>` to select a config file (e.g., `-e stage` loads `~/.config/rcc/stage`).
|
||||
|
||||
## Architecture
|
||||
|
||||
Three-layer structure: **CLI → Service → Repository**, wired together in `container.rs`.
|
||||
|
||||
- `src/cli/command.rs` — Clap structs defining all subcommands (`Merch`, `Schema`, `Filter`, `Page`, `Job`, `Use`)
|
||||
- `src/cli/controller.rs` — dispatches parsed args to the appropriate service
|
||||
- `src/container.rs` — factory functions that construct `Repo → Service` pairs from a `Db` instance
|
||||
- `src/database/db.rs` — `Db` wraps a `Arc<Mutex<Pool>>` (mysql); `Db::initialize(env)` loads the dotenv config and opens a connection pool
|
||||
- `src/repository/` — one repo per domain entity; each holds a `Db` and executes raw MySQL queries
|
||||
- `src/service/` — one service per domain; holds the corresponding repo, formats and prints results to stdout
|
||||
- `src/entity/` — plain structs with `mysql::FromRow` derives representing DB rows
|
||||
- `src/lib.rs` — exports the `TerminalSize` trait (used by services to adapt output width to the terminal)
|
||||
|
||||
## CLI Usage
|
||||
|
||||
```
|
||||
rcc [-e <env>] <subcommand>
|
||||
|
||||
merch <search> # search merchants by name
|
||||
schema <search> # list columns for a table
|
||||
filter <id> [-a] [-c] [-l] # filter info; -a=all, -c=config, -l=log
|
||||
page <id> # page/file info
|
||||
job <id> [-l] # job info; -l=log
|
||||
use <file_name> # find filters that use a given file/module
|
||||
```
|
||||
|
|
@ -48,3 +48,93 @@ pub enum Commands {
|
|||
file_name: String,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use clap::Parser;
|
||||
|
||||
fn parse(args: &[&str]) -> Cli {
|
||||
Cli::try_parse_from(args).expect("parse failed")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn merch_command() {
|
||||
let cli = parse(&["rcc", "merch", "acme"]);
|
||||
assert!(matches!(cli.mode, Commands::Merch { search: Some(ref s) } if s == "acme"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn schema_command() {
|
||||
let cli = parse(&["rcc", "schema", "order_id"]);
|
||||
assert!(matches!(cli.mode, Commands::Schema { search: Some(ref s) } if s == "order_id"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn filter_command_defaults() {
|
||||
let cli = parse(&["rcc", "filter", "42"]);
|
||||
assert!(matches!(
|
||||
cli.mode,
|
||||
Commands::Filter { filter_id: 42, all: false, config: false, log: false }
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn filter_command_all_flag() {
|
||||
let cli = parse(&["rcc", "filter", "42", "--all"]);
|
||||
assert!(matches!(cli.mode, Commands::Filter { all: true, .. }));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn filter_command_config_flag() {
|
||||
let cli = parse(&["rcc", "filter", "42", "--config"]);
|
||||
assert!(matches!(cli.mode, Commands::Filter { config: true, .. }));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn filter_command_log_flag() {
|
||||
let cli = parse(&["rcc", "filter", "42", "--log"]);
|
||||
assert!(matches!(cli.mode, Commands::Filter { log: true, .. }));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn page_command() {
|
||||
let cli = parse(&["rcc", "page", "7"]);
|
||||
assert!(matches!(cli.mode, Commands::Page { page_id: 7 }));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn job_command_default() {
|
||||
let cli = parse(&["rcc", "job", "3"]);
|
||||
assert!(matches!(cli.mode, Commands::Job { job_id: 3, log: false }));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn job_command_log_flag() {
|
||||
let cli = parse(&["rcc", "job", "3", "--log"]);
|
||||
assert!(matches!(cli.mode, Commands::Job { job_id: 3, log: true }));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn use_command() {
|
||||
let cli = parse(&["rcc", "use", "my_module.php"]);
|
||||
assert!(matches!(cli.mode, Commands::Use { ref file_name } if file_name == "my_module.php"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn env_flag_is_captured() {
|
||||
let cli = parse(&["rcc", "-e", "stage", "merch", "x"]);
|
||||
assert_eq!(cli.env.as_deref(), Some("stage"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn env_defaults_to_none_when_not_provided() {
|
||||
let cli = parse(&["rcc", "merch", "x"]);
|
||||
assert!(cli.env.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unknown_subcommand_is_rejected() {
|
||||
assert!(Cli::try_parse_from(["rcc", "bogus"]).is_err());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,6 @@
|
|||
use crate::{
|
||||
container,
|
||||
database::db::Db,
|
||||
service::{
|
||||
file_service::FileService, filter_service::FilterService, job_service::JobService,
|
||||
merchant_service::MerchantService, schema_service::SchemaService,
|
||||
},
|
||||
};
|
||||
use std::io;
|
||||
|
||||
use crate::{container, database::db::Db};
|
||||
|
||||
use super::command::{Cli, Commands};
|
||||
|
||||
|
|
@ -18,14 +13,17 @@ enum CommandError {
|
|||
}
|
||||
|
||||
pub fn process_args(args: Cli, db: Db) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let stdout = io::stdout();
|
||||
let mut out = stdout.lock();
|
||||
|
||||
match args.mode {
|
||||
Commands::Merch { search } => {
|
||||
let mut merchant_service: MerchantService = container::get_merchant_service(db);
|
||||
merchant_service.get_merchant(&search.ok_or(CommandError::EmptySearch())?)?;
|
||||
let mut merchant_service = container::get_merchant_service(db);
|
||||
merchant_service.get_merchant(&search.ok_or(CommandError::EmptySearch())?, &mut out)?;
|
||||
}
|
||||
Commands::Schema { search } => {
|
||||
let mut schema_service: SchemaService = container::get_schema_service(db);
|
||||
schema_service.get_columns(&search.ok_or(CommandError::EmptySearch())?)?;
|
||||
let mut schema_service = container::get_schema_service(db);
|
||||
schema_service.get_columns(&search.ok_or(CommandError::EmptySearch())?, &mut out)?;
|
||||
}
|
||||
Commands::Filter {
|
||||
filter_id,
|
||||
|
|
@ -33,31 +31,31 @@ pub fn process_args(args: Cli, db: Db) -> Result<(), Box<dyn std::error::Error>>
|
|||
all,
|
||||
log,
|
||||
} => {
|
||||
let mut filter_service: FilterService = container::get_filter_service(db);
|
||||
let mut filter_service = container::get_filter_service(db);
|
||||
if config {
|
||||
filter_service.get_filter_configs(&filter_id)?;
|
||||
filter_service.get_filter_configs(&filter_id, &mut out)?;
|
||||
} else if log {
|
||||
filter_service.get_filter_log(&filter_id, all)?;
|
||||
filter_service.get_filter_log(&filter_id, all, &mut out)?;
|
||||
} else {
|
||||
filter_service.get_filter(&filter_id, all)?;
|
||||
filter_service.get_filter(&filter_id, all, &mut out)?;
|
||||
}
|
||||
}
|
||||
Commands::Page { page_id } => {
|
||||
let mut file_service: FileService = container::get_page_service(db);
|
||||
file_service.get_page(&page_id)?;
|
||||
let mut file_service = container::get_page_service(db);
|
||||
file_service.get_page(&page_id, &mut out)?;
|
||||
}
|
||||
Commands::Job { job_id, log } => {
|
||||
let mut job_service: JobService = container::get_job_service(db);
|
||||
let mut job_service = container::get_job_service(db);
|
||||
if log {
|
||||
job_service.get_job_log(&job_id)?;
|
||||
job_service.get_job_log(&job_id, &mut out)?;
|
||||
} else {
|
||||
job_service.get_job_by_id(&job_id)?;
|
||||
job_service.get_job_by_id(&job_id, &mut out)?;
|
||||
}
|
||||
}
|
||||
Commands::Use { file_name } => {
|
||||
let mut filter_service: FilterService = container::get_filter_service(db);
|
||||
filter_service.get_filter_uses(&file_name)?;
|
||||
},
|
||||
let mut filter_service = container::get_filter_service(db);
|
||||
filter_service.get_filter_uses(&file_name, &mut out)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
|
|
|||
|
|
@ -10,27 +10,27 @@ use crate::{
|
|||
},
|
||||
};
|
||||
|
||||
pub fn get_filter_service(pool: Db) -> FilterService {
|
||||
pub fn get_filter_service(pool: Db) -> FilterService<FilterRepo> {
|
||||
let repo = FilterRepo::new(pool);
|
||||
FilterService::new(repo)
|
||||
}
|
||||
|
||||
pub fn get_merchant_service(pool: Db) -> MerchantService {
|
||||
pub fn get_merchant_service(pool: Db) -> MerchantService<MerchantRepo> {
|
||||
let repo = MerchantRepo::new(pool);
|
||||
MerchantService::new(repo)
|
||||
}
|
||||
|
||||
pub fn get_schema_service(pool: Db) -> SchemaService {
|
||||
pub fn get_schema_service(pool: Db) -> SchemaService<SchemaRepo> {
|
||||
let repo = SchemaRepo::new(pool);
|
||||
SchemaService::new(repo)
|
||||
}
|
||||
|
||||
pub fn get_page_service(pool: Db) -> FileService {
|
||||
pub fn get_page_service(pool: Db) -> FileService<FileRepo> {
|
||||
let repo = FileRepo::new(pool);
|
||||
FileService::new(repo)
|
||||
}
|
||||
|
||||
pub(crate) fn get_job_service(pool: Db) -> JobService {
|
||||
pub fn get_job_service(pool: Db) -> JobService<JobRepo> {
|
||||
let repo = JobRepo::new(pool);
|
||||
JobService::new(repo)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -69,13 +69,26 @@ impl Db {
|
|||
|
||||
impl Drop for Db {
|
||||
fn drop(&mut self) {
|
||||
// The drop function is called when the Db instance goes out of scope
|
||||
// This is where you can clean up resources, like returning connections to the pool
|
||||
log::info!("Db instance is being dropped. Cleaning up resources.");
|
||||
|
||||
// You might want to explicitly drop the Mutex guard to release the lock
|
||||
// (This is not strictly necessary as the Mutex will be automatically dropped,
|
||||
// but it can be useful for explicitness)
|
||||
drop(self.pool.lock().unwrap());
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn initialize_fails_when_config_file_missing() {
|
||||
let result = Db::initialize("__nonexistent_env__".to_string());
|
||||
assert!(result.is_err());
|
||||
let msg = result.unwrap_err().to_string();
|
||||
assert!(msg.contains("__nonexistent_env__"), "error should mention the env name: {msg}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn initialize_fails_for_empty_env_name() {
|
||||
let result = Db::initialize(String::new());
|
||||
assert!(result.is_err());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
#[derive(Debug, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Page {
|
||||
pub m_id: usize,
|
||||
pub title: String,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
#[derive(Debug, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Filter {
|
||||
pub filter_module_id: usize,
|
||||
pub file_name: String,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
use time::PrimitiveDateTime;
|
||||
#[derive(Debug, PartialEq)]
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct FilterConfig {
|
||||
pub attribute: String,
|
||||
pub value1: String,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
use time::PrimitiveDateTime;
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct FilterLog {
|
||||
pub run_ts: PrimitiveDateTime,
|
||||
pub error_code: String,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
#[derive(Debug, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct FilterUses {
|
||||
pub filter_module_id: usize,
|
||||
pub description: String,
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
use time::PrimitiveDateTime;
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Job {
|
||||
pub file_name: String,
|
||||
pub description: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct JobLog {
|
||||
pub content: String,
|
||||
pub upd_ts: PrimitiveDateTime,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Merchant {
|
||||
pub m_id: usize,
|
||||
pub m_name: String,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Schema {
|
||||
pub table_name: String,
|
||||
pub column_name: String,
|
||||
|
|
|
|||
21
src/lib.rs
21
src/lib.rs
|
|
@ -6,3 +6,24 @@ pub trait TerminalSize {
|
|||
terminal.cols.into()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
struct Probe;
|
||||
impl TerminalSize for Probe {}
|
||||
|
||||
#[test]
|
||||
fn get_width_returns_positive_value() {
|
||||
assert!(Probe.get_width() > 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_width_falls_back_to_150_when_no_tty() {
|
||||
// In a non-TTY test environment termsize::get() returns None,
|
||||
// so the fallback of 150 columns is used.
|
||||
let w = Probe.get_width();
|
||||
assert!(w >= 1);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,11 +2,21 @@ use mysql::{params, prelude::Queryable};
|
|||
|
||||
use crate::{database::db::Db, entity::file::Page};
|
||||
|
||||
pub trait FileRepoTrait {
|
||||
fn find_page(&mut self, file_id: &usize) -> Result<Vec<Page>, mysql::Error>;
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct FileRepo {
|
||||
db_pool: Db,
|
||||
}
|
||||
|
||||
impl FileRepoTrait for FileRepo {
|
||||
fn find_page(&mut self, file_id: &usize) -> Result<Vec<Page>, mysql::Error> {
|
||||
self.find_page(file_id)
|
||||
}
|
||||
}
|
||||
|
||||
impl FileRepo {
|
||||
pub fn new(db: Db) -> Self {
|
||||
Self { db_pool: db }
|
||||
|
|
|
|||
|
|
@ -9,11 +9,33 @@ use crate::{
|
|||
},
|
||||
};
|
||||
|
||||
pub trait FilterRepoTrait {
|
||||
fn find_by_id(&mut self, filter_id: &usize) -> Result<Vec<Filter>, mysql::Error>;
|
||||
fn find_filter_configs(&mut self, filter_id: &usize) -> Result<Vec<FilterConfig>, mysql::Error>;
|
||||
fn find_filter_log(&mut self, filter_id: &usize) -> Result<Vec<FilterLog>, mysql::Error>;
|
||||
fn find_filter_uses(&mut self, filter_file: &str) -> Result<Vec<FilterUses>, mysql::Error>;
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct FilterRepo {
|
||||
db: Db,
|
||||
}
|
||||
|
||||
impl FilterRepoTrait for FilterRepo {
|
||||
fn find_by_id(&mut self, filter_id: &usize) -> Result<Vec<Filter>, mysql::Error> {
|
||||
self.find_by_id(filter_id)
|
||||
}
|
||||
fn find_filter_configs(&mut self, filter_id: &usize) -> Result<Vec<FilterConfig>, mysql::Error> {
|
||||
self.find_filter_configs(filter_id)
|
||||
}
|
||||
fn find_filter_log(&mut self, filter_id: &usize) -> Result<Vec<FilterLog>, mysql::Error> {
|
||||
self.find_filter_log(filter_id)
|
||||
}
|
||||
fn find_filter_uses(&mut self, filter_file: &str) -> Result<Vec<FilterUses>, mysql::Error> {
|
||||
self.find_filter_uses(filter_file)
|
||||
}
|
||||
}
|
||||
|
||||
impl FilterRepo {
|
||||
pub fn new(db: Db) -> Self {
|
||||
Self { db }
|
||||
|
|
|
|||
|
|
@ -5,11 +5,25 @@ use crate::{
|
|||
entity::job::{Job, JobLog},
|
||||
};
|
||||
|
||||
pub trait JobRepoTrait {
|
||||
fn find_by_id(&mut self, job_id: &usize) -> Result<Vec<Job>, mysql::Error>;
|
||||
fn find_log(&mut self, job_id: &usize) -> Result<Vec<JobLog>, mysql::Error>;
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct JobRepo {
|
||||
db: Db,
|
||||
}
|
||||
|
||||
impl JobRepoTrait for JobRepo {
|
||||
fn find_by_id(&mut self, job_id: &usize) -> Result<Vec<Job>, mysql::Error> {
|
||||
self.find_by_id(job_id)
|
||||
}
|
||||
fn find_log(&mut self, job_id: &usize) -> Result<Vec<JobLog>, mysql::Error> {
|
||||
self.find_log(job_id)
|
||||
}
|
||||
}
|
||||
|
||||
impl JobRepo {
|
||||
pub fn new(db: Db) -> Self {
|
||||
Self { db }
|
||||
|
|
|
|||
|
|
@ -2,10 +2,20 @@ use mysql::{params, prelude::Queryable};
|
|||
|
||||
use crate::{database::db::Db, entity::merchant::Merchant};
|
||||
|
||||
pub trait MerchantRepoTrait {
|
||||
fn find_by_name_or_id(&mut self, search: &str) -> Result<Vec<Merchant>, mysql::Error>;
|
||||
}
|
||||
|
||||
pub struct MerchantRepo {
|
||||
db_pool: Db,
|
||||
}
|
||||
|
||||
impl MerchantRepoTrait for MerchantRepo {
|
||||
fn find_by_name_or_id(&mut self, search: &str) -> Result<Vec<Merchant>, mysql::Error> {
|
||||
self.find_by_name_or_id(search)
|
||||
}
|
||||
}
|
||||
|
||||
impl MerchantRepo {
|
||||
pub fn new(db_pool: Db) -> Self {
|
||||
Self { db_pool }
|
||||
|
|
|
|||
|
|
@ -2,10 +2,20 @@ use mysql::{params, prelude::Queryable};
|
|||
|
||||
use crate::{database::db::Db, entity::schema::Schema};
|
||||
|
||||
pub trait SchemaRepoTrait {
|
||||
fn find_by_column(&mut self, column_name: &str) -> Result<Vec<Schema>, mysql::Error>;
|
||||
}
|
||||
|
||||
pub struct SchemaRepo {
|
||||
db: Db,
|
||||
}
|
||||
|
||||
impl SchemaRepoTrait for SchemaRepo {
|
||||
fn find_by_column(&mut self, column_name: &str) -> Result<Vec<Schema>, mysql::Error> {
|
||||
self.find_by_column(column_name)
|
||||
}
|
||||
}
|
||||
|
||||
impl SchemaRepo {
|
||||
pub fn new(db: Db) -> Self {
|
||||
Self { db }
|
||||
|
|
|
|||
|
|
@ -1,32 +1,85 @@
|
|||
use std::io::Write;
|
||||
|
||||
use rcc::TerminalSize;
|
||||
|
||||
use crate::{entity::file::Page, repository::file_repo::FileRepo};
|
||||
use crate::{entity::file::Page, repository::file_repo::FileRepoTrait};
|
||||
|
||||
pub struct FileService {
|
||||
repo: FileRepo,
|
||||
pub struct FileService<R> {
|
||||
repo: R,
|
||||
}
|
||||
|
||||
impl TerminalSize for FileService {}
|
||||
impl<R> TerminalSize for FileService<R> {}
|
||||
|
||||
impl FileService {
|
||||
pub fn new(repo: FileRepo) -> Self {
|
||||
impl<R: FileRepoTrait> FileService<R> {
|
||||
pub fn new(repo: R) -> Self {
|
||||
Self { repo }
|
||||
}
|
||||
|
||||
pub fn get_page(&mut self, page_id: &usize) -> Result<(), Box<dyn std::error::Error>> {
|
||||
pub fn get_page(&mut self, page_id: &usize, out: &mut impl Write) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let page = self.repo.find_page(page_id)?;
|
||||
|
||||
if page.is_empty() {
|
||||
println!("No page found.");
|
||||
writeln!(out, "No page found.")?;
|
||||
} else {
|
||||
page.into_iter().for_each(|page: Page| {
|
||||
println!("{}", "-".repeat(self.get_width()));
|
||||
println!("M-ID: {}", page.m_id);
|
||||
println!("FileName: {}", page.file);
|
||||
println!("Title: {}", page.title);
|
||||
writeln!(out, "{}", "-".repeat(self.get_width())).ok();
|
||||
writeln!(out, "M-ID: {}", page.m_id).ok();
|
||||
writeln!(out, "FileName: {}", page.file).ok();
|
||||
writeln!(out, "Title: {}", page.title).ok();
|
||||
});
|
||||
println!("{}", "-".repeat(self.get_width()));
|
||||
writeln!(out, "{}", "-".repeat(self.get_width()))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{entity::file::Page, repository::file_repo::FileRepoTrait};
|
||||
|
||||
struct MockFileRepo {
|
||||
pages: Vec<Page>,
|
||||
}
|
||||
|
||||
impl FileRepoTrait for MockFileRepo {
|
||||
fn find_page(&mut self, _: &usize) -> Result<Vec<Page>, mysql::Error> {
|
||||
Ok(std::mem::take(&mut self.pages))
|
||||
}
|
||||
}
|
||||
|
||||
fn run(repo: MockFileRepo) -> String {
|
||||
let mut svc = FileService::new(repo);
|
||||
let mut out = Vec::new();
|
||||
svc.get_page(&1, &mut out).unwrap();
|
||||
String::from_utf8(out).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn not_found_message() {
|
||||
let out = run(MockFileRepo { pages: vec![] });
|
||||
assert_eq!(out, "No page found.\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prints_page_fields() {
|
||||
let out = run(MockFileRepo {
|
||||
pages: vec![Page { m_id: 3, title: "Home".into(), file: "index.php".into() }],
|
||||
});
|
||||
assert!(out.contains("M-ID: 3"));
|
||||
assert!(out.contains("FileName: index.php"));
|
||||
assert!(out.contains("Title: Home"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multiple_pages_all_printed() {
|
||||
let out = run(MockFileRepo {
|
||||
pages: vec![
|
||||
Page { m_id: 1, title: "A".into(), file: "a.php".into() },
|
||||
Page { m_id: 2, title: "B".into(), file: "b.php".into() },
|
||||
],
|
||||
});
|
||||
assert!(out.contains("a.php"));
|
||||
assert!(out.contains("b.php"));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,47 +1,43 @@
|
|||
use std::io::Write;
|
||||
|
||||
use colored::Colorize;
|
||||
use rcc::TerminalSize;
|
||||
|
||||
use crate::{
|
||||
entity::{filter::Filter, filter_config::FilterConfig, filter_log::FilterLog, filter_uses::FilterUses},
|
||||
repository::filter_repo::FilterRepo,
|
||||
repository::filter_repo::FilterRepoTrait,
|
||||
};
|
||||
|
||||
pub struct FilterService {
|
||||
repo: FilterRepo,
|
||||
pub struct FilterService<R> {
|
||||
repo: R,
|
||||
}
|
||||
|
||||
impl TerminalSize for FilterService {}
|
||||
impl<R> TerminalSize for FilterService<R> {}
|
||||
|
||||
impl FilterService {
|
||||
pub fn new(repo: FilterRepo) -> Self {
|
||||
impl<R: FilterRepoTrait> FilterService<R> {
|
||||
pub fn new(repo: R) -> Self {
|
||||
Self { repo }
|
||||
}
|
||||
|
||||
pub fn get_filter_uses(&mut self, file_name: &str) -> Result<(), Box<dyn std::error::Error>> {
|
||||
pub fn get_filter_uses(&mut self, file_name: &str, out: &mut impl Write) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let uses = self.repo.find_filter_uses(file_name)?;
|
||||
|
||||
if uses.is_empty() {
|
||||
println!("No use for file/module found!");
|
||||
writeln!(out, "No use for file/module found!")?;
|
||||
} else {
|
||||
uses.into_iter().for_each(|filter_use: FilterUses| {
|
||||
let is_active = if filter_use.active == 1 {
|
||||
"true".to_string()
|
||||
} else {
|
||||
"false".to_string()
|
||||
};
|
||||
println!("{}", "-".repeat(self.get_width()));
|
||||
|
||||
println!("Module ID: {}", filter_use.filter_module_id);
|
||||
println!("ModuleNo: {}", filter_use.filter_module_no);
|
||||
println!("FilterUser: {}", filter_use.filter_user);
|
||||
println!("Description: {}", filter_use.description);
|
||||
println!("EP_NO: {}", filter_use.ep_no);
|
||||
println!("FilterId: {}", filter_use.filter_id);
|
||||
println!("FilterNo: {}", filter_use.filter_no);
|
||||
println!("Merchant: {}", filter_use.merchant);
|
||||
println!("Active: {}", is_active);
|
||||
let is_active = if filter_use.active == 1 { "true" } else { "false" };
|
||||
writeln!(out, "{}", "-".repeat(self.get_width())).ok();
|
||||
writeln!(out, "Module ID: {}", filter_use.filter_module_id).ok();
|
||||
writeln!(out, "ModuleNo: {}", filter_use.filter_module_no).ok();
|
||||
writeln!(out, "FilterUser: {}", filter_use.filter_user).ok();
|
||||
writeln!(out, "Description: {}", filter_use.description).ok();
|
||||
writeln!(out, "EP_NO: {}", filter_use.ep_no).ok();
|
||||
writeln!(out, "FilterId: {}", filter_use.filter_id).ok();
|
||||
writeln!(out, "FilterNo: {}", filter_use.filter_no).ok();
|
||||
writeln!(out, "Merchant: {}", filter_use.merchant).ok();
|
||||
writeln!(out, "Active: {}", is_active).ok();
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
|
@ -51,25 +47,26 @@ impl FilterService {
|
|||
&mut self,
|
||||
filter_id: &usize,
|
||||
all: bool,
|
||||
out: &mut impl Write,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let filters = self.repo.find_by_id(filter_id)?;
|
||||
|
||||
if filters.is_empty() {
|
||||
println!("Filter not found!");
|
||||
writeln!(out, "Filter not found!")?;
|
||||
} else {
|
||||
filters.into_iter().for_each(|filter: Filter| {
|
||||
println!("{}", "-".repeat(self.get_width()));
|
||||
println!("Module ID: {}", filter.filter_module_id);
|
||||
println!("FileName: {}", filter.file_name);
|
||||
println!("Description: {}", filter.description);
|
||||
println!("ModuleNo: {}", filter.filter_module_no);
|
||||
println!("FilterUser: {}", filter.filter_user);
|
||||
writeln!(out, "{}", "-".repeat(self.get_width())).ok();
|
||||
writeln!(out, "Module ID: {}", filter.filter_module_id).ok();
|
||||
writeln!(out, "FileName: {}", filter.file_name).ok();
|
||||
writeln!(out, "Description: {}", filter.description).ok();
|
||||
writeln!(out, "ModuleNo: {}", filter.filter_module_no).ok();
|
||||
writeln!(out, "FilterUser: {}", filter.filter_user).ok();
|
||||
});
|
||||
|
||||
if all {
|
||||
self.get_filter_configs(filter_id)?
|
||||
self.get_filter_configs(filter_id, out)?;
|
||||
} else {
|
||||
println!("{}", "-".repeat(self.get_width()));
|
||||
writeln!(out, "{}", "-".repeat(self.get_width()))?;
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -79,26 +76,28 @@ impl FilterService {
|
|||
pub fn get_filter_configs(
|
||||
&mut self,
|
||||
filter_id: &usize,
|
||||
out: &mut impl Write,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let filter_configs = self.repo.find_filter_configs(filter_id)?;
|
||||
|
||||
if filter_configs.is_empty() {
|
||||
println!("No filter configs found!");
|
||||
writeln!(out, "No filter configs found!")?;
|
||||
} else {
|
||||
println!("{}", "-".repeat(self.get_width()));
|
||||
writeln!(out, "{}", "-".repeat(self.get_width()))?;
|
||||
filter_configs
|
||||
.into_iter()
|
||||
.for_each(|filter_config: FilterConfig| {
|
||||
println!(
|
||||
writeln!(
|
||||
out,
|
||||
"{0: <25} | {1: <20} | {2: <20} | {3: <10} | {4: <10}",
|
||||
filter_config.attribute,
|
||||
filter_config.value1,
|
||||
filter_config.value2.unwrap_or("n/a".to_string()),
|
||||
filter_config.upd_ts,
|
||||
filter_config.name
|
||||
);
|
||||
).ok();
|
||||
});
|
||||
println!("{}", "-".repeat(self.get_width()));
|
||||
writeln!(out, "{}", "-".repeat(self.get_width()))?;
|
||||
};
|
||||
|
||||
Ok(())
|
||||
|
|
@ -108,45 +107,329 @@ impl FilterService {
|
|||
&mut self,
|
||||
filter_id: &usize,
|
||||
all: bool,
|
||||
out: &mut impl Write,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let filter_log = self.repo.find_filter_log(filter_id)?;
|
||||
if filter_log.is_empty() {
|
||||
println!("No filter log found!");
|
||||
writeln!(out, "No filter log found!")?;
|
||||
} else {
|
||||
println!("{}", "-".repeat(self.get_width()));
|
||||
writeln!(out, "{}", "-".repeat(self.get_width()))?;
|
||||
filter_log.into_iter().for_each(|filter_log: FilterLog| {
|
||||
println!(
|
||||
"TS: {} Error Code: {}",
|
||||
filter_log.run_ts, filter_log.error_code
|
||||
);
|
||||
writeln!(out, "TS: {} Error Code: {}", filter_log.run_ts, filter_log.error_code).ok();
|
||||
match filter_log.error_code.as_str() {
|
||||
"WARNING" => {
|
||||
println!("{}", filter_log.error_msg.yellow());
|
||||
writeln!(out, "{}", filter_log.error_msg.yellow()).ok();
|
||||
if all {
|
||||
println!("{}", filter_log.mysql_error.yellow());
|
||||
writeln!(out, "{}", filter_log.mysql_error.yellow()).ok();
|
||||
}
|
||||
}
|
||||
"ERROR" => {
|
||||
println!("{}", filter_log.error_msg.red());
|
||||
writeln!(out, "{}", filter_log.error_msg.red()).ok();
|
||||
if all {
|
||||
println!("{}", filter_log.mysql_error.red());
|
||||
writeln!(out, "{}", filter_log.mysql_error.red()).ok();
|
||||
}
|
||||
}
|
||||
"DEBUG" => {
|
||||
println!("{}", filter_log.error_msg.green());
|
||||
writeln!(out, "{}", filter_log.error_msg.green()).ok();
|
||||
if all {
|
||||
println!("{}", filter_log.error_msg.green())
|
||||
writeln!(out, "{}", filter_log.error_msg.green()).ok();
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
println!("{}", filter_log.error_msg);
|
||||
writeln!(out, "{}", filter_log.error_msg).ok();
|
||||
}
|
||||
}
|
||||
|
||||
println!("{}", "-".repeat(self.get_width()));
|
||||
writeln!(out, "{}", "-".repeat(self.get_width())).ok();
|
||||
})
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{
|
||||
entity::{
|
||||
filter::Filter, filter_config::FilterConfig, filter_log::FilterLog,
|
||||
filter_uses::FilterUses,
|
||||
},
|
||||
repository::filter_repo::FilterRepoTrait,
|
||||
};
|
||||
use time::{Date, Month, PrimitiveDateTime, Time};
|
||||
|
||||
fn test_dt() -> PrimitiveDateTime {
|
||||
PrimitiveDateTime::new(
|
||||
Date::from_calendar_date(2024, Month::January, 1).unwrap(),
|
||||
Time::MIDNIGHT,
|
||||
)
|
||||
}
|
||||
|
||||
struct MockFilterRepo {
|
||||
filters: Vec<Filter>,
|
||||
configs: Vec<FilterConfig>,
|
||||
logs: Vec<FilterLog>,
|
||||
uses: Vec<FilterUses>,
|
||||
}
|
||||
|
||||
impl MockFilterRepo {
|
||||
fn empty() -> Self {
|
||||
Self { filters: vec![], configs: vec![], logs: vec![], uses: vec![] }
|
||||
}
|
||||
}
|
||||
|
||||
impl FilterRepoTrait for MockFilterRepo {
|
||||
fn find_by_id(&mut self, _: &usize) -> Result<Vec<Filter>, mysql::Error> {
|
||||
Ok(std::mem::take(&mut self.filters))
|
||||
}
|
||||
fn find_filter_configs(&mut self, _: &usize) -> Result<Vec<FilterConfig>, mysql::Error> {
|
||||
Ok(std::mem::take(&mut self.configs))
|
||||
}
|
||||
fn find_filter_log(&mut self, _: &usize) -> Result<Vec<FilterLog>, mysql::Error> {
|
||||
Ok(std::mem::take(&mut self.logs))
|
||||
}
|
||||
fn find_filter_uses(&mut self, _: &str) -> Result<Vec<FilterUses>, mysql::Error> {
|
||||
Ok(std::mem::take(&mut self.uses))
|
||||
}
|
||||
}
|
||||
|
||||
fn capture(f: impl FnOnce(&mut FilterService<MockFilterRepo>, &mut Vec<u8>)) -> String {
|
||||
colored::control::set_override(false);
|
||||
let repo = MockFilterRepo::empty();
|
||||
let mut svc = FilterService::new(repo);
|
||||
let mut out = Vec::new();
|
||||
f(&mut svc, &mut out);
|
||||
String::from_utf8(out).unwrap()
|
||||
}
|
||||
|
||||
fn capture_with(
|
||||
repo: MockFilterRepo,
|
||||
f: impl FnOnce(&mut FilterService<MockFilterRepo>, &mut Vec<u8>),
|
||||
) -> String {
|
||||
colored::control::set_override(false);
|
||||
let mut svc = FilterService::new(repo);
|
||||
let mut out = Vec::new();
|
||||
f(&mut svc, &mut out);
|
||||
String::from_utf8(out).unwrap()
|
||||
}
|
||||
|
||||
// --- get_filter ---
|
||||
|
||||
#[test]
|
||||
fn get_filter_not_found() {
|
||||
let out = capture(|svc, out| { svc.get_filter(&1, false, out).unwrap(); });
|
||||
assert_eq!(out, "Filter not found!\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_filter_prints_fields() {
|
||||
let repo = MockFilterRepo {
|
||||
filters: vec![Filter {
|
||||
filter_module_id: 42,
|
||||
file_name: "foo.php".into(),
|
||||
description: "My filter".into(),
|
||||
filter_module_no: "FM-7".into(),
|
||||
filter_user: 99,
|
||||
}],
|
||||
..MockFilterRepo::empty()
|
||||
};
|
||||
let out = capture_with(repo, |svc, out| { svc.get_filter(&1, false, out).unwrap(); });
|
||||
assert!(out.contains("Module ID: 42"));
|
||||
assert!(out.contains("FileName: foo.php"));
|
||||
assert!(out.contains("Description: My filter"));
|
||||
assert!(out.contains("ModuleNo: FM-7"));
|
||||
assert!(out.contains("FilterUser: 99"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_filter_all_calls_configs() {
|
||||
let repo = MockFilterRepo {
|
||||
filters: vec![Filter {
|
||||
filter_module_id: 1,
|
||||
file_name: "f.php".into(),
|
||||
description: "d".into(),
|
||||
filter_module_no: "FM-1".into(),
|
||||
filter_user: 1,
|
||||
}],
|
||||
configs: vec![FilterConfig {
|
||||
attribute: "attr1".into(),
|
||||
value1: "val1".into(),
|
||||
value2: None,
|
||||
name: "user1".into(),
|
||||
upd_ts: test_dt(),
|
||||
}],
|
||||
..MockFilterRepo::empty()
|
||||
};
|
||||
let out = capture_with(repo, |svc, out| { svc.get_filter(&1, true, out).unwrap(); });
|
||||
assert!(out.contains("attr1"));
|
||||
assert!(out.contains("val1"));
|
||||
}
|
||||
|
||||
// --- get_filter_configs ---
|
||||
|
||||
#[test]
|
||||
fn get_filter_configs_empty() {
|
||||
let out = capture(|svc, out| { svc.get_filter_configs(&1, out).unwrap(); });
|
||||
assert_eq!(out, "No filter configs found!\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_filter_configs_prints_fields() {
|
||||
let repo = MockFilterRepo {
|
||||
configs: vec![FilterConfig {
|
||||
attribute: "colour".into(),
|
||||
value1: "blue".into(),
|
||||
value2: Some("dark".into()),
|
||||
name: "admin".into(),
|
||||
upd_ts: test_dt(),
|
||||
}],
|
||||
..MockFilterRepo::empty()
|
||||
};
|
||||
let out = capture_with(repo, |svc, out| { svc.get_filter_configs(&1, out).unwrap(); });
|
||||
assert!(out.contains("colour"));
|
||||
assert!(out.contains("blue"));
|
||||
assert!(out.contains("dark"));
|
||||
assert!(out.contains("admin"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_filter_configs_none_value2_shows_na() {
|
||||
let repo = MockFilterRepo {
|
||||
configs: vec![FilterConfig {
|
||||
attribute: "x".into(),
|
||||
value1: "y".into(),
|
||||
value2: None,
|
||||
name: "n".into(),
|
||||
upd_ts: test_dt(),
|
||||
}],
|
||||
..MockFilterRepo::empty()
|
||||
};
|
||||
let out = capture_with(repo, |svc, out| { svc.get_filter_configs(&1, out).unwrap(); });
|
||||
assert!(out.contains("n/a"));
|
||||
}
|
||||
|
||||
// --- get_filter_log ---
|
||||
|
||||
#[test]
|
||||
fn get_filter_log_empty() {
|
||||
let out = capture(|svc, out| { svc.get_filter_log(&1, false, out).unwrap(); });
|
||||
assert_eq!(out, "No filter log found!\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_filter_log_shows_error_code_and_msg() {
|
||||
let repo = MockFilterRepo {
|
||||
logs: vec![FilterLog {
|
||||
run_ts: test_dt(),
|
||||
error_code: "ERROR".into(),
|
||||
error_msg: "something broke".into(),
|
||||
mysql_error: "sql err".into(),
|
||||
}],
|
||||
..MockFilterRepo::empty()
|
||||
};
|
||||
let out = capture_with(repo, |svc, out| { svc.get_filter_log(&1, false, out).unwrap(); });
|
||||
assert!(out.contains("Error Code: ERROR"));
|
||||
assert!(out.contains("something broke"));
|
||||
assert!(!out.contains("sql err"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_filter_log_all_shows_mysql_error() {
|
||||
let repo = MockFilterRepo {
|
||||
logs: vec![FilterLog {
|
||||
run_ts: test_dt(),
|
||||
error_code: "WARNING".into(),
|
||||
error_msg: "warn msg".into(),
|
||||
mysql_error: "mysql detail".into(),
|
||||
}],
|
||||
..MockFilterRepo::empty()
|
||||
};
|
||||
let out = capture_with(repo, |svc, out| { svc.get_filter_log(&1, true, out).unwrap(); });
|
||||
assert!(out.contains("warn msg"));
|
||||
assert!(out.contains("mysql detail"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_filter_log_debug_code() {
|
||||
let repo = MockFilterRepo {
|
||||
logs: vec![FilterLog {
|
||||
run_ts: test_dt(),
|
||||
error_code: "DEBUG".into(),
|
||||
error_msg: "debug info".into(),
|
||||
mysql_error: "".into(),
|
||||
}],
|
||||
..MockFilterRepo::empty()
|
||||
};
|
||||
let out = capture_with(repo, |svc, out| { svc.get_filter_log(&1, false, out).unwrap(); });
|
||||
assert!(out.contains("debug info"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_filter_log_unknown_code_printed_plain() {
|
||||
let repo = MockFilterRepo {
|
||||
logs: vec![FilterLog {
|
||||
run_ts: test_dt(),
|
||||
error_code: "INFO".into(),
|
||||
error_msg: "plain message".into(),
|
||||
mysql_error: "".into(),
|
||||
}],
|
||||
..MockFilterRepo::empty()
|
||||
};
|
||||
let out = capture_with(repo, |svc, out| { svc.get_filter_log(&1, false, out).unwrap(); });
|
||||
assert!(out.contains("plain message"));
|
||||
}
|
||||
|
||||
// --- get_filter_uses ---
|
||||
|
||||
#[test]
|
||||
fn get_filter_uses_empty() {
|
||||
let out = capture(|svc, out| { svc.get_filter_uses("foo.php", out).unwrap(); });
|
||||
assert_eq!(out, "No use for file/module found!\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_filter_uses_prints_fields() {
|
||||
let repo = MockFilterRepo {
|
||||
uses: vec![FilterUses {
|
||||
filter_module_id: 7,
|
||||
description: "desc".into(),
|
||||
filter_module_no: "FM-2".into(),
|
||||
filter_user: 3,
|
||||
filter_id: 10,
|
||||
active: 1,
|
||||
merchant: "Shop A".into(),
|
||||
ep_id: 5,
|
||||
ep_no: "EP-9".into(),
|
||||
filter_no: "F-11".into(),
|
||||
}],
|
||||
..MockFilterRepo::empty()
|
||||
};
|
||||
let out = capture_with(repo, |svc, out| { svc.get_filter_uses("foo.php", out).unwrap(); });
|
||||
assert!(out.contains("Module ID: 7"));
|
||||
assert!(out.contains("Merchant: Shop A"));
|
||||
assert!(out.contains("Active: true"));
|
||||
assert!(out.contains("EP_NO: EP-9"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_filter_uses_inactive_shows_false() {
|
||||
let repo = MockFilterRepo {
|
||||
uses: vec![FilterUses {
|
||||
filter_module_id: 1,
|
||||
description: "d".into(),
|
||||
filter_module_no: "FM".into(),
|
||||
filter_user: 1,
|
||||
filter_id: 1,
|
||||
active: 0,
|
||||
merchant: "M".into(),
|
||||
ep_id: 1,
|
||||
ep_no: "E".into(),
|
||||
filter_no: "F".into(),
|
||||
}],
|
||||
..MockFilterRepo::empty()
|
||||
};
|
||||
let out = capture_with(repo, |svc, out| { svc.get_filter_uses("x", out).unwrap(); });
|
||||
assert!(out.contains("Active: false"));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,52 +1,135 @@
|
|||
use std::io::Write;
|
||||
|
||||
use rcc::TerminalSize;
|
||||
|
||||
use crate::{
|
||||
entity::job::{Job, JobLog},
|
||||
repository::job_repo::JobRepo,
|
||||
repository::job_repo::JobRepoTrait,
|
||||
};
|
||||
|
||||
pub struct JobService {
|
||||
pub repo: JobRepo,
|
||||
pub struct JobService<R> {
|
||||
pub repo: R,
|
||||
}
|
||||
|
||||
impl TerminalSize for JobService {}
|
||||
impl<R> TerminalSize for JobService<R> {}
|
||||
|
||||
impl JobService {
|
||||
pub fn new(repo: JobRepo) -> Self {
|
||||
impl<R: JobRepoTrait> JobService<R> {
|
||||
pub fn new(repo: R) -> Self {
|
||||
Self { repo }
|
||||
}
|
||||
|
||||
pub fn get_job_by_id(&mut self, job_id: &usize) -> Result<(), Box<dyn std::error::Error>> {
|
||||
pub fn get_job_by_id(&mut self, job_id: &usize, out: &mut impl Write) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let result = self.repo.find_by_id(job_id)?;
|
||||
|
||||
if result.is_empty() {
|
||||
println!("Job not found!");
|
||||
writeln!(out, "Job not found!")?;
|
||||
} else {
|
||||
result.into_iter().for_each(|job: Job| {
|
||||
println!("{}", "-".repeat(self.get_width()));
|
||||
println!("FileName: {}", job.file_name);
|
||||
println!("Description: {}", job.description);
|
||||
writeln!(out, "{}", "-".repeat(self.get_width())).ok();
|
||||
writeln!(out, "FileName: {}", job.file_name).ok();
|
||||
writeln!(out, "Description: {}", job.description).ok();
|
||||
});
|
||||
|
||||
println!("{}", "-".repeat(self.get_width()));
|
||||
writeln!(out, "{}", "-".repeat(self.get_width()))?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_job_log(&mut self, job_id: &usize) -> Result<(), Box<dyn std::error::Error>> {
|
||||
pub fn get_job_log(&mut self, job_id: &usize, out: &mut impl Write) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let result = self.repo.find_log(job_id)?;
|
||||
|
||||
if result.is_empty() {
|
||||
println!("Job log not found!");
|
||||
writeln!(out, "Job log not found!")?;
|
||||
} else {
|
||||
result.into_iter().for_each(|joblog: JobLog| {
|
||||
println!("{0: <25} | {1: <20} ", joblog.upd_ts, joblog.content,);
|
||||
writeln!(out, "{0: <25} | {1: <20} ", joblog.upd_ts, joblog.content).ok();
|
||||
});
|
||||
|
||||
println!("{}", "-".repeat(self.get_width()));
|
||||
writeln!(out, "{}", "-".repeat(self.get_width()))?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{
|
||||
entity::job::{Job, JobLog},
|
||||
repository::job_repo::JobRepoTrait,
|
||||
};
|
||||
use time::{Date, Month, PrimitiveDateTime, Time};
|
||||
|
||||
fn test_dt() -> PrimitiveDateTime {
|
||||
PrimitiveDateTime::new(
|
||||
Date::from_calendar_date(2024, Month::June, 15).unwrap(),
|
||||
Time::MIDNIGHT,
|
||||
)
|
||||
}
|
||||
|
||||
struct MockJobRepo {
|
||||
jobs: Vec<Job>,
|
||||
logs: Vec<JobLog>,
|
||||
}
|
||||
|
||||
impl MockJobRepo {
|
||||
fn empty() -> Self {
|
||||
Self { jobs: vec![], logs: vec![] }
|
||||
}
|
||||
}
|
||||
|
||||
impl JobRepoTrait for MockJobRepo {
|
||||
fn find_by_id(&mut self, _: &usize) -> Result<Vec<Job>, mysql::Error> {
|
||||
Ok(std::mem::take(&mut self.jobs))
|
||||
}
|
||||
fn find_log(&mut self, _: &usize) -> Result<Vec<JobLog>, mysql::Error> {
|
||||
Ok(std::mem::take(&mut self.logs))
|
||||
}
|
||||
}
|
||||
|
||||
fn run_job(repo: MockJobRepo) -> String {
|
||||
let mut svc = JobService::new(repo);
|
||||
let mut out = Vec::new();
|
||||
svc.get_job_by_id(&1, &mut out).unwrap();
|
||||
String::from_utf8(out).unwrap()
|
||||
}
|
||||
|
||||
fn run_log(repo: MockJobRepo) -> String {
|
||||
let mut svc = JobService::new(repo);
|
||||
let mut out = Vec::new();
|
||||
svc.get_job_log(&1, &mut out).unwrap();
|
||||
String::from_utf8(out).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn job_not_found() {
|
||||
let out = run_job(MockJobRepo::empty());
|
||||
assert_eq!(out, "Job not found!\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn job_prints_fields() {
|
||||
let out = run_job(MockJobRepo {
|
||||
jobs: vec![Job { file_name: "import.php".into(), description: "Imports stuff".into() }],
|
||||
..MockJobRepo::empty()
|
||||
});
|
||||
assert!(out.contains("FileName: import.php"));
|
||||
assert!(out.contains("Description: Imports stuff"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn job_log_not_found() {
|
||||
let out = run_log(MockJobRepo::empty());
|
||||
assert_eq!(out, "Job log not found!\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn job_log_prints_content_and_timestamp() {
|
||||
let out = run_log(MockJobRepo {
|
||||
logs: vec![JobLog { content: "log line".into(), upd_ts: test_dt() }],
|
||||
..MockJobRepo::empty()
|
||||
});
|
||||
assert!(out.contains("log line"));
|
||||
assert!(out.contains("2024-06-15"));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,32 +1,80 @@
|
|||
use std::io::Write;
|
||||
|
||||
use rcc::TerminalSize;
|
||||
|
||||
use crate::repository::merchant_repo::MerchantRepo;
|
||||
use crate::{entity::merchant::Merchant, repository::merchant_repo::MerchantRepoTrait};
|
||||
|
||||
pub struct MerchantService {
|
||||
repo: MerchantRepo,
|
||||
pub struct MerchantService<R> {
|
||||
repo: R,
|
||||
}
|
||||
|
||||
impl TerminalSize for MerchantService {}
|
||||
impl<R> TerminalSize for MerchantService<R> {}
|
||||
|
||||
impl MerchantService {
|
||||
pub fn new(repo: MerchantRepo) -> Self {
|
||||
impl<R: MerchantRepoTrait> MerchantService<R> {
|
||||
pub fn new(repo: R) -> Self {
|
||||
Self { repo }
|
||||
}
|
||||
|
||||
pub fn get_merchant(&mut self, search: &str) -> Result<(), Box<dyn std::error::Error>> {
|
||||
pub fn get_merchant(&mut self, search: &str, out: &mut impl Write) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let merchants = self.repo.find_by_name_or_id(search)?;
|
||||
|
||||
if merchants.is_empty() {
|
||||
println!("Merchant not found!");
|
||||
writeln!(out, "Merchant not found!")?;
|
||||
}
|
||||
merchants.into_iter().for_each(|merchant| {
|
||||
println!("{}", "-".repeat(self.get_width()));
|
||||
println!("Merchant: {}", merchant.m_name);
|
||||
println!("M-ID: {}", merchant.m_id);
|
||||
merchants.into_iter().for_each(|merchant: Merchant| {
|
||||
writeln!(out, "{}", "-".repeat(self.get_width())).ok();
|
||||
writeln!(out, "Merchant: {}", merchant.m_name).ok();
|
||||
writeln!(out, "M-ID: {}", merchant.m_id).ok();
|
||||
});
|
||||
|
||||
println!("{}", "-".repeat(self.get_width()));
|
||||
writeln!(out, "{}", "-".repeat(self.get_width()))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{entity::merchant::Merchant, repository::merchant_repo::MerchantRepoTrait};
|
||||
|
||||
struct MockMerchantRepo {
|
||||
merchants: Vec<Merchant>,
|
||||
}
|
||||
|
||||
impl MerchantRepoTrait for MockMerchantRepo {
|
||||
fn find_by_name_or_id(&mut self, _: &str) -> Result<Vec<Merchant>, mysql::Error> {
|
||||
Ok(std::mem::take(&mut self.merchants))
|
||||
}
|
||||
}
|
||||
|
||||
fn run(repo: MockMerchantRepo) -> String {
|
||||
let mut svc = MerchantService::new(repo);
|
||||
let mut out = Vec::new();
|
||||
svc.get_merchant("any", &mut out).unwrap();
|
||||
String::from_utf8(out).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn not_found_message() {
|
||||
let out = run(MockMerchantRepo { merchants: vec![] });
|
||||
assert!(out.contains("Merchant not found!"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prints_merchant_fields() {
|
||||
let out = run(MockMerchantRepo {
|
||||
merchants: vec![Merchant { m_id: 5, m_name: "Acme".into() }],
|
||||
});
|
||||
assert!(out.contains("Merchant: Acme"));
|
||||
assert!(out.contains("M-ID: 5"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prints_separator_after_results() {
|
||||
let out = run(MockMerchantRepo {
|
||||
merchants: vec![Merchant { m_id: 1, m_name: "X".into() }],
|
||||
});
|
||||
assert!(out.contains('-'));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,32 +1,90 @@
|
|||
use std::io::Write;
|
||||
|
||||
use rcc::TerminalSize;
|
||||
|
||||
use crate::repository::schema_repo::SchemaRepo;
|
||||
use crate::{entity::schema::Schema, repository::schema_repo::SchemaRepoTrait};
|
||||
|
||||
pub struct SchemaService {
|
||||
repo: SchemaRepo,
|
||||
pub struct SchemaService<R> {
|
||||
repo: R,
|
||||
}
|
||||
|
||||
impl TerminalSize for SchemaService {}
|
||||
impl<R> TerminalSize for SchemaService<R> {}
|
||||
|
||||
impl SchemaService {
|
||||
pub fn new(repo: SchemaRepo) -> Self {
|
||||
impl<R: SchemaRepoTrait> SchemaService<R> {
|
||||
pub fn new(repo: R) -> Self {
|
||||
Self { repo }
|
||||
}
|
||||
pub fn get_columns(&mut self, column: &str) -> Result<(), Box<dyn std::error::Error>> {
|
||||
|
||||
pub fn get_columns(&mut self, column: &str, out: &mut impl Write) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let columns = self.repo.find_by_column(column)?;
|
||||
|
||||
if columns.is_empty() {
|
||||
println!("No column found!");
|
||||
writeln!(out, "No column found!")?;
|
||||
} else {
|
||||
columns.into_iter().for_each(|column| {
|
||||
println!("{}", "-".repeat(self.get_width()));
|
||||
println!("Table name: {}", column.table_name);
|
||||
println!("Column name: {}", column.column_name);
|
||||
println!("Type: {}", column.column_type);
|
||||
columns.into_iter().for_each(|column: Schema| {
|
||||
writeln!(out, "{}", "-".repeat(self.get_width())).ok();
|
||||
writeln!(out, "Table name: {}", column.table_name).ok();
|
||||
writeln!(out, "Column name: {}", column.column_name).ok();
|
||||
writeln!(out, "Type: {}", column.column_type).ok();
|
||||
});
|
||||
println!("{}", "-".repeat(self.get_width()));
|
||||
writeln!(out, "{}", "-".repeat(self.get_width()))?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{entity::schema::Schema, repository::schema_repo::SchemaRepoTrait};
|
||||
|
||||
struct MockSchemaRepo {
|
||||
columns: Vec<Schema>,
|
||||
}
|
||||
|
||||
impl SchemaRepoTrait for MockSchemaRepo {
|
||||
fn find_by_column(&mut self, _: &str) -> Result<Vec<Schema>, mysql::Error> {
|
||||
Ok(std::mem::take(&mut self.columns))
|
||||
}
|
||||
}
|
||||
|
||||
fn run(repo: MockSchemaRepo) -> String {
|
||||
let mut svc = SchemaService::new(repo);
|
||||
let mut out = Vec::new();
|
||||
svc.get_columns("any", &mut out).unwrap();
|
||||
String::from_utf8(out).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn not_found_message() {
|
||||
let out = run(MockSchemaRepo { columns: vec![] });
|
||||
assert_eq!(out, "No column found!\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prints_schema_fields() {
|
||||
let out = run(MockSchemaRepo {
|
||||
columns: vec![Schema {
|
||||
table_name: "orders".into(),
|
||||
column_name: "status".into(),
|
||||
column_type: "varchar(32)".into(),
|
||||
}],
|
||||
});
|
||||
assert!(out.contains("Table name: orders"));
|
||||
assert!(out.contains("Column name: status"));
|
||||
assert!(out.contains("Type: varchar(32)"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multiple_columns_all_printed() {
|
||||
let out = run(MockSchemaRepo {
|
||||
columns: vec![
|
||||
Schema { table_name: "t1".into(), column_name: "c1".into(), column_type: "int".into() },
|
||||
Schema { table_name: "t2".into(), column_name: "c2".into(), column_type: "text".into() },
|
||||
],
|
||||
});
|
||||
assert!(out.contains("t1"));
|
||||
assert!(out.contains("t2"));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue